@opengsd/gsd-core 1.20.0 → 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 +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- 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 +99 -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/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- 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-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- 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 +357 -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 +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- 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 +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- 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/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- 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 +4 -3
- 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 +9 -6
- 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 +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- 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 +168 -13
- 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/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- 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 +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/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,22 +2847,62 @@ 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);
|
|
2908
2855
|
}
|
|
2909
2856
|
}
|
|
2910
2857
|
|
|
2858
|
+
/**
|
|
2859
|
+
* #5682: ALSO record a plan-scoped decision at `.gsd/dispatch-isolation/<phase>--<plan>.json`, so a
|
|
2860
|
+
* second orchestrator's write to the single legacy slot cannot erase it. Same payload, same atomic
|
|
2861
|
+
* tmp+rename, same symlink/containment checks as the legacy directory. Best-effort: any failure is
|
|
2862
|
+
* swallowed so the legacy result is unchanged. The key rule lives in hooks/lib/isolation-sentinel.js
|
|
2863
|
+
* (the reader); if that module is unreachable from this install layout, only the legacy file is written.
|
|
2864
|
+
*/
|
|
2865
|
+
function writeKeyedDispatchIsolationRecord(cwd, sentinelDir, payload, clock) {
|
|
2866
|
+
const nodePath = require('path');
|
|
2867
|
+
const nodeFs = require('fs');
|
|
2868
|
+
try {
|
|
2869
|
+
const {
|
|
2870
|
+
keyedSentinelPath, phaseSentinelPath, KEYED_SENTINEL_DIR, AGED_NONE_MAX_MS,
|
|
2871
|
+
} = require('../../hooks/lib/isolation-sentinel.js');
|
|
2872
|
+
// A plan-scoped decision lands at <phase>--<plan>.json; a plan-less one (the phase-level
|
|
2873
|
+
// degrade) at <phase>.json, so concurrent phase-level orchestrators keep separate records.
|
|
2874
|
+
const keyedFile = payload.plan
|
|
2875
|
+
? keyedSentinelPath(cwd, payload.phase, payload.plan)
|
|
2876
|
+
: phaseSentinelPath(cwd, payload.phase);
|
|
2877
|
+
if (!keyedFile) return;
|
|
2878
|
+
const keyedDir = nodePath.join(cwd, KEYED_SENTINEL_DIR);
|
|
2879
|
+
nodeFs.mkdirSync(keyedDir, { recursive: true });
|
|
2880
|
+
if (nodeFs.lstatSync(keyedDir).isSymbolicLink() ||
|
|
2881
|
+
nodePath.relative(nodeFs.realpathSync(sentinelDir), nodeFs.realpathSync(keyedDir)) !== 'dispatch-isolation') {
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
const tmpPath = `${keyedFile}.tmp-${process.pid}-${require('crypto').randomBytes(6).toString('hex')}`;
|
|
2885
|
+
try {
|
|
2886
|
+
nodeFs.writeFileSync(tmpPath, JSON.stringify(payload), { flag: 'wx' });
|
|
2887
|
+
nodeFs.renameSync(tmpPath, keyedFile);
|
|
2888
|
+
} catch (writeError) {
|
|
2889
|
+
try { nodeFs.unlinkSync(tmpPath); } catch (_) { /* best-effort cleanup */ }
|
|
2890
|
+
throw writeError;
|
|
2891
|
+
}
|
|
2892
|
+
// Prune records and orphaned temp files older than the longest any record can still govern
|
|
2893
|
+
// (AGED_NONE_MAX_MS, #5479), measured on the injected clock.
|
|
2894
|
+
const cutoff = clock.now() - AGED_NONE_MAX_MS;
|
|
2895
|
+
const written = nodePath.basename(keyedFile);
|
|
2896
|
+
for (const name of nodeFs.readdirSync(keyedDir)) {
|
|
2897
|
+
if (name === written || !(name.endsWith('.json') || name.includes('.json.tmp-'))) continue;
|
|
2898
|
+
try {
|
|
2899
|
+
const file = nodePath.join(keyedDir, name);
|
|
2900
|
+
if (nodeFs.lstatSync(file).mtimeMs < cutoff) nodeFs.unlinkSync(file);
|
|
2901
|
+
} catch (_) { /* best-effort prune */ }
|
|
2902
|
+
}
|
|
2903
|
+
} catch (_) { /* keyed record is best-effort; the legacy record already landed */ }
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2911
2906
|
/**
|
|
2912
2907
|
* Atomically persist the resolved dispatch-isolation decision to the
|
|
2913
2908
|
* run-scoped sentinel both isolation guard hooks read
|
|
@@ -2918,7 +2913,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2918
2913
|
* per-plan degrade call site and back-compat/tests) share exactly one
|
|
2919
2914
|
* write implementation. Never throws — returns `{ recorded, path, error? }`.
|
|
2920
2915
|
*/
|
|
2921
|
-
function writeDispatchIsolationSentinel(cwd, { isolation, harnessFlag = null, phase = null, plan = null }) {
|
|
2916
|
+
function writeDispatchIsolationSentinel(cwd, { isolation, harnessFlag = null, phase = null, plan = null }, { clock = require('./lib/clock.cjs').realClock } = {}) {
|
|
2922
2917
|
const nodePath = require('path');
|
|
2923
2918
|
const nodeFs = require('fs');
|
|
2924
2919
|
const sentinelDir = nodePath.join(cwd, '.gsd');
|
|
@@ -2928,77 +2923,61 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2928
2923
|
harness_flag: harnessFlag || null,
|
|
2929
2924
|
phase: phase || null,
|
|
2930
2925
|
plan: plan || null,
|
|
2931
|
-
written_at:
|
|
2926
|
+
written_at: clock.now(),
|
|
2932
2927
|
};
|
|
2928
|
+
let tmpPath = null;
|
|
2933
2929
|
try {
|
|
2934
|
-
|
|
2935
|
-
//
|
|
2936
|
-
//
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
throw new Error('sentinel directory is a symlink or resolves outside the project');
|
|
2940
|
-
}
|
|
2941
|
-
// User projects do not inherit gsd-core's root .gitignore. Ignore this
|
|
2942
|
-
// tool-owned directory locally before writing the first sentinel. An
|
|
2943
|
-
// ordinary ignore-file IO failure must not prevent recording isolation.
|
|
2944
|
-
const ignorePath = nodePath.join(sentinelDir, '.gitignore');
|
|
2930
|
+
// #5757: the shared tool-owned .gsd owner refuses a project-supplied
|
|
2931
|
+
// symlink (or ancestor redirect) and seeds/extends .gsd/.gitignore, so
|
|
2932
|
+
// this writer and the fork-probe cache apply one policy. An ordinary
|
|
2933
|
+
// ignore-file IO failure must not prevent recording isolation.
|
|
2934
|
+
const toolDir = require('./lib/gsd-tool-dir.cjs');
|
|
2945
2935
|
try {
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
nodeFs.closeSync(fd);
|
|
2964
|
-
fd = undefined;
|
|
2965
|
-
if (!current.includes('# gsd-core dispatch sentinel') && !/^\*\r?$/m.test(current)) {
|
|
2966
|
-
const additionalRules = `${current.endsWith('\n') ? '' : '\n'}# gsd-core dispatch sentinel\n` +
|
|
2967
|
-
'/dispatch-isolation-sentinel.json\n' +
|
|
2968
|
-
'/dispatch-isolation-sentinel.json.tmp-*\n' +
|
|
2969
|
-
'/.gitignore\n';
|
|
2970
|
-
ignoreTmpPath = `${ignorePath}.tmp-${process.pid}-${require('crypto').randomBytes(6).toString('hex')}`;
|
|
2971
|
-
nodeFs.writeFileSync(ignoreTmpPath, current + additionalRules, { flag: 'wx' });
|
|
2972
|
-
if (nodeFs.lstatSync(ignorePath).isSymbolicLink()) {
|
|
2973
|
-
throw new Error('sentinel ignore file is a symlink');
|
|
2974
|
-
}
|
|
2975
|
-
nodeFs.renameSync(ignoreTmpPath, ignorePath);
|
|
2976
|
-
ignoreTmpPath = null;
|
|
2977
|
-
}
|
|
2978
|
-
} catch (existingIgnoreError) {
|
|
2979
|
-
if (existingIgnoreError.code === 'ELOOP' || existingIgnoreError.message === 'sentinel ignore file is a symlink') {
|
|
2980
|
-
throw new Error('sentinel ignore file is a symlink');
|
|
2981
|
-
}
|
|
2982
|
-
// Ignore seeding is best-effort; the sentinel is still useful if
|
|
2983
|
-
// this path cannot be read or updated.
|
|
2984
|
-
} finally {
|
|
2985
|
-
if (fd !== undefined) nodeFs.closeSync(fd);
|
|
2986
|
-
if (ignoreTmpPath) {
|
|
2987
|
-
try { nodeFs.unlinkSync(ignoreTmpPath); } catch (_) { /* best-effort cleanup */ }
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2936
|
+
toolDir.ensureToolOwnedGsdDir(cwd, {
|
|
2937
|
+
marker: '# gsd-core dispatch sentinel',
|
|
2938
|
+
rules: [
|
|
2939
|
+
'/dispatch-isolation-sentinel.json',
|
|
2940
|
+
'/dispatch-isolation-sentinel.json.tmp-*',
|
|
2941
|
+
'/dispatch-isolation/',
|
|
2942
|
+
'/.gitignore',
|
|
2943
|
+
],
|
|
2944
|
+
});
|
|
2945
|
+
} catch (ownerError) {
|
|
2946
|
+
// The recorded `error` text is observable JSON (hyrums-law): keep the
|
|
2947
|
+
// sentinel's historical wording, translating by the owner's stable code.
|
|
2948
|
+
if (ownerError && ownerError.code === toolDir.GSD_DIR_UNSAFE) {
|
|
2949
|
+
throw new Error('sentinel directory is a symlink or resolves outside the project');
|
|
2950
|
+
}
|
|
2951
|
+
if (ownerError && ownerError.code === toolDir.GSD_IGNORE_SYMLINK) {
|
|
2952
|
+
throw new Error('sentinel ignore file is a symlink');
|
|
2990
2953
|
}
|
|
2954
|
+
throw ownerError;
|
|
2991
2955
|
}
|
|
2992
|
-
// Atomic write:
|
|
2993
|
-
// guard hook firing mid-write) never observes a
|
|
2994
|
-
// sentinel
|
|
2995
|
-
//
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2956
|
+
// Atomic write: exclusive random temp file + rename, so a concurrent
|
|
2957
|
+
// reader (a guard hook firing mid-write) never observes a
|
|
2958
|
+
// partially-written sentinel, concurrent invocations sharing the same
|
|
2959
|
+
// sentinelDir never collide on the temp name, and a planted link at a
|
|
2960
|
+
// guessed temp name is never written through.
|
|
2961
|
+
// `tmpPath` is assigned only once the exclusive create succeeded, so an
|
|
2962
|
+
// EEXIST never unlinks an entry this process did not create.
|
|
2963
|
+
const candidateTmp = `${sentinelPath}.tmp-${process.pid}-${require('crypto').randomBytes(6).toString('hex')}`;
|
|
2964
|
+
const tmpFd = nodeFs.openSync(candidateTmp, 'wx');
|
|
2965
|
+
tmpPath = candidateTmp;
|
|
2966
|
+
try {
|
|
2967
|
+
nodeFs.writeFileSync(tmpFd, JSON.stringify(payload));
|
|
2968
|
+
} finally {
|
|
2969
|
+
nodeFs.closeSync(tmpFd);
|
|
2970
|
+
}
|
|
2971
|
+
require('./lib/shell-command-projection.cjs').retryRenameSync(tmpPath, sentinelPath);
|
|
2972
|
+
tmpPath = null;
|
|
2973
|
+
writeKeyedDispatchIsolationRecord(cwd, sentinelDir, payload, clock);
|
|
2999
2974
|
return { recorded: true, path: '.gsd/dispatch-isolation-sentinel.json' };
|
|
3000
2975
|
} catch (err) {
|
|
3001
2976
|
return { recorded: false, path: '.gsd/dispatch-isolation-sentinel.json', error: err && err.message };
|
|
2977
|
+
} finally {
|
|
2978
|
+
if (tmpPath) {
|
|
2979
|
+
try { nodeFs.unlinkSync(tmpPath); } catch (_) { /* best-effort cleanup */ }
|
|
2980
|
+
}
|
|
3002
2981
|
}
|
|
3003
2982
|
}
|
|
3004
2983
|
|
|
@@ -3020,9 +2999,9 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3020
2999
|
//
|
|
3021
3000
|
// Output: { recorded: true|false, path, error? }
|
|
3022
3001
|
const VALID_ISOLATION = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
|
|
3023
|
-
const
|
|
3024
|
-
const isolation =
|
|
3025
|
-
if (!
|
|
3002
|
+
const parsed = parseNamedArgsOrExit(args, specFor('record-dispatch-isolation'), error);
|
|
3003
|
+
const isolation = parsed.isolation;
|
|
3004
|
+
if (!VALID_ISOLATION.has(isolation)) {
|
|
3026
3005
|
error(
|
|
3027
3006
|
'Usage: record-dispatch-isolation --isolation <harness-worktree|orchestrator-worktree|none> ' +
|
|
3028
3007
|
'[--harness-flag <flag>|--harness-flag=<flag>] [--phase <n>] [--plan <id>]',
|
|
@@ -3045,31 +3024,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3045
3024
|
// unambiguously and is never subject to that guard — any future runtime
|
|
3046
3025
|
// whose registered flag happens to be bare-CLI-shaped benefits the same
|
|
3047
3026
|
// way Cursor's does.
|
|
3048
|
-
|
|
3049
|
-
const
|
|
3050
|
-
|
|
3051
|
-
const value = flagEqArg.slice('--harness-flag='.length);
|
|
3052
|
-
harnessFlag = value.length > 0 ? value : null;
|
|
3053
|
-
} else {
|
|
3054
|
-
const flagIdx = args.indexOf('--harness-flag');
|
|
3055
|
-
harnessFlag = flagIdx !== -1 && args[flagIdx + 1] && !args[flagIdx + 1].startsWith('--')
|
|
3056
|
-
? args[flagIdx + 1]
|
|
3057
|
-
: null;
|
|
3058
|
-
}
|
|
3059
|
-
const phaseIdx = args.indexOf('--phase');
|
|
3060
|
-
const phase = phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--')
|
|
3061
|
-
? args[phaseIdx + 1]
|
|
3062
|
-
: null;
|
|
3063
|
-
const planIdx = args.indexOf('--plan');
|
|
3064
|
-
const plan = planIdx !== -1 && args[planIdx + 1] && !args[planIdx + 1].startsWith('--')
|
|
3065
|
-
? args[planIdx + 1]
|
|
3066
|
-
: null;
|
|
3027
|
+
const harnessFlag = parsed['harness-flag'] || null;
|
|
3028
|
+
const phase = parsed.phase || null;
|
|
3029
|
+
const plan = parsed.plan || null;
|
|
3067
3030
|
|
|
3068
3031
|
const result = writeDispatchIsolationSentinel(cwd, { isolation, harnessFlag, phase, plan });
|
|
3069
3032
|
output(result, raw);
|
|
3070
3033
|
}
|
|
3071
3034
|
|
|
3072
3035
|
function routeResolveDispatchType({ args, cwd, raw, error }) {
|
|
3036
|
+
const parsed = parseNamedArgsOrExit(args, specFor('resolve-dispatch-type'), error);
|
|
3037
|
+
const requested = parsed.requested ?? '';
|
|
3073
3038
|
// #2508 Phase 4 Option A: resolve a requested GSD subagent name to the
|
|
3074
3039
|
// type an Agent() call should use on the current runtime. On
|
|
3075
3040
|
// named-dispatch runtimes (Claude, OpenCode, …) the name is returned
|
|
@@ -3081,8 +3046,6 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3081
3046
|
// --raw (default) → prints the resolved type (e.g. "gsd-planner" or "plan")
|
|
3082
3047
|
// --json → prints { runtime, requested, resolved, dispatch }
|
|
3083
3048
|
try {
|
|
3084
|
-
const requestedIdx = args.indexOf('--requested');
|
|
3085
|
-
const requested = requestedIdx !== -1 ? args[requestedIdx + 1] : '';
|
|
3086
3049
|
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
3087
3050
|
const runtimeId = resolveRuntime(cwd);
|
|
3088
3051
|
const registry = require('./lib/capability-registry.cjs');
|
|
@@ -3092,8 +3055,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3092
3055
|
const dispatch = runtimeEntry?.runtime?.hostIntegration?.dispatch ?? null;
|
|
3093
3056
|
const hostIntegration = require('./lib/host-integration.cjs');
|
|
3094
3057
|
const resolved = hostIntegration.resolveDispatchType(requested, dispatch);
|
|
3095
|
-
|
|
3096
|
-
if (jsonIdx !== -1) {
|
|
3058
|
+
if (parsed.json) {
|
|
3097
3059
|
output({ runtime: runtimeId, requested, resolved, dispatch }, raw);
|
|
3098
3060
|
} else {
|
|
3099
3061
|
process.stdout.write(String(resolved));
|
|
@@ -3102,13 +3064,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3102
3064
|
// Fail-closed: on any error, echo the requested name unchanged
|
|
3103
3065
|
// (named-dispatch is the GSD default; degrading to it preserves
|
|
3104
3066
|
// behavior for every runtime already in the field).
|
|
3105
|
-
const requestedIdx = args.indexOf('--requested');
|
|
3106
|
-
const requested = requestedIdx !== -1 ? args[requestedIdx + 1] : '';
|
|
3107
3067
|
process.stdout.write(String(requested));
|
|
3108
3068
|
}
|
|
3109
3069
|
}
|
|
3110
3070
|
|
|
3111
3071
|
function routeResolveAgent({ args, cwd, raw, error }) {
|
|
3072
|
+
const parsed = parseNamedArgsOrExit(args, specFor('resolve-agent'), error);
|
|
3112
3073
|
// #1689: resolve a per-plan agent_hint specialist name to the subagent_type
|
|
3113
3074
|
// an Agent() call should use. Returns the name unchanged when a
|
|
3114
3075
|
// matching agent file exists in the active runtime's agent dir(s);
|
|
@@ -3121,8 +3082,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3121
3082
|
// --json -> prints { runtime, requested, resolved, fallback }
|
|
3122
3083
|
const FALLBACK = 'gsd-executor';
|
|
3123
3084
|
try {
|
|
3124
|
-
const
|
|
3125
|
-
const requested = nameIdx !== -1 ? args[nameIdx + 1] : '';
|
|
3085
|
+
const requested = parsed.name ?? '';
|
|
3126
3086
|
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
3127
3087
|
const runtimeId = resolveRuntime(cwd);
|
|
3128
3088
|
const { resolveAgentHint } = require('./lib/agent-install-check.cjs');
|
|
@@ -3139,8 +3099,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3139
3099
|
// the named agent did not resolve, or resolution errored). Requesting
|
|
3140
3100
|
// gsd-executor explicitly and resolving to it is NOT a fallback.
|
|
3141
3101
|
const fellBack = !resolvedOk;
|
|
3142
|
-
|
|
3143
|
-
if (jsonIdx !== -1) {
|
|
3102
|
+
if (parsed.json) {
|
|
3144
3103
|
output({ runtime: runtimeId, requested: requested || null, resolved, fallback: fellBack }, raw);
|
|
3145
3104
|
} else {
|
|
3146
3105
|
process.stdout.write(String(resolved));
|
|
@@ -3155,15 +3114,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3155
3114
|
function routeAgentSkills({ args, cwd, raw, error }) {
|
|
3156
3115
|
// --json emits typed IR { agent_type, block, skills_count } for test assertions
|
|
3157
3116
|
// (#455). Default (no flag) outputs raw XML so workflow shell expansions work.
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
const agentSkillsOnly = skillsOnlyIdx !== -1;
|
|
3165
|
-
if (agentSkillsOnly) args.splice(skillsOnlyIdx, 1);
|
|
3166
|
-
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']);
|
|
3167
3123
|
}
|
|
3168
3124
|
|
|
3169
3125
|
function routeSkillManifest({ args, cwd, raw, error }) {
|
|
@@ -3171,6 +3127,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3171
3127
|
}
|
|
3172
3128
|
|
|
3173
3129
|
function routeHistoryDigest({ args, cwd, raw, error }) {
|
|
3130
|
+
parseNamedArgsOrExit(args, specFor('history-digest'), error);
|
|
3174
3131
|
commands.cmdHistoryDigest(cwd, raw);
|
|
3175
3132
|
}
|
|
3176
3133
|
|
|
@@ -3191,6 +3148,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3191
3148
|
// deterministic detectAssumptionDelta, emitting the typed IR as JSON.
|
|
3192
3149
|
const sub = args[1];
|
|
3193
3150
|
if (sub === 'scan') {
|
|
3151
|
+
const parsed = parseNamedArgsOrExit(args, specFor('assumption-delta.scan'), error);
|
|
3194
3152
|
const phaseNum = args[2];
|
|
3195
3153
|
// Reject missing or flag-shaped phase values (QA matrix: values that
|
|
3196
3154
|
// look like flags). `scan --json` must not treat "--json" as a phase.
|
|
@@ -3204,8 +3162,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3204
3162
|
// Terms are normalized (deduped, alphanumeric-only, capped) by
|
|
3205
3163
|
// detectAssumptionDelta's resolveTerms.
|
|
3206
3164
|
let termsOverride;
|
|
3207
|
-
const
|
|
3208
|
-
const termsVal = termsIdx !== -1 ? args[termsIdx + 1] : undefined;
|
|
3165
|
+
const termsVal = parsed.terms;
|
|
3209
3166
|
if (typeof termsVal === 'string' && !termsVal.startsWith('-')) {
|
|
3210
3167
|
const list = termsVal
|
|
3211
3168
|
.split(',')
|
|
@@ -3261,31 +3218,28 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3261
3218
|
function routeMilestone({ args, cwd, raw, error }) {
|
|
3262
3219
|
const subcommand = args[1];
|
|
3263
3220
|
if (subcommand === 'complete') {
|
|
3264
|
-
const
|
|
3221
|
+
const parsed = parseNamedArgsOrExit(args, specFor('milestone.complete'), error);
|
|
3222
|
+
const milestoneName = parsed.name;
|
|
3265
3223
|
// #1871: archive phase dirs by default on milestone complete so the next
|
|
3266
3224
|
// new-milestone never inherits un-archived dirs. --no-archive-phases opts out.
|
|
3267
|
-
const archivePhases = !
|
|
3268
|
-
const force =
|
|
3225
|
+
const archivePhases = !parsed['no-archive-phases'];
|
|
3226
|
+
const force = parsed.force;
|
|
3269
3227
|
// #2118: --dry-run prints a preview plan without mutating.
|
|
3270
|
-
const dryRun =
|
|
3228
|
+
const dryRun = parsed['dry-run'];
|
|
3271
3229
|
// #2142: quick-task archival is opt-in (default OFF) — unlike
|
|
3272
3230
|
// --no-archive-phases' inverted shape, absence of this flag means
|
|
3273
3231
|
// "do nothing" rather than "skip a default-on behavior".
|
|
3274
|
-
const archiveQuick =
|
|
3232
|
+
const archiveQuick = parsed['archive-quick'];
|
|
3275
3233
|
// #3726: explicit mutation opt-in — without --confirm (and without
|
|
3276
3234
|
// --dry-run) the command refuses before touching anything. Distinct
|
|
3277
3235
|
// from --force, which bypasses the narrow scope guards only.
|
|
3278
|
-
const confirm =
|
|
3236
|
+
const confirm = parsed.confirm;
|
|
3279
3237
|
// #5706 (epic #3628): optional caller-supplied release map file.
|
|
3280
|
-
// A flag with no usable value fails loud rather than being dropped
|
|
3281
|
-
|
|
3282
|
-
const
|
|
3283
|
-
if (
|
|
3284
|
-
|
|
3285
|
-
if (typeof releaseMapValue !== 'string' || releaseMapValue.startsWith('--') || releaseMapValue.trim() === '') {
|
|
3286
|
-
error('--release-map requires a file path (a JSON array of release-map entries)');
|
|
3287
|
-
}
|
|
3288
|
-
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)');
|
|
3289
3243
|
}
|
|
3290
3244
|
milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun, archiveQuick, confirm, releaseMap }, raw);
|
|
3291
3245
|
} else if (subcommand === 'archive-quick') {
|
|
@@ -3293,8 +3247,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3293
3247
|
// touch ROADMAP/REQUIREMENTS/MILESTONES.md, runs no completion
|
|
3294
3248
|
// guards) — safe to call against an already-completed milestone,
|
|
3295
3249
|
// unlike `milestone complete --archive-quick`.
|
|
3296
|
-
const
|
|
3297
|
-
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);
|
|
3298
3252
|
} else if (subcommand === 'release-tags') {
|
|
3299
3253
|
// #5705 (epic #3628 Phase 1): repository-scoped annotated release tags.
|
|
3300
3254
|
milestoneReleaseTags.cmdMilestoneReleaseTags(cwd, args, raw);
|
|
@@ -3304,6 +3258,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3304
3258
|
}
|
|
3305
3259
|
|
|
3306
3260
|
function routeProgress({ args, cwd, raw, error }) {
|
|
3261
|
+
parseNamedArgsOrExit(args, specFor('progress.bar'), error);
|
|
3307
3262
|
const subcommand = args[1] || 'json';
|
|
3308
3263
|
commands.cmdProgressRender(cwd, subcommand, raw);
|
|
3309
3264
|
}
|
|
@@ -3312,16 +3267,16 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3312
3267
|
const subcommand = args[1];
|
|
3313
3268
|
if (subcommand === 'render-checkpoint') {
|
|
3314
3269
|
const uat = require('./lib/uat.cjs');
|
|
3315
|
-
const options = parseNamedArgsOrExit(args,
|
|
3270
|
+
const options = parseNamedArgsOrExit(args, specFor('uat.render-checkpoint'), error);
|
|
3316
3271
|
uat.cmdRenderCheckpoint(cwd, options, raw);
|
|
3317
3272
|
} else if (subcommand === 'classify-coverage') {
|
|
3318
3273
|
const coverage = require('./lib/coverage.cjs');
|
|
3319
|
-
const options = parseNamedArgsOrExit(args,
|
|
3274
|
+
const options = parseNamedArgsOrExit(args, specFor('uat.classify-coverage'), error);
|
|
3320
3275
|
coverage.cmdClassify(cwd, options, raw);
|
|
3321
3276
|
} else if (subcommand === 'complete-session') {
|
|
3322
3277
|
const uat = require('./lib/uat.cjs');
|
|
3323
3278
|
const uatPath = args[2];
|
|
3324
|
-
const options = parseNamedArgsOrExit(args,
|
|
3279
|
+
const options = parseNamedArgsOrExit(args, specFor('uat.complete-session'), error);
|
|
3325
3280
|
return uat.cmdUatCompleteSession(cwd, uatPath, { message: options.message }, raw);
|
|
3326
3281
|
} else {
|
|
3327
3282
|
error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage, complete-session', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
@@ -3329,6 +3284,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3329
3284
|
}
|
|
3330
3285
|
|
|
3331
3286
|
function routeStats({ args, cwd, raw, error }) {
|
|
3287
|
+
parseNamedArgsOrExit(args, specFor('stats.json'), error);
|
|
3332
3288
|
const subcommand = args[1] || 'json';
|
|
3333
3289
|
commands.cmdStats(cwd, subcommand, raw);
|
|
3334
3290
|
}
|
|
@@ -3340,17 +3296,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3340
3296
|
// and plumbed (mirroring routeMilestone / #2118), and any OTHER
|
|
3341
3297
|
// flag fails loudly instead of being silently ignored — an
|
|
3342
3298
|
// accepted-but-ignored safety flag converts a deliberate preview
|
|
3343
|
-
// into the mutation it was meant to avoid.
|
|
3344
|
-
//
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
for (const a of args.slice(3)) {
|
|
3348
|
-
if (typeof a === 'string' && a.startsWith('-') && !TODO_COMPLETE_KNOWN_FLAGS.has(a)) {
|
|
3349
|
-
error(`Unknown flag for todo complete: ${a}`, ERROR_REASON.USAGE);
|
|
3350
|
-
}
|
|
3351
|
-
}
|
|
3352
|
-
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);
|
|
3353
3303
|
} else if (subcommand === 'match-phase') {
|
|
3304
|
+
parseNamedArgsOrExit(args, specFor('todo.match-phase'), error);
|
|
3354
3305
|
commands.cmdTodoMatchPhase(cwd, args[2], raw);
|
|
3355
3306
|
} else {
|
|
3356
3307
|
error('Unknown todo subcommand. Available: complete, match-phase', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
@@ -3359,14 +3310,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3359
3310
|
|
|
3360
3311
|
function routeScaffold({ args, cwd, raw, error }) {
|
|
3361
3312
|
const scaffoldType = args[1];
|
|
3362
|
-
// `--name` is multi-word
|
|
3363
|
-
//
|
|
3364
|
-
|
|
3365
|
-
// this call, matching the existing (unchanged) permissive behavior
|
|
3366
|
-
// 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);
|
|
3367
3316
|
const scaffoldOptions = {
|
|
3368
|
-
phase:
|
|
3369
|
-
name:
|
|
3317
|
+
phase: scaffoldParsed.phase,
|
|
3318
|
+
name: scaffoldParsed.name,
|
|
3370
3319
|
};
|
|
3371
3320
|
commands.cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
|
|
3372
3321
|
}
|
|
@@ -3375,74 +3324,20 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3375
3324
|
// loop render-hooks <point>
|
|
3376
3325
|
const loopSubcommand = args[1];
|
|
3377
3326
|
if (loopSubcommand === 'render-hooks') {
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
if (!value || value.startsWith('--')) {
|
|
3388
|
-
error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3389
|
-
}
|
|
3390
|
-
loopConfigDir = value;
|
|
3391
|
-
}
|
|
3392
|
-
// --active-cap <capId>: parse and validate before delegating
|
|
3393
|
-
let loopActiveCap = undefined;
|
|
3394
|
-
const activeCapEqArg = args.find(arg => arg.startsWith('--active-cap='));
|
|
3395
|
-
const activeCapIdx = args.indexOf('--active-cap');
|
|
3396
|
-
if (activeCapEqArg) {
|
|
3397
|
-
const value = activeCapEqArg.slice('--active-cap='.length).trim();
|
|
3398
|
-
if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3399
|
-
loopActiveCap = value;
|
|
3400
|
-
} else if (activeCapIdx !== -1) {
|
|
3401
|
-
const value = args[activeCapIdx + 1];
|
|
3402
|
-
if (!value || value.startsWith('--')) {
|
|
3403
|
-
error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3404
|
-
}
|
|
3405
|
-
loopActiveCap = value;
|
|
3406
|
-
}
|
|
3407
|
-
// --runtime <r> (#2003): explicit runtime override so the config-dir
|
|
3408
|
-
// resolution bypasses the persisted-runtime fallback (GSD_RUNTIME →
|
|
3409
|
-
// config.runtime). Mirrors the --config-dir dual-form (--runtime X /
|
|
3410
|
-
// --runtime=X) and the capability-set --runtime precedent.
|
|
3411
|
-
let loopRuntime = undefined;
|
|
3412
|
-
const runtimeEqArg = args.find(arg => arg.startsWith('--runtime='));
|
|
3413
|
-
const runtimeIdx = args.indexOf('--runtime');
|
|
3414
|
-
if (runtimeEqArg) {
|
|
3415
|
-
const value = runtimeEqArg.slice('--runtime='.length).trim();
|
|
3416
|
-
if (!value) error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3417
|
-
loopRuntime = value;
|
|
3418
|
-
} else if (runtimeIdx !== -1) {
|
|
3419
|
-
const value = args[runtimeIdx + 1];
|
|
3420
|
-
if (!value || value.startsWith('--')) {
|
|
3421
|
-
error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3422
|
-
}
|
|
3423
|
-
loopRuntime = value;
|
|
3424
|
-
}
|
|
3425
|
-
// --after-fingerprint <phaseDir> (#5105 R2): gate out verify:post
|
|
3426
|
-
// steps whose declared artifact already exists in phaseDir.
|
|
3427
|
-
let loopAfterFingerprint = undefined;
|
|
3428
|
-
const afterFpEqArg = args.find(arg => arg.startsWith('--after-fingerprint='));
|
|
3429
|
-
const afterFpIdx = args.indexOf('--after-fingerprint');
|
|
3430
|
-
if (afterFpEqArg) {
|
|
3431
|
-
const value = afterFpEqArg.slice('--after-fingerprint='.length).trim();
|
|
3432
|
-
if (!value) error('Missing value for --after-fingerprint', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3433
|
-
loopAfterFingerprint = value;
|
|
3434
|
-
} else if (afterFpIdx !== -1) {
|
|
3435
|
-
const value = args[afterFpIdx + 1];
|
|
3436
|
-
if (!value || value.startsWith('--')) {
|
|
3437
|
-
error('Missing value for --after-fingerprint', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
3438
|
-
}
|
|
3439
|
-
loopAfterFingerprint = value;
|
|
3440
|
-
}
|
|
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');
|
|
3441
3336
|
loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {
|
|
3442
3337
|
configDir: loopConfigDir ? path.resolve(loopConfigDir) : undefined,
|
|
3443
|
-
activeCap:
|
|
3444
|
-
runtime:
|
|
3445
|
-
afterFingerprint:
|
|
3338
|
+
activeCap: loopValue('active-cap'),
|
|
3339
|
+
runtime: loopValue('runtime'),
|
|
3340
|
+
afterFingerprint: loopValue('after-fingerprint'),
|
|
3446
3341
|
});
|
|
3447
3342
|
} else {
|
|
3448
3343
|
error(
|
|
@@ -3453,38 +3348,41 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3453
3348
|
}
|
|
3454
3349
|
|
|
3455
3350
|
function routePhasePlanIndex({ args, cwd, raw, error }) {
|
|
3351
|
+
parseNamedArgsOrExit(args, specFor('phase-plan-index'), error);
|
|
3456
3352
|
phase.cmdPhasePlanIndex(cwd, args[1], raw);
|
|
3457
3353
|
}
|
|
3458
3354
|
|
|
3459
3355
|
function routeStateSnapshot({ args, cwd, raw, error }) {
|
|
3356
|
+
parseNamedArgsOrExit(args, specFor('state-snapshot'), error);
|
|
3460
3357
|
state.cmdStateSnapshot(cwd, raw);
|
|
3461
3358
|
}
|
|
3462
3359
|
|
|
3463
3360
|
function routeSummaryExtract({ args, cwd, raw, error }) {
|
|
3361
|
+
const parsed = parseNamedArgsOrExit(args, specFor('summary-extract'), error);
|
|
3464
3362
|
const summaryPath = args[1];
|
|
3465
|
-
const
|
|
3466
|
-
const fields = fieldsIndex !== -1 ? args[fieldsIndex + 1].split(',') : null;
|
|
3363
|
+
const fields = parsed.fields !== null ? parsed.fields.split(',') : null;
|
|
3467
3364
|
commands.cmdSummaryExtract(cwd, summaryPath, fields, raw);
|
|
3468
3365
|
}
|
|
3469
3366
|
|
|
3470
3367
|
async function routeWebsearch({ args, cwd, raw, error }) {
|
|
3368
|
+
const parsed = parseNamedArgsOrExit(args, specFor('websearch'), error);
|
|
3471
3369
|
const query = args[1];
|
|
3472
|
-
const limitIdx = args.indexOf('--limit');
|
|
3473
|
-
const freshnessIdx = args.indexOf('--freshness');
|
|
3474
3370
|
await commands.cmdWebsearch(query, {
|
|
3475
|
-
limit:
|
|
3476
|
-
freshness:
|
|
3371
|
+
limit: parsed.limit !== null ? parseInt(parsed.limit, 10) : 10,
|
|
3372
|
+
freshness: parsed.freshness,
|
|
3477
3373
|
}, raw);
|
|
3478
3374
|
}
|
|
3479
3375
|
|
|
3480
3376
|
function routeWorkstream({ args, cwd, raw, error }) {
|
|
3481
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;
|
|
3482
3382
|
if (subcommand === 'create') {
|
|
3483
|
-
const migrateNameIdx = args.indexOf('--migrate-name');
|
|
3484
|
-
const noMigrate = args.includes('--no-migrate');
|
|
3485
3383
|
workstream.cmdWorkstreamCreate(cwd, args[2], {
|
|
3486
|
-
migrate: !
|
|
3487
|
-
migrateName:
|
|
3384
|
+
migrate: !parsed['no-migrate'],
|
|
3385
|
+
migrateName: parsed['migrate-name'],
|
|
3488
3386
|
}, raw);
|
|
3489
3387
|
} else if (subcommand === 'list') {
|
|
3490
3388
|
workstream.cmdWorkstreamList(cwd, raw);
|
|
@@ -3547,20 +3445,18 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3547
3445
|
|
|
3548
3446
|
function routeLearnings({ args, cwd, raw, error }) {
|
|
3549
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;
|
|
3550
3452
|
if (subcommand === 'list') {
|
|
3551
3453
|
learnings.cmdLearningsList(raw);
|
|
3552
3454
|
} else if (subcommand === 'query') {
|
|
3553
|
-
|
|
3554
|
-
const tag = tagIdx !== -1 ? args[tagIdx + 1] : null;
|
|
3555
|
-
if (!tag) error('Usage: gsd-tools learnings query --tag <tag>', ERROR_REASON.USAGE);
|
|
3556
|
-
learnings.cmdLearningsQuery(tag, raw);
|
|
3455
|
+
learnings.cmdLearningsQuery(parsed.tag, raw);
|
|
3557
3456
|
} else if (subcommand === 'copy') {
|
|
3558
3457
|
learnings.cmdLearningsCopy(cwd, raw);
|
|
3559
3458
|
} else if (subcommand === 'prune') {
|
|
3560
|
-
|
|
3561
|
-
const olderThan = olderIdx !== -1 ? args[olderIdx + 1] : null;
|
|
3562
|
-
if (!olderThan) error('Usage: gsd-tools learnings prune --older-than <duration>', ERROR_REASON.USAGE);
|
|
3563
|
-
learnings.cmdLearningsPrune(olderThan, raw);
|
|
3459
|
+
learnings.cmdLearningsPrune(parsed['older-than'], raw);
|
|
3564
3460
|
} else if (subcommand === 'delete') {
|
|
3565
3461
|
const id = args[2];
|
|
3566
3462
|
if (!id) error('Usage: gsd-tools learnings delete <id>', ERROR_REASON.USAGE);
|
|
@@ -3612,8 +3508,9 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3612
3508
|
}
|
|
3613
3509
|
|
|
3614
3510
|
function routeTeamsStatus({ args, cwd, raw, error }) {
|
|
3511
|
+
const parsed = parseNamedArgsOrExit(args, specFor('teams-status'), error);
|
|
3615
3512
|
const teamsStatus = require('./lib/teams-status.cjs');
|
|
3616
|
-
teamsStatus.cmdTeamsStatus(cwd, { active:
|
|
3513
|
+
teamsStatus.cmdTeamsStatus(cwd, { active: parsed.active });
|
|
3617
3514
|
}
|
|
3618
3515
|
|
|
3619
3516
|
// #3023 follow-up (adversarial review finding): the shared hook bundle's
|
|
@@ -3697,11 +3594,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3697
3594
|
}
|
|
3698
3595
|
|
|
3699
3596
|
async function routeDetectCustomFiles({ args, cwd, raw, error }) {
|
|
3700
|
-
|
|
3701
|
-
const configDir =
|
|
3702
|
-
if (!configDir) {
|
|
3703
|
-
error('Usage: gsd-tools detect-custom-files --config-dir <path>', ERROR_REASON.USAGE);
|
|
3704
|
-
}
|
|
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'];
|
|
3705
3599
|
const resolvedConfigDir = path.resolve(configDir);
|
|
3706
3600
|
if (!fs.existsSync(resolvedConfigDir)) {
|
|
3707
3601
|
error(`Config directory not found: ${resolvedConfigDir}`, ERROR_REASON.USAGE);
|
|
@@ -4007,14 +3901,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4007
3901
|
function routeRestoreCustomFiles({ args, error }) {
|
|
4008
3902
|
// Last-wins so a duplicated flag resolves rather than erroring, matching
|
|
4009
3903
|
// the rest of the gsd-tools flag surface.
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
}
|
|
4015
|
-
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;
|
|
4016
3908
|
|
|
4017
|
-
if (configDir
|
|
3909
|
+
if (configDir.trim() === '') {
|
|
4018
3910
|
error('Usage: gsd-tools restore-custom-files --config-dir <path> [--apply]', ERROR_REASON.USAGE);
|
|
4019
3911
|
}
|
|
4020
3912
|
const resolvedConfigDir = path.resolve(configDir);
|
|
@@ -4162,65 +4054,28 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4162
4054
|
async function routePromptBudget({ args, cwd, raw, error }) {
|
|
4163
4055
|
const promptBudget = require('./lib/prompt-budget.cjs');
|
|
4164
4056
|
|
|
4165
|
-
//
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
const
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
if (!next || next.startsWith('--')) {
|
|
4181
|
-
if (!flagMap.has(current)) flagMap.set(current, null);
|
|
4182
|
-
continue;
|
|
4183
|
-
}
|
|
4184
|
-
if (!flagMap.has(current)) flagMap.set(current, next);
|
|
4185
|
-
i++;
|
|
4186
|
-
}
|
|
4187
|
-
const getFlag = (flag) => flagMap.get(flag) ?? null;
|
|
4188
|
-
|
|
4189
|
-
const budgetStr = getFlag('--budget');
|
|
4190
|
-
const instructionsFile = getFlag('--instructions-file');
|
|
4191
|
-
const roadmapFile = getFlag('--roadmap-file');
|
|
4192
|
-
const outputPromptFile = getFlag('--output-prompt');
|
|
4193
|
-
const outputMetadataFile = getFlag('--output-metadata');
|
|
4194
|
-
const safetyMarginStr = getFlag('--safety-margin-pct');
|
|
4195
|
-
const projectMdHeadLinesStr = getFlag('--project-md-head-lines');
|
|
4196
|
-
const projectFile = getFlag('--project-file');
|
|
4197
|
-
const contextFile = getFlag('--context-file');
|
|
4198
|
-
const researchFile = getFlag('--research-file');
|
|
4199
|
-
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'];
|
|
4200
4072
|
|
|
4201
4073
|
// ── Validate required args ─────────────────────────────────────────
|
|
4202
|
-
|
|
4203
|
-
throw new ExitError(1, 'Error: --budget <N> is required');
|
|
4204
|
-
}
|
|
4074
|
+
// Presence of the required flags is enforced by the spec's requiredFlags.
|
|
4205
4075
|
const budget = parseInt(budgetStr, 10);
|
|
4206
4076
|
if (!Number.isFinite(budget) || budget <= 0) {
|
|
4207
4077
|
throw new ExitError(1, 'Error: --budget must be a positive integer');
|
|
4208
4078
|
}
|
|
4209
|
-
if (!instructionsFile) {
|
|
4210
|
-
throw new ExitError(1, 'Error: --instructions-file <path> is required');
|
|
4211
|
-
}
|
|
4212
|
-
if (!roadmapFile) {
|
|
4213
|
-
throw new ExitError(1, 'Error: --roadmap-file <path> is required');
|
|
4214
|
-
}
|
|
4215
|
-
if (planFiles.length === 0) {
|
|
4216
|
-
throw new ExitError(1, 'Error: at least one --plan-file <path> is required');
|
|
4217
|
-
}
|
|
4218
|
-
if (!outputPromptFile) {
|
|
4219
|
-
throw new ExitError(1, 'Error: --output-prompt <path> is required');
|
|
4220
|
-
}
|
|
4221
|
-
if (!outputMetadataFile) {
|
|
4222
|
-
throw new ExitError(1, 'Error: --output-metadata <path> is required');
|
|
4223
|
-
}
|
|
4224
4079
|
|
|
4225
4080
|
// ── Validate and read required files ──────────────────────────────
|
|
4226
4081
|
async function readRequired(filePath, flagName) {
|
|
@@ -4304,7 +4159,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4304
4159
|
// by one or more optional criteria (ANDed together)"); no selector is
|
|
4305
4160
|
// silently dropped or overridden by another.
|
|
4306
4161
|
//
|
|
4307
|
-
//
|
|
4162
|
+
// Flags are read through the CLI surface spec (`context-predicates`): the three
|
|
4308
4163
|
// known flags are recognized in both the space-separated `--flag value`
|
|
4309
4164
|
// form and the inline-assignment `--flag=value` form (the latter is the
|
|
4310
4165
|
// escape hatch for a flag-shaped selector value, e.g. `--contains=--dry-run`
|
|
@@ -4312,61 +4167,26 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4312
4167
|
// design, since a following `--...` token always reads as a missing value).
|
|
4313
4168
|
// `--class=` (empty value) and `--class==A` (double-equals typo shape)
|
|
4314
4169
|
// are rejected the same way under either form. On a duplicate flag the
|
|
4315
|
-
// FIRST occurrence wins
|
|
4316
|
-
//
|
|
4170
|
+
// FIRST occurrence wins, which is deterministic across repeated invocations
|
|
4171
|
+
// with identical argv.
|
|
4317
4172
|
//
|
|
4318
4173
|
// Prototype-pollution safety: selector values are only ever compared via
|
|
4319
4174
|
// `===`/`.startsWith()`/`.includes()` against ordinary string fields — this
|
|
4320
4175
|
// route never uses a user-supplied string as an object property key
|
|
4321
4176
|
// (`obj[userValue] = ...`), so `--class __proto__` / `constructor` /
|
|
4322
4177
|
// `prototype` are just non-matching ordinary strings, not property-access
|
|
4323
|
-
// vectors.
|
|
4324
|
-
//
|
|
4178
|
+
// vectors. The parsed flags are an object keyed only by the spec's own fixed
|
|
4179
|
+
// flag names, never by user input.
|
|
4325
4180
|
function routeContextPredicates({ args, cwd, raw, error }) {
|
|
4326
4181
|
const { parsePredicates, selectPredicates } = require('./lib/context-predicates.cjs');
|
|
4327
4182
|
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
// `--runtime=` convention in routeUpdateContext elsewhere in this file). This is
|
|
4336
|
-
// the ONLY way to pass a flag-shaped selector value (e.g. searching CONTEXT.md
|
|
4337
|
-
// for the literal substring "--dry-run"): the space-separated form below always
|
|
4338
|
-
// treats a following `--...` token as a missing value, by design, so it has no
|
|
4339
|
-
// escape hatch on its own (#2928 review finding C).
|
|
4340
|
-
const eqFlag = [...KNOWN_FLAGS].find((f) => current.startsWith(`${f}=`));
|
|
4341
|
-
if (eqFlag) {
|
|
4342
|
-
const value = current.slice(eqFlag.length + 1);
|
|
4343
|
-
// Reject an empty value (`--class=`) and the `--class==A` double-equals typo
|
|
4344
|
-
// shape (a value starting with `=`) the same way the pre-existing malformed-
|
|
4345
|
-
// assignment behavior did — never silently accept "=A" as a literal value.
|
|
4346
|
-
if (value === '' || value.startsWith('=')) {
|
|
4347
|
-
error(`context-predicates: ${eqFlag} requires a non-empty value`, ERROR_REASON.USAGE);
|
|
4348
|
-
return;
|
|
4349
|
-
}
|
|
4350
|
-
if (!flagMap.has(eqFlag)) flagMap.set(eqFlag, value);
|
|
4351
|
-
continue;
|
|
4352
|
-
}
|
|
4353
|
-
|
|
4354
|
-
if (!KNOWN_FLAGS.has(current)) {
|
|
4355
|
-
error(`Unknown flag for context-predicates: ${current}`, ERROR_REASON.USAGE);
|
|
4356
|
-
return;
|
|
4357
|
-
}
|
|
4358
|
-
const next = args[i + 1];
|
|
4359
|
-
if (next === undefined || next.startsWith('--')) {
|
|
4360
|
-
if (!flagMap.has(current)) flagMap.set(current, null);
|
|
4361
|
-
continue;
|
|
4362
|
-
}
|
|
4363
|
-
if (!flagMap.has(current)) flagMap.set(current, next);
|
|
4364
|
-
i++;
|
|
4365
|
-
}
|
|
4366
|
-
|
|
4367
|
-
const hasClass = flagMap.has('--class');
|
|
4368
|
-
const hasPrefix = flagMap.has('--prefix');
|
|
4369
|
-
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;
|
|
4370
4190
|
|
|
4371
4191
|
if (!hasClass && !hasPrefix && !hasContains) {
|
|
4372
4192
|
error(
|
|
@@ -4377,30 +4197,10 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4377
4197
|
return;
|
|
4378
4198
|
}
|
|
4379
4199
|
|
|
4380
|
-
const requireNonEmpty = (flagName, rawValue) => {
|
|
4381
|
-
if (rawValue === null || rawValue === undefined || rawValue.trim() === '') {
|
|
4382
|
-
error(`context-predicates: ${flagName} requires a non-empty value`, ERROR_REASON.USAGE);
|
|
4383
|
-
return null;
|
|
4384
|
-
}
|
|
4385
|
-
return rawValue;
|
|
4386
|
-
};
|
|
4387
|
-
|
|
4388
4200
|
const opts = {};
|
|
4389
|
-
if (hasClass)
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
opts.klass = v;
|
|
4393
|
-
}
|
|
4394
|
-
if (hasPrefix) {
|
|
4395
|
-
const v = requireNonEmpty('--prefix', flagMap.get('--prefix'));
|
|
4396
|
-
if (v === null) return;
|
|
4397
|
-
opts.prefix = v;
|
|
4398
|
-
}
|
|
4399
|
-
if (hasContains) {
|
|
4400
|
-
const v = requireNonEmpty('--contains', flagMap.get('--contains'));
|
|
4401
|
-
if (v === null) return;
|
|
4402
|
-
opts.contains = v;
|
|
4403
|
-
}
|
|
4201
|
+
if (hasClass) opts.klass = parsed.class;
|
|
4202
|
+
if (hasPrefix) opts.prefix = parsed.prefix;
|
|
4203
|
+
if (hasContains) opts.contains = parsed.contains;
|
|
4404
4204
|
|
|
4405
4205
|
const contextMdPath = path.join(__dirname, '..', '..', 'CONTEXT.md');
|
|
4406
4206
|
let markdown;
|
|
@@ -4424,46 +4224,27 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4424
4224
|
// scope, runtime, gsdDir }. Optional --config-dir / --runtime carry the
|
|
4425
4225
|
// workflow's execution_context hints (the one thing only it can know).
|
|
4426
4226
|
const { loadUpdateContext } = require('./lib/update-context.cjs');
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
if (a.startsWith('--runtime=')) { preferredRuntime = a.slice('--runtime='.length); continue; }
|
|
4434
|
-
if (a === '--config-dir') {
|
|
4435
|
-
const v = ucArgs[i + 1];
|
|
4436
|
-
if (v === undefined || v.startsWith('--')) error('Missing value for --config-dir', ERROR_REASON.USAGE);
|
|
4437
|
-
preferredConfigDir = v; i++; continue;
|
|
4438
|
-
}
|
|
4439
|
-
if (a === '--runtime') {
|
|
4440
|
-
const v = ucArgs[i + 1];
|
|
4441
|
-
if (v === undefined || v.startsWith('--')) error('Missing value for --runtime', ERROR_REASON.USAGE);
|
|
4442
|
-
preferredRuntime = v; i++; continue;
|
|
4443
|
-
}
|
|
4444
|
-
if (a === '--json') continue; // JSON is the only output; accepted for symmetry
|
|
4445
|
-
if (a.startsWith('-')) error(`Unknown flag for update-context: ${a}`, ERROR_REASON.USAGE);
|
|
4446
|
-
}
|
|
4447
|
-
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
|
+
});
|
|
4448
4233
|
process.stdout.write(JSON.stringify(ctx) + '\n');
|
|
4449
4234
|
}
|
|
4450
4235
|
|
|
4451
4236
|
async function routeClassifyConfidence({ args, cwd, raw, error }) {
|
|
4452
4237
|
const researchProvider = require('./lib/research-provider.cjs');
|
|
4453
|
-
|
|
4454
|
-
const
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
const
|
|
4459
|
-
const pkgIdx = args.indexOf('--package');
|
|
4460
|
-
const pkg = pkgIdx !== -1 ? args[pkgIdx + 1] : null;
|
|
4461
|
-
const ecoIdx = args.indexOf('--ecosystem');
|
|
4462
|
-
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;
|
|
4463
4244
|
let legitimacyVerdict = null;
|
|
4464
|
-
if (pkg
|
|
4245
|
+
if (pkg) {
|
|
4465
4246
|
const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
|
|
4466
|
-
if (!ecosystem ||
|
|
4247
|
+
if (!ecosystem || !VALID_ECOSYSTEMS.has(ecosystem)) {
|
|
4467
4248
|
error('Usage: gsd-tools query classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]', ERROR_REASON.USAGE);
|
|
4468
4249
|
}
|
|
4469
4250
|
const pkgLegitimacy = require('./lib/package-legitimacy.cjs');
|
|
@@ -4480,24 +4261,16 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4480
4261
|
if (subcommand !== 'check') {
|
|
4481
4262
|
error('Unknown package-legitimacy subcommand. Available: check', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
4482
4263
|
}
|
|
4483
|
-
|
|
4484
|
-
|
|
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;
|
|
4485
4268
|
const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
|
|
4486
|
-
if (!
|
|
4269
|
+
if (!VALID_ECOSYSTEMS.has(ecosystem)) {
|
|
4487
4270
|
error('Usage: gsd-tools package-legitimacy check --ecosystem <npm|pypi|crates> <pkg1> ...', ERROR_REASON.USAGE);
|
|
4488
4271
|
}
|
|
4489
|
-
//
|
|
4490
|
-
|
|
4491
|
-
// Any unknown --flag is a usage error (do not silently skip+consume the next arg).
|
|
4492
|
-
const packages = [];
|
|
4493
|
-
for (let i = 2; i < args.length; i++) {
|
|
4494
|
-
const a = args[i];
|
|
4495
|
-
if (a === '--ecosystem') { i++; continue; }
|
|
4496
|
-
if (a.startsWith('--')) {
|
|
4497
|
-
error(`package-legitimacy: unknown flag ${a}`, ERROR_REASON.USAGE);
|
|
4498
|
-
}
|
|
4499
|
-
packages.push(a);
|
|
4500
|
-
}
|
|
4272
|
+
// Every non-flag word past `check` that is not the --ecosystem value is a package name.
|
|
4273
|
+
const packages = positionalWords(args, plSpec, 2);
|
|
4501
4274
|
if (packages.length === 0) {
|
|
4502
4275
|
error('Usage: gsd-tools package-legitimacy check --ecosystem <eco> <pkg1> <pkg2> ...', ERROR_REASON.USAGE);
|
|
4503
4276
|
}
|
|
@@ -4513,31 +4286,15 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4513
4286
|
function routeEffort({ args, cwd, raw, error }) {
|
|
4514
4287
|
const subcommand = args[1];
|
|
4515
4288
|
if (subcommand === 'sync') {
|
|
4516
|
-
const
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
if (a === '--config-dir') {
|
|
4526
|
-
const v = effortSyncArgs[i + 1];
|
|
4527
|
-
if (!v || v.startsWith('--')) error('Missing value for --config-dir', ERROR_REASON.USAGE);
|
|
4528
|
-
effortSyncConfigDir = v; i++; continue;
|
|
4529
|
-
}
|
|
4530
|
-
if (a.startsWith('--runtime=')) { effortSyncRuntime = a.slice('--runtime='.length); continue; }
|
|
4531
|
-
if (a === '--runtime') {
|
|
4532
|
-
const v = effortSyncArgs[i + 1];
|
|
4533
|
-
if (!v || v.startsWith('--')) error('Missing value for --runtime', ERROR_REASON.USAGE);
|
|
4534
|
-
effortSyncRuntime = v; i++; continue;
|
|
4535
|
-
}
|
|
4536
|
-
if (a === '--raw') continue;
|
|
4537
|
-
if (a.startsWith('-')) error(`Unknown flag for effort sync: ${a}`, ERROR_REASON.USAGE);
|
|
4538
|
-
error(`effort sync takes no positional arguments; got: ${a}`, ERROR_REASON.USAGE);
|
|
4539
|
-
}
|
|
4540
|
-
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
|
+
});
|
|
4541
4298
|
} else {
|
|
4542
4299
|
error('Unknown effort subcommand. Available: sync', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
4543
4300
|
}
|
|
@@ -4550,10 +4307,9 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4550
4307
|
return;
|
|
4551
4308
|
}
|
|
4552
4309
|
|
|
4553
|
-
const
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
: '';
|
|
4310
|
+
const parsed = parseNamedArgsOrExit(args, specFor('user-story.validate'), error);
|
|
4311
|
+
|
|
4312
|
+
const story = parsed.story || '';
|
|
4557
4313
|
|
|
4558
4314
|
// Canonical extraction regex — requires non-whitespace content in each slot
|
|
4559
4315
|
// (\S.*? ensures the slot isn't whitespace-only).
|
|
@@ -4602,6 +4358,10 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4602
4358
|
// drift-guard authority → effective authority string
|
|
4603
4359
|
// drift-guard severity --status <S> [--authority <A>] → {severity, hardBlock}
|
|
4604
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;
|
|
4605
4365
|
|
|
4606
4366
|
// Read config.json directly for both plan_review.source_grounding_authority
|
|
4607
4367
|
// and intel.enabled. Neither key is in the config-loader.cjs whitelist that
|
|
@@ -4612,8 +4372,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4612
4372
|
try {
|
|
4613
4373
|
const { planningDir } = require('./lib/planning-workspace.cjs');
|
|
4614
4374
|
const cfgPath = require('path').join(planningDir(cwd), 'config.json');
|
|
4615
|
-
|
|
4616
|
-
|
|
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);
|
|
4617
4380
|
if (rawCfg && rawCfg.plan_review && rawCfg.plan_review.source_grounding_authority) {
|
|
4618
4381
|
configuredAuthority = String(rawCfg.plan_review.source_grounding_authority);
|
|
4619
4382
|
}
|
|
@@ -4634,17 +4397,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4634
4397
|
}
|
|
4635
4398
|
|
|
4636
4399
|
if (subcommand === 'severity') {
|
|
4637
|
-
const
|
|
4638
|
-
const
|
|
4639
|
-
if (!statusVal || statusVal.startsWith('--')) {
|
|
4640
|
-
error('drift-guard severity requires --status <VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE>', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
4641
|
-
return;
|
|
4642
|
-
}
|
|
4643
|
-
const authIdx = args.indexOf('--authority');
|
|
4644
|
-
const authVal = authIdx !== -1 ? args[authIdx + 1] : undefined;
|
|
4645
|
-
const authorityForClassify = (authVal && !authVal.startsWith('--'))
|
|
4646
|
-
? authVal
|
|
4647
|
-
: effectiveAuthority;
|
|
4400
|
+
const statusVal = parsed.status;
|
|
4401
|
+
const authorityForClassify = parsed.authority || effectiveAuthority;
|
|
4648
4402
|
const result = classifyDriftSeverity({ status: statusVal, authority: authorityForClassify });
|
|
4649
4403
|
output(result, raw);
|
|
4650
4404
|
return;
|
|
@@ -4652,7 +4406,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4652
4406
|
|
|
4653
4407
|
if (subcommand === 'phase-status') {
|
|
4654
4408
|
// #1956: deterministic STATE.md-vs-ROADMAP.md phase-status drift.
|
|
4655
|
-
const { planningDir } = require('./lib/planning-workspace.cjs');
|
|
4409
|
+
const { planningDir, resolvePhaseIdConvention } = require('./lib/planning-workspace.cjs');
|
|
4656
4410
|
const { stateExtractField, stateCurrentPositionSlice } = require('./lib/state-document.cjs');
|
|
4657
4411
|
const { locateRoadmapProgress } = require('./lib/roadmap-progress.cjs');
|
|
4658
4412
|
const { phaseKeyFromProse } = require('./lib/phase-id.cjs');
|
|
@@ -4664,12 +4418,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4664
4418
|
// is scoped to the body.
|
|
4665
4419
|
const { stripFrontmatter } = require('./lib/frontmatter.cjs');
|
|
4666
4420
|
|
|
4667
|
-
const
|
|
4668
|
-
const phaseArg = (phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--'))
|
|
4669
|
-
? args[phaseIdx + 1]
|
|
4670
|
-
: undefined;
|
|
4421
|
+
const phaseArg = parsed.phase || undefined;
|
|
4671
4422
|
|
|
4672
4423
|
const dir = planningDir(cwd);
|
|
4424
|
+
const phaseConvention = resolvePhaseIdConvention(cwd);
|
|
4425
|
+
const proseKey = (value) => phaseKeyFromProse(value, undefined, phaseConvention);
|
|
4673
4426
|
const statePath = path.join(dir, 'STATE.md');
|
|
4674
4427
|
const roadmapPath = path.join(dir, 'ROADMAP.md');
|
|
4675
4428
|
|
|
@@ -4680,7 +4433,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4680
4433
|
// missing_state below
|
|
4681
4434
|
}
|
|
4682
4435
|
if (stateContent === null) {
|
|
4683
|
-
const phase = phaseArg !== undefined ?
|
|
4436
|
+
const phase = phaseArg !== undefined ? proseKey(phaseArg) : null;
|
|
4684
4437
|
output({
|
|
4685
4438
|
verdict: 'uncheckable',
|
|
4686
4439
|
reason: 'missing_state',
|
|
@@ -4720,7 +4473,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4720
4473
|
// document.
|
|
4721
4474
|
const currentPositionBody = stateCurrentPositionSlice(stateBody);
|
|
4722
4475
|
if (currentPositionBody === null) {
|
|
4723
|
-
const phase = phaseArg !== undefined ?
|
|
4476
|
+
const phase = phaseArg !== undefined ? proseKey(phaseArg) : null;
|
|
4724
4477
|
output({
|
|
4725
4478
|
verdict: 'uncheckable',
|
|
4726
4479
|
reason: 'no_current_position',
|
|
@@ -4735,8 +4488,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4735
4488
|
// Resolve the target phase: --phase if given, else whatever
|
|
4736
4489
|
// STATE.md's Current Position reports as current.
|
|
4737
4490
|
const phase = phaseArg !== undefined
|
|
4738
|
-
?
|
|
4739
|
-
:
|
|
4491
|
+
? proseKey(phaseArg)
|
|
4492
|
+
: proseKey(stateExtractField(currentPositionBody, 'Phase'));
|
|
4740
4493
|
|
|
4741
4494
|
if (roadmapContent === null) {
|
|
4742
4495
|
output({
|
|
@@ -4756,7 +4509,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4756
4509
|
// Progress owner's rule (roadmap-progress.cts), shared with every reader and writer (#5762).
|
|
4757
4510
|
const { table } = locateRoadmapProgress(roadmapContent);
|
|
4758
4511
|
const matchedRow = table
|
|
4759
|
-
? table.rows.find((row) =>
|
|
4512
|
+
? table.rows.find((row) => proseKey(row.Phase) === phase && phase !== null)
|
|
4760
4513
|
: undefined;
|
|
4761
4514
|
|
|
4762
4515
|
if (!matchedRow) {
|
|
@@ -4840,31 +4593,6 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4840
4593
|
* declares that file "All OS-facing I/O; single platform seam", and a private
|
|
4841
4594
|
* duplicate here is what made it untrue.
|
|
4842
4595
|
*/
|
|
4843
|
-
const COMMIT_LIST_FLAGS = new Set(['--files', '--files-removed']);
|
|
4844
|
-
|
|
4845
|
-
// #4208 review: hoisted out of routeCommit's closure so the parser is reachable
|
|
4846
|
-
// from a test. It is the whole of the two-list argument contract, and its edge
|
|
4847
|
-
// cases (a boolean flag inside a run, a repeated list flag, either order) were
|
|
4848
|
-
// already the subject of a review round -- a parser that only the CLI can reach
|
|
4849
|
-
// can only be tested by example, one spawn at a time.
|
|
4850
|
-
//
|
|
4851
|
-
// Every occurrence of `flag` contributes a run; a run ends at the next LIST
|
|
4852
|
-
// flag and skips boolean flags on the way, so no token strictly between one
|
|
4853
|
-
// list flag and the next is ever dropped. Repeated runs of the same flag merge,
|
|
4854
|
-
// as the pre-#4208 slice-to-end parse merged them.
|
|
4855
|
-
function collectListFlagValues(args, flag) {
|
|
4856
|
-
const values = [];
|
|
4857
|
-
args.forEach((a, i) => {
|
|
4858
|
-
if (a !== flag) return;
|
|
4859
|
-
for (const b of args.slice(i + 1)) {
|
|
4860
|
-
if (COMMIT_LIST_FLAGS.has(b)) break;
|
|
4861
|
-
if (b.startsWith('--')) continue;
|
|
4862
|
-
values.push(b);
|
|
4863
|
-
}
|
|
4864
|
-
});
|
|
4865
|
-
return values;
|
|
4866
|
-
}
|
|
4867
|
-
|
|
4868
4596
|
function resolveSpawnBinary(name, platform = process.platform, env = process.env) {
|
|
4869
4597
|
const { resolveExecutableBinary } = require('./lib/shell-command-projection.cjs');
|
|
4870
4598
|
return resolveExecutableBinary(name, { platform, env });
|
|
@@ -4912,12 +4640,12 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
4912
4640
|
// contract and this one mutates; ADR-2346 promotes a leaf to a family only at
|
|
4913
4641
|
// >=3 verbs, and this is one.
|
|
4914
4642
|
'stamp-codebase-map': ({ args, cwd, raw, error }) => {
|
|
4915
|
-
const { files } = parseNamedArgsOrExit(args,
|
|
4916
|
-
//
|
|
4917
|
-
//
|
|
4918
|
-
//
|
|
4919
|
-
const only =
|
|
4920
|
-
?
|
|
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)
|
|
4921
4649
|
: undefined;
|
|
4922
4650
|
verify.cmdStampCodebaseMap(cwd, raw, only);
|
|
4923
4651
|
},
|
|
@@ -4937,6 +4665,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
4937
4665
|
'commit': routeCommit,
|
|
4938
4666
|
'check-commit': routeCheckCommit,
|
|
4939
4667
|
'commit-docs-guard': routeCommitDocsGuard,
|
|
4668
|
+
'project-context': routeProjectContext,
|
|
4940
4669
|
'commit-to-subrepo': routeCommitToSubrepo,
|
|
4941
4670
|
'pr-subrepo': routePrSubrepo,
|
|
4942
4671
|
'verify-summary': routeVerifySummary,
|
|
@@ -5012,7 +4741,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
5012
4741
|
// through. Prototype-pollution-safe: own-property lookup rejects
|
|
5013
4742
|
// `__proto__`/`constructor`/`prototype` command keys (same guard as
|
|
5014
4743
|
// dispatchCapabilityCommand).
|
|
5015
|
-
async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd }) {
|
|
4744
|
+
async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd, ctx }) {
|
|
5016
4745
|
if (
|
|
5017
4746
|
command === '__proto__' ||
|
|
5018
4747
|
command === 'constructor' ||
|
|
@@ -5033,7 +4762,9 @@ async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValu
|
|
|
5033
4762
|
// every other router legitimately wants via `cwd`. Optional — callers/tests
|
|
5034
4763
|
// that omit it (every existing one) leave routers that don't read it
|
|
5035
4764
|
// byte-unaffected.
|
|
5036
|
-
|
|
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 });
|
|
5037
4768
|
return true; // consumed — don't emit "Unknown command"
|
|
5038
4769
|
}
|
|
5039
4770
|
|
|
@@ -5262,7 +4993,7 @@ function runWithTimeout(argv) {
|
|
|
5262
4993
|
// independently hand-maintained sites and nothing previously caught them
|
|
5263
4994
|
// drifting apart when a query command was added to only one or two.
|
|
5264
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' +
|
|
5265
|
-
'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, ' +
|
|
5266
4997
|
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, resolve-package-manager, ' +
|
|
5267
4998
|
'context-predicates, current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
|
5268
4999
|
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
|
@@ -5311,6 +5042,9 @@ const SKIP_ROOT_RESOLUTION = new Set([
|
|
|
5311
5042
|
// explicitly via --config-dir; it never reads .planning/.
|
|
5312
5043
|
'restore-custom-files',
|
|
5313
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',
|
|
5314
5048
|
// context-predicates is a pure repo-root CONTEXT.md read (like
|
|
5315
5049
|
// prompt-budget); it never touches .planning/, so it needs no project
|
|
5316
5050
|
// root resolution and must work from any cwd (including one with no
|
|
@@ -5340,34 +5074,22 @@ function skipsRootResolution(command) {
|
|
|
5340
5074
|
}
|
|
5341
5075
|
|
|
5342
5076
|
/**
|
|
5343
|
-
*
|
|
5344
|
-
*
|
|
5345
|
-
*
|
|
5346
|
-
* 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).
|
|
5347
5080
|
*
|
|
5348
|
-
* @param {string}
|
|
5349
|
-
* @param {
|
|
5350
|
-
* @returns {string} resolved cwd
|
|
5081
|
+
* @param {{ worktreeRoot: string, provenance: { reason: string } }} ctx
|
|
5082
|
+
* @param {(msg: string) => void} [writeWarning]
|
|
5351
5083
|
*/
|
|
5352
|
-
function
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
if (worktreeRootReason === 'git_timed_out') {
|
|
5362
|
-
writeWarning(
|
|
5363
|
-
'WARNING: could not determine the git worktree root (git timed out). ' +
|
|
5364
|
-
'Planning artifacts (STATE.md, ROADMAP.md, etc.) may be written to the ' +
|
|
5365
|
-
`wrong tree — proceeding with "${worktreeRoot}" as a best-effort fallback. ` +
|
|
5366
|
-
'Retry the command; if this persists, check for a stalled filesystem mount ' +
|
|
5367
|
-
'or a stale git index lock (.git/index.lock) in this worktree.\n'
|
|
5368
|
-
);
|
|
5369
|
-
}
|
|
5370
|
-
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
|
+
);
|
|
5371
5093
|
}
|
|
5372
5094
|
|
|
5373
5095
|
// ─── #5118: an out-of-set verification status thrown past a command ─────────
|
|
@@ -5419,6 +5141,7 @@ async function main() {
|
|
|
5419
5141
|
// The argv splice must happen here too, otherwise the dispatcher below sees
|
|
5420
5142
|
// "--json-errors" as an unknown command. Default off — human operators keep
|
|
5421
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)
|
|
5422
5145
|
const jsonErrorsIdx = args.indexOf('--json-errors');
|
|
5423
5146
|
if (jsonErrorsIdx !== -1) {
|
|
5424
5147
|
setJsonErrorMode(true);
|
|
@@ -5442,6 +5165,7 @@ async function main() {
|
|
|
5442
5165
|
// survive into the dispatcher and reproduce the same "Unknown command".
|
|
5443
5166
|
resolveContractVersion({ argv: process.argv, env: process.env });
|
|
5444
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)
|
|
5445
5169
|
if (typeof args[i] === 'string' && args[i].startsWith('--exit-contract=')) {
|
|
5446
5170
|
args.splice(i, 1);
|
|
5447
5171
|
}
|
|
@@ -5464,7 +5188,9 @@ async function main() {
|
|
|
5464
5188
|
|
|
5465
5189
|
// Optional cwd override for sandboxed subagents running outside project root.
|
|
5466
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)
|
|
5467
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)
|
|
5468
5194
|
const cwdIdx = args.indexOf('--cwd');
|
|
5469
5195
|
if (cwdEqArg) {
|
|
5470
5196
|
const value = cwdEqArg.slice('--cwd='.length).trim();
|
|
@@ -5490,7 +5216,9 @@ async function main() {
|
|
|
5490
5216
|
// runs, and its result must skip that call entirely below. A relative
|
|
5491
5217
|
// value resolves against process.cwd(), matching --cwd's own resolution.
|
|
5492
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)
|
|
5493
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)
|
|
5494
5222
|
const projectDirIdx = args.indexOf('--project-dir');
|
|
5495
5223
|
let projectDirValue;
|
|
5496
5224
|
if (projectDirEqArg) {
|
|
@@ -5514,26 +5242,25 @@ async function main() {
|
|
|
5514
5242
|
cwd = resolvedProjectDir;
|
|
5515
5243
|
projectDirExplicit = true;
|
|
5516
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);
|
|
5517
5256
|
// #4894: verification derives its root from a phase directory, not `cwd`, so
|
|
5518
5257
|
// hand it the validated explicit root. Always (re)set — `null` when the flag is
|
|
5519
5258
|
// absent — so a prior in-process main() call can never leak an override.
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
//
|
|
5523
|
-
//
|
|
5524
|
-
|
|
5525
|
-
//
|
|
5526
|
-
// #4906/#4465: this remap is a BLANKET rewrite applied to every command, before
|
|
5527
|
-
// `command` is even known — most commands need it (they read `.planning/`, which
|
|
5528
|
-
// in a linked worktree lives only in the main one). A command whose own git
|
|
5529
|
-
// operation must run against the INVOKING worktree's OWN HEAD/branch (never the
|
|
5530
|
-
// main worktree's) needs the PRE-remap value instead — preserved here so it can be
|
|
5531
|
-
// threaded through dispatchHostCommand as `invokingCwd` (see routeSelectRevertCommits,
|
|
5532
|
-
// the first consumer: its `git log` must see the caller's own reachable history, not
|
|
5533
|
-
// main's, or a linked-worktree revert range silently resolves against the wrong repo
|
|
5534
|
-
// state — reproduced in tests/undo-commit-selection-4465.test.cjs's linked-worktree case).
|
|
5535
|
-
const preWorktreeRemapCwd = cwd;
|
|
5536
|
-
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);
|
|
5537
5264
|
|
|
5538
5265
|
// Optional workstream override for parallel milestone work.
|
|
5539
5266
|
// Priority: --ws flag > GSD_WORKSTREAM env var > session/shared pointer > null.
|
|
@@ -5562,6 +5289,7 @@ async function main() {
|
|
|
5562
5289
|
error(err.message || String(err));
|
|
5563
5290
|
}
|
|
5564
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)
|
|
5565
5293
|
const rawIndex = args.indexOf('--raw');
|
|
5566
5294
|
const raw = rawIndex !== -1;
|
|
5567
5295
|
if (rawIndex !== -1) args.splice(rawIndex, 1);
|
|
@@ -5569,6 +5297,7 @@ async function main() {
|
|
|
5569
5297
|
// --pick <name>: extract a single field from JSON output (replaces jq dependency).
|
|
5570
5298
|
// Supports dot-notation (e.g., --pick workflow.research) and bracket notation
|
|
5571
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)
|
|
5572
5301
|
const pickIdx = args.indexOf('--pick');
|
|
5573
5302
|
let pickField = null;
|
|
5574
5303
|
if (pickIdx !== -1) {
|
|
@@ -5580,6 +5309,7 @@ async function main() {
|
|
|
5580
5309
|
// --default <value>: for config-get, return this value instead of erroring
|
|
5581
5310
|
// when the key is absent. Allows workflows to express optional config reads
|
|
5582
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)
|
|
5583
5313
|
const defaultIdx = args.indexOf('--default');
|
|
5584
5314
|
let defaultValue = undefined;
|
|
5585
5315
|
if (defaultIdx !== -1) {
|
|
@@ -5626,7 +5356,7 @@ async function main() {
|
|
|
5626
5356
|
// discovering the command surface via subcommand help requests routed
|
|
5627
5357
|
// through this dispatcher. Rendering top-level usage on --help is strictly
|
|
5628
5358
|
// better UX than the old short-circuit that printed unrelated usage text.
|
|
5629
|
-
const HELP_FLAGS = new Set(
|
|
5359
|
+
const HELP_FLAGS = new Set(GLOBAL_FLAGS.helpTokens);
|
|
5630
5360
|
if (args.some((a) => HELP_FLAGS.has(a))) {
|
|
5631
5361
|
process.stdout.write(TOP_LEVEL_USAGE + '\n');
|
|
5632
5362
|
return;
|
|
@@ -5635,7 +5365,7 @@ async function main() {
|
|
|
5635
5365
|
// Reject version flags. AI agents sometimes hallucinate --version on tool
|
|
5636
5366
|
// invocations; silently ignoring it can cause destructive operations to
|
|
5637
5367
|
// proceed unchecked. (Help flags are handled above.)
|
|
5638
|
-
const NEVER_VALID_FLAGS = new Set(
|
|
5368
|
+
const NEVER_VALID_FLAGS = new Set(GLOBAL_FLAGS.rejectedTokens);
|
|
5639
5369
|
for (const arg of args) {
|
|
5640
5370
|
if (NEVER_VALID_FLAGS.has(arg)) {
|
|
5641
5371
|
error(`Unknown flag: ${arg}\ngsd-tools does not accept version flags. Run "gsd-tools" with no arguments for usage.`, ERROR_REASON.USAGE);
|
|
@@ -5643,10 +5373,9 @@ async function main() {
|
|
|
5643
5373
|
}
|
|
5644
5374
|
|
|
5645
5375
|
// #3881: an explicit --project-dir already IS the resolved project root
|
|
5646
|
-
// (validated above)
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
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;
|
|
5650
5379
|
}
|
|
5651
5380
|
|
|
5652
5381
|
// When --pick is active, capture stdout and extract the requested field.
|
|
@@ -5658,7 +5387,7 @@ async function main() {
|
|
|
5658
5387
|
// false "output was not JSON" (negative space N8).
|
|
5659
5388
|
if (pickField) {
|
|
5660
5389
|
const captured = await captureTranslatingVerificationStatus(async () => {
|
|
5661
|
-
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd);
|
|
5390
|
+
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd, ctx);
|
|
5662
5391
|
});
|
|
5663
5392
|
const resolved = resolveAtFileOutput(captured);
|
|
5664
5393
|
let obj;
|
|
@@ -5690,7 +5419,7 @@ async function main() {
|
|
|
5690
5419
|
// every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
|
|
5691
5420
|
// that breaks on PowerShell and other non-bash shells.
|
|
5692
5421
|
const captured = await captureTranslatingVerificationStatus(async () => {
|
|
5693
|
-
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd);
|
|
5422
|
+
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd, ctx);
|
|
5694
5423
|
});
|
|
5695
5424
|
fs.writeSync(1, resolveAtFileOutput(captured));
|
|
5696
5425
|
}
|
|
@@ -5763,7 +5492,7 @@ function extractField(obj, fieldPath) {
|
|
|
5763
5492
|
return { found: true, value: current };
|
|
5764
5493
|
}
|
|
5765
5494
|
|
|
5766
|
-
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) {
|
|
5767
5496
|
switch (command) {
|
|
5768
5497
|
|
|
5769
5498
|
default: {
|
|
@@ -5784,7 +5513,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
5784
5513
|
// commands (state, …) routed via their `route*Command` router instead of
|
|
5785
5514
|
// a hardcoded `case` arm. Tried after capability/overlay dispatch and
|
|
5786
5515
|
// before the unknown-command error.
|
|
5787
|
-
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;
|
|
5788
5517
|
|
|
5789
5518
|
// #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
|
|
5790
5519
|
// above split it so `command` here is the head ("foo"). Use
|
|
@@ -5817,7 +5546,13 @@ if (require.main === module) {
|
|
|
5817
5546
|
// synthetic registry + requireModule injections.
|
|
5818
5547
|
// ADR-1244 Phase 5: export dispatchOverlayCapabilityCommand + defaultRequireFromInstallRoot for
|
|
5819
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
|
+
|
|
5820
5554
|
module.exports = {
|
|
5555
|
+
hostCommandNames,
|
|
5821
5556
|
dispatchCapabilityCommand,
|
|
5822
5557
|
dispatchOverlayCapabilityCommand,
|
|
5823
5558
|
defaultRequireFromInstallRoot,
|
|
@@ -5825,14 +5560,10 @@ module.exports = {
|
|
|
5825
5560
|
HOST_COMMAND_ROUTERS,
|
|
5826
5561
|
TOP_LEVEL_USAGE,
|
|
5827
5562
|
skipsRootResolution,
|
|
5828
|
-
|
|
5563
|
+
warnIfGitTimedOut,
|
|
5829
5564
|
// #3275: exported for tests — the shared PATH+PATHEXT resolver behind
|
|
5830
5565
|
// review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
|
|
5831
5566
|
resolveSpawnBinary,
|
|
5832
|
-
// #4208 review: exported for tests — the two-list commit parser is otherwise
|
|
5833
|
-
// reachable only by spawning the CLI, which a property test cannot afford.
|
|
5834
|
-
collectListFlagValues,
|
|
5835
|
-
COMMIT_LIST_FLAGS,
|
|
5836
5567
|
// #3714 follow-up: exported for tests — the dispatch model-pin VALUE
|
|
5837
5568
|
// policy (charset accept/render parity, max-length boundary, leading-char
|
|
5838
5569
|
// anchor) is otherwise unreachable from outside the dispatchOverlayCapabilityCommand closure.
|