Shell completion¶
Every bob is answered live by the bob on your PATH,
so completion always matches the installed binary: commands, options,
and vault values such as capture routes, tasks, and open Pomodoros.
Say "shell completion" (never just "completion") so it is never
confused with capture-complete.
Overview¶
The bob binary is the grammar. Each bob __complete request against one composed clap tree plus bob's own
read-only, vault-aware value providers. The file installed into the
shell is a small, stable, protocol-versioned adapter that only renders
what bob returns. "Updating completion" means ensuring that adapter is
present and current, not regenerating anything.
zsh/bash ββTABβββΆ adapter (_bob / bob, written by `bob completion install`)
β command bob __complete zsh --protocol 1 --suffix "$SUFFIX" -- <wordsβ¦> "$PREFIX"
βΌ
bob __complete ββΆ early intercept in run_bob (nothing else runs)
ββΆ completion::tree() one exhaustive clap tree
ββΆ engine.rs β clap_complete subcommands, options, static choices
ββΆ kinds + providers routes, tasks, Pomodoros, capture markers β¦
ββΆ present.rs presentation rules, human groups, directives
ββΆ protocol.rs value\tdesc\tgroup\tsuffix | !prefix !dirs !files !message
Completion never writes: no vault writes, git, network, clipboard,
locks, script materialization, or state/log files. The only exception
is the explicitly requested BOB_COMPLETE_DEBUG=<file>.
Protocol 1¶
Request:
bob __complete <SHELL> --protocol <N> [--suffix <TEXT>] -- <WORD>...
SHELLiszshorbash. It changes only presentation.WORDs are the command line's words up to the cursor, shell-unquoted. The first word is the command as typed and is ignored. The last word is the cursor word's text before the cursor, and may be empty.--suffixis the unquoted text of the cursor word after the cursor. It is absent or empty when the cursor sits at the end of the word.- A malformed request exits 2 with a message on stderr; adapters discard stderr.
Response. UTF-8 lines on stdout. Exit 0, including on internal failure, which yields empty output.
- Directives are whole lines, emitted before any candidate:
!prefix <N>: keep the first N Unicode scalar values of the cursor-word prefix, and let candidates replace only the rest.!dirs: complete directories natively.!files/!files <glob>: complete files natively, optionally filtered.!files-in <root>\t<glob>: complete paths relative toroot(vault notes). The separator is a TAB, so roots may contain spaces.!message <text>: show a hint and offer no candidates (free-text slots, version skew).- Candidate lines:
value<TAB>description<TAB>group<TAB>space|nospace. valueis never empty and never contains TAB, LF, or CR; bob drops such candidates.descriptionmay be empty, and LF/CR/TAB become spaces.- An empty
groupmeansvalues. nospacemeans the user is expected to keep typing.- Order is display order. Groups display in order of first
appearance, and adapters never re-sort. Root commands use
daily workflow,tasks and projects,vault,integrations,setup, andcapture protocol, in that order. Members stay undercommands. - Version skew. The binary supports protocol 1 only.
- An older adapter gets
!message bob shell completion is out of date β run: bob completion install. - A newer adapter gets
!message this bob is older than its shell completion β reinstall bob (just install). - Debugging.
BOB_COMPLETE_DEBUG=<file>appends the request, the response, the elapsed milliseconds, and any error. - The shell filters, not bob. Bob never prefix-filters within a
slot: it resolves the slot from the cursor word and returns that
slot's full candidate set. Attached
--opt=values are served with!prefix.
What completes¶
- Commands. Root commands use the same six workflow sections as
bob -h, in help order. Members under any command stay incommands. Hidden aliases, hiddenfreshness seed, and thehelproute never appear. - Options. A lone
-offers short and long forms adjacent with identical descriptions;--offers long forms only. Options already present (unless repeatable), options conflicting with present ones, and-h/--helponce other arguments exist are dropped. Once capture text has started, or after--, no options are offered. - Static choices. Every option with clap possible values
(
--format,--engine, ref--status/--prefer, gkeep--source, β¦) completes its values under a group named after the slot, such asformat. - Paths. Directory options (
--bob-dir,--repo,--backup-dir,--lib-dir,--ref-dir,--xlib-dir,query --vault) answer!dirs. File options (--query-file,--tasks-file, ref markdown inputs, PDF arguments, clip--html) answer!fileswith a glob where one applies. A slot the builder annotates with a clapValueHint(for examplecompletion install -t,completion zsh -o) answers the same way with no table entry; a path-specific table entry (the ref PDF--output) beats the hint. - Free text. Everything else answers a
!messagehint naming the slot, for exampleMESSAGE β Override the generated Git commit message. Stale-safe refs (--task-ref) and the bare note name ofref --parentstay free text: they name one thing, not a set. - Capture markers.
TEXToncapture,capture-parse, andcapture-rewritecompletes the marker at the end of the active word through the in-processcapture_completeextraction (see below). Wikilinks ([[β¦) are deferred and offer nothing. - Saved templates.
capture-templatesis a nested capture-protocol command group; itsclear,delete,list,save, andshowmembers complete under thecommandsgroup. TEMPLATE selectors andsaveTEXT are free text; no vault scan or capture-marker completion runs for them. - Vault values. These slots read the live vault through the same
in-process scans as the commands they complete, so short clusters
(
-rcash),--route=cash, andBOB_DIRdefaults behave exactly as at runtime. A slot whose prerequisite is missing answers!message pass --route first(orpass --task first) instead of guessing. Vault scans run behind a 150 ms deadline so a slow vault can never delay the prompt; on timeout bob answers nothing. --route(oncapture,capture-sections,capture-tasks,capture-task-id,capture-task-sections): routes in capture-targets scan order, groupedinbox/areas/projects.capture --section: non-Tasks sections of the routed note, groupedsections in <route>.capture --taskandcapture-task-sections --block-id: open tasks of the routed note by block ID, groupedtasks in <route>.capture --task-section: ALL-CAPS child sections of the--taskparent by exact title, groupedtask sections.capture-pomodoro-name --pomodoro-ref: open Pomodoros by stale-safe ref, groupedopen Pomodoros.plugins sync --plugin: directory names under<--repo or BOB_PLUGINS_DIR>/plugins/*/, groupedplugins. Never git.task reroll --level: configured priority labels in config order; the description is the roll window (for example2β7 days).query --tasks-note,query --origin,ready NOTE,capture-task-id --note-path: vault notes as!files-in <bob-dir>\t*.md.
Live transcripts (fixture vault)¶
The menus below are real bob __complete transcripts captured in a
sandboxed session against the completion test fixture (one area cash,
one project dev, one open task ^ship-it in dev), so no private
vault content lands in docs. Each candidate line is
value<TAB>description<TAB>group<TAB>space|nospace.
$ bob __complete zsh --protocol 1 -- bob ""
capture<TAB>Capture tasks, bullets, and Pomodoro commands into the vault<TAB>daily workflow<TAB>space
freshness<TAB>Walk the tiered freshness review queue<TAB>daily workflow<TAB>space
plan<TAB>Show today's plan budget, Today's tasks, and NEXT/PENDING lanes<TAB>daily workflow<TAB>space
β¦ (remaining daily workflow commands)
task<TAB>Vault-wide task maintenance: reconcile, reroll, archive<TAB>tasks and projects<TAB>space
β¦ (tasks and projects, vault, integrations, and setup sections)
capture-complete<TAB>Complete the capture marker at the cursor<TAB>capture protocol<TAB>space
β¦ (all eleven capture-* endpoints, grouped last)
$ bob __complete zsh --protocol 1 -- bob capture-sections --route ""
mac_inbox<TAB>inbox Β· default capture target<TAB>inbox<TAB>space
cash<TAB>area<TAB>areas<TAB>space
dev<TAB>project Β· active<TAB>projects<TAB>space
$ bob __complete zsh --protocol 1 -- bob capture "@dev:"
@dev:ship-it<TAB>Ship the release<TAB>tasks in dev<TAB>space
$ bob __complete zsh --protocol 1 -- bob capture fix it "@dev:"
!prefix 7
@dev:fix<TAB>new block ID<TAB>new task ID<TAB>space
$ bob __complete zsh --protocol 1 -- bob capture --format ""
human<TAB>Colored text for people<TAB>format<TAB>space
json<TAB>Machine-readable JSON<TAB>format<TAB>space
$ bob __complete zsh --protocol 1 -- bob completion ""
bash<TAB>Print the bash completion adapter<TAB>commands<TAB>space
install<TAB>Install or refresh the completion adapter for your shells<TAB>commands<TAB>space
status<TAB>Show installed adapters and the bob they call<TAB>commands<TAB>space
uninstall<TAB>Remove completion adapters that bob installed<TAB>commands<TAB>space
zsh<TAB>Print the zsh completion adapter<TAB>commands<TAB>space
Performance¶
Measured 2026-10-02 on apollo (debug build) against the real vault
(BOB_DIR=~/bob), 20 samples after one warmup. __complete never
writes, so measuring on the real vault is safe.
| Slot | p50 | p95 |
|---|---|---|
bob <TAB> (structural) |
10.9 ms | 13.8 ms |
capture-sections --route <TAB> (vault) |
27.6 ms | 33.0 ms |
capture --route dev --task <TAB> (vault) |
13.5 ms | 16.3 ms |
capture fix it @dev:<TAB> (capture text) |
14.5 ms | 17.4 ms |
capture-pomodoro-name --pomodoro-ref <TAB> (vault) |
11.5 ms | 15.9 ms |
Structural p95 is under 20 ms and vault p95 is under 75 ms, so no
fix was needed. If a slot regresses past those budgets, the latency
checks in tests/cli/completion/ warn first; a real regression gets a
task bead.
Installing¶
just install installs bob from this checkout and refreshes shell
completion in one step. From the Git remote instead:
cargo install --git git@github.com:bobs-org/bob-cli.git --locked bob-obsidian && bob completion install
bob completion install targets your shells directly:
bob completion install # $SHELL, plus every bob-owned adapter
bob completion install zsh -d # show the plan without writing anything
With no SHELL arguments bob installs for $SHELL plus every bob-owned
adapter; with -t/--target and no SHELL arguments it installs only
$SHELL's shell, so a second owned adapter never turns a single-target
install into a usage error.
For zsh the target directory is the first match: --target DIR, the
previous install location from the manifest, the first writable fpath
entry under $HOME, oh-my-zsh completions, then ~/.zfunc (created as
needed). Installs that land on the ~/.zfunc home default always print
the exact fpath=(~/.zfunc $fpath) line to add before compinit β
including -n and dry runs, which never probe. For bash it is
${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-~/.local/share}/bash-completion}/completions/bob.
Files bob did not write are refused without --force, and every write is
atomic. Moving an owned install with -t removes the previous adapter
when it still matches the manifest (plus any .zwc next to it) and says
so; an edited previous file is left behind with the exact rm command.
Verification probes a real shell unless --no-verify is given;
--quiet prints only warnings and errors. Combining --target with more
than one shell is a usage error.
Commands¶
bob completion(same asstatus, plus the hiddenlistalias): show installed adapters and thebobthey call.bob completion install [SHELL]...: install or refresh adapters.-d/--dry-run,-f/--force,-n/--no-verify,-q/--quiet,-t/--target DIR.bob completion status:-j/--jsonfor machine output,-v/--verifyto probe a real shell now.bob completion uninstall [SHELL]...: remove only files whose stamp and manifest digest prove bob wrote them, plus any stale_bob.zwc. An edited file is refused with the exactrmcommand instead.bob completion bash [-o FILE]: print the bash adapter, or write it to FILE yourself (unrecorded: status reports it as externally managed).bob completion zsh [-o FILE]: print the zsh adapter, or write it to FILE yourself (unrecorded: status reports it as externally managed).
Exit codes are 0 for success or an explicit no-op, 1 when an install or
uninstall failed, when a live install verification is not registered,
shadowed by β¦, or bob is bound to β¦, or when status -v found a
broken install (an unverified probe is a warning and stays 0), and 2
for usage errors. A dry run never prints a closer; Completion is live
prints only when every row is unchanged and registered.
Status states¶
not installed: nothing there and nothing recorded. RendersΒ· bash not installed β bob completion install bash, never probes in any mode, and never fails the exit code.current: bob-owned, and the bytes equal what this binary writes.outdated: bob-owned, but the bytes differ. Hint:bob completion install.edited: the stamp is present, but the digest differs from the manifest. Hint: reinstall with-f.foreign: no bob stamp. Hint: reinstall with-f.current (externally managed): an unrecorded file whose bytes equal this adapter. Bob reports it and never adopts it.outdated (externally managed): an unrecorded stamped file whose bytes differ. Refused without-f, like a foreign file, and never adopted.missing: the manifest records an install, but the file is gone. Stays a failure.
Without -v, status never spawns a shell: the target is the manifest
record or the probe-free default. A recorded unhealthy verification
renders β , never β, and the exit code stays 0.
Troubleshooting¶
bob completion status -vprobes a real shell now and reportsregistered as _bob,not registered(zsh: anfpathline or a stale-compdumprmplusexec zsh; bash: the exactsource <path>line for~/.bashrc),shadowed by <file>,bob is bound to <fn>, orunverified (<reason>). The probe initializes compinit only when the rc did not ((( ${+_comps} )) || compinit -D), so a stale compdump from the rc stays visible and therm β¦β/.zcompdump* && exec zshremedy (rm -f "${ZDOTDIR:-$HOME}"/.zcompdump* && exec zsh) can fire. Without-v, status shows the verification recorded at install time, but only when its digest and path still match the file. With-n, install reportsregistration not checked β bob completion status -vinstead of probing.BOB_COMPLETE_DEBUG=<file>appends each request, response, elapsed milliseconds, and any error or timeout.- A stale compinit dump looks installed but never loads: remove it with
rm -f "${ZDOTDIR:-$HOME}"/.zcompdump*and restart the shell. - PATH shadowing:
<TAB>asks thebobonPATH, so when status warns that it differs from the running binary, fixPATHor reinstall. - Version skew answers: an old adapter hears
bob shell completion is out of date β run: bob completion install; a newer adapter than the binary hearsthis bob is older than its shell completion β reinstall bob (just install).
Why no rc edits and no eval¶
bob completion install writes one file and records it in the manifest
under $XDG_STATE_HOME/bob-cli/completion/. It never edits ~/.zshrc,
~/.bashrc, or any rc file: it prints the exact line to add instead, so
your shell startup stays yours. And adapters never eval generated code β every
<TAB> runs bob __complete against the binary on PATH and renders
what it returns, so completion can never drift from the CLI.
Styling¶
The zsh adapter ships bob-scoped presentation defaults, applied only where you have set no style of your own:
- Group headers render as bold green
ββ <group> ββ, matchingbob --help. - Candidates stay grouped (
group-nameis set to the empty style), so each slot keeps its own header instead of merging into one list.
Both are scoped to bob, so other commands are unaffected. To use
your own look, set the same styles yourself β yours win:
zstyle ':completion:*:*:bob:*:descriptions' format 'ββ %d ββ'
Directives go through the native _files and _message widgets,
so your list-colors, menu select, quoting, and native colors
keep working. Setting NO_COLOR switches bob's default header to
the plain ββ %d ββ form.
Bash¶
The bash adapter is values-only: descriptions and groups are ignored, so
every slot completes plain values through COMPREPLY. It registers with
complete -F _bob bob (no -o default, so free-text slots never fall
back to filenames).
- Word reassembly. Prior words and the cursor word are rebuilt from
COMP_LINE/COMP_POINT, rejoining tokens thatCOMP_WORDBREAKSsplit at:and=. Simple quoting is unquoted, and the text after the cursor is sent as--suffix. !prefix N. Each reply is the kept text plus the value, minus the cursor-word text up to and including its last wordbreak character, whatever it is (=,:, β¦) β the part readline already broke off and will replace. A word whose quote is still open at the cursor skips the stripping and the escaping: readline replaces the whole quoted text and adds the closing quote itself. Other unquoted values with spaces or shell metacharacters are%q-escaped so each stays one argument.- Directives.
!dirscompletes directories viacompgen -d;!files [glob]completes files viacompgen -ffiltered by the glob;!files-incompletes paths relative to its root, filtered by the text after the kept prefix;!messageanswers an empty reply. Unknown!directives are ignored. - Spacing.
compopt -o nospaceis set only when every candidate isnospace; file directives setcompopt -o filenames. - Installing. The target is
${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-~/.local/share}/bash-completion}/completions/bob. Verification triggers bash-completion's lazy loader (_comp_load bobin bash-completion β₯ 2.12, else__load_completion bob), then checks thatcomplete -p bobnames_bob. Without bash-completion the report isnot registeredwith the exactsource <path>line for~/.bashrc, since bob never edits rc files.
Capture markers¶
TEXT on capture, capture-parse, and capture-rewrite completes the
capture marker at the end of the active word through an in-process
capture_complete extraction, so shell completion can never disagree with
the marker highlighting bob capture-parse derives. Shell completion is
another thin client of that service (see docs/capture.md
bob capture-complete).
- Slots. The cursor word is
TEXTwhen the trailingTEXThas already received a word, when--precedes it, or when it sits at theTEXTposition and does not start with-.raw_textjoins theTEXTwords before the cursor with single spaces plus the cursor prefix, withcursor = raw_text.len(). - Release gates. Safe rows only (no
requires_block_idorrequires_name; create-Pomodoro rows stay asnew Pomodoro); end of the active word only (a non-empty--suffixoffers nothing, the replacement must end at the cursor and start inside the cursor word, a newline in the word offers nothing); nothing beyond completion (nocapture-task-id,capture-pomodoro-name, writes, or dry runs); wikilinks ([[β¦) are deferred before the note index read and offer nothing. - Block IDs follow
capture-complete. A solo@route:links existing tasks, groupedtasks in <route>. Body-bearing text mints a new task with that ID, so shell completion offers the field's suggestions instead (for example@dev:fix, groupednew task ID) and never an IDcapture-completelists as used. - Action items offer nothing, exactly as in
capture-complete: closes (xβ¦,x*,x!), starts (=,=<X>), numeric+N/-Nadjustments and++N/--Nshifts, and Work Log text on or below thexline. The exact lone+remains an action; capture completion separately opens the parent-task picker, while shell completion offers only identified@route+block-idvalues in theparent tasksgroup. - Whole-item
!completes open tasks. A leading!token opens the Complete picker over every open task in the vault, and shell completion offers only identified, enabled!note:block-idvalues (ID-less and guarded rows stay in the interactive picker), groupedtoday/in progress/next/openwith today rows first. &completes prerequisite tasks. A leading or trailing&/&query/¬e:token opens the prerequisite picker (task_dependency), and shell completion offers identified¬e:block-idvalues, groupedprerequisite tasks.*completes blocked tasks. A leading-run or trailing*/*query/*note:token opens the blocked-task picker (blocked_task); a line-initial bare*is never this marker. Shell completion offers identified, enabled*note:block-idvalues, groupedblocked tasks. Quote*on the shell; zsh globs it.~/~~complete relation targets. A leading-run or trailing~/~~token opens the relation picker (relation_to/relation_from); a label position completes labels (relation_label). Shell completion offers identified, enabled~note:block-id/~~note:block-idvalues, groupedlinks to/linked from. Quote~on the shell; zsh globs it. See Linking tasks.- Presentation. Values are full marker texts such as
@dev:remote-power. Descriptions come from the row: task text, route kind,new block ID, or Pomodoro time and name. Groups are human words:inbox/areas/projects,sections in dev,tasks in dev,parent tasks,new task ID,task sections,open Pomodoros,active tasks,prerequisite tasks,blocked tasks,links to,linked from, andrelation labels. A value ending in:+#=^isnospace; a complete marker gets a space. A quoted word carrying its own prefix (for examplefix it @dev:) is served with!prefix 7, counted in Unicode scalar values.