Implementation notes β bootstrap-prerun-traversal¶
Spec: docs/development/specs/2026-06-12-bootstrap-prerun-traversal.md
Branch: fix/bootstrap-prerun-traversal
What I implemented¶
Per the spec's resolved design (D1, D2, D3):
-
D1 β
cobra.EnableTraverseRunHooks = trueset at the top ofNewCmdRootWithConfiginpkg/cmd/root/root.go(whichNewCmdRootdelegates to, so both entry points are covered). With this, cobra runs everyPersistentPreRunEfrom root to leaf instead of only the closest, so the framework bootstrap (root hook) always runs first and a downstream child hook runs after it. Confirmed the flag was not previously set anywhere. -
D2 β no opt-out. Bootstrap always runs; nothing added to suppress it.
-
D3 β one-time debug log + docs. Added
logShadowingPreRunHooks(withcommandTreeHasPersistentPreRun), called once after the full tree is assembled inNewCmdRootWithConfig. If any non-root command in the tree defines aPersistentPreRunE/PersistentPreRun, it emits a single debug-level log noting the hook runs AFTER the framework bootstrap, not instead of it. Updated: docs/concepts/command-middleware.mdβ new "Hooks vs. the framework bootstrap" section + amended the "Middleware vs. Hooks" tip.docs/how-to/nested-subcommands.mdβ new subsection on subcommandPersistentPreRunEordering, cross-linking the concept doc.
Tests (TDD)¶
Added to pkg/cmd/root/root_test.go:
TestBootstrapRunsDespiteChildPersistentPreRunEβ the core regression: a child with its ownPersistentPreRunEis executed via the fullrootCmd.ExecuteContext, and we assertprops.Configandprops.Collectorare populated (proving the root bootstrap ran). Verified it FAILS with the flag off and passes with it on.TestBootstrapOrdering_RootBeforeChildβ capturesprops.Configfrom inside the child hook and asserts it is already non-nil, proving rootβleaf ordering.
Both use a bootstrapTestProps helper (network/prompt features disabled,
logger.NewNoop(), a real in-memory config file pointed at via --config).
They follow the existing non-parallel + setup.ResetRegistryForTesting() +
t.Cleanup pattern because NewCmdRoot seals the global middleware registry.
Judgment calls¶
-
Where to set the flag. Set it inside
NewCmdRootWithConfigrather than aninit()so it is scoped to GTB root construction and easy to find. It is a cobra process-global; the spec explicitly accepts this (only the root defines a persistent pre-run hook in GTB's tree, so rootβleaf traversal is otherwise a no-op).gochecknoglobalsdoes not flag assigning a third-party package var. -
D3 detection site. Implemented as a post-registration tree walk in the root constructor rather than hooking into
setup.Command.Register. This keepspkg/setupuntouched (no logger dependency added there) and gives a single, deterministic "once per tool" log. The walk also checks the deprecatedPersistentPreRun(non-E) form for completeness. -
Test config strategy. I kept
InitCmdenabled and wrote a real config file rather than disablingInitCmd. DisablingInitCmdflipsallowEmptytrue and makes bootstrap look up the embeddedassets/init/config.yaml, which an empty testAssetsdoesn't have β an unrelated failure mode. A real--configfile keeps the test focused on the traversal behaviour.
Verification status¶
go build ./...β clean.go test ./pkg/cmd/root/...β pass (also-raceon root + setup).golangci-lint run pkg/cmd/root/...β 0 issues. No//nolintadded.- E2E (verification-plan item 4): added the
config-probefixture command incmd/e2e/config_probe.go(defines its ownPersistentPreRunEand readsprobe.valuefromprops.ConfiginRunE) and thefeatures/cli/bootstrap_traversal.featureGherkin scenario. Runs green underINT_TEST_E2E=1 INT_TEST_E2E_CLI=1and the@smokeset (INT_TEST_E2E_SMOKE=1); needs no external services. Asserts both the child hook marker and the config-loaded value print, proving rootβleaf traversal.
Deferred / out of scope¶
- O3 (middleware unification) β moving bootstrap to be the outermost
registered middleware instead of a
PersistentPreRunEremains deferred per the spec. Not touched here.
Questions for Matt¶
Do you want an E2E BDD scenario (verification-plan item 4: scaffolded tool with a custom subcommand hook resolving config correctly)?Resolved: added. A contrivedconfig-probesubcommand incmd/e2e/(its ownPersistentPreRunE+ aRunEthat readsprobe.valuefromprops.Config) plusfeatures/cli/bootstrap_traversal.feature. The scenario runs the subcommand against a known--configfile and asserts the config value prints (root bootstrap ran) and the child-hook marker prints (both hooks ran, in order). Tagged@cli @smoke; green underjust test-e2e-smokeand the CLI gate. Thecmd/e2ebinary enables all feature flags, which is the closest standing analogue to a scaffolded tool with a custom hook.- The D3 shadowing notice is a debug log emitted once at construction time. Comfortable with debug level, or would you prefer it surfaced more loudly (e.g. info) for tool authors during development?
- Generator templates: scaffolded tools call
root.NewCmdRoot, so they inherit the fix automatically β no template change needed. Confirm you're happy there is nothing to change ininternal/generator/.