diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index 0c66073546..a2e6f1f930 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -246,6 +246,7 @@ "cli_modules": [ "artifacts.cjs", "audit.cjs", + "command-aliases.generated.cjs", "commands.cjs", "config-schema.cjs", "config.cjs", @@ -258,23 +259,30 @@ "gap-checker.cjs", "graphify.cjs", "gsd2-import.cjs", + "init-command-router.cjs", "init.cjs", "install-profiles.cjs", "intel.cjs", "learnings.cjs", "milestone.cjs", "model-profiles.cjs", + "phase-command-router.cjs", "phase.cjs", + "phases-command-router.cjs", "planning-workspace.cjs", "profile-output.cjs", "profile-pipeline.cjs", + "roadmap-command-router.cjs", "roadmap.cjs", "schema-detect.cjs", "secrets.cjs", "security.cjs", + "state-command-router.cjs", "state.cjs", "template.cjs", "uat.cjs", + "validate-command-router.cjs", + "verify-command-router.cjs", "verify.cjs", "workstream.cjs" ], diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index 65dcb8d8d7..9d4b9851d2 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -348,7 +348,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t --- -## CLI Modules (33 shipped) +## CLI Modules (41 shipped) Full listing: `get-shit-done/bin/lib/*.cjs`. @@ -356,6 +356,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`. |--------|----------------| | `artifacts.cjs` | Canonical artifact registry — known `.planning/` root file names; used by `gsd-health` W019 lint | | `audit.cjs` | Audit dispatch, audit open sessions, audit storage helpers | +| `command-aliases.generated.cjs` | Generated CJS alias/subcommand metadata for manifest-backed family routers | | `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` | @@ -368,23 +369,30 @@ Full listing: `get-shit-done/bin/lib/*.cjs`. | `gap-checker.cjs` | Post-planning gap analysis (#2493): unified REQUIREMENTS.md + CONTEXT.md decisions vs PLAN.md coverage report (`gsd-tools gap-analysis`) | | `graphify.cjs` | Knowledge-graph build/query/status/diff for `/gsd-graphify` | | `gsd2-import.cjs` | External-plan ingest for `/gsd-from-gsd2` | +| `init-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools init` | | `init.cjs` | Compound context loading for each workflow type | | `install-profiles.cjs` | Install profile allowlist + skill staging for `--minimal` install (#2762); single source of truth for which `gsd-*` skills/agents land in runtime config dirs | | `intel.cjs` | Codebase intel store backing `/gsd-intel` and `gsd-intel-updater` | | `learnings.cjs` | Cross-phase learnings extraction for `/gsd-extract-learnings` | | `milestone.cjs` | Milestone archival, requirements marking | | `model-profiles.cjs` | Model profile resolution table (authoritative profile data) | +| `phase-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools phase` | | `phase.cjs` | Phase directory operations, decimal numbering, plan indexing | +| `phases-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools phases` | | `planning-workspace.cjs` | Planning path/workstream seam (`planningDir`, `planningPaths`, active-workstream routing, `.planning/.lock` orchestration) | | `profile-output.cjs` | Profile rendering, USER-PROFILE.md and dev-preferences.md generation | | `profile-pipeline.cjs` | User behavioral profiling data pipeline, session file scanning | +| `roadmap-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools roadmap` | | `roadmap.cjs` | ROADMAP.md parsing, phase extraction, plan progress | | `schema-detect.cjs` | Schema-drift detection for ORM patterns (Prisma, Drizzle, etc.) | | `secrets.cjs` | Secret-config masking convention (`****`) for integration keys managed by `/gsd-settings-integrations` — keeps plaintext out of `config-set` output | | `security.cjs` | Path traversal prevention, prompt injection detection, safe JSON/shell helpers | +| `state-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools state` | | `state.cjs` | STATE.md parsing, updating, progression, metrics | | `template.cjs` | Template selection and filling with variable substitution | | `uat.cjs` | UAT file parsing, verification debt tracking, audit-uat support | +| `validate-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools validate` | +| `verify-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools verify` | | `verify.cjs` | Plan structure, phase completeness, reference, commit validation | | `workstream.cjs` | Workstream CRUD, migration, session-scoped active pointer | diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index cbb6cd92e2..23db8d6725 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -190,6 +190,13 @@ const workstream = require('./lib/workstream.cjs'); const docs = require('./lib/docs.cjs'); const learnings = require('./lib/learnings.cjs'); const gapChecker = require('./lib/gap-checker.cjs'); +const { routeStateCommand } = require('./lib/state-command-router.cjs'); +const { routeVerifyCommand } = require('./lib/verify-command-router.cjs'); +const { routeInitCommand } = require('./lib/init-command-router.cjs'); +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'); // ─── Arg parsing helpers ────────────────────────────────────────────────────── @@ -430,73 +437,14 @@ function extractField(obj, fieldPath) { async function runCommand(command, args, cwd, raw, defaultValue) { switch (command) { case 'state': { - const subcommand = args[1]; - if (subcommand === 'json') { - state.cmdStateJson(cwd, raw); - } else if (subcommand === 'update') { - state.cmdStateUpdate(cwd, args[2], args[3]); - } else if (subcommand === 'get') { - state.cmdStateGet(cwd, args[2], raw); - } else if (subcommand === 'patch') { - const patches = {}; - for (let i = 2; i < args.length; i += 2) { - const key = args[i].replace(/^--/, ''); - const value = args[i + 1]; - if (key && value !== undefined) { - patches[key] = value; - } - } - state.cmdStatePatch(cwd, patches, raw); - } else if (subcommand === 'advance-plan') { - state.cmdStateAdvancePlan(cwd, raw); - } else if (subcommand === 'record-metric') { - const { phase: p, plan, duration, tasks, files } = parseNamedArgs(args, ['phase', 'plan', 'duration', 'tasks', 'files']); - state.cmdStateRecordMetric(cwd, { phase: p, plan, duration, tasks, files }, raw); - } else if (subcommand === 'update-progress') { - state.cmdStateUpdateProgress(cwd, raw); - } else if (subcommand === 'add-decision') { - const { phase: p, summary, 'summary-file': summary_file, rationale, 'rationale-file': rationale_file } = parseNamedArgs(args, ['phase', 'summary', 'summary-file', 'rationale', 'rationale-file']); - state.cmdStateAddDecision(cwd, { phase: p, summary, summary_file, rationale: rationale || '', rationale_file }, raw); - } else if (subcommand === 'add-blocker') { - const { text, 'text-file': text_file } = parseNamedArgs(args, ['text', 'text-file']); - state.cmdStateAddBlocker(cwd, { text, text_file }, raw); - } else if (subcommand === 'resolve-blocker') { - state.cmdStateResolveBlocker(cwd, parseNamedArgs(args, ['text']).text, raw); - } else if (subcommand === 'record-session') { - const { 'stopped-at': stopped_at, 'resume-file': resume_file } = parseNamedArgs(args, ['stopped-at', 'resume-file']); - state.cmdStateRecordSession(cwd, { stopped_at, resume_file: resume_file || 'None' }, raw); - } else if (subcommand === 'begin-phase') { - const { phase: p, name, plans } = parseNamedArgs(args, ['phase', 'name', 'plans']); - state.cmdStateBeginPhase(cwd, p, name, plans !== null ? parseInt(plans, 10) : null, raw); - } else if (subcommand === 'signal-waiting') { - const { type, question, options, phase: p } = parseNamedArgs(args, ['type', 'question', 'options', 'phase']); - state.cmdSignalWaiting(cwd, type, question, options, p, raw); - } else if (subcommand === 'signal-resume') { - state.cmdSignalResume(cwd, raw); - } else if (subcommand === 'planned-phase') { - const { phase: p, name, plans } = parseNamedArgs(args, ['phase', 'name', 'plans']); - state.cmdStatePlannedPhase(cwd, p, plans !== null ? parseInt(plans, 10) : null, raw); - } else if (subcommand === 'validate') { - state.cmdStateValidate(cwd, raw); - } else if (subcommand === 'sync') { - const { verify } = parseNamedArgs(args, [], ['verify']); - state.cmdStateSync(cwd, { verify }, raw); - } else if (subcommand === 'prune') { - const { 'keep-recent': keepRecent, 'dry-run': dryRun } = parseNamedArgs(args, ['keep-recent'], ['dry-run']); - state.cmdStatePrune(cwd, { keepRecent: keepRecent || '3', dryRun: !!dryRun }, raw); - } else if (subcommand === 'complete-phase') { - state.cmdStateCompletePhase(cwd, raw); - } else if (subcommand === 'milestone-switch') { - // Bug #2630: reset STATE.md frontmatter + Current Position for new milestone. - // NB: the flag is `--milestone`, not `--version` — gsd-tools reserves - // `--version` as a globally-invalid help flag (see NEVER_VALID_FLAGS above). - const { milestone, name } = parseNamedArgs(args, ['milestone', 'name']); - state.cmdStateMilestoneSwitch(cwd, milestone, name, raw); - } else if (subcommand === undefined || subcommand === 'load') { - state.cmdStateLoad(cwd, raw); - } else { - error(`Unknown state subcommand: "${subcommand}". Available: load, json, get, patch, update, advance-plan, record-metric, update-progress, add-decision, add-blocker, resolve-blocker, record-session, begin-phase, signal-waiting, signal-resume, planned-phase, validate, sync, prune, complete-phase, milestone-switch`); - } + routeStateCommand({ + state, + args, + cwd, + raw, + parseNamedArgs, + error, + }); break; } @@ -590,27 +538,13 @@ async function runCommand(command, args, cwd, raw, defaultValue) { } case 'verify': { - const subcommand = args[1]; - if (subcommand === 'plan-structure') { - verify.cmdVerifyPlanStructure(cwd, args[2], raw); - } else if (subcommand === 'phase-completeness') { - verify.cmdVerifyPhaseCompleteness(cwd, args[2], raw); - } else if (subcommand === 'references') { - verify.cmdVerifyReferences(cwd, args[2], raw); - } else if (subcommand === 'commits') { - verify.cmdVerifyCommits(cwd, args.slice(2), raw); - } else if (subcommand === 'artifacts') { - verify.cmdVerifyArtifacts(cwd, args[2], raw); - } else if (subcommand === 'key-links') { - verify.cmdVerifyKeyLinks(cwd, args[2], raw); - } else if (subcommand === 'schema-drift') { - const skipFlag = args.includes('--skip'); - verify.cmdVerifySchemaDrift(cwd, args[2], skipFlag, raw); - } else if (subcommand === 'codebase-drift') { - verify.cmdVerifyCodebaseDrift(cwd, raw); - } else { - error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, schema-drift, codebase-drift'); - } + routeVerifyCommand({ + verify, + args, + cwd, + raw, + error, + }); break; } @@ -680,37 +614,25 @@ async function runCommand(command, args, cwd, raw, defaultValue) { } case 'phases': { - const subcommand = args[1]; - if (subcommand === '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); - } else if (subcommand === 'clear') { - milestone.cmdPhasesClear(cwd, raw, args.slice(2)); - } else { - error('Unknown phases subcommand. Available: list, clear'); - } + routePhasesCommand({ + phase, + milestone, + args, + cwd, + raw, + error, + }); break; } case 'roadmap': { - const subcommand = args[1]; - if (subcommand === 'get-phase') { - roadmap.cmdRoadmapGetPhase(cwd, args[2], raw); - } else if (subcommand === 'analyze') { - roadmap.cmdRoadmapAnalyze(cwd, raw); - } else if (subcommand === 'update-plan-progress') { - roadmap.cmdRoadmapUpdatePlanProgress(cwd, args[2], raw); - } else if (subcommand === 'annotate-dependencies') { - roadmap.cmdRoadmapAnnotateDependencies(cwd, args[2], raw); - } else { - error('Unknown roadmap subcommand. Available: get-phase, analyze, update-plan-progress, annotate-dependencies'); - } + routeRoadmapCommand({ + roadmap, + args, + cwd, + raw, + error, + }); break; } @@ -732,42 +654,13 @@ async function runCommand(command, args, cwd, raw, defaultValue) { } case 'phase': { - const subcommand = args[1]; - if (subcommand === 'next-decimal') { - phase.cmdPhaseNextDecimal(cwd, args[2], raw); - } else if (subcommand === 'add') { - const idIdx = args.indexOf('--id'); - let customId = null; - const descArgs = []; - for (let i = 2; i < args.length; i++) { - if (args[i] === '--id' && i + 1 < args.length) { - customId = args[i + 1]; - i++; // skip value - } else { - descArgs.push(args[i]); - } - } - phase.cmdPhaseAdd(cwd, descArgs.join(' '), raw, customId); - } else if (subcommand === 'add-batch') { - // Accepts JSON array of descriptions via --descriptions '[...]' or positional args - const descFlagIdx = args.indexOf('--descriptions'); - let descriptions; - if (descFlagIdx !== -1 && args[descFlagIdx + 1]) { - try { descriptions = JSON.parse(args[descFlagIdx + 1]); } catch (e) { error('--descriptions must be a JSON array'); } - } else { - descriptions = args.slice(2).filter(a => a !== '--raw'); - } - phase.cmdPhaseAddBatch(cwd, descriptions, raw); - } else if (subcommand === 'insert') { - phase.cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw); - } else if (subcommand === 'remove') { - const forceFlag = args.includes('--force'); - phase.cmdPhaseRemove(cwd, args[2], { force: forceFlag }, raw); - } else if (subcommand === 'complete') { - phase.cmdPhaseComplete(cwd, args[2], raw); - } else { - error('Unknown phase subcommand. Available: next-decimal, add, add-batch, insert, remove, complete'); - } + routePhaseCommand({ + phase, + args, + cwd, + raw, + error, + }); break; } @@ -784,58 +677,15 @@ async function runCommand(command, args, cwd, raw, defaultValue) { } case 'validate': { - const subcommand = args[1]; - if (subcommand === 'consistency') { - verify.cmdValidateConsistency(cwd, raw); - } else if (subcommand === 'health') { - const repairFlag = args.includes('--repair'); - const backfillFlag = args.includes('--backfill'); - verify.cmdValidateHealth(cwd, { repair: repairFlag, backfill: backfillFlag }, raw); - } else if (subcommand === 'agents') { - verify.cmdValidateAgents(cwd, raw); - } else if (subcommand === 'context') { - // The model self-reports tokensUsed and contextWindow — the SDK has - // no privileged access to either. Recommendation copy lives here - // (the renderer), not in the classifier, so it can change without - // re-validating the math layer. - const opts = parseNamedArgs(args, ['tokens-used', 'context-window']); - if (opts['tokens-used'] === null) { - error('--tokens-used is required for `validate context`'); - break; - } - if (opts['context-window'] === null) { - error('--context-window is required for `validate context`'); - break; - } - const { classifyContextUtilization, STATES } = require('./lib/context-utilization.cjs'); - const RECOMMENDATIONS = { - [STATES.HEALTHY]: null, - [STATES.WARNING]: 'Context is approaching the fracture zone — consider /gsd-thread to continue in a fresh window.', - [STATES.CRITICAL]: 'Reasoning quality may degrade past 70% utilization (fracture point). Run /gsd-thread now to preserve output quality.', - }; - let classified; - try { - classified = classifyContextUtilization(Number(opts['tokens-used']), Number(opts['context-window'])); - } catch (e) { - // Translate the classifier's TypeError into a CLI-shaped error - // message that names the offending flag. - const flag = /tokensUsed/.test(e.message) ? '--tokens-used' : '--context-window'; - error(`${flag} must be a non-negative integer (window > 0), got the values supplied`); - break; - } - const result = { ...classified, recommendation: RECOMMENDATIONS[classified.state] }; - if (args.includes('--json')) { - core.output(result, raw); - } else { - const lines = [`Context utilization: ${result.percent}% (${result.state})`]; - if (result.recommendation) lines.push(result.recommendation); - // Use core.output's rawValue path for the sync-flush guarantee - // — process.stdout.write can be truncated on process exit. - core.output(result, true, lines.join('\n')); - } - } else { - error('Unknown validate subcommand. Available: consistency, health, agents, context'); - } + routeValidateCommand({ + verify, + args, + cwd, + raw, + parseNamedArgs, + output: core.output, + error, + }); break; } @@ -904,66 +754,14 @@ async function runCommand(command, args, cwd, raw, defaultValue) { } case 'init': { - const workflow = args[1]; - switch (workflow) { - case 'execute-phase': { - const { validate: epValidate, tdd: epTdd } = parseNamedArgs(args, [], ['validate', 'tdd']); - init.cmdInitExecutePhase(cwd, args[2], raw, { validate: epValidate, tdd: epTdd }); - break; - } - case 'plan-phase': { - const { validate: ppValidate, tdd: ppTdd } = parseNamedArgs(args, [], ['validate', 'tdd']); - init.cmdInitPlanPhase(cwd, args[2], raw, { validate: ppValidate, tdd: ppTdd }); - break; - } - case 'new-project': - init.cmdInitNewProject(cwd, raw); - break; - case 'new-milestone': - init.cmdInitNewMilestone(cwd, raw); - break; - case 'quick': - init.cmdInitQuick(cwd, args.slice(2).join(' '), raw); - break; - case 'ingest-docs': - init.cmdInitIngestDocs(cwd, raw); - break; - case 'resume': - init.cmdInitResume(cwd, raw); - break; - case 'verify-work': - init.cmdInitVerifyWork(cwd, args[2], raw); - break; - case 'phase-op': - init.cmdInitPhaseOp(cwd, args[2], raw); - break; - case 'todos': - init.cmdInitTodos(cwd, args[2], raw); - break; - case 'milestone-op': - init.cmdInitMilestoneOp(cwd, raw); - break; - case 'map-codebase': - init.cmdInitMapCodebase(cwd, raw); - break; - case 'progress': - init.cmdInitProgress(cwd, raw); - break; - case 'manager': - init.cmdInitManager(cwd, raw); - break; - case 'new-workspace': - init.cmdInitNewWorkspace(cwd, raw); - break; - case 'list-workspaces': - init.cmdInitListWorkspaces(cwd, raw); - break; - case 'remove-workspace': - init.cmdInitRemoveWorkspace(cwd, args[2], raw); - break; - default: - error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, ingest-docs, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress, manager, new-workspace, list-workspaces, remove-workspace`); - } + routeInitCommand({ + init, + args, + cwd, + raw, + parseNamedArgs, + error, + }); break; } diff --git a/get-shit-done/bin/lib/command-aliases.generated.cjs b/get-shit-done/bin/lib/command-aliases.generated.cjs new file mode 100644 index 0000000000..db18629761 --- /dev/null +++ b/get-shit-done/bin/lib/command-aliases.generated.cjs @@ -0,0 +1,118 @@ +'use strict'; + +/** + * GENERATED FILE — state.*, verify.*, init.*, phase.*, phases.*, validate.*, and roadmap.* alias/subcommand metadata for CJS routing. + * Source: sdk/src/query/command-manifest.{state,verify,init,phase,phases,validate,roadmap}.ts + */ + +const STATE_COMMAND_ALIASES = [ + { canonical: 'state.load', aliases: [], subcommand: 'load', mutation: false }, + { canonical: 'state.json', aliases: ['state json'], subcommand: 'json', mutation: false }, + { canonical: 'state.get', aliases: ['state get'], subcommand: 'get', mutation: false }, + { canonical: 'state.update', aliases: ['state update'], subcommand: 'update', mutation: true }, + { canonical: 'state.patch', aliases: ['state patch'], subcommand: 'patch', mutation: true }, + { canonical: 'state.begin-phase', aliases: ['state begin-phase'], subcommand: 'begin-phase', mutation: true }, + { canonical: 'state.advance-plan', aliases: ['state advance-plan'], subcommand: 'advance-plan', mutation: true }, + { canonical: 'state.record-metric', aliases: ['state record-metric'], subcommand: 'record-metric', mutation: true }, + { canonical: 'state.update-progress', aliases: ['state update-progress'], subcommand: 'update-progress', mutation: true }, + { canonical: 'state.add-decision', aliases: ['state add-decision'], subcommand: 'add-decision', mutation: true }, + { canonical: 'state.add-blocker', aliases: ['state add-blocker'], subcommand: 'add-blocker', mutation: true }, + { canonical: 'state.resolve-blocker', aliases: ['state resolve-blocker'], subcommand: 'resolve-blocker', mutation: true }, + { canonical: 'state.record-session', aliases: ['state record-session'], subcommand: 'record-session', mutation: true }, + { canonical: 'state.signal-waiting', aliases: ['state signal-waiting'], subcommand: 'signal-waiting', mutation: true }, + { canonical: 'state.signal-resume', aliases: ['state signal-resume'], subcommand: 'signal-resume', mutation: true }, + { canonical: 'state.planned-phase', aliases: ['state planned-phase'], subcommand: 'planned-phase', mutation: true }, + { canonical: 'state.validate', aliases: ['state validate'], subcommand: 'validate', mutation: false }, + { canonical: 'state.sync', aliases: ['state sync'], subcommand: 'sync', mutation: true }, + { canonical: 'state.prune', aliases: ['state prune'], subcommand: 'prune', mutation: true }, + { canonical: 'state.milestone-switch', aliases: ['state milestone-switch'], subcommand: 'milestone-switch', mutation: true }, + { canonical: 'state.add-roadmap-evolution', aliases: ['state add-roadmap-evolution'], subcommand: 'add-roadmap-evolution', mutation: true }, +]; + +const VERIFY_COMMAND_ALIASES = [ + { canonical: 'verify.plan-structure', aliases: ['verify plan-structure'], subcommand: 'plan-structure', mutation: false }, + { canonical: 'verify.phase-completeness', aliases: ['verify phase-completeness'], subcommand: 'phase-completeness', mutation: false }, + { canonical: 'verify.references', aliases: ['verify references'], subcommand: 'references', mutation: false }, + { canonical: 'verify.commits', aliases: ['verify commits'], subcommand: 'commits', mutation: false }, + { canonical: 'verify.artifacts', aliases: ['verify artifacts'], subcommand: 'artifacts', mutation: false }, + { canonical: 'verify.key-links', aliases: ['verify key-links'], subcommand: 'key-links', mutation: false }, + { canonical: 'verify.schema-drift', aliases: ['verify schema-drift'], subcommand: 'schema-drift', mutation: false }, + { canonical: 'verify.codebase-drift', aliases: ['verify codebase-drift'], subcommand: 'codebase-drift', mutation: false }, +]; + +const INIT_COMMAND_ALIASES = [ + { canonical: 'init.execute-phase', aliases: ['init execute-phase'], subcommand: 'execute-phase', mutation: false }, + { canonical: 'init.plan-phase', aliases: ['init plan-phase'], subcommand: 'plan-phase', mutation: false }, + { canonical: 'init.new-project', aliases: ['init new-project'], subcommand: 'new-project', mutation: false }, + { canonical: 'init.new-milestone', aliases: ['init new-milestone'], subcommand: 'new-milestone', mutation: false }, + { canonical: 'init.quick', aliases: ['init quick'], subcommand: 'quick', mutation: false }, + { canonical: 'init.ingest-docs', aliases: ['init ingest-docs'], subcommand: 'ingest-docs', mutation: false }, + { canonical: 'init.resume', aliases: ['init resume'], subcommand: 'resume', mutation: false }, + { canonical: 'init.verify-work', aliases: ['init verify-work'], subcommand: 'verify-work', mutation: false }, + { canonical: 'init.phase-op', aliases: ['init phase-op'], subcommand: 'phase-op', mutation: false }, + { canonical: 'init.todos', aliases: ['init todos'], subcommand: 'todos', mutation: false }, + { canonical: 'init.milestone-op', aliases: ['init milestone-op'], subcommand: 'milestone-op', mutation: false }, + { canonical: 'init.map-codebase', aliases: ['init map-codebase'], subcommand: 'map-codebase', mutation: false }, + { canonical: 'init.progress', aliases: ['init progress'], subcommand: 'progress', mutation: false }, + { canonical: 'init.manager', aliases: ['init manager'], subcommand: 'manager', mutation: false }, + { canonical: 'init.new-workspace', aliases: ['init new-workspace'], subcommand: 'new-workspace', mutation: false }, + { canonical: 'init.list-workspaces', aliases: ['init list-workspaces'], subcommand: 'list-workspaces', mutation: false }, + { canonical: 'init.remove-workspace', aliases: ['init remove-workspace'], subcommand: 'remove-workspace', mutation: false }, +]; + +const PHASE_COMMAND_ALIASES = [ + { canonical: 'phase.list-plans', aliases: ['phase list-plans'], subcommand: 'list-plans', mutation: false }, + { canonical: 'phase.list-artifacts', aliases: ['phase list-artifacts'], subcommand: 'list-artifacts', mutation: false }, + { canonical: 'phase.next-decimal', aliases: ['phase next-decimal'], subcommand: 'next-decimal', mutation: false }, + { canonical: 'phase.add', aliases: ['phase add'], subcommand: 'add', mutation: true }, + { canonical: 'phase.add-batch', aliases: ['phase add-batch'], subcommand: 'add-batch', mutation: true }, + { canonical: 'phase.insert', aliases: ['phase insert'], subcommand: 'insert', mutation: true }, + { canonical: 'phase.remove', aliases: ['phase remove'], subcommand: 'remove', mutation: true }, + { canonical: 'phase.complete', aliases: ['phase complete'], subcommand: 'complete', mutation: true }, + { canonical: 'phase.scaffold', aliases: ['phase scaffold'], subcommand: 'scaffold', mutation: true }, +]; + +const PHASES_COMMAND_ALIASES = [ + { canonical: 'phases.list', aliases: ['phases list'], subcommand: 'list', mutation: false }, + { canonical: 'phases.clear', aliases: ['phases clear'], subcommand: 'clear', mutation: true }, + { canonical: 'phases.archive', aliases: ['phases archive'], subcommand: 'archive', mutation: true }, +]; + +const VALIDATE_COMMAND_ALIASES = [ + { canonical: 'validate.consistency', aliases: ['validate consistency'], subcommand: 'consistency', mutation: false }, + { canonical: 'validate.health', aliases: ['validate health'], subcommand: 'health', mutation: false }, + { canonical: 'validate.agents', aliases: ['validate agents'], subcommand: 'agents', mutation: false }, + { canonical: 'validate.context', aliases: ['validate context'], subcommand: 'context', mutation: false }, +]; + +const ROADMAP_COMMAND_ALIASES = [ + { canonical: 'roadmap.analyze', aliases: ['roadmap analyze'], subcommand: 'analyze', mutation: false }, + { canonical: 'roadmap.get-phase', aliases: ['roadmap get-phase'], subcommand: 'get-phase', mutation: false }, + { canonical: 'roadmap.update-plan-progress', aliases: ['roadmap update-plan-progress'], subcommand: 'update-plan-progress', mutation: true }, + { canonical: 'roadmap.annotate-dependencies', aliases: ['roadmap annotate-dependencies'], subcommand: 'annotate-dependencies', mutation: true }, +]; + +const STATE_SUBCOMMANDS = STATE_COMMAND_ALIASES.map((entry) => entry.subcommand); +const VERIFY_SUBCOMMANDS = VERIFY_COMMAND_ALIASES.map((entry) => entry.subcommand); +const INIT_SUBCOMMANDS = INIT_COMMAND_ALIASES.map((entry) => entry.subcommand); +const PHASE_SUBCOMMANDS = PHASE_COMMAND_ALIASES.map((entry) => entry.subcommand); +const PHASES_SUBCOMMANDS = PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand); +const VALIDATE_SUBCOMMANDS = VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand); +const ROADMAP_SUBCOMMANDS = ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand); + +module.exports = { + STATE_COMMAND_ALIASES, + VERIFY_COMMAND_ALIASES, + INIT_COMMAND_ALIASES, + PHASE_COMMAND_ALIASES, + PHASES_COMMAND_ALIASES, + VALIDATE_COMMAND_ALIASES, + ROADMAP_COMMAND_ALIASES, + STATE_SUBCOMMANDS, + VERIFY_SUBCOMMANDS, + INIT_SUBCOMMANDS, + PHASE_SUBCOMMANDS, + PHASES_SUBCOMMANDS, + VALIDATE_SUBCOMMANDS, + ROADMAP_SUBCOMMANDS, +}; diff --git a/get-shit-done/bin/lib/init-command-router.cjs b/get-shit-done/bin/lib/init-command-router.cjs new file mode 100644 index 0000000000..b756311e7e --- /dev/null +++ b/get-shit-done/bin/lib/init-command-router.cjs @@ -0,0 +1,70 @@ +'use strict'; + +const { INIT_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); + +function routeInitCommand({ init, args, cwd, raw, parseNamedArgs, error }) { + const workflow = args[1]; + switch (workflow) { + case 'execute-phase': { + const { validate: epValidate, tdd: epTdd } = parseNamedArgs(args, [], ['validate', 'tdd']); + init.cmdInitExecutePhase(cwd, args[2], raw, { validate: epValidate, tdd: epTdd }); + break; + } + case 'plan-phase': { + const { validate: ppValidate, tdd: ppTdd } = parseNamedArgs(args, [], ['validate', 'tdd']); + init.cmdInitPlanPhase(cwd, args[2], raw, { validate: ppValidate, tdd: ppTdd }); + break; + } + case 'new-project': + init.cmdInitNewProject(cwd, raw); + break; + case 'new-milestone': + init.cmdInitNewMilestone(cwd, raw); + break; + case 'quick': + init.cmdInitQuick(cwd, args.slice(2).join(' '), raw); + break; + case 'ingest-docs': + init.cmdInitIngestDocs(cwd, raw); + break; + case 'resume': + init.cmdInitResume(cwd, raw); + break; + case 'verify-work': + init.cmdInitVerifyWork(cwd, args[2], raw); + break; + case 'phase-op': + init.cmdInitPhaseOp(cwd, args[2], raw); + break; + case 'todos': + init.cmdInitTodos(cwd, args[2], raw); + break; + case 'milestone-op': + init.cmdInitMilestoneOp(cwd, raw); + break; + case 'map-codebase': + init.cmdInitMapCodebase(cwd, raw); + break; + case 'progress': + init.cmdInitProgress(cwd, raw); + break; + case 'manager': + init.cmdInitManager(cwd, raw); + break; + case 'new-workspace': + init.cmdInitNewWorkspace(cwd, raw); + break; + case 'list-workspaces': + init.cmdInitListWorkspaces(cwd, raw); + break; + case 'remove-workspace': + init.cmdInitRemoveWorkspace(cwd, args[2], raw); + break; + default: + error(`Unknown init workflow: ${workflow}\nAvailable: ${INIT_SUBCOMMANDS.join(', ')}`); + } +} + +module.exports = { + routeInitCommand, +}; diff --git a/get-shit-done/bin/lib/phase-command-router.cjs b/get-shit-done/bin/lib/phase-command-router.cjs new file mode 100644 index 0000000000..e4f4a66536 --- /dev/null +++ b/get-shit-done/bin/lib/phase-command-router.cjs @@ -0,0 +1,49 @@ +'use strict'; + +const { PHASE_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); + +function routePhaseCommand({ phase, args, cwd, raw, error }) { + const subcommand = args[1]; + + if (subcommand === 'next-decimal') { + phase.cmdPhaseNextDecimal(cwd, args[2], raw); + } else if (subcommand === 'add') { + let customId = null; + const descArgs = []; + for (let i = 2; i < args.length; i++) { + if (args[i] === '--id' && i + 1 < args.length) { + customId = args[i + 1]; + i++; + } else { + descArgs.push(args[i]); + } + } + phase.cmdPhaseAdd(cwd, descArgs.join(' '), raw, customId); + } else if (subcommand === 'add-batch') { + const descFlagIdx = args.indexOf('--descriptions'); + let descriptions; + if (descFlagIdx !== -1 && args[descFlagIdx + 1]) { + try { + descriptions = JSON.parse(args[descFlagIdx + 1]); + } catch { + error('--descriptions must be a JSON array'); + } + } else { + descriptions = args.slice(2).filter(a => a !== '--raw'); + } + phase.cmdPhaseAddBatch(cwd, descriptions, raw); + } else if (subcommand === 'insert') { + phase.cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw); + } else if (subcommand === 'remove') { + const forceFlag = args.includes('--force'); + phase.cmdPhaseRemove(cwd, args[2], { force: forceFlag }, raw); + } else if (subcommand === 'complete') { + phase.cmdPhaseComplete(cwd, args[2], raw); + } else { + error(`Unknown phase subcommand. Available: ${PHASE_SUBCOMMANDS.filter((s) => s !== 'list-plans' && s !== 'list-artifacts' && s !== 'scaffold').join(', ')}`); + } +} + +module.exports = { + routePhaseCommand, +}; diff --git a/get-shit-done/bin/lib/phases-command-router.cjs b/get-shit-done/bin/lib/phases-command-router.cjs new file mode 100644 index 0000000000..686ec08850 --- /dev/null +++ b/get-shit-done/bin/lib/phases-command-router.cjs @@ -0,0 +1,36 @@ +'use strict'; + +const { PHASES_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); + +/** + * Manifest-backed phases subcommand router. + * Keeps gsd-tools.cjs thin while preserving current CJS semantics: + * - list + * - clear + * + * Note: `archive` is currently SDK-only (`phases.archive` handler in SDK query + * registry). CJS `gsd-tools phases` intentionally supports list/clear only. + */ +function routePhasesCommand({ phase, milestone, args, cwd, raw, error }) { + const subcommand = args[1]; + + if (subcommand === '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); + } else if (subcommand === 'clear') { + milestone.cmdPhasesClear(cwd, raw, args.slice(2)); + } else { + const cjsSupported = PHASES_SUBCOMMANDS.filter((s) => s !== 'archive'); + error(`Unknown phases subcommand. Available: ${cjsSupported.join(', ')}`); + } +} + +module.exports = { + routePhasesCommand, +}; diff --git a/get-shit-done/bin/lib/roadmap-command-router.cjs b/get-shit-done/bin/lib/roadmap-command-router.cjs new file mode 100644 index 0000000000..060443bcb3 --- /dev/null +++ b/get-shit-done/bin/lib/roadmap-command-router.cjs @@ -0,0 +1,23 @@ +'use strict'; + +const { ROADMAP_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); + +function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) { + const subcommand = args[1]; + + if (subcommand === 'get-phase') { + roadmap.cmdRoadmapGetPhase(cwd, args[2], raw); + } else if (subcommand === 'analyze') { + roadmap.cmdRoadmapAnalyze(cwd, raw); + } else if (subcommand === 'update-plan-progress') { + roadmap.cmdRoadmapUpdatePlanProgress(cwd, args[2], raw); + } else if (subcommand === 'annotate-dependencies') { + roadmap.cmdRoadmapAnnotateDependencies(cwd, args[2], raw); + } else { + error(`Unknown roadmap subcommand. Available: ${ROADMAP_SUBCOMMANDS.join(', ')}`); + } +} + +module.exports = { + routeRoadmapCommand, +}; diff --git a/get-shit-done/bin/lib/state-command-router.cjs b/get-shit-done/bin/lib/state-command-router.cjs new file mode 100644 index 0000000000..5aeec9b41b --- /dev/null +++ b/get-shit-done/bin/lib/state-command-router.cjs @@ -0,0 +1,90 @@ +'use strict'; + +const { STATE_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); + +/** + * Manifest-backed state subcommand router. + * Keeps gsd-tools.cjs thin while preserving existing command semantics. + */ +function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) { + const subcommand = args[1]; + + if (subcommand === 'json') { + state.cmdStateJson(cwd, raw); + } else if (subcommand === 'update') { + state.cmdStateUpdate(cwd, args[2], args[3]); + } else if (subcommand === 'get') { + state.cmdStateGet(cwd, args[2], raw); + } else if (subcommand === 'patch') { + const patches = {}; + for (let i = 2; i < args.length; i += 2) { + const key = args[i].replace(/^--/, ''); + const value = args[i + 1]; + if (key && value !== undefined) { + patches[key] = value; + } + } + state.cmdStatePatch(cwd, patches, raw); + } else if (subcommand === 'advance-plan') { + state.cmdStateAdvancePlan(cwd, raw); + } else if (subcommand === 'record-metric') { + const { phase: p, plan, duration, tasks, files } = parseNamedArgs(args, ['phase', 'plan', 'duration', 'tasks', 'files']); + state.cmdStateRecordMetric(cwd, { phase: p, plan, duration, tasks, files }, raw); + } else if (subcommand === 'update-progress') { + state.cmdStateUpdateProgress(cwd, raw); + } else if (subcommand === 'add-decision') { + const { phase: p, summary, 'summary-file': summary_file, rationale, 'rationale-file': rationale_file } = parseNamedArgs(args, ['phase', 'summary', 'summary-file', 'rationale', 'rationale-file']); + state.cmdStateAddDecision(cwd, { phase: p, summary, summary_file, rationale: rationale || '', rationale_file }, raw); + } else if (subcommand === 'add-blocker') { + const { text, 'text-file': text_file } = parseNamedArgs(args, ['text', 'text-file']); + state.cmdStateAddBlocker(cwd, { text, text_file }, raw); + } else if (subcommand === 'resolve-blocker') { + state.cmdStateResolveBlocker(cwd, parseNamedArgs(args, ['text']).text, raw); + } else if (subcommand === 'record-session') { + const { 'stopped-at': stopped_at, 'resume-file': resume_file } = parseNamedArgs(args, ['stopped-at', 'resume-file']); + state.cmdStateRecordSession(cwd, { stopped_at, resume_file: resume_file || 'None' }, raw); + } else if (subcommand === 'begin-phase') { + const { phase: p, name, plans } = parseNamedArgs(args, ['phase', 'name', 'plans']); + const parsedPlans = plans == null ? null : Number.parseInt(plans, 10); + if (plans != null && Number.isNaN(parsedPlans)) { + return error('Invalid --plans value. Expected an integer.'); + } + state.cmdStateBeginPhase(cwd, p, name, parsedPlans, raw); + } else if (subcommand === 'signal-waiting') { + const { type, question, options, phase: p } = parseNamedArgs(args, ['type', 'question', 'options', 'phase']); + state.cmdSignalWaiting(cwd, type, question, options, p, raw); + } else if (subcommand === 'signal-resume') { + state.cmdSignalResume(cwd, raw); + } else if (subcommand === 'planned-phase') { + const { phase: p, plans } = parseNamedArgs(args, ['phase', 'name', 'plans']); + const parsedPlans = plans == null ? null : Number.parseInt(plans, 10); + if (plans != null && Number.isNaN(parsedPlans)) { + return error('Invalid --plans value. Expected an integer.'); + } + state.cmdStatePlannedPhase(cwd, p, parsedPlans, raw); + } else if (subcommand === 'validate') { + state.cmdStateValidate(cwd, raw); + } else if (subcommand === 'sync') { + const { verify } = parseNamedArgs(args, [], ['verify']); + state.cmdStateSync(cwd, { verify }, raw); + } else if (subcommand === 'prune') { + const { 'keep-recent': keepRecent, 'dry-run': dryRun } = parseNamedArgs(args, ['keep-recent'], ['dry-run']); + state.cmdStatePrune(cwd, { keepRecent: keepRecent || '3', dryRun: !!dryRun }, raw); + } else if (subcommand === 'complete-phase') { + state.cmdStateCompletePhase(cwd, raw); + } else if (subcommand === 'milestone-switch') { + const { milestone, name } = parseNamedArgs(args, ['milestone', 'name']); + state.cmdStateMilestoneSwitch(cwd, milestone, name, raw); + } else if (subcommand === 'add-roadmap-evolution') { + error('state add-roadmap-evolution is SDK-only. Use: gsd-sdk query state.add-roadmap-evolution ...'); + } else if (subcommand === undefined || subcommand === 'load') { + state.cmdStateLoad(cwd, raw); + } else { + const available = ['load', 'complete-phase', ...STATE_SUBCOMMANDS.filter((s) => s !== 'load')]; + error(`Unknown state subcommand: "${subcommand}". Available: ${available.join(', ')}`); + } +} + +module.exports = { + routeStateCommand, +}; diff --git a/get-shit-done/bin/lib/validate-command-router.cjs b/get-shit-done/bin/lib/validate-command-router.cjs new file mode 100644 index 0000000000..fb5575467f --- /dev/null +++ b/get-shit-done/bin/lib/validate-command-router.cjs @@ -0,0 +1,55 @@ +'use strict'; + +const { VALIDATE_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); + +function routeValidateCommand({ verify, args, cwd, raw, parseNamedArgs, output, error }) { + const subcommand = args[1]; + + if (subcommand === 'consistency') { + verify.cmdValidateConsistency(cwd, raw); + } else if (subcommand === 'health') { + const repairFlag = args.includes('--repair'); + const backfillFlag = args.includes('--backfill'); + verify.cmdValidateHealth(cwd, { repair: repairFlag, backfill: backfillFlag }, raw); + } else if (subcommand === 'agents') { + verify.cmdValidateAgents(cwd, raw); + } else if (subcommand === 'context') { + const opts = parseNamedArgs(args, ['tokens-used', 'context-window']); + if (opts['tokens-used'] === null) { + error('--tokens-used is required for `validate context`'); + return; + } + if (opts['context-window'] === null) { + error('--context-window is required for `validate context`'); + return; + } + const { classifyContextUtilization, STATES } = require('./context-utilization.cjs'); + const RECOMMENDATIONS = { + [STATES.HEALTHY]: null, + [STATES.WARNING]: 'Context is approaching the fracture zone — consider /gsd-thread to continue in a fresh window.', + [STATES.CRITICAL]: 'Reasoning quality may degrade past 70% utilization (fracture point). Run /gsd-thread now to preserve output quality.', + }; + let classified; + try { + classified = classifyContextUtilization(Number(opts['tokens-used']), Number(opts['context-window'])); + } catch (e) { + const flag = /tokensUsed/.test(e.message) ? '--tokens-used' : '--context-window'; + error(`${flag} must be a non-negative integer (window > 0), got the values supplied`); + return; + } + const result = { ...classified, recommendation: RECOMMENDATIONS[classified.state] }; + if (args.includes('--json')) { + output(result, raw); + } else { + const lines = [`Context utilization: ${result.percent}% (${result.state})`]; + if (result.recommendation) lines.push(result.recommendation); + output(result, true, lines.join('\n')); + } + } else { + error(`Unknown validate subcommand. Available: ${VALIDATE_SUBCOMMANDS.join(', ')}`); + } +} + +module.exports = { + routeValidateCommand, +}; diff --git a/get-shit-done/bin/lib/verify-command-router.cjs b/get-shit-done/bin/lib/verify-command-router.cjs new file mode 100644 index 0000000000..806b2ddd0c --- /dev/null +++ b/get-shit-done/bin/lib/verify-command-router.cjs @@ -0,0 +1,34 @@ +'use strict'; + +const { VERIFY_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); + +function routeVerifyCommand({ verify, args, cwd, raw, error }) { + const subcommand = args[1]; + + if (subcommand === 'plan-structure') { + verify.cmdVerifyPlanStructure(cwd, args[2], raw); + } else if (subcommand === 'phase-completeness') { + verify.cmdVerifyPhaseCompleteness(cwd, args[2], raw); + } else if (subcommand === 'references') { + verify.cmdVerifyReferences(cwd, args[2], raw); + } else if (subcommand === 'commits') { + verify.cmdVerifyCommits(cwd, args.slice(2), raw); + } else if (subcommand === 'artifacts') { + verify.cmdVerifyArtifacts(cwd, args[2], raw); + } else if (subcommand === 'key-links') { + verify.cmdVerifyKeyLinks(cwd, args[2], raw); + } else if (subcommand === '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); + } else if (subcommand === 'codebase-drift') { + verify.cmdVerifyCodebaseDrift(cwd, raw); + } else { + error(`Unknown verify subcommand. Available: ${VERIFY_SUBCOMMANDS.join(', ')}`); + } +} + +module.exports = { + routeVerifyCommand, +}; diff --git a/sdk/scripts/check-command-aliases-fresh.mjs b/sdk/scripts/check-command-aliases-fresh.mjs new file mode 100644 index 0000000000..0a4a8aa303 --- /dev/null +++ b/sdk/scripts/check-command-aliases-fresh.mjs @@ -0,0 +1,87 @@ +#!/usr/bin/env node +import { createRequire } from 'node:module'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const here = dirname(fileURLToPath(import.meta.url)); + +const { + STATE_COMMAND_MANIFEST, +} = await import('../dist/query/command-manifest.state.js'); +const { + VERIFY_COMMAND_MANIFEST, +} = await import('../dist/query/command-manifest.verify.js'); +const { + INIT_COMMAND_MANIFEST, +} = await import('../dist/query/command-manifest.init.js'); +const { + PHASE_COMMAND_MANIFEST, +} = await import('../dist/query/command-manifest.phase.js'); +const { + PHASES_COMMAND_MANIFEST, +} = await import('../dist/query/command-manifest.phases.js'); +const { + VALIDATE_COMMAND_MANIFEST, +} = await import('../dist/query/command-manifest.validate.js'); +const { + ROADMAP_COMMAND_MANIFEST, +} = await import('../dist/query/command-manifest.roadmap.js'); + +const { + STATE_COMMAND_ALIASES, + VERIFY_COMMAND_ALIASES, + INIT_COMMAND_ALIASES, + PHASE_COMMAND_ALIASES, + PHASES_COMMAND_ALIASES, + VALIDATE_COMMAND_ALIASES, + ROADMAP_COMMAND_ALIASES, +} = await import('../dist/query/command-aliases.generated.js'); + +const cjsAliases = require(resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs')); + +function toAliasEntries(manifest, family) { + const prefix = `${family}.`; + return manifest.map((entry) => ({ + canonical: entry.canonical, + aliases: [...entry.aliases], + subcommand: entry.canonical.slice(prefix.length), + mutation: entry.mutation, + })); +} + +function assertEqual(label, actual, expected) { + const a = JSON.stringify(actual); + const e = JSON.stringify(expected); + if (a !== e) { + throw new Error( + `${label} drift detected. Regenerate command alias artifacts and commit them.`, + ); + } +} + +const expectedState = toAliasEntries(STATE_COMMAND_MANIFEST, 'state'); +const expectedVerify = toAliasEntries(VERIFY_COMMAND_MANIFEST, 'verify'); +const expectedInit = toAliasEntries(INIT_COMMAND_MANIFEST, 'init'); +const expectedPhase = toAliasEntries(PHASE_COMMAND_MANIFEST, 'phase'); +const expectedPhases = toAliasEntries(PHASES_COMMAND_MANIFEST, 'phases'); +const expectedValidate = toAliasEntries(VALIDATE_COMMAND_MANIFEST, 'validate'); +const expectedRoadmap = toAliasEntries(ROADMAP_COMMAND_MANIFEST, 'roadmap'); + +assertEqual('TS STATE_COMMAND_ALIASES', STATE_COMMAND_ALIASES, expectedState); +assertEqual('TS VERIFY_COMMAND_ALIASES', VERIFY_COMMAND_ALIASES, expectedVerify); +assertEqual('TS INIT_COMMAND_ALIASES', INIT_COMMAND_ALIASES, expectedInit); +assertEqual('TS PHASE_COMMAND_ALIASES', PHASE_COMMAND_ALIASES, expectedPhase); +assertEqual('TS PHASES_COMMAND_ALIASES', PHASES_COMMAND_ALIASES, expectedPhases); +assertEqual('TS VALIDATE_COMMAND_ALIASES', VALIDATE_COMMAND_ALIASES, expectedValidate); +assertEqual('TS ROADMAP_COMMAND_ALIASES', ROADMAP_COMMAND_ALIASES, expectedRoadmap); + +assertEqual('CJS STATE_COMMAND_ALIASES', cjsAliases.STATE_COMMAND_ALIASES, expectedState); +assertEqual('CJS VERIFY_COMMAND_ALIASES', cjsAliases.VERIFY_COMMAND_ALIASES, expectedVerify); +assertEqual('CJS INIT_COMMAND_ALIASES', cjsAliases.INIT_COMMAND_ALIASES, expectedInit); +assertEqual('CJS PHASE_COMMAND_ALIASES', cjsAliases.PHASE_COMMAND_ALIASES, expectedPhase); +assertEqual('CJS PHASES_COMMAND_ALIASES', cjsAliases.PHASES_COMMAND_ALIASES, expectedPhases); +assertEqual('CJS VALIDATE_COMMAND_ALIASES', cjsAliases.VALIDATE_COMMAND_ALIASES, expectedValidate); +assertEqual('CJS ROADMAP_COMMAND_ALIASES', cjsAliases.ROADMAP_COMMAND_ALIASES, expectedRoadmap); + +console.log('command alias artifacts are fresh'); diff --git a/sdk/scripts/gen-command-aliases.ts b/sdk/scripts/gen-command-aliases.ts new file mode 100644 index 0000000000..0a61c0c004 --- /dev/null +++ b/sdk/scripts/gen-command-aliases.ts @@ -0,0 +1,124 @@ +#!/usr/bin/env node +/** + * Build-time alias generator skeleton for command-manifest-driven routing. + * + * This pilot commits generated artifacts directly; this script documents and + * preserves the generation seam so future command families can be migrated + * without hand-maintained alias duplication. + */ + +import { writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +import { STATE_COMMAND_MANIFEST } from '../src/query/command-manifest.state.js'; +import { VERIFY_COMMAND_MANIFEST } from '../src/query/command-manifest.verify.js'; +import { INIT_COMMAND_MANIFEST } from '../src/query/command-manifest.init.js'; +import { PHASE_COMMAND_MANIFEST } from '../src/query/command-manifest.phase.js'; +import { PHASES_COMMAND_MANIFEST } from '../src/query/command-manifest.phases.js'; +import { VALIDATE_COMMAND_MANIFEST } from '../src/query/command-manifest.validate.js'; +import { ROADMAP_COMMAND_MANIFEST } from '../src/query/command-manifest.roadmap.js'; + +function toSubcommand(canonical: string, family: 'state' | 'verify' | 'init' | 'phase' | 'phases' | 'validate' | 'roadmap'): string { + const prefix = `${family}.`; + return canonical.startsWith(prefix) ? canonical.slice(prefix.length) : canonical; +} + +async function main(): Promise { + const stateEntries = STATE_COMMAND_MANIFEST.map((entry) => ({ + canonical: entry.canonical, + aliases: entry.aliases, + subcommand: toSubcommand(entry.canonical, 'state'), + mutation: entry.mutation, + })); + + const verifyEntries = VERIFY_COMMAND_MANIFEST.map((entry) => ({ + canonical: entry.canonical, + aliases: entry.aliases, + subcommand: toSubcommand(entry.canonical, 'verify'), + mutation: entry.mutation, + })); + + const initEntries = INIT_COMMAND_MANIFEST.map((entry) => ({ + canonical: entry.canonical, + aliases: entry.aliases, + subcommand: toSubcommand(entry.canonical, 'init'), + mutation: entry.mutation, + })); + + const phaseEntries = PHASE_COMMAND_MANIFEST.map((entry) => ({ + canonical: entry.canonical, + aliases: entry.aliases, + subcommand: toSubcommand(entry.canonical, 'phase'), + mutation: entry.mutation, + })); + + const phasesEntries = PHASES_COMMAND_MANIFEST.map((entry) => ({ + canonical: entry.canonical, + aliases: entry.aliases, + subcommand: toSubcommand(entry.canonical, 'phases'), + mutation: entry.mutation, + })); + + const validateEntries = VALIDATE_COMMAND_MANIFEST.map((entry) => ({ + canonical: entry.canonical, + aliases: entry.aliases, + subcommand: toSubcommand(entry.canonical, 'validate'), + mutation: entry.mutation, + })); + + const roadmapEntries = ROADMAP_COMMAND_MANIFEST.map((entry) => ({ + canonical: entry.canonical, + aliases: entry.aliases, + subcommand: toSubcommand(entry.canonical, 'roadmap'), + mutation: entry.mutation, + })); + + const outPath = fileURLToPath(new URL('../src/query/command-aliases.generated.ts', import.meta.url)); + const header = `/**\n * GENERATED FILE — command alias expansion for state.*, verify.*, init.*, phase.*, phases.*, validate.*, and roadmap.* pilots.\n * Source: sdk/src/query/command-manifest.{state,verify,init,phase,phases,validate,roadmap}.ts\n */\n\n`; + const body = [ + `export const STATE_COMMAND_ALIASES = ${JSON.stringify(stateEntries, null, 2)} as const;`, + '', + `export const VERIFY_COMMAND_ALIASES = ${JSON.stringify(verifyEntries, null, 2)} as const;`, + '', + `export const INIT_COMMAND_ALIASES = ${JSON.stringify(initEntries, null, 2)} as const;`, + '', + `export const PHASE_COMMAND_ALIASES = ${JSON.stringify(phaseEntries, null, 2)} as const;`, + '', + `export const PHASES_COMMAND_ALIASES = ${JSON.stringify(phasesEntries, null, 2)} as const;`, + '', + `export const VALIDATE_COMMAND_ALIASES = ${JSON.stringify(validateEntries, null, 2)} as const;`, + '', + `export const ROADMAP_COMMAND_ALIASES = ${JSON.stringify(roadmapEntries, null, 2)} as const;`, + '', + 'export const STATE_SUBCOMMANDS = new Set(STATE_COMMAND_ALIASES.map((entry) => entry.subcommand));', + 'export const VERIFY_SUBCOMMANDS = new Set(VERIFY_COMMAND_ALIASES.map((entry) => entry.subcommand));', + 'export const INIT_SUBCOMMANDS = new Set(INIT_COMMAND_ALIASES.map((entry) => entry.subcommand));', + 'export const PHASE_SUBCOMMANDS = new Set(PHASE_COMMAND_ALIASES.map((entry) => entry.subcommand));', + 'export const PHASES_SUBCOMMANDS = new Set(PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand));', + 'export const VALIDATE_SUBCOMMANDS = new Set(VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand));', + 'export const ROADMAP_SUBCOMMANDS = new Set(ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand));', + '', + 'export const STATE_MUTATION_COMMANDS: readonly string[] = STATE_COMMAND_ALIASES', + ' .filter((entry) => entry.mutation)', + ' .flatMap((entry) => [entry.canonical, ...entry.aliases]);', + '', + 'export const PHASE_MUTATION_COMMANDS: readonly string[] = PHASE_COMMAND_ALIASES', + ' .filter((entry) => entry.mutation)', + ' .flatMap((entry) => [entry.canonical, ...entry.aliases]);', + '', + 'export const PHASES_MUTATION_COMMANDS: readonly string[] = PHASES_COMMAND_ALIASES', + ' .filter((entry) => entry.mutation)', + ' .flatMap((entry) => [entry.canonical, ...entry.aliases]);', + '', + 'export const ROADMAP_MUTATION_COMMANDS: readonly string[] = ROADMAP_COMMAND_ALIASES', + ' .filter((entry) => entry.mutation)', + ' .flatMap((entry) => [entry.canonical, ...entry.aliases]);', + '', + ].join('\n'); + await writeFile(outPath, header + body, 'utf-8'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/sdk/src/query/QUERY-HANDLERS.md b/sdk/src/query/QUERY-HANDLERS.md index 98bbfa7228..6a262281b4 100644 --- a/sdk/src/query/QUERY-HANDLERS.md +++ b/sdk/src/query/QUERY-HANDLERS.md @@ -10,6 +10,20 @@ This document records contracts for the typed query layer consumed by `gsd-sdk q - CJS `**summary-extract**` → SDK `**summary.extract**` / `**summary extract**` / `**history-digest**` (see `index.ts`). - CJS top-level `**scaffold ...**` → SDK `**phase.scaffold**` / `**phase scaffold**` with the scaffold type as the first argument (no separate `scaffold` alias on the registry). +### Manifest-backed family ownership + +These families are sourced from `command-manifest.*.ts` files and expanded into generated alias artifacts (`command-aliases.generated.ts` + CJS mirror): + +- `state.*` → `command-manifest.state.ts` +- `verify.*` → `command-manifest.verify.ts` +- `init.*` → `command-manifest.init.ts` +- `phase.*` → `command-manifest.phase.ts` +- `phases.*` → `command-manifest.phases.ts` +- `validate.*` → `command-manifest.validate.ts` +- `roadmap.*` → `command-manifest.roadmap.ts` + +CJS routing seams mirror these families with thin adapters (`state/verify/init/phase/phases/validate/roadmap-command-router.cjs`) so `gsd-tools.cjs` stays orchestration-only. + ## `gsd-sdk query` routing 1. **`normalizeQueryCommand()`** (`normalize-query-command.ts`) — maps the first argv tokens to the same **command + subcommand** patterns as `gsd-tools` `runCommand()` where needed (e.g. `state json` → `state.json`, `init execute-phase 9` → `init.execute-phase` with args `['9']`, `scaffold …` → `phase.scaffold`). Re-exported from **`@gsd-build/sdk`** and **`createRegistry`’s module** (`sdk/src/query/index.ts`) so programmatic callers can mirror CLI tokenization without importing a deep path. diff --git a/sdk/src/query/command-aliases.generated.ts b/sdk/src/query/command-aliases.generated.ts new file mode 100644 index 0000000000..9692c34465 --- /dev/null +++ b/sdk/src/query/command-aliases.generated.ts @@ -0,0 +1,122 @@ +/** + * GENERATED FILE — command alias expansion for state.*, verify.*, init.*, phase.*, phases.*, validate.*, and roadmap.* pilots. + * Source: sdk/src/query/command-manifest.{state,verify,init,phase,phases,validate,roadmap}.ts + */ + +export interface FamilyCommandAlias { + canonical: string; + aliases: string[]; + subcommand: string; + mutation: boolean; +} + +export const STATE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [ + { canonical: 'state.load', aliases: [], subcommand: 'load', mutation: false }, + { canonical: 'state.json', aliases: ['state json'], subcommand: 'json', mutation: false }, + { canonical: 'state.get', aliases: ['state get'], subcommand: 'get', mutation: false }, + { canonical: 'state.update', aliases: ['state update'], subcommand: 'update', mutation: true }, + { canonical: 'state.patch', aliases: ['state patch'], subcommand: 'patch', mutation: true }, + { canonical: 'state.begin-phase', aliases: ['state begin-phase'], subcommand: 'begin-phase', mutation: true }, + { canonical: 'state.advance-plan', aliases: ['state advance-plan'], subcommand: 'advance-plan', mutation: true }, + { canonical: 'state.record-metric', aliases: ['state record-metric'], subcommand: 'record-metric', mutation: true }, + { canonical: 'state.update-progress', aliases: ['state update-progress'], subcommand: 'update-progress', mutation: true }, + { canonical: 'state.add-decision', aliases: ['state add-decision'], subcommand: 'add-decision', mutation: true }, + { canonical: 'state.add-blocker', aliases: ['state add-blocker'], subcommand: 'add-blocker', mutation: true }, + { canonical: 'state.resolve-blocker', aliases: ['state resolve-blocker'], subcommand: 'resolve-blocker', mutation: true }, + { canonical: 'state.record-session', aliases: ['state record-session'], subcommand: 'record-session', mutation: true }, + { canonical: 'state.signal-waiting', aliases: ['state signal-waiting'], subcommand: 'signal-waiting', mutation: true }, + { canonical: 'state.signal-resume', aliases: ['state signal-resume'], subcommand: 'signal-resume', mutation: true }, + { canonical: 'state.planned-phase', aliases: ['state planned-phase'], subcommand: 'planned-phase', mutation: true }, + { canonical: 'state.validate', aliases: ['state validate'], subcommand: 'validate', mutation: false }, + { canonical: 'state.sync', aliases: ['state sync'], subcommand: 'sync', mutation: true }, + { canonical: 'state.prune', aliases: ['state prune'], subcommand: 'prune', mutation: true }, + { canonical: 'state.milestone-switch', aliases: ['state milestone-switch'], subcommand: 'milestone-switch', mutation: true }, + { canonical: 'state.add-roadmap-evolution', aliases: ['state add-roadmap-evolution'], subcommand: 'add-roadmap-evolution', mutation: true }, +] as const; + +export const VERIFY_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [ + { canonical: 'verify.plan-structure', aliases: ['verify plan-structure'], subcommand: 'plan-structure', mutation: false }, + { canonical: 'verify.phase-completeness', aliases: ['verify phase-completeness'], subcommand: 'phase-completeness', mutation: false }, + { canonical: 'verify.references', aliases: ['verify references'], subcommand: 'references', mutation: false }, + { canonical: 'verify.commits', aliases: ['verify commits'], subcommand: 'commits', mutation: false }, + { canonical: 'verify.artifacts', aliases: ['verify artifacts'], subcommand: 'artifacts', mutation: false }, + { canonical: 'verify.key-links', aliases: ['verify key-links'], subcommand: 'key-links', mutation: false }, + { canonical: 'verify.schema-drift', aliases: ['verify schema-drift'], subcommand: 'schema-drift', mutation: false }, + { canonical: 'verify.codebase-drift', aliases: ['verify codebase-drift'], subcommand: 'codebase-drift', mutation: false }, +] as const; + +export const INIT_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [ + { canonical: 'init.execute-phase', aliases: ['init execute-phase'], subcommand: 'execute-phase', mutation: false }, + { canonical: 'init.plan-phase', aliases: ['init plan-phase'], subcommand: 'plan-phase', mutation: false }, + { canonical: 'init.new-project', aliases: ['init new-project'], subcommand: 'new-project', mutation: false }, + { canonical: 'init.new-milestone', aliases: ['init new-milestone'], subcommand: 'new-milestone', mutation: false }, + { canonical: 'init.quick', aliases: ['init quick'], subcommand: 'quick', mutation: false }, + { canonical: 'init.ingest-docs', aliases: ['init ingest-docs'], subcommand: 'ingest-docs', mutation: false }, + { canonical: 'init.resume', aliases: ['init resume'], subcommand: 'resume', mutation: false }, + { canonical: 'init.verify-work', aliases: ['init verify-work'], subcommand: 'verify-work', mutation: false }, + { canonical: 'init.phase-op', aliases: ['init phase-op'], subcommand: 'phase-op', mutation: false }, + { canonical: 'init.todos', aliases: ['init todos'], subcommand: 'todos', mutation: false }, + { canonical: 'init.milestone-op', aliases: ['init milestone-op'], subcommand: 'milestone-op', mutation: false }, + { canonical: 'init.map-codebase', aliases: ['init map-codebase'], subcommand: 'map-codebase', mutation: false }, + { canonical: 'init.progress', aliases: ['init progress'], subcommand: 'progress', mutation: false }, + { canonical: 'init.manager', aliases: ['init manager'], subcommand: 'manager', mutation: false }, + { canonical: 'init.new-workspace', aliases: ['init new-workspace'], subcommand: 'new-workspace', mutation: false }, + { canonical: 'init.list-workspaces', aliases: ['init list-workspaces'], subcommand: 'list-workspaces', mutation: false }, + { canonical: 'init.remove-workspace', aliases: ['init remove-workspace'], subcommand: 'remove-workspace', mutation: false }, +] as const; + +export const PHASE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [ + { canonical: 'phase.list-plans', aliases: ['phase list-plans'], subcommand: 'list-plans', mutation: false }, + { canonical: 'phase.list-artifacts', aliases: ['phase list-artifacts'], subcommand: 'list-artifacts', mutation: false }, + { canonical: 'phase.next-decimal', aliases: ['phase next-decimal'], subcommand: 'next-decimal', mutation: false }, + { canonical: 'phase.add', aliases: ['phase add'], subcommand: 'add', mutation: true }, + { canonical: 'phase.add-batch', aliases: ['phase add-batch'], subcommand: 'add-batch', mutation: true }, + { canonical: 'phase.insert', aliases: ['phase insert'], subcommand: 'insert', mutation: true }, + { canonical: 'phase.remove', aliases: ['phase remove'], subcommand: 'remove', mutation: true }, + { canonical: 'phase.complete', aliases: ['phase complete'], subcommand: 'complete', mutation: true }, + { canonical: 'phase.scaffold', aliases: ['phase scaffold'], subcommand: 'scaffold', mutation: true }, +] as const; + +export const PHASES_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [ + { canonical: 'phases.list', aliases: ['phases list'], subcommand: 'list', mutation: false }, + { canonical: 'phases.clear', aliases: ['phases clear'], subcommand: 'clear', mutation: true }, + { canonical: 'phases.archive', aliases: ['phases archive'], subcommand: 'archive', mutation: true }, +] as const; + +export const VALIDATE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [ + { canonical: 'validate.consistency', aliases: ['validate consistency'], subcommand: 'consistency', mutation: false }, + { canonical: 'validate.health', aliases: ['validate health'], subcommand: 'health', mutation: false }, + { canonical: 'validate.agents', aliases: ['validate agents'], subcommand: 'agents', mutation: false }, + { canonical: 'validate.context', aliases: ['validate context'], subcommand: 'context', mutation: false }, +] as const; + +export const ROADMAP_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [ + { canonical: 'roadmap.analyze', aliases: ['roadmap analyze'], subcommand: 'analyze', mutation: false }, + { canonical: 'roadmap.get-phase', aliases: ['roadmap get-phase'], subcommand: 'get-phase', mutation: false }, + { canonical: 'roadmap.update-plan-progress', aliases: ['roadmap update-plan-progress'], subcommand: 'update-plan-progress', mutation: true }, + { canonical: 'roadmap.annotate-dependencies', aliases: ['roadmap annotate-dependencies'], subcommand: 'annotate-dependencies', mutation: true }, +] as const; + +export const STATE_SUBCOMMANDS = new Set(STATE_COMMAND_ALIASES.map((entry) => entry.subcommand)); +export const VERIFY_SUBCOMMANDS = new Set(VERIFY_COMMAND_ALIASES.map((entry) => entry.subcommand)); +export const INIT_SUBCOMMANDS = new Set(INIT_COMMAND_ALIASES.map((entry) => entry.subcommand)); +export const PHASE_SUBCOMMANDS = new Set(PHASE_COMMAND_ALIASES.map((entry) => entry.subcommand)); +export const PHASES_SUBCOMMANDS = new Set(PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand)); +export const VALIDATE_SUBCOMMANDS = new Set(VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand)); +export const ROADMAP_SUBCOMMANDS = new Set(ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand)); + +export const STATE_MUTATION_COMMANDS: readonly string[] = STATE_COMMAND_ALIASES + .filter((entry) => entry.mutation) + .flatMap((entry) => [entry.canonical, ...entry.aliases]); + +export const PHASE_MUTATION_COMMANDS: readonly string[] = PHASE_COMMAND_ALIASES + .filter((entry) => entry.mutation) + .flatMap((entry) => [entry.canonical, ...entry.aliases]); + +export const PHASES_MUTATION_COMMANDS: readonly string[] = PHASES_COMMAND_ALIASES + .filter((entry) => entry.mutation) + .flatMap((entry) => [entry.canonical, ...entry.aliases]); + +export const ROADMAP_MUTATION_COMMANDS: readonly string[] = ROADMAP_COMMAND_ALIASES + .filter((entry) => entry.mutation) + .flatMap((entry) => [entry.canonical, ...entry.aliases]); diff --git a/sdk/src/query/command-manifest.init.ts b/sdk/src/query/command-manifest.init.ts new file mode 100644 index 0000000000..b71af648a5 --- /dev/null +++ b/sdk/src/query/command-manifest.init.ts @@ -0,0 +1,24 @@ +import type { CommandManifestEntry } from './command-manifest.types.js'; + +/** + * Canonical init.* command manifest. + */ +export const INIT_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [ + { family: 'init', canonical: 'init.execute-phase', aliases: ['init execute-phase'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.plan-phase', aliases: ['init plan-phase'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.new-project', aliases: ['init new-project'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.new-milestone', aliases: ['init new-milestone'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.quick', aliases: ['init quick'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.ingest-docs', aliases: ['init ingest-docs'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.resume', aliases: ['init resume'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.verify-work', aliases: ['init verify-work'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.phase-op', aliases: ['init phase-op'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.todos', aliases: ['init todos'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.milestone-op', aliases: ['init milestone-op'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.map-codebase', aliases: ['init map-codebase'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.progress', aliases: ['init progress'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.manager', aliases: ['init manager'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.new-workspace', aliases: ['init new-workspace'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.list-workspaces', aliases: ['init list-workspaces'], mutation: false, outputMode: 'json' }, + { family: 'init', canonical: 'init.remove-workspace', aliases: ['init remove-workspace'], mutation: false, outputMode: 'json' }, +] as const; diff --git a/sdk/src/query/command-manifest.phase.ts b/sdk/src/query/command-manifest.phase.ts new file mode 100644 index 0000000000..f87b988a7f --- /dev/null +++ b/sdk/src/query/command-manifest.phase.ts @@ -0,0 +1,16 @@ +import type { CommandManifestEntry } from './command-manifest.types.js'; + +/** + * Canonical phase.* command manifest. + */ +export const PHASE_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [ + { family: 'phase', canonical: 'phase.list-plans', aliases: ['phase list-plans'], mutation: false, outputMode: 'json' }, + { family: 'phase', canonical: 'phase.list-artifacts', aliases: ['phase list-artifacts'], mutation: false, outputMode: 'json' }, + { family: 'phase', canonical: 'phase.next-decimal', aliases: ['phase next-decimal'], mutation: false, outputMode: 'json' }, + { family: 'phase', canonical: 'phase.add', aliases: ['phase add'], mutation: true, outputMode: 'json' }, + { family: 'phase', canonical: 'phase.add-batch', aliases: ['phase add-batch'], mutation: true, outputMode: 'json' }, + { family: 'phase', canonical: 'phase.insert', aliases: ['phase insert'], mutation: true, outputMode: 'json' }, + { family: 'phase', canonical: 'phase.remove', aliases: ['phase remove'], mutation: true, outputMode: 'json' }, + { family: 'phase', canonical: 'phase.complete', aliases: ['phase complete'], mutation: true, outputMode: 'json' }, + { family: 'phase', canonical: 'phase.scaffold', aliases: ['phase scaffold'], mutation: true, outputMode: 'json' }, +] as const; diff --git a/sdk/src/query/command-manifest.phases.ts b/sdk/src/query/command-manifest.phases.ts new file mode 100644 index 0000000000..a76e873ea4 --- /dev/null +++ b/sdk/src/query/command-manifest.phases.ts @@ -0,0 +1,11 @@ +import type { CommandManifestEntry } from './command-manifest.types.js'; + +/** + * Canonical phases.* command manifest. + * Note: `phases.archive` is SDK-only; CJS `gsd-tools phases` currently supports list/clear. + */ +export const PHASES_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [ + { family: 'phases', canonical: 'phases.list', aliases: ['phases list'], mutation: false, outputMode: 'json' }, + { family: 'phases', canonical: 'phases.clear', aliases: ['phases clear'], mutation: true, outputMode: 'json' }, + { family: 'phases', canonical: 'phases.archive', aliases: ['phases archive'], mutation: true, outputMode: 'json' }, +] as const; diff --git a/sdk/src/query/command-manifest.roadmap.ts b/sdk/src/query/command-manifest.roadmap.ts new file mode 100644 index 0000000000..23f6c78c02 --- /dev/null +++ b/sdk/src/query/command-manifest.roadmap.ts @@ -0,0 +1,11 @@ +import type { CommandManifestEntry } from './command-manifest.types.js'; + +/** + * Canonical roadmap.* command manifest. + */ +export const ROADMAP_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [ + { family: 'roadmap', canonical: 'roadmap.analyze', aliases: ['roadmap analyze'], mutation: false, outputMode: 'json' }, + { family: 'roadmap', canonical: 'roadmap.get-phase', aliases: ['roadmap get-phase'], mutation: false, outputMode: 'json' }, + { family: 'roadmap', canonical: 'roadmap.update-plan-progress', aliases: ['roadmap update-plan-progress'], mutation: true, outputMode: 'json' }, + { family: 'roadmap', canonical: 'roadmap.annotate-dependencies', aliases: ['roadmap annotate-dependencies'], mutation: true, outputMode: 'json' }, +] as const; diff --git a/sdk/src/query/command-manifest.state.ts b/sdk/src/query/command-manifest.state.ts new file mode 100644 index 0000000000..2bb6d6f15a --- /dev/null +++ b/sdk/src/query/command-manifest.state.ts @@ -0,0 +1,31 @@ +import type { CommandManifestEntry } from './command-manifest.types.js'; + +/** + * Canonical state.* command manifest. + * + * Source of truth for the state family seam. Adapters derive registry aliases, + * mutation classification, and CJS subcommand routing metadata from this list. + */ +export const STATE_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [ + { family: 'state', canonical: 'state.load', aliases: [], mutation: false, outputMode: 'raw' }, + { family: 'state', canonical: 'state.json', aliases: ['state json'], mutation: false, outputMode: 'json' }, + { family: 'state', canonical: 'state.get', aliases: ['state get'], mutation: false, outputMode: 'json' }, + { family: 'state', canonical: 'state.update', aliases: ['state update'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.patch', aliases: ['state patch'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.begin-phase', aliases: ['state begin-phase'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.advance-plan', aliases: ['state advance-plan'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.record-metric', aliases: ['state record-metric'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.update-progress', aliases: ['state update-progress'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.add-decision', aliases: ['state add-decision'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.add-blocker', aliases: ['state add-blocker'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.resolve-blocker', aliases: ['state resolve-blocker'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.record-session', aliases: ['state record-session'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.signal-waiting', aliases: ['state signal-waiting'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.signal-resume', aliases: ['state signal-resume'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.planned-phase', aliases: ['state planned-phase'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.validate', aliases: ['state validate'], mutation: false, outputMode: 'json' }, + { family: 'state', canonical: 'state.sync', aliases: ['state sync'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.prune', aliases: ['state prune'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.milestone-switch', aliases: ['state milestone-switch'], mutation: true, outputMode: 'json' }, + { family: 'state', canonical: 'state.add-roadmap-evolution', aliases: ['state add-roadmap-evolution'], mutation: true, outputMode: 'json' }, +] as const; diff --git a/sdk/src/query/command-manifest.ts b/sdk/src/query/command-manifest.ts new file mode 100644 index 0000000000..250b372ded --- /dev/null +++ b/sdk/src/query/command-manifest.ts @@ -0,0 +1,17 @@ +import { STATE_COMMAND_MANIFEST } from './command-manifest.state.js'; +import { VERIFY_COMMAND_MANIFEST } from './command-manifest.verify.js'; +import { INIT_COMMAND_MANIFEST } from './command-manifest.init.js'; +import { PHASE_COMMAND_MANIFEST } from './command-manifest.phase.js'; +import { PHASES_COMMAND_MANIFEST } from './command-manifest.phases.js'; +import { VALIDATE_COMMAND_MANIFEST } from './command-manifest.validate.js'; +import { ROADMAP_COMMAND_MANIFEST } from './command-manifest.roadmap.js'; + +export const COMMAND_MANIFEST = [ + ...STATE_COMMAND_MANIFEST, + ...VERIFY_COMMAND_MANIFEST, + ...INIT_COMMAND_MANIFEST, + ...PHASE_COMMAND_MANIFEST, + ...PHASES_COMMAND_MANIFEST, + ...VALIDATE_COMMAND_MANIFEST, + ...ROADMAP_COMMAND_MANIFEST, +] as const; diff --git a/sdk/src/query/command-manifest.types.ts b/sdk/src/query/command-manifest.types.ts new file mode 100644 index 0000000000..807f7664f5 --- /dev/null +++ b/sdk/src/query/command-manifest.types.ts @@ -0,0 +1,11 @@ +export type CommandFamily = 'state' | 'verify' | 'init' | 'phase' | 'phases' | 'validate' | 'roadmap'; + +export type OutputMode = 'json' | 'raw'; + +export interface CommandManifestEntry { + family: CommandFamily; + canonical: string; + aliases: string[]; + mutation: boolean; + outputMode: OutputMode; +} diff --git a/sdk/src/query/command-manifest.validate.ts b/sdk/src/query/command-manifest.validate.ts new file mode 100644 index 0000000000..291badfbae --- /dev/null +++ b/sdk/src/query/command-manifest.validate.ts @@ -0,0 +1,11 @@ +import type { CommandManifestEntry } from './command-manifest.types.js'; + +/** + * Canonical validate.* command manifest. + */ +export const VALIDATE_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [ + { family: 'validate', canonical: 'validate.consistency', aliases: ['validate consistency'], mutation: false, outputMode: 'json' }, + { family: 'validate', canonical: 'validate.health', aliases: ['validate health'], mutation: false, outputMode: 'json' }, + { family: 'validate', canonical: 'validate.agents', aliases: ['validate agents'], mutation: false, outputMode: 'json' }, + { family: 'validate', canonical: 'validate.context', aliases: ['validate context'], mutation: false, outputMode: 'json' }, +] as const; diff --git a/sdk/src/query/command-manifest.verify.ts b/sdk/src/query/command-manifest.verify.ts new file mode 100644 index 0000000000..7dd34ad43f --- /dev/null +++ b/sdk/src/query/command-manifest.verify.ts @@ -0,0 +1,15 @@ +import type { CommandManifestEntry } from './command-manifest.types.js'; + +/** + * Canonical verify.* command manifest. + */ +export const VERIFY_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [ + { family: 'verify', canonical: 'verify.plan-structure', aliases: ['verify plan-structure'], mutation: false, outputMode: 'json' }, + { family: 'verify', canonical: 'verify.phase-completeness', aliases: ['verify phase-completeness'], mutation: false, outputMode: 'json' }, + { family: 'verify', canonical: 'verify.references', aliases: ['verify references'], mutation: false, outputMode: 'json' }, + { family: 'verify', canonical: 'verify.commits', aliases: ['verify commits'], mutation: false, outputMode: 'json' }, + { family: 'verify', canonical: 'verify.artifacts', aliases: ['verify artifacts'], mutation: false, outputMode: 'json' }, + { family: 'verify', canonical: 'verify.key-links', aliases: ['verify key-links'], mutation: false, outputMode: 'json' }, + { family: 'verify', canonical: 'verify.schema-drift', aliases: ['verify schema-drift'], mutation: false, outputMode: 'json' }, + { family: 'verify', canonical: 'verify.codebase-drift', aliases: ['verify codebase-drift'], mutation: false, outputMode: 'json' }, +] as const; diff --git a/sdk/src/query/command-seam-coverage.test.ts b/sdk/src/query/command-seam-coverage.test.ts new file mode 100644 index 0000000000..b0bca6cb94 --- /dev/null +++ b/sdk/src/query/command-seam-coverage.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from 'vitest'; +import { createRequire } from 'node:module'; + +import { createRegistry } from './index.js'; +import { STATE_COMMAND_MANIFEST } from './command-manifest.state.js'; +import { VERIFY_COMMAND_MANIFEST } from './command-manifest.verify.js'; +import { INIT_COMMAND_MANIFEST } from './command-manifest.init.js'; +import { PHASE_COMMAND_MANIFEST } from './command-manifest.phase.js'; +import { PHASES_COMMAND_MANIFEST } from './command-manifest.phases.js'; +import { VALIDATE_COMMAND_MANIFEST } from './command-manifest.validate.js'; +import { ROADMAP_COMMAND_MANIFEST } from './command-manifest.roadmap.js'; +import { + STATE_COMMAND_ALIASES, + VERIFY_COMMAND_ALIASES, + INIT_COMMAND_ALIASES, + PHASE_COMMAND_ALIASES, + PHASES_COMMAND_ALIASES, + VALIDATE_COMMAND_ALIASES, + ROADMAP_COMMAND_ALIASES, +} from './command-aliases.generated.js'; + +function subcommandFor(canonical: string, family: 'state' | 'verify' | 'init' | 'phase' | 'phases' | 'validate' | 'roadmap'): string { + return canonical.slice(`${family}.`.length); +} + +describe('command seam coverage (manifest -> generated -> adapters)', () => { + it('state/verify/init/phase/phases/validate/roadmap manifest canonicals are present in generated alias artifacts', () => { + const generated = new Map(); + for (const entry of [...STATE_COMMAND_ALIASES, ...VERIFY_COMMAND_ALIASES, ...INIT_COMMAND_ALIASES, ...PHASE_COMMAND_ALIASES, ...PHASES_COMMAND_ALIASES, ...VALIDATE_COMMAND_ALIASES, ...ROADMAP_COMMAND_ALIASES]) { + generated.set(entry.canonical, { aliases: [...entry.aliases], subcommand: entry.subcommand, mutation: !!entry.mutation }); + } + + for (const entry of STATE_COMMAND_MANIFEST) { + const g = generated.get(entry.canonical); + expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy(); + expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'state')); + expect(g?.aliases ?? []).toEqual(entry.aliases); + expect(g?.mutation).toBe(entry.mutation); + } + + for (const entry of VERIFY_COMMAND_MANIFEST) { + const g = generated.get(entry.canonical); + expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy(); + expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'verify')); + expect(g?.aliases ?? []).toEqual(entry.aliases); + expect(g?.mutation).toBe(entry.mutation); + } + + for (const entry of INIT_COMMAND_MANIFEST) { + const g = generated.get(entry.canonical); + expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy(); + expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'init')); + expect(g?.aliases ?? []).toEqual(entry.aliases); + expect(g?.mutation).toBe(entry.mutation); + } + + for (const entry of PHASE_COMMAND_MANIFEST) { + const g = generated.get(entry.canonical); + expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy(); + expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'phase')); + expect(g?.aliases ?? []).toEqual(entry.aliases); + expect(g?.mutation).toBe(entry.mutation); + } + + for (const entry of PHASES_COMMAND_MANIFEST) { + const g = generated.get(entry.canonical); + expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy(); + expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'phases')); + expect(g?.aliases ?? []).toEqual(entry.aliases); + expect(g?.mutation).toBe(entry.mutation); + } + + for (const entry of VALIDATE_COMMAND_MANIFEST) { + const g = generated.get(entry.canonical); + expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy(); + expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'validate')); + expect(g?.aliases ?? []).toEqual(entry.aliases); + expect(g?.mutation).toBe(entry.mutation); + } + + for (const entry of ROADMAP_COMMAND_MANIFEST) { + const g = generated.get(entry.canonical); + expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy(); + expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'roadmap')); + expect(g?.aliases ?? []).toEqual(entry.aliases); + expect(g?.mutation).toBe(entry.mutation); + } + }); + + it('registry has every canonical + alias for migrated families', () => { + const registry = createRegistry(); + for (const entry of [...STATE_COMMAND_ALIASES, ...VERIFY_COMMAND_ALIASES, ...INIT_COMMAND_ALIASES, ...PHASE_COMMAND_ALIASES, ...PHASES_COMMAND_ALIASES, ...VALIDATE_COMMAND_ALIASES, ...ROADMAP_COMMAND_ALIASES]) { + expect(registry.has(entry.canonical), `missing registry canonical ${entry.canonical}`).toBe(true); + for (const alias of entry.aliases) { + expect(registry.has(alias), `missing registry alias ${alias}`).toBe(true); + } + } + }); + + it('CJS seam adapters export expected router functions', () => { + const require = createRequire(import.meta.url); + const stateRouter = require('../../../get-shit-done/bin/lib/state-command-router.cjs'); + const verifyRouter = require('../../../get-shit-done/bin/lib/verify-command-router.cjs'); + const initRouter = require('../../../get-shit-done/bin/lib/init-command-router.cjs'); + const phaseRouter = require('../../../get-shit-done/bin/lib/phase-command-router.cjs'); + const phasesRouter = require('../../../get-shit-done/bin/lib/phases-command-router.cjs'); + const validateRouter = require('../../../get-shit-done/bin/lib/validate-command-router.cjs'); + const roadmapRouter = require('../../../get-shit-done/bin/lib/roadmap-command-router.cjs'); + + expect(typeof stateRouter.routeStateCommand).toBe('function'); + expect(typeof verifyRouter.routeVerifyCommand).toBe('function'); + expect(typeof initRouter.routeInitCommand).toBe('function'); + expect(typeof phaseRouter.routePhaseCommand).toBe('function'); + expect(typeof phasesRouter.routePhasesCommand).toBe('function'); + expect(typeof validateRouter.routeValidateCommand).toBe('function'); + expect(typeof roadmapRouter.routeRoadmapCommand).toBe('function'); + }); +}); diff --git a/sdk/src/query/index.ts b/sdk/src/query/index.ts index f5183c2b29..5ef9a07f37 100644 --- a/sdk/src/query/index.ts +++ b/sdk/src/query/index.ts @@ -20,6 +20,19 @@ import { frontmatterGet } from './frontmatter.js'; import { configGet, configPath, resolveModel } from './config-query.js'; import { stateJson, stateGet, stateSnapshot } from './state.js'; import { stateProjectLoad } from './state-project-load.js'; +import { + STATE_COMMAND_ALIASES, + STATE_MUTATION_COMMANDS, + VERIFY_COMMAND_ALIASES, + INIT_COMMAND_ALIASES, + PHASE_COMMAND_ALIASES, + PHASE_MUTATION_COMMANDS, + PHASES_COMMAND_ALIASES, + PHASES_MUTATION_COMMANDS, + VALIDATE_COMMAND_ALIASES, + ROADMAP_COMMAND_ALIASES, + ROADMAP_MUTATION_COMMANDS, +} from './command-aliases.generated.js'; import { findPhase, phasePlanIndex } from './phase.js'; import { phaseListPlans, phaseListArtifacts } from './phase-list-queries.js'; import { planTaskStructure } from './plan-task-structure.js'; @@ -126,28 +139,14 @@ export { normalizeQueryCommand } from './normalize-query-command.js'; * (they emit JSON for workflows; agents perform writes). */ export const QUERY_MUTATION_COMMANDS = new Set([ - 'state.update', 'state.patch', 'state.begin-phase', 'state.advance-plan', - 'state.record-metric', 'state.update-progress', 'state.add-decision', - 'state.add-blocker', 'state.resolve-blocker', 'state.record-session', - 'state.planned-phase', 'state planned-phase', - 'state.signal-waiting', 'state signal-waiting', - 'state.signal-resume', 'state signal-resume', - 'state.sync', 'state sync', - 'state.prune', 'state prune', - 'state.milestone-switch', 'state milestone-switch', - 'state.add-roadmap-evolution', 'state add-roadmap-evolution', + ...STATE_MUTATION_COMMANDS, 'frontmatter.set', 'frontmatter.merge', 'frontmatter.validate', 'frontmatter validate', 'config-set', 'config-set-model-profile', 'config-new-project', 'config-ensure-section', 'commit', 'check-commit', 'commit-to-subrepo', 'template.fill', 'template.select', 'template select', - 'validate.health', 'validate health', - 'validate.context', 'validate context', - 'phase.add', 'phase.add-batch', 'phase.insert', 'phase.remove', 'phase.complete', - 'phase.scaffold', 'phases.clear', 'phases.archive', - 'phase add', 'phase add-batch', 'phase insert', 'phase remove', 'phase complete', - 'phase scaffold', 'phases clear', 'phases archive', - 'roadmap.update-plan-progress', 'roadmap update-plan-progress', - 'roadmap.annotate-dependencies', 'roadmap annotate-dependencies', + ...PHASE_MUTATION_COMMANDS, + ...PHASES_MUTATION_COMMANDS, + ...ROADMAP_MUTATION_COMMANDS, 'requirements.mark-complete', 'requirements mark-complete', 'todo.complete', 'todo complete', 'milestone.complete', 'milestone complete', @@ -284,22 +283,62 @@ export function createRegistry( registry.register('config-get', configGet); registry.register('config-path', configPath); registry.register('resolve-model', resolveModel); - registry.register('state.load', stateProjectLoad); - registry.register('state.json', stateJson); - registry.register('state.get', stateGet); + const stateHandlers: Record = { + 'state.load': stateProjectLoad, + 'state.json': stateJson, + 'state.get': stateGet, + 'state.update': stateUpdate, + 'state.patch': statePatch, + 'state.begin-phase': stateBeginPhase, + 'state.advance-plan': stateAdvancePlan, + 'state.record-metric': stateRecordMetric, + 'state.update-progress': stateUpdateProgress, + 'state.add-decision': stateAddDecision, + 'state.add-blocker': stateAddBlocker, + 'state.resolve-blocker': stateResolveBlocker, + 'state.record-session': stateRecordSession, + 'state.signal-waiting': stateSignalWaiting, + 'state.signal-resume': stateSignalResume, + 'state.planned-phase': statePlannedPhase, + 'state.validate': stateValidate, + 'state.sync': stateSync, + 'state.prune': statePrune, + 'state.milestone-switch': stateMilestoneSwitch, + 'state.add-roadmap-evolution': stateAddRoadmapEvolution, + }; + + for (const entry of STATE_COMMAND_ALIASES) { + const handler = stateHandlers[entry.canonical]; + if (!handler) continue; + registry.register(entry.canonical, handler); + for (const alias of entry.aliases) { + registry.register(alias, handler); + } + } + registry.register('state-snapshot', stateSnapshot); registry.register('find-phase', findPhase); registry.register('phase-plan-index', phasePlanIndex); - registry.register('phase.list-plans', phaseListPlans); - registry.register('phase list-plans', phaseListPlans); - registry.register('phase.list-artifacts', phaseListArtifacts); - registry.register('phase list-artifacts', phaseListArtifacts); registry.register('plan.task-structure', planTaskStructure); registry.register('plan task-structure', planTaskStructure); registry.register('requirements.extract-from-plans', requirementsExtractFromPlans); registry.register('requirements extract-from-plans', requirementsExtractFromPlans); - registry.register('roadmap.analyze', roadmapAnalyze); - registry.register('roadmap.get-phase', roadmapGetPhase); + const roadmapHandlers: Record = { + 'roadmap.analyze': roadmapAnalyze, + 'roadmap.get-phase': roadmapGetPhase, + 'roadmap.update-plan-progress': roadmapUpdatePlanProgress, + 'roadmap.annotate-dependencies': roadmapAnnotateDependencies, + }; + + for (const entry of ROADMAP_COMMAND_ALIASES) { + const handler = roadmapHandlers[entry.canonical]; + if (!handler) continue; + registry.register(entry.canonical, handler); + for (const alias of entry.aliases) { + registry.register(alias, handler); + } + } + registry.register('progress', progressJson); registry.register('progress.json', progressJson); @@ -309,32 +348,6 @@ export function createRegistry( registry.register('frontmatter.validate', frontmatterValidate); registry.register('frontmatter validate', frontmatterValidate); - // State mutation handlers - registry.register('state.update', stateUpdate); - registry.register('state.patch', statePatch); - registry.register('state.begin-phase', stateBeginPhase); - registry.register('state.advance-plan', stateAdvancePlan); - registry.register('state.record-metric', stateRecordMetric); - registry.register('state.update-progress', stateUpdateProgress); - registry.register('state.add-decision', stateAddDecision); - registry.register('state.add-blocker', stateAddBlocker); - registry.register('state.resolve-blocker', stateResolveBlocker); - registry.register('state.record-session', stateRecordSession); - registry.register('state.signal-waiting', stateSignalWaiting); - registry.register('state.signal-resume', stateSignalResume); - registry.register('state.validate', stateValidate); - registry.register('state.sync', stateSync); - registry.register('state.prune', statePrune); - registry.register('state.milestone-switch', stateMilestoneSwitch); - registry.register('state.add-roadmap-evolution', stateAddRoadmapEvolution); - registry.register('state milestone-switch', stateMilestoneSwitch); - registry.register('state add-roadmap-evolution', stateAddRoadmapEvolution); - registry.register('state signal-waiting', stateSignalWaiting); - registry.register('state signal-resume', stateSignalResume); - registry.register('state validate', stateValidate); - registry.register('state sync', stateSync); - registry.register('state prune', statePrune); - // Config mutation handlers registry.register('config-set', configSet); registry.register('config-set-model-profile', configSetModelProfile); @@ -350,19 +363,26 @@ export function createRegistry( registry.register('template.select', templateSelect); registry.register('template select', templateSelect); - // Verification handlers - registry.register('verify.plan-structure', verifyPlanStructure); - registry.register('verify plan-structure', verifyPlanStructure); - registry.register('verify.phase-completeness', verifyPhaseCompleteness); - registry.register('verify phase-completeness', verifyPhaseCompleteness); - registry.register('verify.artifacts', verifyArtifacts); - registry.register('verify artifacts', verifyArtifacts); - registry.register('verify.key-links', verifyKeyLinks); - registry.register('verify key-links', verifyKeyLinks); - registry.register('verify.commits', verifyCommits); - registry.register('verify commits', verifyCommits); - registry.register('verify.references', verifyReferences); - registry.register('verify references', verifyReferences); + const verifyHandlers: Record = { + 'verify.plan-structure': verifyPlanStructure, + 'verify.phase-completeness': verifyPhaseCompleteness, + 'verify.references': verifyReferences, + 'verify.commits': verifyCommits, + 'verify.artifacts': verifyArtifacts, + 'verify.key-links': verifyKeyLinks, + 'verify.schema-drift': verifySchemaDrift, + 'verify.codebase-drift': verifyCodebaseDrift, + }; + + for (const entry of VERIFY_COMMAND_ALIASES) { + const handler = verifyHandlers[entry.canonical]; + if (!handler) continue; + registry.register(entry.canonical, handler); + for (const alias of entry.aliases) { + registry.register(alias, handler); + } + } + registry.register('verify-summary', verifySummary); registry.register('verify.summary', verifySummary); registry.register('verify summary', verifySummary); @@ -377,14 +397,21 @@ export function createRegistry( registry.register('check decision-coverage-plan', checkDecisionCoveragePlan); registry.register('check.decision-coverage-verify', checkDecisionCoverageVerify); registry.register('check decision-coverage-verify', checkDecisionCoverageVerify); - registry.register('validate.consistency', validateConsistency); - registry.register('validate consistency', validateConsistency); - registry.register('validate.health', validateHealth); - registry.register('validate health', validateHealth); - registry.register('validate.agents', validateAgents); - registry.register('validate agents', validateAgents); - registry.register('validate.context', validateContext); - registry.register('validate context', validateContext); + const validateHandlers: Record = { + 'validate.consistency': validateConsistency, + 'validate.health': validateHealth, + 'validate.agents': validateAgents, + 'validate.context': validateContext, + }; + + for (const entry of VALIDATE_COMMAND_ALIASES) { + const handler = validateHandlers[entry.canonical]; + if (!handler) continue; + registry.register(entry.canonical, handler); + for (const alias of entry.aliases) { + registry.register(alias, handler); + } + } // Decision routing (SDK-only — no `gsd-tools.cjs` mirror yet; see QUERY-HANDLERS.md) registry.register('check.config-gates', checkConfigGates); @@ -406,82 +433,75 @@ export function createRegistry( registry.register('check.ship-ready', checkShipReady); registry.register('check ship-ready', checkShipReady); - // Phase lifecycle handlers - registry.register('phase.add', phaseAdd); - registry.register('phase.add-batch', phaseAddBatch); - registry.register('phase.insert', phaseInsert); - registry.register('phase.remove', phaseRemove); - registry.register('phase.complete', phaseComplete); - registry.register('phase.scaffold', phaseScaffold); - registry.register('phases.clear', phasesClear); - registry.register('phases.archive', phasesArchive); - registry.register('phases.list', phasesList); - registry.register('phase.next-decimal', phaseNextDecimal); - // Space-delimited aliases for CJS compatibility - registry.register('phase add', phaseAdd); - registry.register('phase add-batch', phaseAddBatch); - registry.register('phase insert', phaseInsert); - registry.register('phase remove', phaseRemove); - registry.register('phase complete', phaseComplete); - registry.register('phase scaffold', phaseScaffold); - registry.register('phases clear', phasesClear); - registry.register('phases archive', phasesArchive); - registry.register('phases list', phasesList); - registry.register('phase next-decimal', phaseNextDecimal); - - // Init composition handlers - registry.register('init.execute-phase', initExecutePhase); - registry.register('init.plan-phase', initPlanPhase); - registry.register('init.new-milestone', initNewMilestone); - registry.register('init.quick', initQuick); - registry.register('init.resume', initResume); - registry.register('init.verify-work', initVerifyWork); - registry.register('init.phase-op', initPhaseOp); - registry.register('init.todos', initTodos); - registry.register('init.milestone-op', initMilestoneOp); - registry.register('init.map-codebase', initMapCodebase); - registry.register('init.new-workspace', initNewWorkspace); - registry.register('init.list-workspaces', initListWorkspaces); - registry.register('init.remove-workspace', initRemoveWorkspace); - registry.register('init.ingest-docs', initIngestDocs); - // Space-delimited aliases for CJS compatibility - registry.register('init execute-phase', initExecutePhase); - registry.register('init plan-phase', initPlanPhase); - registry.register('init new-milestone', initNewMilestone); - registry.register('init quick', initQuick); - registry.register('init resume', initResume); - registry.register('init verify-work', initVerifyWork); - registry.register('init phase-op', initPhaseOp); - registry.register('init todos', initTodos); - registry.register('init milestone-op', initMilestoneOp); - registry.register('init map-codebase', initMapCodebase); - registry.register('init new-workspace', initNewWorkspace); - registry.register('init list-workspaces', initListWorkspaces); - registry.register('init remove-workspace', initRemoveWorkspace); - registry.register('init ingest-docs', initIngestDocs); - - // Complex init handlers - registry.register('init.new-project', initNewProject); - registry.register('init.progress', initProgress); - registry.register('init.manager', initManager); - registry.register('init new-project', initNewProject); - registry.register('init progress', initProgress); - registry.register('init manager', initManager); + const phaseHandlers: Record = { + 'phase.list-plans': phaseListPlans, + 'phase.list-artifacts': phaseListArtifacts, + 'phase.add': phaseAdd, + 'phase.add-batch': phaseAddBatch, + 'phase.insert': phaseInsert, + 'phase.remove': phaseRemove, + 'phase.complete': phaseComplete, + 'phase.scaffold': phaseScaffold, + 'phase.next-decimal': phaseNextDecimal, + }; + + for (const entry of PHASE_COMMAND_ALIASES) { + const handler = phaseHandlers[entry.canonical]; + if (!handler) continue; + registry.register(entry.canonical, handler); + for (const alias of entry.aliases) { + registry.register(alias, handler); + } + } + + const phasesHandlers: Record = { + 'phases.list': phasesList, + 'phases.clear': phasesClear, + 'phases.archive': phasesArchive, + }; + + for (const entry of PHASES_COMMAND_ALIASES) { + const handler = phasesHandlers[entry.canonical]; + if (!handler) continue; + registry.register(entry.canonical, handler); + for (const alias of entry.aliases) { + registry.register(alias, handler); + } + } + + const initHandlers: Record = { + 'init.execute-phase': initExecutePhase, + 'init.plan-phase': initPlanPhase, + 'init.new-project': initNewProject, + 'init.new-milestone': initNewMilestone, + 'init.quick': initQuick, + 'init.ingest-docs': initIngestDocs, + 'init.resume': initResume, + 'init.verify-work': initVerifyWork, + 'init.phase-op': initPhaseOp, + 'init.todos': initTodos, + 'init.milestone-op': initMilestoneOp, + 'init.map-codebase': initMapCodebase, + 'init.progress': initProgress, + 'init.manager': initManager, + 'init.new-workspace': initNewWorkspace, + 'init.list-workspaces': initListWorkspaces, + 'init.remove-workspace': initRemoveWorkspace, + }; + + for (const entry of INIT_COMMAND_ALIASES) { + const handler = initHandlers[entry.canonical]; + if (!handler) continue; + registry.register(entry.canonical, handler); + for (const alias of entry.aliases) { + registry.register(alias, handler); + } + } // Domain-specific handlers (fully implemented) registry.register('agent-skills', agentSkills); - registry.register('roadmap.update-plan-progress', roadmapUpdatePlanProgress); - registry.register('roadmap update-plan-progress', roadmapUpdatePlanProgress); - registry.register('roadmap.annotate-dependencies', roadmapAnnotateDependencies); - registry.register('roadmap annotate-dependencies', roadmapAnnotateDependencies); registry.register('requirements.mark-complete', requirementsMarkComplete); registry.register('requirements mark-complete', requirementsMarkComplete); - registry.register('state.planned-phase', statePlannedPhase); - registry.register('state planned-phase', statePlannedPhase); - registry.register('verify.schema-drift', verifySchemaDrift); - registry.register('verify schema-drift', verifySchemaDrift); - registry.register('verify.codebase-drift', verifyCodebaseDrift); - registry.register('verify codebase-drift', verifyCodebaseDrift); registry.register('todo.match-phase', todoMatchPhase); registry.register('todo match-phase', todoMatchPhase); registry.register('list-todos', listTodos); diff --git a/sdk/src/query/normalize-query-command.test.ts b/sdk/src/query/normalize-query-command.test.ts index bde97cdc8d..11da5abf02 100644 --- a/sdk/src/query/normalize-query-command.test.ts +++ b/sdk/src/query/normalize-query-command.test.ts @@ -7,15 +7,28 @@ describe('normalizeQueryCommand', () => { expect(normalizeQueryCommand('state', ['validate'])).toEqual(['state.validate', []]); }); + it('merges verify known subcommands only', () => { + expect(normalizeQueryCommand('verify', ['plan-structure', 'x.md'])).toEqual(['verify.plan-structure', ['x.md']]); + expect(normalizeQueryCommand('verify', ['unknown-op'])).toEqual(['verify', ['unknown-op']]); + }); + it('maps bare state to state.load', () => { expect(normalizeQueryCommand('state', [])).toEqual(['state.load', []]); }); + it('does not merge unknown state subcommands', () => { + expect(normalizeQueryCommand('state', ['not-a-subcommand'])).toEqual(['state', ['not-a-subcommand']]); + }); + it('merges init workflows', () => { expect(normalizeQueryCommand('init', ['execute-phase', '9'])).toEqual(['init.execute-phase', ['9']]); expect(normalizeQueryCommand('init', ['new-project'])).toEqual(['init.new-project', []]); }); + it('does not merge unknown init subcommands', () => { + expect(normalizeQueryCommand('init', ['made-up-init-op', 'x'])).toEqual(['init', ['made-up-init-op', 'x']]); + }); + it('maps scaffold to phase.scaffold', () => { expect(normalizeQueryCommand('scaffold', ['phase-dir', '--phase', '1'])).toEqual([ 'phase.scaffold', @@ -33,7 +46,7 @@ describe('normalizeQueryCommand', () => { expect(normalizeQueryCommand('generate-slug', ['Hello'])).toEqual(['generate-slug', ['Hello']]); }); - it('merges phase add-batch for future handler', () => { + it('merges check/route helper commands', () => { expect(normalizeQueryCommand('check', ['config-gates', 'plan-phase'])).toEqual([ 'check.config-gates', ['plan-phase'], @@ -41,10 +54,49 @@ describe('normalizeQueryCommand', () => { expect(normalizeQueryCommand('check', ['phase-ready', '3'])).toEqual(['check.phase-ready', ['3']]); expect(normalizeQueryCommand('check', ['auto-mode'])).toEqual(['check.auto-mode', []]); expect(normalizeQueryCommand('route', ['next-action'])).toEqual(['route.next-action', []]); + }); + it('merges known phase subcommands and preserves unknown ones', () => { expect(normalizeQueryCommand('phase', ['add-batch', '--descriptions', '[]'])).toEqual([ 'phase.add-batch', ['--descriptions', '[]'], ]); + expect(normalizeQueryCommand('phase', ['made-up-phase-op', 'x'])).toEqual([ + 'phase', + ['made-up-phase-op', 'x'], + ]); + }); + + it('merges known phases subcommands and preserves unknown ones', () => { + expect(normalizeQueryCommand('phases', ['clear', 'v1.0'])).toEqual([ + 'phases.clear', + ['v1.0'], + ]); + expect(normalizeQueryCommand('phases', ['made-up-phases-op', 'x'])).toEqual([ + 'phases', + ['made-up-phases-op', 'x'], + ]); + }); + + it('merges known validate subcommands and preserves unknown ones', () => { + expect(normalizeQueryCommand('validate', ['consistency'])).toEqual([ + 'validate.consistency', + [], + ]); + expect(normalizeQueryCommand('validate', ['made-up-validate-op', 'x'])).toEqual([ + 'validate', + ['made-up-validate-op', 'x'], + ]); + }); + + it('merges known roadmap subcommands and preserves unknown ones', () => { + expect(normalizeQueryCommand('roadmap', ['analyze'])).toEqual([ + 'roadmap.analyze', + [], + ]); + expect(normalizeQueryCommand('roadmap', ['made-up-roadmap-op', 'x'])).toEqual([ + 'roadmap', + ['made-up-roadmap-op', 'x'], + ]); }); }); diff --git a/sdk/src/query/normalize-query-command.ts b/sdk/src/query/normalize-query-command.ts index 78b97f5461..7be25b1920 100644 --- a/sdk/src/query/normalize-query-command.ts +++ b/sdk/src/query/normalize-query-command.ts @@ -7,16 +7,23 @@ * under `runCommand()` so two-token (and longer) invocations resolve to dotted registry names. */ +import { + STATE_SUBCOMMANDS, + VERIFY_SUBCOMMANDS, + INIT_SUBCOMMANDS, + PHASE_SUBCOMMANDS, + PHASES_SUBCOMMANDS, + VALIDATE_SUBCOMMANDS, + ROADMAP_SUBCOMMANDS, +} from './command-aliases.generated.js'; + const MERGE_FIRST_WITH_SUBCOMMAND = new Set([ 'state', 'template', 'frontmatter', 'verify', 'phase', - 'phases', - 'roadmap', 'requirements', - 'validate', 'init', 'workstream', 'intel', @@ -43,6 +50,62 @@ export function normalizeQueryCommand(command: string, args: string[]): [string, return ['state.load', []]; } + if (command === 'state' && args.length > 0) { + const sub = args[0]; + if (STATE_SUBCOMMANDS.has(sub)) { + return [`state.${sub}`, args.slice(1)]; + } + return [command, args]; + } + + if (command === 'verify' && args.length > 0) { + const sub = args[0]; + if (VERIFY_SUBCOMMANDS.has(sub)) { + return [`verify.${sub}`, args.slice(1)]; + } + return [command, args]; + } + + if (command === 'init' && args.length > 0) { + const sub = args[0]; + if (INIT_SUBCOMMANDS.has(sub)) { + return [`init.${sub}`, args.slice(1)]; + } + return [command, args]; + } + + if (command === 'phase' && args.length > 0) { + const sub = args[0]; + if (PHASE_SUBCOMMANDS.has(sub)) { + return [`phase.${sub}`, args.slice(1)]; + } + return [command, args]; + } + + if (command === 'phases' && args.length > 0) { + const sub = args[0]; + if (PHASES_SUBCOMMANDS.has(sub)) { + return [`phases.${sub}`, args.slice(1)]; + } + return [command, args]; + } + + if (command === 'validate' && args.length > 0) { + const sub = args[0]; + if (VALIDATE_SUBCOMMANDS.has(sub)) { + return [`validate.${sub}`, args.slice(1)]; + } + return [command, args]; + } + + if (command === 'roadmap' && args.length > 0) { + const sub = args[0]; + if (ROADMAP_SUBCOMMANDS.has(sub)) { + return [`roadmap.${sub}`, args.slice(1)]; + } + return [command, args]; + } + if (MERGE_FIRST_WITH_SUBCOMMAND.has(command) && args.length > 0) { const sub = args[0]; return [`${command}.${sub}`, args.slice(1)]; diff --git a/tests/gsd-sdk-query-registry-integration.test.cjs b/tests/gsd-sdk-query-registry-integration.test.cjs index e793ca418a..f8be63f175 100644 --- a/tests/gsd-sdk-query-registry-integration.test.cjs +++ b/tests/gsd-sdk-query-registry-integration.test.cjs @@ -14,6 +14,7 @@ const path = require('path'); const REPO_ROOT = path.join(__dirname, '..'); const REGISTRY_FILE = path.join(REPO_ROOT, 'sdk', 'src', 'query', 'index.ts'); +const COMMAND_ALIASES_FILE = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs'); // Prose tokens that repeatedly appear after `gsd-sdk query` in English // documentation but aren't real command names. @@ -41,9 +42,39 @@ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build']); function collectRegisteredNames() { const src = fs.readFileSync(REGISTRY_FILE, 'utf8'); const names = new Set(); + + // Static registrations in index.ts const re = /registry\.register\(\s*['"]([^'"]+)['"]/g; let m; while ((m = re.exec(src)) !== null) names.add(m[1]); + + // Manifest-generated family aliases registered via loop in index.ts. + // Keep this in sync with command-manifest-driven routing seams. + try { + // eslint-disable-next-line global-require, import/no-dynamic-require + const aliases = require(COMMAND_ALIASES_FILE); + const familyArrays = [ + aliases.STATE_COMMAND_ALIASES, + aliases.VERIFY_COMMAND_ALIASES, + aliases.INIT_COMMAND_ALIASES, + aliases.PHASE_COMMAND_ALIASES, + aliases.PHASES_COMMAND_ALIASES, + aliases.VALIDATE_COMMAND_ALIASES, + aliases.ROADMAP_COMMAND_ALIASES, + ]; + for (const arr of familyArrays) { + if (!Array.isArray(arr)) continue; + for (const entry of arr) { + if (entry?.canonical) names.add(entry.canonical); + if (Array.isArray(entry?.aliases)) { + for (const alias of entry.aliases) names.add(alias); + } + } + } + } catch { + // If generated aliases are unavailable, fall back to static extraction only. + } + return names; }