You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Parent: none Supersedes:#98's "CLI library: stdlib flag + os.Args dispatch. Cobra/urfave are out" decision (locked 2026-02-XX), whose own revisit clause ("revisit if sub-sub-commands or shell completion become load-bearing") has now fired Depends on: none
Decision (locked 2026-04-17)
Replace the os.Args[1] switch + flag.NewFlagSet dispatch in cmd/deadzone/ with spf13/cobra. Each runX(args []string) error function becomes a *cobra.Command with RunE:. Sub-subcommands (packs upload|download|list) become child commands under a packs parent. Shell completion ships out-of-the-box via cobra's completion subcommand. -flag single-dash form gets replaced by POSIX --flag everywhere, with all callsites (justfile, GH workflows, tests, docs) updated in the same PR.
Framework choice: cobra over kong/urfave-cli. Rationale: de facto Go CLI standard (43.7k stars, used by kubectl, gh, hugo), best shell-completion ergonomics, zero surprises for contributors familiar with the Go ecosystem. The boilerplate cost is real but manageable at 6 top-level commands + 3 child commands.
Why
Three observations converged to trigger this migration:
-version / --version / version accepted but undocumented — dispatch accepts all three forms (main.go:79), the usage text lists none. Pure UX debt that a CLI framework would resolve for free
go.mod gains github.com/spf13/cobra (latest stable at impl time). github.com/spf13/pflag is cobra's transitive dep — do not add explicitly
cmd/deadzone/main.go — replace the dispatch(args []string) error switch with a cobra rootCmd that AddsCommand for each subcommand. main() becomes if err := rootCmd.Execute(); ...
Per-subcommand files (server.go, scrape.go, consolidate.go, dbrelease.go, fetchdb.go, packs.go) each export a Cmd() or package-level *cobra.Command variable. Internal logic stays in the same file — only the flag wiring layer changes
All flags migrate from flag.StringVar(&dbPath, "db", "", "...") to cmd.Flags().StringVar(&dbPath, "db", "", "..."). No short-form aliases added beyond what existed before (keep POSIX convention: --db long, -d only if deliberate)
Top-level -version behavior preserved via rootCmd.Version = buildinfo.Format(...). deadzone --version and deadzone version both work; undocumented -version single-dash form may break, document in the PR body
deadzone completion bash|zsh|fish|powershell works — cobra ships this automatically; verify it appears in deadzone --help output
All flag invocations in the codebase migrate from -flag to --flag — this is the single widest change:
justfile recipes: scrape lib=... → likely already kwargs, verify; any literal -flag in recipe bodies gets updated
.github/workflows/ci.yml and .github/workflows/release.yml — audit for single-dash flag uses
cmd/deadzone/*_test.go — update test harnesses that exec the binary or build args lists
docs/research/*.md and README.md — any usage example with -flag form
CLAUDE.md → Architectural context bullet on subcommand dispatch updates: "Routing via spf13/cobra. rootCmd in cmd/deadzone/main.go registers child commands declared in sibling files (one *cobra.Command per file). Revisit if completion / cobra.CompletionOptions needs become more exotic, not before."
CLAUDE.md → Conventions bullet updates: "POSIX --flag form everywhere. Single-dash -flag no longer accepted (cobra/pflag semantics)."
README.md → Build-from-source section: all deadzone ... -flag examples updated to --flag
just test -short passes
Manual smoke: deadzone --help, deadzone completion bash | head, deadzone scrape --help, deadzone packs upload (returns the disabled error), deadzone --version, deadzone server --db /tmp/foo.db — all work as expected
Code skeleton (sketch — finalize in implementation)
// cmd/deadzone/server.go (sketch)varserverCmd=&cobra.Command{
Use: "server",
Short: "Run the MCP stdio server against a deadzone.db",
RunE: func(cmd*cobra.Command, args []string) error {
returnrunServer() // existing logic, minus the flag.Parse wiring
},
}
funcinit() {
serverCmd.Flags().StringVar(&dbPath, "db", "", "path to turso database file (default: auto-resolve)")
serverCmd.Flags().StringVar(&embedderKind, "embedder", "hugot", "embedder to use (valid: hugot)")
serverCmd.Flags().BoolVar(&verbose, "verbose", false, "include the raw query text in per-call logs")
}
cmd/deadzone/server.go, scrape.go, consolidate.go, dbrelease.go, fetchdb.go, packs.go — replace flag.NewFlagSet with cobra flag wiring; keep the internal business logic functions
cmd/deadzone/*_test.go — update any test that exec'd the binary or built os.Args-style slices
justfile — audit/update any recipe body that uses -flag form
.github/workflows/scrape-pack.yml — update scrape -artifacts ... -lib ... to POSIX
.github/workflows/ci.yml, .github/workflows/release.yml — same audit
deadzone version AND deadzone --version AND deadzone -v — all surface the buildinfo banner
deadzone packs upload — returns the disabled-error exit code 1 with a clear message
deadzone scrape --help — POSIX --flag form, no -flag stub (confirm it REJECTS -flag form cleanly now)
Re-dispatch scrape-pack.yml on a branch with this migration — the workflow's scrape step must still succeed end-to-end (validates the .github/workflows/scrape-pack.yml update)
Out of scope (fenced)
No new subcommands — migration only; no deadzone init, deadzone status, deadzone search even though they'd be trivial in cobra. File separately
No renaming of existing subcommands — server, scrape, consolidate stay as-is
No introduction of viper for config — cobra can work with or without viper; stay without
No struct-based config objects — keep package-level vars per subcommand; cobra's Flags().XxxVar binding reads them the same way
No addition of shorthand flags beyond what currently exists (avoid API bloat for one-off short forms)
No documentation site generation via cobra.GenManTree — static man pages are their own issue
No cobra.OnInitialize magic for env-var loading — keep the existing os.Getenv reads inline in each runX function
No backward-compat -flag shim — POSIX --flag only. Pre-1.0, no user base
No changes to internal packages — internal/{db,embed,scraper,packs,ort} untouched
No test-suite framework swap — keep existing stdlib testing
Open sub-decisions for the implementer
Top-level flag placement: --verbose is currently duplicated across scrape and server. Cobra allows a PersistentFlags() on rootCmd that all children inherit. Consider hoisting OR keep per-subcommand. Implementer picks based on whether the semantics are truly identical (they might not be — server --verbose logs raw queries, scrape --verbose logs per-doc debug lines)
Completion install path: cobra produces a script; whether to add a just install-completion recipe or leave operators to run deadzone completion bash > /etc/bash_completion.d/deadzone manually is an ergonomics call
fetch-db vs fetch_db vs fetchdb: cobra accepts hyphens. Current name is fetch-db. Keep it
The global trend of "revisit if X triggers" in CLAUDE.md — this is a textbook case of a trigger firing cleanly. Document the pattern in the retrospective when this lands
Parent: none
Supersedes: #98's "CLI library: stdlib
flag+ os.Args dispatch. Cobra/urfave are out" decision (locked 2026-02-XX), whose own revisit clause ("revisit if sub-sub-commands or shell completion become load-bearing") has now firedDepends on: none
Decision (locked 2026-04-17)
Replace the
os.Args[1]switch +flag.NewFlagSetdispatch incmd/deadzone/with spf13/cobra. EachrunX(args []string) errorfunction becomes a*cobra.CommandwithRunE:. Sub-subcommands (packs upload|download|list) become child commands under apacksparent. Shell completion ships out-of-the-box via cobra'scompletionsubcommand.-flagsingle-dash form gets replaced by POSIX--flageverywhere, with all callsites (justfile, GH workflows, tests, docs) updated in the same PR.Framework choice: cobra over kong/urfave-cli. Rationale: de facto Go CLI standard (43.7k stars, used by kubectl, gh, hugo), best shell-completion ergonomics, zero surprises for contributors familiar with the Go ecosystem. The boilerplate cost is real but manageable at 6 top-level commands + 3 child commands.
Why
Three observations converged to trigger this migration:
packs upload|download|listare in the code (disabled per scraper/packs: folder-per-lib layout + retire per-artifact release, ship deadzone.db only #101 but structurally present). cli: consolidate the 4 binaries into a singledeadzonewith subcommands #98's "revisit if sub-sub-commands become load-bearing" trigger has fired-version/--version/versionaccepted but undocumented — dispatch accepts all three forms (main.go:79), the usage text lists none. Pure UX debt that a CLI framework would resolve for freedeadzonewith subcommands #98's "Out of scope → Future issue". With cobra it'scobra.Command{}.GenBashCompletionFile(...)— the cost drops from "write a completion script by hand" to "enable a built-in"Acceptance criteria
go.modgainsgithub.com/spf13/cobra(latest stable at impl time).github.com/spf13/pflagis cobra's transitive dep — do not add explicitlycmd/deadzone/main.go— replace thedispatch(args []string) errorswitch with a cobra rootCmd that AddsCommand for each subcommand.main()becomesif err := rootCmd.Execute(); ...server.go,scrape.go,consolidate.go,dbrelease.go,fetchdb.go,packs.go) each export aCmd()or package-level*cobra.Commandvariable. Internal logic stays in the same file — only the flag wiring layer changesflag.StringVar(&dbPath, "db", "", "...")tocmd.Flags().StringVar(&dbPath, "db", "", "..."). No short-form aliases added beyond what existed before (keep POSIX convention:--dblong,-donly if deliberate)packsparent command ownspacks upload,packs download,packs listas cobra child commands — even though all three are DISABLED per scraper/packs: folder-per-lib layout + retire per-artifact release, ship deadzone.db only #101, they still surface indeadzone packs --helpwith their disabled-state error-versionbehavior preserved viarootCmd.Version = buildinfo.Format(...).deadzone --versionanddeadzone versionboth work; undocumented-versionsingle-dash form may break, document in the PR bodydeadzone completion bash|zsh|fish|powershellworks — cobra ships this automatically; verify it appears indeadzone --helpoutput-flagto--flag— this is the single widest change:justfilerecipes:scrape lib=...→ likely already kwargs, verify; any literal-flagin recipe bodies gets updated.github/workflows/scrape-pack.yml—scrape -artifacts ./artifacts -lib <id>→--artifacts ./artifacts --lib <id>.github/workflows/ci.ymland.github/workflows/release.yml— audit for single-dash flag usescmd/deadzone/*_test.go— update test harnesses that exec the binary or build args listsdocs/research/*.mdandREADME.md— any usage example with-flagformCLAUDE.md→ Architectural context bullet on subcommand dispatch updates: "Routing viaspf13/cobra.rootCmdincmd/deadzone/main.goregisters child commands declared in sibling files (one*cobra.Commandper file). Revisit if completion /cobra.CompletionOptionsneeds become more exotic, not before."CLAUDE.md→ Conventions bullet updates: "POSIX--flagform everywhere. Single-dash-flagno longer accepted (cobra/pflag semantics)."README.md→ Build-from-source section: alldeadzone ... -flagexamples updated to--flagjust test -shortpassesdeadzone --help,deadzone completion bash | head,deadzone scrape --help,deadzone packs upload(returns the disabled error),deadzone --version,deadzone server --db /tmp/foo.db— all work as expectedCode skeleton (sketch — finalize in implementation)
Concrete file pointers
Files to modify:
go.mod/go.sum—go get github.com/spf13/cobra@latestcmd/deadzone/main.go— complete rewrite of dispatch (preserve buildinfo + exit-code behavior)cmd/deadzone/server.go,scrape.go,consolidate.go,dbrelease.go,fetchdb.go,packs.go— replaceflag.NewFlagSetwith cobra flag wiring; keep the internal business logic functionscmd/deadzone/*_test.go— update any test that exec'd the binary or builtos.Args-style slicesjustfile— audit/update any recipe body that uses-flagform.github/workflows/scrape-pack.yml— updatescrape -artifacts ... -lib ...to POSIX.github/workflows/ci.yml,.github/workflows/release.yml— same auditCLAUDE.md— two bullet updates per AC aboveREADME.md— Build-from-source section usage examplesdocs/research/ingestion-architecture.md, other research docs — audit for-flagform in shell examplesFiles to read as reference — do NOT refactor:
cmd/deadzone/main.go:72-108— current dispatch shapecmd/packs/main.go(if still present as a reference) — the pre-cli: consolidate the 4 binaries into a singledeadzonewith subcommands #98 pattern this issue supersedesTest commands (literal, for agent self-check)
mise exec -- go build -tags ORT ./...— builds cleanjust test -short— full suite green (will catch any callsite not updated from-flagto--flag)deadzone --help— full subcommand list withcompletionshowndeadzone completion bash > /tmp/deadzone.bash && head /tmp/deadzone.bash— valid bash completion scriptdeadzone versionANDdeadzone --versionANDdeadzone -v— all surface the buildinfo bannerdeadzone packs upload— returns the disabled-error exit code 1 with a clear messagedeadzone scrape --help— POSIX--flagform, no-flagstub (confirm it REJECTS-flagform cleanly now)scrape-pack.ymlon a branch with this migration — the workflow's scrape step must still succeed end-to-end (validates the.github/workflows/scrape-pack.ymlupdate)Out of scope (fenced)
deadzone init,deadzone status,deadzone searcheven though they'd be trivial in cobra. File separatelyserver,scrape,consolidatestay as-isviperfor config — cobra can work with or without viper; stay withoutcobra.GenManTree— static man pages are their own issuecobra.OnInitializemagic for env-var loading — keep the existingos.Getenvreads inline in each runX function-flagshim — POSIX--flagonly. Pre-1.0, no user baseinternal/{db,embed,scraper,packs,ort}untouchedOpen sub-decisions for the implementer
--verboseis currently duplicated acrossscrapeandserver. Cobra allows aPersistentFlags()on rootCmd that all children inherit. Consider hoisting OR keep per-subcommand. Implementer picks based on whether the semantics are truly identical (they might not be —server --verboselogs raw queries,scrape --verboselogs per-doc debug lines)just install-completionrecipe or leave operators to rundeadzone completion bash > /etc/bash_completion.d/deadzonemanually is an ergonomics callfetch-dbvsfetch_dbvsfetchdb: cobra accepts hyphens. Current name isfetch-db. Keep itRelated
deadzonewith subcommands #98 — the original consolidation issue this supersedes on the CLI-library decision. Leave cli: consolidate the 4 binaries into a singledeadzonewith subcommands #98 closed; this issue's decision block explicitly supersedes it0.3milestone; unrelated but co-inhabits