chore: introduce CommandRoutingHub and migrate phase-command-router (PoC) (#3828)

* 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>
This commit is contained in:
Tom Boucher
2026-05-21 23:32:10 -04:00
committed by GitHub
parent 420c64da4b
commit b533f71857
12 changed files with 1457 additions and 145 deletions

View File

@@ -277,6 +277,10 @@ Programmatic SDK callers (`GSDTools`) route through one seam that owns query dis
This keeps callers thin adapters and centralizes transport decisions for SDK publishability.
### Command Routing Hub (`get-shit-done/bin/lib/command-routing-hub.cjs`)
CJS command family routers migrate to dispatch through `CommandRoutingHub` incrementally. `phase-command-router.cjs` is the first migration (issue #3788); remaining routers (`phases-command-router.cjs`, `roadmap-command-router.cjs`, etc.) continue using `routeCjsCommandFamily` until migrated in follow-up issues. The hub owns three cross-cutting concerns that each router previously duplicated: (1) mode selection (`sdk` when `tryLoadSdk()` succeeds and no `GSD_WORKSTREAM` is active, `cjs` otherwise), set once at construction; (2) a no-throw pure-result contract (`hub.dispatch()` catches all exceptions and returns `{ ok: false, errorKind, message, details }` instead of propagating); and (3) a closed six-value `errorKind` enum exported as the frozen `ERROR_KINDS` object. Router adapters remain thin CLI translators — they build the hub, call `dispatch`, then map the Result to `output()`/`error()` calls. No transparent SDK→CJS fallback: an SDK-mode hub that encounters a load or dispatch failure returns `SdkLoadFailed` or `SdkDispatchFailed` without retrying via CJS. See `docs/adr/0012-command-routing-hub.md`.
### CLI Tools (`get-shit-done/bin/`)
Node.js CLI utility (`gsd-tools.cjs`) with domain modules split across `get-shit-done/bin/lib/` (see [`docs/INVENTORY.md`](INVENTORY.md#cli-modules-33-shipped) for the authoritative roster):

View File

@@ -268,6 +268,7 @@
"clusters.cjs",
"code-review-flags.cjs",
"command-aliases.generated.cjs",
"command-routing-hub.cjs",
"commands.cjs",
"config-schema.cjs",
"config.cjs",

View File

@@ -361,7 +361,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
---
## CLI Modules (73 shipped)
## CLI Modules (74 shipped)
Full listing: `get-shit-done/bin/lib/*.cjs`.
@@ -376,6 +376,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
| `clusters.cjs` | Skill cluster definitions for the runtime surface module (ADR-0011 Phase 2) |
| `code-review-flags.cjs` | Typed flag parser for `/gsd:code-review`; exports `parseCodeReviewFlags(argv)` (→ `{ fix, all, auto, depth, files }`) and `resolveCodeReviewWorkflow(flags)` (→ `'code-review.md' \| 'code-review-fix.md'`); canonical dispatch seam for `--fix`/`--all`/`--auto` routing |
| `command-aliases.generated.cjs` | Generated CJS alias/subcommand metadata for manifest-backed family routers |
| `command-routing-hub.cjs` | Pure-result dispatch hub that centralizes mode decision (SDK vs CJS), error taxonomy, and no-throw contract for all command-family routers (#3788) |
| `commands.cjs` | Misc CLI commands (slug, timestamp, todos, scaffolding, stats) |
| `config-schema.cjs` | Single source of truth for `VALID_CONFIG_KEYS` and dynamic key patterns; imported by both the validator and the config-schema-docs parity test |
| `config.cjs` | `config.json` read/write, section initialization; imports validator from `config-schema.cjs` |

View File

@@ -0,0 +1,51 @@
# 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**: `mode` is set once when `createHub` is 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 `errorKind` enum**: the six error kinds (`UnknownCommand`, `InvalidArgs`, `HandlerRefusal`, `HandlerFailure`, `SdkLoadFailed`, `SdkDispatchFailed`) are exported as a frozen `ERROR_KINDS` object. Callers switch on `ERROR_KINDS` values, 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 `cjsRegistry` entries rather than hand-replicating the SDK/CJS conditional block.
- **Constraint**: adding a new `errorKind` value requires updating `ERROR_KINDS` in `command-routing-hub.cjs` AND 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](https://github.com/gsd-build/get-shit-done/issues/3788)

View File

@@ -44,6 +44,7 @@ See **[CONTRIBUTING.md — "Proposing an ADR or PRD"](../../CONTRIBUTING.md#prop
| [0011-skill-surface-budget-module.md](0011-skill-surface-budget-module.md) | Skill Surface Budget Module owns install-time profile staging and runtime surface control | Accepted |
| [0011-review-default-reviewers.md](0011-review-default-reviewers.md) | Review default-reviewers selection policy for /gsd:review | Accepted |
| [0011-review-default-reviewers-prd.md](0011-review-default-reviewers-prd.md) | PRD for review.default_reviewers feature (#3464) | Reference |
| [0012-command-routing-hub.md](0012-command-routing-hub.md) | CommandRoutingHub as single dispatch seam for CJS command families | Accepted |
| [3524-cjs-sdk-hard-seam.md](3524-cjs-sdk-hard-seam.md) | CJS↔SDK hard seam — single canonical owner per responsibility (#3524) | Proposed |
| [3660-runtime-artifact-layout-module.md](3660-runtime-artifact-layout-module.md) | Runtime Artifact Layout Module owns per-runtime artifact placement | Proposed |