fix(#303): deepen command routing seams and alias parity guard (#304)

This commit is contained in:
Tom Boucher
2026-05-26 11:23:53 -04:00
committed by GitHub
parent 06cf7826e3
commit 43eafea3d5
13 changed files with 523 additions and 302 deletions

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
if git diff --cached --name-only | grep -Eq "^sdk/src/query/command-manifest\.|^sdk/src/query/command-aliases\.generated\.ts$|^get-shit-done/bin/lib/command-aliases\.generated\.cjs$|^sdk/scripts/gen-command-aliases\.ts$"; then
if git diff --cached --name-only | grep -Eq "^sdk/src/query/command-manifest\.|^sdk/src/query/command-aliases\.generated\.ts$|^get-shit-done/bin/lib/command-aliases\.cjs$|^sdk/scripts/gen-command-aliases\.ts$"; then
npm run check:alias-drift
fi

View File

@@ -198,6 +198,7 @@ const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
const { routeRoadmapCommand } = require('./lib/roadmap-command-router.cjs');
const { parseNamedArgs, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
// ─── Bridge collapsed (Phase 4) ────────────────────────────────────────────────
// Non-family commands now run through their CJS handlers directly. Keep the
@@ -235,44 +236,6 @@ function _dispatchNonFamily({ registryCommand, registryArgs, legacyCommand, lega
// ─── Arg parsing helpers ──────────────────────────────────────────────────────
/**
* Extract named --flag <value> pairs from an args array.
* Returns an object mapping flag names to their values (null if absent).
* Flags listed in `booleanFlags` are treated as boolean (no value consumed).
*
* parseNamedArgs(args, 'phase', 'plan') → { phase: '3', plan: '1' }
* parseNamedArgs(args, [], ['amend', 'force']) → { amend: true, force: false }
*/
function parseNamedArgs(args, valueFlags = [], booleanFlags = []) {
const result = {};
for (const flag of valueFlags) {
const idx = args.indexOf(`--${flag}`);
result[flag] = idx !== -1 && args[idx + 1] !== undefined && !args[idx + 1].startsWith('--')
? args[idx + 1]
: null;
}
for (const flag of booleanFlags) {
result[flag] = args.includes(`--${flag}`);
}
return result;
}
/**
* Collect all tokens after --flag until the next --flag or end of args.
* Handles multi-word values like --name Foo Bar Version 1.
* Returns null if the flag is absent.
*/
function parseMultiwordArg(args, flag) {
const idx = args.indexOf(`--${flag}`);
if (idx === -1) return null;
const tokens = [];
for (let i = idx + 1; i < args.length; i++) {
if (args[i].startsWith('--')) break;
tokens.push(args[i]);
}
return tokens.length > 0 ? tokens.join(' ') : null;
}
// ─── CLI Router ───────────────────────────────────────────────────────────────
async function main() {
@@ -563,7 +526,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
args,
cwd,
raw,
parseNamedArgs,
error,
});
break;
@@ -935,7 +897,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
args,
cwd,
raw,
parseNamedArgs,
output: core.output,
error,
});
@@ -1015,7 +976,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
args,
cwd,
raw,
parseNamedArgs,
error,
});
break;

View File

@@ -1,5 +1,7 @@
'use strict';
const { createHub, ERROR_KINDS } = require('./command-routing-hub.cjs');
/**
* CJS Command Router Adapter Module
*
@@ -16,6 +18,41 @@ function routeCjsCommandFamily({
unsupported = {},
unknownMessage,
error,
cwd,
raw,
}) {
routeHubCommandFamily({
family: '__legacy_cjs_family__',
args,
subcommands,
handlers,
defaultSubcommand,
unsupported,
unknownMessage,
error,
cwd,
raw,
});
}
/**
* Hub-backed family router adapter.
*
* Deepens the command-topology seam by routing family handlers through
* CommandRoutingHub's typed Result contract instead of ad-hoc per-router
* lookup + error handling branches.
*/
function routeHubCommandFamily({
family,
args,
subcommands,
handlers,
defaultSubcommand,
unsupported = {},
unknownMessage,
error,
cwd,
raw,
}) {
const subcommand = args[1] || defaultSubcommand;
@@ -24,16 +61,58 @@ function routeCjsCommandFamily({
return;
}
const handler = subcommand ? handlers[subcommand] : null;
if (handler) {
handler();
const available = subcommands.filter((s) => !unsupported[s]);
const registryHandlers = Object.fromEntries(
Object.entries(handlers).map(([name, handler]) => [
name,
() => {
const result = handler();
if (result && typeof result === 'object' && Object.prototype.hasOwnProperty.call(result, 'ok')) {
return result;
}
return { ok: true, data: null };
},
]),
);
const hub = createHub({
cjsRegistry: { [family]: registryHandlers },
manifest: { [family]: available },
});
const result = hub.dispatch({
family,
subcommand,
args: args.slice(2),
cwd,
raw,
});
if (result.ok) return;
if (result.kind === ERROR_KINDS.UnknownCommand) {
error(unknownMessage(subcommand, available));
return;
}
if (result.kind === ERROR_KINDS.InvalidArgs || result.kind === ERROR_KINDS.HandlerRefusal) {
error(result.reason);
return;
}
error(result.message);
}
const available = subcommands.filter(s => !unsupported[s]);
error(unknownMessage(subcommand, available));
/**
* Projection helper for family routers that still declare SDK registry metadata
* but execute the CJS fallback path at this seam.
*
* Accepts variable argument shapes so routers can pass legacy projection tuples
* (`registryCommand`, `registryArgs`, `legacyArgs`, optional `rawFormatter`, `cjsFallback`).
*/
function cjsFallbackHandler(...projectionArgs) {
return projectionArgs[projectionArgs.length - 1];
}
module.exports = {
routeCjsCommandFamily,
routeHubCommandFamily,
cjsFallbackHandler,
};

View File

@@ -0,0 +1,55 @@
'use strict';
/**
* Command Argument Projection Module
*
* Shared helpers for command-family adapters to project argv tokens into
* typed named values and multi-word segments.
*/
/**
* Extract named --flag <value> pairs from an args array.
* Returns an object mapping flag names to their values (null if absent).
* Flags listed in `booleanFlags` are treated as booleans.
*
* @param {string[]} args
* @param {string[]} [valueFlags]
* @param {string[]} [booleanFlags]
* @returns {Record<string, string|boolean|null>}
*/
function parseNamedArgs(args, valueFlags = [], booleanFlags = []) {
const result = {};
for (const flag of valueFlags) {
const idx = args.indexOf(`--${flag}`);
result[flag] = idx !== -1 && args[idx + 1] !== undefined && !args[idx + 1].startsWith('--')
? args[idx + 1]
: null;
}
for (const flag of booleanFlags) {
result[flag] = args.includes(`--${flag}`);
}
return result;
}
/**
* Collect all tokens after --flag until the next --flag or end of args.
*
* @param {string[]} args
* @param {string} flag
* @returns {string|null}
*/
function parseMultiwordArg(args, flag) {
const idx = args.indexOf(`--${flag}`);
if (idx === -1) return null;
const tokens = [];
for (let i = idx + 1; i < args.length; i++) {
if (args[i].startsWith('--')) break;
tokens.push(args[i]);
}
return tokens.length > 0 ? tokens.join(' ') : null;
}
module.exports = {
parseNamedArgs,
parseMultiwordArg,
};

View File

@@ -2,6 +2,7 @@
const { INIT_SUBCOMMANDS } = require('./command-aliases.cjs');
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
const { parseNamedArgs } = require('./command-arg-projection.cjs');
/**
* Manifest-backed init subcommand router.
@@ -15,11 +16,7 @@ const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
* CJS-only subcommands: none.
* SDK-only (unsupported in CJS router): none.
*/
function routeInitCommand({ init, args, cwd, raw, parseNamedArgs, error }) {
function sdkHandler(_registryCommand, _registryArgs, _legacyArgs, cjsFallback) {
return cjsFallback;
}
function routeInitCommand({ init, args, cwd, raw, error }) {
routeCjsCommandFamily({
args,
subcommands: INIT_SUBCOMMANDS,
@@ -27,111 +24,31 @@ function routeInitCommand({ init, args, cwd, raw, parseNamedArgs, error }) {
error,
unknownMessage: (_subcommand, available) => `Unknown init workflow: ${_subcommand}\nAvailable: ${available.join(', ')}`,
handlers: {
'execute-phase': sdkHandler(
'init.execute-phase',
args.slice(2),
args.slice(1),
() => {
const { validate: epValidate, tdd: epTdd } = parseNamedArgs(args, [], ['validate', 'tdd']);
init.cmdInitExecutePhase(cwd, args[2], raw, { validate: epValidate, tdd: epTdd });
},
),
'plan-phase': sdkHandler(
'init.plan-phase',
args.slice(2),
args.slice(1),
() => {
const { validate: ppValidate, tdd: ppTdd } = parseNamedArgs(args, [], ['validate', 'tdd']);
init.cmdInitPlanPhase(cwd, args[2], raw, { validate: ppValidate, tdd: ppTdd });
},
),
'new-project': sdkHandler(
'init.new-project',
args.slice(2),
args.slice(1),
() => init.cmdInitNewProject(cwd, raw),
),
'new-milestone': sdkHandler(
'init.new-milestone',
args.slice(2),
args.slice(1),
() => init.cmdInitNewMilestone(cwd, raw),
),
quick: sdkHandler(
'init.quick',
args.slice(2),
args.slice(1),
() => init.cmdInitQuick(cwd, args.slice(2).join(' '), raw),
),
'ingest-docs': sdkHandler(
'init.ingest-docs',
args.slice(2),
args.slice(1),
() => init.cmdInitIngestDocs(cwd, raw),
),
resume: sdkHandler(
'init.resume',
args.slice(2),
args.slice(1),
() => init.cmdInitResume(cwd, raw),
),
'verify-work': sdkHandler(
'init.verify-work',
args.slice(2),
args.slice(1),
() => init.cmdInitVerifyWork(cwd, args[2], raw),
),
'phase-op': sdkHandler(
'init.phase-op',
args.slice(2),
args.slice(1),
() => init.cmdInitPhaseOp(cwd, args[2], raw),
),
todos: sdkHandler(
'init.todos',
args.slice(2),
args.slice(1),
() => init.cmdInitTodos(cwd, args[2], raw),
),
'milestone-op': sdkHandler(
'init.milestone-op',
args.slice(2),
args.slice(1),
() => init.cmdInitMilestoneOp(cwd, raw),
),
'map-codebase': sdkHandler(
'init.map-codebase',
args.slice(2),
args.slice(1),
() => init.cmdInitMapCodebase(cwd, raw),
),
progress: sdkHandler(
'init.progress',
args.slice(2),
args.slice(1),
() => init.cmdInitProgress(cwd, raw),
),
'execute-phase': () => {
const { validate: epValidate, tdd: epTdd } = parseNamedArgs(args, [], ['validate', 'tdd']);
init.cmdInitExecutePhase(cwd, args[2], raw, { validate: epValidate, tdd: epTdd });
},
'plan-phase': () => {
const { validate: ppValidate, tdd: ppTdd } = parseNamedArgs(args, [], ['validate', 'tdd']);
init.cmdInitPlanPhase(cwd, args[2], raw, { validate: ppValidate, tdd: ppTdd });
},
'new-project': () => init.cmdInitNewProject(cwd, raw),
'new-milestone': () => init.cmdInitNewMilestone(cwd, raw),
quick: () => init.cmdInitQuick(cwd, args.slice(2).join(' '), raw),
'ingest-docs': () => init.cmdInitIngestDocs(cwd, raw),
resume: () => init.cmdInitResume(cwd, raw),
'verify-work': () => init.cmdInitVerifyWork(cwd, args[2], raw),
'phase-op': () => init.cmdInitPhaseOp(cwd, args[2], raw),
todos: () => init.cmdInitTodos(cwd, args[2], raw),
'milestone-op': () => init.cmdInitMilestoneOp(cwd, raw),
'map-codebase': () => init.cmdInitMapCodebase(cwd, raw),
progress: () => init.cmdInitProgress(cwd, raw),
// Keep manager on CJS for now so runtime-specific command rendering
// (e.g. $gsd-* for codex) stays consistent with runtime-slash helpers.
manager: () => init.cmdInitManager(cwd, raw),
'new-workspace': sdkHandler(
'init.new-workspace',
args.slice(2),
args.slice(1),
() => init.cmdInitNewWorkspace(cwd, raw),
),
'list-workspaces': sdkHandler(
'init.list-workspaces',
args.slice(2),
args.slice(1),
() => init.cmdInitListWorkspaces(cwd, raw),
),
'remove-workspace': sdkHandler(
'init.remove-workspace',
args.slice(2),
args.slice(1),
() => init.cmdInitRemoveWorkspace(cwd, args[2], raw),
),
'new-workspace': () => init.cmdInitNewWorkspace(cwd, raw),
'list-workspaces': () => init.cmdInitListWorkspaces(cwd, raw),
'remove-workspace': () => init.cmdInitRemoveWorkspace(cwd, args[2], raw),
},
});
}

View File

@@ -21,10 +21,6 @@ const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
* CJS-only subcommands: none.
*/
function routePhasesCommand({ phase, milestone, args, cwd, raw, error }) {
function sdkHandler(_registryCommand, _registryArgs, _legacyArgs, cjsFallback) {
return cjsFallback;
}
routeCjsCommandFamily({
args,
// Exclude 'archive' — it's SDK-only and not supported in CJS. Excluding
@@ -34,27 +30,17 @@ function routePhasesCommand({ phase, milestone, args, cwd, raw, error }) {
error,
unknownMessage: (_subcommand, available) => `Unknown phases subcommand. Available: ${available.join(', ')}`,
handlers: {
list: sdkHandler(
'phases.list',
args.slice(2),
args.slice(1),
() => {
const typeIndex = args.indexOf('--type');
const phaseIndex = args.indexOf('--phase');
const options = {
type: typeIndex !== -1 ? args[typeIndex + 1] : null,
phase: phaseIndex !== -1 ? args[phaseIndex + 1] : null,
includeArchived: args.includes('--include-archived'),
};
phase.cmdPhasesList(cwd, options, raw);
},
),
clear: sdkHandler(
'phases.clear',
args.slice(2),
args.slice(1),
() => milestone.cmdPhasesClear(cwd, raw, args.slice(2)),
),
list: () => {
const typeIndex = args.indexOf('--type');
const phaseIndex = args.indexOf('--phase');
const options = {
type: typeIndex !== -1 ? args[typeIndex + 1] : null,
phase: phaseIndex !== -1 ? args[phaseIndex + 1] : null,
includeArchived: args.includes('--include-archived'),
};
phase.cmdPhasesList(cwd, options, raw);
},
clear: () => milestone.cmdPhasesClear(cwd, raw, args.slice(2)),
},
});
}

View File

@@ -16,10 +16,6 @@ const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
* SDK-only (unsupported in CJS router): none.
*/
function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) {
function sdkHandler(_registryCommand, _registryArgs, _legacyArgs, cjsFallback) {
return cjsFallback;
}
routeCjsCommandFamily({
args,
subcommands: ROADMAP_SUBCOMMANDS,
@@ -27,30 +23,10 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) {
error,
unknownMessage: (_subcommand, available) => `Unknown roadmap subcommand. Available: ${available.join(', ')}`,
handlers: {
'get-phase': sdkHandler(
'roadmap.get-phase',
args.slice(2),
args.slice(1),
() => roadmap.cmdRoadmapGetPhase(cwd, args[2], raw),
),
analyze: sdkHandler(
'roadmap.analyze',
args.slice(2),
args.slice(1),
() => roadmap.cmdRoadmapAnalyze(cwd, raw),
),
'update-plan-progress': sdkHandler(
'roadmap.update-plan-progress',
args.slice(2),
args.slice(1),
() => roadmap.cmdRoadmapUpdatePlanProgress(cwd, args[2], raw),
),
'annotate-dependencies': sdkHandler(
'roadmap.annotate-dependencies',
args.slice(2),
args.slice(1),
() => roadmap.cmdRoadmapAnnotateDependencies(cwd, args[2], raw),
),
'get-phase': () => roadmap.cmdRoadmapGetPhase(cwd, args[2], raw),
analyze: () => roadmap.cmdRoadmapAnalyze(cwd, raw),
'update-plan-progress': () => roadmap.cmdRoadmapUpdatePlanProgress(cwd, args[2], raw),
'annotate-dependencies': () => roadmap.cmdRoadmapAnnotateDependencies(cwd, args[2], raw),
},
});
}

View File

@@ -1,7 +1,8 @@
'use strict';
const { STATE_SUBCOMMANDS } = require('./command-aliases.cjs');
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
const { routeHubCommandFamily, cjsFallbackHandler } = require('./cjs-command-router-adapter.cjs');
const { parseNamedArgs } = require('./command-arg-projection.cjs');
/**
* Manifest-backed state subcommand router.
@@ -14,7 +15,7 @@ const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
* for workstream requests; subprocess is disabled in the sync bridge worker).
* - Any command when the SDK is not available (build not present).
*/
function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
function routeStateCommand({ state, args, cwd, raw, error }) {
const parsePlans = (plans) => {
const parsedPlans = plans == null ? null : Number.parseInt(plans, 10);
if (plans != null && Number.isNaN(parsedPlans)) {
@@ -24,11 +25,8 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
return parsedPlans;
};
function sdkHandler(_registryCommand, _registryArgs, _legacyArgs, _rawFormatter, cjsFallback) {
return cjsFallback;
}
routeCjsCommandFamily({
routeHubCommandFamily({
family: 'state',
args,
subcommands: ['load', 'complete-phase', ...STATE_SUBCOMMANDS.filter((s) => s !== 'load')],
defaultSubcommand: 'load',
@@ -36,37 +34,39 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
'add-roadmap-evolution': 'state add-roadmap-evolution is SDK-only. Use: gsd-sdk query state.add-roadmap-evolution ...',
},
error,
cwd,
raw,
unknownMessage: (subcommand, available) => `Unknown state subcommand: "${subcommand}". Available: ${available.join(', ')}`,
handlers: {
load: sdkHandler(
load: cjsFallbackHandler(
'state.load',
[],
args.slice(1),
null,
() => state.cmdStateLoad(cwd, raw),
),
json: sdkHandler(
json: cjsFallbackHandler(
'state.json',
[],
args.slice(1),
null,
() => state.cmdStateJson(cwd, raw),
),
get: sdkHandler(
get: cjsFallbackHandler(
'state.get',
args.slice(2),
args.slice(1),
null,
() => state.cmdStateGet(cwd, args[2], raw),
),
update: sdkHandler(
update: cjsFallbackHandler(
'state.update',
args.slice(2),
args.slice(1),
null,
() => state.cmdStateUpdate(cwd, args[2], args[3]),
),
patch: sdkHandler(
patch: cjsFallbackHandler(
'state.patch',
args.slice(2),
args.slice(1),
@@ -83,14 +83,14 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
state.cmdStatePatch(cwd, patches, raw);
},
),
'advance-plan': sdkHandler(
'advance-plan': cjsFallbackHandler(
'state.advance-plan',
[],
args.slice(1),
null,
() => state.cmdStateAdvancePlan(cwd, raw),
),
'record-metric': sdkHandler(
'record-metric': cjsFallbackHandler(
'state.record-metric',
args.slice(2),
args.slice(1),
@@ -100,14 +100,14 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
state.cmdStateRecordMetric(cwd, { phase: p, plan, duration, tasks, files }, raw);
},
),
'update-progress': sdkHandler(
'update-progress': cjsFallbackHandler(
'state.update-progress',
[],
args.slice(1),
null,
() => state.cmdStateUpdateProgress(cwd, raw),
),
'add-decision': sdkHandler(
'add-decision': cjsFallbackHandler(
'state.add-decision',
args.slice(2),
args.slice(1),
@@ -117,7 +117,7 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
state.cmdStateAddDecision(cwd, { phase: p, summary, summary_file, rationale: rationale || '', rationale_file }, raw);
},
),
'add-blocker': sdkHandler(
'add-blocker': cjsFallbackHandler(
'state.add-blocker',
args.slice(2),
args.slice(1),
@@ -127,14 +127,14 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
state.cmdStateAddBlocker(cwd, { text, text_file }, raw);
},
),
'resolve-blocker': sdkHandler(
'resolve-blocker': cjsFallbackHandler(
'state.resolve-blocker',
args.slice(2),
args.slice(1),
null,
() => state.cmdStateResolveBlocker(cwd, parseNamedArgs(args, ['text']).text, raw),
),
'record-session': sdkHandler(
'record-session': cjsFallbackHandler(
'state.record-session',
args.slice(2),
args.slice(1),
@@ -144,7 +144,7 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
state.cmdStateRecordSession(cwd, { stopped_at, resume_file: resume_file || 'None' }, raw);
},
),
'begin-phase': sdkHandler(
'begin-phase': cjsFallbackHandler(
'state.begin-phase',
args.slice(2),
args.slice(1),
@@ -154,7 +154,7 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
state.cmdStateBeginPhase(cwd, p, name, parsePlans(plans), raw);
},
),
'signal-waiting': sdkHandler(
'signal-waiting': cjsFallbackHandler(
'state.signal-waiting',
args.slice(2),
args.slice(1),
@@ -164,14 +164,14 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
state.cmdSignalWaiting(cwd, type, question, options, p, raw);
},
),
'signal-resume': sdkHandler(
'signal-resume': cjsFallbackHandler(
'state.signal-resume',
[],
args.slice(1),
null,
() => state.cmdSignalResume(cwd, raw),
),
'planned-phase': sdkHandler(
'planned-phase': cjsFallbackHandler(
'state.planned-phase',
args.slice(2),
args.slice(1),
@@ -181,14 +181,14 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
state.cmdStatePlannedPhase(cwd, p, parsePlans(plans), raw);
},
),
validate: sdkHandler(
validate: cjsFallbackHandler(
'state.validate',
[],
args.slice(1),
null,
() => state.cmdStateValidate(cwd, raw),
),
sync: sdkHandler(
sync: cjsFallbackHandler(
'state.sync',
args.slice(2),
args.slice(1),
@@ -198,7 +198,7 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
state.cmdStateSync(cwd, { verify }, raw);
},
),
prune: sdkHandler(
prune: cjsFallbackHandler(
'state.prune',
args.slice(2),
args.slice(1),
@@ -213,7 +213,7 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
const { phase: p } = parseNamedArgs(args, ['phase']);
state.cmdStateCompletePhase(cwd, raw, p || args[2]);
},
'milestone-switch': sdkHandler(
'milestone-switch': cjsFallbackHandler(
'state.milestone-switch',
args.slice(2),
args.slice(1),

View File

@@ -3,6 +3,7 @@
const { VALIDATE_SUBCOMMANDS } = require('./command-aliases.cjs');
const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs');
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
const { parseNamedArgs } = require('./command-arg-projection.cjs');
/**
* Manifest-backed validate subcommand router.
@@ -20,11 +21,7 @@ const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
*
* SDK-only (unsupported in CJS router): none.
*/
function routeValidateCommand({ verify, args, cwd, raw, parseNamedArgs, output: outputFn, error }) {
function sdkHandler(_registryCommand, _registryArgs, _legacyArgs, cjsFallback) {
return cjsFallback;
}
function routeValidateCommand({ verify, args, cwd, raw, output: outputFn, error }) {
routeCjsCommandFamily({
args,
subcommands: VALIDATE_SUBCOMMANDS,
@@ -32,12 +29,7 @@ function routeValidateCommand({ verify, args, cwd, raw, parseNamedArgs, output:
error,
unknownMessage: (_subcommand, available) => `Unknown validate subcommand. Available: ${available.join(', ')}`,
handlers: {
consistency: sdkHandler(
'validate.consistency',
args.slice(2),
args.slice(1),
() => verify.cmdValidateConsistency(cwd, raw),
),
consistency: () => verify.cmdValidateConsistency(cwd, raw),
// Keep health on CJS for now so fix hints are rendered via runtime-slash
// helpers (codex expects $gsd-* command shape).
health: () => {
@@ -45,12 +37,7 @@ function routeValidateCommand({ verify, args, cwd, raw, parseNamedArgs, output:
const backfillFlag = args.includes('--backfill');
verify.cmdValidateHealth(cwd, { repair: repairFlag, backfill: backfillFlag }, raw);
},
agents: sdkHandler(
'validate.agents',
args.slice(2),
args.slice(1),
() => verify.cmdValidateAgents(cwd, raw),
),
agents: () => verify.cmdValidateAgents(cwd, raw),
// context: CJS-only — complex inline logic using classifyContextUtilization
// with custom output formatting that has no direct SDK counterpart.
context: () => {

View File

@@ -16,10 +16,6 @@ const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
* SDK-only (unsupported in CJS router): none.
*/
function routeVerifyCommand({ verify, args, cwd, raw, error }) {
function sdkHandler(_registryCommand, _registryArgs, _legacyArgs, cjsFallback) {
return cjsFallback;
}
routeCjsCommandFamily({
args,
subcommands: VERIFY_SUBCOMMANDS,
@@ -27,53 +23,18 @@ function routeVerifyCommand({ verify, args, cwd, raw, error }) {
error,
unknownMessage: (_subcommand, available) => `Unknown verify subcommand. Available: ${available.join(', ')}`,
handlers: {
'plan-structure': sdkHandler(
'verify.plan-structure',
args.slice(2),
args.slice(1),
() => verify.cmdVerifyPlanStructure(cwd, args[2], raw),
),
'phase-completeness': sdkHandler(
'verify.phase-completeness',
args.slice(2),
args.slice(1),
() => verify.cmdVerifyPhaseCompleteness(cwd, args[2], raw),
),
references: sdkHandler(
'verify.references',
args.slice(2),
args.slice(1),
() => verify.cmdVerifyReferences(cwd, args[2], raw),
),
commits: sdkHandler(
'verify.commits',
args.slice(2),
args.slice(1),
() => verify.cmdVerifyCommits(cwd, args.slice(2), raw),
),
artifacts: sdkHandler(
'verify.artifacts',
args.slice(2),
args.slice(1),
() => verify.cmdVerifyArtifacts(cwd, args[2], raw),
),
'key-links': sdkHandler(
'verify.key-links',
args.slice(2),
args.slice(1),
() => verify.cmdVerifyKeyLinks(cwd, args[2], raw),
),
'schema-drift': sdkHandler(
'verify.schema-drift',
args.slice(2),
args.slice(1),
() => {
const rest = args.slice(2);
const skipFlag = rest.includes('--skip');
const phaseArg = rest.find((arg) => !arg.startsWith('-'));
verify.cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw);
},
),
'plan-structure': () => verify.cmdVerifyPlanStructure(cwd, args[2], raw),
'phase-completeness': () => verify.cmdVerifyPhaseCompleteness(cwd, args[2], raw),
references: () => verify.cmdVerifyReferences(cwd, args[2], raw),
commits: () => verify.cmdVerifyCommits(cwd, args.slice(2), raw),
artifacts: () => verify.cmdVerifyArtifacts(cwd, args[2], raw),
'key-links': () => verify.cmdVerifyKeyLinks(cwd, args[2], raw),
'schema-drift': () => {
const rest = args.slice(2);
const skipFlag = rest.includes('--skip');
const phaseArg = rest.find((arg) => !arg.startsWith('-'));
verify.cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw);
},
// verify codebase-drift dispatches direct to CJS — drift is out-of-seam
// per ADR/PRD 3524 §3 / L160 (CJS-only by design). Routing through
// sdkHandler would re-enter the SDK bridge, and Phase 6's removed

View File

@@ -55,6 +55,7 @@
},
"scripts": {
"check:env": "bash scripts/check-env.sh",
"check:alias-drift": "node scripts/check-alias-drift.cjs",
"check:integrity": "./scripts/check-npm-integrity.sh",
"build": "npm run build:hooks",
"build:hooks": "node scripts/build-hooks.js",

View File

@@ -0,0 +1,108 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
const aliasesPath = path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.cjs');
function fail(message) {
process.stderr.write(`${message}\n`);
process.exit(1);
}
function ensureArray(value, name) {
if (!Array.isArray(value)) {
fail(`check:alias-drift: expected ${name} to be an array`);
}
}
function assertNoDuplicates(values, label) {
const seen = new Set();
for (const value of values) {
if (seen.has(value)) {
fail(`check:alias-drift: duplicate ${label} value "${value}"`);
}
seen.add(value);
}
}
if (!fs.existsSync(aliasesPath)) {
fail(`check:alias-drift: missing ${path.relative(ROOT, aliasesPath)}`);
}
const aliases = require(aliasesPath);
const families = [
{
commandAliases: 'STATE_COMMAND_ALIASES',
subcommands: 'STATE_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'state-command-router.cjs'),
},
{
commandAliases: 'VERIFY_COMMAND_ALIASES',
subcommands: 'VERIFY_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'verify-command-router.cjs'),
},
{
commandAliases: 'INIT_COMMAND_ALIASES',
subcommands: 'INIT_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'init-command-router.cjs'),
},
{
commandAliases: 'PHASE_COMMAND_ALIASES',
subcommands: 'PHASE_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'phase-command-router.cjs'),
},
{
commandAliases: 'PHASES_COMMAND_ALIASES',
subcommands: 'PHASES_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'phases-command-router.cjs'),
},
{
commandAliases: 'VALIDATE_COMMAND_ALIASES',
subcommands: 'VALIDATE_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'validate-command-router.cjs'),
},
{
commandAliases: 'ROADMAP_COMMAND_ALIASES',
subcommands: 'ROADMAP_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'roadmap-command-router.cjs'),
},
];
for (const family of families) {
const commandAliases = aliases[family.commandAliases];
const subcommands = aliases[family.subcommands];
ensureArray(commandAliases, family.commandAliases);
ensureArray(subcommands, family.subcommands);
const derivedSubcommands = commandAliases.map((entry) => entry && entry.subcommand);
assertNoDuplicates(derivedSubcommands, `${family.commandAliases}.subcommand`);
if (derivedSubcommands.length !== subcommands.length) {
fail(
`check:alias-drift: ${family.subcommands} length ${subcommands.length} does not match ` +
`${family.commandAliases} length ${derivedSubcommands.length}`,
);
}
for (let i = 0; i < derivedSubcommands.length; i++) {
if (derivedSubcommands[i] !== subcommands[i]) {
fail(
`check:alias-drift: ${family.subcommands}[${i}] = "${subcommands[i]}" ` +
`does not match ${family.commandAliases}[${i}].subcommand = "${derivedSubcommands[i]}"`,
);
}
}
const routerSource = fs.readFileSync(family.routerPath, 'utf8');
if (!routerSource.includes(family.subcommands)) {
fail(
`check:alias-drift: ${path.relative(ROOT, family.routerPath)} does not reference ${family.subcommands}`,
);
}
}
process.stdout.write('check:alias-drift ok\n');

View File

@@ -0,0 +1,191 @@
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const { routeCjsCommandFamily, routeHubCommandFamily } = require('../get-shit-done/bin/lib/cjs-command-router-adapter.cjs');
const { makeInvalidArgs } = require('../get-shit-done/bin/lib/command-routing-hub.cjs');
describe('cjs-command-router-adapter routeHubCommandFamily', () => {
test('routes known subcommand handler through the hub', () => {
let calls = 0;
let errorMessage = null;
routeHubCommandFamily({
family: 'unit',
args: ['unit', 'ok'],
subcommands: ['ok'],
handlers: {
ok: () => {
calls += 1;
},
},
unknownMessage: (subcommand, available) => `Unknown ${subcommand}. Available: ${available.join(', ')}`,
error: (message) => {
errorMessage = message;
},
cwd: '/tmp/proj',
raw: false,
});
assert.equal(calls, 1);
assert.equal(errorMessage, null);
});
test('maps unknown subcommands via unknownMessage and filtered availability', () => {
let errorMessage = null;
routeHubCommandFamily({
family: 'unit',
args: ['unit', 'missing'],
subcommands: ['ok', 'legacy'],
unsupported: { legacy: 'legacy disabled' },
handlers: { ok: () => {} },
unknownMessage: (subcommand, available) => `Unknown ${subcommand}. Available: ${available.join(', ')}`,
error: (message) => {
errorMessage = message;
},
cwd: '/tmp/proj',
raw: false,
});
assert.equal(errorMessage, 'Unknown missing. Available: ok');
});
test('returns unsupported subcommand error before dispatch', () => {
let errorMessage = null;
routeHubCommandFamily({
family: 'unit',
args: ['unit', 'legacy'],
subcommands: ['ok', 'legacy'],
unsupported: { legacy: 'legacy disabled' },
handlers: { ok: () => {} },
unknownMessage: () => 'should not be used',
error: (message) => {
errorMessage = message;
},
cwd: '/tmp/proj',
raw: false,
});
assert.equal(errorMessage, 'legacy disabled');
});
test('projects InvalidArgs result reason via error callback', () => {
let errorMessage = null;
routeHubCommandFamily({
family: 'unit',
args: ['unit', 'invalid'],
subcommands: ['invalid'],
handlers: {
invalid: () => makeInvalidArgs('--phase', '--phase must be an integer'),
},
unknownMessage: () => 'should not be used',
error: (message) => {
errorMessage = message;
},
cwd: '/tmp/proj',
raw: false,
});
assert.equal(errorMessage, '--phase must be an integer');
});
test('projects thrown handler exceptions as HandlerFailure message', () => {
let errorMessage = null;
routeHubCommandFamily({
family: 'unit',
args: ['unit', 'boom'],
subcommands: ['boom'],
handlers: {
boom: () => {
throw new Error('boom');
},
},
unknownMessage: () => 'should not be used',
error: (message) => {
errorMessage = message;
},
cwd: '/tmp/proj',
raw: false,
});
assert.equal(errorMessage, 'boom');
});
});
describe('cjs-command-router-adapter routeCjsCommandFamily', () => {
test('routes known subcommand handler via the legacy adapter', () => {
let calls = 0;
let errorMessage = null;
routeCjsCommandFamily({
args: ['unit', 'ok'],
subcommands: ['ok'],
handlers: {
ok: () => {
calls += 1;
},
},
unknownMessage: (subcommand, available) => `Unknown ${subcommand}. Available: ${available.join(', ')}`,
error: (message) => {
errorMessage = message;
},
cwd: '/tmp/proj',
raw: false,
});
assert.equal(calls, 1);
assert.equal(errorMessage, null);
});
test('honors defaultSubcommand when args[1] is absent', () => {
let calls = 0;
let errorMessage = null;
routeCjsCommandFamily({
args: ['unit'],
subcommands: ['load'],
defaultSubcommand: 'load',
handlers: {
load: () => {
calls += 1;
},
},
unknownMessage: (subcommand, available) => `Unknown ${subcommand}. Available: ${available.join(', ')}`,
error: (message) => {
errorMessage = message;
},
cwd: '/tmp/proj',
raw: false,
});
assert.equal(calls, 1);
assert.equal(errorMessage, null);
});
test('converts thrown handler exceptions into error callback messages', () => {
let errorMessage = null;
routeCjsCommandFamily({
args: ['unit', 'boom'],
subcommands: ['boom'],
handlers: {
boom: () => {
throw new Error('boom');
},
},
unknownMessage: () => 'should not be used',
error: (message) => {
errorMessage = message;
},
cwd: '/tmp/proj',
raw: false,
});
assert.equal(errorMessage, 'boom');
});
});