* feat(routing): add CommandRoutingHub with behavioral test suite (#3788) Introduces createHub({ mode, sdkLoader, cjsRegistry, manifest }) and hub.dispatch({ family, subcommand, args, cwd, raw }) -> Result with a closed 6-value ERROR_KINDS frozen enum. Hub never throws, never prints, and enforces no transparent fallback between sdk/cjs modes. 34 behavioral tests cover all errorKind values, mode fixation, and the no-throw contract. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(routing): migrate phase-command-router to CommandRoutingHub (#3788) Rewrites phase-command-router.cjs to dispatch through CommandRoutingHub. Public entry point routePhaseCommand({ phase, args, cwd, raw, error }) is unchanged. The adapter determines mode (sdk/cjs) from env + tryLoadSdk(), constructs a hub, dispatches, and translates the pure Result back to output()/error() calls. New behavioral test suite (23 tests) replaces the old mock-heavy approach and includes two integration tests through the real hub. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(routing): ADR + glossary + changeset for CommandRoutingHub (#3788) Adds ADR-3788 documenting the hub's design contract (pure result, fixed mode, closed 6-value errorKind enum, no transparent fallback). Adds Command Routing Hub glossary entry to CONTEXT.md and a one-paragraph reference to ARCHITECTURE.md. Changeset fragment records the Changed entry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(docs): rename ADR to sequential convention 0012 (#3788) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(inventory): register CommandRoutingHub in INVENTORY (#3788) Add command-routing-hub.cjs row to docs/INVENTORY.md CLI Modules table, bump headline count from 72 to 73, and regenerate INVENTORY-MANIFEST.json via scripts/gen-inventory-manifest.cjs --write. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(adr): add 0012 to ADR index (#3788) Add entry for 0012-command-routing-hub.md to the index table in docs/adr/README.md so the enh-3271-sdk-adr-structure lint passes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(lint): bump phase test-file ceiling to accommodate command-router suite (#3788) phase-command-router.test.cjs added by the CommandRoutingHub migration pushes the phase prefix cluster from 4 to 5 test files. Bump the allowlist ceiling from 4 to 5 (issue 3788) so lint-test-file-count passes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(routing): preserve phase.mvp-mode JSON error and ROADMAP scan through hub (#3788) mvp-mode was never registered in the SDK; the pre-#3788 CJS router always dispatched it via the CJS handler even when sdkAvailable was true. After the hub migration, SDK-mode hubs (Docker, where the SDK build exists) sent mvp-mode to the SDK bridge, which returned SdkDispatchFailed with reason 'unknown' instead of the expected 'usage' code, and failed ROADMAP lookups. Fix by short-circuiting mvp-mode to the CJS handler before hub construction, matching the pre-migration observable behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(adr): note SDK-incomplete subcommand limitation in ADR-0012 (#3788) * fix(inventory): bump CLI Modules headline to 74 after rebase onto main (#3788) Upstream added code-review-flags.cjs (72→73) at the same time our branch added command-routing-hub.cjs. After rebase both modules exist (74 total) but the headline stayed at 73; bump to 74. * fix(routing): remove dead mvp-mode handler from cjsRegistry (#3788) The cjsRegistry['phase']['mvp-mode'] handler (previously lines 65–68) was unreachable: the early-return bypass at line 56 intercepts mvp-mode before hub construction in CJS mode, and in SDK mode cjsRegistry is passed as undefined. Remove the dead handler; all 57 tests still pass. * docs(adr): correct router count in ADR-0012 (#3788) The context section cited "eight" routers including "frontmatter" but there is no frontmatter-command-router.cjs. The actual count is seven: phase, phases, roadmap, state, verify, validate, init. * fix(routing): guard missing subcommand + use ERROR_KINDS constant (#3788) Two fixes in phase-command-router.cjs: 1. Add early-return for missing subcommand before hub construction. Pre-#3788 the routeCjsCommandFamily fell through to error() for undefined args[1]; post-#3788 the hub's manifest check skips falsy subcommands, which would have sent bare 'phase' into SDK dispatch in SDK mode instead of the expected "Available: ..." error message. 2. Switch on ERROR_KINDS.UnknownCommand instead of bare 'UnknownCommand' string, per ADR-0012's closed-enum contract ("callers switch on ERROR_KINDS values, not bare string literals"). * docs(routing): fix factual errors in ARCHITECTURE, ADR-0012, changeset (#3788) Three corrections: 1. ARCHITECTURE.md: softened "All CJS command family routers dispatch through CommandRoutingHub" — only phase-command-router.cjs is migrated in this PR; remaining routers still use routeCjsCommandFamily and migrate in follow-up issues. 2. ADR-0012: corrected the SDK mvp-mode claim. The ADR said "the SDK has no equivalent entry" but sdk/src/query/command-static-catalog- domain.ts:104-105 registers phase.mvp-mode. The actual reason for the early-return bypass is divergent ROADMAP scan behaviour and error reason codes, not SDK absence. 3. .changeset/mellow-tigers-gather.md: corrected pr: 1 → pr: 3828. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
5.1 KiB
CommandRoutingHub as single dispatch seam for CJS command families
- Status: Accepted
- Date: 2026-05-20
Context
Seven *-command-router.cjs files (phase, phases, roadmap, state, verify, validate, init) each duplicate the same three-part dispatch pattern: (1) check GSD_WORKSTREAM + tryLoadSdk() to decide whether to use the SDK or CJS handler, (2) invoke the selected path, (3) map errors to the error() callback. The duplicated mode-selection logic means a policy change (e.g., adding a new fallback condition) must be applied in eight places. Tests for these routers are mock-heavy — they stub tryLoadSdk, stub getExecuteForCjs, and assert on internal call shapes rather than observable dispatch outcomes. The SDK-vs-CJS fallback decision is smeared across every router, making it impossible to reason about or test the policy in isolation.
Decision
Introduce CommandRoutingHub (get-shit-done/bin/lib/command-routing-hub.cjs) as the single dispatch seam for all CJS command family routers. The hub contract:
createHub({ mode: 'sdk' | 'cjs', sdkLoader, cjsRegistry, manifest }) -> hub
hub.dispatch({ family, subcommand, args, cwd, raw }) -> Result
Result = { ok: true, data }
| { ok: false, errorKind, message, details? }
Load-bearing design properties:
- Pure result: the hub never prints to stdout/stderr, never calls
process.exit, and never throws. All internal throws are caught and converted to{ ok: false, errorKind: 'HandlerFailure' }. - Mode fixed at construction:
modeis set once whencreateHubis called; it is never re-evaluated per dispatch call. Each adapter (caller) computes mode based on its own env/sdk-load context before constructing the hub. - No transparent fallback: an SDK-mode hub that encounters an SDK crash or load failure returns
{ ok: false, errorKind: 'SdkDispatchFailed' }or'SdkLoadFailed'respectively. It does not silently retry via the CJS registry. - Closed
errorKindenum: the six error kinds (UnknownCommand,InvalidArgs,HandlerRefusal,HandlerFailure,SdkLoadFailed,SdkDispatchFailed) are exported as a frozenERROR_KINDSobject. Callers switch onERROR_KINDSvalues, not bare string literals. Adding a new error kind requires amending this ADR.
The router adapter's responsibilities shrink to: determine mode from env, build stubs/registry, construct hub, dispatch, translate the pure Result to output()/error() calls. Each adapter remains a thin CLI-facing translation layer.
phase-command-router.cjs is migrated as the proof-of-concept for this PR. Remaining routers migrate in follow-up issues.
Consequences
- Positive: policy (mode decision, no-throw contract, error taxonomy) is concentrated in one module rather than duplicated across eight. Testing the policy requires only the hub unit tests; adapter tests verify translation correctness (args → dispatch, Result → output/error).
- Positive: future routers can be onboarded by wiring
cjsRegistryentries rather than hand-replicating the SDK/CJS conditional block. - Constraint: adding a new
errorKindvalue requires updatingERROR_KINDSincommand-routing-hub.cjsAND amending this ADR. The closed enum is the drift-prevention property; the amendment requirement makes scope of impact explicit. - Constraint: each adapter must compute mode before hub construction (no lazy re-evaluation). This is intentional — mode ambiguity at dispatch time is a prior source of subtle test flakiness.
Known limitation: SDK-incomplete subcommands
The hub's mode is fixed at construction ('sdk' or 'cjs'). This works cleanly only when every subcommand in a family has an implementation in the active mode. Today some phase subcommands have divergent CJS and SDK implementations. phase.mvp-mode is present in the SDK catalog (command-static-catalog-domain.ts) but its CJS-native implementation (phase.cmdPhaseMvpMode) differs in ROADMAP scan behaviour and error reason codes from the SDK query layer. Routing mvp-mode through the SDK hub would silently change observable CLI behaviour (exit codes, JSON error shape).
The proof-of-concept adapter (phase-command-router.cjs) handles this with an early-return bypass: mvp-mode is intercepted before the dispatch call so it never reaches the hub. This preserves observable behavior but introduces a hub-level abstraction leak — the adapter now carries per-subcommand routing policy that the hub was meant to own.
Future direction (deferred): the hub should consult manifest to detect per-subcommand SDK coverage and route to CJS automatically for subcommands not present in the SDK manifest. That refinement stays inside the global-mode decision — the mode still applies to the family as a whole — and avoids the per-command policy ladder that was explicitly rejected during design. This work is tracked alongside SDK-CJS migration #3524 closure.
References
- Extends ADR-0001 (Dispatch Policy Module) — the hub implements the no-throw + structured-result contract ADR-0001 established for the SDK query layer, applying it to the CJS adapter layer.
- Issue: #3788