17 KiB
Shell completion implementation plan
Status: Stages 1–4 complete. Stage 5 is next.
Implement the shell completion roadmap from baseline
2301872. Follow the stages in order in one sprint. Intermediate stages may
leave unfinished wiring or a noncompiling tree; validate the assembled result
before calling the sprint complete. Use the existing ownership boundaries in
ADR 0013,
ADR 0014, and the
architecture policy. Apply the
testing policy to integration assertions. Keep protocol
names, candidate ordering, and backend mechanics private.
Stage 1: Prove the shell transport
Status: complete.
- Use the Cobra version pinned in the root module. In a disposable prototype,
generate scripts with
GenBashCompletionV2(w, true),GenZshCompletion(w), andGenFishCompletion(w, true). Give the generator a minimal root whose name is the installed executable basename. Do not install Cobra's completion command or execute its completion parser. - Supply a fixture executable that handles the generated scripts' hidden
request (
__completewith these generator settings). Verify the actual arguments and replies using each script in its shell. The request carries the words through the cursor, including the partial word as its last argument; preserve an explicitly empty last argument. Check what each script does with the prefix of an attached value before deciding how to encode its replacement. Cover--config=./par, separated path values, spaces, an empty no-file reply, directory-only completion, and a partial short cluster. Verify the edited line, not only the printed candidate list. - Use Cobra's line protocol: candidate lines with optional tab-separated descriptions, followed by the colon-prefixed directive integer and newline. Use the adapter's Cobra directive constants, not hardcoded numbers in core. Map command/flag/static choices and no candidates to NoFileComp, file hints to native file completion, and directory hints to FilterDirs. If a generated script cannot express the selected behavior as emitted, make the smallest adapter-owned script adjustment that preserves it and add a shell regression. In particular, verify attached-value prefix reconstruction and Fish's filesystem handling. Do not narrow the supported contexts to fit a script.
- Record the observed argument and replacement conventions in the sprint evidence. Carry forward only the adapter and fixtures needed for the final implementation; do not commit disposable prototype machinery.
Completion: generated scripts using Cobra v1.10.1 and exercised them through
interactive Bash 5.3.9, Zsh 5.9, and Fish 4.2.1 with a fixture that records the
query and the executed edited line. In the tested attached-value cases the
query includes the entered flag and value; a static candidate contains the value
alone, and the shell script preserves the flag prefix. Explicit empty words,
quoted prior arguments, short clusters, file and directory hints, and no-file
replies were exercised. Zsh's generated word splitting lost the attached flag
context when a path contained an escaped space. Fish split quoted prior words,
passed escapes in current words, and treated directory-only replies as ordinary
file completion. The adapter adjusts those scripts and preserves the first =
as the flag/value separator. The opt-in TestCompletionShells regression checks
actual edited lines for these cases. Run it with Bash, Zsh, Fish, Python pexpect,
and Bash completion installed using COMMANDKIT_COMPLETION_SHELLS=1 GOWORK=off GOTOOLCHAIN=local go test -count=1 -run '^TestCompletionShells$' ./internal/cobra. Zsh and Fish were extracted into a disposable directory for
this run; the focused adapter suite and the full interactive matrix pass.
Stage 2: Capture completion hints
Status: complete.
-
Add the following public API in the root package. Add
Completion CompletionHintto bothFlagMetaandArgumentMeta; existing binding constructors and their signatures stay in place.type CompletionChoice struct { Value string Description string } type CompletionHint struct { /* private representation */ } func CompleteChoices(choices ...CompletionChoice) CompletionHint func CompleteFiles() CompletionHint func CompleteDirectories() CompletionHint func NoCompletion() CompletionHint -
Represent the four strategies as a private discriminator and choice list. The zero hint and
NoCompletion()are equivalent. Copy choice slices when constructing the hint. Capture the hint through ordinary bindings, repeatable bindings, andCaptureFlags; bridge it throughbridgeFlagsandbridgeArgumentsto neutral metadata. Use neutral strategy and choice types ininternal/commandmeta; backend types do not cross the adapter boundary. -
Keep hints independent of binding identity, requiredness, defaults, repetition, and converters. Static choices are literal prefix matches and do not constrain accepted values. Empty choices mean no suggestions, not file fallback. Use ordinary boolean literals
trueandfalsefor the built-in boolean assignment case; an application boolean binding without a hint has no value suggestions. -
Verify caller mutation of the original choice slice cannot change captured suggestions, repeated/captured bindings keep hints, and values absent from a suggestion list still reach the existing converter.
Completion: added the public choice and hint constructors, carried hints through
flag and operand bindings into neutral metadata, and supplied explicit boolean
choices for built-in help and reporting flags. Focused tests cover copied
choices, repeated and captured bindings, the empty-choice strategy, independent
bridge copies, and execution of values outside the suggested choices. Root,
internal adapter, and neutral metadata package tests pass with GOWORK=off and
GOTOOLCHAIN=local.
Stage 3: Install completion as an ordinary declaration
Status: complete.
- Add
func Completion(executable string) Command. The parameter is the executable basename the operator types, independent of the human-readableIdentity.Name. Use a root-onlycompletiongroup with three runnable children,bash,zsh, andfish. Use the existing declaration and registration API (New(..., Completion("app"))orapp.Register(Completion("app"))). Keep script-specific name handling inside completion; do not change validation of ordinary application identity. - Carry a private completion-installation marker with the declaration and
derive query availability from the committed routing snapshot. Use the
existing registration candidate to validate collisions in either order,
including aliases and same-batch declarations. Reserve the query's first
token only while installed: a canonical command or alias beginning with
that token would be shadowed by raw query dispatch. Reuse existing
command/alias collision rules for
completion. Do not represent the query as a canonical node, alias target, or public help entry. - Generate scripts in the internal adapter using the transport established
in Stage 1. Surface generator errors through ordinary presentation.
Use
Reporter.RenderOutputfor script generation and delivery, soapp completion bash --quietfollows normal reporting. Document the basename expected by the generated scripts; handle unsupported generator names at the completion boundary. - Verify opt-in behavior, root-only placement, help visibility, batch rollback, late installation, and absence of new reserved names for an app that does not install completion.
Completion: added the root-only completion declaration and three script
commands, with generator output delivered through the invocation reporter.
Registration validates hidden-query collisions in either order, and the routing
snapshot reflects committed installation. Tests cover help, quiet output,
delivery failure, invalid basenames, opt-in behavior, collisions, rollback,
nesting, and late installation. Fish variable names are adjusted to support
dotted executable basenames; interactive checks of those names pass in Bash,
Zsh, and Fish. Root and adapter package tests pass with GOWORK=off and
GOTOOLCHAIN=local.
Stage 4: Resolve the entered command context
Status: complete.
- Add a private core completion request that separates completed words from
the current word. Call
resolveEnteredRouteon completed words only; use the snapshot's canonical metadata, resolved flag-alias context, and discovery context. Keep entered words unchanged. For canonical routes with a runnable operand owner, use the existing canonical prefix length to distinguish its trailing operands/options from command words. - Suggest canonical child names matching the partial word while the input is still in command-path position. At a node with both children and operands, matching children take precedence; if there are no matching children, use the operand context when available. Check insertion against normal routing: resolve completed entered words plus the candidate and retain it only if it reaches the intended canonical child. Do not treat a partial alias as already entered. After a flag or operand has ended the command path, do not suggest children.
- An alias-only discovery prefix provides no command continuations, but can supply applicable canonical flags using discovery metadata and alias mappings. Unknown routes with no valid context yield no candidates. At root, use the installed visible commands, including built-ins and integrations. Exclude the protocol endpoint everywhere.
- Add context tests for root/group/runnable paths, a partial canonical child, a complete alias and alias chain, an alias-only prefix, a canonical child shadowed in the entered route, and flags beginning before the complete command path. Use existing routing rules as the oracle.
Completion: added private request context resolution using completed words and
the committed routing snapshot. Canonical child candidates are checked by
resolving the entered route with the candidate inserted; discovery-only and
unknown contexts do not invent command continuations. Tests cover canonical
and aliased routes, chains, scoped and discovery flag aliases, shadowed
children, runnable operand metadata, and paths ended by flags or operands.
The root package suite passes with GOWORK=off and GOTOOLCHAIN=local.
Stage 5: Interpret flags and locate value positions
- Walk the completed tail using the same canonical and alias spellings and
flag token rules as normal parsing. Factor shared token classification
from
internal/cobra/flags.gointo neutral helpers where needed; keep backend parsing and failure wording at their current owner. The completion walk records supplied canonical flags, consumed operands, whether options ended, and any pending value flag. It does not run Cobra dispatch, converters, required checks, input factories, or providers. - A pending value takes precedence even if the current word starts with
a dash. For long equals forms, explicit boolean assignments, and short
value flags in clusters, separate the value prefix from the entered
spelling and separator. Preserve that spelling, including an alias.
For a partially typed short cluster still accepting flag members, use its
canonical short spellings for continuations; do not rewrite earlier members.
A standalone
--ends flags unless consumed as a value. After it, dash-looking words are operands. An unknown completed option that makes further interpretation ambiguous yields no suggestions. - For flag-name suggestions, match canonical spellings and suppress already supplied singletons by canonical identity, including occurrences through aliases. Keep repeatable flags and the four reporting flags eligible. When a repeated singleton is typed anyway, still complete its value.
- For an operand, use the same cardinality allocation as
allocateOperandsinbinding.gowithout required validation: include the current word as a placeholder, determine which binding receives that final position, and use its hint. Extract the cardinality calculation if necessary to avoid a second allocation policy. Excess operands receive no suggestions. When both operand and flag positions are available, an empty word uses the operand hint and an option-looking partial word requests flags. - Test pending/attached/empty values, short clusters, scoped aliases and unavailable targets, repeats, booleans, option-looking values, interspersed operands, terminators, and optional/repeated operands before a required suffix. Keep existing normal execution tests green at the sprint boundary.
Stage 6: Assemble candidates and deliver query replies
- Produce a neutral result of candidate replacements, optional descriptions, or a filesystem strategy with value context. Keep prefix preservation distinct from matching; the adapter translates the neutral replacement according to the shell conventions verified in Stage 1. Do not interpret choice values as shell source. Normalize description line breaks and omit candidates the line protocol cannot represent rather than changing what an inserted value means. Do not add execution-time validation for these presentation limitations.
- In
Execute, after normal receiver/context checks and before ordinary routing, recognize the exact raw query token only on an installed snapshot. Treat all following tokens as data. Split off the final word; a missing current-word argument is a malformed transport request, distinct from an empty string. Use the current snapshot on each query, including after later registration. Do not enter normal parsing, reporting selection, input preparation, or handler execution for this path. - Write the adapter response only to normalized
Streams.Out, with managed output delivery in ordinary mode. A valid query with no inferable context returns success and a NoFileComp reply. For a malformed request or query construction failure, return an operational outcome and, if the output is still usable, emit an error directive with no candidates. Explanations go to diagnostics, never into the reply. Preserve write failure evidence through normal presentation bookkeeping; do not retry a broken output stream. No generated script should consume ordinary usage guidance or debug output as a candidate. - Test query dispatch via
App.Execute, including explicit empty last word, no-context reply, malformed request, writer failure, hidden discovery, later registration, and target words containing--quietand--debug. Use counter fixtures to prove target handlers, converters, factories, and providers are untouched. Verify no special dispatch without installation.
Stage 7: Annotate maintained commands and examples
- Annotate static format/scope flags where their choices already come from fixed command metadata or literals. Use the same values as the existing version, config, and PromptKit command implementations; omit scope hints that would advertise values not supported by that installed declaration. Mark existing config/overlay filename flags as files where the binding takes a filename. Leave prompt IDs, dynamic selectors, and composite assignments unannotated. Do not infer domain semantics or load providers.
- Add explicit installation to the maintained core and composed examples. Demonstrate a static choice, a path hint, and an already-entered alias with no new runtime dependency. Include an integration-level check that installed hints reach completion without acquisition.
- Update GoDoc, the CLI reference, and consumer/example instructions with actual syntax, basename selection, manual script loading for each shell, and examples of hints. Keep the wire protocol private. Once delivered, update the feature status and keep alias discovery and dynamic discovery in deferred work.
Stage 8: Validate the assembled sprint
- Exercise the generated scripts in Bash, Zsh, and Fish using a built fixture executable, a disposable directory with filenames and directories containing spaces, and isolated shell configuration. Verify actual completion output and replacement behavior. Cover canonical commands, an entered alias, static choices and descriptions, no fallback, file/directory hints in separated and attached values, and the empty operand/leading-dash rule. Use a PTY or a shell's native completion test interface where appropriate. Syntax checking alone is insufficient. Record shell versions and the commands or harness used so the evidence is reproducible.
- Run the root and both optional module suites under the local workspace
procedure in development.md, plus the root suite
with
GOWORK=off. Keep shell-dependent checks outside the default Go suite unless the existing environment provides those shells; an absent shell is missing acceptance evidence, not a passing check. - Verify no backend types in public declarations, no reverse optional-module dependency, and no changes to ordinary routing, flag acceptance, or report behavior outside the selected completion scope. Review documentation links, formatting, and the final diff. Record the actual validation results.