@opengsd/gsd-core 1.20.1 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +234 -126
- package/commands/gsd/autonomous.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +450 -753
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +80 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +13 -11
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +11 -19
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +107 -31
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +328 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -4
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +91 -41
- package/gsd-core/bin/lib/state.cjs +446 -61
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -2
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +8 -5
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +97 -17
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +3 -2
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +8 -5
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +97 -17
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +55 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
* --schema plan|summary|verification
|
|
170
170
|
*
|
|
171
171
|
* Verification Suite:
|
|
172
|
-
* verify plan-structure <file>
|
|
172
|
+
* verify plan-structure <file> [--replay] Check PLAN.md structure + tasks
|
|
173
173
|
* verify phase-completeness <phase> Check all plans have summaries
|
|
174
174
|
* verify references <file> Check @-refs + paths resolve
|
|
175
175
|
* verify commits <h1> [h2] ... Batch verify commit hashes
|
|
@@ -296,6 +296,7 @@ const { ExitError, runMain, resolveContractVersion } = require('./lib/cli-exit.c
|
|
|
296
296
|
const io = require('./lib/io.cjs');
|
|
297
297
|
const { error, ERROR_REASON, setJsonErrorMode, output, formatDiagnosticToken, captureStdoutSyncWrites, resolveAtFileOutput } = io;
|
|
298
298
|
const projectRoot = require('./lib/project-root.cjs');
|
|
299
|
+
const { resolveProjectContext, legacyDispatchCwd } = require('./lib/project-context.cjs');
|
|
299
300
|
// Resolve findProjectRoot lazily at call time rather than binding it at module
|
|
300
301
|
// load. It is sourced from project-root.cjs; a call-time lookup is robust
|
|
301
302
|
// against any require/load-ordering edge where the export isn't bound yet
|
|
@@ -371,10 +372,36 @@ const { routeAgentCommand, AGENT_FAILURE_CLASSES } = require('./lib/agent-comman
|
|
|
371
372
|
const smartEntryMod = require('./lib/smart-entry.cjs');
|
|
372
373
|
const { routeCheckCommand } = require('./lib/check-command-router.cjs');
|
|
373
374
|
const { routeTaskCommand } = require('./lib/task-command-router.cjs');
|
|
374
|
-
const { parseNamedArgsOrExit,
|
|
375
|
+
const { parseNamedArgsOrExit, isFlagToken } = require('./lib/command-arg-projection.cjs');
|
|
376
|
+
const { specFor, GLOBAL_FLAGS } = require('./lib/cli-surface.cjs');
|
|
375
377
|
const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
|
|
376
378
|
const { getEffectiveAuthority, classifyDriftSeverity, comparePhaseStatus } = require('./lib/plan-drift-guard.cjs');
|
|
377
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Free positional words from `args[from..]` for a spec that sets `freePositionals`: every
|
|
382
|
+
* non-flag token that is not the value of a declared value flag. The walk mirrors the seam's
|
|
383
|
+
* own advance (a value flag takes the next token unless that is flag-shaped; an eq-form token
|
|
384
|
+
* takes none), so a word is never mistaken for a flag value or the reverse. Flag names come
|
|
385
|
+
* from the spec, never from literals.
|
|
386
|
+
*/
|
|
387
|
+
function positionalWords(args, spec, from = 1) {
|
|
388
|
+
const valued = new Set([...(spec.valueFlags ?? []), ...(spec.optionalValueFlags ?? [])]);
|
|
389
|
+
const eq = new Set(spec.eqFlags ?? []);
|
|
390
|
+
const words = [];
|
|
391
|
+
for (let i = from; i < args.length; i++) {
|
|
392
|
+
const tok = args[i];
|
|
393
|
+
if (!isFlagToken(tok)) {
|
|
394
|
+
words.push(tok);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const eqAt = tok.indexOf('=');
|
|
398
|
+
if (eqAt > 2 && eq.has(tok.slice(2, eqAt))) continue;
|
|
399
|
+
const next = args[i + 1];
|
|
400
|
+
if (valued.has(tok.slice(2)) && next !== undefined && !isFlagToken(next)) i += 1;
|
|
401
|
+
}
|
|
402
|
+
return words;
|
|
403
|
+
}
|
|
404
|
+
|
|
378
405
|
// ─── Bridge collapsed (Phase 4) ────────────────────────────────────────────────
|
|
379
406
|
// Non-family commands now run through their CJS handlers directly. Keep the
|
|
380
407
|
// helper contract so existing call sites remain unchanged during the phase
|
|
@@ -662,116 +689,49 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
662
689
|
// arrive via the destructured context.
|
|
663
690
|
|
|
664
691
|
function routeResolveModel({ args, cwd, raw }) {
|
|
692
|
+
parseNamedArgsOrExit(args, specFor('resolve-model'), error);
|
|
665
693
|
commands.cmdResolveModel(cwd, args[1], raw);
|
|
666
694
|
}
|
|
667
695
|
|
|
668
696
|
function routeResolveGranularity({ args, cwd, raw }) {
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (granOverride === undefined) { granOverride = granArgs[++i]; } else { ++i; }
|
|
676
|
-
} else {
|
|
677
|
-
granPositionals.push(a);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
commands.cmdResolveGranularity(cwd, granPositionals[0], raw, granOverride);
|
|
697
|
+
const spec = specFor('resolve-granularity');
|
|
698
|
+
const parsed = parseNamedArgsOrExit(args, spec, error);
|
|
699
|
+
// The phase-type positional may sit before or after --granularity: it is the first
|
|
700
|
+
// non-flag token that is not the value --granularity consumed.
|
|
701
|
+
const [positional] = positionalWords(args, spec);
|
|
702
|
+
commands.cmdResolveGranularity(cwd, positional, raw, parsed.granularity ?? undefined);
|
|
681
703
|
}
|
|
682
704
|
|
|
683
705
|
function routeResolveExecution({ args, cwd, raw }) {
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
let fastModeOverride;
|
|
687
|
-
let attempt;
|
|
688
|
-
let failureClass;
|
|
689
|
-
let host;
|
|
690
|
-
const positionals = [];
|
|
706
|
+
const spec = specFor('resolve-execution');
|
|
707
|
+
const parsed = parseNamedArgsOrExit(args, spec, error);
|
|
691
708
|
// #2296: the valid classes come from the classifier's own frozen enum, so
|
|
692
709
|
// this validator can never drift from what `agent classify-failure` emits.
|
|
693
710
|
const validFailureClasses = Object.values(AGENT_FAILURE_CLASSES);
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
failureClass = v;
|
|
702
|
-
};
|
|
703
|
-
for (let i = 0; i < execArgs.length; i++) {
|
|
704
|
-
const a = execArgs[i];
|
|
705
|
-
if (a.startsWith('--effort=')) {
|
|
706
|
-
effortOverride = a.slice('--effort='.length);
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
if (a.startsWith('--fast-mode=')) {
|
|
710
|
-
const v = a.slice('--fast-mode='.length);
|
|
711
|
-
fastModeOverride = v === 'true' ? true : v === 'false' ? false : undefined;
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
if (a.startsWith('--attempt=')) {
|
|
715
|
-
const v = a.slice('--attempt='.length);
|
|
716
|
-
const n = parseInt(v, 10);
|
|
717
|
-
if (!Number.isInteger(n) || n < 0) error('--attempt requires a non-negative integer', ERROR_REASON.USAGE);
|
|
718
|
-
attempt = n;
|
|
719
|
-
continue;
|
|
720
|
-
}
|
|
721
|
-
if (a.startsWith('--failure-class=')) {
|
|
722
|
-
setFailureClass(a.slice('--failure-class='.length));
|
|
723
|
-
continue;
|
|
724
|
-
}
|
|
725
|
-
if (a === '--effort') {
|
|
726
|
-
const val = execArgs[i + 1];
|
|
727
|
-
if (val === undefined || val.startsWith('--')) error('Missing value for --effort', ERROR_REASON.USAGE);
|
|
728
|
-
effortOverride = val;
|
|
729
|
-
i++;
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
if (a === '--fast-mode') {
|
|
733
|
-
const val = execArgs[i + 1];
|
|
734
|
-
if (val === undefined || val.startsWith('--')) error('Missing value for --fast-mode', ERROR_REASON.USAGE);
|
|
735
|
-
fastModeOverride = val === 'true' ? true : val === 'false' ? false : undefined;
|
|
736
|
-
i++;
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
if (a === '--attempt') {
|
|
740
|
-
const val = execArgs[i + 1];
|
|
741
|
-
if (val === undefined || val.startsWith('--')) error('Missing value for --attempt', ERROR_REASON.USAGE);
|
|
742
|
-
const n = parseInt(val, 10);
|
|
743
|
-
if (!Number.isInteger(n) || n < 0) error('--attempt requires a non-negative integer', ERROR_REASON.USAGE);
|
|
744
|
-
attempt = n;
|
|
745
|
-
i++;
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
if (a === '--failure-class') {
|
|
749
|
-
const val = execArgs[i + 1];
|
|
750
|
-
if (val === undefined || val.startsWith('--')) error('Missing value for --failure-class', ERROR_REASON.USAGE);
|
|
751
|
-
setFailureClass(val);
|
|
752
|
-
i++;
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
if (a === '--host') {
|
|
756
|
-
const val = execArgs[i + 1];
|
|
757
|
-
if (val === undefined || val.startsWith('--')) error('Missing value for --host', ERROR_REASON.USAGE);
|
|
758
|
-
host = val;
|
|
759
|
-
i++;
|
|
760
|
-
continue;
|
|
761
|
-
}
|
|
762
|
-
if (a === '--raw') continue;
|
|
763
|
-
if (a.startsWith('-')) error(`Unknown flag for resolve-execution: ${a}`, ERROR_REASON.USAGE);
|
|
764
|
-
positionals.push(a);
|
|
711
|
+
const failureClass = parsed['failure-class'] ?? undefined;
|
|
712
|
+
if (failureClass !== undefined && !validFailureClasses.includes(failureClass)) {
|
|
713
|
+
error(
|
|
714
|
+
`--failure-class must be one of: ${validFailureClasses.join(', ')}`,
|
|
715
|
+
ERROR_REASON.USAGE,
|
|
716
|
+
);
|
|
765
717
|
}
|
|
718
|
+
const fastMode = parsed['fast-mode'];
|
|
719
|
+
const fastModeOverride = fastMode === 'true' ? true : fastMode === 'false' ? false : undefined;
|
|
720
|
+
let attempt;
|
|
721
|
+
if (parsed.attempt !== null) {
|
|
722
|
+
attempt = parseInt(parsed.attempt, 10);
|
|
723
|
+
if (!Number.isInteger(attempt) || attempt < 0) error('--attempt requires a non-negative integer', ERROR_REASON.USAGE);
|
|
724
|
+
}
|
|
725
|
+
const positionals = positionalWords(args, spec);
|
|
766
726
|
if (positionals.length === 0) error('agent-type required', ERROR_REASON.USAGE);
|
|
767
727
|
if (positionals.length > 1) error(`resolve-execution requires exactly one agent-type argument; got: ${positionals.join(', ')}`, ERROR_REASON.USAGE);
|
|
768
728
|
const agentTypeArg = positionals[0];
|
|
769
729
|
commands.cmdResolveExecution(cwd, agentTypeArg, raw, {
|
|
770
|
-
effortOverride,
|
|
730
|
+
effortOverride: parsed.effort ?? undefined,
|
|
771
731
|
fastModeOverride,
|
|
772
732
|
attempt,
|
|
773
733
|
failureClass,
|
|
774
|
-
host,
|
|
734
|
+
host: parsed.host ?? undefined,
|
|
775
735
|
});
|
|
776
736
|
}
|
|
777
737
|
|
|
@@ -802,6 +762,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
802
762
|
}
|
|
803
763
|
|
|
804
764
|
function routeConfigSet({ args, cwd, raw }) {
|
|
765
|
+
const parsed = parseNamedArgsOrExit(args, specFor('config-set'), error);
|
|
805
766
|
const handled = _dispatchNonFamily({
|
|
806
767
|
registryCommand: 'config-set',
|
|
807
768
|
registryArgs: args.slice(1),
|
|
@@ -812,7 +773,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
812
773
|
error,
|
|
813
774
|
output: output,
|
|
814
775
|
});
|
|
815
|
-
if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw, { dryRun:
|
|
776
|
+
if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw, { dryRun: parsed['dry-run'] });
|
|
816
777
|
}
|
|
817
778
|
|
|
818
779
|
function routeConfigSetModelProfile({ args, cwd, raw }) {
|
|
@@ -847,6 +808,9 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
847
808
|
}
|
|
848
809
|
|
|
849
810
|
function routeConfigNewProject({ args, cwd, raw }) {
|
|
811
|
+
// A flag is a dash followed by a letter (`-x`, `--force`); `-1` is a negative-number JSON argument.
|
|
812
|
+
// The spec's rejectShortFlags/unknownFlagMessage refuse both forms with today's wording (#5268 pins both).
|
|
813
|
+
const parsed = parseNamedArgsOrExit(args, specFor('config-new-project'), error);
|
|
850
814
|
const handled = _dispatchNonFamily({
|
|
851
815
|
registryCommand: 'config-new-project',
|
|
852
816
|
registryArgs: args.slice(1),
|
|
@@ -859,42 +823,38 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
859
823
|
});
|
|
860
824
|
if (handled) return;
|
|
861
825
|
// #5268: this verb's flag surface is closed, as `todo complete`'s is
|
|
862
|
-
// (#4096). `--dry-run` is
|
|
863
|
-
// instead of being silently
|
|
864
|
-
// wrote the config it was meant
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
const CONFIG_NEW_PROJECT_KNOWN_FLAGS = new Set(['--dry-run', '--raw']);
|
|
871
|
-
const positionals = [];
|
|
872
|
-
for (const a of args.slice(1)) {
|
|
873
|
-
if (typeof a === 'string' && /^--?[A-Za-z]/.test(a)) {
|
|
874
|
-
if (!CONFIG_NEW_PROJECT_KNOWN_FLAGS.has(a)) {
|
|
875
|
-
error(`Unknown flag for config-new-project: ${a}`, ERROR_REASON.USAGE);
|
|
876
|
-
}
|
|
877
|
-
continue;
|
|
878
|
-
}
|
|
879
|
-
positionals.push(a);
|
|
880
|
-
}
|
|
826
|
+
// (#4096). `--dry-run` is the only flag (declared in the CLI surface registry),
|
|
827
|
+
// and any OTHER flag fails loudly in the seam instead of being silently
|
|
828
|
+
// ignored: an accepted-but-ignored `--dry-run` wrote the config it was meant
|
|
829
|
+
// to preview. The choices JSON is the one positional argument, wherever the
|
|
830
|
+
// flag sits (the spec's freePositionals), and a second one is refused for the
|
|
831
|
+
// same reason. `-1` is a (negative-number) JSON argument, refused below as
|
|
832
|
+
// choices that are not an object, not a flag.
|
|
833
|
+
const positionals = args.slice(1).filter((a) => !isFlagToken(a));
|
|
881
834
|
if (positionals.length > 1) {
|
|
882
835
|
error(`Unexpected argument for config-new-project: ${positionals[1]}. Usage: config-new-project [choices-json] [--dry-run]`, ERROR_REASON.USAGE);
|
|
883
836
|
}
|
|
884
|
-
config.cmdConfigNewProject(cwd, positionals[0], raw, { dryRun:
|
|
837
|
+
config.cmdConfigNewProject(cwd, positionals[0], raw, { dryRun: parsed['dry-run'] });
|
|
885
838
|
}
|
|
886
839
|
|
|
887
|
-
function routeConfigPath({ cwd, raw, workstreamContext }) {
|
|
840
|
+
function routeConfigPath({ args, cwd, raw, workstreamContext }) {
|
|
841
|
+
parseNamedArgsOrExit(args, specFor('config-path'), error);
|
|
888
842
|
config.cmdConfigPath(cwd, raw, workstreamContext);
|
|
889
843
|
}
|
|
890
844
|
|
|
891
|
-
async function routeMigrateConfig({ cwd, raw }) {
|
|
845
|
+
async function routeMigrateConfig({ args, cwd, raw }) {
|
|
846
|
+
parseNamedArgsOrExit(args, specFor('migrate-config'), error);
|
|
892
847
|
await config.cmdMigrateConfig(cwd, raw);
|
|
893
848
|
}
|
|
894
849
|
|
|
895
850
|
function routeResearchStore({ args, cwd, raw }) {
|
|
896
|
-
const researchStore = require('./lib/research-store.cjs');
|
|
897
851
|
const subcommand = args[1];
|
|
852
|
+
if (subcommand !== 'get' && subcommand !== 'put') {
|
|
853
|
+
error('Unknown research-store subcommand. Available: get, put', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const parsed = parseNamedArgsOrExit(args, specFor(`research-store.${subcommand}`), error);
|
|
857
|
+
const researchStore = require('./lib/research-store.cjs');
|
|
898
858
|
const homeDir = process.env.HOME || require('os').homedir();
|
|
899
859
|
if (subcommand === 'get') {
|
|
900
860
|
const key = args[2];
|
|
@@ -906,7 +866,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
906
866
|
}
|
|
907
867
|
const result = researchStore.getResearch(cwd, key, { homeDir });
|
|
908
868
|
output(result, raw);
|
|
909
|
-
} else
|
|
869
|
+
} else {
|
|
910
870
|
const key = args[2];
|
|
911
871
|
if (!key || key.startsWith('--')) {
|
|
912
872
|
error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
|
|
@@ -914,41 +874,15 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
914
874
|
if (!researchStore.isValidResearchKey(key)) {
|
|
915
875
|
error('research-store: <key> must be a 64-char sha256 hex (use research-plan to obtain keys)', ERROR_REASON.USAGE);
|
|
916
876
|
}
|
|
917
|
-
const
|
|
918
|
-
const sourceIdx = args.indexOf('--source');
|
|
919
|
-
const providerIdx = args.indexOf('--provider');
|
|
920
|
-
const confidenceIdx = args.indexOf('--confidence');
|
|
921
|
-
const kindIdx = args.indexOf('--kind');
|
|
922
|
-
function getFlagValue(idx, flagName) {
|
|
923
|
-
if (idx === -1) return null;
|
|
924
|
-
const val = args[idx + 1];
|
|
925
|
-
if (val === undefined || val.startsWith('--')) {
|
|
926
|
-
error(`research-store put: missing value for ${flagName}`, ERROR_REASON.USAGE);
|
|
927
|
-
}
|
|
928
|
-
return val;
|
|
929
|
-
}
|
|
930
|
-
const content = getFlagValue(contentIdx, '--content');
|
|
931
|
-
const source = getFlagValue(sourceIdx, '--source');
|
|
932
|
-
const provider = getFlagValue(providerIdx, '--provider');
|
|
933
|
-
const confidence = getFlagValue(confidenceIdx, '--confidence');
|
|
934
|
-
const kind = getFlagValue(kindIdx, '--kind');
|
|
935
|
-
if (!content || !source || !provider || !confidence || !kind) {
|
|
936
|
-
error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
|
|
937
|
-
}
|
|
877
|
+
const { content, source, provider, confidence, kind } = parsed;
|
|
938
878
|
const entry = researchStore.putResearch(cwd, key, { content, source, provider, confidence, kind }, { homeDir });
|
|
939
879
|
output(entry, raw);
|
|
940
|
-
} else {
|
|
941
|
-
error('Unknown research-store subcommand. Available: get, put', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
942
880
|
}
|
|
943
881
|
}
|
|
944
882
|
|
|
945
883
|
function routeResearchPlan({ args, cwd, raw }) {
|
|
884
|
+
const { input: inputPath } = parseNamedArgsOrExit(args, specFor('research-plan'), error);
|
|
946
885
|
const researchProvider = require('./lib/research-provider.cjs');
|
|
947
|
-
const inputIdx = args.indexOf('--input');
|
|
948
|
-
const inputPath = inputIdx !== -1 ? args[inputIdx + 1] : null;
|
|
949
|
-
if (!inputPath || inputPath.startsWith('--')) {
|
|
950
|
-
error('Usage: gsd-tools research-plan --input <path>', ERROR_REASON.USAGE);
|
|
951
|
-
}
|
|
952
886
|
let planInput;
|
|
953
887
|
try {
|
|
954
888
|
const raw_ = fs.readFileSync(path.resolve(inputPath), 'utf8');
|
|
@@ -1034,7 +968,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1034
968
|
*/
|
|
1035
969
|
function routeSelectRevertCommits({ args, cwd, raw, error, invokingCwd }) {
|
|
1036
970
|
// #4906/#4465 review fix: `cwd` here has already been remapped to the
|
|
1037
|
-
// MAIN worktree root by `resolveMainWorktreeCwd` (
|
|
971
|
+
// MAIN worktree root by the retired `resolveMainWorktreeCwd` (now legacyDispatchCwd, applied
|
|
1038
972
|
// BLANKET to every command before dispatch — correct for a router that
|
|
1039
973
|
// reads `.planning/`, e.g. `find-phase`, but wrong for this one). This
|
|
1040
974
|
// command reads NO `.planning/` content — it only runs `git log` — and
|
|
@@ -1086,11 +1020,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1086
1020
|
// guard below is therefore exactly the safe/unsafe boundary.
|
|
1087
1021
|
const normalizePaddedPhase = (id) => (/^\d/.test(id) ? normalizePhaseName(id) : id);
|
|
1088
1022
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
{ valueFlags: ['phase', 'plan', 'range'], positionals: 1 },
|
|
1092
|
-
error,
|
|
1093
|
-
);
|
|
1023
|
+
// `--range` is a required flag of the spec, so the seam refuses a missing one.
|
|
1024
|
+
const parsed = parseNamedArgsOrExit(args, specFor('select-revert-commits'), error);
|
|
1094
1025
|
const rawPhase = parsed.phase;
|
|
1095
1026
|
const rawPlan = parsed.plan;
|
|
1096
1027
|
const range = parsed.range;
|
|
@@ -1099,10 +1030,6 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1099
1030
|
error('select-revert-commits requires exactly one of --phase <id> or --plan <id>', ERROR_REASON.USAGE);
|
|
1100
1031
|
return;
|
|
1101
1032
|
}
|
|
1102
|
-
if (!range) {
|
|
1103
|
-
error('select-revert-commits requires --range <git-revision-range>', ERROR_REASON.USAGE);
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
1033
|
// Belt-and-suspenders alongside the --end-of-options guard below (mirrors
|
|
1107
1034
|
// src/git-base-branch.cts's isSafeRevisionRef posture for the same class
|
|
1108
1035
|
// of argument): refuse an option-shaped range before it ever reaches git.
|
|
@@ -1212,9 +1139,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1212
1139
|
output(result, raw, rawLines);
|
|
1213
1140
|
}
|
|
1214
1141
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1142
|
+
// #6043 (ADR-4908 §3): the one sanctioned source of roots for workflow shell.
|
|
1143
|
+
// Prints the ProjectContext the dispatcher resolved once for this process;
|
|
1144
|
+
// `--raw` prints the worktree root.
|
|
1145
|
+
function routeProjectContext({ args, raw, error, ctx }) {
|
|
1146
|
+
parseNamedArgsOrExit(args, specFor('project-context'), error);
|
|
1147
|
+
output(ctx, raw, ctx.worktreeRoot);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function routeCommit({ args, cwd, raw, error, ctx }) {
|
|
1151
|
+
const spec = specFor('commit');
|
|
1152
|
+
const parsed = parseNamedArgsOrExit(args, spec, error);
|
|
1218
1153
|
// #4208: `--files` and `--files-removed` are two path lists, each
|
|
1219
1154
|
// running from its flag to the NEXT LIST FLAG. A boolean flag
|
|
1220
1155
|
// inside a list (`--files a --amend b`) is skipped, not a
|
|
@@ -1226,27 +1161,29 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1226
1161
|
// A REPEATED list flag (`--files a --files b`) merges, as the old
|
|
1227
1162
|
// slice-to-end parse merged it: every occurrence contributes its
|
|
1228
1163
|
// run, and none of them ends another's silently.
|
|
1229
|
-
|
|
1230
|
-
|
|
1164
|
+
// The seam's `listFlags` extraction implements exactly that contract.
|
|
1165
|
+
const listTokens = new Set(spec.listFlags.map((f) => `--${f}`));
|
|
1166
|
+
const firstListFlag = args.findIndex((a, i) => i > 0 && listTokens.has(a));
|
|
1167
|
+
// Collect all positional args between command name and first list flag,
|
|
1231
1168
|
// then join them — handles both quoted ("multi word msg") and
|
|
1232
1169
|
// unquoted (multi word msg) invocations from different shells
|
|
1233
|
-
const
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
const files = collectListFlagValues(args, '--files');
|
|
1237
|
-
const filesRemoved = collectListFlagValues(args, '--files-removed');
|
|
1238
|
-
commands.cmdCommit(cwd, message, files, raw, amend, noVerify, filesRemoved);
|
|
1170
|
+
const head = firstListFlag !== -1 ? args.slice(0, firstListFlag) : args;
|
|
1171
|
+
const message = positionalWords(head, spec).join(' ') || undefined;
|
|
1172
|
+
commands.cmdCommit(cwd, message, parsed.files, raw, parsed.amend, parsed['no-verify'], parsed['files-removed'], ctx);
|
|
1239
1173
|
}
|
|
1240
1174
|
|
|
1241
1175
|
function routeCheckCommit({ args, cwd, raw, error }) {
|
|
1176
|
+
parseNamedArgsOrExit(args, specFor('check-commit'), error);
|
|
1242
1177
|
commands.cmdCheckCommit(cwd, raw);
|
|
1243
1178
|
}
|
|
1244
1179
|
|
|
1245
1180
|
function routeCommitDocsGuard({ args, cwd, raw, error }) {
|
|
1246
1181
|
const subcommand = args[1];
|
|
1247
1182
|
if (subcommand === 'enable') {
|
|
1183
|
+
parseNamedArgsOrExit(args, specFor('commit-docs-guard.enable'), error);
|
|
1248
1184
|
commands.cmdCommitDocsGuardEnable(cwd, raw);
|
|
1249
1185
|
} else if (subcommand === 'disable') {
|
|
1186
|
+
parseNamedArgsOrExit(args, specFor('commit-docs-guard.disable'), error);
|
|
1250
1187
|
commands.cmdCommitDocsGuardDisable(cwd, raw);
|
|
1251
1188
|
} else {
|
|
1252
1189
|
error('Unknown commit-docs-guard subcommand. Available: enable, disable', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
@@ -1254,41 +1191,46 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1254
1191
|
}
|
|
1255
1192
|
|
|
1256
1193
|
function routeCommitToSubrepo({ args, cwd, raw, error }) {
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
|
|
1260
|
-
commands.cmdCommitToSubrepo(cwd, message, files, raw);
|
|
1194
|
+
const parsed = parseNamedArgsOrExit(args, specFor('commit-to-subrepo'), error);
|
|
1195
|
+
commands.cmdCommitToSubrepo(cwd, args[1], parsed.files, raw);
|
|
1261
1196
|
}
|
|
1262
1197
|
|
|
1263
1198
|
function routePrSubrepo({ args, cwd, raw, error }) {
|
|
1264
|
-
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
// message
|
|
1272
|
-
//
|
|
1273
|
-
const
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1199
|
+
// #3884: the commit message is an optional leading positional the caller
|
|
1200
|
+
// owns (args[1]); when it is OMITTED, args[1] is itself the first flag
|
|
1201
|
+
// (e.g. `--repo`). The spec makes words past the verb free, so a flag's own
|
|
1202
|
+
// value is never mistaken for a trailing positional before cmdPrSubrepo's
|
|
1203
|
+
// own "commit message required" guard runs.
|
|
1204
|
+
const prSubrepoSpec = specFor('pr-subrepo');
|
|
1205
|
+
const { repo, branch } = parseNamedArgsOrExit(args, prSubrepoSpec, error);
|
|
1206
|
+
// At most the one optional message word may stand outside a flag: a second free word is the same
|
|
1207
|
+
// stray positional the strict boundary (2 with a message, else 1) reported.
|
|
1208
|
+
const prSubrepoWords = positionalWords(args, prSubrepoSpec);
|
|
1209
|
+
const prSubrepoMessageCount = args[1] !== undefined && !isFlagToken(args[1]) ? 1 : 0;
|
|
1210
|
+
if (prSubrepoWords.length > prSubrepoMessageCount) {
|
|
1211
|
+
error(
|
|
1212
|
+
`unexpected positional argument ${formatDiagnosticToken(prSubrepoWords[prSubrepoMessageCount])}`,
|
|
1213
|
+
ERROR_REASON.USAGE,
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
commands.cmdPrSubrepo(cwd, repo, branch, args[1], raw);
|
|
1276
1217
|
}
|
|
1277
1218
|
|
|
1278
1219
|
function routeVerifySummary({ args, cwd, raw, error }) {
|
|
1220
|
+
const parsed = parseNamedArgsOrExit(args, specFor('verify-summary'), error);
|
|
1279
1221
|
const summaryPath = args[1];
|
|
1280
|
-
const
|
|
1281
|
-
const checkCount = countIndex !== -1 ? parseInt(args[countIndex + 1], 10) : 2;
|
|
1222
|
+
const checkCount = parsed['check-count'] !== null ? parseInt(parsed['check-count'], 10) : 2;
|
|
1282
1223
|
verify.cmdVerifySummary(cwd, summaryPath, checkCount, raw);
|
|
1283
1224
|
}
|
|
1284
1225
|
|
|
1285
1226
|
function routeTemplate({ args, cwd, raw, error }) {
|
|
1286
1227
|
const subcommand = args[1];
|
|
1287
1228
|
if (subcommand === 'select') {
|
|
1229
|
+
parseNamedArgsOrExit(args, specFor('template.select'), error);
|
|
1288
1230
|
template.cmdTemplateSelect(cwd, args[2], raw);
|
|
1289
1231
|
} else if (subcommand === 'fill') {
|
|
1290
1232
|
const templateType = args[2];
|
|
1291
|
-
const { phase, plan, name, type, wave, fields: fieldsRaw } = parseNamedArgsOrExit(args,
|
|
1233
|
+
const { phase, plan, name, type, wave, fields: fieldsRaw } = parseNamedArgsOrExit(args, specFor('template.fill'), error);
|
|
1292
1234
|
let fields = {};
|
|
1293
1235
|
if (fieldsRaw) {
|
|
1294
1236
|
const { safeJsonParse } = require('./lib/security.cjs');
|
|
@@ -1337,14 +1279,23 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1337
1279
|
}
|
|
1338
1280
|
// CJS fallback (SDK unavailable or unknown subcommand)
|
|
1339
1281
|
if (subcommand === 'get') {
|
|
1340
|
-
|
|
1282
|
+
// The field is `--field <k>` or one positional word after the file (shipped callers pass
|
|
1283
|
+
// `frontmatter.get <file> <field>`); a second word, or a word beside --field, is a stray.
|
|
1284
|
+
const fmGetSpec = specFor('frontmatter.get');
|
|
1285
|
+
const fmGetParsed = parseNamedArgsOrExit(args, fmGetSpec, error);
|
|
1286
|
+
const fmGetWords = positionalWords(args, fmGetSpec, 3);
|
|
1287
|
+
const fmGetAllowed = fmGetParsed.field === null ? 1 : 0;
|
|
1288
|
+
if (fmGetWords.length > fmGetAllowed) {
|
|
1289
|
+
error(`unexpected positional argument ${formatDiagnosticToken(fmGetWords[fmGetAllowed])}`, ERROR_REASON.USAGE);
|
|
1290
|
+
}
|
|
1291
|
+
frontmatter.cmdFrontmatterGet(cwd, file, fmGetParsed.field ?? fmGetWords[0], raw);
|
|
1341
1292
|
} else if (subcommand === 'set') {
|
|
1342
|
-
const { field, value } = parseNamedArgsOrExit(args,
|
|
1293
|
+
const { field, value } = parseNamedArgsOrExit(args, specFor('frontmatter.set'), error);
|
|
1343
1294
|
frontmatter.cmdFrontmatterSet(cwd, file, field, value !== null ? value : undefined, raw);
|
|
1344
1295
|
} else if (subcommand === 'merge') {
|
|
1345
|
-
frontmatter.cmdFrontmatterMerge(cwd, file, parseNamedArgsOrExit(args,
|
|
1296
|
+
frontmatter.cmdFrontmatterMerge(cwd, file, parseNamedArgsOrExit(args, specFor('frontmatter.merge'), error).data, raw);
|
|
1346
1297
|
} else if (subcommand === 'validate') {
|
|
1347
|
-
frontmatter.cmdFrontmatterValidate(cwd, file, parseNamedArgsOrExit(args,
|
|
1298
|
+
frontmatter.cmdFrontmatterValidate(cwd, file, parseNamedArgsOrExit(args, specFor('frontmatter.validate'), error).schema, raw);
|
|
1348
1299
|
} else {
|
|
1349
1300
|
error('Unknown frontmatter subcommand. Available: get, set, merge, validate', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1350
1301
|
}
|
|
@@ -1385,10 +1336,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1385
1336
|
// Rationale: it is a pure local formatter and avoids SDK bridge startup
|
|
1386
1337
|
// in tight subprocess loops where Windows CI has shown intermittent
|
|
1387
1338
|
// native crashes (0xC0000005 / 3221225477).
|
|
1339
|
+
parseNamedArgsOrExit(args, specFor('current-timestamp'), error);
|
|
1388
1340
|
commands.cmdCurrentTimestamp(args[1] || 'full', raw);
|
|
1389
1341
|
}
|
|
1390
1342
|
|
|
1391
|
-
function routeRuntimeIdentity({ raw }) {
|
|
1343
|
+
function routeRuntimeIdentity({ args, raw }) {
|
|
1344
|
+
parseNamedArgsOrExit(args, specFor('runtime-identity'), error);
|
|
1392
1345
|
// #3146: report this runtime's package identity so a shipped workflow can
|
|
1393
1346
|
// tell whether it reached THIS package's gsd-tools or a colliding one.
|
|
1394
1347
|
// Kept on the CJS fast path for the same reason as current-timestamp — the
|
|
@@ -1398,6 +1351,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1398
1351
|
}
|
|
1399
1352
|
|
|
1400
1353
|
function routeSkillsRoot({ args, raw, error }) {
|
|
1354
|
+
parseNamedArgsOrExit(args, specFor('skills-root'), error);
|
|
1401
1355
|
// #3024: resolve the global skills base directory for a runtime.
|
|
1402
1356
|
// The sync-skills workflow previously shelled out to install.js --skills-root,
|
|
1403
1357
|
// but install.js is not shipped in installed trees. gsd-tools IS shipped, so
|
|
@@ -1435,17 +1389,14 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1435
1389
|
// source of truth (getProjectInstructionFile in runtime-name-policy.cjs).
|
|
1436
1390
|
// No SDK bridge — pure local lookup, runs before .planning/ exists.
|
|
1437
1391
|
const { getProjectInstructionFile } = require('./lib/runtime-name-policy.cjs');
|
|
1438
|
-
//
|
|
1439
|
-
//
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
// First positional that isn't a flag also works (lenient); otherwise ignore unknown flags.
|
|
1447
|
-
if (!a.startsWith('-') && !pifRuntime) { pifRuntime = a; }
|
|
1448
|
-
}
|
|
1392
|
+
// --runtime <value> (space or = form); default to empty so the safe
|
|
1393
|
+
// AGENTS.md cross-agent default applies. The first bare word also works
|
|
1394
|
+
// (lenient), and the flag wins over it.
|
|
1395
|
+
const pifSpec = specFor('project-instruction-file');
|
|
1396
|
+
const pifParsed = parseNamedArgsOrExit(args, pifSpec, error);
|
|
1397
|
+
const pifRuntime = pifParsed.runtime
|
|
1398
|
+
|| positionalWords(args, pifSpec).find((w) => !w.startsWith('-'))
|
|
1399
|
+
|| '';
|
|
1449
1400
|
// A retired runtime id now THROWS rather than resolving (#4709 AC#1).
|
|
1450
1401
|
// Map it to the same clean single-line error routeSkillsRoot emits for
|
|
1451
1402
|
// an unknown runtime — a CLI must not answer a bad flag value with a
|
|
@@ -1462,18 +1413,22 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1462
1413
|
}
|
|
1463
1414
|
|
|
1464
1415
|
function routeListTodos({ args, cwd, raw, error }) {
|
|
1416
|
+
parseNamedArgsOrExit(args, specFor('list-todos'), error);
|
|
1465
1417
|
commands.cmdListTodos(cwd, args[1], raw);
|
|
1466
1418
|
}
|
|
1467
1419
|
|
|
1468
1420
|
function routeListSeeds({ args, cwd, raw, error }) {
|
|
1421
|
+
parseNamedArgsOrExit(args, specFor('list-seeds'), error);
|
|
1469
1422
|
commands.cmdListSeeds(cwd, args[1], raw);
|
|
1470
1423
|
}
|
|
1471
1424
|
|
|
1472
1425
|
function routeVerifyPathExists({ args, cwd, raw, error }) {
|
|
1426
|
+
parseNamedArgsOrExit(args, specFor('verify-path-exists'), error);
|
|
1473
1427
|
commands.cmdVerifyPathExists(cwd, args[1], raw);
|
|
1474
1428
|
}
|
|
1475
1429
|
|
|
1476
|
-
function routeQuickTasksMigrate({ cwd, raw }) {
|
|
1430
|
+
function routeQuickTasksMigrate({ args, cwd, raw }) {
|
|
1431
|
+
parseNamedArgsOrExit(args, specFor('quick-tasks-migrate'), error);
|
|
1477
1432
|
// #3730 (option b, maintainer decision 2026-09-02): the supported repair
|
|
1478
1433
|
// path for a legacy Quick Tasks table GSD's own pre-registry prose created.
|
|
1479
1434
|
// Silent no-op when the section is absent or the table is already canonical
|
|
@@ -1510,15 +1465,14 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1510
1465
|
// `awk NF-2` Quick Tasks column arithmetic. Row construction is delegated
|
|
1511
1466
|
// to the pure appendQuickTaskRow (markdown-table.cjs); this case only
|
|
1512
1467
|
// handles the I/O (read STATE.md, resolve date/commit, write STATE.md).
|
|
1513
|
-
const qtaArgs = args.slice(1);
|
|
1514
1468
|
// Ambiguous boundary (ADR-3473 §8.4 Item 2 note): this command accepts
|
|
1515
|
-
// EITHER a positional free-text description (
|
|
1469
|
+
// EITHER a positional free-text description (args[1]) OR --task
|
|
1516
1470
|
// <value> — the same token index is a caller-owned positional in one
|
|
1517
1471
|
// input shape and a flag in the other, which a single fixed
|
|
1518
|
-
// `positionals` cursor cannot represent.
|
|
1519
|
-
//
|
|
1520
|
-
// (
|
|
1521
|
-
//
|
|
1472
|
+
// `positionals` cursor cannot represent. The spec declares the
|
|
1473
|
+
// description a free word (`freePositionals`), so the `|| args[1]`
|
|
1474
|
+
// fallback (positional form) and the --task form both keep working,
|
|
1475
|
+
// while an undeclared flag is refused.
|
|
1522
1476
|
// #3356 defect 1: `--quick-id` / `--slug` / `--directory` are
|
|
1523
1477
|
// OPTIONAL widenings. A caller with no quick id or task directory
|
|
1524
1478
|
// (fast.md, the original #2133 caller) omits them and keeps the
|
|
@@ -1542,12 +1496,10 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1542
1496
|
// Omitted, `status` stays undefined and `appendQuickTaskRow` falls
|
|
1543
1497
|
// back to its own `'—'` default — unchanged for every existing
|
|
1544
1498
|
// caller (fast.md, quick.md's non-VALIDATE_MODE branch).
|
|
1545
|
-
const
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
);
|
|
1550
|
-
const qtaTask = qtaParsed.task || args[1];
|
|
1499
|
+
const qtaSpec = specFor('quick-tasks-append');
|
|
1500
|
+
const qtaParsed = parseNamedArgsOrExit(args, qtaSpec, error);
|
|
1501
|
+
// The positional description is the first free word, never a flag (`--status ok desc` reads `desc`).
|
|
1502
|
+
const qtaTask = qtaParsed.task || positionalWords(args, qtaSpec)[0];
|
|
1551
1503
|
if (!qtaTask) {
|
|
1552
1504
|
error('quick-tasks-append requires --task <description> (or a positional description)', ERROR_REASON.USAGE);
|
|
1553
1505
|
}
|
|
@@ -1647,10 +1599,6 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1647
1599
|
const cfgLoader = require('./lib/config-loader.cjs');
|
|
1648
1600
|
const capabilityLoader = require('./lib/capability-loader.cjs');
|
|
1649
1601
|
|
|
1650
|
-
const flag = (name) => {
|
|
1651
|
-
const i = args.indexOf(name);
|
|
1652
|
-
return i !== -1 && args[i + 1] && !String(args[i + 1]).startsWith('--') ? args[i + 1] : null;
|
|
1653
|
-
};
|
|
1654
1602
|
const sub = args[1];
|
|
1655
1603
|
// Fail fast on an unrecognized subcommand. Without this check, `sub` fell through
|
|
1656
1604
|
// to the `sub !== 'invoke'` usage-error branch far below (after loading the
|
|
@@ -1671,8 +1619,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1671
1619
|
error(REVIEW_LANE_USAGE);
|
|
1672
1620
|
return;
|
|
1673
1621
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1622
|
+
// Each sub declares its own flags (`--explicit` is a value under dispatch-step and a boolean
|
|
1623
|
+
// under invoke), and the parse comes before any config, registry or lane work.
|
|
1624
|
+
const parsedFlags = parseNamedArgsOrExit(args, specFor(`review-lane.${sub}`), error);
|
|
1625
|
+
const runDir = parsedFlags['run-dir'] || '.';
|
|
1626
|
+
const repoRoot = parsedFlags['repo-root'] || cwd;
|
|
1676
1627
|
|
|
1677
1628
|
// Resolved config, read ONCE. Reading in-process (rather than shelling out to config-get per
|
|
1678
1629
|
// key, as the legs did) is what removes the stringly `"null"` sentinel the bash had to test for.
|
|
@@ -1796,7 +1747,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1796
1747
|
warn: (m) => process.stderr.write(`${m}\n`),
|
|
1797
1748
|
});
|
|
1798
1749
|
|
|
1799
|
-
const selected = (
|
|
1750
|
+
const selected = (parsedFlags.selected || '')
|
|
1800
1751
|
.split(',').map((s) => s.trim()).filter(Boolean);
|
|
1801
1752
|
// #5266: `<slug>:available|missing` per openai-http lane. Host + default come from the lane
|
|
1802
1753
|
// descriptor (resolveLanePlan), probing is runner.probeLane — review.md keeps no host table.
|
|
@@ -1848,8 +1799,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1848
1799
|
// flag-parsing surface — do not replicate inline bash parsing" for exactly this reason).
|
|
1849
1800
|
// Everything after `--` is a candidate flag; matched lane slugs print sorted and comma-joined.
|
|
1850
1801
|
if (sub === 'explicit-from-argv') {
|
|
1851
|
-
const
|
|
1852
|
-
const candidateArgs = new Set(sepIdx === -1 ? [] : args.slice(sepIdx + 1));
|
|
1802
|
+
const candidateArgs = new Set(parsedFlags['--']);
|
|
1853
1803
|
const slugs = [];
|
|
1854
1804
|
for (const lane of mergedLanes) {
|
|
1855
1805
|
const flags = Array.isArray(lane.flags) ? lane.flags : [];
|
|
@@ -1938,17 +1888,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1938
1888
|
// requested slugs, not the whole roster.
|
|
1939
1889
|
if (sub === 'dispatch-step') {
|
|
1940
1890
|
const { dispatchReviewerLanes } = require('./lib/reviewer-step-dispatch.cjs');
|
|
1941
|
-
const explicitFlags = (
|
|
1942
|
-
const depth =
|
|
1943
|
-
const baseSha =
|
|
1891
|
+
const explicitFlags = (parsedFlags.explicit || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
1892
|
+
const depth = parsedFlags.depth || '';
|
|
1893
|
+
const baseSha = parsedFlags['base-sha'] || '';
|
|
1944
1894
|
// #4209: this command IS the reusable capability/step-dispatch trait check — see
|
|
1945
1895
|
// gsd-core/references/loop-hook-dispatch.md for what supportsReviewerLanes means and why
|
|
1946
1896
|
// this is the one place it's resolved. Calls the SAME resolver `loop render-hooks` uses,
|
|
1947
1897
|
// `resolveActiveHooksForPoint`, directly in-process — no subprocess, no JSON re-parse, and
|
|
1948
1898
|
// no exposure to `io.cjs`'s `@file:` overflow protocol (which only applies to the
|
|
1949
1899
|
// rendered-string envelope this path never touches).
|
|
1950
|
-
const capId =
|
|
1951
|
-
const point =
|
|
1900
|
+
const capId = parsedFlags['cap-id'] || '';
|
|
1901
|
+
const point = parsedFlags.point || '';
|
|
1952
1902
|
let trait = false;
|
|
1953
1903
|
if (capId && point) {
|
|
1954
1904
|
try {
|
|
@@ -2085,8 +2035,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2085
2035
|
return;
|
|
2086
2036
|
}
|
|
2087
2037
|
|
|
2088
|
-
const slug =
|
|
2089
|
-
if (!slug) { error('review-lane invoke requires --slug'); return; }
|
|
2038
|
+
const slug = parsedFlags.slug;
|
|
2090
2039
|
const entry = plans.find((p) => p.slug === slug);
|
|
2091
2040
|
if (!entry || !entry.ok) {
|
|
2092
2041
|
output({ slug, ok: false, reason: entry ? entry.reason : 'malformed_lane', detail: entry ? entry.detail : 'unknown lane' }, raw);
|
|
@@ -2102,9 +2051,9 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2102
2051
|
// lane's invocation. Everything the lane declares — probe, channels, timeout, empty-output
|
|
2103
2052
|
// policy, handler — then applies to the instance unchanged, which is the whole reason to route
|
|
2104
2053
|
// them here: a cross-cutting fix reaches instances for free.
|
|
2105
|
-
const asIdentity =
|
|
2106
|
-
const instanceModel =
|
|
2107
|
-
const instanceAgent =
|
|
2054
|
+
const asIdentity = parsedFlags.as || null;
|
|
2055
|
+
const instanceModel = parsedFlags.model || null;
|
|
2056
|
+
const instanceAgent = parsedFlags.agent || null;
|
|
2108
2057
|
|
|
2109
2058
|
if (asIdentity) {
|
|
2110
2059
|
// Write under the INSTANCE name so two instances of one adapter never overwrite each other.
|
|
@@ -2185,7 +2134,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2185
2134
|
// to whichever channel this lane actually reads from (stdin, argv, an argv-embedded file
|
|
2186
2135
|
// reference, or the http promptPath), so one flag serves every lane (#5049). It is no longer a
|
|
2187
2136
|
// trim hand-off: nothing in review.md trims a prompt to fit a lane since #4559.
|
|
2188
|
-
const promptOverride =
|
|
2137
|
+
const promptOverride = parsedFlags['prompt-file'] || null;
|
|
2189
2138
|
if (promptOverride) {
|
|
2190
2139
|
entry.plan = applyPromptOverride(entry.plan, promptOverride, repoRoot);
|
|
2191
2140
|
}
|
|
@@ -2253,7 +2202,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2253
2202
|
|
|
2254
2203
|
const result = await runner.runLane(entry.plan, deps, {
|
|
2255
2204
|
consentedHost,
|
|
2256
|
-
explicitlyRequested:
|
|
2205
|
+
explicitlyRequested: parsedFlags.explicit,
|
|
2257
2206
|
repoRoot,
|
|
2258
2207
|
});
|
|
2259
2208
|
output(result, raw);
|
|
@@ -2264,11 +2213,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2264
2213
|
// watch-mode runner (vitest/jest) cannot hang a verification gate. Shared
|
|
2265
2214
|
// by the regression gate and the post-merge gate. args[1] is the raw
|
|
2266
2215
|
// resolved command; --cwd (already parsed into `cwd`) locates package.json.
|
|
2216
|
+
parseNamedArgsOrExit(args, specFor('normalize-test-command'), error);
|
|
2267
2217
|
const testCommandNormalizer = require('./lib/normalize-test-command.cjs');
|
|
2268
2218
|
testCommandNormalizer.cmdNormalizeTestCommand(cwd, args[1]);
|
|
2269
2219
|
}
|
|
2270
2220
|
|
|
2271
2221
|
function routeResolvePackageManager({ args, cwd }) {
|
|
2222
|
+
parseNamedArgsOrExit(args, specFor('resolve-package-manager'), error);
|
|
2272
2223
|
// #5346: the gates' no-configured-command fallback asks here for the
|
|
2273
2224
|
// project's package manager (packageManager field, then lockfile, else npm).
|
|
2274
2225
|
// args[1] is the script name; --cwd (already parsed into `cwd`) locates package.json.
|
|
@@ -2276,6 +2227,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2276
2227
|
}
|
|
2277
2228
|
|
|
2278
2229
|
function routeDispatchShouldFlatten({ args, cwd, raw, error }) {
|
|
2230
|
+
const parsed = parseNamedArgsOrExit(args, specFor('dispatch-should-flatten'), error);
|
|
2279
2231
|
// #1708 / #853: typed query replacing the `RUNTIME === 'codex'` prose rule.
|
|
2280
2232
|
//
|
|
2281
2233
|
// Resolves the current runtime (GSD_RUNTIME > config.runtime > per-install
|
|
@@ -2308,8 +2260,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2308
2260
|
? hostIntegration.shouldFlattenDispatch(dispatch)
|
|
2309
2261
|
: true; // fail-closed: unknown runtime → inline
|
|
2310
2262
|
|
|
2311
|
-
|
|
2312
|
-
if (jsonIdx !== -1) {
|
|
2263
|
+
if (parsed.json) {
|
|
2313
2264
|
output({
|
|
2314
2265
|
runtime: runtimeId,
|
|
2315
2266
|
shouldFlatten: shouldFlat,
|
|
@@ -2351,6 +2302,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2351
2302
|
}
|
|
2352
2303
|
|
|
2353
2304
|
function routeDispatchIsolation({ args, cwd, raw, error }) {
|
|
2305
|
+
const parsed = parseNamedArgsOrExit(args, specFor('dispatch-isolation'), error);
|
|
2354
2306
|
// #2584 Phase 3 (#2627): typed query exposing the negotiated
|
|
2355
2307
|
// `dispatch.isolation` to the execute-phase wave scheduler, so the
|
|
2356
2308
|
// scheduler branches on a declared capability instead of on a runtime id.
|
|
@@ -2394,7 +2346,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2394
2346
|
// `per-plan-worktree-gate.md`) override the naturally-resolved mode
|
|
2395
2347
|
// while still going through this single write path. Best-effort: a
|
|
2396
2348
|
// sentinel write failure here must never fail the wave.
|
|
2397
|
-
const decision = resolveDispatchIsolationDecision({
|
|
2349
|
+
const decision = resolveDispatchIsolationDecision({ parsed, cwd });
|
|
2398
2350
|
const runtimeId = decision.runtimeId;
|
|
2399
2351
|
let { isolation, exec, harnessFlag } = decision;
|
|
2400
2352
|
|
|
@@ -2404,8 +2356,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2404
2356
|
// ignored rather than erroring — this is a best-effort recording call,
|
|
2405
2357
|
// not a hard usage gate. Forcing to 'none' clears harnessFlag/exec since
|
|
2406
2358
|
// neither applies to sequential dispatch.
|
|
2407
|
-
const
|
|
2408
|
-
const forcedIsolation = forceIdx !== -1 ? args[forceIdx + 1] : undefined;
|
|
2359
|
+
const forcedIsolation = parsed['force-isolation'];
|
|
2409
2360
|
if (forcedIsolation && DISPATCH_ISOLATION_VOCABULARY.has(forcedIsolation)) {
|
|
2410
2361
|
isolation = forcedIsolation;
|
|
2411
2362
|
if (isolation === 'none') {
|
|
@@ -2414,14 +2365,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2414
2365
|
}
|
|
2415
2366
|
}
|
|
2416
2367
|
|
|
2417
|
-
const
|
|
2418
|
-
const
|
|
2419
|
-
? args[phaseIdx + 1]
|
|
2420
|
-
: null;
|
|
2421
|
-
const planIdx = args.indexOf('--plan');
|
|
2422
|
-
const planArg = planIdx !== -1 && args[planIdx + 1] && !args[planIdx + 1].startsWith('--')
|
|
2423
|
-
? args[planIdx + 1]
|
|
2424
|
-
: null;
|
|
2368
|
+
const phaseArg = parsed.phase || null;
|
|
2369
|
+
const planArg = parsed.plan || null;
|
|
2425
2370
|
|
|
2426
2371
|
// #3737: the project-level opt-out (workflow.use_worktrees === false) is
|
|
2427
2372
|
// decided HERE, before the sentinel write — not only in the workflow
|
|
@@ -2451,7 +2396,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2451
2396
|
// a { recorded: false } result; this catch is defense in depth only.
|
|
2452
2397
|
}
|
|
2453
2398
|
|
|
2454
|
-
if (
|
|
2399
|
+
if (parsed.json) {
|
|
2455
2400
|
output({ runtime: runtimeId, isolation, exec, harnessFlag }, raw);
|
|
2456
2401
|
} else {
|
|
2457
2402
|
process.stdout.write(isolation);
|
|
@@ -2497,6 +2442,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2497
2442
|
* default → same as --raw
|
|
2498
2443
|
*/
|
|
2499
2444
|
function routeDispatchCapacity({ args, cwd, raw }) {
|
|
2445
|
+
const capacityFlags = parseNamedArgsOrExit(args, specFor('dispatch-capacity'), error);
|
|
2500
2446
|
const { UNDOCUMENTED, isPositiveSafeInteger } = require('./lib/host-integration.cjs');
|
|
2501
2447
|
|
|
2502
2448
|
let runtimeId = null;
|
|
@@ -2557,7 +2503,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2557
2503
|
reason = 'non_positive';
|
|
2558
2504
|
}
|
|
2559
2505
|
|
|
2560
|
-
if (
|
|
2506
|
+
if (capacityFlags.json) {
|
|
2561
2507
|
output({ runtime: runtimeId, capacity, declared, source, reason }, raw);
|
|
2562
2508
|
} else {
|
|
2563
2509
|
process.stdout.write(String(capacity));
|
|
@@ -2726,7 +2672,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2726
2672
|
* drift apart. Resolution only — the caller decides whether the decision is
|
|
2727
2673
|
* recorded to the sentinel.
|
|
2728
2674
|
*/
|
|
2729
|
-
function resolveDispatchIsolationDecision({
|
|
2675
|
+
function resolveDispatchIsolationDecision({ parsed, cwd }) {
|
|
2730
2676
|
let isolation = 'none';
|
|
2731
2677
|
let runtimeId = null;
|
|
2732
2678
|
let exec = null;
|
|
@@ -2770,11 +2716,9 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2770
2716
|
}
|
|
2771
2717
|
|
|
2772
2718
|
if (isolation === 'orchestrator-worktree') {
|
|
2773
|
-
const
|
|
2774
|
-
const cwdTarget = cwdIdx !== -1 ? args[cwdIdx + 1] : '';
|
|
2719
|
+
const cwdTarget = parsed['cwd-target'];
|
|
2775
2720
|
if (cwdTarget) {
|
|
2776
|
-
const
|
|
2777
|
-
const promptArg = promptIdx !== -1 ? args[promptIdx + 1] : undefined;
|
|
2721
|
+
const promptArg = parsed.prompt ?? undefined;
|
|
2778
2722
|
const hostIntegration = require('./lib/host-integration.cjs');
|
|
2779
2723
|
// #3714: resolve an EXPLICIT, non-sentinel per-agent model pin for the
|
|
2780
2724
|
// spawned worktree executor. Passing `null` as the runtime resolver
|
|
@@ -2821,6 +2765,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2821
2765
|
model = resolveDispatchModelPin('gsd-executor', pinned);
|
|
2822
2766
|
}
|
|
2823
2767
|
let orchestratorExec = runtimeEntry?.runtime?.orchestratorExec;
|
|
2768
|
+
// eslint-disable-next-line local/named-args-spec-from-registry -- compares a runtime descriptor's cwd flag for the host process argv gsd-tools builds, not a gsd-tools CLI flag
|
|
2824
2769
|
if (orchestratorExec && orchestratorExec.cwdFlag === '--dir' && runtimeId === 'opencode') {
|
|
2825
2770
|
// OpenCode 2.x `run` has no --dir: bind the worktree through the process cwd (ADR-5331 D5).
|
|
2826
2771
|
const ocv = require('./lib/opencode-version.cjs');
|
|
@@ -2859,6 +2804,16 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2859
2804
|
}
|
|
2860
2805
|
|
|
2861
2806
|
function routeInspectDispatchIsolation({ args, cwd, raw }) {
|
|
2807
|
+
const parsed = parseNamedArgsOrExit(args, specFor('inspect-dispatch-isolation'), error);
|
|
2808
|
+
// The recording-only knobs (--force-isolation, --phase, --plan) are declared in the CLI surface
|
|
2809
|
+
// registry only so they reach this refusal, which says why they have no read-path meaning.
|
|
2810
|
+
const rejected = ['force-isolation', 'phase', 'plan'].filter((name) => parsed[name] !== null).map((name) => `--${name}`);
|
|
2811
|
+
if (rejected.length > 0) {
|
|
2812
|
+
error(
|
|
2813
|
+
`inspect-dispatch-isolation: ${rejected.join(', ')} ${rejected.length === 1 ? 'is a' : 'are'} recording-only argument${rejected.length === 1 ? '' : 's'} and cannot be used on the inspection verb — it resolves the runtime's DECLARED capability and records nothing. Use 'query dispatch-isolation' if you need the override applied and the decision recorded.`,
|
|
2814
|
+
ERROR_REASON.USAGE,
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2862
2817
|
// #2486: sentinel-free sibling of `dispatch-isolation` for INSPECTION
|
|
2863
2818
|
// surfaces — /gsd:health's W025 check and /gsd:settings' Worktrees
|
|
2864
2819
|
// branching. The dispatch verb above intentionally records its resolved
|
|
@@ -2892,16 +2847,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2892
2847
|
// caller receive a silently different answer, this verb REJECTS the
|
|
2893
2848
|
// recording-only knobs outright — they exist to be recorded, and a
|
|
2894
2849
|
// read has nothing to record.
|
|
2895
|
-
const
|
|
2896
|
-
|
|
2897
|
-
if (rejected.length > 0) {
|
|
2898
|
-
error(
|
|
2899
|
-
`inspect-dispatch-isolation: ${rejected.join(', ')} ${rejected.length === 1 ? 'is a' : 'are'} recording-only argument${rejected.length === 1 ? '' : 's'} and cannot be used on the inspection verb — it resolves the runtime's DECLARED capability and records nothing. Use 'query dispatch-isolation' if you need the override applied and the decision recorded.`,
|
|
2900
|
-
ERROR_REASON.USAGE,
|
|
2901
|
-
);
|
|
2902
|
-
}
|
|
2903
|
-
const { runtimeId, isolation, exec, harnessFlag } = resolveDispatchIsolationDecision({ args, cwd });
|
|
2904
|
-
if (args.indexOf('--json') !== -1) {
|
|
2850
|
+
const { runtimeId, isolation, exec, harnessFlag } = resolveDispatchIsolationDecision({ parsed, cwd });
|
|
2851
|
+
if (parsed.json) {
|
|
2905
2852
|
output({ runtime: runtimeId, isolation, exec, harnessFlag }, raw);
|
|
2906
2853
|
} else {
|
|
2907
2854
|
process.stdout.write(isolation);
|
|
@@ -3052,9 +2999,9 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3052
2999
|
//
|
|
3053
3000
|
// Output: { recorded: true|false, path, error? }
|
|
3054
3001
|
const VALID_ISOLATION = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
|
|
3055
|
-
const
|
|
3056
|
-
const isolation =
|
|
3057
|
-
if (!
|
|
3002
|
+
const parsed = parseNamedArgsOrExit(args, specFor('record-dispatch-isolation'), error);
|
|
3003
|
+
const isolation = parsed.isolation;
|
|
3004
|
+
if (!VALID_ISOLATION.has(isolation)) {
|
|
3058
3005
|
error(
|
|
3059
3006
|
'Usage: record-dispatch-isolation --isolation <harness-worktree|orchestrator-worktree|none> ' +
|
|
3060
3007
|
'[--harness-flag <flag>|--harness-flag=<flag>] [--phase <n>] [--plan <id>]',
|
|
@@ -3077,31 +3024,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3077
3024
|
// unambiguously and is never subject to that guard — any future runtime
|
|
3078
3025
|
// whose registered flag happens to be bare-CLI-shaped benefits the same
|
|
3079
3026
|
// way Cursor's does.
|
|
3080
|
-
|
|
3081
|
-
const
|
|
3082
|
-
|
|
3083
|
-
const value = flagEqArg.slice('--harness-flag='.length);
|
|
3084
|
-
harnessFlag = value.length > 0 ? value : null;
|
|
3085
|
-
} else {
|
|
3086
|
-
const flagIdx = args.indexOf('--harness-flag');
|
|
3087
|
-
harnessFlag = flagIdx !== -1 && args[flagIdx + 1] && !args[flagIdx + 1].startsWith('--')
|
|
3088
|
-
? args[flagIdx + 1]
|
|
3089
|
-
: null;
|
|
3090
|
-
}
|
|
3091
|
-
const phaseIdx = args.indexOf('--phase');
|
|
3092
|
-
const phase = phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--')
|
|
3093
|
-
? args[phaseIdx + 1]
|
|
3094
|
-
: null;
|
|
3095
|
-
const planIdx = args.indexOf('--plan');
|
|
3096
|
-
const plan = planIdx !== -1 && args[planIdx + 1] && !args[planIdx + 1].startsWith('--')
|
|
3097
|
-
? args[planIdx + 1]
|
|
3098
|
-
: null;
|
|
3027
|
+
const harnessFlag = parsed['harness-flag'] || null;
|
|
3028
|
+
const phase = parsed.phase || null;
|
|
3029
|
+
const plan = parsed.plan || null;
|
|
3099
3030
|
|
|
3100
3031
|
const result = writeDispatchIsolationSentinel(cwd, { isolation, harnessFlag, phase, plan });
|
|
3101
3032
|
output(result, raw);
|
|
3102
3033
|
}
|
|
3103
3034
|
|
|
3104
3035
|
function routeResolveDispatchType({ args, cwd, raw, error }) {
|
|
3036
|
+
const parsed = parseNamedArgsOrExit(args, specFor('resolve-dispatch-type'), error);
|
|
3037
|
+
const requested = parsed.requested ?? '';
|
|
3105
3038
|
// #2508 Phase 4 Option A: resolve a requested GSD subagent name to the
|
|
3106
3039
|
// type an Agent() call should use on the current runtime. On
|
|
3107
3040
|
// named-dispatch runtimes (Claude, OpenCode, …) the name is returned
|
|
@@ -3113,8 +3046,6 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3113
3046
|
// --raw (default) → prints the resolved type (e.g. "gsd-planner" or "plan")
|
|
3114
3047
|
// --json → prints { runtime, requested, resolved, dispatch }
|
|
3115
3048
|
try {
|
|
3116
|
-
const requestedIdx = args.indexOf('--requested');
|
|
3117
|
-
const requested = requestedIdx !== -1 ? args[requestedIdx + 1] : '';
|
|
3118
3049
|
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
3119
3050
|
const runtimeId = resolveRuntime(cwd);
|
|
3120
3051
|
const registry = require('./lib/capability-registry.cjs');
|
|
@@ -3124,8 +3055,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3124
3055
|
const dispatch = runtimeEntry?.runtime?.hostIntegration?.dispatch ?? null;
|
|
3125
3056
|
const hostIntegration = require('./lib/host-integration.cjs');
|
|
3126
3057
|
const resolved = hostIntegration.resolveDispatchType(requested, dispatch);
|
|
3127
|
-
|
|
3128
|
-
if (jsonIdx !== -1) {
|
|
3058
|
+
if (parsed.json) {
|
|
3129
3059
|
output({ runtime: runtimeId, requested, resolved, dispatch }, raw);
|
|
3130
3060
|
} else {
|
|
3131
3061
|
process.stdout.write(String(resolved));
|
|
@@ -3134,13 +3064,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3134
3064
|
// Fail-closed: on any error, echo the requested name unchanged
|
|
3135
3065
|
// (named-dispatch is the GSD default; degrading to it preserves
|
|
3136
3066
|
// behavior for every runtime already in the field).
|
|
3137
|
-
const requestedIdx = args.indexOf('--requested');
|
|
3138
|
-
const requested = requestedIdx !== -1 ? args[requestedIdx + 1] : '';
|
|
3139
3067
|
process.stdout.write(String(requested));
|
|
3140
3068
|
}
|
|
3141
3069
|
}
|
|
3142
3070
|
|
|
3143
3071
|
function routeResolveAgent({ args, cwd, raw, error }) {
|
|
3072
|
+
const parsed = parseNamedArgsOrExit(args, specFor('resolve-agent'), error);
|
|
3144
3073
|
// #1689: resolve a per-plan agent_hint specialist name to the subagent_type
|
|
3145
3074
|
// an Agent() call should use. Returns the name unchanged when a
|
|
3146
3075
|
// matching agent file exists in the active runtime's agent dir(s);
|
|
@@ -3153,8 +3082,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3153
3082
|
// --json -> prints { runtime, requested, resolved, fallback }
|
|
3154
3083
|
const FALLBACK = 'gsd-executor';
|
|
3155
3084
|
try {
|
|
3156
|
-
const
|
|
3157
|
-
const requested = nameIdx !== -1 ? args[nameIdx + 1] : '';
|
|
3085
|
+
const requested = parsed.name ?? '';
|
|
3158
3086
|
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
3159
3087
|
const runtimeId = resolveRuntime(cwd);
|
|
3160
3088
|
const { resolveAgentHint } = require('./lib/agent-install-check.cjs');
|
|
@@ -3171,8 +3099,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3171
3099
|
// the named agent did not resolve, or resolution errored). Requesting
|
|
3172
3100
|
// gsd-executor explicitly and resolving to it is NOT a fallback.
|
|
3173
3101
|
const fellBack = !resolvedOk;
|
|
3174
|
-
|
|
3175
|
-
if (jsonIdx !== -1) {
|
|
3102
|
+
if (parsed.json) {
|
|
3176
3103
|
output({ runtime: runtimeId, requested: requested || null, resolved, fallback: fellBack }, raw);
|
|
3177
3104
|
} else {
|
|
3178
3105
|
process.stdout.write(String(resolved));
|
|
@@ -3187,15 +3114,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3187
3114
|
function routeAgentSkills({ args, cwd, raw, error }) {
|
|
3188
3115
|
// --json emits typed IR { agent_type, block, skills_count } for test assertions
|
|
3189
3116
|
// (#455). Default (no flag) outputs raw XML so workflow shell expansions work.
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
const agentSkillsOnly = skillsOnlyIdx !== -1;
|
|
3197
|
-
if (agentSkillsOnly) args.splice(skillsOnlyIdx, 1);
|
|
3198
|
-
init.cmdAgentSkills(cwd, args[1], raw, agentSkillsJsonMode, agentSkillsOnly);
|
|
3117
|
+
// #5122: `--skills-only` is the agent's own self-load, asking for configured
|
|
3118
|
+
// skills only, never the persona fallback served to orchestrator-side injection.
|
|
3119
|
+
// The agent type is the first free word, whichever side of the flags it sits on.
|
|
3120
|
+
const agentSkillsSpec = specFor('agent-skills');
|
|
3121
|
+
const agentSkillsFlags = parseNamedArgsOrExit(args, agentSkillsSpec, error);
|
|
3122
|
+
init.cmdAgentSkills(cwd, positionalWords(args, agentSkillsSpec)[0], raw, agentSkillsFlags.json, agentSkillsFlags['skills-only']);
|
|
3199
3123
|
}
|
|
3200
3124
|
|
|
3201
3125
|
function routeSkillManifest({ args, cwd, raw, error }) {
|
|
@@ -3203,6 +3127,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3203
3127
|
}
|
|
3204
3128
|
|
|
3205
3129
|
function routeHistoryDigest({ args, cwd, raw, error }) {
|
|
3130
|
+
parseNamedArgsOrExit(args, specFor('history-digest'), error);
|
|
3206
3131
|
commands.cmdHistoryDigest(cwd, raw);
|
|
3207
3132
|
}
|
|
3208
3133
|
|
|
@@ -3223,6 +3148,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3223
3148
|
// deterministic detectAssumptionDelta, emitting the typed IR as JSON.
|
|
3224
3149
|
const sub = args[1];
|
|
3225
3150
|
if (sub === 'scan') {
|
|
3151
|
+
const parsed = parseNamedArgsOrExit(args, specFor('assumption-delta.scan'), error);
|
|
3226
3152
|
const phaseNum = args[2];
|
|
3227
3153
|
// Reject missing or flag-shaped phase values (QA matrix: values that
|
|
3228
3154
|
// look like flags). `scan --json` must not treat "--json" as a phase.
|
|
@@ -3236,8 +3162,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3236
3162
|
// Terms are normalized (deduped, alphanumeric-only, capped) by
|
|
3237
3163
|
// detectAssumptionDelta's resolveTerms.
|
|
3238
3164
|
let termsOverride;
|
|
3239
|
-
const
|
|
3240
|
-
const termsVal = termsIdx !== -1 ? args[termsIdx + 1] : undefined;
|
|
3165
|
+
const termsVal = parsed.terms;
|
|
3241
3166
|
if (typeof termsVal === 'string' && !termsVal.startsWith('-')) {
|
|
3242
3167
|
const list = termsVal
|
|
3243
3168
|
.split(',')
|
|
@@ -3293,31 +3218,28 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3293
3218
|
function routeMilestone({ args, cwd, raw, error }) {
|
|
3294
3219
|
const subcommand = args[1];
|
|
3295
3220
|
if (subcommand === 'complete') {
|
|
3296
|
-
const
|
|
3221
|
+
const parsed = parseNamedArgsOrExit(args, specFor('milestone.complete'), error);
|
|
3222
|
+
const milestoneName = parsed.name;
|
|
3297
3223
|
// #1871: archive phase dirs by default on milestone complete so the next
|
|
3298
3224
|
// new-milestone never inherits un-archived dirs. --no-archive-phases opts out.
|
|
3299
|
-
const archivePhases = !
|
|
3300
|
-
const force =
|
|
3225
|
+
const archivePhases = !parsed['no-archive-phases'];
|
|
3226
|
+
const force = parsed.force;
|
|
3301
3227
|
// #2118: --dry-run prints a preview plan without mutating.
|
|
3302
|
-
const dryRun =
|
|
3228
|
+
const dryRun = parsed['dry-run'];
|
|
3303
3229
|
// #2142: quick-task archival is opt-in (default OFF) — unlike
|
|
3304
3230
|
// --no-archive-phases' inverted shape, absence of this flag means
|
|
3305
3231
|
// "do nothing" rather than "skip a default-on behavior".
|
|
3306
|
-
const archiveQuick =
|
|
3232
|
+
const archiveQuick = parsed['archive-quick'];
|
|
3307
3233
|
// #3726: explicit mutation opt-in — without --confirm (and without
|
|
3308
3234
|
// --dry-run) the command refuses before touching anything. Distinct
|
|
3309
3235
|
// from --force, which bypasses the narrow scope guards only.
|
|
3310
|
-
const confirm =
|
|
3236
|
+
const confirm = parsed.confirm;
|
|
3311
3237
|
// #5706 (epic #3628): optional caller-supplied release map file.
|
|
3312
|
-
// A flag with no usable value fails loud rather than being dropped
|
|
3313
|
-
|
|
3314
|
-
const
|
|
3315
|
-
if (
|
|
3316
|
-
|
|
3317
|
-
if (typeof releaseMapValue !== 'string' || releaseMapValue.startsWith('--') || releaseMapValue.trim() === '') {
|
|
3318
|
-
error('--release-map requires a file path (a JSON array of release-map entries)');
|
|
3319
|
-
}
|
|
3320
|
-
releaseMap = releaseMapValue;
|
|
3238
|
+
// A flag with no usable value fails loud rather than being dropped: the spec reads a
|
|
3239
|
+
// bare --release-map (no value, or a flag-shaped one) as '' and an absent one as null.
|
|
3240
|
+
const releaseMap = parsed['release-map'];
|
|
3241
|
+
if (releaseMap !== null && releaseMap.trim() === '') {
|
|
3242
|
+
error('--release-map requires a file path (a JSON array of release-map entries)');
|
|
3321
3243
|
}
|
|
3322
3244
|
milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun, archiveQuick, confirm, releaseMap }, raw);
|
|
3323
3245
|
} else if (subcommand === 'archive-quick') {
|
|
@@ -3325,8 +3247,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3325
3247
|
// touch ROADMAP/REQUIREMENTS/MILESTONES.md, runs no completion
|
|
3326
3248
|
// guards) — safe to call against an already-completed milestone,
|
|
3327
3249
|
// unlike `milestone complete --archive-quick`.
|
|
3328
|
-
const
|
|
3329
|
-
milestone.cmdQuickArchive(cwd, args[2], { dryRun }, raw);
|
|
3250
|
+
const parsed = parseNamedArgsOrExit(args, specFor('milestone.archive-quick'), error);
|
|
3251
|
+
milestone.cmdQuickArchive(cwd, args[2], { dryRun: parsed['dry-run'] }, raw);
|
|
3330
3252
|
} else if (subcommand === 'release-tags') {
|
|
3331
3253
|
// #5705 (epic #3628 Phase 1): repository-scoped annotated release tags.
|
|
3332
3254
|
milestoneReleaseTags.cmdMilestoneReleaseTags(cwd, args, raw);
|
|
@@ -3336,6 +3258,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3336
3258
|
}
|
|
3337
3259
|
|
|
3338
3260
|
function routeProgress({ args, cwd, raw, error }) {
|
|
3261
|
+
parseNamedArgsOrExit(args, specFor('progress.bar'), error);
|
|
3339
3262
|
const subcommand = args[1] || 'json';
|
|
3340
3263
|
commands.cmdProgressRender(cwd, subcommand, raw);
|
|
3341
3264
|
}
|
|
@@ -3344,16 +3267,16 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3344
3267
|
const subcommand = args[1];
|
|
3345
3268
|
if (subcommand === 'render-checkpoint') {
|
|
3346
3269
|
const uat = require('./lib/uat.cjs');
|
|
3347
|
-
const options = parseNamedArgsOrExit(args,
|
|
3270
|
+
const options = parseNamedArgsOrExit(args, specFor('uat.render-checkpoint'), error);
|
|
3348
3271
|
uat.cmdRenderCheckpoint(cwd, options, raw);
|
|
3349
3272
|
} else if (subcommand === 'classify-coverage') {
|
|
3350
3273
|
const coverage = require('./lib/coverage.cjs');
|
|
3351
|
-
const options = parseNamedArgsOrExit(args,
|
|
3274
|
+
const options = parseNamedArgsOrExit(args, specFor('uat.classify-coverage'), error);
|
|
3352
3275
|
coverage.cmdClassify(cwd, options, raw);
|
|
3353
3276
|
} else if (subcommand === 'complete-session') {
|
|
3354
3277
|
const uat = require('./lib/uat.cjs');
|
|
3355
3278
|
const uatPath = args[2];
|
|
3356
|
-
const options = parseNamedArgsOrExit(args,
|
|
3279
|
+
const options = parseNamedArgsOrExit(args, specFor('uat.complete-session'), error);
|
|
3357
3280
|
return uat.cmdUatCompleteSession(cwd, uatPath, { message: options.message }, raw);
|
|
3358
3281
|
} else {
|
|
3359
3282
|
error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage, complete-session', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
@@ -3361,6 +3284,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3361
3284
|
}
|
|
3362
3285
|
|
|
3363
3286
|
function routeStats({ args, cwd, raw, error }) {
|
|
3287
|
+
parseNamedArgsOrExit(args, specFor('stats.json'), error);
|
|
3364
3288
|
const subcommand = args[1] || 'json';
|
|
3365
3289
|
commands.cmdStats(cwd, subcommand, raw);
|
|
3366
3290
|
}
|
|
@@ -3372,17 +3296,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3372
3296
|
// and plumbed (mirroring routeMilestone / #2118), and any OTHER
|
|
3373
3297
|
// flag fails loudly instead of being silently ignored — an
|
|
3374
3298
|
// accepted-but-ignored safety flag converts a deliberate preview
|
|
3375
|
-
// into the mutation it was meant to avoid.
|
|
3376
|
-
//
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
for (const a of args.slice(3)) {
|
|
3380
|
-
if (typeof a === 'string' && a.startsWith('-') && !TODO_COMPLETE_KNOWN_FLAGS.has(a)) {
|
|
3381
|
-
error(`Unknown flag for todo complete: ${a}`, ERROR_REASON.USAGE);
|
|
3382
|
-
}
|
|
3383
|
-
}
|
|
3384
|
-
commands.cmdTodoComplete(cwd, args[2], { dryRun: args.includes('--dry-run') }, raw);
|
|
3299
|
+
// into the mutation it was meant to avoid. The spec's rejectShortFlags
|
|
3300
|
+
// and unknownFlagMessage refuse long and short flags with that wording.
|
|
3301
|
+
const parsed = parseNamedArgsOrExit(args, specFor('todo.complete'), error);
|
|
3302
|
+
commands.cmdTodoComplete(cwd, args[2], { dryRun: parsed['dry-run'] }, raw);
|
|
3385
3303
|
} else if (subcommand === 'match-phase') {
|
|
3304
|
+
parseNamedArgsOrExit(args, specFor('todo.match-phase'), error);
|
|
3386
3305
|
commands.cmdTodoMatchPhase(cwd, args[2], raw);
|
|
3387
3306
|
} else {
|
|
3388
3307
|
error('Unknown todo subcommand. Available: complete, match-phase', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
@@ -3391,14 +3310,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3391
3310
|
|
|
3392
3311
|
function routeScaffold({ args, cwd, raw, error }) {
|
|
3393
3312
|
const scaffoldType = args[1];
|
|
3394
|
-
// `--name` is multi-word
|
|
3395
|
-
//
|
|
3396
|
-
|
|
3397
|
-
// this call, matching the existing (unchanged) permissive behavior
|
|
3398
|
-
// for --name; --phase extraction is unaffected either way.
|
|
3313
|
+
// `--name` is multi-word: the spec's `multiwordFlags` collects the words up to the
|
|
3314
|
+
// next flag, a token count the single-token-per-flag boundary walk cannot represent.
|
|
3315
|
+
const scaffoldParsed = parseNamedArgsOrExit(args, specFor('scaffold'), error);
|
|
3399
3316
|
const scaffoldOptions = {
|
|
3400
|
-
phase:
|
|
3401
|
-
name:
|
|
3317
|
+
phase: scaffoldParsed.phase,
|
|
3318
|
+
name: scaffoldParsed.name,
|
|
3402
3319
|
};
|
|
3403
3320
|
commands.cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
|
|
3404
3321
|
}
|
|
@@ -3407,74 +3324,20 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3407
3324
|
// loop render-hooks <point>
|
|
3408
3325
|
const loopSubcommand = args[1];
|
|
3409
3326
|
if (loopSubcommand === 'render-hooks') {
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
if (!value || value.startsWith('--')) {
|
|
3420
|
-
error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3421
|
-
}
|
|
3422
|
-
loopConfigDir = value;
|
|
3423
|
-
}
|
|
3424
|
-
// --active-cap <capId>: parse and validate before delegating
|
|
3425
|
-
let loopActiveCap = undefined;
|
|
3426
|
-
const activeCapEqArg = args.find(arg => arg.startsWith('--active-cap='));
|
|
3427
|
-
const activeCapIdx = args.indexOf('--active-cap');
|
|
3428
|
-
if (activeCapEqArg) {
|
|
3429
|
-
const value = activeCapEqArg.slice('--active-cap='.length).trim();
|
|
3430
|
-
if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3431
|
-
loopActiveCap = value;
|
|
3432
|
-
} else if (activeCapIdx !== -1) {
|
|
3433
|
-
const value = args[activeCapIdx + 1];
|
|
3434
|
-
if (!value || value.startsWith('--')) {
|
|
3435
|
-
error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3436
|
-
}
|
|
3437
|
-
loopActiveCap = value;
|
|
3438
|
-
}
|
|
3439
|
-
// --runtime <r> (#2003): explicit runtime override so the config-dir
|
|
3440
|
-
// resolution bypasses the persisted-runtime fallback (GSD_RUNTIME →
|
|
3441
|
-
// config.runtime). Mirrors the --config-dir dual-form (--runtime X /
|
|
3442
|
-
// --runtime=X) and the capability-set --runtime precedent.
|
|
3443
|
-
let loopRuntime = undefined;
|
|
3444
|
-
const runtimeEqArg = args.find(arg => arg.startsWith('--runtime='));
|
|
3445
|
-
const runtimeIdx = args.indexOf('--runtime');
|
|
3446
|
-
if (runtimeEqArg) {
|
|
3447
|
-
const value = runtimeEqArg.slice('--runtime='.length).trim();
|
|
3448
|
-
if (!value) error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3449
|
-
loopRuntime = value;
|
|
3450
|
-
} else if (runtimeIdx !== -1) {
|
|
3451
|
-
const value = args[runtimeIdx + 1];
|
|
3452
|
-
if (!value || value.startsWith('--')) {
|
|
3453
|
-
error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3454
|
-
}
|
|
3455
|
-
loopRuntime = value;
|
|
3456
|
-
}
|
|
3457
|
-
// --after-fingerprint <phaseDir> (#5105 R2): gate out verify:post
|
|
3458
|
-
// steps whose declared artifact already exists in phaseDir.
|
|
3459
|
-
let loopAfterFingerprint = undefined;
|
|
3460
|
-
const afterFpEqArg = args.find(arg => arg.startsWith('--after-fingerprint='));
|
|
3461
|
-
const afterFpIdx = args.indexOf('--after-fingerprint');
|
|
3462
|
-
if (afterFpEqArg) {
|
|
3463
|
-
const value = afterFpEqArg.slice('--after-fingerprint='.length).trim();
|
|
3464
|
-
if (!value) error('Missing value for --after-fingerprint', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3465
|
-
loopAfterFingerprint = value;
|
|
3466
|
-
} else if (afterFpIdx !== -1) {
|
|
3467
|
-
const value = args[afterFpIdx + 1];
|
|
3468
|
-
if (!value || value.startsWith('--')) {
|
|
3469
|
-
error('Missing value for --after-fingerprint', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3470
|
-
}
|
|
3471
|
-
loopAfterFingerprint = value;
|
|
3472
|
-
}
|
|
3327
|
+
// --config-dir, --active-cap <capId>, --runtime <r> (#2003: explicit runtime override so
|
|
3328
|
+
// the config-dir resolution bypasses the persisted-runtime fallback) and
|
|
3329
|
+
// --after-fingerprint <phaseDir> (#5105 R2: gate out verify:post steps whose declared
|
|
3330
|
+
// artifact already exists) each take a value in the space or `=` form; the seam refuses
|
|
3331
|
+
// a flag that is present without one, before any of it is used.
|
|
3332
|
+
const loopParsed = parseNamedArgsOrExit(args, specFor('loop.render-hooks'), error);
|
|
3333
|
+
// The spec trims the `=` form (`--runtime= claude` reads as `claude`); a space-form value is as typed.
|
|
3334
|
+
const loopValue = (name) => (loopParsed[name] === null ? undefined : loopParsed[name]);
|
|
3335
|
+
const loopConfigDir = loopValue('config-dir');
|
|
3473
3336
|
loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {
|
|
3474
3337
|
configDir: loopConfigDir ? path.resolve(loopConfigDir) : undefined,
|
|
3475
|
-
activeCap:
|
|
3476
|
-
runtime:
|
|
3477
|
-
afterFingerprint:
|
|
3338
|
+
activeCap: loopValue('active-cap'),
|
|
3339
|
+
runtime: loopValue('runtime'),
|
|
3340
|
+
afterFingerprint: loopValue('after-fingerprint'),
|
|
3478
3341
|
});
|
|
3479
3342
|
} else {
|
|
3480
3343
|
error(
|
|
@@ -3485,38 +3348,41 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3485
3348
|
}
|
|
3486
3349
|
|
|
3487
3350
|
function routePhasePlanIndex({ args, cwd, raw, error }) {
|
|
3351
|
+
parseNamedArgsOrExit(args, specFor('phase-plan-index'), error);
|
|
3488
3352
|
phase.cmdPhasePlanIndex(cwd, args[1], raw);
|
|
3489
3353
|
}
|
|
3490
3354
|
|
|
3491
3355
|
function routeStateSnapshot({ args, cwd, raw, error }) {
|
|
3356
|
+
parseNamedArgsOrExit(args, specFor('state-snapshot'), error);
|
|
3492
3357
|
state.cmdStateSnapshot(cwd, raw);
|
|
3493
3358
|
}
|
|
3494
3359
|
|
|
3495
3360
|
function routeSummaryExtract({ args, cwd, raw, error }) {
|
|
3361
|
+
const parsed = parseNamedArgsOrExit(args, specFor('summary-extract'), error);
|
|
3496
3362
|
const summaryPath = args[1];
|
|
3497
|
-
const
|
|
3498
|
-
const fields = fieldsIndex !== -1 ? args[fieldsIndex + 1].split(',') : null;
|
|
3363
|
+
const fields = parsed.fields !== null ? parsed.fields.split(',') : null;
|
|
3499
3364
|
commands.cmdSummaryExtract(cwd, summaryPath, fields, raw);
|
|
3500
3365
|
}
|
|
3501
3366
|
|
|
3502
3367
|
async function routeWebsearch({ args, cwd, raw, error }) {
|
|
3368
|
+
const parsed = parseNamedArgsOrExit(args, specFor('websearch'), error);
|
|
3503
3369
|
const query = args[1];
|
|
3504
|
-
const limitIdx = args.indexOf('--limit');
|
|
3505
|
-
const freshnessIdx = args.indexOf('--freshness');
|
|
3506
3370
|
await commands.cmdWebsearch(query, {
|
|
3507
|
-
limit:
|
|
3508
|
-
freshness:
|
|
3371
|
+
limit: parsed.limit !== null ? parseInt(parsed.limit, 10) : 10,
|
|
3372
|
+
freshness: parsed.freshness,
|
|
3509
3373
|
}, raw);
|
|
3510
3374
|
}
|
|
3511
3375
|
|
|
3512
3376
|
function routeWorkstream({ args, cwd, raw, error }) {
|
|
3513
3377
|
const subcommand = args[1];
|
|
3378
|
+
const WORKSTREAM_SUBCOMMANDS = ['create', 'list', 'status', 'complete', 'set', 'get', 'progress'];
|
|
3379
|
+
const parsed = WORKSTREAM_SUBCOMMANDS.includes(subcommand)
|
|
3380
|
+
? parseNamedArgsOrExit(args, specFor(`workstream.${subcommand}`), error)
|
|
3381
|
+
: null;
|
|
3514
3382
|
if (subcommand === 'create') {
|
|
3515
|
-
const migrateNameIdx = args.indexOf('--migrate-name');
|
|
3516
|
-
const noMigrate = args.includes('--no-migrate');
|
|
3517
3383
|
workstream.cmdWorkstreamCreate(cwd, args[2], {
|
|
3518
|
-
migrate: !
|
|
3519
|
-
migrateName:
|
|
3384
|
+
migrate: !parsed['no-migrate'],
|
|
3385
|
+
migrateName: parsed['migrate-name'],
|
|
3520
3386
|
}, raw);
|
|
3521
3387
|
} else if (subcommand === 'list') {
|
|
3522
3388
|
workstream.cmdWorkstreamList(cwd, raw);
|
|
@@ -3579,20 +3445,18 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3579
3445
|
|
|
3580
3446
|
function routeLearnings({ args, cwd, raw, error }) {
|
|
3581
3447
|
const subcommand = args[1];
|
|
3448
|
+
const LEARNINGS_SUBCOMMANDS = ['list', 'query', 'copy', 'prune', 'delete'];
|
|
3449
|
+
const parsed = LEARNINGS_SUBCOMMANDS.includes(subcommand)
|
|
3450
|
+
? parseNamedArgsOrExit(args, specFor(`learnings.${subcommand}`), error)
|
|
3451
|
+
: null;
|
|
3582
3452
|
if (subcommand === 'list') {
|
|
3583
3453
|
learnings.cmdLearningsList(raw);
|
|
3584
3454
|
} else if (subcommand === 'query') {
|
|
3585
|
-
|
|
3586
|
-
const tag = tagIdx !== -1 ? args[tagIdx + 1] : null;
|
|
3587
|
-
if (!tag) error('Usage: gsd-tools learnings query --tag <tag>', ERROR_REASON.USAGE);
|
|
3588
|
-
learnings.cmdLearningsQuery(tag, raw);
|
|
3455
|
+
learnings.cmdLearningsQuery(parsed.tag, raw);
|
|
3589
3456
|
} else if (subcommand === 'copy') {
|
|
3590
3457
|
learnings.cmdLearningsCopy(cwd, raw);
|
|
3591
3458
|
} else if (subcommand === 'prune') {
|
|
3592
|
-
|
|
3593
|
-
const olderThan = olderIdx !== -1 ? args[olderIdx + 1] : null;
|
|
3594
|
-
if (!olderThan) error('Usage: gsd-tools learnings prune --older-than <duration>', ERROR_REASON.USAGE);
|
|
3595
|
-
learnings.cmdLearningsPrune(olderThan, raw);
|
|
3459
|
+
learnings.cmdLearningsPrune(parsed['older-than'], raw);
|
|
3596
3460
|
} else if (subcommand === 'delete') {
|
|
3597
3461
|
const id = args[2];
|
|
3598
3462
|
if (!id) error('Usage: gsd-tools learnings delete <id>', ERROR_REASON.USAGE);
|
|
@@ -3644,8 +3508,9 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3644
3508
|
}
|
|
3645
3509
|
|
|
3646
3510
|
function routeTeamsStatus({ args, cwd, raw, error }) {
|
|
3511
|
+
const parsed = parseNamedArgsOrExit(args, specFor('teams-status'), error);
|
|
3647
3512
|
const teamsStatus = require('./lib/teams-status.cjs');
|
|
3648
|
-
teamsStatus.cmdTeamsStatus(cwd, { active:
|
|
3513
|
+
teamsStatus.cmdTeamsStatus(cwd, { active: parsed.active });
|
|
3649
3514
|
}
|
|
3650
3515
|
|
|
3651
3516
|
// #3023 follow-up (adversarial review finding): the shared hook bundle's
|
|
@@ -3729,11 +3594,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3729
3594
|
}
|
|
3730
3595
|
|
|
3731
3596
|
async function routeDetectCustomFiles({ args, cwd, raw, error }) {
|
|
3732
|
-
|
|
3733
|
-
const configDir =
|
|
3734
|
-
if (!configDir) {
|
|
3735
|
-
error('Usage: gsd-tools detect-custom-files --config-dir <path>', ERROR_REASON.USAGE);
|
|
3736
|
-
}
|
|
3597
|
+
// `--config-dir` is a required flag of the spec; the seam refuses a missing or empty one.
|
|
3598
|
+
const configDir = parseNamedArgsOrExit(args, specFor('detect-custom-files'), error)['config-dir'];
|
|
3737
3599
|
const resolvedConfigDir = path.resolve(configDir);
|
|
3738
3600
|
if (!fs.existsSync(resolvedConfigDir)) {
|
|
3739
3601
|
error(`Config directory not found: ${resolvedConfigDir}`, ERROR_REASON.USAGE);
|
|
@@ -4039,14 +3901,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4039
3901
|
function routeRestoreCustomFiles({ args, error }) {
|
|
4040
3902
|
// Last-wins so a duplicated flag resolves rather than erroring, matching
|
|
4041
3903
|
// the rest of the gsd-tools flag surface.
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
}
|
|
4047
|
-
const apply = args.includes('--apply');
|
|
3904
|
+
const parsed = parseNamedArgsOrExit(args, specFor('restore-custom-files'), error);
|
|
3905
|
+
const configDirs = parsed['config-dir'];
|
|
3906
|
+
const configDir = configDirs[configDirs.length - 1];
|
|
3907
|
+
const apply = parsed.apply;
|
|
4048
3908
|
|
|
4049
|
-
if (configDir
|
|
3909
|
+
if (configDir.trim() === '') {
|
|
4050
3910
|
error('Usage: gsd-tools restore-custom-files --config-dir <path> [--apply]', ERROR_REASON.USAGE);
|
|
4051
3911
|
}
|
|
4052
3912
|
const resolvedConfigDir = path.resolve(configDir);
|
|
@@ -4194,65 +4054,28 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4194
4054
|
async function routePromptBudget({ args, cwd, raw, error }) {
|
|
4195
4055
|
const promptBudget = require('./lib/prompt-budget.cjs');
|
|
4196
4056
|
|
|
4197
|
-
//
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
const
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
if (!next || next.startsWith('--')) {
|
|
4213
|
-
if (!flagMap.has(current)) flagMap.set(current, null);
|
|
4214
|
-
continue;
|
|
4215
|
-
}
|
|
4216
|
-
if (!flagMap.has(current)) flagMap.set(current, next);
|
|
4217
|
-
i++;
|
|
4218
|
-
}
|
|
4219
|
-
const getFlag = (flag) => flagMap.get(flag) ?? null;
|
|
4220
|
-
|
|
4221
|
-
const budgetStr = getFlag('--budget');
|
|
4222
|
-
const instructionsFile = getFlag('--instructions-file');
|
|
4223
|
-
const roadmapFile = getFlag('--roadmap-file');
|
|
4224
|
-
const outputPromptFile = getFlag('--output-prompt');
|
|
4225
|
-
const outputMetadataFile = getFlag('--output-metadata');
|
|
4226
|
-
const safetyMarginStr = getFlag('--safety-margin-pct');
|
|
4227
|
-
const projectMdHeadLinesStr = getFlag('--project-md-head-lines');
|
|
4228
|
-
const projectFile = getFlag('--project-file');
|
|
4229
|
-
const contextFile = getFlag('--context-file');
|
|
4230
|
-
const researchFile = getFlag('--research-file');
|
|
4231
|
-
const requirementsFile = getFlag('--requirements-file');
|
|
4057
|
+
// The spec reads every value flag first-wins and `--plan-file` as a repeatable list
|
|
4058
|
+
// (an occurrence without a value contributes nothing).
|
|
4059
|
+
const parsed = parseNamedArgsOrExit(args, specFor('prompt-budget'), error);
|
|
4060
|
+
const planFiles = parsed['plan-file'];
|
|
4061
|
+
const budgetStr = parsed.budget;
|
|
4062
|
+
const instructionsFile = parsed['instructions-file'];
|
|
4063
|
+
const roadmapFile = parsed['roadmap-file'];
|
|
4064
|
+
const outputPromptFile = parsed['output-prompt'];
|
|
4065
|
+
const outputMetadataFile = parsed['output-metadata'];
|
|
4066
|
+
const safetyMarginStr = parsed['safety-margin-pct'] || null;
|
|
4067
|
+
const projectMdHeadLinesStr = parsed['project-md-head-lines'] || null;
|
|
4068
|
+
const projectFile = parsed['project-file'];
|
|
4069
|
+
const contextFile = parsed['context-file'];
|
|
4070
|
+
const researchFile = parsed['research-file'];
|
|
4071
|
+
const requirementsFile = parsed['requirements-file'];
|
|
4232
4072
|
|
|
4233
4073
|
// ── Validate required args ─────────────────────────────────────────
|
|
4234
|
-
|
|
4235
|
-
throw new ExitError(1, 'Error: --budget <N> is required');
|
|
4236
|
-
}
|
|
4074
|
+
// Presence of the required flags is enforced by the spec's requiredFlags.
|
|
4237
4075
|
const budget = parseInt(budgetStr, 10);
|
|
4238
4076
|
if (!Number.isFinite(budget) || budget <= 0) {
|
|
4239
4077
|
throw new ExitError(1, 'Error: --budget must be a positive integer');
|
|
4240
4078
|
}
|
|
4241
|
-
if (!instructionsFile) {
|
|
4242
|
-
throw new ExitError(1, 'Error: --instructions-file <path> is required');
|
|
4243
|
-
}
|
|
4244
|
-
if (!roadmapFile) {
|
|
4245
|
-
throw new ExitError(1, 'Error: --roadmap-file <path> is required');
|
|
4246
|
-
}
|
|
4247
|
-
if (planFiles.length === 0) {
|
|
4248
|
-
throw new ExitError(1, 'Error: at least one --plan-file <path> is required');
|
|
4249
|
-
}
|
|
4250
|
-
if (!outputPromptFile) {
|
|
4251
|
-
throw new ExitError(1, 'Error: --output-prompt <path> is required');
|
|
4252
|
-
}
|
|
4253
|
-
if (!outputMetadataFile) {
|
|
4254
|
-
throw new ExitError(1, 'Error: --output-metadata <path> is required');
|
|
4255
|
-
}
|
|
4256
4079
|
|
|
4257
4080
|
// ── Validate and read required files ──────────────────────────────
|
|
4258
4081
|
async function readRequired(filePath, flagName) {
|
|
@@ -4336,7 +4159,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4336
4159
|
// by one or more optional criteria (ANDed together)"); no selector is
|
|
4337
4160
|
// silently dropped or overridden by another.
|
|
4338
4161
|
//
|
|
4339
|
-
//
|
|
4162
|
+
// Flags are read through the CLI surface spec (`context-predicates`): the three
|
|
4340
4163
|
// known flags are recognized in both the space-separated `--flag value`
|
|
4341
4164
|
// form and the inline-assignment `--flag=value` form (the latter is the
|
|
4342
4165
|
// escape hatch for a flag-shaped selector value, e.g. `--contains=--dry-run`
|
|
@@ -4344,61 +4167,26 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4344
4167
|
// design, since a following `--...` token always reads as a missing value).
|
|
4345
4168
|
// `--class=` (empty value) and `--class==A` (double-equals typo shape)
|
|
4346
4169
|
// are rejected the same way under either form. On a duplicate flag the
|
|
4347
|
-
// FIRST occurrence wins
|
|
4348
|
-
//
|
|
4170
|
+
// FIRST occurrence wins, which is deterministic across repeated invocations
|
|
4171
|
+
// with identical argv.
|
|
4349
4172
|
//
|
|
4350
4173
|
// Prototype-pollution safety: selector values are only ever compared via
|
|
4351
4174
|
// `===`/`.startsWith()`/`.includes()` against ordinary string fields — this
|
|
4352
4175
|
// route never uses a user-supplied string as an object property key
|
|
4353
4176
|
// (`obj[userValue] = ...`), so `--class __proto__` / `constructor` /
|
|
4354
4177
|
// `prototype` are just non-matching ordinary strings, not property-access
|
|
4355
|
-
// vectors.
|
|
4356
|
-
//
|
|
4178
|
+
// vectors. The parsed flags are an object keyed only by the spec's own fixed
|
|
4179
|
+
// flag names, never by user input.
|
|
4357
4180
|
function routeContextPredicates({ args, cwd, raw, error }) {
|
|
4358
4181
|
const { parsePredicates, selectPredicates } = require('./lib/context-predicates.cjs');
|
|
4359
4182
|
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
// `--runtime=` convention in routeUpdateContext elsewhere in this file). This is
|
|
4368
|
-
// the ONLY way to pass a flag-shaped selector value (e.g. searching CONTEXT.md
|
|
4369
|
-
// for the literal substring "--dry-run"): the space-separated form below always
|
|
4370
|
-
// treats a following `--...` token as a missing value, by design, so it has no
|
|
4371
|
-
// escape hatch on its own (#2928 review finding C).
|
|
4372
|
-
const eqFlag = [...KNOWN_FLAGS].find((f) => current.startsWith(`${f}=`));
|
|
4373
|
-
if (eqFlag) {
|
|
4374
|
-
const value = current.slice(eqFlag.length + 1);
|
|
4375
|
-
// Reject an empty value (`--class=`) and the `--class==A` double-equals typo
|
|
4376
|
-
// shape (a value starting with `=`) the same way the pre-existing malformed-
|
|
4377
|
-
// assignment behavior did — never silently accept "=A" as a literal value.
|
|
4378
|
-
if (value === '' || value.startsWith('=')) {
|
|
4379
|
-
error(`context-predicates: ${eqFlag} requires a non-empty value`, ERROR_REASON.USAGE);
|
|
4380
|
-
return;
|
|
4381
|
-
}
|
|
4382
|
-
if (!flagMap.has(eqFlag)) flagMap.set(eqFlag, value);
|
|
4383
|
-
continue;
|
|
4384
|
-
}
|
|
4385
|
-
|
|
4386
|
-
if (!KNOWN_FLAGS.has(current)) {
|
|
4387
|
-
error(`Unknown flag for context-predicates: ${current}`, ERROR_REASON.USAGE);
|
|
4388
|
-
return;
|
|
4389
|
-
}
|
|
4390
|
-
const next = args[i + 1];
|
|
4391
|
-
if (next === undefined || next.startsWith('--')) {
|
|
4392
|
-
if (!flagMap.has(current)) flagMap.set(current, null);
|
|
4393
|
-
continue;
|
|
4394
|
-
}
|
|
4395
|
-
if (!flagMap.has(current)) flagMap.set(current, next);
|
|
4396
|
-
i++;
|
|
4397
|
-
}
|
|
4398
|
-
|
|
4399
|
-
const hasClass = flagMap.has('--class');
|
|
4400
|
-
const hasPrefix = flagMap.has('--prefix');
|
|
4401
|
-
const hasContains = flagMap.has('--contains');
|
|
4183
|
+
// The spec reads `--flag value` and `--flag=value`, refuses a selector that is present
|
|
4184
|
+
// without a value (a following `--...` token reads as missing, `--class=` is empty), and
|
|
4185
|
+
// keeps the FIRST occurrence of a duplicate.
|
|
4186
|
+
const parsed = parseNamedArgsOrExit(args, specFor('context-predicates'), error);
|
|
4187
|
+
const hasClass = parsed.class !== null;
|
|
4188
|
+
const hasPrefix = parsed.prefix !== null;
|
|
4189
|
+
const hasContains = parsed.contains !== null;
|
|
4402
4190
|
|
|
4403
4191
|
if (!hasClass && !hasPrefix && !hasContains) {
|
|
4404
4192
|
error(
|
|
@@ -4409,30 +4197,10 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4409
4197
|
return;
|
|
4410
4198
|
}
|
|
4411
4199
|
|
|
4412
|
-
const requireNonEmpty = (flagName, rawValue) => {
|
|
4413
|
-
if (rawValue === null || rawValue === undefined || rawValue.trim() === '') {
|
|
4414
|
-
error(`context-predicates: ${flagName} requires a non-empty value`, ERROR_REASON.USAGE);
|
|
4415
|
-
return null;
|
|
4416
|
-
}
|
|
4417
|
-
return rawValue;
|
|
4418
|
-
};
|
|
4419
|
-
|
|
4420
4200
|
const opts = {};
|
|
4421
|
-
if (hasClass)
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
opts.klass = v;
|
|
4425
|
-
}
|
|
4426
|
-
if (hasPrefix) {
|
|
4427
|
-
const v = requireNonEmpty('--prefix', flagMap.get('--prefix'));
|
|
4428
|
-
if (v === null) return;
|
|
4429
|
-
opts.prefix = v;
|
|
4430
|
-
}
|
|
4431
|
-
if (hasContains) {
|
|
4432
|
-
const v = requireNonEmpty('--contains', flagMap.get('--contains'));
|
|
4433
|
-
if (v === null) return;
|
|
4434
|
-
opts.contains = v;
|
|
4435
|
-
}
|
|
4201
|
+
if (hasClass) opts.klass = parsed.class;
|
|
4202
|
+
if (hasPrefix) opts.prefix = parsed.prefix;
|
|
4203
|
+
if (hasContains) opts.contains = parsed.contains;
|
|
4436
4204
|
|
|
4437
4205
|
const contextMdPath = path.join(__dirname, '..', '..', 'CONTEXT.md');
|
|
4438
4206
|
let markdown;
|
|
@@ -4456,46 +4224,27 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4456
4224
|
// scope, runtime, gsdDir }. Optional --config-dir / --runtime carry the
|
|
4457
4225
|
// workflow's execution_context hints (the one thing only it can know).
|
|
4458
4226
|
const { loadUpdateContext } = require('./lib/update-context.cjs');
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
if (a.startsWith('--runtime=')) { preferredRuntime = a.slice('--runtime='.length); continue; }
|
|
4466
|
-
if (a === '--config-dir') {
|
|
4467
|
-
const v = ucArgs[i + 1];
|
|
4468
|
-
if (v === undefined || v.startsWith('--')) error('Missing value for --config-dir', ERROR_REASON.USAGE);
|
|
4469
|
-
preferredConfigDir = v; i++; continue;
|
|
4470
|
-
}
|
|
4471
|
-
if (a === '--runtime') {
|
|
4472
|
-
const v = ucArgs[i + 1];
|
|
4473
|
-
if (v === undefined || v.startsWith('--')) error('Missing value for --runtime', ERROR_REASON.USAGE);
|
|
4474
|
-
preferredRuntime = v; i++; continue;
|
|
4475
|
-
}
|
|
4476
|
-
if (a === '--json') continue; // JSON is the only output; accepted for symmetry
|
|
4477
|
-
if (a.startsWith('-')) error(`Unknown flag for update-context: ${a}`, ERROR_REASON.USAGE);
|
|
4478
|
-
}
|
|
4479
|
-
const ctx = loadUpdateContext({ preferredConfigDir, preferredRuntime });
|
|
4227
|
+
// `--json` is declared but unread: JSON is the only output, accepted for symmetry.
|
|
4228
|
+
const ucParsed = parseNamedArgsOrExit(args, specFor('update-context'), error);
|
|
4229
|
+
const ctx = loadUpdateContext({
|
|
4230
|
+
preferredConfigDir: ucParsed['config-dir'] ?? '',
|
|
4231
|
+
preferredRuntime: ucParsed.runtime ?? '',
|
|
4232
|
+
});
|
|
4480
4233
|
process.stdout.write(JSON.stringify(ctx) + '\n');
|
|
4481
4234
|
}
|
|
4482
4235
|
|
|
4483
4236
|
async function routeClassifyConfidence({ args, cwd, raw, error }) {
|
|
4484
4237
|
const researchProvider = require('./lib/research-provider.cjs');
|
|
4485
|
-
|
|
4486
|
-
const
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
const
|
|
4491
|
-
const pkgIdx = args.indexOf('--package');
|
|
4492
|
-
const pkg = pkgIdx !== -1 ? args[pkgIdx + 1] : null;
|
|
4493
|
-
const ecoIdx = args.indexOf('--ecosystem');
|
|
4494
|
-
const ecosystem = ecoIdx !== -1 ? args[ecoIdx + 1] : null;
|
|
4238
|
+
// `--provider` is a required flag of the spec; the seam refuses a missing or flag-shaped one.
|
|
4239
|
+
const ccParsed = parseNamedArgsOrExit(args, specFor('classify-confidence'), error);
|
|
4240
|
+
const provider = ccParsed.provider;
|
|
4241
|
+
const verified = ccParsed.verified;
|
|
4242
|
+
const pkg = ccParsed.package;
|
|
4243
|
+
const ecosystem = ccParsed.ecosystem;
|
|
4495
4244
|
let legitimacyVerdict = null;
|
|
4496
|
-
if (pkg
|
|
4245
|
+
if (pkg) {
|
|
4497
4246
|
const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
|
|
4498
|
-
if (!ecosystem ||
|
|
4247
|
+
if (!ecosystem || !VALID_ECOSYSTEMS.has(ecosystem)) {
|
|
4499
4248
|
error('Usage: gsd-tools query classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]', ERROR_REASON.USAGE);
|
|
4500
4249
|
}
|
|
4501
4250
|
const pkgLegitimacy = require('./lib/package-legitimacy.cjs');
|
|
@@ -4512,24 +4261,16 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4512
4261
|
if (subcommand !== 'check') {
|
|
4513
4262
|
error('Unknown package-legitimacy subcommand. Available: check', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
4514
4263
|
}
|
|
4515
|
-
|
|
4516
|
-
|
|
4264
|
+
// `--ecosystem` is a required flag of the spec; an unknown --flag is a usage error
|
|
4265
|
+
// (it is never skipped along with the next arg).
|
|
4266
|
+
const plSpec = specFor('package-legitimacy.check');
|
|
4267
|
+
const ecosystem = parseNamedArgsOrExit(args, plSpec, error).ecosystem;
|
|
4517
4268
|
const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
|
|
4518
|
-
if (!
|
|
4269
|
+
if (!VALID_ECOSYSTEMS.has(ecosystem)) {
|
|
4519
4270
|
error('Usage: gsd-tools package-legitimacy check --ecosystem <npm|pypi|crates> <pkg1> ...', ERROR_REASON.USAGE);
|
|
4520
4271
|
}
|
|
4521
|
-
//
|
|
4522
|
-
|
|
4523
|
-
// Any unknown --flag is a usage error (do not silently skip+consume the next arg).
|
|
4524
|
-
const packages = [];
|
|
4525
|
-
for (let i = 2; i < args.length; i++) {
|
|
4526
|
-
const a = args[i];
|
|
4527
|
-
if (a === '--ecosystem') { i++; continue; }
|
|
4528
|
-
if (a.startsWith('--')) {
|
|
4529
|
-
error(`package-legitimacy: unknown flag ${a}`, ERROR_REASON.USAGE);
|
|
4530
|
-
}
|
|
4531
|
-
packages.push(a);
|
|
4532
|
-
}
|
|
4272
|
+
// Every non-flag word past `check` that is not the --ecosystem value is a package name.
|
|
4273
|
+
const packages = positionalWords(args, plSpec, 2);
|
|
4533
4274
|
if (packages.length === 0) {
|
|
4534
4275
|
error('Usage: gsd-tools package-legitimacy check --ecosystem <eco> <pkg1> <pkg2> ...', ERROR_REASON.USAGE);
|
|
4535
4276
|
}
|
|
@@ -4545,31 +4286,15 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4545
4286
|
function routeEffort({ args, cwd, raw, error }) {
|
|
4546
4287
|
const subcommand = args[1];
|
|
4547
4288
|
if (subcommand === 'sync') {
|
|
4548
|
-
const
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
if (a === '--config-dir') {
|
|
4558
|
-
const v = effortSyncArgs[i + 1];
|
|
4559
|
-
if (!v || v.startsWith('--')) error('Missing value for --config-dir', ERROR_REASON.USAGE);
|
|
4560
|
-
effortSyncConfigDir = v; i++; continue;
|
|
4561
|
-
}
|
|
4562
|
-
if (a.startsWith('--runtime=')) { effortSyncRuntime = a.slice('--runtime='.length); continue; }
|
|
4563
|
-
if (a === '--runtime') {
|
|
4564
|
-
const v = effortSyncArgs[i + 1];
|
|
4565
|
-
if (!v || v.startsWith('--')) error('Missing value for --runtime', ERROR_REASON.USAGE);
|
|
4566
|
-
effortSyncRuntime = v; i++; continue;
|
|
4567
|
-
}
|
|
4568
|
-
if (a === '--raw') continue;
|
|
4569
|
-
if (a.startsWith('-')) error(`Unknown flag for effort sync: ${a}`, ERROR_REASON.USAGE);
|
|
4570
|
-
error(`effort sync takes no positional arguments; got: ${a}`, ERROR_REASON.USAGE);
|
|
4571
|
-
}
|
|
4572
|
-
commands.cmdEffortSync(cwd, raw, { dryRun, configDir: effortSyncConfigDir, runtime: effortSyncRuntime });
|
|
4289
|
+
const effortSyncParsed = parseNamedArgsOrExit(args, specFor('effort.sync'), error);
|
|
4290
|
+
// Preview is the default; `--apply` writes. When both are given the preview wins, so a
|
|
4291
|
+
// contradictory argv never writes.
|
|
4292
|
+
const dryRun = !effortSyncParsed.apply || effortSyncParsed['dry-run'];
|
|
4293
|
+
commands.cmdEffortSync(cwd, raw, {
|
|
4294
|
+
dryRun,
|
|
4295
|
+
configDir: effortSyncParsed['config-dir'] ?? undefined,
|
|
4296
|
+
runtime: effortSyncParsed.runtime ?? undefined,
|
|
4297
|
+
});
|
|
4573
4298
|
} else {
|
|
4574
4299
|
error('Unknown effort subcommand. Available: sync', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
4575
4300
|
}
|
|
@@ -4582,10 +4307,9 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4582
4307
|
return;
|
|
4583
4308
|
}
|
|
4584
4309
|
|
|
4585
|
-
const
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
: '';
|
|
4310
|
+
const parsed = parseNamedArgsOrExit(args, specFor('user-story.validate'), error);
|
|
4311
|
+
|
|
4312
|
+
const story = parsed.story || '';
|
|
4589
4313
|
|
|
4590
4314
|
// Canonical extraction regex — requires non-whitespace content in each slot
|
|
4591
4315
|
// (\S.*? ensures the slot isn't whitespace-only).
|
|
@@ -4634,6 +4358,10 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4634
4358
|
// drift-guard authority → effective authority string
|
|
4635
4359
|
// drift-guard severity --status <S> [--authority <A>] → {severity, hardBlock}
|
|
4636
4360
|
const subcommand = args[1];
|
|
4361
|
+
const DRIFT_GUARD_SUBCOMMANDS = ['authority', 'severity', 'phase-status'];
|
|
4362
|
+
const parsed = DRIFT_GUARD_SUBCOMMANDS.includes(subcommand)
|
|
4363
|
+
? parseNamedArgsOrExit(args, specFor(`drift-guard.${subcommand}`), error)
|
|
4364
|
+
: null;
|
|
4637
4365
|
|
|
4638
4366
|
// Read config.json directly for both plan_review.source_grounding_authority
|
|
4639
4367
|
// and intel.enabled. Neither key is in the config-loader.cjs whitelist that
|
|
@@ -4644,8 +4372,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4644
4372
|
try {
|
|
4645
4373
|
const { planningDir } = require('./lib/planning-workspace.cjs');
|
|
4646
4374
|
const cfgPath = require('path').join(planningDir(cwd), 'config.json');
|
|
4647
|
-
|
|
4648
|
-
|
|
4375
|
+
// #5759: bounded regular-file read — a FIFO/device/oversized config throws to the
|
|
4376
|
+
// catch below (defaults apply) instead of blocking; null is an absent file.
|
|
4377
|
+
const rawCfgText = require('./lib/config-file-read.cjs').readConfigFileBounded(cfgPath);
|
|
4378
|
+
if (rawCfgText !== null) {
|
|
4379
|
+
const rawCfg = JSON.parse(rawCfgText);
|
|
4649
4380
|
if (rawCfg && rawCfg.plan_review && rawCfg.plan_review.source_grounding_authority) {
|
|
4650
4381
|
configuredAuthority = String(rawCfg.plan_review.source_grounding_authority);
|
|
4651
4382
|
}
|
|
@@ -4666,17 +4397,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4666
4397
|
}
|
|
4667
4398
|
|
|
4668
4399
|
if (subcommand === 'severity') {
|
|
4669
|
-
const
|
|
4670
|
-
const
|
|
4671
|
-
if (!statusVal || statusVal.startsWith('--')) {
|
|
4672
|
-
error('drift-guard severity requires --status <VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE>', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
4673
|
-
return;
|
|
4674
|
-
}
|
|
4675
|
-
const authIdx = args.indexOf('--authority');
|
|
4676
|
-
const authVal = authIdx !== -1 ? args[authIdx + 1] : undefined;
|
|
4677
|
-
const authorityForClassify = (authVal && !authVal.startsWith('--'))
|
|
4678
|
-
? authVal
|
|
4679
|
-
: effectiveAuthority;
|
|
4400
|
+
const statusVal = parsed.status;
|
|
4401
|
+
const authorityForClassify = parsed.authority || effectiveAuthority;
|
|
4680
4402
|
const result = classifyDriftSeverity({ status: statusVal, authority: authorityForClassify });
|
|
4681
4403
|
output(result, raw);
|
|
4682
4404
|
return;
|
|
@@ -4696,10 +4418,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4696
4418
|
// is scoped to the body.
|
|
4697
4419
|
const { stripFrontmatter } = require('./lib/frontmatter.cjs');
|
|
4698
4420
|
|
|
4699
|
-
const
|
|
4700
|
-
const phaseArg = (phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--'))
|
|
4701
|
-
? args[phaseIdx + 1]
|
|
4702
|
-
: undefined;
|
|
4421
|
+
const phaseArg = parsed.phase || undefined;
|
|
4703
4422
|
|
|
4704
4423
|
const dir = planningDir(cwd);
|
|
4705
4424
|
const phaseConvention = resolvePhaseIdConvention(cwd);
|
|
@@ -4874,31 +4593,6 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4874
4593
|
* declares that file "All OS-facing I/O; single platform seam", and a private
|
|
4875
4594
|
* duplicate here is what made it untrue.
|
|
4876
4595
|
*/
|
|
4877
|
-
const COMMIT_LIST_FLAGS = new Set(['--files', '--files-removed']);
|
|
4878
|
-
|
|
4879
|
-
// #4208 review: hoisted out of routeCommit's closure so the parser is reachable
|
|
4880
|
-
// from a test. It is the whole of the two-list argument contract, and its edge
|
|
4881
|
-
// cases (a boolean flag inside a run, a repeated list flag, either order) were
|
|
4882
|
-
// already the subject of a review round -- a parser that only the CLI can reach
|
|
4883
|
-
// can only be tested by example, one spawn at a time.
|
|
4884
|
-
//
|
|
4885
|
-
// Every occurrence of `flag` contributes a run; a run ends at the next LIST
|
|
4886
|
-
// flag and skips boolean flags on the way, so no token strictly between one
|
|
4887
|
-
// list flag and the next is ever dropped. Repeated runs of the same flag merge,
|
|
4888
|
-
// as the pre-#4208 slice-to-end parse merged them.
|
|
4889
|
-
function collectListFlagValues(args, flag) {
|
|
4890
|
-
const values = [];
|
|
4891
|
-
args.forEach((a, i) => {
|
|
4892
|
-
if (a !== flag) return;
|
|
4893
|
-
for (const b of args.slice(i + 1)) {
|
|
4894
|
-
if (COMMIT_LIST_FLAGS.has(b)) break;
|
|
4895
|
-
if (b.startsWith('--')) continue;
|
|
4896
|
-
values.push(b);
|
|
4897
|
-
}
|
|
4898
|
-
});
|
|
4899
|
-
return values;
|
|
4900
|
-
}
|
|
4901
|
-
|
|
4902
4596
|
function resolveSpawnBinary(name, platform = process.platform, env = process.env) {
|
|
4903
4597
|
const { resolveExecutableBinary } = require('./lib/shell-command-projection.cjs');
|
|
4904
4598
|
return resolveExecutableBinary(name, { platform, env });
|
|
@@ -4946,12 +4640,12 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
4946
4640
|
// contract and this one mutates; ADR-2346 promotes a leaf to a family only at
|
|
4947
4641
|
// >=3 verbs, and this is one.
|
|
4948
4642
|
'stamp-codebase-map': ({ args, cwd, raw, error }) => {
|
|
4949
|
-
const { files } = parseNamedArgsOrExit(args,
|
|
4950
|
-
//
|
|
4951
|
-
//
|
|
4952
|
-
//
|
|
4953
|
-
const only =
|
|
4954
|
-
?
|
|
4643
|
+
const { files } = parseNamedArgsOrExit(args, specFor('stamp-codebase-map'), error);
|
|
4644
|
+
// The spec reads a bare `--files` as '' and an absent one as null, so a bare
|
|
4645
|
+
// `--files` (an unquoted empty shell variable) hits the empty-filter refusal
|
|
4646
|
+
// instead of widening to all seven.
|
|
4647
|
+
const only = files !== null
|
|
4648
|
+
? files.split(',').map((f) => f.trim()).filter(Boolean)
|
|
4955
4649
|
: undefined;
|
|
4956
4650
|
verify.cmdStampCodebaseMap(cwd, raw, only);
|
|
4957
4651
|
},
|
|
@@ -4971,6 +4665,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
4971
4665
|
'commit': routeCommit,
|
|
4972
4666
|
'check-commit': routeCheckCommit,
|
|
4973
4667
|
'commit-docs-guard': routeCommitDocsGuard,
|
|
4668
|
+
'project-context': routeProjectContext,
|
|
4974
4669
|
'commit-to-subrepo': routeCommitToSubrepo,
|
|
4975
4670
|
'pr-subrepo': routePrSubrepo,
|
|
4976
4671
|
'verify-summary': routeVerifySummary,
|
|
@@ -5046,7 +4741,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
5046
4741
|
// through. Prototype-pollution-safe: own-property lookup rejects
|
|
5047
4742
|
// `__proto__`/`constructor`/`prototype` command keys (same guard as
|
|
5048
4743
|
// dispatchCapabilityCommand).
|
|
5049
|
-
async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd }) {
|
|
4744
|
+
async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd, ctx }) {
|
|
5050
4745
|
if (
|
|
5051
4746
|
command === '__proto__' ||
|
|
5052
4747
|
command === 'constructor' ||
|
|
@@ -5067,7 +4762,9 @@ async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValu
|
|
|
5067
4762
|
// every other router legitimately wants via `cwd`. Optional — callers/tests
|
|
5068
4763
|
// that omit it (every existing one) leave routers that don't read it
|
|
5069
4764
|
// byte-unaffected.
|
|
5070
|
-
|
|
4765
|
+
// `ctx` (#6043): the ProjectContext main() resolved once; routers that take a
|
|
4766
|
+
// named root (commit, project-context) read it, the rest ignore it.
|
|
4767
|
+
await router({ args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd, ctx });
|
|
5071
4768
|
return true; // consumed — don't emit "Unknown command"
|
|
5072
4769
|
}
|
|
5073
4770
|
|
|
@@ -5296,7 +4993,7 @@ function runWithTimeout(argv) {
|
|
|
5296
4993
|
// independently hand-maintained sites and nothing previously caught them
|
|
5297
4994
|
// drifting apart when a query command was added to only one or two.
|
|
5298
4995
|
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--project-dir <path>] [--ws <name>] [--json-errors] [--exit-contract=<v>]\n' +
|
|
5299
|
-
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-docs-guard, commit-to-subrepo, pr-subrepo, ' +
|
|
4996
|
+
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-docs-guard, commit-to-subrepo, pr-subrepo, project-context, ' +
|
|
5300
4997
|
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, resolve-package-manager, ' +
|
|
5301
4998
|
'context-predicates, current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
|
5302
4999
|
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
|
@@ -5345,6 +5042,9 @@ const SKIP_ROOT_RESOLUTION = new Set([
|
|
|
5345
5042
|
// explicitly via --config-dir; it never reads .planning/.
|
|
5346
5043
|
'restore-custom-files',
|
|
5347
5044
|
'worktree', 'prompt-budget',
|
|
5045
|
+
// #6043: project-context prints the resolved ProjectContext itself, so it
|
|
5046
|
+
// needs no remapped cwd; the dispatcher hands every router `ctx`.
|
|
5047
|
+
'project-context',
|
|
5348
5048
|
// context-predicates is a pure repo-root CONTEXT.md read (like
|
|
5349
5049
|
// prompt-budget); it never touches .planning/, so it needs no project
|
|
5350
5050
|
// root resolution and must work from any cwd (including one with no
|
|
@@ -5374,34 +5074,22 @@ function skipsRootResolution(command) {
|
|
|
5374
5074
|
}
|
|
5375
5075
|
|
|
5376
5076
|
/**
|
|
5377
|
-
*
|
|
5378
|
-
*
|
|
5379
|
-
*
|
|
5380
|
-
* be driven directly in tests via injected deps.
|
|
5077
|
+
* Warn to stderr when git could not determine the worktree root (provenance
|
|
5078
|
+
* reason 'git_timed_out') rather than silently trusting a best-effort
|
|
5079
|
+
* fallback (#3050). Keyed on the resolved ProjectContext (#6043).
|
|
5381
5080
|
*
|
|
5382
|
-
* @param {string}
|
|
5383
|
-
* @param {
|
|
5384
|
-
* @returns {string} resolved cwd
|
|
5081
|
+
* @param {{ worktreeRoot: string, provenance: { reason: string } }} ctx
|
|
5082
|
+
* @param {(msg: string) => void} [writeWarning]
|
|
5385
5083
|
*/
|
|
5386
|
-
function
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
if (worktreeRootReason === 'git_timed_out') {
|
|
5396
|
-
writeWarning(
|
|
5397
|
-
'WARNING: could not determine the git worktree root (git timed out). ' +
|
|
5398
|
-
'Planning artifacts (STATE.md, ROADMAP.md, etc.) may be written to the ' +
|
|
5399
|
-
`wrong tree — proceeding with "${worktreeRoot}" as a best-effort fallback. ` +
|
|
5400
|
-
'Retry the command; if this persists, check for a stalled filesystem mount ' +
|
|
5401
|
-
'or a stale git index lock (.git/index.lock) in this worktree.\n'
|
|
5402
|
-
);
|
|
5403
|
-
}
|
|
5404
|
-
return worktreeRoot;
|
|
5084
|
+
function warnIfGitTimedOut(ctx, writeWarning = (msg) => process.stderr.write(msg)) {
|
|
5085
|
+
if (ctx.provenance.reason !== 'git_timed_out') return;
|
|
5086
|
+
writeWarning(
|
|
5087
|
+
'WARNING: could not determine the git worktree root (git timed out). ' +
|
|
5088
|
+
'Planning artifacts (STATE.md, ROADMAP.md, etc.) may be written to the ' +
|
|
5089
|
+
`wrong tree — proceeding with "${ctx.worktreeRoot}" as a best-effort fallback. ` +
|
|
5090
|
+
'Retry the command; if this persists, check for a stalled filesystem mount ' +
|
|
5091
|
+
'or a stale git index lock (.git/index.lock) in this worktree.\n'
|
|
5092
|
+
);
|
|
5405
5093
|
}
|
|
5406
5094
|
|
|
5407
5095
|
// ─── #5118: an out-of-set verification status thrown past a command ─────────
|
|
@@ -5453,6 +5141,7 @@ async function main() {
|
|
|
5453
5141
|
// The argv splice must happen here too, otherwise the dispatcher below sees
|
|
5454
5142
|
// "--json-errors" as an unknown command. Default off — human operators keep
|
|
5455
5143
|
// their plain-text diagnostic.
|
|
5144
|
+
// eslint-disable-next-line local/named-args-spec-from-registry -- global flags are parsed once in main() and declared in GLOBAL_FLAGS (src/cli-surface.cts)
|
|
5456
5145
|
const jsonErrorsIdx = args.indexOf('--json-errors');
|
|
5457
5146
|
if (jsonErrorsIdx !== -1) {
|
|
5458
5147
|
setJsonErrorMode(true);
|
|
@@ -5476,6 +5165,7 @@ async function main() {
|
|
|
5476
5165
|
// survive into the dispatcher and reproduce the same "Unknown command".
|
|
5477
5166
|
resolveContractVersion({ argv: process.argv, env: process.env });
|
|
5478
5167
|
for (let i = args.length - 1; i >= 0; i--) {
|
|
5168
|
+
// eslint-disable-next-line local/named-args-spec-from-registry -- global flags are parsed once in main() and declared in GLOBAL_FLAGS (src/cli-surface.cts)
|
|
5479
5169
|
if (typeof args[i] === 'string' && args[i].startsWith('--exit-contract=')) {
|
|
5480
5170
|
args.splice(i, 1);
|
|
5481
5171
|
}
|
|
@@ -5498,7 +5188,9 @@ async function main() {
|
|
|
5498
5188
|
|
|
5499
5189
|
// Optional cwd override for sandboxed subagents running outside project root.
|
|
5500
5190
|
let cwd = process.cwd();
|
|
5191
|
+
// eslint-disable-next-line local/named-args-spec-from-registry -- global flags are parsed once in main() and declared in GLOBAL_FLAGS (src/cli-surface.cts)
|
|
5501
5192
|
const cwdEqArg = args.find(arg => arg.startsWith('--cwd='));
|
|
5193
|
+
// eslint-disable-next-line local/named-args-spec-from-registry -- global flags are parsed once in main() and declared in GLOBAL_FLAGS (src/cli-surface.cts)
|
|
5502
5194
|
const cwdIdx = args.indexOf('--cwd');
|
|
5503
5195
|
if (cwdEqArg) {
|
|
5504
5196
|
const value = cwdEqArg.slice('--cwd='.length).trim();
|
|
@@ -5524,7 +5216,9 @@ async function main() {
|
|
|
5524
5216
|
// runs, and its result must skip that call entirely below. A relative
|
|
5525
5217
|
// value resolves against process.cwd(), matching --cwd's own resolution.
|
|
5526
5218
|
let projectDirExplicit = false;
|
|
5219
|
+
// eslint-disable-next-line local/named-args-spec-from-registry -- global flags are parsed once in main() and declared in GLOBAL_FLAGS (src/cli-surface.cts)
|
|
5527
5220
|
const projectDirEqArg = args.find(arg => arg.startsWith('--project-dir='));
|
|
5221
|
+
// eslint-disable-next-line local/named-args-spec-from-registry -- global flags are parsed once in main() and declared in GLOBAL_FLAGS (src/cli-surface.cts)
|
|
5528
5222
|
const projectDirIdx = args.indexOf('--project-dir');
|
|
5529
5223
|
let projectDirValue;
|
|
5530
5224
|
if (projectDirEqArg) {
|
|
@@ -5548,26 +5242,25 @@ async function main() {
|
|
|
5548
5242
|
cwd = resolvedProjectDir;
|
|
5549
5243
|
projectDirExplicit = true;
|
|
5550
5244
|
}
|
|
5245
|
+
// #6043 (ADR-4908): the project context is resolved ONCE, here, and every
|
|
5246
|
+
// root a command needs comes from it. `--project-dir` replaces the start
|
|
5247
|
+
// directory for every rule (explicit validated above).
|
|
5248
|
+
//
|
|
5249
|
+
// #4906/#4465: `invokingCwd` stays the raw pre-resolution cwd — a command
|
|
5250
|
+
// whose own git operation must run against the INVOKING worktree's HEAD
|
|
5251
|
+
// (routeSelectRevertCommits) needs a path inside that worktree, never a
|
|
5252
|
+
// remapped root (tests/undo-commit-selection-4465.test.cjs).
|
|
5253
|
+
const preWorktreeRemapCwd = cwd;
|
|
5254
|
+
const ctx = resolveProjectContext(cwd, { explicit: projectDirExplicit ? cwd : null });
|
|
5255
|
+
warnIfGitTimedOut(ctx);
|
|
5551
5256
|
// #4894: verification derives its root from a phase directory, not `cwd`, so
|
|
5552
5257
|
// hand it the validated explicit root. Always (re)set — `null` when the flag is
|
|
5553
5258
|
// absent — so a prior in-process main() call can never leak an override.
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
//
|
|
5557
|
-
//
|
|
5558
|
-
|
|
5559
|
-
//
|
|
5560
|
-
// #4906/#4465: this remap is a BLANKET rewrite applied to every command, before
|
|
5561
|
-
// `command` is even known — most commands need it (they read `.planning/`, which
|
|
5562
|
-
// in a linked worktree lives only in the main one). A command whose own git
|
|
5563
|
-
// operation must run against the INVOKING worktree's OWN HEAD/branch (never the
|
|
5564
|
-
// main worktree's) needs the PRE-remap value instead — preserved here so it can be
|
|
5565
|
-
// threaded through dispatchHostCommand as `invokingCwd` (see routeSelectRevertCommits,
|
|
5566
|
-
// the first consumer: its `git log` must see the caller's own reachable history, not
|
|
5567
|
-
// main's, or a linked-worktree revert range silently resolves against the wrong repo
|
|
5568
|
-
// state — reproduced in tests/undo-commit-selection-4465.test.cjs's linked-worktree case).
|
|
5569
|
-
const preWorktreeRemapCwd = cwd;
|
|
5570
|
-
cwd = resolveMainWorktreeCwd(cwd);
|
|
5259
|
+
// Deleted in Phase 2 (#6044) when verification takes the context itself.
|
|
5260
|
+
projectRoot.setExplicitProjectRoot(projectDirExplicit ? ctx.projectRoot : null);
|
|
5261
|
+
// SKIP_ROOT_RESOLUTION commands and workstream resolution keep the value the
|
|
5262
|
+
// retired resolveMainWorktreeCwd produced (Phase 2 migrates them to named fields).
|
|
5263
|
+
cwd = legacyDispatchCwd(ctx, cwd);
|
|
5571
5264
|
|
|
5572
5265
|
// Optional workstream override for parallel milestone work.
|
|
5573
5266
|
// Priority: --ws flag > GSD_WORKSTREAM env var > session/shared pointer > null.
|
|
@@ -5596,6 +5289,7 @@ async function main() {
|
|
|
5596
5289
|
error(err.message || String(err));
|
|
5597
5290
|
}
|
|
5598
5291
|
|
|
5292
|
+
// eslint-disable-next-line local/named-args-spec-from-registry -- global flags are parsed once in main() and declared in GLOBAL_FLAGS (src/cli-surface.cts)
|
|
5599
5293
|
const rawIndex = args.indexOf('--raw');
|
|
5600
5294
|
const raw = rawIndex !== -1;
|
|
5601
5295
|
if (rawIndex !== -1) args.splice(rawIndex, 1);
|
|
@@ -5603,6 +5297,7 @@ async function main() {
|
|
|
5603
5297
|
// --pick <name>: extract a single field from JSON output (replaces jq dependency).
|
|
5604
5298
|
// Supports dot-notation (e.g., --pick workflow.research) and bracket notation
|
|
5605
5299
|
// for arrays (e.g., --pick directories[-1]).
|
|
5300
|
+
// eslint-disable-next-line local/named-args-spec-from-registry -- global flags are parsed once in main() and declared in GLOBAL_FLAGS (src/cli-surface.cts)
|
|
5606
5301
|
const pickIdx = args.indexOf('--pick');
|
|
5607
5302
|
let pickField = null;
|
|
5608
5303
|
if (pickIdx !== -1) {
|
|
@@ -5614,6 +5309,7 @@ async function main() {
|
|
|
5614
5309
|
// --default <value>: for config-get, return this value instead of erroring
|
|
5615
5310
|
// when the key is absent. Allows workflows to express optional config reads
|
|
5616
5311
|
// without defensive `2>/dev/null || true` boilerplate (#1893).
|
|
5312
|
+
// eslint-disable-next-line local/named-args-spec-from-registry -- global flags are parsed once in main() and declared in GLOBAL_FLAGS (src/cli-surface.cts)
|
|
5617
5313
|
const defaultIdx = args.indexOf('--default');
|
|
5618
5314
|
let defaultValue = undefined;
|
|
5619
5315
|
if (defaultIdx !== -1) {
|
|
@@ -5660,7 +5356,7 @@ async function main() {
|
|
|
5660
5356
|
// discovering the command surface via subcommand help requests routed
|
|
5661
5357
|
// through this dispatcher. Rendering top-level usage on --help is strictly
|
|
5662
5358
|
// better UX than the old short-circuit that printed unrelated usage text.
|
|
5663
|
-
const HELP_FLAGS = new Set(
|
|
5359
|
+
const HELP_FLAGS = new Set(GLOBAL_FLAGS.helpTokens);
|
|
5664
5360
|
if (args.some((a) => HELP_FLAGS.has(a))) {
|
|
5665
5361
|
process.stdout.write(TOP_LEVEL_USAGE + '\n');
|
|
5666
5362
|
return;
|
|
@@ -5669,7 +5365,7 @@ async function main() {
|
|
|
5669
5365
|
// Reject version flags. AI agents sometimes hallucinate --version on tool
|
|
5670
5366
|
// invocations; silently ignoring it can cause destructive operations to
|
|
5671
5367
|
// proceed unchecked. (Help flags are handled above.)
|
|
5672
|
-
const NEVER_VALID_FLAGS = new Set(
|
|
5368
|
+
const NEVER_VALID_FLAGS = new Set(GLOBAL_FLAGS.rejectedTokens);
|
|
5673
5369
|
for (const arg of args) {
|
|
5674
5370
|
if (NEVER_VALID_FLAGS.has(arg)) {
|
|
5675
5371
|
error(`Unknown flag: ${arg}\ngsd-tools does not accept version flags. Run "gsd-tools" with no arguments for usage.`, ERROR_REASON.USAGE);
|
|
@@ -5677,10 +5373,9 @@ async function main() {
|
|
|
5677
5373
|
}
|
|
5678
5374
|
|
|
5679
5375
|
// #3881: an explicit --project-dir already IS the resolved project root
|
|
5680
|
-
// (validated above)
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
cwd = findProjectRoot(cwd);
|
|
5376
|
+
// (validated above); the resolver reports it as `explicit` without a walk.
|
|
5377
|
+
if (!SKIP_ROOT_RESOLUTION.has(command)) {
|
|
5378
|
+
cwd = ctx.projectRoot;
|
|
5684
5379
|
}
|
|
5685
5380
|
|
|
5686
5381
|
// When --pick is active, capture stdout and extract the requested field.
|
|
@@ -5692,7 +5387,7 @@ async function main() {
|
|
|
5692
5387
|
// false "output was not JSON" (negative space N8).
|
|
5693
5388
|
if (pickField) {
|
|
5694
5389
|
const captured = await captureTranslatingVerificationStatus(async () => {
|
|
5695
|
-
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd);
|
|
5390
|
+
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd, ctx);
|
|
5696
5391
|
});
|
|
5697
5392
|
const resolved = resolveAtFileOutput(captured);
|
|
5698
5393
|
let obj;
|
|
@@ -5724,7 +5419,7 @@ async function main() {
|
|
|
5724
5419
|
// every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
|
|
5725
5420
|
// that breaks on PowerShell and other non-bash shells.
|
|
5726
5421
|
const captured = await captureTranslatingVerificationStatus(async () => {
|
|
5727
|
-
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd);
|
|
5422
|
+
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd, ctx);
|
|
5728
5423
|
});
|
|
5729
5424
|
fs.writeSync(1, resolveAtFileOutput(captured));
|
|
5730
5425
|
}
|
|
@@ -5797,7 +5492,7 @@ function extractField(obj, fieldPath) {
|
|
|
5797
5492
|
return { found: true, value: current };
|
|
5798
5493
|
}
|
|
5799
5494
|
|
|
5800
|
-
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null, invokingCwd) {
|
|
5495
|
+
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null, invokingCwd, ctx) {
|
|
5801
5496
|
switch (command) {
|
|
5802
5497
|
|
|
5803
5498
|
default: {
|
|
@@ -5818,7 +5513,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
5818
5513
|
// commands (state, …) routed via their `route*Command` router instead of
|
|
5819
5514
|
// a hardcoded `case` arm. Tried after capability/overlay dispatch and
|
|
5820
5515
|
// before the unknown-command error.
|
|
5821
|
-
if (await dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd })) break;
|
|
5516
|
+
if (await dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd, ctx })) break;
|
|
5822
5517
|
|
|
5823
5518
|
// #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
|
|
5824
5519
|
// above split it so `command` here is the head ("foo"). Use
|
|
@@ -5851,7 +5546,13 @@ if (require.main === module) {
|
|
|
5851
5546
|
// synthetic registry + requireModule injections.
|
|
5852
5547
|
// ADR-1244 Phase 5: export dispatchOverlayCapabilityCommand + defaultRequireFromInstallRoot for
|
|
5853
5548
|
// the third-party overlay dispatch + install-root confinement tests.
|
|
5549
|
+
/** Top-level host command names (fresh array each call). */
|
|
5550
|
+
function hostCommandNames() {
|
|
5551
|
+
return Object.keys(HOST_COMMAND_ROUTERS);
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5854
5554
|
module.exports = {
|
|
5555
|
+
hostCommandNames,
|
|
5855
5556
|
dispatchCapabilityCommand,
|
|
5856
5557
|
dispatchOverlayCapabilityCommand,
|
|
5857
5558
|
defaultRequireFromInstallRoot,
|
|
@@ -5859,14 +5560,10 @@ module.exports = {
|
|
|
5859
5560
|
HOST_COMMAND_ROUTERS,
|
|
5860
5561
|
TOP_LEVEL_USAGE,
|
|
5861
5562
|
skipsRootResolution,
|
|
5862
|
-
|
|
5563
|
+
warnIfGitTimedOut,
|
|
5863
5564
|
// #3275: exported for tests — the shared PATH+PATHEXT resolver behind
|
|
5864
5565
|
// review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
|
|
5865
5566
|
resolveSpawnBinary,
|
|
5866
|
-
// #4208 review: exported for tests — the two-list commit parser is otherwise
|
|
5867
|
-
// reachable only by spawning the CLI, which a property test cannot afford.
|
|
5868
|
-
collectListFlagValues,
|
|
5869
|
-
COMMIT_LIST_FLAGS,
|
|
5870
5567
|
// #3714 follow-up: exported for tests — the dispatch model-pin VALUE
|
|
5871
5568
|
// policy (charset accept/render parity, max-length boundary, leading-char
|
|
5872
5569
|
// anchor) is otherwise unreachable from outside the dispatchOverlayCapabilityCommand closure.
|