From 9811782e6debc8e770fe45b83a31954c9f093e0e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 5 May 2026 15:02:34 -0400 Subject: [PATCH] fix(#3121): implement commands verb in SDK native registry (#3146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add commandsList handler — returns sorted JSON array of all registered verb strings; satisfies workstream-flag.md + agent tooling discoverability - Register ['commands', commandsList] in DECISION_ROUTING_STATIC_CATALOG - Add golden-policy exemption (SDK-only, no CJS mirror needed) - check.decision-coverage-plan/verify were already registered; commands was the remaining gap Closes #3121 --- .../fix-3121-gsd-tools-commands-verb.md | 5 +++ sdk/src/golden/golden-policy.ts | 2 ++ .../command-static-catalog-foundation.ts | 2 ++ sdk/src/query/commands-list.test.ts | 36 +++++++++++++++++++ sdk/src/query/commands-list.ts | 19 ++++++++++ 5 files changed, 64 insertions(+) create mode 100644 .changeset/fix-3121-gsd-tools-commands-verb.md create mode 100644 sdk/src/query/commands-list.test.ts create mode 100644 sdk/src/query/commands-list.ts diff --git a/.changeset/fix-3121-gsd-tools-commands-verb.md b/.changeset/fix-3121-gsd-tools-commands-verb.md new file mode 100644 index 000000000..9f5f9d740 --- /dev/null +++ b/.changeset/fix-3121-gsd-tools-commands-verb.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3121 +--- +**`gsd-sdk query commands` no longer returns "Unknown command"** — `commands` was referenced in `references/workstream-flag.md` and by agent tooling for verb discovery but had no SDK handler. A new `commandsList` handler in the native registry returns a sorted JSON array of all registered verb strings. `check.decision-coverage-plan` and `check.decision-coverage-verify` were already registered in the SDK native registry; the remaining gap was the `commands` introspection verb. Closes #3121. diff --git a/sdk/src/golden/golden-policy.ts b/sdk/src/golden/golden-policy.ts index 1c3f15445..8a1432771 100644 --- a/sdk/src/golden/golden-policy.ts +++ b/sdk/src/golden/golden-policy.ts @@ -51,6 +51,8 @@ const NO_CJS_SUBPROCESS_REASON: Record = { 'SDK-only structured plan parse (no CJS mirror). Covered in sdk/src/query/plan-task-structure.test.ts.', 'requirements.extract-from-plans': 'SDK-only requirements aggregation (no CJS mirror). Covered in sdk/src/query/requirements-extract-from-plans.test.ts.', + 'commands': + 'SDK-only registry introspection (no gsd-tools.cjs equivalent — the CJS layer has no self-describing verb). Covered in sdk/src/query/commands-list.test.ts. Closes #3121.', }; const READ_HANDLER_ONLY_REASON = (cmd: string) => diff --git a/sdk/src/query/command-static-catalog-foundation.ts b/sdk/src/query/command-static-catalog-foundation.ts index 62fa650e9..794ca9494 100644 --- a/sdk/src/query/command-static-catalog-foundation.ts +++ b/sdk/src/query/command-static-catalog-foundation.ts @@ -14,6 +14,7 @@ import { templateFill, templateSelect } from './template.js'; import { verifySummary, verifyPathExists } from './verify.js'; import { decisionsParse } from './decisions.js'; import { checkDecisionCoveragePlan, checkDecisionCoverageVerify } from './check-decision-coverage.js'; +import { commandsList } from './commands-list.js'; import { checkConfigGates } from './config-gates.js'; import { checkAutoMode } from './check-auto-mode.js'; import { checkPhaseReady } from './phase-ready.js'; @@ -95,4 +96,5 @@ export const DECISION_ROUTING_STATIC_CATALOG: ReadonlyArray { + it('returns a non-empty sorted JSON array of command strings', async () => { + const result = await commandsList([], '/tmp', undefined); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect((result.data as string[]).length).toBeGreaterThan(10); + }); + + it('includes known canonical commands', async () => { + const result = await commandsList([], '/tmp', undefined); + const cmds = result.data as string[]; + expect(cmds).toContain('state.begin-phase'); + expect(cmds).toContain('check.decision-coverage-plan'); + expect(cmds).toContain('commands'); + }); + + it('is sorted alphabetically', async () => { + const result = await commandsList([], '/tmp', undefined); + const cmds = result.data as string[]; + const sorted = [...cmds].sort((a, b) => a.localeCompare(b)); + expect(cmds).toEqual(sorted); + }); + + it('args and workstream are ignored (introspection verb)', async () => { + const r1 = await commandsList([], '/tmp', undefined); + const r2 = await commandsList(['ignored'], '/other', 'ws'); + expect(r1.data).toEqual(r2.data); + }); +}); diff --git a/sdk/src/query/commands-list.ts b/sdk/src/query/commands-list.ts new file mode 100644 index 000000000..8311f5d5e --- /dev/null +++ b/sdk/src/query/commands-list.ts @@ -0,0 +1,19 @@ +import type { QueryHandler } from './utils.js'; +import { createRegistry } from './index.js'; + +/** + * `commands` — return the full list of registered query command strings. + * + * Closes #3121: the `commands` verb was referenced in workflow files + * (references/workstream-flag.md) but had no native SDK handler, causing + * a fallback to gsd-tools.cjs which threw "Unknown command: commands". + * + * Returns: JSON array of all canonical + alias command strings the SDK + * registry accepts, sorted alphabetically. Suitable for discoverability + * and for agent auto-complete when constructing `gsd-sdk query` calls. + */ +export const commandsList: QueryHandler = async (_args, _projectDir) => { + const registry = createRegistry(); + const cmds = registry.commands().sort((a, b) => a.localeCompare(b)); + return { data: cmds }; +};