@opengsd/gsd-core 1.7.0-rc.6 → 1.8.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 +14 -0
- package/README.md +2 -0
- package/agents/gsd-debug-session-manager.md +42 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +31 -3
- package/agents/gsd-planner.md +29 -36
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1157 -84
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +31 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +3 -2
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +1866 -2434
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +341 -49
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-registry.cjs +157 -88
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +129 -26
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
- package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +129 -13
- package/gsd-core/bin/lib/config-loader.cjs +20 -4
- package/gsd-core/bin/lib/config.cjs +81 -18
- package/gsd-core/bin/lib/core-utils.cjs +14 -3
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/init.cjs +138 -60
- package/gsd-core/bin/lib/install-engine.cjs +301 -25
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +45 -6
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +449 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/milestone.cjs +463 -43
- package/gsd-core/bin/lib/model-catalog.cjs +19 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-id.cjs +26 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +636 -72
- package/gsd-core/bin/lib/plan-scan.cjs +73 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +225 -17
- package/gsd-core/bin/lib/roadmap.cjs +113 -52
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +14 -7
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +41 -17
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +1 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
- package/gsd-core/bin/lib/smart-entry.cjs +73 -7
- package/gsd-core/bin/lib/state-document.cjs +7 -4
- package/gsd-core/bin/lib/state-transition.cjs +122 -46
- package/gsd-core/bin/lib/state.cjs +456 -137
- package/gsd-core/bin/lib/surface.cjs +53 -11
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +474 -13
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/validate.cjs +12 -8
- package/gsd-core/bin/lib/verification.cjs +112 -17
- package/gsd-core/bin/lib/verify.cjs +224 -25
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -2
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +13 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/reviewer-instances.md +9 -7
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +4 -2
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -2
- package/gsd-core/workflows/audit-fix.md +2 -2
- package/gsd-core/workflows/check-todos.md +3 -1
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review.md +17 -5
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +27 -5
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +1 -0
- package/gsd-core/workflows/eval-review.md +3 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +30 -37
- package/gsd-core/workflows/execute-plan.md +15 -4
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +6 -2
- package/gsd-core/workflows/import.md +8 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +15 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +69 -21
- package/gsd-core/workflows/new-project.md +17 -15
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +14 -5
- package/gsd-core/workflows/plan-review-convergence.md +48 -3
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +33 -5
- package/gsd-core/workflows/quick.md +21 -7
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +123 -68
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +4 -1
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +58 -5
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +3 -0
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +3 -1
- package/gsd-core/workflows/ui-review.md +3 -0
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +2 -0
- package/gsd-core/workflows/validate-phase.md +3 -0
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +7 -3
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-statusline.js +252 -17
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-statusline.js +252 -17
- package/package.json +8 -4
- package/pi/gsd.cjs +8 -2
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-glossary-refs.cjs +220 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-adr-index.cjs +526 -0
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +31 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +3 -2
- package/skills/gsd-surface/SKILL.md +6 -6
- package/vscode/package.json +1 -1
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
* list-todos [area] Count and enumerate pending todos
|
|
28
28
|
* list-seeds [status] List captured seeds (optional status filter)
|
|
29
29
|
* verify-path-exists <path> Check file/directory existence
|
|
30
|
+
* quick-tasks-append --task <text> Append a row to STATE.md's "Quick Tasks
|
|
31
|
+
* Completed" table (schema-backed via
|
|
32
|
+
* markdown-table.cjs; #2133/ADR-2143).
|
|
33
|
+
* Fails loud (non-zero exit) on a missing
|
|
34
|
+
* or unrecognized table instead of the old
|
|
35
|
+
* awk NF-2 silent-skip guess.
|
|
30
36
|
* config-ensure-section Initialize .planning/config.json
|
|
31
37
|
* history-digest Aggregate all SUMMARY.md data
|
|
32
38
|
* summary-extract <path> [--fields] Extract structured data from SUMMARY.md
|
|
@@ -53,6 +59,10 @@
|
|
|
53
59
|
* Requirements Operations:
|
|
54
60
|
* requirements mark-complete <ids> Mark requirement IDs as complete in REQUIREMENTS.md
|
|
55
61
|
* Accepts: REQ-01,REQ-02 or REQ-01 REQ-02 or [REQ-01, REQ-02]
|
|
62
|
+
* requirements ready-ids <plan-path> <ids> Read-only: which of <ids> are safe to mark-complete now
|
|
63
|
+
* (no sibling *-PLAN.md in the same phase dir still missing its SUMMARY for that ID)
|
|
64
|
+
* requirements revert-phase <ids> Revert requirement IDs out of Complete (checkbox + traceability row);
|
|
65
|
+
* gaps_found-only, never call on the pass path
|
|
56
66
|
*
|
|
57
67
|
* Milestone Operations:
|
|
58
68
|
* milestone complete <version> Archive milestone, create MILESTONES.md
|
|
@@ -279,13 +289,13 @@ const { routeInitCommand } = require('./lib/init-command-router.cjs');
|
|
|
279
289
|
// here, invoked from case 'init' below.
|
|
280
290
|
const { warnIfStaleBake } = require('./lib/stale-bake-guard.cjs');
|
|
281
291
|
const loopResolver = require('./lib/loop-resolver.cjs');
|
|
282
|
-
const
|
|
283
|
-
const capabilityWriter = require('./lib/capability-writer.cjs');
|
|
292
|
+
const brokenWindows = require('./lib/broken-windows.cjs');
|
|
284
293
|
const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
|
|
285
294
|
const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
|
|
286
295
|
const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
|
|
287
296
|
const { routeRoadmapCommand } = require('./lib/roadmap-command-router.cjs');
|
|
288
|
-
const {
|
|
297
|
+
const { routeCapabilityCommand } = require('./lib/capability-command-router.cjs');
|
|
298
|
+
const { routeAgentCommand, AGENT_FAILURE_CLASSES } = require('./lib/agent-command-router.cjs');
|
|
289
299
|
const smartEntryMod = require('./lib/smart-entry.cjs');
|
|
290
300
|
const { routeCheckCommand } = require('./lib/check-command-router.cjs');
|
|
291
301
|
const { routeTaskCommand } = require('./lib/task-command-router.cjs');
|
|
@@ -556,2606 +566,2021 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
556
566
|
return true;
|
|
557
567
|
}
|
|
558
568
|
|
|
559
|
-
// ───
|
|
560
|
-
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
} else if (process.env.GSD_JSON_ERRORS === '1') {
|
|
581
|
-
setJsonErrorMode(true);
|
|
569
|
+
// ─── ADR-2346 (epic #2345): host dispatch table ───────────────────────────────
|
|
570
|
+
// Layer-2 of the two-layer dispatch. Core, non-capability host commands live
|
|
571
|
+
// here — NOT in the capability registry (ADR-959's commandFamilies is reserved
|
|
572
|
+
// for toggleable feature capabilities: graphify/audit/intel). A host command
|
|
573
|
+
// like `state` is core, non-toggleable, carries no tier/activationKey, so it
|
|
574
|
+
// cannot be a capability. Each entry maps a top-level command to its standard
|
|
575
|
+
// `route*Command` router (the same routers the hardcoded `case` arms called).
|
|
576
|
+
// Consulted in runCommand's `default` case, after capability + overlay
|
|
577
|
+
// dispatch, before the unknown-command error. A migrated command's `case` arm
|
|
578
|
+
// is removed at cutover so it reaches here; an unmigrated command still hits
|
|
579
|
+
// its `case` (collision structurally impossible, same property as ADR-959).
|
|
580
|
+
|
|
581
|
+
// ─── ADR-2346 P3: resolve/git/config/research host routers ────────────────
|
|
582
|
+
// Each body was relocated VERBATIM from its `case` arm (cutover: the arm is
|
|
583
|
+
// removed so dispatch reaches HOST_COMMAND_ROUTERS). Closures over module-
|
|
584
|
+
// scope libs (commands/config/output/error/_dispatchNonFamily) are preserved;
|
|
585
|
+
// only per-dispatch values (args/cwd/raw/defaultValue/workstreamContext)
|
|
586
|
+
// arrive via the destructured context.
|
|
587
|
+
|
|
588
|
+
function routeResolveModel({ args, cwd, raw }) {
|
|
589
|
+
commands.cmdResolveModel(cwd, args[1], raw);
|
|
582
590
|
}
|
|
583
591
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
cwd = path.resolve(value);
|
|
592
|
+
function routeResolveGranularity({ args, cwd, raw }) {
|
|
593
|
+
const granArgs = args.slice(1);
|
|
594
|
+
let granOverride;
|
|
595
|
+
const granPositionals = [];
|
|
596
|
+
for (let i = 0; i < granArgs.length; i++) {
|
|
597
|
+
const a = granArgs[i];
|
|
598
|
+
if (a === '--granularity' && granArgs[i + 1] !== undefined && !granArgs[i + 1].startsWith('--')) {
|
|
599
|
+
if (granOverride === undefined) { granOverride = granArgs[++i]; } else { ++i; }
|
|
600
|
+
} else {
|
|
601
|
+
granPositionals.push(a);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
commands.cmdResolveGranularity(cwd, granPositionals[0], raw, granOverride);
|
|
598
605
|
}
|
|
599
606
|
|
|
600
|
-
|
|
601
|
-
|
|
607
|
+
function routeResolveExecution({ args, cwd, raw }) {
|
|
608
|
+
const execArgs = args.slice(1);
|
|
609
|
+
let effortOverride;
|
|
610
|
+
let fastModeOverride;
|
|
611
|
+
let attempt;
|
|
612
|
+
let failureClass;
|
|
613
|
+
const positionals = [];
|
|
614
|
+
// #2296: the valid classes come from the classifier's own frozen enum, so
|
|
615
|
+
// this validator can never drift from what `agent classify-failure` emits.
|
|
616
|
+
const validFailureClasses = Object.values(AGENT_FAILURE_CLASSES);
|
|
617
|
+
const setFailureClass = (v) => {
|
|
618
|
+
if (!validFailureClasses.includes(v)) {
|
|
619
|
+
error(
|
|
620
|
+
`--failure-class must be one of: ${validFailureClasses.join(', ')}`,
|
|
621
|
+
ERROR_REASON.USAGE,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
failureClass = v;
|
|
625
|
+
};
|
|
626
|
+
for (let i = 0; i < execArgs.length; i++) {
|
|
627
|
+
const a = execArgs[i];
|
|
628
|
+
if (a.startsWith('--effort=')) {
|
|
629
|
+
effortOverride = a.slice('--effort='.length);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (a.startsWith('--fast-mode=')) {
|
|
633
|
+
const v = a.slice('--fast-mode='.length);
|
|
634
|
+
fastModeOverride = v === 'true' ? true : v === 'false' ? false : undefined;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (a.startsWith('--attempt=')) {
|
|
638
|
+
const v = a.slice('--attempt='.length);
|
|
639
|
+
const n = parseInt(v, 10);
|
|
640
|
+
if (!Number.isInteger(n) || n < 0) error('--attempt requires a non-negative integer', ERROR_REASON.USAGE);
|
|
641
|
+
attempt = n;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (a.startsWith('--failure-class=')) {
|
|
645
|
+
setFailureClass(a.slice('--failure-class='.length));
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (a === '--effort') {
|
|
649
|
+
const val = execArgs[i + 1];
|
|
650
|
+
if (val === undefined || val.startsWith('--')) error('Missing value for --effort', ERROR_REASON.USAGE);
|
|
651
|
+
effortOverride = val;
|
|
652
|
+
i++;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (a === '--fast-mode') {
|
|
656
|
+
const val = execArgs[i + 1];
|
|
657
|
+
if (val === undefined || val.startsWith('--')) error('Missing value for --fast-mode', ERROR_REASON.USAGE);
|
|
658
|
+
fastModeOverride = val === 'true' ? true : val === 'false' ? false : undefined;
|
|
659
|
+
i++;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (a === '--attempt') {
|
|
663
|
+
const val = execArgs[i + 1];
|
|
664
|
+
if (val === undefined || val.startsWith('--')) error('Missing value for --attempt', ERROR_REASON.USAGE);
|
|
665
|
+
const n = parseInt(val, 10);
|
|
666
|
+
if (!Number.isInteger(n) || n < 0) error('--attempt requires a non-negative integer', ERROR_REASON.USAGE);
|
|
667
|
+
attempt = n;
|
|
668
|
+
i++;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (a === '--failure-class') {
|
|
672
|
+
const val = execArgs[i + 1];
|
|
673
|
+
if (val === undefined || val.startsWith('--')) error('Missing value for --failure-class', ERROR_REASON.USAGE);
|
|
674
|
+
setFailureClass(val);
|
|
675
|
+
i++;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (a === '--raw') continue;
|
|
679
|
+
if (a.startsWith('-')) error(`Unknown flag for resolve-execution: ${a}`, ERROR_REASON.USAGE);
|
|
680
|
+
positionals.push(a);
|
|
681
|
+
}
|
|
682
|
+
if (positionals.length === 0) error('agent-type required', ERROR_REASON.USAGE);
|
|
683
|
+
if (positionals.length > 1) error(`resolve-execution requires exactly one agent-type argument; got: ${positionals.join(', ')}`, ERROR_REASON.USAGE);
|
|
684
|
+
const agentTypeArg = positionals[0];
|
|
685
|
+
commands.cmdResolveExecution(cwd, agentTypeArg, raw, {
|
|
686
|
+
effortOverride,
|
|
687
|
+
fastModeOverride,
|
|
688
|
+
attempt,
|
|
689
|
+
failureClass,
|
|
690
|
+
});
|
|
602
691
|
}
|
|
603
692
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
693
|
+
function routeGit({ args, cwd }) {
|
|
694
|
+
const subcommand = args[1];
|
|
695
|
+
if (subcommand !== 'base-branch') {
|
|
696
|
+
error(
|
|
697
|
+
`Unknown git subcommand: ${subcommand || '(none)'}. Available: base-branch`,
|
|
698
|
+
ERROR_REASON.SDK_UNKNOWN_COMMAND,
|
|
699
|
+
);
|
|
700
|
+
return;
|
|
612
701
|
}
|
|
702
|
+
cmdGitBaseBranch(cwd, args.slice(2));
|
|
613
703
|
}
|
|
614
704
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
705
|
+
function routeConfigEnsureSection({ args, cwd, raw }) {
|
|
706
|
+
const handled = _dispatchNonFamily({
|
|
707
|
+
registryCommand: 'config-ensure-section',
|
|
708
|
+
registryArgs: args.slice(1),
|
|
709
|
+
legacyCommand: 'config-ensure-section',
|
|
710
|
+
legacyArgs: args.slice(1),
|
|
711
|
+
cwd,
|
|
712
|
+
raw,
|
|
713
|
+
error,
|
|
714
|
+
output: output,
|
|
621
715
|
});
|
|
622
|
-
|
|
623
|
-
// Set env var so all modules (planningDir, planningPaths) auto-resolve workstream paths.
|
|
624
|
-
applyResolvedWorkstreamEnv(workstreamContext, process.env);
|
|
625
|
-
} catch (err) {
|
|
626
|
-
error(err.message || String(err));
|
|
716
|
+
if (!handled) config.cmdConfigEnsureSection(cwd, raw);
|
|
627
717
|
}
|
|
628
718
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (!
|
|
641
|
-
args.splice(pickIdx, 2);
|
|
719
|
+
function routeConfigSet({ args, cwd, raw }) {
|
|
720
|
+
const handled = _dispatchNonFamily({
|
|
721
|
+
registryCommand: 'config-set',
|
|
722
|
+
registryArgs: args.slice(1),
|
|
723
|
+
legacyCommand: 'config-set',
|
|
724
|
+
legacyArgs: args.slice(1),
|
|
725
|
+
cwd,
|
|
726
|
+
raw,
|
|
727
|
+
error,
|
|
728
|
+
output: output,
|
|
729
|
+
});
|
|
730
|
+
if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw);
|
|
642
731
|
}
|
|
643
732
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
733
|
+
function routeConfigSetModelProfile({ args, cwd, raw }) {
|
|
734
|
+
const handled = _dispatchNonFamily({
|
|
735
|
+
registryCommand: 'config-set-model-profile',
|
|
736
|
+
registryArgs: args.slice(1),
|
|
737
|
+
legacyCommand: 'config-set-model-profile',
|
|
738
|
+
legacyArgs: args.slice(1),
|
|
739
|
+
cwd,
|
|
740
|
+
raw,
|
|
741
|
+
error,
|
|
742
|
+
output: output,
|
|
743
|
+
});
|
|
744
|
+
if (!handled) config.cmdConfigSetModelProfile(cwd, args[1], raw);
|
|
653
745
|
}
|
|
654
746
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
747
|
+
function routeConfigGet({ args, cwd, raw, defaultValue }) {
|
|
748
|
+
const configGetSdkArgs = defaultValue !== undefined
|
|
749
|
+
? [args[1], '--default', defaultValue]
|
|
750
|
+
: args.slice(1);
|
|
751
|
+
const handled = _dispatchNonFamily({
|
|
752
|
+
registryCommand: 'config-get',
|
|
753
|
+
registryArgs: configGetSdkArgs,
|
|
754
|
+
legacyCommand: 'config-get',
|
|
755
|
+
legacyArgs: args.slice(1),
|
|
756
|
+
cwd,
|
|
757
|
+
raw,
|
|
758
|
+
error,
|
|
759
|
+
output: output,
|
|
760
|
+
});
|
|
761
|
+
if (!handled) config.cmdConfigGet(cwd, args[1], raw, defaultValue);
|
|
662
762
|
}
|
|
663
763
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const rest = command.slice(dotIdx + 1);
|
|
677
|
-
if (head && rest) {
|
|
678
|
-
command = head;
|
|
679
|
-
args = [head, rest, ...args.slice(1)];
|
|
680
|
-
}
|
|
764
|
+
function routeConfigNewProject({ args, cwd, raw }) {
|
|
765
|
+
const handled = _dispatchNonFamily({
|
|
766
|
+
registryCommand: 'config-new-project',
|
|
767
|
+
registryArgs: args.slice(1),
|
|
768
|
+
legacyCommand: 'config-new-project',
|
|
769
|
+
legacyArgs: args.slice(1),
|
|
770
|
+
cwd,
|
|
771
|
+
raw,
|
|
772
|
+
error,
|
|
773
|
+
output: output,
|
|
774
|
+
});
|
|
775
|
+
if (!handled) config.cmdConfigNewProject(cwd, args[1], raw);
|
|
681
776
|
}
|
|
682
777
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
// CR feedback: the command list must enumerate every top-level command
|
|
686
|
-
// supported by the dispatcher so `--help` is actually useful for
|
|
687
|
-
// discovery; previously it was a partial subset that didn't include
|
|
688
|
-
// phase / roadmap / milestone / progress / etc.
|
|
689
|
-
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
|
|
690
|
-
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, pr-subrepo, ' +
|
|
691
|
-
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, ' +
|
|
692
|
-
'current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
|
693
|
-
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
|
694
|
-
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
|
695
|
-
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
|
696
|
-
'profile-sample, progress, project-instruction-file, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, smart-entry, state, ' +
|
|
697
|
-
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
|
698
|
-
'Global flags:\n' +
|
|
699
|
-
' --raw Emit raw output without post-processing\n' +
|
|
700
|
-
' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
|
|
701
|
-
' --cwd <path> Override working directory for project-root resolution\n' +
|
|
702
|
-
' --ws <name> Override active workstream (or set GSD_WORKSTREAM)\n' +
|
|
703
|
-
' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n\n' +
|
|
704
|
-
'For command-specific argument requirements, invoke the command without args ' +
|
|
705
|
-
'(e.g. `gsd-tools phase add`) — the resulting error lists what is required.';
|
|
706
|
-
|
|
707
|
-
if (!command) {
|
|
708
|
-
error(TOP_LEVEL_USAGE);
|
|
778
|
+
function routeConfigPath({ cwd, raw, workstreamContext }) {
|
|
779
|
+
config.cmdConfigPath(cwd, raw, workstreamContext);
|
|
709
780
|
}
|
|
710
781
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
// erred on agent-hallucinated flags, but it also blocked humans from
|
|
714
|
-
// discovering the command surface via subcommand help requests routed
|
|
715
|
-
// through this dispatcher. Rendering top-level usage on --help is strictly
|
|
716
|
-
// better UX than the old short-circuit that printed unrelated usage text.
|
|
717
|
-
const HELP_FLAGS = new Set(['-h', '--help', '-?', '--h', '--usage']);
|
|
718
|
-
if (args.some((a) => HELP_FLAGS.has(a))) {
|
|
719
|
-
process.stdout.write(TOP_LEVEL_USAGE + '\n');
|
|
720
|
-
return;
|
|
782
|
+
async function routeMigrateConfig({ cwd, raw }) {
|
|
783
|
+
await config.cmdMigrateConfig(cwd, raw);
|
|
721
784
|
}
|
|
722
785
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
786
|
+
function routeResearchStore({ args, cwd, raw }) {
|
|
787
|
+
const researchStore = require('./lib/research-store.cjs');
|
|
788
|
+
const subcommand = args[1];
|
|
789
|
+
const homeDir = process.env.HOME || require('os').homedir();
|
|
790
|
+
if (subcommand === 'get') {
|
|
791
|
+
const key = args[2];
|
|
792
|
+
if (!key || key.startsWith('--')) {
|
|
793
|
+
error('Usage: gsd-tools research-store get <key> [--kind <k>]', ERROR_REASON.USAGE);
|
|
794
|
+
}
|
|
795
|
+
if (!researchStore.isValidResearchKey(key)) {
|
|
796
|
+
error('research-store: <key> must be a 64-char sha256 hex (use research-plan to obtain keys)', ERROR_REASON.USAGE);
|
|
797
|
+
}
|
|
798
|
+
const result = researchStore.getResearch(cwd, key, { homeDir });
|
|
799
|
+
output(result, raw);
|
|
800
|
+
} else if (subcommand === 'put') {
|
|
801
|
+
const key = args[2];
|
|
802
|
+
if (!key || key.startsWith('--')) {
|
|
803
|
+
error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
|
|
804
|
+
}
|
|
805
|
+
if (!researchStore.isValidResearchKey(key)) {
|
|
806
|
+
error('research-store: <key> must be a 64-char sha256 hex (use research-plan to obtain keys)', ERROR_REASON.USAGE);
|
|
807
|
+
}
|
|
808
|
+
const contentIdx = args.indexOf('--content');
|
|
809
|
+
const sourceIdx = args.indexOf('--source');
|
|
810
|
+
const providerIdx = args.indexOf('--provider');
|
|
811
|
+
const confidenceIdx = args.indexOf('--confidence');
|
|
812
|
+
const kindIdx = args.indexOf('--kind');
|
|
813
|
+
function getFlagValue(idx, flagName) {
|
|
814
|
+
if (idx === -1) return null;
|
|
815
|
+
const val = args[idx + 1];
|
|
816
|
+
if (val === undefined || val.startsWith('--')) {
|
|
817
|
+
error(`research-store put: missing value for ${flagName}`, ERROR_REASON.USAGE);
|
|
818
|
+
}
|
|
819
|
+
return val;
|
|
820
|
+
}
|
|
821
|
+
const content = getFlagValue(contentIdx, '--content');
|
|
822
|
+
const source = getFlagValue(sourceIdx, '--source');
|
|
823
|
+
const provider = getFlagValue(providerIdx, '--provider');
|
|
824
|
+
const confidence = getFlagValue(confidenceIdx, '--confidence');
|
|
825
|
+
const kind = getFlagValue(kindIdx, '--kind');
|
|
826
|
+
if (!content || !source || !provider || !confidence || !kind) {
|
|
827
|
+
error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
|
|
828
|
+
}
|
|
829
|
+
const entry = researchStore.putResearch(cwd, key, { content, source, provider, confidence, kind }, { homeDir });
|
|
830
|
+
output(entry, raw);
|
|
831
|
+
} else {
|
|
832
|
+
error('Unknown research-store subcommand. Available: get, put', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
730
833
|
}
|
|
731
834
|
}
|
|
732
835
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const SKIP_ROOT_RESOLUTION = new Set([
|
|
742
|
-
'generate-slug', 'current-timestamp', 'verify-path-exists',
|
|
743
|
-
'verify-summary', 'template', 'frontmatter', 'detect-custom-files',
|
|
744
|
-
'worktree', 'prompt-budget',
|
|
745
|
-
'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
|
|
746
|
-
'user-story', // pure string validation — no .planning/ access needed
|
|
747
|
-
// #1529: pure runtime→filename projection via getProjectInstructionFile; no
|
|
748
|
-
// .planning/ access needed, and resolving project root would break workflow
|
|
749
|
-
// invocations that run before .planning/ exists (new-project Step 1).
|
|
750
|
-
'project-instruction-file',
|
|
751
|
-
// #1579: eval.score is pure arithmetic (covered/total + infra weights); it
|
|
752
|
-
// needs no .planning/ access, so skip the findProjectRoot traversal.
|
|
753
|
-
'eval',
|
|
754
|
-
]);
|
|
755
|
-
if (!SKIP_ROOT_RESOLUTION.has(command)) {
|
|
756
|
-
cwd = findProjectRoot(cwd);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// When --pick is active, capture stdout and extract the requested field.
|
|
760
|
-
if (pickField) {
|
|
761
|
-
const captured = await captureStdoutSyncWrites(async () => {
|
|
762
|
-
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
763
|
-
});
|
|
764
|
-
const resolved = resolveAtFileOutput(captured);
|
|
836
|
+
function routeResearchPlan({ args, cwd, raw }) {
|
|
837
|
+
const researchProvider = require('./lib/research-provider.cjs');
|
|
838
|
+
const inputIdx = args.indexOf('--input');
|
|
839
|
+
const inputPath = inputIdx !== -1 ? args[inputIdx + 1] : null;
|
|
840
|
+
if (!inputPath || inputPath.startsWith('--')) {
|
|
841
|
+
error('Usage: gsd-tools research-plan --input <path>', ERROR_REASON.USAGE);
|
|
842
|
+
}
|
|
843
|
+
let planInput;
|
|
765
844
|
try {
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
} catch {
|
|
771
|
-
fs.writeSync(1, captured);
|
|
845
|
+
const raw_ = fs.readFileSync(path.resolve(inputPath), 'utf8');
|
|
846
|
+
planInput = JSON.parse(raw_);
|
|
847
|
+
} catch (readErr) {
|
|
848
|
+
error(`research-plan: cannot read/parse --input file: ${inputPath}`, ERROR_REASON.USAGE);
|
|
772
849
|
}
|
|
773
|
-
|
|
850
|
+
if (planInput === null || typeof planInput !== 'object' || Array.isArray(planInput)) {
|
|
851
|
+
error('research-plan: --input must be an object with a questions array', ERROR_REASON.USAGE);
|
|
852
|
+
}
|
|
853
|
+
if (!Array.isArray(planInput.questions)) {
|
|
854
|
+
error('research-plan: --input must be an object with a questions array', ERROR_REASON.USAGE);
|
|
855
|
+
}
|
|
856
|
+
const { ecosystem = '', config: planConfig = {}, questions } = planInput;
|
|
857
|
+
const homeDir = process.env.HOME || require('os').homedir();
|
|
858
|
+
const plan = researchProvider.planResearch({ questions, ecosystem, config: planConfig, cwd, homeDir });
|
|
859
|
+
output(plan, raw);
|
|
774
860
|
}
|
|
775
861
|
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
// already resolves this, but the normal path wrote @file: to stdout, forcing
|
|
779
|
-
// every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
|
|
780
|
-
// that breaks on PowerShell and other non-bash shells.
|
|
781
|
-
const captured = await captureStdoutSyncWrites(async () => {
|
|
782
|
-
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
783
|
-
});
|
|
784
|
-
fs.writeSync(1, resolveAtFileOutput(captured));
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function captureStdoutSyncWrites(run) {
|
|
788
|
-
const originalWriteSync = fs.writeSync;
|
|
789
|
-
let captured = '';
|
|
862
|
+
// ─── ADR-2346 P4: leaf host routers (all remaining commands) ───────────
|
|
863
|
+
// Each body relocated verbatim from its `case` arm; inner break; → return;.
|
|
790
864
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
captured += data.toString('utf-8');
|
|
795
|
-
return data.length;
|
|
796
|
-
}
|
|
797
|
-
const text = String(data);
|
|
798
|
-
captured += text;
|
|
799
|
-
let encoding = 'utf-8';
|
|
800
|
-
if (typeof rest[1] === 'string') encoding = rest[1];
|
|
801
|
-
return Buffer.byteLength(text, encoding);
|
|
802
|
-
}
|
|
803
|
-
return originalWriteSync.call(fs, fd, data, ...rest);
|
|
804
|
-
};
|
|
865
|
+
function routeAgent({ args, cwd, raw, error }) {
|
|
866
|
+
routeAgentCommand({ args, raw });
|
|
867
|
+
}
|
|
805
868
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
869
|
+
function routeSmartEntry({ args, cwd, raw, error }) {
|
|
870
|
+
smartEntryMod.runSmartEntry(cwd, args, raw);
|
|
871
|
+
}
|
|
809
872
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
restore();
|
|
814
|
-
return captured;
|
|
815
|
-
}, (err) => {
|
|
816
|
-
restore();
|
|
817
|
-
// The wrapped command may have written to stdout BEFORE it threw — e.g. a --raw
|
|
818
|
-
// command that emits a JSON result/error envelope and THEN throws ExitError to set a
|
|
819
|
-
// non-zero exit code (capability set/disable on an unknown id). Without this flush that
|
|
820
|
-
// captured output is silently discarded (the success-path flush at the call site never
|
|
821
|
-
// runs on a throw). Emit it now; the error still propagates so the exit code is preserved.
|
|
822
|
-
if (captured) {
|
|
823
|
-
try { originalWriteSync.call(fs, 1, resolveAtFileOutput(captured)); } catch { /* best-effort flush */ }
|
|
824
|
-
}
|
|
825
|
-
throw err;
|
|
826
|
-
});
|
|
827
|
-
}
|
|
873
|
+
function routeCheck({ args, cwd, raw, error }) {
|
|
874
|
+
routeCheckCommand({ args, cwd, raw });
|
|
875
|
+
}
|
|
828
876
|
|
|
829
|
-
function
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
877
|
+
function routeFindPhase({ args, cwd, raw, error }) {
|
|
878
|
+
// Phase 6 (#3575): dispatch via SDK executeForCjs when available.
|
|
879
|
+
// SDK handler: findPhase in sdk/src/query/phase.ts.
|
|
880
|
+
const handled = _dispatchNonFamily({
|
|
881
|
+
registryCommand: 'find-phase',
|
|
882
|
+
registryArgs: args.slice(1),
|
|
883
|
+
legacyCommand: 'find-phase',
|
|
884
|
+
legacyArgs: args.slice(1),
|
|
885
|
+
cwd,
|
|
886
|
+
raw,
|
|
887
|
+
error,
|
|
888
|
+
output: output,
|
|
889
|
+
});
|
|
890
|
+
if (!handled) phase.cmdFindPhase(cwd, args[1], raw);
|
|
891
|
+
}
|
|
833
892
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const index = parseInt(bracketMatch[2], 10);
|
|
847
|
-
current = current[key];
|
|
848
|
-
if (!Array.isArray(current)) return undefined;
|
|
849
|
-
current = index < 0 ? current[current.length + index] : current[index];
|
|
850
|
-
} else {
|
|
851
|
-
current = current[part];
|
|
852
|
-
}
|
|
893
|
+
function routeCommit({ args, cwd, raw, error }) {
|
|
894
|
+
const amend = args.includes('--amend');
|
|
895
|
+
const noVerify = args.includes('--no-verify');
|
|
896
|
+
const filesIndex = args.indexOf('--files');
|
|
897
|
+
// Collect all positional args between command name and first flag,
|
|
898
|
+
// then join them — handles both quoted ("multi word msg") and
|
|
899
|
+
// unquoted (multi word msg) invocations from different shells
|
|
900
|
+
const endIndex = filesIndex !== -1 ? filesIndex : args.length;
|
|
901
|
+
const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
|
|
902
|
+
const message = messageArgs.join(' ') || undefined;
|
|
903
|
+
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
|
|
904
|
+
commands.cmdCommit(cwd, message, files, raw, amend, noVerify);
|
|
853
905
|
}
|
|
854
|
-
return current;
|
|
855
|
-
}
|
|
856
906
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
routeAgentCommand({ args, raw });
|
|
861
|
-
break;
|
|
862
|
-
}
|
|
907
|
+
function routeCheckCommit({ args, cwd, raw, error }) {
|
|
908
|
+
commands.cmdCheckCommit(cwd, raw);
|
|
909
|
+
}
|
|
863
910
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
911
|
+
function routeCommitToSubrepo({ args, cwd, raw, error }) {
|
|
912
|
+
const message = args[1];
|
|
913
|
+
const filesIndex = args.indexOf('--files');
|
|
914
|
+
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
|
|
915
|
+
commands.cmdCommitToSubrepo(cwd, message, files, raw);
|
|
916
|
+
}
|
|
868
917
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
918
|
+
function routePrSubrepo({ args, cwd, raw, error }) {
|
|
919
|
+
const message = args[1];
|
|
920
|
+
const { repo, branch } = parseNamedArgs(args, ['repo', 'branch']);
|
|
921
|
+
commands.cmdPrSubrepo(cwd, repo, branch, message, raw);
|
|
922
|
+
}
|
|
873
923
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
error,
|
|
881
|
-
});
|
|
882
|
-
break;
|
|
883
|
-
}
|
|
924
|
+
function routeVerifySummary({ args, cwd, raw, error }) {
|
|
925
|
+
const summaryPath = args[1];
|
|
926
|
+
const countIndex = args.indexOf('--check-count');
|
|
927
|
+
const checkCount = countIndex !== -1 ? parseInt(args[countIndex + 1], 10) : 2;
|
|
928
|
+
verify.cmdVerifySummary(cwd, summaryPath, checkCount, raw);
|
|
929
|
+
}
|
|
884
930
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
931
|
+
function routeTemplate({ args, cwd, raw, error }) {
|
|
932
|
+
const subcommand = args[1];
|
|
933
|
+
if (subcommand === 'select') {
|
|
934
|
+
template.cmdTemplateSelect(cwd, args[2], raw);
|
|
935
|
+
} else if (subcommand === 'fill') {
|
|
936
|
+
const templateType = args[2];
|
|
937
|
+
const { phase, plan, name, type, wave, fields: fieldsRaw } = parseNamedArgs(args, ['phase', 'plan', 'name', 'type', 'wave', 'fields']);
|
|
938
|
+
let fields = {};
|
|
939
|
+
if (fieldsRaw) {
|
|
940
|
+
const { safeJsonParse } = require('./lib/security.cjs');
|
|
941
|
+
const result = safeJsonParse(fieldsRaw, { label: '--fields' });
|
|
942
|
+
if (!result.ok) error(result.error);
|
|
943
|
+
fields = result.value;
|
|
944
|
+
}
|
|
945
|
+
template.cmdTemplateFill(cwd, templateType, {
|
|
946
|
+
phase, plan, name, fields,
|
|
947
|
+
type: type || 'execute',
|
|
948
|
+
wave: wave || '1',
|
|
949
|
+
}, raw);
|
|
950
|
+
} else {
|
|
951
|
+
error('Unknown template subcommand. Available: select, fill', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
889
954
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
// the /gsd:plan-phase + init plan-phase paths accept only the space form, so supporting
|
|
894
|
-
// = here alone would create an inconsistency (#703).
|
|
895
|
-
const granArgs = args.slice(1);
|
|
896
|
-
let granOverride;
|
|
897
|
-
const granPositionals = [];
|
|
898
|
-
for (let i = 0; i < granArgs.length; i++) {
|
|
899
|
-
const a = granArgs[i];
|
|
900
|
-
if (a === '--granularity' && granArgs[i + 1] !== undefined && !granArgs[i + 1].startsWith('--')) {
|
|
901
|
-
if (granOverride === undefined) { granOverride = granArgs[++i]; } else { ++i; }
|
|
902
|
-
} else {
|
|
903
|
-
granPositionals.push(a);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
commands.cmdResolveGranularity(cwd, granPositionals[0], raw, granOverride);
|
|
907
|
-
break;
|
|
908
|
-
}
|
|
955
|
+
function routeTask({ args, cwd, raw, error }) {
|
|
956
|
+
routeTaskCommand({ args, cwd, raw });
|
|
957
|
+
}
|
|
909
958
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
// --fast-mode <val>
|
|
950
|
-
if (a === '--fast-mode') {
|
|
951
|
-
const val = execArgs[i + 1];
|
|
952
|
-
if (val === undefined || val.startsWith('--')) error('Missing value for --fast-mode', ERROR_REASON.USAGE);
|
|
953
|
-
fastModeOverride = val === 'true' ? true : val === 'false' ? false : undefined;
|
|
954
|
-
i++;
|
|
955
|
-
continue;
|
|
956
|
-
}
|
|
957
|
-
// --attempt <val>
|
|
958
|
-
if (a === '--attempt') {
|
|
959
|
-
const val = execArgs[i + 1];
|
|
960
|
-
if (val === undefined || val.startsWith('--')) error('Missing value for --attempt', ERROR_REASON.USAGE);
|
|
961
|
-
const n = parseInt(val, 10);
|
|
962
|
-
if (!Number.isInteger(n) || n < 0) error('--attempt requires a non-negative integer', ERROR_REASON.USAGE);
|
|
963
|
-
attempt = n;
|
|
964
|
-
i++;
|
|
965
|
-
continue;
|
|
966
|
-
}
|
|
967
|
-
// --raw is handled by top-level arg processing; skip it here
|
|
968
|
-
if (a === '--raw') continue;
|
|
969
|
-
// Unknown flag
|
|
970
|
-
if (a.startsWith('-')) error(`Unknown flag for resolve-execution: ${a}`, ERROR_REASON.USAGE);
|
|
971
|
-
// Positional
|
|
972
|
-
positionals.push(a);
|
|
973
|
-
}
|
|
974
|
-
if (positionals.length === 0) error('agent-type required', ERROR_REASON.USAGE);
|
|
975
|
-
if (positionals.length > 1) error(`resolve-execution requires exactly one agent-type argument; got: ${positionals.join(', ')}`, ERROR_REASON.USAGE);
|
|
976
|
-
const agentTypeArg = positionals[0];
|
|
977
|
-
commands.cmdResolveExecution(cwd, agentTypeArg, raw, {
|
|
978
|
-
effortOverride,
|
|
979
|
-
fastModeOverride,
|
|
980
|
-
attempt,
|
|
981
|
-
});
|
|
982
|
-
break;
|
|
983
|
-
}
|
|
959
|
+
function routeFrontmatter({ args, cwd, raw, error }) {
|
|
960
|
+
// Phase 6 (#3575): dispatch via SDK executeForCjs when available.
|
|
961
|
+
// SDK handler: sdk/src/query/frontmatter.ts + frontmatter-mutation.ts.
|
|
962
|
+
// CJS fallback: frontmatter.cjs (cooperating sibling).
|
|
963
|
+
const subcommand = args[1];
|
|
964
|
+
const file = args[2];
|
|
965
|
+
const FRONTMATTER_SDK_MAP = {
|
|
966
|
+
get: 'frontmatter.get',
|
|
967
|
+
set: 'frontmatter.set',
|
|
968
|
+
merge: 'frontmatter.merge',
|
|
969
|
+
validate: 'frontmatter.validate',
|
|
970
|
+
};
|
|
971
|
+
if (subcommand in FRONTMATTER_SDK_MAP) {
|
|
972
|
+
const handled = _dispatchNonFamily({
|
|
973
|
+
registryCommand: FRONTMATTER_SDK_MAP[subcommand],
|
|
974
|
+
registryArgs: args.slice(2),
|
|
975
|
+
legacyCommand: 'frontmatter',
|
|
976
|
+
legacyArgs: args.slice(1),
|
|
977
|
+
cwd,
|
|
978
|
+
raw,
|
|
979
|
+
error,
|
|
980
|
+
output: output,
|
|
981
|
+
});
|
|
982
|
+
if (handled) return;
|
|
983
|
+
}
|
|
984
|
+
// CJS fallback (SDK unavailable or unknown subcommand)
|
|
985
|
+
if (subcommand === 'get') {
|
|
986
|
+
frontmatter.cmdFrontmatterGet(cwd, file, parseNamedArgs(args, ['field']).field, raw);
|
|
987
|
+
} else if (subcommand === 'set') {
|
|
988
|
+
const { field, value } = parseNamedArgs(args, ['field', 'value']);
|
|
989
|
+
frontmatter.cmdFrontmatterSet(cwd, file, field, value !== null ? value : undefined, raw);
|
|
990
|
+
} else if (subcommand === 'merge') {
|
|
991
|
+
frontmatter.cmdFrontmatterMerge(cwd, file, parseNamedArgs(args, ['data']).data, raw);
|
|
992
|
+
} else if (subcommand === 'validate') {
|
|
993
|
+
frontmatter.cmdFrontmatterValidate(cwd, file, parseNamedArgs(args, ['schema']).schema, raw);
|
|
994
|
+
} else {
|
|
995
|
+
error('Unknown frontmatter subcommand. Available: get, set, merge, validate', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
984
998
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const handled = _dispatchNonFamily({
|
|
989
|
-
registryCommand: 'find-phase',
|
|
990
|
-
registryArgs: args.slice(1),
|
|
991
|
-
legacyCommand: 'find-phase',
|
|
992
|
-
legacyArgs: args.slice(1),
|
|
993
|
-
cwd,
|
|
994
|
-
raw,
|
|
995
|
-
error,
|
|
996
|
-
output: output,
|
|
997
|
-
});
|
|
998
|
-
if (!handled) phase.cmdFindPhase(cwd, args[1], raw);
|
|
999
|
-
break;
|
|
1000
|
-
}
|
|
999
|
+
function routeEval({ args, cwd, raw, error }) {
|
|
1000
|
+
routeEvalCommand({ evalMod, args, cwd, raw, error });
|
|
1001
|
+
}
|
|
1001
1002
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const message = messageArgs.join(' ') || undefined;
|
|
1012
|
-
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
|
|
1013
|
-
commands.cmdCommit(cwd, message, files, raw, amend, noVerify);
|
|
1014
|
-
break;
|
|
1015
|
-
}
|
|
1003
|
+
function routeVerification({ args, cwd, raw, error }) {
|
|
1004
|
+
routeVerificationCommand({
|
|
1005
|
+
verification,
|
|
1006
|
+
args,
|
|
1007
|
+
cwd,
|
|
1008
|
+
raw,
|
|
1009
|
+
error,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1016
1012
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1013
|
+
function routeGenerateSlug({ args, cwd, raw, error }) {
|
|
1014
|
+
// Phase 6 (#3575): dispatch via SDK executeForCjs when available.
|
|
1015
|
+
// SDK handler: generateSlug in sdk/src/query/utils.ts.
|
|
1016
|
+
const handled = _dispatchNonFamily({
|
|
1017
|
+
registryCommand: 'generate-slug',
|
|
1018
|
+
registryArgs: args.slice(1),
|
|
1019
|
+
legacyCommand: 'generate-slug',
|
|
1020
|
+
legacyArgs: args.slice(1),
|
|
1021
|
+
cwd,
|
|
1022
|
+
raw,
|
|
1023
|
+
error,
|
|
1024
|
+
output: output,
|
|
1025
|
+
});
|
|
1026
|
+
if (!handled) commands.cmdGenerateSlug(args[1], raw);
|
|
1027
|
+
}
|
|
1021
1028
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
+
function routeCurrentTimestamp({ args, cwd, raw, error }) {
|
|
1030
|
+
// Keep this command on the CJS fast path.
|
|
1031
|
+
// Rationale: it is a pure local formatter and avoids SDK bridge startup
|
|
1032
|
+
// in tight subprocess loops where Windows CI has shown intermittent
|
|
1033
|
+
// native crashes (0xC0000005 / 3221225477).
|
|
1034
|
+
commands.cmdCurrentTimestamp(args[1] || 'full', raw);
|
|
1035
|
+
}
|
|
1029
1036
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1037
|
+
function routeProjectInstructionFile({ args, cwd, raw, error }) {
|
|
1038
|
+
// #1529: pure runtime→filename projection. Backs the
|
|
1039
|
+
// `gsd_run query project-instruction-file --runtime <r>` call in
|
|
1040
|
+
// new-project.md so the bash workflow and profile-output.cjs share one
|
|
1041
|
+
// source of truth (getProjectInstructionFile in runtime-name-policy.cjs).
|
|
1042
|
+
// No SDK bridge — pure local lookup, runs before .planning/ exists.
|
|
1043
|
+
const { getProjectInstructionFile } = require('./lib/runtime-name-policy.cjs');
|
|
1044
|
+
// Parse --runtime <value> (space or = form); default to empty so the
|
|
1045
|
+
// safe AGENTS.md cross-agent default applies.
|
|
1046
|
+
const pifArgs = args.slice(1);
|
|
1047
|
+
let pifRuntime = '';
|
|
1048
|
+
for (let i = 0; i < pifArgs.length; i++) {
|
|
1049
|
+
const a = pifArgs[i];
|
|
1050
|
+
if (a === '--runtime' && pifArgs[i + 1] !== undefined) { pifRuntime = pifArgs[++i]; continue; }
|
|
1051
|
+
if (a.startsWith('--runtime=')) { pifRuntime = a.slice('--runtime='.length); continue; }
|
|
1052
|
+
// First positional that isn't a flag also works (lenient); otherwise ignore unknown flags.
|
|
1053
|
+
if (!a.startsWith('-') && !pifRuntime) { pifRuntime = a; }
|
|
1054
|
+
}
|
|
1055
|
+
const filename = getProjectInstructionFile(pifRuntime);
|
|
1056
|
+
process.stdout.write(filename + '\n');
|
|
1057
|
+
}
|
|
1036
1058
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
const checkCount = countIndex !== -1 ? parseInt(args[countIndex + 1], 10) : 2;
|
|
1041
|
-
verify.cmdVerifySummary(cwd, summaryPath, checkCount, raw);
|
|
1042
|
-
break;
|
|
1043
|
-
}
|
|
1059
|
+
function routeListTodos({ args, cwd, raw, error }) {
|
|
1060
|
+
commands.cmdListTodos(cwd, args[1], raw);
|
|
1061
|
+
}
|
|
1044
1062
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
template.cmdTemplateSelect(cwd, args[2], raw);
|
|
1049
|
-
} else if (subcommand === 'fill') {
|
|
1050
|
-
const templateType = args[2];
|
|
1051
|
-
const { phase, plan, name, type, wave, fields: fieldsRaw } = parseNamedArgs(args, ['phase', 'plan', 'name', 'type', 'wave', 'fields']);
|
|
1052
|
-
let fields = {};
|
|
1053
|
-
if (fieldsRaw) {
|
|
1054
|
-
const { safeJsonParse } = require('./lib/security.cjs');
|
|
1055
|
-
const result = safeJsonParse(fieldsRaw, { label: '--fields' });
|
|
1056
|
-
if (!result.ok) error(result.error);
|
|
1057
|
-
fields = result.value;
|
|
1058
|
-
}
|
|
1059
|
-
template.cmdTemplateFill(cwd, templateType, {
|
|
1060
|
-
phase, plan, name, fields,
|
|
1061
|
-
type: type || 'execute',
|
|
1062
|
-
wave: wave || '1',
|
|
1063
|
-
}, raw);
|
|
1064
|
-
} else {
|
|
1065
|
-
error('Unknown template subcommand. Available: select, fill', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1066
|
-
}
|
|
1067
|
-
break;
|
|
1068
|
-
}
|
|
1063
|
+
function routeListSeeds({ args, cwd, raw, error }) {
|
|
1064
|
+
commands.cmdListSeeds(cwd, args[1], raw);
|
|
1065
|
+
}
|
|
1069
1066
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
}
|
|
1067
|
+
function routeVerifyPathExists({ args, cwd, raw, error }) {
|
|
1068
|
+
commands.cmdVerifyPathExists(cwd, args[1], raw);
|
|
1069
|
+
}
|
|
1074
1070
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
validate: 'frontmatter.validate',
|
|
1086
|
-
};
|
|
1087
|
-
if (subcommand in FRONTMATTER_SDK_MAP) {
|
|
1088
|
-
const handled = _dispatchNonFamily({
|
|
1089
|
-
registryCommand: FRONTMATTER_SDK_MAP[subcommand],
|
|
1090
|
-
registryArgs: args.slice(2),
|
|
1091
|
-
legacyCommand: 'frontmatter',
|
|
1092
|
-
legacyArgs: args.slice(1),
|
|
1093
|
-
cwd,
|
|
1094
|
-
raw,
|
|
1095
|
-
error,
|
|
1096
|
-
output: output,
|
|
1097
|
-
});
|
|
1098
|
-
if (handled) break;
|
|
1099
|
-
}
|
|
1100
|
-
// CJS fallback (SDK unavailable or unknown subcommand)
|
|
1101
|
-
if (subcommand === 'get') {
|
|
1102
|
-
frontmatter.cmdFrontmatterGet(cwd, file, parseNamedArgs(args, ['field']).field, raw);
|
|
1103
|
-
} else if (subcommand === 'set') {
|
|
1104
|
-
const { field, value } = parseNamedArgs(args, ['field', 'value']);
|
|
1105
|
-
frontmatter.cmdFrontmatterSet(cwd, file, field, value !== null ? value : undefined, raw);
|
|
1106
|
-
} else if (subcommand === 'merge') {
|
|
1107
|
-
frontmatter.cmdFrontmatterMerge(cwd, file, parseNamedArgs(args, ['data']).data, raw);
|
|
1108
|
-
} else if (subcommand === 'validate') {
|
|
1109
|
-
frontmatter.cmdFrontmatterValidate(cwd, file, parseNamedArgs(args, ['schema']).schema, raw);
|
|
1110
|
-
} else {
|
|
1111
|
-
error('Unknown frontmatter subcommand. Available: get, set, merge, validate', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1112
|
-
}
|
|
1113
|
-
break;
|
|
1114
|
-
}
|
|
1071
|
+
function routeQuickTasksAppend({ args, cwd, raw, error }) {
|
|
1072
|
+
// #2133 / ADR-2143 §3,§7: schema-backed replacement for fast.md's inline
|
|
1073
|
+
// `awk NF-2` Quick Tasks column arithmetic. Row construction is delegated
|
|
1074
|
+
// to the pure appendQuickTaskRow (markdown-table.cjs); this case only
|
|
1075
|
+
// handles the I/O (read STATE.md, resolve date/commit, write STATE.md).
|
|
1076
|
+
const qtaArgs = args.slice(1);
|
|
1077
|
+
const qtaTask = parseNamedArgs(qtaArgs, ['task']).task || args[1];
|
|
1078
|
+
if (!qtaTask) {
|
|
1079
|
+
error('quick-tasks-append requires --task <description> (or a positional description)', ERROR_REASON.USAGE);
|
|
1080
|
+
}
|
|
1115
1081
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
cwd,
|
|
1121
|
-
raw,
|
|
1122
|
-
error,
|
|
1123
|
-
});
|
|
1124
|
-
break;
|
|
1125
|
-
}
|
|
1082
|
+
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
|
1083
|
+
if (!fs.existsSync(statePath)) {
|
|
1084
|
+
error(`quick-tasks-append: STATE.md not found at ${statePath}`, ERROR_REASON.USAGE);
|
|
1085
|
+
}
|
|
1126
1086
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1087
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
1088
|
+
const { execGit } = require('./lib/shell-command-projection.cjs');
|
|
1089
|
+
const hashResult = execGit(['rev-parse', '--short', 'HEAD'], { cwd });
|
|
1090
|
+
const commit = hashResult.exitCode === 0 && hashResult.stdout ? hashResult.stdout : '—';
|
|
1091
|
+
|
|
1092
|
+
const { appendQuickTaskRow } = require('./lib/markdown-table.cjs');
|
|
1093
|
+
|
|
1094
|
+
// #2242 review fix: route the read -> mutate -> write cycle through
|
|
1095
|
+
// state.readModifyWriteStateMd (lib/state.cjs) instead of a raw
|
|
1096
|
+
// fs.readFileSync + fs.writeFileSync pair, so the whole read-modify-write
|
|
1097
|
+
// is atomic under STATE.md's lockfile — closing the lost-update race a
|
|
1098
|
+
// raw read/write pair left open (cf. #500/#905/#1230). This mirrors the
|
|
1099
|
+
// pattern every other STATE.md-mutating case in state.cts uses (e.g.
|
|
1100
|
+
// cmdStateAddBlocker, cmdStateAddDecision): a mutable outer variable
|
|
1101
|
+
// captures the pure helper's side output, and a fail-loud reason throws
|
|
1102
|
+
// ExitError from INSIDE the transform (readModifyWriteStateMd's finally
|
|
1103
|
+
// still releases the lock before the throw propagates; the transform
|
|
1104
|
+
// throws before returning new content, so nothing is ever written).
|
|
1105
|
+
let mutation;
|
|
1106
|
+
state.readModifyWriteStateMd(statePath, (content) => {
|
|
1107
|
+
const result = appendQuickTaskRow(content, { description: qtaTask, date, commit });
|
|
1108
|
+
if (!result.ok) {
|
|
1109
|
+
// Mirrors fast.md's old "skip with a brief log" behaviour (#2133): this
|
|
1110
|
+
// is an expected, recoverable condition (no table / unrecognized
|
|
1111
|
+
// schema), not a hard crash. ExitError sets a non-zero exit code (so
|
|
1112
|
+
// fast.md's `|| echo ...` fallback fires) without calling
|
|
1113
|
+
// process.exit() directly — stdout stays flushed and untouched.
|
|
1114
|
+
throw new ExitError(1, `⚠ quick-tasks-append: ${result.reason}`);
|
|
1115
|
+
}
|
|
1116
|
+
mutation = result.value;
|
|
1117
|
+
return result.value.content;
|
|
1118
|
+
}, cwd);
|
|
1131
1119
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
// verification status <phaseDir>
|
|
1135
|
-
// Read the first *-VERIFICATION.md in phaseDir and return
|
|
1136
|
-
// { status, next_action, next_command } routing result.
|
|
1137
|
-
//
|
|
1138
|
-
// Note: `verification` (reads verifier-emitted status) is distinct from
|
|
1139
|
-
// `verify` (runs verification checks like plan-structure/artifacts).
|
|
1140
|
-
|
|
1141
|
-
case 'verification': {
|
|
1142
|
-
routeVerificationCommand({
|
|
1143
|
-
verification,
|
|
1144
|
-
args,
|
|
1145
|
-
cwd,
|
|
1146
|
-
raw,
|
|
1147
|
-
error,
|
|
1148
|
-
});
|
|
1149
|
-
break;
|
|
1150
|
-
}
|
|
1120
|
+
output({ ok: true, row: mutation.row, variant: mutation.variant }, raw, mutation.row);
|
|
1121
|
+
}
|
|
1151
1122
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
cwd,
|
|
1161
|
-
raw,
|
|
1162
|
-
error,
|
|
1163
|
-
output: output,
|
|
1164
|
-
});
|
|
1165
|
-
if (!handled) commands.cmdGenerateSlug(args[1], raw);
|
|
1166
|
-
break;
|
|
1167
|
-
}
|
|
1123
|
+
function routeNormalizeTestCommand({ args, cwd, raw, error }) {
|
|
1124
|
+
// #1857: rewrite a resolved test command to a one-shot form so a
|
|
1125
|
+
// watch-mode runner (vitest/jest) cannot hang a verification gate. Shared
|
|
1126
|
+
// by the regression gate and the post-merge gate. args[1] is the raw
|
|
1127
|
+
// resolved command; --cwd (already parsed into `cwd`) locates package.json.
|
|
1128
|
+
const testCommandNormalizer = require('./lib/normalize-test-command.cjs');
|
|
1129
|
+
testCommandNormalizer.cmdNormalizeTestCommand(cwd, args[1]);
|
|
1130
|
+
}
|
|
1168
1131
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1132
|
+
function routeDispatchShouldFlatten({ args, cwd, raw, error }) {
|
|
1133
|
+
// #1708 / #853: typed query replacing the `RUNTIME === 'codex'` prose rule.
|
|
1134
|
+
//
|
|
1135
|
+
// Resolves the current runtime (GSD_RUNTIME > config.runtime > 'claude'),
|
|
1136
|
+
// looks up registry.runtimes[id].runtime.hostIntegration.dispatch, and
|
|
1137
|
+
// calls shouldFlattenDispatch(dispatch) from host-integration.cjs.
|
|
1138
|
+
//
|
|
1139
|
+
// Fail-closed: any unknown runtime, missing dispatch, or thrown error
|
|
1140
|
+
// yields `true` (inline — the always-safe default).
|
|
1141
|
+
//
|
|
1142
|
+
// Output:
|
|
1143
|
+
// --raw → prints exactly `true` or `false`
|
|
1144
|
+
// --json → prints { runtime, shouldFlatten, dispatch }
|
|
1145
|
+
// default → same as --raw
|
|
1146
|
+
try {
|
|
1147
|
+
// Resolve runtime using the same precedence as `config-get runtime`.
|
|
1148
|
+
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
1149
|
+
const runtimeId = resolveRuntime(cwd);
|
|
1150
|
+
|
|
1151
|
+
// Look up dispatch from the capability registry.
|
|
1152
|
+
const registry = require('./lib/capability-registry.cjs');
|
|
1153
|
+
const runtimeEntry = registry.runtimes != null
|
|
1154
|
+
? registry.runtimes[runtimeId]
|
|
1155
|
+
: null;
|
|
1156
|
+
const dispatch = runtimeEntry?.runtime?.hostIntegration?.dispatch ?? null;
|
|
1157
|
+
|
|
1158
|
+
// Call shouldFlattenDispatch from host-integration.cjs.
|
|
1159
|
+
const hostIntegration = require('./lib/host-integration.cjs');
|
|
1160
|
+
const shouldFlat = dispatch !== null
|
|
1161
|
+
? hostIntegration.shouldFlattenDispatch(dispatch)
|
|
1162
|
+
: true; // fail-closed: unknown runtime → inline
|
|
1163
|
+
|
|
1164
|
+
const jsonIdx = args.indexOf('--json');
|
|
1165
|
+
if (jsonIdx !== -1) {
|
|
1166
|
+
output({
|
|
1167
|
+
runtime: runtimeId,
|
|
1168
|
+
shouldFlatten: shouldFlat,
|
|
1169
|
+
dispatch: dispatch,
|
|
1170
|
+
}, raw);
|
|
1171
|
+
} else {
|
|
1172
|
+
// --raw or default: print exactly true or false
|
|
1173
|
+
process.stdout.write(shouldFlat ? 'true' : 'false');
|
|
1174
|
+
}
|
|
1175
|
+
} catch {
|
|
1176
|
+
// Fail-closed on any error: inline is always safe.
|
|
1177
|
+
process.stdout.write('true');
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1177
1180
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
// safe AGENTS.md cross-agent default applies.
|
|
1187
|
-
const pifArgs = args.slice(1);
|
|
1188
|
-
let pifRuntime = '';
|
|
1189
|
-
for (let i = 0; i < pifArgs.length; i++) {
|
|
1190
|
-
const a = pifArgs[i];
|
|
1191
|
-
if (a === '--runtime' && pifArgs[i + 1] !== undefined) { pifRuntime = pifArgs[++i]; continue; }
|
|
1192
|
-
if (a.startsWith('--runtime=')) { pifRuntime = a.slice('--runtime='.length); continue; }
|
|
1193
|
-
// First positional that isn't a flag also works (lenient); otherwise ignore unknown flags.
|
|
1194
|
-
if (!a.startsWith('-') && !pifRuntime) { pifRuntime = a; }
|
|
1195
|
-
}
|
|
1196
|
-
const filename = getProjectInstructionFile(pifRuntime);
|
|
1197
|
-
process.stdout.write(filename + '\n');
|
|
1198
|
-
break;
|
|
1199
|
-
}
|
|
1181
|
+
function routeAgentSkills({ args, cwd, raw, error }) {
|
|
1182
|
+
// --json emits typed IR { agent_type, block, skills_count } for test assertions
|
|
1183
|
+
// (#455). Default (no flag) outputs raw XML so workflow shell expansions work.
|
|
1184
|
+
const jsonIdx = args.indexOf('--json');
|
|
1185
|
+
const agentSkillsJsonMode = jsonIdx !== -1;
|
|
1186
|
+
if (agentSkillsJsonMode) args.splice(jsonIdx, 1);
|
|
1187
|
+
init.cmdAgentSkills(cwd, args[1], raw, agentSkillsJsonMode);
|
|
1188
|
+
}
|
|
1200
1189
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1190
|
+
function routeSkillManifest({ args, cwd, raw, error }) {
|
|
1191
|
+
init.cmdSkillManifest(cwd, args, raw);
|
|
1192
|
+
}
|
|
1205
1193
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1194
|
+
function routeHistoryDigest({ args, cwd, raw, error }) {
|
|
1195
|
+
commands.cmdHistoryDigest(cwd, raw);
|
|
1196
|
+
}
|
|
1210
1197
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1198
|
+
function routePhases({ args, cwd, raw, error }) {
|
|
1199
|
+
routePhasesCommand({
|
|
1200
|
+
phase,
|
|
1201
|
+
milestone,
|
|
1202
|
+
args,
|
|
1203
|
+
cwd,
|
|
1204
|
+
raw,
|
|
1205
|
+
error,
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1215
1208
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1209
|
+
function routeAssumptionDelta({ args, cwd, raw, error }) {
|
|
1210
|
+
// #1561 — advisory architecture checkpoint. `scan <phase>` reads the
|
|
1211
|
+
// phase section via the same resolver as roadmap.get-phase and runs the
|
|
1212
|
+
// deterministic detectAssumptionDelta, emitting the typed IR as JSON.
|
|
1213
|
+
const sub = args[1];
|
|
1214
|
+
if (sub === 'scan') {
|
|
1215
|
+
const phaseNum = args[2];
|
|
1216
|
+
// Reject missing or flag-shaped phase values (QA matrix: values that
|
|
1217
|
+
// look like flags). `scan --json` must not treat "--json" as a phase.
|
|
1218
|
+
if (!phaseNum || phaseNum.startsWith('-')) {
|
|
1219
|
+
error('Usage: assumption-delta scan <phase> [--terms <csv>]', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
// Optional --terms <csv> override (replaces the pluralization cues;
|
|
1223
|
+
// optional/chosen keep defaults). An EMPTY value ("") or a flag-shaped
|
|
1224
|
+
// value restores the curated defaults (does NOT disable pluralization).
|
|
1225
|
+
// Terms are normalized (deduped, alphanumeric-only, capped) by
|
|
1226
|
+
// detectAssumptionDelta's resolveTerms.
|
|
1227
|
+
let termsOverride;
|
|
1228
|
+
const termsIdx = args.indexOf('--terms');
|
|
1229
|
+
const termsVal = termsIdx !== -1 ? args[termsIdx + 1] : undefined;
|
|
1230
|
+
if (typeof termsVal === 'string' && !termsVal.startsWith('-')) {
|
|
1231
|
+
const list = termsVal
|
|
1232
|
+
.split(',')
|
|
1233
|
+
.map((t) => t.trim().toLowerCase())
|
|
1234
|
+
.filter((t) => t.length > 0);
|
|
1235
|
+
termsOverride = list.length > 0 ? { pluralization: list } : undefined;
|
|
1236
|
+
}
|
|
1237
|
+
const section = roadmap.getRoadmapPhaseWithFallback(cwd, phaseNum);
|
|
1238
|
+
const result = detectAssumptionDelta(section ?? '', termsOverride);
|
|
1239
|
+
output(result, raw);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
error(`Unknown assumption-delta subcommand: ${sub}. Available: scan`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1243
|
+
}
|
|
1236
1244
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1245
|
+
function routeRequirements({ args, cwd, raw, error }) {
|
|
1246
|
+
const subcommand = args[1];
|
|
1247
|
+
if (subcommand === 'mark-complete') {
|
|
1248
|
+
milestone.cmdRequirementsMarkComplete(cwd, args.slice(2), raw);
|
|
1249
|
+
} else if (subcommand === 'ready-ids') {
|
|
1250
|
+
// #2388: read-only shared-ID gate — computes which of the given
|
|
1251
|
+
// requirement IDs are safe to hand to mark-complete right now
|
|
1252
|
+
// (no sibling *-PLAN.md in the same phase dir still missing its
|
|
1253
|
+
// *-SUMMARY.md for that ID).
|
|
1254
|
+
milestone.cmdRequirementsReadyIds(cwd, args.slice(2), raw);
|
|
1255
|
+
} else if (subcommand === 'revert-phase') {
|
|
1256
|
+
// #2388: gaps_found-only revert — flips this phase's own
|
|
1257
|
+
// requirement IDs back out of Complete (checkbox + traceability
|
|
1258
|
+
// row) before the gap report renders.
|
|
1259
|
+
milestone.cmdRequirementsRevertPhase(cwd, args.slice(2), raw);
|
|
1260
|
+
} else {
|
|
1261
|
+
error('Unknown requirements subcommand. Available: mark-complete, ready-ids, revert-phase', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1252
1264
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
legacyCommand: 'config-set-model-profile',
|
|
1259
|
-
legacyArgs: args.slice(1),
|
|
1260
|
-
cwd,
|
|
1261
|
-
raw,
|
|
1262
|
-
error,
|
|
1263
|
-
output: output,
|
|
1264
|
-
});
|
|
1265
|
-
if (!handled) config.cmdConfigSetModelProfile(cwd, args[1], raw);
|
|
1266
|
-
break;
|
|
1267
|
-
}
|
|
1265
|
+
function routeGapAnalysis({ args, cwd, raw, error }) {
|
|
1266
|
+
// Post-planning gap checker (#2493) — unified REQUIREMENTS.md +
|
|
1267
|
+
// CONTEXT.md <decisions> coverage report against PLAN.md files.
|
|
1268
|
+
gapChecker.cmdGapAnalysis(cwd, args.slice(1), raw);
|
|
1269
|
+
}
|
|
1268
1270
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
legacyArgs: args.slice(1),
|
|
1285
|
-
cwd,
|
|
1286
|
-
raw,
|
|
1287
|
-
error,
|
|
1288
|
-
output: output,
|
|
1289
|
-
});
|
|
1290
|
-
if (!handled) config.cmdConfigGet(cwd, args[1], raw, defaultValue);
|
|
1291
|
-
break;
|
|
1292
|
-
}
|
|
1271
|
+
function routeMilestone({ args, cwd, raw, error }) {
|
|
1272
|
+
const subcommand = args[1];
|
|
1273
|
+
if (subcommand === 'complete') {
|
|
1274
|
+
const milestoneName = parseMultiwordArg(args, 'name');
|
|
1275
|
+
// #1871: archive phase dirs by default on milestone complete so the next
|
|
1276
|
+
// new-milestone never inherits un-archived dirs. --no-archive-phases opts out.
|
|
1277
|
+
const archivePhases = !args.includes('--no-archive-phases');
|
|
1278
|
+
const force = args.includes('--force');
|
|
1279
|
+
// #2118: --dry-run prints a preview plan without mutating.
|
|
1280
|
+
const dryRun = args.includes('--dry-run');
|
|
1281
|
+
milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun }, raw);
|
|
1282
|
+
} else {
|
|
1283
|
+
error('Unknown milestone subcommand. Available: complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1293
1286
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
// resolved command; --cwd (already parsed into `cwd`) locates package.json.
|
|
1299
|
-
const testCommandNormalizer = require('./lib/normalize-test-command.cjs');
|
|
1300
|
-
testCommandNormalizer.cmdNormalizeTestCommand(cwd, args[1]);
|
|
1301
|
-
break;
|
|
1302
|
-
}
|
|
1287
|
+
function routeProgress({ args, cwd, raw, error }) {
|
|
1288
|
+
const subcommand = args[1] || 'json';
|
|
1289
|
+
commands.cmdProgressRender(cwd, subcommand, raw);
|
|
1290
|
+
}
|
|
1303
1291
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
try {
|
|
1319
|
-
// Resolve runtime using the same precedence as `config-get runtime`.
|
|
1320
|
-
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
1321
|
-
const runtimeId = resolveRuntime(cwd);
|
|
1322
|
-
|
|
1323
|
-
// Look up dispatch from the capability registry.
|
|
1324
|
-
const registry = require('./lib/capability-registry.cjs');
|
|
1325
|
-
const runtimeEntry = registry.runtimes != null
|
|
1326
|
-
? registry.runtimes[runtimeId]
|
|
1327
|
-
: null;
|
|
1328
|
-
const dispatch = runtimeEntry?.runtime?.hostIntegration?.dispatch ?? null;
|
|
1329
|
-
|
|
1330
|
-
// Call shouldFlattenDispatch from host-integration.cjs.
|
|
1331
|
-
const hostIntegration = require('./lib/host-integration.cjs');
|
|
1332
|
-
const shouldFlat = dispatch !== null
|
|
1333
|
-
? hostIntegration.shouldFlattenDispatch(dispatch)
|
|
1334
|
-
: true; // fail-closed: unknown runtime → inline
|
|
1335
|
-
|
|
1336
|
-
const jsonIdx = args.indexOf('--json');
|
|
1337
|
-
if (jsonIdx !== -1) {
|
|
1338
|
-
output({
|
|
1339
|
-
runtime: runtimeId,
|
|
1340
|
-
shouldFlatten: shouldFlat,
|
|
1341
|
-
dispatch: dispatch,
|
|
1342
|
-
}, raw);
|
|
1343
|
-
} else {
|
|
1344
|
-
// --raw or default: print exactly true or false
|
|
1345
|
-
process.stdout.write(shouldFlat ? 'true' : 'false');
|
|
1346
|
-
}
|
|
1347
|
-
} catch {
|
|
1348
|
-
// Fail-closed on any error: inline is always safe.
|
|
1349
|
-
process.stdout.write('true');
|
|
1350
|
-
}
|
|
1351
|
-
break;
|
|
1352
|
-
}
|
|
1292
|
+
function routeUat({ args, cwd, raw, error }) {
|
|
1293
|
+
const subcommand = args[1];
|
|
1294
|
+
if (subcommand === 'render-checkpoint') {
|
|
1295
|
+
const uat = require('./lib/uat.cjs');
|
|
1296
|
+
const options = parseNamedArgs(args, ['file']);
|
|
1297
|
+
uat.cmdRenderCheckpoint(cwd, options, raw);
|
|
1298
|
+
} else if (subcommand === 'classify-coverage') {
|
|
1299
|
+
const coverage = require('./lib/coverage.cjs');
|
|
1300
|
+
const options = parseNamedArgs(args, ['summary', 'file']);
|
|
1301
|
+
coverage.cmdClassify(cwd, options, raw);
|
|
1302
|
+
} else {
|
|
1303
|
+
error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1353
1306
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
registryArgs: args.slice(1),
|
|
1359
|
-
legacyCommand: 'config-new-project',
|
|
1360
|
-
legacyArgs: args.slice(1),
|
|
1361
|
-
cwd,
|
|
1362
|
-
raw,
|
|
1363
|
-
error,
|
|
1364
|
-
output: output,
|
|
1365
|
-
});
|
|
1366
|
-
if (!handled) config.cmdConfigNewProject(cwd, args[1], raw);
|
|
1367
|
-
break;
|
|
1368
|
-
}
|
|
1307
|
+
function routeStats({ args, cwd, raw, error }) {
|
|
1308
|
+
const subcommand = args[1] || 'json';
|
|
1309
|
+
commands.cmdStats(cwd, subcommand, raw);
|
|
1310
|
+
}
|
|
1369
1311
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1312
|
+
function routeTodo({ args, cwd, raw, error }) {
|
|
1313
|
+
const subcommand = args[1];
|
|
1314
|
+
if (subcommand === 'complete') {
|
|
1315
|
+
commands.cmdTodoComplete(cwd, args[2], raw);
|
|
1316
|
+
} else if (subcommand === 'match-phase') {
|
|
1317
|
+
commands.cmdTodoMatchPhase(cwd, args[2], raw);
|
|
1318
|
+
} else {
|
|
1319
|
+
error('Unknown todo subcommand. Available: complete, match-phase', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1378
1322
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1323
|
+
function routeScaffold({ args, cwd, raw, error }) {
|
|
1324
|
+
const scaffoldType = args[1];
|
|
1325
|
+
const scaffoldOptions = {
|
|
1326
|
+
phase: parseNamedArgs(args, ['phase']).phase,
|
|
1327
|
+
name: parseMultiwordArg(args, 'name'),
|
|
1328
|
+
};
|
|
1329
|
+
commands.cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
|
|
1330
|
+
}
|
|
1386
1331
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1332
|
+
function routeLoop({ args, cwd, raw, error }) {
|
|
1333
|
+
// loop render-hooks <point>
|
|
1334
|
+
const loopSubcommand = args[1];
|
|
1335
|
+
if (loopSubcommand === 'render-hooks') {
|
|
1336
|
+
let loopConfigDir = null;
|
|
1337
|
+
const configDirEqArg = args.find(arg => arg.startsWith('--config-dir='));
|
|
1338
|
+
const configDirIdx = args.indexOf('--config-dir');
|
|
1339
|
+
if (configDirEqArg) {
|
|
1340
|
+
const value = configDirEqArg.slice('--config-dir='.length).trim();
|
|
1341
|
+
if (!value) error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1342
|
+
loopConfigDir = value;
|
|
1343
|
+
} else if (configDirIdx !== -1) {
|
|
1344
|
+
const value = args[configDirIdx + 1];
|
|
1345
|
+
if (!value || value.startsWith('--')) {
|
|
1346
|
+
error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1347
|
+
}
|
|
1348
|
+
loopConfigDir = value;
|
|
1349
|
+
}
|
|
1350
|
+
// --active-cap <capId>: parse and validate before delegating
|
|
1351
|
+
let loopActiveCap = undefined;
|
|
1352
|
+
const activeCapEqArg = args.find(arg => arg.startsWith('--active-cap='));
|
|
1353
|
+
const activeCapIdx = args.indexOf('--active-cap');
|
|
1354
|
+
if (activeCapEqArg) {
|
|
1355
|
+
const value = activeCapEqArg.slice('--active-cap='.length).trim();
|
|
1356
|
+
if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1357
|
+
loopActiveCap = value;
|
|
1358
|
+
} else if (activeCapIdx !== -1) {
|
|
1359
|
+
const value = args[activeCapIdx + 1];
|
|
1360
|
+
if (!value || value.startsWith('--')) {
|
|
1361
|
+
error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1362
|
+
}
|
|
1363
|
+
loopActiveCap = value;
|
|
1364
|
+
}
|
|
1365
|
+
// --runtime <r> (#2003): explicit runtime override so the config-dir
|
|
1366
|
+
// resolution bypasses the persisted-runtime fallback (GSD_RUNTIME →
|
|
1367
|
+
// config.runtime). Mirrors the --config-dir dual-form (--runtime X /
|
|
1368
|
+
// --runtime=X) and the capability-set --runtime precedent.
|
|
1369
|
+
let loopRuntime = undefined;
|
|
1370
|
+
const runtimeEqArg = args.find(arg => arg.startsWith('--runtime='));
|
|
1371
|
+
const runtimeIdx = args.indexOf('--runtime');
|
|
1372
|
+
if (runtimeEqArg) {
|
|
1373
|
+
const value = runtimeEqArg.slice('--runtime='.length).trim();
|
|
1374
|
+
if (!value) error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1375
|
+
loopRuntime = value;
|
|
1376
|
+
} else if (runtimeIdx !== -1) {
|
|
1377
|
+
const value = args[runtimeIdx + 1];
|
|
1378
|
+
if (!value || value.startsWith('--')) {
|
|
1379
|
+
error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1380
|
+
}
|
|
1381
|
+
loopRuntime = value;
|
|
1382
|
+
}
|
|
1383
|
+
loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {
|
|
1384
|
+
configDir: loopConfigDir ? path.resolve(loopConfigDir) : undefined,
|
|
1385
|
+
activeCap: loopActiveCap,
|
|
1386
|
+
runtime: loopRuntime,
|
|
1387
|
+
});
|
|
1388
|
+
} else {
|
|
1389
|
+
error(
|
|
1390
|
+
`Unknown loop subcommand: ${loopSubcommand}. Available: render-hooks`,
|
|
1391
|
+
ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1396
1395
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
}
|
|
1396
|
+
function routePhasePlanIndex({ args, cwd, raw, error }) {
|
|
1397
|
+
phase.cmdPhasePlanIndex(cwd, args[1], raw);
|
|
1398
|
+
}
|
|
1401
1399
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
}
|
|
1400
|
+
function routeStateSnapshot({ args, cwd, raw, error }) {
|
|
1401
|
+
state.cmdStateSnapshot(cwd, raw);
|
|
1402
|
+
}
|
|
1406
1403
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
raw,
|
|
1414
|
-
error,
|
|
1415
|
-
});
|
|
1416
|
-
break;
|
|
1417
|
-
}
|
|
1404
|
+
function routeSummaryExtract({ args, cwd, raw, error }) {
|
|
1405
|
+
const summaryPath = args[1];
|
|
1406
|
+
const fieldsIndex = args.indexOf('--fields');
|
|
1407
|
+
const fields = fieldsIndex !== -1 ? args[fieldsIndex + 1].split(',') : null;
|
|
1408
|
+
commands.cmdSummaryExtract(cwd, summaryPath, fields, raw);
|
|
1409
|
+
}
|
|
1418
1410
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1411
|
+
async function routeWebsearch({ args, cwd, raw, error }) {
|
|
1412
|
+
const query = args[1];
|
|
1413
|
+
const limitIdx = args.indexOf('--limit');
|
|
1414
|
+
const freshnessIdx = args.indexOf('--freshness');
|
|
1415
|
+
await commands.cmdWebsearch(query, {
|
|
1416
|
+
limit: limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 10,
|
|
1417
|
+
freshness: freshnessIdx !== -1 ? args[freshnessIdx + 1] : null,
|
|
1418
|
+
}, raw);
|
|
1419
|
+
}
|
|
1429
1420
|
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
.filter((t) => t.length > 0);
|
|
1456
|
-
termsOverride = list.length > 0 ? { pluralization: list } : undefined;
|
|
1457
|
-
}
|
|
1458
|
-
const section = roadmap.getRoadmapPhaseWithFallback(cwd, phaseNum);
|
|
1459
|
-
const result = detectAssumptionDelta(section ?? '', termsOverride);
|
|
1460
|
-
output(result, raw);
|
|
1461
|
-
break;
|
|
1462
|
-
}
|
|
1463
|
-
error(`Unknown assumption-delta subcommand: ${sub}. Available: scan`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1464
|
-
break;
|
|
1465
|
-
}
|
|
1421
|
+
function routeWorkstream({ args, cwd, raw, error }) {
|
|
1422
|
+
const subcommand = args[1];
|
|
1423
|
+
if (subcommand === 'create') {
|
|
1424
|
+
const migrateNameIdx = args.indexOf('--migrate-name');
|
|
1425
|
+
const noMigrate = args.includes('--no-migrate');
|
|
1426
|
+
workstream.cmdWorkstreamCreate(cwd, args[2], {
|
|
1427
|
+
migrate: !noMigrate,
|
|
1428
|
+
migrateName: migrateNameIdx !== -1 ? args[migrateNameIdx + 1] : null,
|
|
1429
|
+
}, raw);
|
|
1430
|
+
} else if (subcommand === 'list') {
|
|
1431
|
+
workstream.cmdWorkstreamList(cwd, raw);
|
|
1432
|
+
} else if (subcommand === 'status') {
|
|
1433
|
+
workstream.cmdWorkstreamStatus(cwd, args[2], raw);
|
|
1434
|
+
} else if (subcommand === 'complete') {
|
|
1435
|
+
workstream.cmdWorkstreamComplete(cwd, args[2], {}, raw);
|
|
1436
|
+
} else if (subcommand === 'set') {
|
|
1437
|
+
workstream.cmdWorkstreamSet(cwd, args[2], raw);
|
|
1438
|
+
} else if (subcommand === 'get') {
|
|
1439
|
+
workstream.cmdWorkstreamGet(cwd, raw);
|
|
1440
|
+
} else if (subcommand === 'progress') {
|
|
1441
|
+
workstream.cmdWorkstreamProgress(cwd, raw);
|
|
1442
|
+
} else {
|
|
1443
|
+
error('Unknown workstream subcommand. Available: create, list, status, complete, set, get, progress', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1466
1446
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1447
|
+
function routeWorktree({ args, cwd, raw, error }) {
|
|
1448
|
+
const subcommand = args[1];
|
|
1449
|
+
const worktreeSafety = require('./lib/worktree-safety.cjs');
|
|
1450
|
+
if (subcommand === 'cleanup-wave') {
|
|
1451
|
+
worktreeSafety.cmdWorktreeCleanupWave(cwd, args.slice(2));
|
|
1452
|
+
} else if (subcommand === 'record-agent') {
|
|
1453
|
+
worktreeSafety.cmdWorktreeRecordAgent(cwd, args.slice(2));
|
|
1454
|
+
} else if (subcommand === 'reap-orphans') {
|
|
1455
|
+
worktreeSafety.cmdWorktreeReapOrphans(cwd);
|
|
1456
|
+
} else if (subcommand === 'base-check') {
|
|
1457
|
+
require('./lib/worktree-base-ref.cjs').cmdWorktreeBaseCheck(cwd, args.slice(2));
|
|
1458
|
+
} else if (subcommand === 'set-baseref') {
|
|
1459
|
+
require('./lib/worktree-base-ref.cjs').cmdWorktreeSetBaseRef(cwd, args.slice(2));
|
|
1460
|
+
} else {
|
|
1461
|
+
error('Unknown worktree subcommand. Available: cleanup-wave, record-agent, reap-orphans, base-check, set-baseref', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1476
1464
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1465
|
+
function routeDocsInit({ args, cwd, raw, error }) {
|
|
1466
|
+
// Phase 6 (#3575): dispatch via SDK executeForCjs when available.
|
|
1467
|
+
// SDK handler: docsInit in sdk/src/query/docs-init.ts.
|
|
1468
|
+
const handled = _dispatchNonFamily({
|
|
1469
|
+
registryCommand: 'docs-init',
|
|
1470
|
+
registryArgs: args.slice(1),
|
|
1471
|
+
legacyCommand: 'docs-init',
|
|
1472
|
+
legacyArgs: args.slice(1),
|
|
1473
|
+
cwd,
|
|
1474
|
+
raw,
|
|
1475
|
+
error,
|
|
1476
|
+
output: output,
|
|
1477
|
+
});
|
|
1478
|
+
if (!handled) docs.cmdDocsInit(cwd, raw);
|
|
1479
|
+
}
|
|
1483
1480
|
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1481
|
+
function routeLearnings({ args, cwd, raw, error }) {
|
|
1482
|
+
const subcommand = args[1];
|
|
1483
|
+
if (subcommand === 'list') {
|
|
1484
|
+
learnings.cmdLearningsList(raw);
|
|
1485
|
+
} else if (subcommand === 'query') {
|
|
1486
|
+
const tagIdx = args.indexOf('--tag');
|
|
1487
|
+
const tag = tagIdx !== -1 ? args[tagIdx + 1] : null;
|
|
1488
|
+
if (!tag) error('Usage: gsd-tools learnings query --tag <tag>', ERROR_REASON.USAGE);
|
|
1489
|
+
learnings.cmdLearningsQuery(tag, raw);
|
|
1490
|
+
} else if (subcommand === 'copy') {
|
|
1491
|
+
learnings.cmdLearningsCopy(cwd, raw);
|
|
1492
|
+
} else if (subcommand === 'prune') {
|
|
1493
|
+
const olderIdx = args.indexOf('--older-than');
|
|
1494
|
+
const olderThan = olderIdx !== -1 ? args[olderIdx + 1] : null;
|
|
1495
|
+
if (!olderThan) error('Usage: gsd-tools learnings prune --older-than <duration>', ERROR_REASON.USAGE);
|
|
1496
|
+
learnings.cmdLearningsPrune(olderThan, raw);
|
|
1497
|
+
} else if (subcommand === 'delete') {
|
|
1498
|
+
const id = args[2];
|
|
1499
|
+
if (!id) error('Usage: gsd-tools learnings delete <id>', ERROR_REASON.USAGE);
|
|
1500
|
+
learnings.cmdLearningsDelete(id, raw);
|
|
1501
|
+
} else {
|
|
1502
|
+
error('Unknown learnings subcommand. Available: list, query, copy, prune, delete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1494
1505
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1506
|
+
function routeWindows({ args, cwd, raw, error }) {
|
|
1507
|
+
// windows status | append | waive | fixed (issue #1950)
|
|
1508
|
+
// All subcommands emit JSON; `--raw` is accepted for forward-compat with
|
|
1509
|
+
// capture-stdout hooks but is a no-op (output shape is JSON in both modes).
|
|
1510
|
+
const subcommand = args[1];
|
|
1511
|
+
const rest = args.slice(2);
|
|
1512
|
+
try {
|
|
1513
|
+
if (subcommand === 'status') {
|
|
1514
|
+
brokenWindows.cmdWindowsStatus(cwd, { raw });
|
|
1515
|
+
} else if (subcommand === 'append') {
|
|
1516
|
+
brokenWindows.cmdWindowsAppend(cwd, rest, { raw });
|
|
1517
|
+
} else if (subcommand === 'waive') {
|
|
1518
|
+
brokenWindows.cmdWindowsWaive(cwd, rest, { raw });
|
|
1519
|
+
} else if (subcommand === 'fixed') {
|
|
1520
|
+
brokenWindows.cmdWindowsMarkFixed(cwd, rest, { raw });
|
|
1504
1521
|
} else {
|
|
1505
|
-
error(
|
|
1522
|
+
error(
|
|
1523
|
+
`Unknown windows subcommand: ${subcommand || '(none)'}. Available: status, append, waive, fixed`,
|
|
1524
|
+
ERROR_REASON.SDK_UNKNOWN_COMMAND,
|
|
1525
|
+
);
|
|
1506
1526
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
args,
|
|
1514
|
-
cwd,
|
|
1515
|
-
raw,
|
|
1516
|
-
output: output,
|
|
1517
|
-
error,
|
|
1518
|
-
});
|
|
1519
|
-
break;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
case 'progress': {
|
|
1523
|
-
const subcommand = args[1] || 'json';
|
|
1524
|
-
commands.cmdProgressRender(cwd, subcommand, raw);
|
|
1525
|
-
break;
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
case 'uat': {
|
|
1529
|
-
const subcommand = args[1];
|
|
1530
|
-
if (subcommand === 'render-checkpoint') {
|
|
1531
|
-
const uat = require('./lib/uat.cjs');
|
|
1532
|
-
const options = parseNamedArgs(args, ['file']);
|
|
1533
|
-
uat.cmdRenderCheckpoint(cwd, options, raw);
|
|
1534
|
-
} else if (subcommand === 'classify-coverage') {
|
|
1535
|
-
const coverage = require('./lib/coverage.cjs');
|
|
1536
|
-
const options = parseNamedArgs(args, ['summary', 'file']);
|
|
1537
|
-
coverage.cmdClassify(cwd, options, raw);
|
|
1538
|
-
} else {
|
|
1539
|
-
error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1527
|
+
} catch (e) {
|
|
1528
|
+
// WindowsError carries a REASON code; surface it through the structured
|
|
1529
|
+
// error path so tests can assert on the typed reason. `error()` calls
|
|
1530
|
+
// process.exit(1) internally so we never reach the fall-through.
|
|
1531
|
+
if (e && e.name === 'WindowsError' && typeof e.reason === 'string') {
|
|
1532
|
+
error(e.message || 'broken-windows error', e.reason);
|
|
1540
1533
|
}
|
|
1541
|
-
|
|
1534
|
+
// Non-WindowsError: surface the message verbatim and exit non-zero.
|
|
1535
|
+
error(`broken-windows: ${(e && e.message) ? e.message : String(e)}`, ERROR_REASON.UNKNOWN);
|
|
1542
1536
|
}
|
|
1537
|
+
}
|
|
1543
1538
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
}
|
|
1539
|
+
function routeTeamsStatus({ args, cwd, raw, error }) {
|
|
1540
|
+
const teamsStatus = require('./lib/teams-status.cjs');
|
|
1541
|
+
teamsStatus.cmdTeamsStatus(cwd, { active: args.includes('--active') });
|
|
1542
|
+
}
|
|
1549
1543
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
}
|
|
1544
|
+
async function routeDetectCustomFiles({ args, cwd, raw, error }) {
|
|
1545
|
+
const configDirIdx = args.indexOf('--config-dir');
|
|
1546
|
+
const configDir = configDirIdx !== -1 ? args[configDirIdx + 1] : null;
|
|
1547
|
+
if (!configDir) {
|
|
1548
|
+
error('Usage: gsd-tools detect-custom-files --config-dir <path>', ERROR_REASON.USAGE);
|
|
1549
|
+
}
|
|
1550
|
+
const resolvedConfigDir = path.resolve(configDir);
|
|
1551
|
+
if (!fs.existsSync(resolvedConfigDir)) {
|
|
1552
|
+
error(`Config directory not found: ${resolvedConfigDir}`, ERROR_REASON.USAGE);
|
|
1553
|
+
}
|
|
1561
1554
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
}
|
|
1555
|
+
const manifestPath = path.join(resolvedConfigDir, 'gsd-file-manifest.json');
|
|
1556
|
+
if (!fs.existsSync(manifestPath)) {
|
|
1557
|
+
// No manifest — cannot determine what is custom. Return empty list
|
|
1558
|
+
// (same behaviour as saveLocalPatches in install.js when no manifest).
|
|
1559
|
+
const out = { custom_files: [], custom_count: 0, manifest_found: false };
|
|
1560
|
+
process.stdout.write(JSON.stringify(out, null, 2));
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1571
1563
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
cwd,
|
|
1581
|
-
raw,
|
|
1582
|
-
error,
|
|
1583
|
-
});
|
|
1584
|
-
break;
|
|
1585
|
-
}
|
|
1564
|
+
let manifest;
|
|
1565
|
+
try {
|
|
1566
|
+
manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
|
|
1567
|
+
} catch {
|
|
1568
|
+
const out = { custom_files: [], custom_count: 0, manifest_found: false, error: 'manifest parse error' };
|
|
1569
|
+
process.stdout.write(JSON.stringify(out, null, 2));
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1586
1572
|
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1573
|
+
const manifestKeys = new Set(Object.keys(manifest.files || {}));
|
|
1574
|
+
|
|
1575
|
+
// GSD-managed directories to scan for user-added files. Whole-owned
|
|
1576
|
+
// roots are wiped recursively; shared runtime roots are pruned by the
|
|
1577
|
+
// same gsd-* top-level prefix used by install.js _removeGsdEntries.
|
|
1578
|
+
const GSD_WHOLE_MANAGED_DIRS = [
|
|
1579
|
+
'gsd-core',
|
|
1580
|
+
path.join('commands', 'gsd'),
|
|
1581
|
+
];
|
|
1582
|
+
const GSD_PREFIX_MANAGED_DIRS = [
|
|
1583
|
+
'agents',
|
|
1584
|
+
'hooks',
|
|
1585
|
+
'skills',
|
|
1586
|
+
];
|
|
1587
|
+
|
|
1588
|
+
function collectCustomFiles(dir, baseDir, manifestKeys, out) {
|
|
1589
|
+
if (!fs.existsSync(dir)) return;
|
|
1590
|
+
const stat = fs.statSync(dir);
|
|
1591
|
+
if (stat.isFile()) {
|
|
1592
|
+
const relPath = path.relative(baseDir, dir).replace(/\\/g, '/');
|
|
1593
|
+
if (!manifestKeys.has(relPath)) {
|
|
1594
|
+
out.push(relPath);
|
|
1595
|
+
}
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (!stat.isDirectory()) return;
|
|
1599
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1600
|
+
const fullPath = path.join(dir, entry.name);
|
|
1601
|
+
if (entry.isDirectory()) {
|
|
1602
|
+
collectCustomFiles(fullPath, baseDir, manifestKeys, out);
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
// Use forward slashes for cross-platform manifest key compatibility
|
|
1606
|
+
const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
1607
|
+
if (!manifestKeys.has(relPath)) {
|
|
1608
|
+
out.push(relPath);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1602
1611
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (activeCapEqArg) {
|
|
1610
|
-
const value = activeCapEqArg.slice('--active-cap='.length).trim();
|
|
1611
|
-
if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1612
|
-
loopActiveCap = value;
|
|
1613
|
-
} else if (activeCapIdx !== -1) {
|
|
1614
|
-
const value = args[activeCapIdx + 1];
|
|
1615
|
-
if (!value || value.startsWith('--')) {
|
|
1616
|
-
error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1612
|
+
|
|
1613
|
+
const customFiles = [];
|
|
1614
|
+
for (const managedDir of GSD_WHOLE_MANAGED_DIRS) {
|
|
1615
|
+
const absDir = path.join(resolvedConfigDir, managedDir);
|
|
1616
|
+
if (!fs.existsSync(absDir)) continue;
|
|
1617
|
+
collectCustomFiles(absDir, resolvedConfigDir, manifestKeys, customFiles);
|
|
1617
1618
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
const runtimeEqArg = args.find(arg => arg.startsWith('--runtime='));
|
|
1626
|
-
const runtimeIdx = args.indexOf('--runtime');
|
|
1627
|
-
if (runtimeEqArg) {
|
|
1628
|
-
const value = runtimeEqArg.slice('--runtime='.length).trim();
|
|
1629
|
-
if (!value) error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1630
|
-
loopRuntime = value;
|
|
1631
|
-
} else if (runtimeIdx !== -1) {
|
|
1632
|
-
const value = args[runtimeIdx + 1];
|
|
1633
|
-
if (!value || value.startsWith('--')) {
|
|
1634
|
-
error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1619
|
+
for (const managedDir of GSD_PREFIX_MANAGED_DIRS) {
|
|
1620
|
+
const absDir = path.join(resolvedConfigDir, managedDir);
|
|
1621
|
+
if (!fs.existsSync(absDir)) continue;
|
|
1622
|
+
for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
|
|
1623
|
+
if (!entry.name.startsWith('gsd-')) continue;
|
|
1624
|
+
collectCustomFiles(path.join(absDir, entry.name), resolvedConfigDir, manifestKeys, customFiles);
|
|
1625
|
+
}
|
|
1635
1626
|
}
|
|
1636
|
-
loopRuntime = value;
|
|
1637
|
-
}
|
|
1638
|
-
loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {
|
|
1639
|
-
configDir: loopConfigDir ? path.resolve(loopConfigDir) : undefined,
|
|
1640
|
-
activeCap: loopActiveCap,
|
|
1641
|
-
runtime: loopRuntime,
|
|
1642
|
-
});
|
|
1643
|
-
} else {
|
|
1644
|
-
error(
|
|
1645
|
-
`Unknown loop subcommand: ${loopSubcommand}. Available: render-hooks`,
|
|
1646
|
-
ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
|
|
1647
|
-
);
|
|
1648
|
-
}
|
|
1649
|
-
break;
|
|
1650
|
-
}
|
|
1651
1627
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1628
|
+
const out = {
|
|
1629
|
+
custom_files: customFiles,
|
|
1630
|
+
custom_count: customFiles.length,
|
|
1631
|
+
manifest_found: true,
|
|
1632
|
+
manifest_version: manifest.version || null,
|
|
1633
|
+
};
|
|
1634
|
+
process.stdout.write(JSON.stringify(out, null, 2));
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function routeFromGsd2({ args, cwd, raw, error }) {
|
|
1638
|
+
const gsd2Import = require('./lib/gsd2-import.cjs');
|
|
1639
|
+
gsd2Import.cmdFromGsd2(args.slice(1), cwd, raw);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
async function routePromptBudget({ args, cwd, raw, error }) {
|
|
1643
|
+
const promptBudget = require('./lib/prompt-budget.cjs');
|
|
1644
|
+
|
|
1645
|
+
// ── Collect multi-value --plan-file flags ──────────────────────────
|
|
1646
|
+
const planFiles = [];
|
|
1647
|
+
for (let i = 1; i < args.length; i++) {
|
|
1648
|
+
if (args[i] === '--plan-file' && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
1649
|
+
planFiles.push(args[i + 1]);
|
|
1650
|
+
i++;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// ── Parse single-value flags ───────────────────────────────────────
|
|
1655
|
+
const flagMap = new Map();
|
|
1656
|
+
for (let i = 1; i < args.length; i++) {
|
|
1657
|
+
const current = args[i];
|
|
1658
|
+
const next = args[i + 1];
|
|
1659
|
+
if (!current.startsWith('--')) continue;
|
|
1660
|
+
if (!next || next.startsWith('--')) {
|
|
1661
|
+
if (!flagMap.has(current)) flagMap.set(current, null);
|
|
1662
|
+
continue;
|
|
1680
1663
|
}
|
|
1681
|
-
|
|
1682
|
-
i++;
|
|
1664
|
+
if (!flagMap.has(current)) flagMap.set(current, next);
|
|
1665
|
+
i++;
|
|
1683
1666
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
return { scope: 'global', runtimeDir: process.env.GSD_HOME || os.homedir() };
|
|
1702
|
-
};
|
|
1703
|
-
// capabilities.strict_known_registries policy (null=permissive, []=lockdown, [hosts]=allowlist).
|
|
1704
|
-
// loadConfig's whitelist does not surface this key, so read config.json directly (drift-guard pattern);
|
|
1705
|
-
// undefined => the lifecycle's permissive default. The raw value is passed THROUGH verbatim — a
|
|
1706
|
-
// malformed (non-array, non-null) value must reach the trust gate so it can fail CLOSED, not be
|
|
1707
|
-
// silently downgraded to permissive here.
|
|
1708
|
-
const capReadStrict = () => {
|
|
1709
|
-
let cfgPath;
|
|
1710
|
-
try {
|
|
1711
|
-
const { planningDir } = require('./lib/planning-workspace.cjs');
|
|
1712
|
-
cfgPath = path.join(planningDir(cwd), 'config.json');
|
|
1713
|
-
} catch {
|
|
1714
|
-
return undefined; // cannot even resolve the project config dir — permissive default
|
|
1715
|
-
}
|
|
1716
|
-
if (!fs.existsSync(cfgPath)) return undefined; // no project config — permissive default
|
|
1717
|
-
let cfg;
|
|
1718
|
-
try {
|
|
1719
|
-
cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
1720
|
-
} catch {
|
|
1721
|
-
// Config is PRESENT but unreadable/unparseable: a security policy must not silently
|
|
1722
|
-
// downgrade to permissive. Fail CLOSED — lockdown ([]) blocks external installs (local
|
|
1723
|
-
// still allowed) until the config is fixed.
|
|
1724
|
-
return [];
|
|
1725
|
-
}
|
|
1726
|
-
if (cfg && cfg.capabilities && Object.prototype.hasOwnProperty.call(cfg.capabilities, 'strict_known_registries')) {
|
|
1727
|
-
return cfg.capabilities.strict_known_registries;
|
|
1728
|
-
}
|
|
1729
|
-
return undefined;
|
|
1730
|
-
};
|
|
1731
|
-
// Running GSD version (hard gate for engines.gsd at install/load); fail-closed to 0.0.0.
|
|
1732
|
-
// #1920: prefer the authoritative gsd-core/VERSION the installer writes for EVERY runtime
|
|
1733
|
-
// (gsd-core/bin/ -> ../VERSION), so installed layouts report the true version even when the
|
|
1734
|
-
// walked-up ../../package.json is the versionless CommonJS marker or the user's own project.
|
|
1735
|
-
// Fall back to the runtime-root package.json (dev/source tree), then fail-closed. Mirrors
|
|
1736
|
-
// readHostVersion() in capability-loader.cts.
|
|
1737
|
-
const capHostVersion = () => {
|
|
1738
|
-
const SEMVER_PREFIX = /^\d+\.\d+\.\d+/;
|
|
1739
|
-
try {
|
|
1740
|
-
const v = fs.readFileSync(path.join(__dirname, '..', 'VERSION'), 'utf8').trim();
|
|
1741
|
-
if (SEMVER_PREFIX.test(v)) return v;
|
|
1742
|
-
} catch { /* not an installed tree (no gsd-core/VERSION) */ }
|
|
1743
|
-
try {
|
|
1744
|
-
const pkg = require(path.join(__dirname, '..', '..', 'package.json')); // gsd-core/bin/ -> repo root is two up
|
|
1745
|
-
if (pkg && typeof pkg.version === 'string' && SEMVER_PREFIX.test(pkg.version)) return pkg.version;
|
|
1746
|
-
} catch { /* runtime root has no package.json */ }
|
|
1747
|
-
return '0.0.0';
|
|
1748
|
-
};
|
|
1749
|
-
// #1459: the USER-OWNED consent home (GSD_HOME||homedir()) where project-scope consent records
|
|
1750
|
-
// live — OUTSIDE any repo. SAME rule as the loader/consent-store path resolution so a record
|
|
1751
|
-
// written here is the record the loader checks.
|
|
1752
|
-
const capConsentHome = () => {
|
|
1753
|
-
const osMod = require('node:os');
|
|
1754
|
-
return process.env.GSD_HOME || osMod.homedir();
|
|
1755
|
-
};
|
|
1756
|
-
// #1459: realpath(cwd) — the canonical PROJECT ROOT used to bind/lookup a project consent
|
|
1757
|
-
// record (the consent store realpaths it too, so loader + CLI agree). Best-effort: cwd if the
|
|
1758
|
-
// path cannot be realpath'd (e.g. it does not exist yet).
|
|
1759
|
-
const capProjectRoot = () => {
|
|
1760
|
-
try { return fs.realpathSync(cwd); } catch { return cwd; }
|
|
1761
|
-
};
|
|
1762
|
-
// UX-2: run the best-effort pre-op crash-recovery sweep AND surface any warnings it reports
|
|
1763
|
-
// (e.g. a corrupt-present ledger, or a rollback that could not complete) on stderr. The previous
|
|
1764
|
-
// bare `try { reconcile } catch {}` discarded the report entirely, so corruption detected during
|
|
1765
|
-
// reconcile was invisible. We never abort on a reconcile warning here — the mutating op that
|
|
1766
|
-
// follows runs its own fail-closed checks — but the warning must be OBSERVABLE.
|
|
1767
|
-
// #1459 IC-03: pass scope + the user-owned consent home so a rollback that DELETES a committed/
|
|
1768
|
-
// half-committed PROJECT-scope entry whose bundle dir is gone also REVOKES the now-stale consent
|
|
1769
|
-
// record (an identical re-drop then stays inactive until re-consented). Global scope / no store →
|
|
1770
|
-
// reconcile revokes nothing.
|
|
1771
|
-
const capRunReconcile = (runtimeDir, lifecycle, scope) => {
|
|
1772
|
-
try {
|
|
1773
|
-
const report = lifecycle.reconcileCapabilities({ runtimeDir, scope, consentStoreDir: capConsentHome() });
|
|
1774
|
-
if (report && Array.isArray(report.warnings)) {
|
|
1775
|
-
for (const w of report.warnings) {
|
|
1776
|
-
try { process.stderr.write(`capability reconcile: ${w}\n`); } catch { /* best-effort */ }
|
|
1777
|
-
}
|
|
1667
|
+
const getFlag = (flag) => flagMap.get(flag) ?? null;
|
|
1668
|
+
|
|
1669
|
+
const budgetStr = getFlag('--budget');
|
|
1670
|
+
const instructionsFile = getFlag('--instructions-file');
|
|
1671
|
+
const roadmapFile = getFlag('--roadmap-file');
|
|
1672
|
+
const outputPromptFile = getFlag('--output-prompt');
|
|
1673
|
+
const outputMetadataFile = getFlag('--output-metadata');
|
|
1674
|
+
const safetyMarginStr = getFlag('--safety-margin-pct');
|
|
1675
|
+
const projectMdHeadLinesStr = getFlag('--project-md-head-lines');
|
|
1676
|
+
const projectFile = getFlag('--project-file');
|
|
1677
|
+
const contextFile = getFlag('--context-file');
|
|
1678
|
+
const researchFile = getFlag('--research-file');
|
|
1679
|
+
const requirementsFile = getFlag('--requirements-file');
|
|
1680
|
+
|
|
1681
|
+
// ── Validate required args ─────────────────────────────────────────
|
|
1682
|
+
if (!budgetStr) {
|
|
1683
|
+
throw new ExitError(1, 'Error: --budget <N> is required');
|
|
1778
1684
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
const configDirIdx = args.indexOf('--config-dir');
|
|
1783
|
-
let configDir = null;
|
|
1784
|
-
if (configDirIdx !== -1) {
|
|
1785
|
-
const configDirVal = args[configDirIdx + 1];
|
|
1786
|
-
// Validate that --config-dir has a following non-flag value.
|
|
1787
|
-
if (!configDirVal || configDirVal.startsWith('--')) {
|
|
1788
|
-
error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1685
|
+
const budget = parseInt(budgetStr, 10);
|
|
1686
|
+
if (!Number.isFinite(budget) || budget <= 0) {
|
|
1687
|
+
throw new ExitError(1, 'Error: --budget must be a positive integer');
|
|
1789
1688
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
const resolvedConfigDir = configDir ? path.resolve(configDir) : null;
|
|
1793
|
-
// --runtime <r> (#2003): explicit runtime override so the config-dir
|
|
1794
|
-
// resolution bypasses the persisted-runtime fallback. Dual-form like
|
|
1795
|
-
// --config-dir (--runtime X / --runtime=X).
|
|
1796
|
-
let stateRuntime = undefined;
|
|
1797
|
-
const stateRuntimeEqArg = args.find(arg => arg.startsWith('--runtime='));
|
|
1798
|
-
const stateRuntimeIdx = args.indexOf('--runtime');
|
|
1799
|
-
if (stateRuntimeEqArg) {
|
|
1800
|
-
const value = stateRuntimeEqArg.slice('--runtime='.length).trim();
|
|
1801
|
-
if (!value) error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1802
|
-
stateRuntime = value;
|
|
1803
|
-
} else if (stateRuntimeIdx !== -1) {
|
|
1804
|
-
const value = args[stateRuntimeIdx + 1];
|
|
1805
|
-
if (!value || value.startsWith('--')) {
|
|
1806
|
-
error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1689
|
+
if (!instructionsFile) {
|
|
1690
|
+
throw new ExitError(1, 'Error: --instructions-file <path> is required');
|
|
1807
1691
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
capabilityState.cmdCapabilityState(cwd, resolvedConfigDir, raw, { runtime: stateRuntime });
|
|
1811
|
-
} else if (capSubcommand === 'set') {
|
|
1812
|
-
// capability set <id> [--on|--off|--enable|--disable] [--gate <key>=<bool>]... [--config-dir <dir>] [--runtime <r>] [--scope <s>]
|
|
1813
|
-
const capId = args[2];
|
|
1814
|
-
if (!capId || capId.startsWith('--')) {
|
|
1815
|
-
error('Missing capability id for: capability set <id>', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1816
|
-
}
|
|
1817
|
-
// Parse --config-dir
|
|
1818
|
-
const setConfigDirIdx = args.indexOf('--config-dir');
|
|
1819
|
-
let setConfigDir = null;
|
|
1820
|
-
if (setConfigDirIdx !== -1) {
|
|
1821
|
-
const setConfigDirVal = args[setConfigDirIdx + 1];
|
|
1822
|
-
if (!setConfigDirVal || setConfigDirVal.startsWith('--')) {
|
|
1823
|
-
error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1692
|
+
if (!roadmapFile) {
|
|
1693
|
+
throw new ExitError(1, 'Error: --roadmap-file <path> is required');
|
|
1824
1694
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
if (!gateVal || gateVal.startsWith('--')) {
|
|
1846
|
-
error('Missing value for --gate (expected <key>=<true|false>)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1695
|
+
if (planFiles.length === 0) {
|
|
1696
|
+
throw new ExitError(1, 'Error: at least one --plan-file <path> is required');
|
|
1697
|
+
}
|
|
1698
|
+
if (!outputPromptFile) {
|
|
1699
|
+
throw new ExitError(1, 'Error: --output-prompt <path> is required');
|
|
1700
|
+
}
|
|
1701
|
+
if (!outputMetadataFile) {
|
|
1702
|
+
throw new ExitError(1, 'Error: --output-metadata <path> is required');
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// ── Validate and read required files ──────────────────────────────
|
|
1706
|
+
async function readRequired(filePath, flagName) {
|
|
1707
|
+
const resolved = path.resolve(filePath);
|
|
1708
|
+
try {
|
|
1709
|
+
return await fs.promises.readFile(resolved, 'utf8');
|
|
1710
|
+
} catch (err) {
|
|
1711
|
+
if (err && err.code === 'ENOENT') {
|
|
1712
|
+
throw new ExitError(1, `Error: file not found for ${flagName}: ${resolved}`);
|
|
1713
|
+
}
|
|
1714
|
+
throw new ExitError(1, `Error: cannot read file for ${flagName}: ${resolved}`);
|
|
1847
1715
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
async function readOptional(filePath) {
|
|
1719
|
+
if (!filePath) return null;
|
|
1720
|
+
const resolved = path.resolve(filePath);
|
|
1721
|
+
try {
|
|
1722
|
+
return await fs.promises.readFile(resolved, 'utf8');
|
|
1723
|
+
} catch (err) {
|
|
1724
|
+
if (err && err.code === 'ENOENT') return null;
|
|
1725
|
+
throw new ExitError(1, `Error: cannot read optional file: ${resolved}`);
|
|
1851
1726
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const instructions = await readRequired(instructionsFile, '--instructions-file');
|
|
1730
|
+
const roadmap = await readRequired(roadmapFile, '--roadmap-file');
|
|
1731
|
+
const plans = await Promise.all(planFiles.map(async (p) => {
|
|
1732
|
+
const resolved = path.resolve(p);
|
|
1733
|
+
try {
|
|
1734
|
+
const content = await fs.promises.readFile(resolved, 'utf8');
|
|
1735
|
+
return { file: path.basename(p), content };
|
|
1736
|
+
} catch (err) {
|
|
1737
|
+
if (err && err.code === 'ENOENT') {
|
|
1738
|
+
throw new ExitError(1, `Error: plan file not found: ${resolved}`);
|
|
1739
|
+
}
|
|
1740
|
+
throw new ExitError(1, `Error: cannot read plan file: ${resolved}`);
|
|
1856
1741
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1742
|
+
}));
|
|
1743
|
+
|
|
1744
|
+
const projectMd = await readOptional(projectFile);
|
|
1745
|
+
const context = await readOptional(contextFile);
|
|
1746
|
+
const research = await readOptional(researchFile);
|
|
1747
|
+
const requirements = await readOptional(requirementsFile);
|
|
1748
|
+
|
|
1749
|
+
// ── Build options ─────────────────────────────────────────────────
|
|
1750
|
+
const options = {};
|
|
1751
|
+
if (safetyMarginStr !== null) {
|
|
1752
|
+
const pct = parseInt(safetyMarginStr, 10);
|
|
1753
|
+
if (Number.isFinite(pct)) options.safetyMarginPct = pct;
|
|
1859
1754
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
let setRuntime;
|
|
1864
|
-
if (runtimeIdx !== -1) {
|
|
1865
|
-
const runtimeVal = args[runtimeIdx + 1];
|
|
1866
|
-
if (!runtimeVal || runtimeVal.startsWith('--')) {
|
|
1867
|
-
error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1755
|
+
if (projectMdHeadLinesStr !== null) {
|
|
1756
|
+
const lines = parseInt(projectMdHeadLinesStr, 10);
|
|
1757
|
+
if (Number.isFinite(lines)) options.projectMdHeadLines = lines;
|
|
1868
1758
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1759
|
+
|
|
1760
|
+
// ── Call applyBudget ──────────────────────────────────────────────
|
|
1761
|
+
const sections = { instructions, roadmap, plans, projectMd, context, research, requirements };
|
|
1762
|
+
const { prompt, metadata } = promptBudget.applyBudget({ sections, budget, options });
|
|
1763
|
+
|
|
1764
|
+
// ── Write outputs ─────────────────────────────────────────────────
|
|
1765
|
+
await fs.promises.writeFile(path.resolve(outputMetadataFile), JSON.stringify(metadata, null, 2));
|
|
1766
|
+
await fs.promises.writeFile(path.resolve(outputPromptFile), prompt);
|
|
1767
|
+
|
|
1768
|
+
if (metadata.hardFailed) {
|
|
1769
|
+
throw new ExitError(2);
|
|
1877
1770
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
`
|
|
1906
|
-
ERROR_REASON ? ERROR_REASON.USAGE : undefined,
|
|
1907
|
-
);
|
|
1908
|
-
}
|
|
1909
|
-
capRunReconcile(runtimeDir, lifecycle, scope); // UX-2: surface reconcile warnings on stderr
|
|
1910
|
-
const res = await lifecycle.installCapability(spec, {
|
|
1911
|
-
runtimeDir,
|
|
1912
|
-
hostVersion: capHostVersion(),
|
|
1913
|
-
consentGranted: capHasFlag('--yes'),
|
|
1914
|
-
integrity: capFlagValue('--integrity'),
|
|
1915
|
-
sharedFiles: installSharedFiles,
|
|
1916
|
-
strictKnownRegistries: capReadStrict(),
|
|
1917
|
-
// #1459: bind a user consent record for a CONSENTED project install (under the user-owned
|
|
1918
|
-
// consent home, NOT in the repo). The lifecycle records nothing for global scope.
|
|
1919
|
-
scope,
|
|
1920
|
-
consentStoreDir: capConsentHome(),
|
|
1921
|
-
});
|
|
1922
|
-
if (res.status === 'installed') {
|
|
1923
|
-
output({
|
|
1924
|
-
status: 'installed',
|
|
1925
|
-
id: res.id,
|
|
1926
|
-
version: res.version,
|
|
1927
|
-
scope,
|
|
1928
|
-
disclosure: trust.summarizeDisclosure(res.disclosure || {}),
|
|
1929
|
-
}, raw);
|
|
1930
|
-
} else if (res.status === 'aborted') {
|
|
1931
|
-
// 'aborted' always means "executable surface needs consent" in the lifecycle contract —
|
|
1932
|
-
// match it regardless of the requiresConsent flag so a future aborted path can't fall
|
|
1933
|
-
// through to the generic "blocked: unknown reason" arm with a misleading message.
|
|
1934
|
-
const disclosure = trust.summarizeDisclosure(res.disclosure || {});
|
|
1935
|
-
// UX-5: emit a structured aborted envelope on STDOUT before the non-zero exit so automation
|
|
1936
|
-
// can detect the consent requirement programmatically. We throw ExitError (not error(),
|
|
1937
|
-
// which calls process.exit and would bypass the stdout-capture flush) so the buffered stdout
|
|
1938
|
-
// is flushed before exit; the human-readable guidance still lands on stderr.
|
|
1939
|
-
output({ status: 'aborted', requiresConsent: true, scope, disclosure }, raw);
|
|
1940
|
-
throw new ExitError(
|
|
1941
|
-
1,
|
|
1942
|
-
['Error: This capability declares executable surfaces and needs your consent before install:']
|
|
1943
|
-
.concat(disclosure.map((l) => ' ' + l))
|
|
1944
|
-
.concat(['Re-run with --yes to grant consent and install.'])
|
|
1945
|
-
.join('\n'),
|
|
1946
|
-
);
|
|
1947
|
-
} else {
|
|
1948
|
-
error(
|
|
1949
|
-
`capability install blocked: ${(res.blockReasons || ['unknown reason']).join('; ')}`,
|
|
1950
|
-
ERROR_REASON ? ERROR_REASON.SDK_FAIL_FAST : undefined,
|
|
1951
|
-
);
|
|
1952
|
-
}
|
|
1953
|
-
} else if (capSubcommand === 'update') {
|
|
1954
|
-
// capability update [<id> | --all] [--scope global|project] [--yes] [--shared-file <rel>]…
|
|
1955
|
-
const all = capHasFlag('--all');
|
|
1956
|
-
const id = args[2] && !args[2].startsWith('--') ? args[2] : undefined;
|
|
1957
|
-
if (!all && !id) {
|
|
1958
|
-
error('capability update requires <id> or --all', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1959
|
-
}
|
|
1960
|
-
if (all && id) {
|
|
1961
|
-
error('capability update: pass either <id> or --all, not both', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1962
|
-
}
|
|
1963
|
-
const { scope, runtimeDir } = capResolveScope(capFlagValue('--scope'));
|
|
1964
|
-
const lifecycle = require('./lib/capability-lifecycle.cjs');
|
|
1965
|
-
const ledgerMod = require('./lib/capability-ledger.cjs');
|
|
1966
|
-
const trust = require('./lib/capability-trust.cjs');
|
|
1967
|
-
// Finding 4 (MEDIUM): parse the --shared-file list ONCE and enforce MAX_SHARED_FILES BEFORE
|
|
1968
|
-
// the pre-op reconcile (install has this early guard; update did not — it ran reconcile, then
|
|
1969
|
-
// re-parsed --shared-file per entry inside upgradeOne). An over-cap update now fails fast with
|
|
1970
|
-
// a clear count error and leaves no reconcile side-effects, mirroring the install dispatch.
|
|
1971
|
-
const updateSharedFiles = capRepeatedFlag('--shared-file');
|
|
1972
|
-
if (updateSharedFiles.length > ledgerMod.MAX_SHARED_FILES) {
|
|
1973
|
-
error(
|
|
1974
|
-
`capability update blocked: too many --shared-file entries: ${updateSharedFiles.length} ` +
|
|
1975
|
-
`exceeds the maximum of ${ledgerMod.MAX_SHARED_FILES}.`,
|
|
1976
|
-
ERROR_REASON ? ERROR_REASON.USAGE : undefined,
|
|
1977
|
-
);
|
|
1978
|
-
}
|
|
1979
|
-
capRunReconcile(runtimeDir, lifecycle, scope); // UX-2: surface reconcile warnings on stderr
|
|
1980
|
-
// readLedgerStrict: returns null when MISSING (no installs yet), throws CorruptLedgerError
|
|
1981
|
-
// when the ledger FILE EXISTS but is unparseable. Using the strict variant ensures a
|
|
1982
|
-
// corrupt-but-present ledger fails closed rather than silently reporting not_installed (<id>)
|
|
1983
|
-
// or succeeding with an empty list (--all), both of which bypass fail-closed (Codex pass 3 M2).
|
|
1984
|
-
let ledger;
|
|
1985
|
-
try {
|
|
1986
|
-
ledger = ledgerMod.readLedgerStrict(runtimeDir);
|
|
1987
|
-
} catch (err) {
|
|
1988
|
-
error(`capability update blocked: ${err.message}`, ERROR_REASON ? ERROR_REASON.SDK_FAIL_FAST : undefined);
|
|
1989
|
-
}
|
|
1990
|
-
const entries = (ledger && ledger.entries) || {};
|
|
1991
|
-
const upgradeOne = async (capId) => {
|
|
1992
|
-
const entry = entries[capId];
|
|
1993
|
-
if (!entry) return { id: capId, status: 'not_installed' };
|
|
1994
|
-
// expectedId pins the op to the requested id: a retargeted/edited source that now resolves
|
|
1995
|
-
// to a different manifest id is refused by the lifecycle rather than upgrading the wrong cap.
|
|
1996
|
-
const r = await lifecycle.upgradeCapability(entry.source, {
|
|
1997
|
-
runtimeDir,
|
|
1998
|
-
hostVersion: capHostVersion(),
|
|
1999
|
-
consentGranted: capHasFlag('--yes'),
|
|
2000
|
-
sharedFiles: updateSharedFiles, // finding 4: parsed once, count-checked before reconcile
|
|
2001
|
-
strictKnownRegistries: capReadStrict(),
|
|
2002
|
-
expectedId: capId,
|
|
2003
|
-
// #1459: re-record the project consent for the upgraded bundle (new integrity/signature).
|
|
2004
|
-
scope,
|
|
2005
|
-
consentStoreDir: capConsentHome(),
|
|
2006
|
-
});
|
|
2007
|
-
// UX-6: normalize absent fields to explicit null so a not_installed/blocked row serializes
|
|
2008
|
-
// them as null rather than omitting them (JSON.stringify drops undefined keys), giving a
|
|
2009
|
-
// stable per-entry shape for `--all` consumers.
|
|
2010
|
-
return {
|
|
2011
|
-
id: capId,
|
|
2012
|
-
status: r.status,
|
|
2013
|
-
fromVersion: r.fromVersion ?? null,
|
|
2014
|
-
toVersion: r.toVersion ?? null,
|
|
2015
|
-
requiresConsent: r.requiresConsent ?? null,
|
|
2016
|
-
blockReasons: r.blockReasons ?? null,
|
|
2017
|
-
disclosure: r.disclosure ? trust.summarizeDisclosure(r.disclosure) : null,
|
|
2018
|
-
};
|
|
2019
|
-
};
|
|
2020
|
-
if (all) {
|
|
2021
|
-
// Sequential by design: each upgrade takes the per-scope capability lock; parallel
|
|
2022
|
-
// runs would contend on the ledger/lock (mirrors the worktree config.lock policy).
|
|
2023
|
-
const results = [];
|
|
2024
|
-
for (const capId of Object.keys(entries)) {
|
|
2025
|
-
results.push(await upgradeOne(capId));
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function routeUpdateContext({ args, cwd, raw, error }) {
|
|
1774
|
+
// #498: resolve the installed GSD version, scope, runtime, and config dir
|
|
1775
|
+
// for /gsd:update. Replaces ~280 lines of inline bash in update.md with a
|
|
1776
|
+
// tested projection. Emits the contract as JSON: { installedVersion,
|
|
1777
|
+
// scope, runtime, gsdDir }. Optional --config-dir / --runtime carry the
|
|
1778
|
+
// workflow's execution_context hints (the one thing only it can know).
|
|
1779
|
+
const { loadUpdateContext } = require('./lib/update-context.cjs');
|
|
1780
|
+
const ucArgs = args.slice(1);
|
|
1781
|
+
let preferredConfigDir = '';
|
|
1782
|
+
let preferredRuntime = '';
|
|
1783
|
+
for (let i = 0; i < ucArgs.length; i++) {
|
|
1784
|
+
const a = ucArgs[i];
|
|
1785
|
+
if (a.startsWith('--config-dir=')) { preferredConfigDir = a.slice('--config-dir='.length); continue; }
|
|
1786
|
+
if (a.startsWith('--runtime=')) { preferredRuntime = a.slice('--runtime='.length); continue; }
|
|
1787
|
+
if (a === '--config-dir') {
|
|
1788
|
+
const v = ucArgs[i + 1];
|
|
1789
|
+
if (v === undefined || v.startsWith('--')) error('Missing value for --config-dir', ERROR_REASON.USAGE);
|
|
1790
|
+
preferredConfigDir = v; i++; continue;
|
|
1791
|
+
}
|
|
1792
|
+
if (a === '--runtime') {
|
|
1793
|
+
const v = ucArgs[i + 1];
|
|
1794
|
+
if (v === undefined || v.startsWith('--')) error('Missing value for --runtime', ERROR_REASON.USAGE);
|
|
1795
|
+
preferredRuntime = v; i++; continue;
|
|
1796
|
+
}
|
|
1797
|
+
if (a === '--json') continue; // JSON is the only output; accepted for symmetry
|
|
1798
|
+
if (a.startsWith('-')) error(`Unknown flag for update-context: ${a}`, ERROR_REASON.USAGE);
|
|
2026
1799
|
}
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
1,
|
|
2038
|
-
`Error: capability update --all: ${failed.length} of ${results.length} did not upgrade ` +
|
|
2039
|
-
`(see the JSON result on stdout for per-capability status).`,
|
|
2040
|
-
);
|
|
1800
|
+
const ctx = loadUpdateContext({ preferredConfigDir, preferredRuntime });
|
|
1801
|
+
process.stdout.write(JSON.stringify(ctx) + '\n');
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
async function routeClassifyConfidence({ args, cwd, raw, error }) {
|
|
1805
|
+
const researchProvider = require('./lib/research-provider.cjs');
|
|
1806
|
+
const providerIdx = args.indexOf('--provider');
|
|
1807
|
+
const provider = providerIdx !== -1 ? args[providerIdx + 1] : null;
|
|
1808
|
+
if (!provider || provider.startsWith('--')) {
|
|
1809
|
+
error('Usage: gsd-tools query classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]', ERROR_REASON.USAGE);
|
|
2041
1810
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
const
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
.join('\n'),
|
|
2057
|
-
ERROR_REASON ? ERROR_REASON.USAGE : undefined,
|
|
2058
|
-
);
|
|
2059
|
-
} else {
|
|
2060
|
-
error(`capability update blocked: ${(r.blockReasons || ['unknown reason']).join('; ')}`, ERROR_REASON ? ERROR_REASON.SDK_FAIL_FAST : undefined);
|
|
1811
|
+
const verified = args.includes('--verified');
|
|
1812
|
+
const pkgIdx = args.indexOf('--package');
|
|
1813
|
+
const pkg = pkgIdx !== -1 ? args[pkgIdx + 1] : null;
|
|
1814
|
+
const ecoIdx = args.indexOf('--ecosystem');
|
|
1815
|
+
const ecosystem = ecoIdx !== -1 ? args[ecoIdx + 1] : null;
|
|
1816
|
+
let legitimacyVerdict = null;
|
|
1817
|
+
if (pkg && (!pkg.startsWith('--'))) {
|
|
1818
|
+
const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
|
|
1819
|
+
if (!ecosystem || ecosystem.startsWith('--') || !VALID_ECOSYSTEMS.has(ecosystem)) {
|
|
1820
|
+
error('Usage: gsd-tools query classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]', ERROR_REASON.USAGE);
|
|
1821
|
+
}
|
|
1822
|
+
const pkgLegitimacy = require('./lib/package-legitimacy.cjs');
|
|
1823
|
+
const results = await pkgLegitimacy.checkPackages({ ecosystem, packages: [pkg] }, {});
|
|
1824
|
+
legitimacyVerdict = results[0] ? results[0].verdict : null;
|
|
2061
1825
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
const ledgerMod = require('./lib/capability-ledger.cjs');
|
|
2072
|
-
capRunReconcile(runtimeDir, lifecycle, scope); // UX-2: surface reconcile warnings on stderr
|
|
2073
|
-
// Ledger first: an installed overlay is removable even if its id shadows a first-party name.
|
|
2074
|
-
// Only when the id is NOT an installed overlay do we reject a first-party id (vs. a typo).
|
|
2075
|
-
// Use readLedgerStrict so a corrupt-but-present ledger surfaces corruption here rather than
|
|
2076
|
-
// silently reporting "first-party cannot be removed" for any id (finding 7).
|
|
2077
|
-
let removeLedger;
|
|
2078
|
-
try {
|
|
2079
|
-
removeLedger = ledgerMod.readLedgerStrict(runtimeDir);
|
|
2080
|
-
} catch (err) {
|
|
2081
|
-
error(`capability remove blocked: ${err.message}`, ERROR_REASON ? ERROR_REASON.SDK_FAIL_FAST : undefined);
|
|
2082
|
-
}
|
|
2083
|
-
const inLedger = !!(removeLedger && removeLedger.entries && Object.prototype.hasOwnProperty.call(removeLedger.entries, id));
|
|
2084
|
-
if (!inLedger) {
|
|
2085
|
-
const base = require('./lib/capability-loader.cjs').loadRegistry();
|
|
2086
|
-
if (base && base.capabilities && Object.prototype.hasOwnProperty.call(base.capabilities, id)) {
|
|
2087
|
-
error(`"${id}" is a first-party capability and cannot be removed here; use the product uninstaller (gsd --uninstall)`, ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1826
|
+
const confidence = researchProvider.classifyConfidence({ provider, verifiedAgainstOfficial: verified, legitimacyVerdict });
|
|
1827
|
+
output({ provider, package: pkg || null, ecosystem: ecosystem || null, legitimacyVerdict, verified, confidence }, raw);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
async function routePackageLegitimacy({ args, cwd, raw, error }) {
|
|
1831
|
+
const pkgLegitimacy = require('./lib/package-legitimacy.cjs');
|
|
1832
|
+
const subcommand = args[1];
|
|
1833
|
+
if (subcommand !== 'check') {
|
|
1834
|
+
error('Unknown package-legitimacy subcommand. Available: check', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2088
1835
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
// bundle of the same id cannot silently re-activate against a stale consent.
|
|
2095
|
-
scope,
|
|
2096
|
-
consentStoreDir: capConsentHome(),
|
|
2097
|
-
});
|
|
2098
|
-
if (res.status === 'removed') {
|
|
2099
|
-
// #1459 finding 3: a project removal whose consent revoke FAILED (e.g. the consent-store lock
|
|
2100
|
-
// could not be acquired) is a NON-CLEAN removal — the bundle/ledger are gone but a STALE consent
|
|
2101
|
-
// record remains. Surface it on stderr + in the JSON so the user knows to clear it.
|
|
2102
|
-
if (res.consentRevokeFailed) {
|
|
2103
|
-
process.stderr.write(`warning: ${res.consentRevokeWarning || `consent record for "${id}" could not be revoked; clear it with: gsd capability trust revoke ${id}`}\n`);
|
|
1836
|
+
const ecoIdx = args.indexOf('--ecosystem');
|
|
1837
|
+
const ecosystem = ecoIdx !== -1 ? args[ecoIdx + 1] : null;
|
|
1838
|
+
const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
|
|
1839
|
+
if (!ecosystem || !VALID_ECOSYSTEMS.has(ecosystem)) {
|
|
1840
|
+
error('Usage: gsd-tools package-legitimacy check --ecosystem <npm|pypi|crates> <pkg1> ...', ERROR_REASON.USAGE);
|
|
2104
1841
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
}, raw);
|
|
2115
|
-
} else if (res.status === 'not_installed') {
|
|
2116
|
-
error(`capability "${id}" is not installed in ${scope} scope`, ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
2117
|
-
} else {
|
|
2118
|
-
error(`capability remove blocked: ${(res.blockReasons || ['unknown reason']).join('; ')}`, ERROR_REASON ? ERROR_REASON.SDK_FAIL_FAST : undefined);
|
|
2119
|
-
}
|
|
2120
|
-
} else if (capSubcommand === 'list') {
|
|
2121
|
-
// capability list [--json] [--scope global|project] — emits a JSON array of capability descriptors.
|
|
2122
|
-
// When --scope is given, only that scope's overlay ledger is read (finding 8: honor --scope so a
|
|
2123
|
-
// corrupt unrelated ledger in another scope does not block a scoped list).
|
|
2124
|
-
const loader = require('./lib/capability-loader.cjs');
|
|
2125
|
-
const ledgerMod = require('./lib/capability-ledger.cjs');
|
|
2126
|
-
const semver = require('./lib/semver-compare.cjs');
|
|
2127
|
-
const host = capHostVersion();
|
|
2128
|
-
const rows = [];
|
|
2129
|
-
const listScopeArg = capFlagValue('--scope');
|
|
2130
|
-
// Validate --scope if provided.
|
|
2131
|
-
if (listScopeArg && listScopeArg !== 'global' && listScopeArg !== 'project') {
|
|
2132
|
-
error(`Invalid --scope "${listScopeArg}": must be "global" or "project"`, ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
2133
|
-
}
|
|
2134
|
-
// First-party capabilities are always included (they have no scope concept).
|
|
2135
|
-
const base = loader.loadRegistry();
|
|
2136
|
-
const fp = (base && base.capabilities) || {};
|
|
2137
|
-
// #1459: consult the composed overlay's warnings so a DISCOVERED-BUT-INACTIVE project overlay
|
|
2138
|
-
// (a bundle whose project ledger looks committed but has no user consent record on THIS
|
|
2139
|
-
// machine) is marked status:'inactive' with a reason, instead of silently appearing active.
|
|
2140
|
-
// loadRegistry is non-throwing; a failure here just leaves rows un-annotated.
|
|
2141
|
-
const inactiveById = {};
|
|
2142
|
-
try {
|
|
2143
|
-
const composed = loader.loadRegistry({ includeInstalled: true, cwd });
|
|
2144
|
-
const overlayWarnings = (composed && composed._overlay && composed._overlay.warnings) || [];
|
|
2145
|
-
for (const w of overlayWarnings) {
|
|
2146
|
-
// #1459 IC-02: classify by the STRUCTURAL discriminant `kind`, not by matching the
|
|
2147
|
-
// human-readable reason prose (which is free to change without breaking this filter).
|
|
2148
|
-
if (w && typeof w.id === 'string' && w.kind === 'unconsented') {
|
|
2149
|
-
inactiveById[`${w.scope} ${w.id}`] = w.reason;
|
|
1842
|
+
// Collect positional package names.
|
|
1843
|
+
// Only --ecosystem takes a value. Every non-flag arg is a package name.
|
|
1844
|
+
// Any unknown --flag is a usage error (do not silently skip+consume the next arg).
|
|
1845
|
+
const packages = [];
|
|
1846
|
+
for (let i = 2; i < args.length; i++) {
|
|
1847
|
+
const a = args[i];
|
|
1848
|
+
if (a === '--ecosystem') { i++; continue; }
|
|
1849
|
+
if (a.startsWith('--')) {
|
|
1850
|
+
error(`package-legitimacy: unknown flag ${a}`, ERROR_REASON.USAGE);
|
|
2150
1851
|
}
|
|
1852
|
+
packages.push(a);
|
|
2151
1853
|
}
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
// SAME resolver `capability state` uses (resolveCapabilityRuntimeState), so
|
|
2155
|
-
// `list` and `state` stop disagreeing. `list` previously derived `status`
|
|
2156
|
-
// purely from ledger-entry existence — an installed-but-not-surfaced cap
|
|
2157
|
-
// reported active in `list` and absent in `state`. Surfaced is evaluated at
|
|
2158
|
-
// the default runtime config dir (the resolver resolves it when undefined),
|
|
2159
|
-
// matching `capability state <id>` with no --config-dir. Best-effort: a
|
|
2160
|
-
// resolver failure leaves surfacedById empty (rows report surfaced:null).
|
|
2161
|
-
const surfacedById = {};
|
|
2162
|
-
// surfacedById is keyed by capId only (NOT `${scope} ${capId}`): surface
|
|
2163
|
-
// state is single-source — one runtime config dir → one .gsd-surface.json
|
|
2164
|
-
// → one surfaced truth per capId — and the loader dedupes overlay caps to
|
|
2165
|
-
// one registry entry per id (first-party-wins). So a cap installed in both
|
|
2166
|
-
// scopes correctly shares one surfaced value across its list rows.
|
|
2167
|
-
try {
|
|
2168
|
-
const surfaceState = capabilityState.resolveCapabilityRuntimeState(cwd, undefined);
|
|
2169
|
-
for (const cap of (surfaceState && surfaceState.capabilities) || []) {
|
|
2170
|
-
if (cap && typeof cap.id === 'string') {
|
|
2171
|
-
surfacedById[cap.id] = cap.surfaced === true;
|
|
2172
|
-
}
|
|
1854
|
+
if (packages.length === 0) {
|
|
1855
|
+
error('Usage: gsd-tools package-legitimacy check --ecosystem <eco> <pkg1> <pkg2> ...', ERROR_REASON.USAGE);
|
|
2173
1856
|
}
|
|
2174
|
-
|
|
2175
|
-
for (const capId of Object.keys(fp)) {
|
|
2176
|
-
const cap = fp[capId] || {};
|
|
2177
|
-
rows.push({
|
|
2178
|
-
id: capId,
|
|
2179
|
-
role: cap.role || null,
|
|
2180
|
-
version: cap.version || null,
|
|
2181
|
-
tier: cap.tier || null,
|
|
2182
|
-
source: 'first-party',
|
|
2183
|
-
scope: 'first-party',
|
|
2184
|
-
status: 'active',
|
|
2185
|
-
surfaced: Object.prototype.hasOwnProperty.call(surfacedById, capId) ? surfacedById[capId] === true : null,
|
|
2186
|
-
title: cap.title || null,
|
|
2187
|
-
});
|
|
2188
|
-
}
|
|
2189
|
-
// Overlay scopes: honor --scope to read only the requested scope (finding 8).
|
|
2190
|
-
const overlayScopes = listScopeArg ? [listScopeArg] : ['global', 'project'];
|
|
2191
|
-
for (const sc of overlayScopes) {
|
|
2192
|
-
const { runtimeDir } = capResolveScope(sc);
|
|
2193
|
-
// readLedgerStrict: returns null when MISSING (no overlays yet), throws CorruptLedgerError
|
|
2194
|
-
// when the ledger FILE EXISTS but is unparseable. Using the strict variant ensures a
|
|
2195
|
-
// corrupt-but-present ledger is visible to the user (blocked/error) rather than silently
|
|
2196
|
-
// dropping overlay entries and returning a first-party-only list (site A fix, #1462).
|
|
2197
|
-
let ledger;
|
|
1857
|
+
let pkgResults;
|
|
2198
1858
|
try {
|
|
2199
|
-
|
|
2200
|
-
} catch (
|
|
2201
|
-
|
|
2202
|
-
error(`capability list blocked (${sc} scope): ${err.message}`, ERROR_REASON ? ERROR_REASON.SDK_FAIL_FAST : undefined);
|
|
1859
|
+
pkgResults = await pkgLegitimacy.checkPackages({ ecosystem, packages }, {});
|
|
1860
|
+
} catch (pkgErr) {
|
|
1861
|
+
error(`package-legitimacy: ${pkgErr && pkgErr.message ? pkgErr.message : String(pkgErr)}`, ERROR_REASON.UNKNOWN);
|
|
2203
1862
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
// surfaced:false by definition; otherwise defer to the resolver.
|
|
2238
|
-
surfaced: status === 'active'
|
|
2239
|
-
? (Object.prototype.hasOwnProperty.call(surfacedById, capId) ? surfacedById[capId] === true : null)
|
|
2240
|
-
: false,
|
|
2241
|
-
title: manifest.title || null,
|
|
2242
|
-
});
|
|
1863
|
+
output(pkgResults, raw);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function routeEffort({ args, cwd, raw, error }) {
|
|
1867
|
+
const subcommand = args[1];
|
|
1868
|
+
if (subcommand === 'sync') {
|
|
1869
|
+
const effortSyncArgs = args.slice(2);
|
|
1870
|
+
let dryRun = true;
|
|
1871
|
+
let effortSyncConfigDir;
|
|
1872
|
+
let effortSyncRuntime;
|
|
1873
|
+
for (let i = 0; i < effortSyncArgs.length; i++) {
|
|
1874
|
+
const a = effortSyncArgs[i];
|
|
1875
|
+
if (a === '--apply') { dryRun = false; continue; }
|
|
1876
|
+
if (a === '--dry-run') { dryRun = true; continue; }
|
|
1877
|
+
if (a.startsWith('--config-dir=')) { effortSyncConfigDir = a.slice('--config-dir='.length); continue; }
|
|
1878
|
+
if (a === '--config-dir') {
|
|
1879
|
+
const v = effortSyncArgs[i + 1];
|
|
1880
|
+
if (!v || v.startsWith('--')) error('Missing value for --config-dir', ERROR_REASON.USAGE);
|
|
1881
|
+
effortSyncConfigDir = v; i++; continue;
|
|
1882
|
+
}
|
|
1883
|
+
if (a.startsWith('--runtime=')) { effortSyncRuntime = a.slice('--runtime='.length); continue; }
|
|
1884
|
+
if (a === '--runtime') {
|
|
1885
|
+
const v = effortSyncArgs[i + 1];
|
|
1886
|
+
if (!v || v.startsWith('--')) error('Missing value for --runtime', ERROR_REASON.USAGE);
|
|
1887
|
+
effortSyncRuntime = v; i++; continue;
|
|
1888
|
+
}
|
|
1889
|
+
if (a === '--raw') continue;
|
|
1890
|
+
if (a.startsWith('-')) error(`Unknown flag for effort sync: ${a}`, ERROR_REASON.USAGE);
|
|
1891
|
+
error(`effort sync takes no positional arguments; got: ${a}`, ERROR_REASON.USAGE);
|
|
1892
|
+
}
|
|
1893
|
+
commands.cmdEffortSync(cwd, raw, { dryRun, configDir: effortSyncConfigDir, runtime: effortSyncRuntime });
|
|
1894
|
+
} else {
|
|
1895
|
+
error('Unknown effort subcommand. Available: sync', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2243
1896
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
}
|
|
2252
|
-
const dCfg = capFlagValue('--config-dir');
|
|
2253
|
-
capabilityWriter.cmdCapabilitySet(
|
|
2254
|
-
cwd,
|
|
2255
|
-
dCfg ? path.resolve(dCfg) : null,
|
|
2256
|
-
id,
|
|
2257
|
-
{ enabled: capSubcommand === 'enable', runtime: capFlagValue('--runtime'), scope: capFlagValue('--scope') },
|
|
2258
|
-
raw,
|
|
2259
|
-
);
|
|
2260
|
-
} else if (capSubcommand === 'outdated') {
|
|
2261
|
-
// capability outdated [--json] [--scope global|project] — ADR-1244 D6 "Update available?".
|
|
2262
|
-
// For each installed overlay in the chosen scope(s), LIGHT-PEEK its recorded source for the
|
|
2263
|
-
// latest available version and report whether a newer one exists. This never re-clones/re-packs;
|
|
2264
|
-
// a failing/unsupported peek DEGRADES that row to status 'unknown' (the verb never crashes).
|
|
2265
|
-
const lifecycle = require('./lib/capability-lifecycle.cjs');
|
|
2266
|
-
const outdatedScopeArg = capFlagValue('--scope');
|
|
2267
|
-
if (outdatedScopeArg && outdatedScopeArg !== 'global' && outdatedScopeArg !== 'project') {
|
|
2268
|
-
error(`Invalid --scope "${outdatedScopeArg}": must be "global" or "project"`, ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
2269
|
-
}
|
|
2270
|
-
// Honor --scope (read only that scope's ledger); default sweeps both, mirroring `list`.
|
|
2271
|
-
const outdatedScopes = outdatedScopeArg ? [outdatedScopeArg] : ['global', 'project'];
|
|
2272
|
-
const records = [];
|
|
2273
|
-
for (const sc of outdatedScopes) {
|
|
2274
|
-
const { runtimeDir } = capResolveScope(sc);
|
|
2275
|
-
// outdatedCapabilities is read-only + non-throwing (returns [] on a missing/corrupt ledger).
|
|
2276
|
-
const scRecords = lifecycle.outdatedCapabilities({ runtimeDir });
|
|
2277
|
-
for (const r of scRecords) records.push({ ...r, scope: sc });
|
|
2278
|
-
}
|
|
2279
|
-
const asJson = raw || capHasFlag('--json');
|
|
2280
|
-
if (asJson) {
|
|
2281
|
-
output(records, false); // machine output: the records array (JSON).
|
|
2282
|
-
} else {
|
|
2283
|
-
// Human-readable table: ID | Source | Current | Latest | Status.
|
|
2284
|
-
const headers = ['ID', 'Source', 'Current', 'Latest', 'Status'];
|
|
2285
|
-
const cell = (v) => (v === null || v === undefined ? '-' : String(v));
|
|
2286
|
-
const tableRows = records.map((r) => [cell(r.id), cell(r.sourceKind), cell(r.current), cell(r.latest), cell(r.status)]);
|
|
2287
|
-
const widths = headers.map((h, i) => Math.max(h.length, ...tableRows.map((row) => row[i].length), 0));
|
|
2288
|
-
const fmt = (row) => row.map((c, i) => c.padEnd(widths[i])).join(' ').replace(/\s+$/, '');
|
|
2289
|
-
const lines = [fmt(headers), widths.map((w) => '-'.repeat(w)).join(' ').replace(/\s+$/, '')];
|
|
2290
|
-
for (const row of tableRows) lines.push(fmt(row));
|
|
2291
|
-
if (tableRows.length === 0) lines.push('(no installed overlay capabilities)');
|
|
2292
|
-
output(records, true, lines.join('\n') + '\n');
|
|
2293
|
-
}
|
|
2294
|
-
} else if (capSubcommand === 'trust') {
|
|
2295
|
-
// capability trust list [--scope project] [--json]
|
|
2296
|
-
// capability trust revoke <id> [--project <path>]
|
|
2297
|
-
// The user-owned consent store (#1459) gates PROJECT-scope third-party capability activation.
|
|
2298
|
-
const consentMod = require('./lib/capability-consent.cjs');
|
|
2299
|
-
const trustSub = args[2];
|
|
2300
|
-
if (trustSub === 'list') {
|
|
2301
|
-
// --scope is accepted for symmetry; only 'project' records exist today.
|
|
2302
|
-
const listScope = capFlagValue('--scope');
|
|
2303
|
-
if (listScope && listScope !== 'project') {
|
|
2304
|
-
error(`Invalid --scope "${listScope}" for trust list: only "project" consent records exist`, ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function routeUserStory({ args, cwd, raw, error }) {
|
|
1900
|
+
const subcommand = args[1];
|
|
1901
|
+
if (subcommand !== 'validate') {
|
|
1902
|
+
error(`Unknown user-story subcommand: ${subcommand || '(none)'}. Available: validate`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1903
|
+
return;
|
|
2305
1904
|
}
|
|
2306
|
-
|
|
2307
|
-
const
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
1905
|
+
|
|
1906
|
+
const storyIdx = args.indexOf('--story');
|
|
1907
|
+
const story = (storyIdx !== -1 && args[storyIdx + 1] && !args[storyIdx + 1].startsWith('--'))
|
|
1908
|
+
? args[storyIdx + 1]
|
|
1909
|
+
: '';
|
|
1910
|
+
|
|
1911
|
+
// Canonical extraction regex — requires non-whitespace content in each slot
|
|
1912
|
+
// (\S.*? ensures the slot isn't whitespace-only).
|
|
1913
|
+
// Named groups: role / capability / outcome.
|
|
1914
|
+
const USER_STORY_RE = /^As a (\S.*?), I want to (\S.*?), so that (\S.*?)\.$/;
|
|
1915
|
+
|
|
1916
|
+
const errors = [];
|
|
1917
|
+
const trimmed = story.trim();
|
|
1918
|
+
let slots = null;
|
|
1919
|
+
|
|
1920
|
+
if (!trimmed) {
|
|
1921
|
+
errors.push('Story is empty. Required format: "As a [role], I want to [capability], so that [outcome]."');
|
|
1922
|
+
} else {
|
|
1923
|
+
// Per-clause guards produce targeted, actionable error messages before
|
|
1924
|
+
// attempting the full regex. Guards are ordered: role → capability → outcome → period.
|
|
1925
|
+
if (!/^As a \S/i.test(trimmed)) {
|
|
1926
|
+
errors.push('Story must start with "As a [user role]," (role must be non-empty).');
|
|
1927
|
+
}
|
|
1928
|
+
if (!/, I want to \S/i.test(trimmed)) {
|
|
1929
|
+
errors.push('Story must include ", I want to [capability]," (capability must be non-empty).');
|
|
1930
|
+
}
|
|
1931
|
+
if (!/, so that \S/i.test(trimmed)) {
|
|
1932
|
+
errors.push('Story must include ", so that [outcome]." (outcome must be non-empty).');
|
|
1933
|
+
}
|
|
1934
|
+
if (!trimmed.endsWith('.')) {
|
|
1935
|
+
errors.push('Story must end with a period (.).');
|
|
1936
|
+
}
|
|
1937
|
+
// Full-regex check only when per-clause guards all passed — avoids
|
|
1938
|
+
// redundant "format mismatch" noise on top of specific error messages.
|
|
1939
|
+
if (errors.length === 0) {
|
|
1940
|
+
const m = USER_STORY_RE.exec(trimmed);
|
|
1941
|
+
if (!m) {
|
|
1942
|
+
errors.push('Story does not match the canonical format: "As a [role], I want to [capability], so that [outcome]."');
|
|
1943
|
+
} else {
|
|
1944
|
+
slots = { role: m[1], capability: m[2], outcome: m[3] };
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
2324
1947
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
//
|
|
2332
|
-
//
|
|
2333
|
-
//
|
|
1948
|
+
|
|
1949
|
+
output({ valid: errors.length === 0, errors, slots }, raw);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
function routeDriftGuard({ args, cwd, raw, error }) {
|
|
1953
|
+
// ADR-22: deterministic authority resolution + severity classification.
|
|
1954
|
+
// Subcommands:
|
|
1955
|
+
// drift-guard authority → effective authority string
|
|
1956
|
+
// drift-guard severity --status <S> [--authority <A>] → {severity, hardBlock}
|
|
1957
|
+
const subcommand = args[1];
|
|
1958
|
+
|
|
1959
|
+
// Read config.json directly for both plan_review.source_grounding_authority
|
|
1960
|
+
// and intel.enabled. Neither key is in the config-loader.cjs whitelist that
|
|
1961
|
+
// config-loader.cjs's loadConfig() whitelist does not return; plan_review is only in config.cjs's private
|
|
1962
|
+
// buildConfig(), and intel is a federated capability config key.
|
|
1963
|
+
let configuredAuthority = 'grep';
|
|
1964
|
+
let intelEnabled = false;
|
|
2334
1965
|
try {
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
1966
|
+
const { planningDir } = require('./lib/planning-workspace.cjs');
|
|
1967
|
+
const cfgPath = require('path').join(planningDir(cwd), 'config.json');
|
|
1968
|
+
if (require('fs').existsSync(cfgPath)) {
|
|
1969
|
+
const rawCfg = JSON.parse(require('fs').readFileSync(cfgPath, 'utf-8'));
|
|
1970
|
+
if (rawCfg && rawCfg.plan_review && rawCfg.plan_review.source_grounding_authority) {
|
|
1971
|
+
configuredAuthority = String(rawCfg.plan_review.source_grounding_authority);
|
|
1972
|
+
}
|
|
1973
|
+
if (rawCfg && rawCfg.intel && rawCfg.intel.enabled === true) {
|
|
1974
|
+
intelEnabled = true;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
} catch {
|
|
1978
|
+
// not fatal — defaults apply
|
|
2342
1979
|
}
|
|
2343
|
-
output({ status: 'revoked', id, projectRoot, scope: 'project' }, raw);
|
|
2344
|
-
} else {
|
|
2345
|
-
error(
|
|
2346
|
-
`Unknown capability trust subcommand: ${trustSub}. Available: list, revoke`,
|
|
2347
|
-
ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
|
|
2348
|
-
);
|
|
2349
|
-
}
|
|
2350
|
-
} else {
|
|
2351
|
-
error(
|
|
2352
|
-
`Unknown capability subcommand: ${capSubcommand}. Available: install, update, remove, list, outdated, trust, disable, enable, state, set`,
|
|
2353
|
-
ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
|
|
2354
|
-
);
|
|
2355
|
-
}
|
|
2356
|
-
break;
|
|
2357
|
-
}
|
|
2358
1980
|
|
|
2359
|
-
|
|
2360
|
-
phase.cmdPhasePlanIndex(cwd, args[1], raw);
|
|
2361
|
-
break;
|
|
2362
|
-
}
|
|
1981
|
+
const effectiveAuthority = getEffectiveAuthority(configuredAuthority, intelEnabled);
|
|
2363
1982
|
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
1983
|
+
if (subcommand === 'authority') {
|
|
1984
|
+
// Pass rawValue as 3rd arg so --raw returns unquoted string (not JSON)
|
|
1985
|
+
output(effectiveAuthority, raw, effectiveAuthority);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
2368
1988
|
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
1989
|
+
if (subcommand === 'severity') {
|
|
1990
|
+
const statusIdx = args.indexOf('--status');
|
|
1991
|
+
const statusVal = statusIdx !== -1 ? args[statusIdx + 1] : undefined;
|
|
1992
|
+
if (!statusVal || statusVal.startsWith('--')) {
|
|
1993
|
+
error('drift-guard severity requires --status <VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE>', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const authIdx = args.indexOf('--authority');
|
|
1997
|
+
const authVal = authIdx !== -1 ? args[authIdx + 1] : undefined;
|
|
1998
|
+
const authorityForClassify = (authVal && !authVal.startsWith('--'))
|
|
1999
|
+
? authVal
|
|
2000
|
+
: effectiveAuthority;
|
|
2001
|
+
const result = classifyDriftSeverity({ status: statusVal, authority: authorityForClassify });
|
|
2002
|
+
output(result, raw);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2376
2005
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
limit: limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 10,
|
|
2383
|
-
freshness: freshnessIdx !== -1 ? args[freshnessIdx + 1] : null,
|
|
2384
|
-
}, raw);
|
|
2385
|
-
break;
|
|
2386
|
-
}
|
|
2006
|
+
error(
|
|
2007
|
+
`Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity`,
|
|
2008
|
+
ERROR_REASON.SDK_UNKNOWN_COMMAND,
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2387
2011
|
|
|
2388
|
-
case 'workstream': {
|
|
2389
|
-
const subcommand = args[1];
|
|
2390
|
-
if (subcommand === 'create') {
|
|
2391
|
-
const migrateNameIdx = args.indexOf('--migrate-name');
|
|
2392
|
-
const noMigrate = args.includes('--no-migrate');
|
|
2393
|
-
workstream.cmdWorkstreamCreate(cwd, args[2], {
|
|
2394
|
-
migrate: !noMigrate,
|
|
2395
|
-
migrateName: migrateNameIdx !== -1 ? args[migrateNameIdx + 1] : null,
|
|
2396
|
-
}, raw);
|
|
2397
|
-
} else if (subcommand === 'list') {
|
|
2398
|
-
workstream.cmdWorkstreamList(cwd, raw);
|
|
2399
|
-
} else if (subcommand === 'status') {
|
|
2400
|
-
workstream.cmdWorkstreamStatus(cwd, args[2], raw);
|
|
2401
|
-
} else if (subcommand === 'complete') {
|
|
2402
|
-
workstream.cmdWorkstreamComplete(cwd, args[2], {}, raw);
|
|
2403
|
-
} else if (subcommand === 'set') {
|
|
2404
|
-
workstream.cmdWorkstreamSet(cwd, args[2], raw);
|
|
2405
|
-
} else if (subcommand === 'get') {
|
|
2406
|
-
workstream.cmdWorkstreamGet(cwd, raw);
|
|
2407
|
-
} else if (subcommand === 'progress') {
|
|
2408
|
-
workstream.cmdWorkstreamProgress(cwd, raw);
|
|
2409
|
-
} else {
|
|
2410
|
-
error('Unknown workstream subcommand. Available: create, list, status, complete, set, get, progress', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2411
|
-
}
|
|
2412
|
-
break;
|
|
2413
|
-
}
|
|
2414
2012
|
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2013
|
+
const HOST_COMMAND_ROUTERS = {
|
|
2014
|
+
// Each entry wraps its `route*Command` router so it receives the module-scope
|
|
2015
|
+
// lib the old `case` arm passed, plus the per-dispatch context
|
|
2016
|
+
// { args, cwd, raw, error }. Closes over module-scope libs (state/phase/…)
|
|
2017
|
+
// exactly as the old inline arms did — byte-identical dispatch.
|
|
2018
|
+
state: (ctx) => routeStateCommand({ state, ...ctx }),
|
|
2019
|
+
phase: (ctx) => routePhaseCommand({ phase, ...ctx }),
|
|
2020
|
+
roadmap: (ctx) => routeRoadmapCommand({ roadmap, ...ctx }),
|
|
2021
|
+
verify: (ctx) => routeVerifyCommand({ verify, ...ctx }),
|
|
2022
|
+
// validate additionally binds the module-scope `output` emitter.
|
|
2023
|
+
validate: (ctx) => routeValidateCommand({ verify, output, ...ctx }),
|
|
2024
|
+
// init preserves the #1688 stale-bake warning (best-effort, swallowed) that
|
|
2025
|
+
// ran before the router in the old `case 'init':` arm.
|
|
2026
|
+
init: (ctx) => {
|
|
2027
|
+
try { warnIfStaleBake(ctx.cwd); } catch { /* guard must never break init */ }
|
|
2028
|
+
routeInitCommand({ init, ...ctx });
|
|
2029
|
+
},
|
|
2030
|
+
// capability → routeCapabilityCommand (ADR-2346 P2). The router is async
|
|
2031
|
+
// (install/upgrade/consent ops await the lifecycle); dispatchHostCommand
|
|
2032
|
+
// awaits it. The router imports its own io/cli-exit/deps, so no module
|
|
2033
|
+
// injection needed — it receives {args,cwd,raw} (+error, ignored).
|
|
2034
|
+
capability: routeCapabilityCommand,
|
|
2035
|
+
// ADR-2346 P3: resolve/git/config/research host routers. Each body was
|
|
2036
|
+
// relocated verbatim from its `case` arm to a module-scope function above.
|
|
2037
|
+
'resolve-model': routeResolveModel,
|
|
2038
|
+
'resolve-granularity': routeResolveGranularity,
|
|
2039
|
+
'resolve-execution': routeResolveExecution,
|
|
2040
|
+
git: routeGit,
|
|
2041
|
+
'config-ensure-section': routeConfigEnsureSection,
|
|
2042
|
+
'config-set': routeConfigSet,
|
|
2043
|
+
'config-set-model-profile': routeConfigSetModelProfile,
|
|
2044
|
+
'config-get': routeConfigGet,
|
|
2045
|
+
'config-new-project': routeConfigNewProject,
|
|
2046
|
+
'config-path': routeConfigPath,
|
|
2047
|
+
'migrate-config': routeMigrateConfig,
|
|
2048
|
+
'research-store': routeResearchStore,
|
|
2049
|
+
'research-plan': routeResearchPlan,
|
|
2050
|
+
// ADR-2346 P4: all remaining leaf commands
|
|
2051
|
+
'agent': routeAgent,
|
|
2052
|
+
'smart-entry': routeSmartEntry,
|
|
2053
|
+
'check': routeCheck,
|
|
2054
|
+
'find-phase': routeFindPhase,
|
|
2055
|
+
'commit': routeCommit,
|
|
2056
|
+
'check-commit': routeCheckCommit,
|
|
2057
|
+
'commit-to-subrepo': routeCommitToSubrepo,
|
|
2058
|
+
'pr-subrepo': routePrSubrepo,
|
|
2059
|
+
'verify-summary': routeVerifySummary,
|
|
2060
|
+
'template': routeTemplate,
|
|
2061
|
+
'task': routeTask,
|
|
2062
|
+
'frontmatter': routeFrontmatter,
|
|
2063
|
+
'eval': routeEval,
|
|
2064
|
+
'verification': routeVerification,
|
|
2065
|
+
'generate-slug': routeGenerateSlug,
|
|
2066
|
+
'current-timestamp': routeCurrentTimestamp,
|
|
2067
|
+
'project-instruction-file': routeProjectInstructionFile,
|
|
2068
|
+
'list-todos': routeListTodos,
|
|
2069
|
+
'list-seeds': routeListSeeds,
|
|
2070
|
+
'verify-path-exists': routeVerifyPathExists,
|
|
2071
|
+
'quick-tasks-append': routeQuickTasksAppend,
|
|
2072
|
+
'normalize-test-command': routeNormalizeTestCommand,
|
|
2073
|
+
'dispatch-should-flatten': routeDispatchShouldFlatten,
|
|
2074
|
+
'agent-skills': routeAgentSkills,
|
|
2075
|
+
'skill-manifest': routeSkillManifest,
|
|
2076
|
+
'history-digest': routeHistoryDigest,
|
|
2077
|
+
'phases': routePhases,
|
|
2078
|
+
'assumption-delta': routeAssumptionDelta,
|
|
2079
|
+
'requirements': routeRequirements,
|
|
2080
|
+
'gap-analysis': routeGapAnalysis,
|
|
2081
|
+
'milestone': routeMilestone,
|
|
2082
|
+
'progress': routeProgress,
|
|
2083
|
+
'uat': routeUat,
|
|
2084
|
+
'stats': routeStats,
|
|
2085
|
+
'todo': routeTodo,
|
|
2086
|
+
'scaffold': routeScaffold,
|
|
2087
|
+
'loop': routeLoop,
|
|
2088
|
+
'phase-plan-index': routePhasePlanIndex,
|
|
2089
|
+
'state-snapshot': routeStateSnapshot,
|
|
2090
|
+
'summary-extract': routeSummaryExtract,
|
|
2091
|
+
'websearch': routeWebsearch,
|
|
2092
|
+
'workstream': routeWorkstream,
|
|
2093
|
+
'worktree': routeWorktree,
|
|
2094
|
+
'docs-init': routeDocsInit,
|
|
2095
|
+
'learnings': routeLearnings,
|
|
2096
|
+
'teams-status': routeTeamsStatus,
|
|
2097
|
+
'detect-custom-files': routeDetectCustomFiles,
|
|
2098
|
+
'from-gsd2': routeFromGsd2,
|
|
2099
|
+
'prompt-budget': routePromptBudget,
|
|
2100
|
+
'update-context': routeUpdateContext,
|
|
2101
|
+
'classify-confidence': routeClassifyConfidence,
|
|
2102
|
+
'package-legitimacy': routePackageLegitimacy,
|
|
2103
|
+
'effort': routeEffort,
|
|
2104
|
+
'user-story': routeUserStory,
|
|
2105
|
+
'drift-guard': routeDriftGuard,
|
|
2106
|
+
'windows': routeWindows,
|
|
2107
|
+
};
|
|
2108
|
+
|
|
2109
|
+
// Returns true when consumed (suppress "Unknown command"), false to fall
|
|
2110
|
+
// through. Prototype-pollution-safe: own-property lookup rejects
|
|
2111
|
+
// `__proto__`/`constructor`/`prototype` command keys (same guard as
|
|
2112
|
+
// dispatchCapabilityCommand).
|
|
2113
|
+
async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext }) {
|
|
2114
|
+
if (
|
|
2115
|
+
command === '__proto__' ||
|
|
2116
|
+
command === 'constructor' ||
|
|
2117
|
+
command === 'prototype'
|
|
2118
|
+
) {
|
|
2119
|
+
return false;
|
|
2120
|
+
}
|
|
2121
|
+
if (!Object.prototype.hasOwnProperty.call(HOST_COMMAND_ROUTERS, command)) {
|
|
2122
|
+
return false;
|
|
2123
|
+
}
|
|
2124
|
+
const router = HOST_COMMAND_ROUTERS[command];
|
|
2125
|
+
if (typeof router !== 'function') return false;
|
|
2126
|
+
// `await` so async host routers (e.g. capability's install/upgrade ops)
|
|
2127
|
+
// complete before runCommand returns; sync routers pass through unchanged.
|
|
2128
|
+
await router({ args, cwd, raw, error, defaultValue, workstreamContext });
|
|
2129
|
+
return true; // consumed — don't emit "Unknown command"
|
|
2130
|
+
}
|
|
2433
2131
|
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
case 'docs-init': {
|
|
2437
|
-
// Phase 6 (#3575): dispatch via SDK executeForCjs when available.
|
|
2438
|
-
// SDK handler: docsInit in sdk/src/query/docs-init.ts.
|
|
2439
|
-
const handled = _dispatchNonFamily({
|
|
2440
|
-
registryCommand: 'docs-init',
|
|
2441
|
-
registryArgs: args.slice(1),
|
|
2442
|
-
legacyCommand: 'docs-init',
|
|
2443
|
-
legacyArgs: args.slice(1),
|
|
2444
|
-
cwd,
|
|
2445
|
-
raw,
|
|
2446
|
-
error,
|
|
2447
|
-
output: output,
|
|
2448
|
-
});
|
|
2449
|
-
if (!handled) docs.cmdDocsInit(cwd, raw);
|
|
2450
|
-
break;
|
|
2451
|
-
}
|
|
2132
|
+
// ─── Arg parsing helpers ──────────────────────────────────────────────────────
|
|
2452
2133
|
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2134
|
+
// ─── run-with-timeout (#2351) ─────────────────────────────────────────────────
|
|
2135
|
+
// Portable, coreutils-independent wall-clock cap for a spawned command. Replaces
|
|
2136
|
+
// the GNU-only `timeout <n> …` calls that were hardcoded across gsd
|
|
2137
|
+
// workflow/agent files: stock macOS ships neither `timeout` nor `gtimeout`, so
|
|
2138
|
+
// those calls exited 127 ("command not found") and a passing build/test was
|
|
2139
|
+
// misreported as a FAILURE. The resolution lives here ONCE — every call site
|
|
2140
|
+
// invokes `gsd_run run-with-timeout <secs> [--] <cmd> [args…]` instead of
|
|
2141
|
+
// hand-rolling a `command -v timeout` probe per file.
|
|
2142
|
+
//
|
|
2143
|
+
// Exit-code contract (kept identical to GNU `timeout` so the existing per-site
|
|
2144
|
+
// dispatch — `-eq 124` for timeout, `-eq 0` for pass, non-zero for fail — is
|
|
2145
|
+
// unchanged):
|
|
2146
|
+
// • command exits normally → exit with the command's own code
|
|
2147
|
+
// • wall-clock budget exceeded → exit 124
|
|
2148
|
+
// • command killed by a signal → exit 128+signum
|
|
2149
|
+
// • command not found / not exec → exit 127 / 126 (spawn ENOENT / EACCES)
|
|
2150
|
+
// • bad wrapper args → exit 2 (usage — a workflow-authoring bug)
|
|
2151
|
+
// • <secs> == 0 → run with NO timer (matches `timeout 0`)
|
|
2152
|
+
// • blank / negative / NaN <secs> → exit 2 (usage — fails SAFE, never unbounded)
|
|
2153
|
+
//
|
|
2154
|
+
// The wrapped command's argv is OPAQUE: this executes BEFORE gsd-tools' own
|
|
2155
|
+
// global-flag parsing (see main()), so a wrapped `--raw`/`--cwd`/`--pick` passes
|
|
2156
|
+
// through verbatim rather than being consumed by this dispatcher. stdio is
|
|
2157
|
+
// inherited so shell pipes (`echo x | gsd_run run-with-timeout …`) and redirects
|
|
2158
|
+
// keep working. No shell is spawned (argv array) — no injection surface beyond
|
|
2159
|
+
// the old `timeout … bash -c "$CMD"`.
|
|
2160
|
+
function runWithTimeout(argv) {
|
|
2161
|
+
const { spawn } = require('node:child_process');
|
|
2162
|
+
const os = require('node:os');
|
|
2163
|
+
|
|
2164
|
+
const USAGE = 'Usage: gsd_run run-with-timeout <seconds> [--] <command> [args...]';
|
|
2165
|
+
const usageError = (msg) => new ExitError(2, `run-with-timeout: ${msg}\n${USAGE}`);
|
|
2166
|
+
|
|
2167
|
+
const rawSecs = argv[0];
|
|
2168
|
+
if (rawSecs === undefined) throw usageError('missing <seconds>');
|
|
2169
|
+
// Accept a bare number or a GNU-style trailing `s` unit (the only unit callers
|
|
2170
|
+
// use). A blank/whitespace value is a USAGE ERROR — never a silent "no timer",
|
|
2171
|
+
// which would drop the wall-clock bound if a config value ever resolved to "".
|
|
2172
|
+
const secsText = String(rawSecs).trim().replace(/s$/, '');
|
|
2173
|
+
const secs = Number(secsText);
|
|
2174
|
+
if (secsText === '' || !Number.isFinite(secs) || secs < 0) {
|
|
2175
|
+
throw usageError(`invalid <seconds>: ${rawSecs}`);
|
|
2176
|
+
}
|
|
2480
2177
|
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2178
|
+
let i = 1;
|
|
2179
|
+
if (argv[i] === '--') i += 1; // optional POSIX end-of-options separator
|
|
2180
|
+
const cmd = argv[i];
|
|
2181
|
+
if (cmd === undefined) throw usageError('missing <command>');
|
|
2182
|
+
const cmdArgs = argv.slice(i + 1);
|
|
2183
|
+
|
|
2184
|
+
const isWin = process.platform === 'win32';
|
|
2185
|
+
// Detached (own process group) on POSIX so a timeout can reap the WHOLE tree —
|
|
2186
|
+
// a bare child.kill() misses grandchildren (e.g. a test runner's workers) and
|
|
2187
|
+
// would not actually bound the wall clock. Windows has no POSIX process
|
|
2188
|
+
// groups; a direct kill is the best portable option there.
|
|
2189
|
+
const detached = !isWin && secs > 0;
|
|
2190
|
+
const spawnFailureCode = (err) =>
|
|
2191
|
+
(err && err.code === 'ENOENT' ? 127 : err && err.code === 'EACCES' ? 126 : 125);
|
|
2192
|
+
// Node's setTimeout delay is a 32-bit signed ms int; a larger value silently
|
|
2193
|
+
// clamps to 1ms → a spurious immediate timeout. Cap the budget (~24.8 days).
|
|
2194
|
+
const timerMs = Math.min(Math.round(secs * 1000), 2 ** 31 - 1);
|
|
2195
|
+
|
|
2196
|
+
// Resolve with the numeric exit code — never process.exit() (banned by
|
|
2197
|
+
// n/no-process-exit). main() returns this code and runMain() maps it to
|
|
2198
|
+
// process.exitCode, so stdout/stderr flush and cleanup hooks still fire.
|
|
2199
|
+
return new Promise((resolve) => {
|
|
2200
|
+
let child;
|
|
2201
|
+
try {
|
|
2202
|
+
child = spawn(cmd, cmdArgs, { stdio: 'inherit', detached });
|
|
2203
|
+
} catch (err) {
|
|
2204
|
+
process.stderr.write(`run-with-timeout: ${cmd}: ${err && err.message ? err.message : 'failed to start'}\n`);
|
|
2205
|
+
resolve(spawnFailureCode(err));
|
|
2206
|
+
return;
|
|
2490
2207
|
}
|
|
2491
2208
|
|
|
2492
|
-
|
|
2493
|
-
// CJS-native: no SDK counterpart exists in the command registry.
|
|
2494
|
-
// detect-custom-files reads a gsd-file-manifest.json against the
|
|
2495
|
-
// live filesystem to identify user-added files. It is installer-specific
|
|
2496
|
-
// logic that has no async query equivalent in the SDK.
|
|
2497
|
-
//
|
|
2498
|
-
// Detect user-added files inside GSD-managed directories that are not
|
|
2499
|
-
// tracked in gsd-file-manifest.json. Used by the update workflow to back
|
|
2500
|
-
// up custom files before the installer wipes those directories.
|
|
2501
|
-
//
|
|
2502
|
-
// This replaces the fragile bash pattern:
|
|
2503
|
-
// MANIFEST_FILES=$(node -e "require('$RUNTIME_DIR/...')" 2>/dev/null)
|
|
2504
|
-
// ${filepath#$RUNTIME_DIR/} # unreliable path stripping
|
|
2505
|
-
// which silently returns CUSTOM_COUNT=0 when $RUNTIME_DIR is unset or
|
|
2506
|
-
// when the stripped path does not match the manifest key format (#1997).
|
|
2507
|
-
|
|
2508
|
-
case 'detect-custom-files': {
|
|
2509
|
-
const configDirIdx = args.indexOf('--config-dir');
|
|
2510
|
-
const configDir = configDirIdx !== -1 ? args[configDirIdx + 1] : null;
|
|
2511
|
-
if (!configDir) {
|
|
2512
|
-
error('Usage: gsd-tools detect-custom-files --config-dir <path>', ERROR_REASON.USAGE);
|
|
2513
|
-
}
|
|
2514
|
-
const resolvedConfigDir = path.resolve(configDir);
|
|
2515
|
-
if (!fs.existsSync(resolvedConfigDir)) {
|
|
2516
|
-
error(`Config directory not found: ${resolvedConfigDir}`, ERROR_REASON.USAGE);
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
const manifestPath = path.join(resolvedConfigDir, 'gsd-file-manifest.json');
|
|
2520
|
-
if (!fs.existsSync(manifestPath)) {
|
|
2521
|
-
// No manifest — cannot determine what is custom. Return empty list
|
|
2522
|
-
// (same behaviour as saveLocalPatches in install.js when no manifest).
|
|
2523
|
-
const out = { custom_files: [], custom_count: 0, manifest_found: false };
|
|
2524
|
-
process.stdout.write(JSON.stringify(out, null, 2));
|
|
2525
|
-
break;
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
let manifest;
|
|
2209
|
+
const killTree = (signal) => {
|
|
2529
2210
|
try {
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
if (!manifestKeys.has(relPath)) {
|
|
2572
|
-
out.push(relPath);
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2211
|
+
if (detached && child.pid) {
|
|
2212
|
+
try { process.kill(-child.pid, signal); return; } catch { /* group already gone */ }
|
|
2213
|
+
}
|
|
2214
|
+
child.kill(signal);
|
|
2215
|
+
} catch { /* already exited */ }
|
|
2216
|
+
};
|
|
2217
|
+
|
|
2218
|
+
let timedOut = false;
|
|
2219
|
+
let killTimer = null;
|
|
2220
|
+
// Backstop SIGKILL for a descendant that traps SIGTERM. The child keeps the
|
|
2221
|
+
// event loop alive until this fires, so it stays ref'd (not unref'd).
|
|
2222
|
+
const armEscalation = () => {
|
|
2223
|
+
if (!killTimer) killTimer = setTimeout(() => killTree('SIGKILL'), 3000);
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2226
|
+
const timer = secs > 0
|
|
2227
|
+
? setTimeout(() => { timedOut = true; killTree('SIGTERM'); armEscalation(); }, timerMs)
|
|
2228
|
+
: null;
|
|
2229
|
+
|
|
2230
|
+
// Forward an interrupt to the child tree rather than dying and orphaning it
|
|
2231
|
+
// (GNU `timeout` forwards received signals). Without this, SIGINT/SIGTERM to
|
|
2232
|
+
// the wrapper — Ctrl-C, CI cancellation — would leave the detached child
|
|
2233
|
+
// running unbounded with no supervisor left to enforce the cap.
|
|
2234
|
+
const onSignal = (sig) => { killTree(sig); armEscalation(); };
|
|
2235
|
+
const onSigint = () => onSignal('SIGINT');
|
|
2236
|
+
const onSigterm = () => onSignal('SIGTERM');
|
|
2237
|
+
process.on('SIGINT', onSigint);
|
|
2238
|
+
process.on('SIGTERM', onSigterm);
|
|
2239
|
+
|
|
2240
|
+
const finish = (exitCode) => {
|
|
2241
|
+
if (timer) clearTimeout(timer);
|
|
2242
|
+
if (killTimer) clearTimeout(killTimer);
|
|
2243
|
+
process.removeListener('SIGINT', onSigint);
|
|
2244
|
+
process.removeListener('SIGTERM', onSigterm);
|
|
2245
|
+
resolve(exitCode);
|
|
2246
|
+
};
|
|
2247
|
+
|
|
2248
|
+
child.on('error', (err) => {
|
|
2249
|
+
process.stderr.write(`run-with-timeout: ${cmd}: ${err && err.message ? err.message : 'failed to start'}\n`);
|
|
2250
|
+
finish(spawnFailureCode(err));
|
|
2251
|
+
});
|
|
2576
2252
|
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2253
|
+
child.on('exit', (code, signal) => {
|
|
2254
|
+
if (timedOut) {
|
|
2255
|
+
// The direct child exited on our SIGTERM, but a SIGTERM-trapping descendant
|
|
2256
|
+
// may still hold the inherited stdio — orphaning it would hang a captured
|
|
2257
|
+
// or piped gate. Reap the whole group SYNCHRONOUSLY here; the escalation
|
|
2258
|
+
// timer can't fire once we resolve and the loop drains.
|
|
2259
|
+
killTree('SIGKILL');
|
|
2260
|
+
finish(124); // matches GNU `timeout`
|
|
2261
|
+
return;
|
|
2582
2262
|
}
|
|
2583
|
-
|
|
2584
|
-
const
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
if (!entry.name.startsWith('gsd-')) continue;
|
|
2588
|
-
collectCustomFiles(path.join(absDir, entry.name), resolvedConfigDir, manifestKeys, customFiles);
|
|
2589
|
-
}
|
|
2263
|
+
if (signal) {
|
|
2264
|
+
const num = os.constants.signals[signal] || 0;
|
|
2265
|
+
finish(num ? 128 + num : 1); // bash's 128+signum convention
|
|
2266
|
+
return;
|
|
2590
2267
|
}
|
|
2268
|
+
finish(code == null ? 1 : code);
|
|
2269
|
+
});
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2591
2272
|
|
|
2592
|
-
|
|
2593
|
-
custom_files: customFiles,
|
|
2594
|
-
custom_count: customFiles.length,
|
|
2595
|
-
manifest_found: true,
|
|
2596
|
-
manifest_version: manifest.version || null,
|
|
2597
|
-
};
|
|
2598
|
-
process.stdout.write(JSON.stringify(out, null, 2));
|
|
2599
|
-
break;
|
|
2600
|
-
}
|
|
2273
|
+
// ─── CLI Router ───────────────────────────────────────────────────────────────
|
|
2601
2274
|
|
|
2602
|
-
|
|
2275
|
+
async function main() {
|
|
2276
|
+
let args = process.argv.slice(2);
|
|
2603
2277
|
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2278
|
+
// #2351: run-with-timeout bounds a spawned command's wall clock portably
|
|
2279
|
+
// (coreutils-independent). It MUST intercept HERE, before the global-flag
|
|
2280
|
+
// parsing below — the wrapped command's argv is opaque and may itself contain
|
|
2281
|
+
// --raw / --cwd / --pick that this dispatcher would otherwise consume.
|
|
2282
|
+
{
|
|
2283
|
+
let rwt = args;
|
|
2284
|
+
if (rwt[0] === 'query') rwt = rwt.slice(1);
|
|
2285
|
+
if (rwt[0] === 'run-with-timeout') {
|
|
2286
|
+
// Return the child's exit code; runMain() maps it to process.exitCode.
|
|
2287
|
+
return runWithTimeout(rwt.slice(1));
|
|
2608
2288
|
}
|
|
2289
|
+
}
|
|
2609
2290
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
// --project-file <path>
|
|
2628
|
-
// --context-file <path>
|
|
2629
|
-
// --research-file <path>
|
|
2630
|
-
// --requirements-file <path>
|
|
2631
|
-
//
|
|
2632
|
-
// Exit codes:
|
|
2633
|
-
// 0 success (trim or no-trim)
|
|
2634
|
-
// 1 invocation error (missing required arg, missing file, invalid budget)
|
|
2635
|
-
// 2 hardFailed: prompt cannot fit effective budget after trim policy
|
|
2636
|
-
|
|
2637
|
-
case 'prompt-budget': {
|
|
2638
|
-
const promptBudget = require('./lib/prompt-budget.cjs');
|
|
2639
|
-
|
|
2640
|
-
// ── Collect multi-value --plan-file flags ──────────────────────────
|
|
2641
|
-
const planFiles = [];
|
|
2642
|
-
for (let i = 1; i < args.length; i++) {
|
|
2643
|
-
if (args[i] === '--plan-file' && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
2644
|
-
planFiles.push(args[i + 1]);
|
|
2645
|
-
i++;
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2291
|
+
// --json-errors / GSD_JSON_ERRORS=1: when active, error() emits structured
|
|
2292
|
+
// JSON ({ ok: false, reason: <ERROR_REASON code>, message }) to stderr
|
|
2293
|
+
// instead of "Error: <text>". Lets test suites assert on typed reason codes
|
|
2294
|
+
// per CONTRIBUTING.md "Prohibited: Raw Text Matching" (#2974).
|
|
2295
|
+
//
|
|
2296
|
+
// Detect early — before any flag parsing that can fire error() — so even
|
|
2297
|
+
// --cwd and workstream-resolution failures emit structured stderr (#3310).
|
|
2298
|
+
// The argv splice must happen here too, otherwise the dispatcher below sees
|
|
2299
|
+
// "--json-errors" as an unknown command. Default off — human operators keep
|
|
2300
|
+
// their plain-text diagnostic.
|
|
2301
|
+
const jsonErrorsIdx = args.indexOf('--json-errors');
|
|
2302
|
+
if (jsonErrorsIdx !== -1) {
|
|
2303
|
+
setJsonErrorMode(true);
|
|
2304
|
+
args.splice(jsonErrorsIdx, 1);
|
|
2305
|
+
} else if (process.env.GSD_JSON_ERRORS === '1') {
|
|
2306
|
+
setJsonErrorMode(true);
|
|
2307
|
+
}
|
|
2648
2308
|
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
const budgetStr = getFlag('--budget');
|
|
2665
|
-
const instructionsFile = getFlag('--instructions-file');
|
|
2666
|
-
const roadmapFile = getFlag('--roadmap-file');
|
|
2667
|
-
const outputPromptFile = getFlag('--output-prompt');
|
|
2668
|
-
const outputMetadataFile = getFlag('--output-metadata');
|
|
2669
|
-
const safetyMarginStr = getFlag('--safety-margin-pct');
|
|
2670
|
-
const projectMdHeadLinesStr = getFlag('--project-md-head-lines');
|
|
2671
|
-
const projectFile = getFlag('--project-file');
|
|
2672
|
-
const contextFile = getFlag('--context-file');
|
|
2673
|
-
const researchFile = getFlag('--research-file');
|
|
2674
|
-
const requirementsFile = getFlag('--requirements-file');
|
|
2675
|
-
|
|
2676
|
-
// ── Validate required args ─────────────────────────────────────────
|
|
2677
|
-
if (!budgetStr) {
|
|
2678
|
-
throw new ExitError(1, 'Error: --budget <N> is required');
|
|
2679
|
-
}
|
|
2680
|
-
const budget = parseInt(budgetStr, 10);
|
|
2681
|
-
if (!Number.isFinite(budget) || budget <= 0) {
|
|
2682
|
-
throw new ExitError(1, 'Error: --budget must be a positive integer');
|
|
2683
|
-
}
|
|
2684
|
-
if (!instructionsFile) {
|
|
2685
|
-
throw new ExitError(1, 'Error: --instructions-file <path> is required');
|
|
2686
|
-
}
|
|
2687
|
-
if (!roadmapFile) {
|
|
2688
|
-
throw new ExitError(1, 'Error: --roadmap-file <path> is required');
|
|
2689
|
-
}
|
|
2690
|
-
if (planFiles.length === 0) {
|
|
2691
|
-
throw new ExitError(1, 'Error: at least one --plan-file <path> is required');
|
|
2692
|
-
}
|
|
2693
|
-
if (!outputPromptFile) {
|
|
2694
|
-
throw new ExitError(1, 'Error: --output-prompt <path> is required');
|
|
2695
|
-
}
|
|
2696
|
-
if (!outputMetadataFile) {
|
|
2697
|
-
throw new ExitError(1, 'Error: --output-metadata <path> is required');
|
|
2698
|
-
}
|
|
2309
|
+
// Optional cwd override for sandboxed subagents running outside project root.
|
|
2310
|
+
let cwd = process.cwd();
|
|
2311
|
+
const cwdEqArg = args.find(arg => arg.startsWith('--cwd='));
|
|
2312
|
+
const cwdIdx = args.indexOf('--cwd');
|
|
2313
|
+
if (cwdEqArg) {
|
|
2314
|
+
const value = cwdEqArg.slice('--cwd='.length).trim();
|
|
2315
|
+
if (!value) error('Missing value for --cwd', ERROR_REASON.USAGE);
|
|
2316
|
+
args.splice(args.indexOf(cwdEqArg), 1);
|
|
2317
|
+
cwd = path.resolve(value);
|
|
2318
|
+
} else if (cwdIdx !== -1) {
|
|
2319
|
+
const value = args[cwdIdx + 1];
|
|
2320
|
+
if (!value || value.startsWith('--')) error('Missing value for --cwd', ERROR_REASON.USAGE);
|
|
2321
|
+
args.splice(cwdIdx, 2);
|
|
2322
|
+
cwd = path.resolve(value);
|
|
2323
|
+
}
|
|
2699
2324
|
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
try {
|
|
2704
|
-
return await fs.promises.readFile(resolved, 'utf8');
|
|
2705
|
-
} catch (err) {
|
|
2706
|
-
if (err && err.code === 'ENOENT') {
|
|
2707
|
-
throw new ExitError(1, `Error: file not found for ${flagName}: ${resolved}`);
|
|
2708
|
-
}
|
|
2709
|
-
throw new ExitError(1, `Error: cannot read file for ${flagName}: ${resolved}`);
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2325
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
|
|
2326
|
+
error(`Invalid --cwd: ${cwd}`, ERROR_REASON.USAGE);
|
|
2327
|
+
}
|
|
2712
2328
|
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2329
|
+
// Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
|
|
2330
|
+
// However, in monorepo worktrees where the subdirectory itself owns .planning/,
|
|
2331
|
+
// skip worktree resolution — the CWD is already the correct project root.
|
|
2332
|
+
const { resolveWorktreeRoot } = require('./lib/worktree-safety.cjs');
|
|
2333
|
+
if (!fs.existsSync(path.join(cwd, '.planning'))) {
|
|
2334
|
+
const worktreeRoot = resolveWorktreeRoot(cwd);
|
|
2335
|
+
if (worktreeRoot !== cwd) {
|
|
2336
|
+
cwd = worktreeRoot;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2723
2339
|
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
const research = await readOptional(researchFile);
|
|
2742
|
-
const requirements = await readOptional(requirementsFile);
|
|
2743
|
-
|
|
2744
|
-
// ── Build options ─────────────────────────────────────────────────
|
|
2745
|
-
const options = {};
|
|
2746
|
-
if (safetyMarginStr !== null) {
|
|
2747
|
-
const pct = parseInt(safetyMarginStr, 10);
|
|
2748
|
-
if (Number.isFinite(pct)) options.safetyMarginPct = pct;
|
|
2749
|
-
}
|
|
2750
|
-
if (projectMdHeadLinesStr !== null) {
|
|
2751
|
-
const lines = parseInt(projectMdHeadLinesStr, 10);
|
|
2752
|
-
if (Number.isFinite(lines)) options.projectMdHeadLines = lines;
|
|
2753
|
-
}
|
|
2340
|
+
// Optional workstream override for parallel milestone work.
|
|
2341
|
+
// Priority: --ws flag > GSD_WORKSTREAM env var > session/shared pointer > null.
|
|
2342
|
+
let workstreamContext = null;
|
|
2343
|
+
try {
|
|
2344
|
+
workstreamContext = resolveActiveWorkstream(cwd, args, process.env, {
|
|
2345
|
+
getStored: getActiveWorkstream,
|
|
2346
|
+
});
|
|
2347
|
+
args = workstreamContext.args;
|
|
2348
|
+
// Set env var so all modules (planningDir, planningPaths) auto-resolve workstream paths.
|
|
2349
|
+
applyResolvedWorkstreamEnv(workstreamContext, process.env);
|
|
2350
|
+
} catch (err) {
|
|
2351
|
+
error(err.message || String(err));
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
const rawIndex = args.indexOf('--raw');
|
|
2355
|
+
const raw = rawIndex !== -1;
|
|
2356
|
+
if (rawIndex !== -1) args.splice(rawIndex, 1);
|
|
2754
2357
|
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2358
|
+
// --pick <name>: extract a single field from JSON output (replaces jq dependency).
|
|
2359
|
+
// Supports dot-notation (e.g., --pick workflow.research) and bracket notation
|
|
2360
|
+
// for arrays (e.g., --pick directories[-1]).
|
|
2361
|
+
const pickIdx = args.indexOf('--pick');
|
|
2362
|
+
let pickField = null;
|
|
2363
|
+
if (pickIdx !== -1) {
|
|
2364
|
+
pickField = args[pickIdx + 1];
|
|
2365
|
+
if (!pickField || pickField.startsWith('--')) error('Missing value for --pick', ERROR_REASON.USAGE);
|
|
2366
|
+
args.splice(pickIdx, 2);
|
|
2367
|
+
}
|
|
2758
2368
|
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2369
|
+
// --default <value>: for config-get, return this value instead of erroring
|
|
2370
|
+
// when the key is absent. Allows workflows to express optional config reads
|
|
2371
|
+
// without defensive `2>/dev/null || true` boilerplate (#1893).
|
|
2372
|
+
const defaultIdx = args.indexOf('--default');
|
|
2373
|
+
let defaultValue = undefined;
|
|
2374
|
+
if (defaultIdx !== -1) {
|
|
2375
|
+
defaultValue = args[defaultIdx + 1];
|
|
2376
|
+
if (defaultValue === undefined) defaultValue = '';
|
|
2377
|
+
args.splice(defaultIdx, 2);
|
|
2378
|
+
}
|
|
2762
2379
|
|
|
2763
|
-
|
|
2764
|
-
throw new ExitError(2);
|
|
2765
|
-
}
|
|
2766
|
-
break;
|
|
2767
|
-
}
|
|
2380
|
+
let command = args[0];
|
|
2768
2381
|
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
const { loadUpdateContext } = require('./lib/update-context.cjs');
|
|
2776
|
-
const ucArgs = args.slice(1);
|
|
2777
|
-
let preferredConfigDir = '';
|
|
2778
|
-
let preferredRuntime = '';
|
|
2779
|
-
for (let i = 0; i < ucArgs.length; i++) {
|
|
2780
|
-
const a = ucArgs[i];
|
|
2781
|
-
if (a.startsWith('--config-dir=')) { preferredConfigDir = a.slice('--config-dir='.length); continue; }
|
|
2782
|
-
if (a.startsWith('--runtime=')) { preferredRuntime = a.slice('--runtime='.length); continue; }
|
|
2783
|
-
if (a === '--config-dir') {
|
|
2784
|
-
const v = ucArgs[i + 1];
|
|
2785
|
-
if (v === undefined || v.startsWith('--')) error('Missing value for --config-dir', ERROR_REASON.USAGE);
|
|
2786
|
-
preferredConfigDir = v; i++; continue;
|
|
2787
|
-
}
|
|
2788
|
-
if (a === '--runtime') {
|
|
2789
|
-
const v = ucArgs[i + 1];
|
|
2790
|
-
if (v === undefined || v.startsWith('--')) error('Missing value for --runtime', ERROR_REASON.USAGE);
|
|
2791
|
-
preferredRuntime = v; i++; continue;
|
|
2792
|
-
}
|
|
2793
|
-
if (a === '--json') continue; // JSON is the only output; accepted for symmetry
|
|
2794
|
-
if (a.startsWith('-')) error(`Unknown flag for update-context: ${a}`, ERROR_REASON.USAGE);
|
|
2795
|
-
}
|
|
2796
|
-
const ctx = loadUpdateContext({ preferredConfigDir, preferredRuntime });
|
|
2797
|
-
process.stdout.write(JSON.stringify(ctx) + '\n');
|
|
2798
|
-
break;
|
|
2799
|
-
}
|
|
2382
|
+
// Accept `query` as a meta-prefix for canonical dotted/spaced commands.
|
|
2383
|
+
// Workflows may call `node gsd-tools.cjs query <command>` directly.
|
|
2384
|
+
if (command === 'query') {
|
|
2385
|
+
args.shift();
|
|
2386
|
+
command = args[0];
|
|
2387
|
+
}
|
|
2800
2388
|
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
const homeDir = process.env.HOME || require('os').homedir();
|
|
2818
|
-
if (subcommand === 'get') {
|
|
2819
|
-
const key = args[2];
|
|
2820
|
-
if (!key || key.startsWith('--')) {
|
|
2821
|
-
error('Usage: gsd-tools research-store get <key> [--kind <k>]', ERROR_REASON.USAGE);
|
|
2822
|
-
}
|
|
2823
|
-
if (!researchStore.isValidResearchKey(key)) {
|
|
2824
|
-
error('research-store: <key> must be a 64-char sha256 hex (use research-plan to obtain keys)', ERROR_REASON.USAGE);
|
|
2825
|
-
}
|
|
2826
|
-
// --kind is accepted but no longer drives tier selection; getResearch searches both tiers
|
|
2827
|
-
const result = researchStore.getResearch(cwd, key, { homeDir });
|
|
2828
|
-
output(result, raw);
|
|
2829
|
-
} else if (subcommand === 'put') {
|
|
2830
|
-
const key = args[2];
|
|
2831
|
-
if (!key || key.startsWith('--')) {
|
|
2832
|
-
error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
|
|
2833
|
-
}
|
|
2834
|
-
if (!researchStore.isValidResearchKey(key)) {
|
|
2835
|
-
error('research-store: <key> must be a 64-char sha256 hex (use research-plan to obtain keys)', ERROR_REASON.USAGE);
|
|
2836
|
-
}
|
|
2837
|
-
const contentIdx = args.indexOf('--content');
|
|
2838
|
-
const sourceIdx = args.indexOf('--source');
|
|
2839
|
-
const providerIdx = args.indexOf('--provider');
|
|
2840
|
-
const confidenceIdx = args.indexOf('--confidence');
|
|
2841
|
-
const kindIdx = args.indexOf('--kind');
|
|
2842
|
-
// For each flag, if the following value is missing or itself starts with '--', reject.
|
|
2843
|
-
function getFlagValue(idx, flagName) {
|
|
2844
|
-
if (idx === -1) return null;
|
|
2845
|
-
const val = args[idx + 1];
|
|
2846
|
-
if (val === undefined || val.startsWith('--')) {
|
|
2847
|
-
error(`research-store put: missing value for ${flagName}`, ERROR_REASON.USAGE);
|
|
2848
|
-
}
|
|
2849
|
-
return val;
|
|
2850
|
-
}
|
|
2851
|
-
const content = getFlagValue(contentIdx, '--content');
|
|
2852
|
-
const source = getFlagValue(sourceIdx, '--source');
|
|
2853
|
-
const provider = getFlagValue(providerIdx, '--provider');
|
|
2854
|
-
const confidence = getFlagValue(confidenceIdx, '--confidence');
|
|
2855
|
-
const kind = getFlagValue(kindIdx, '--kind');
|
|
2856
|
-
if (!content || !source || !provider || !confidence || !kind) {
|
|
2857
|
-
error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
|
|
2858
|
-
}
|
|
2859
|
-
const entry = researchStore.putResearch(cwd, key, { content, source, provider, confidence, kind }, { homeDir });
|
|
2860
|
-
output(entry, raw);
|
|
2861
|
-
} else {
|
|
2862
|
-
error('Unknown research-store subcommand. Available: get, put', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2863
|
-
}
|
|
2864
|
-
break;
|
|
2389
|
+
// #3243: accept dotted canonical form (e.g. `state.update`) as well as the
|
|
2390
|
+
// spaced form (`state update`). Some workflow callers pass the dotted
|
|
2391
|
+
// canonical form directly; this normalization keeps both forms valid.
|
|
2392
|
+
//
|
|
2393
|
+
// Split on the FIRST dot only — `check.decision-coverage-plan` becomes
|
|
2394
|
+
// command='check', args=['check','decision-coverage-plan',...rest].
|
|
2395
|
+
// Guard: head and rest must both be non-empty (rejects leading-dot args like
|
|
2396
|
+
// ".hidden" and bare-dot ".").
|
|
2397
|
+
const originalCommand = command; // preserved for "Unknown command" suggestion
|
|
2398
|
+
if (typeof command === 'string' && command.includes('.')) {
|
|
2399
|
+
const dotIdx = command.indexOf('.');
|
|
2400
|
+
const head = command.slice(0, dotIdx);
|
|
2401
|
+
const rest = command.slice(dotIdx + 1);
|
|
2402
|
+
if (head && rest) {
|
|
2403
|
+
command = head;
|
|
2404
|
+
args = [head, rest, ...args.slice(1)];
|
|
2865
2405
|
}
|
|
2406
|
+
}
|
|
2866
2407
|
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
if (!Array.isArray(planInput.questions)) {
|
|
2891
|
-
error('research-plan: --input must be an object with a questions array', ERROR_REASON.USAGE);
|
|
2892
|
-
}
|
|
2893
|
-
const { ecosystem = '', config: planConfig = {}, questions } = planInput;
|
|
2894
|
-
const homeDir = process.env.HOME || require('os').homedir();
|
|
2895
|
-
const plan = researchProvider.planResearch({ questions, ecosystem, config: planConfig, cwd, homeDir });
|
|
2896
|
-
output(plan, raw);
|
|
2897
|
-
break;
|
|
2898
|
-
}
|
|
2408
|
+
// Top-level usage string — emitted by `gsd-tools` (no args) and by
|
|
2409
|
+
// `gsd-tools --help` / any `--help` request below.
|
|
2410
|
+
// CR feedback: the command list must enumerate every top-level command
|
|
2411
|
+
// supported by the dispatcher so `--help` is actually useful for
|
|
2412
|
+
// discovery; previously it was a partial subset that didn't include
|
|
2413
|
+
// phase / roadmap / milestone / progress / etc.
|
|
2414
|
+
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
|
|
2415
|
+
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, pr-subrepo, ' +
|
|
2416
|
+
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, ' +
|
|
2417
|
+
'current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
|
2418
|
+
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
|
2419
|
+
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
|
2420
|
+
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
|
2421
|
+
'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, smart-entry, state, ' +
|
|
2422
|
+
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
|
2423
|
+
'Global flags:\n' +
|
|
2424
|
+
' --raw Emit raw output without post-processing\n' +
|
|
2425
|
+
' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
|
|
2426
|
+
' --cwd <path> Override working directory for project-root resolution\n' +
|
|
2427
|
+
' --ws <name> Override active workstream (or set GSD_WORKSTREAM)\n' +
|
|
2428
|
+
' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n\n' +
|
|
2429
|
+
'For command-specific argument requirements, invoke the command without args ' +
|
|
2430
|
+
'(e.g. `gsd-tools phase add`) — the resulting error lists what is required.';
|
|
2899
2431
|
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
// -> classifyConfidence({ provider, verifiedAgainstOfficial, legitimacyVerdict }); output(result, raw)
|
|
2904
|
-
//
|
|
2905
|
-
// legitimacyVerdict is CODE-COMPUTED via checkPackages — never caller-supplied — so an agent cannot self-assert OK→HIGH.
|
|
2432
|
+
if (!command) {
|
|
2433
|
+
error(TOP_LEVEL_USAGE);
|
|
2434
|
+
}
|
|
2906
2435
|
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
const ecosystem = ecoIdx !== -1 ? args[ecoIdx + 1] : null;
|
|
2919
|
-
let legitimacyVerdict = null;
|
|
2920
|
-
if (pkg && (!pkg.startsWith('--'))) {
|
|
2921
|
-
const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
|
|
2922
|
-
if (!ecosystem || ecosystem.startsWith('--') || !VALID_ECOSYSTEMS.has(ecosystem)) {
|
|
2923
|
-
error('Usage: gsd-tools query classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]', ERROR_REASON.USAGE);
|
|
2924
|
-
}
|
|
2925
|
-
const pkgLegitimacy = require('./lib/package-legitimacy.cjs');
|
|
2926
|
-
const results = await pkgLegitimacy.checkPackages({ ecosystem, packages: [pkg] }, {});
|
|
2927
|
-
legitimacyVerdict = results[0] ? results[0].verdict : null;
|
|
2928
|
-
}
|
|
2929
|
-
const confidence = researchProvider.classifyConfidence({ provider, verifiedAgainstOfficial: verified, legitimacyVerdict });
|
|
2930
|
-
output({ provider, package: pkg || null, ecosystem: ecosystem || null, legitimacyVerdict, verified, confidence }, raw);
|
|
2931
|
-
break;
|
|
2932
|
-
}
|
|
2436
|
+
// #3019: a `--help` / `-h` flag in argv must render the top-level usage
|
|
2437
|
+
// and exit 0 — not error out with "Unknown flag". The previous shape
|
|
2438
|
+
// erred on agent-hallucinated flags, but it also blocked humans from
|
|
2439
|
+
// discovering the command surface via subcommand help requests routed
|
|
2440
|
+
// through this dispatcher. Rendering top-level usage on --help is strictly
|
|
2441
|
+
// better UX than the old short-circuit that printed unrelated usage text.
|
|
2442
|
+
const HELP_FLAGS = new Set(['-h', '--help', '-?', '--h', '--usage']);
|
|
2443
|
+
if (args.some((a) => HELP_FLAGS.has(a))) {
|
|
2444
|
+
process.stdout.write(TOP_LEVEL_USAGE + '\n');
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2933
2447
|
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
case 'package-legitimacy': {
|
|
2942
|
-
const pkgLegitimacy = require('./lib/package-legitimacy.cjs');
|
|
2943
|
-
const subcommand = args[1];
|
|
2944
|
-
if (subcommand !== 'check') {
|
|
2945
|
-
error('Unknown package-legitimacy subcommand. Available: check', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2946
|
-
}
|
|
2947
|
-
const ecoIdx = args.indexOf('--ecosystem');
|
|
2948
|
-
const ecosystem = ecoIdx !== -1 ? args[ecoIdx + 1] : null;
|
|
2949
|
-
const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
|
|
2950
|
-
if (!ecosystem || !VALID_ECOSYSTEMS.has(ecosystem)) {
|
|
2951
|
-
error('Usage: gsd-tools package-legitimacy check --ecosystem <npm|pypi|crates> <pkg1> ...', ERROR_REASON.USAGE);
|
|
2952
|
-
}
|
|
2953
|
-
// Collect positional package names.
|
|
2954
|
-
// Only --ecosystem takes a value. Every non-flag arg is a package name.
|
|
2955
|
-
// Any unknown --flag is a usage error (do not silently skip+consume the next arg).
|
|
2956
|
-
const packages = [];
|
|
2957
|
-
for (let i = 2; i < args.length; i++) {
|
|
2958
|
-
const a = args[i];
|
|
2959
|
-
if (a === '--ecosystem') { i++; continue; }
|
|
2960
|
-
if (a.startsWith('--')) {
|
|
2961
|
-
error(`package-legitimacy: unknown flag ${a}`, ERROR_REASON.USAGE);
|
|
2962
|
-
}
|
|
2963
|
-
packages.push(a);
|
|
2964
|
-
}
|
|
2965
|
-
if (packages.length === 0) {
|
|
2966
|
-
error('Usage: gsd-tools package-legitimacy check --ecosystem <eco> <pkg1> <pkg2> ...', ERROR_REASON.USAGE);
|
|
2967
|
-
}
|
|
2968
|
-
let pkgResults;
|
|
2969
|
-
try {
|
|
2970
|
-
pkgResults = await pkgLegitimacy.checkPackages({ ecosystem, packages }, {});
|
|
2971
|
-
} catch (pkgErr) {
|
|
2972
|
-
error(`package-legitimacy: ${pkgErr && pkgErr.message ? pkgErr.message : String(pkgErr)}`, ERROR_REASON.UNKNOWN);
|
|
2973
|
-
}
|
|
2974
|
-
output(pkgResults, raw);
|
|
2975
|
-
break;
|
|
2448
|
+
// Reject version flags. AI agents sometimes hallucinate --version on tool
|
|
2449
|
+
// invocations; silently ignoring it can cause destructive operations to
|
|
2450
|
+
// proceed unchecked. (Help flags are handled above.)
|
|
2451
|
+
const NEVER_VALID_FLAGS = new Set(['--version', '-v']);
|
|
2452
|
+
for (const arg of args) {
|
|
2453
|
+
if (NEVER_VALID_FLAGS.has(arg)) {
|
|
2454
|
+
error(`Unknown flag: ${arg}\ngsd-tools does not accept version flags. Run "gsd-tools" with no arguments for usage.`, ERROR_REASON.USAGE);
|
|
2976
2455
|
}
|
|
2456
|
+
}
|
|
2977
2457
|
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
error(`effort sync takes no positional arguments; got: ${a}`, ERROR_REASON.USAGE);
|
|
3004
|
-
}
|
|
3005
|
-
commands.cmdEffortSync(cwd, raw, { dryRun, configDir: effortSyncConfigDir, runtime: effortSyncRuntime });
|
|
3006
|
-
} else {
|
|
3007
|
-
error('Unknown effort subcommand. Available: sync', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
3008
|
-
}
|
|
3009
|
-
break;
|
|
3010
|
-
}
|
|
2458
|
+
// Multi-repo guard: resolve project root for commands that read/write .planning/.
|
|
2459
|
+
// Skip for pure-utility commands that don't touch .planning/ to avoid unnecessary
|
|
2460
|
+
// filesystem traversal on every invocation.
|
|
2461
|
+
// 'loop' and 'capability' are intentionally NOT in SKIP_ROOT_RESOLUTION.
|
|
2462
|
+
// Both are registry/config queries that resolve activation via
|
|
2463
|
+
// .planning/config.json; they need the project root (cwd) for correct
|
|
2464
|
+
// `when` key resolution. If one is ever moved to SKIP_ROOT_RESOLUTION,
|
|
2465
|
+
// move the other at the same time (keep them consistent).
|
|
2466
|
+
const SKIP_ROOT_RESOLUTION = new Set([
|
|
2467
|
+
'generate-slug', 'current-timestamp', 'verify-path-exists',
|
|
2468
|
+
'verify-summary', 'template', 'frontmatter', 'detect-custom-files',
|
|
2469
|
+
'worktree', 'prompt-budget',
|
|
2470
|
+
'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
|
|
2471
|
+
'user-story', // pure string validation — no .planning/ access needed
|
|
2472
|
+
// #1529: pure runtime→filename projection via getProjectInstructionFile; no
|
|
2473
|
+
// .planning/ access needed, and resolving project root would break workflow
|
|
2474
|
+
// invocations that run before .planning/ exists (new-project Step 1).
|
|
2475
|
+
'project-instruction-file',
|
|
2476
|
+
// #1579: eval.score is pure arithmetic (covered/total + infra weights); it
|
|
2477
|
+
// needs no .planning/ access, so skip the findProjectRoot traversal.
|
|
2478
|
+
'eval',
|
|
2479
|
+
]);
|
|
2480
|
+
if (!SKIP_ROOT_RESOLUTION.has(command)) {
|
|
2481
|
+
cwd = findProjectRoot(cwd);
|
|
2482
|
+
}
|
|
3011
2483
|
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
// Each slot must be non-empty and contain non-whitespace content.
|
|
3026
|
-
//
|
|
3027
|
-
// No .planning/ access needed — pure string validation.
|
|
3028
|
-
|
|
3029
|
-
// #1146: single base-branch resolver for all forking workflows.
|
|
3030
|
-
// Workflows call `gsd_run query git.base-branch` (dotted form normalised to
|
|
3031
|
-
// command='git', args=['git','base-branch']).
|
|
3032
|
-
case 'git': {
|
|
3033
|
-
const subcommand = args[1];
|
|
3034
|
-
if (subcommand !== 'base-branch') {
|
|
3035
|
-
error(
|
|
3036
|
-
`Unknown git subcommand: ${subcommand || '(none)'}. Available: base-branch`,
|
|
3037
|
-
ERROR_REASON.SDK_UNKNOWN_COMMAND,
|
|
3038
|
-
);
|
|
3039
|
-
break;
|
|
3040
|
-
}
|
|
3041
|
-
cmdGitBaseBranch(cwd, args.slice(2));
|
|
3042
|
-
break;
|
|
2484
|
+
// When --pick is active, capture stdout and extract the requested field.
|
|
2485
|
+
if (pickField) {
|
|
2486
|
+
const captured = await captureStdoutSyncWrites(async () => {
|
|
2487
|
+
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
2488
|
+
});
|
|
2489
|
+
const resolved = resolveAtFileOutput(captured);
|
|
2490
|
+
try {
|
|
2491
|
+
const obj = JSON.parse(resolved);
|
|
2492
|
+
const value = extractField(obj, pickField);
|
|
2493
|
+
const result = value === null || value === undefined ? '' : String(value);
|
|
2494
|
+
fs.writeSync(1, result);
|
|
2495
|
+
} catch {
|
|
2496
|
+
fs.writeSync(1, captured);
|
|
3043
2497
|
}
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
3044
2500
|
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
: '';
|
|
3056
|
-
|
|
3057
|
-
// Canonical extraction regex — requires non-whitespace content in each slot
|
|
3058
|
-
// (\S.*? ensures the slot isn't whitespace-only).
|
|
3059
|
-
// Named groups: role / capability / outcome.
|
|
3060
|
-
const USER_STORY_RE = /^As a (\S.*?), I want to (\S.*?), so that (\S.*?)\.$/;
|
|
2501
|
+
// Intercept stdout to transparently resolve @file: references (#1891).
|
|
2502
|
+
// io.cjs output() writes @file:<path> when JSON > 50KB. The --pick path
|
|
2503
|
+
// already resolves this, but the normal path wrote @file: to stdout, forcing
|
|
2504
|
+
// every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
|
|
2505
|
+
// that breaks on PowerShell and other non-bash shells.
|
|
2506
|
+
const captured = await captureStdoutSyncWrites(async () => {
|
|
2507
|
+
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
2508
|
+
});
|
|
2509
|
+
fs.writeSync(1, resolveAtFileOutput(captured));
|
|
2510
|
+
}
|
|
3061
2511
|
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
2512
|
+
function captureStdoutSyncWrites(run) {
|
|
2513
|
+
const originalWriteSync = fs.writeSync;
|
|
2514
|
+
let captured = '';
|
|
3065
2515
|
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
if (!/^As a \S/i.test(trimmed)) {
|
|
3072
|
-
errors.push('Story must start with "As a [user role]," (role must be non-empty).');
|
|
3073
|
-
}
|
|
3074
|
-
if (!/, I want to \S/i.test(trimmed)) {
|
|
3075
|
-
errors.push('Story must include ", I want to [capability]," (capability must be non-empty).');
|
|
3076
|
-
}
|
|
3077
|
-
if (!/, so that \S/i.test(trimmed)) {
|
|
3078
|
-
errors.push('Story must include ", so that [outcome]." (outcome must be non-empty).');
|
|
3079
|
-
}
|
|
3080
|
-
if (!trimmed.endsWith('.')) {
|
|
3081
|
-
errors.push('Story must end with a period (.).');
|
|
3082
|
-
}
|
|
3083
|
-
// Full-regex check only when per-clause guards all passed — avoids
|
|
3084
|
-
// redundant "format mismatch" noise on top of specific error messages.
|
|
3085
|
-
if (errors.length === 0) {
|
|
3086
|
-
const m = USER_STORY_RE.exec(trimmed);
|
|
3087
|
-
if (!m) {
|
|
3088
|
-
errors.push('Story does not match the canonical format: "As a [role], I want to [capability], so that [outcome]."');
|
|
3089
|
-
} else {
|
|
3090
|
-
slots = { role: m[1], capability: m[2], outcome: m[3] };
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
2516
|
+
fs.writeSync = function patchedWriteSync(fd, data, ...rest) {
|
|
2517
|
+
if (fd === 1) {
|
|
2518
|
+
if (Buffer.isBuffer(data)) {
|
|
2519
|
+
captured += data.toString('utf-8');
|
|
2520
|
+
return data.length;
|
|
3093
2521
|
}
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
2522
|
+
const text = String(data);
|
|
2523
|
+
captured += text;
|
|
2524
|
+
let encoding = 'utf-8';
|
|
2525
|
+
if (typeof rest[1] === 'string') encoding = rest[1];
|
|
2526
|
+
return Buffer.byteLength(text, encoding);
|
|
3097
2527
|
}
|
|
2528
|
+
return originalWriteSync.call(fs, fd, data, ...rest);
|
|
2529
|
+
};
|
|
3098
2530
|
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
// drift-guard authority → effective authority string
|
|
3103
|
-
// drift-guard severity --status <S> [--authority <A>] → {severity, hardBlock}
|
|
3104
|
-
const subcommand = args[1];
|
|
3105
|
-
|
|
3106
|
-
// Read config.json directly for both plan_review.source_grounding_authority
|
|
3107
|
-
// and intel.enabled. Neither key is in the config-loader.cjs whitelist that
|
|
3108
|
-
// config-loader.cjs's loadConfig() whitelist does not return; plan_review is only in config.cjs's private
|
|
3109
|
-
// buildConfig(), and intel is a federated capability config key.
|
|
3110
|
-
let configuredAuthority = 'grep';
|
|
3111
|
-
let intelEnabled = false;
|
|
3112
|
-
try {
|
|
3113
|
-
const { planningDir } = require('./lib/planning-workspace.cjs');
|
|
3114
|
-
const cfgPath = require('path').join(planningDir(cwd), 'config.json');
|
|
3115
|
-
if (require('fs').existsSync(cfgPath)) {
|
|
3116
|
-
const rawCfg = JSON.parse(require('fs').readFileSync(cfgPath, 'utf-8'));
|
|
3117
|
-
if (rawCfg && rawCfg.plan_review && rawCfg.plan_review.source_grounding_authority) {
|
|
3118
|
-
configuredAuthority = String(rawCfg.plan_review.source_grounding_authority);
|
|
3119
|
-
}
|
|
3120
|
-
if (rawCfg && rawCfg.intel && rawCfg.intel.enabled === true) {
|
|
3121
|
-
intelEnabled = true;
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
} catch {
|
|
3125
|
-
// not fatal — defaults apply
|
|
3126
|
-
}
|
|
3127
|
-
|
|
3128
|
-
const effectiveAuthority = getEffectiveAuthority(configuredAuthority, intelEnabled);
|
|
2531
|
+
const restore = () => {
|
|
2532
|
+
fs.writeSync = originalWriteSync;
|
|
2533
|
+
};
|
|
3129
2534
|
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
2535
|
+
return Promise.resolve()
|
|
2536
|
+
.then(() => run())
|
|
2537
|
+
.then(() => {
|
|
2538
|
+
restore();
|
|
2539
|
+
return captured;
|
|
2540
|
+
}, (err) => {
|
|
2541
|
+
restore();
|
|
2542
|
+
// The wrapped command may have written to stdout BEFORE it threw — e.g. a --raw
|
|
2543
|
+
// command that emits a JSON result/error envelope and THEN throws ExitError to set a
|
|
2544
|
+
// non-zero exit code (capability set/disable on an unknown id). Without this flush that
|
|
2545
|
+
// captured output is silently discarded (the success-path flush at the call site never
|
|
2546
|
+
// runs on a throw). Emit it now; the error still propagates so the exit code is preserved.
|
|
2547
|
+
if (captured) {
|
|
2548
|
+
try { originalWriteSync.call(fs, 1, resolveAtFileOutput(captured)); } catch { /* best-effort flush */ }
|
|
3134
2549
|
}
|
|
2550
|
+
throw err;
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
3135
2553
|
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
error('drift-guard severity requires --status <VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE>', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
3141
|
-
break;
|
|
3142
|
-
}
|
|
3143
|
-
const authIdx = args.indexOf('--authority');
|
|
3144
|
-
const authVal = authIdx !== -1 ? args[authIdx + 1] : undefined;
|
|
3145
|
-
const authorityForClassify = (authVal && !authVal.startsWith('--'))
|
|
3146
|
-
? authVal
|
|
3147
|
-
: effectiveAuthority;
|
|
3148
|
-
const result = classifyDriftSeverity({ status: statusVal, authority: authorityForClassify });
|
|
3149
|
-
output(result, raw);
|
|
3150
|
-
break;
|
|
3151
|
-
}
|
|
2554
|
+
function resolveAtFileOutput(captured) {
|
|
2555
|
+
if (!captured.startsWith('@file:')) return captured;
|
|
2556
|
+
return fs.readFileSync(captured.slice(6), 'utf-8');
|
|
2557
|
+
}
|
|
3152
2558
|
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
2559
|
+
/**
|
|
2560
|
+
* Extract a field from an object using dot-notation and bracket syntax.
|
|
2561
|
+
* Supports: 'field', 'parent.child', 'arr[-1]', 'arr[0]'
|
|
2562
|
+
*/
|
|
2563
|
+
function extractField(obj, fieldPath) {
|
|
2564
|
+
const parts = fieldPath.split('.');
|
|
2565
|
+
let current = obj;
|
|
2566
|
+
for (const part of parts) {
|
|
2567
|
+
if (current === null || current === undefined) return undefined;
|
|
2568
|
+
const bracketMatch = part.match(/^(.+?)\[(-?\d+)]$/);
|
|
2569
|
+
if (bracketMatch) {
|
|
2570
|
+
const key = bracketMatch[1];
|
|
2571
|
+
const index = parseInt(bracketMatch[2], 10);
|
|
2572
|
+
current = current[key];
|
|
2573
|
+
if (!Array.isArray(current)) return undefined;
|
|
2574
|
+
current = index < 0 ? current[current.length + index] : current[index];
|
|
2575
|
+
} else {
|
|
2576
|
+
current = current[part];
|
|
3158
2577
|
}
|
|
2578
|
+
}
|
|
2579
|
+
return current;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) {
|
|
2583
|
+
switch (command) {
|
|
3159
2584
|
|
|
3160
2585
|
default: {
|
|
3161
2586
|
// ADR-959: try capability-registry dispatch before emitting the unknown-command error.
|
|
@@ -3171,6 +2596,12 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
3171
2596
|
// require()-ing its router FROM the capability's install root (confined to that root).
|
|
3172
2597
|
if (dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error })) break;
|
|
3173
2598
|
|
|
2599
|
+
// ADR-2346 (epic #2345): host dispatch table — core, non-capability
|
|
2600
|
+
// commands (state, …) routed via their `route*Command` router instead of
|
|
2601
|
+
// a hardcoded `case` arm. Tried after capability/overlay dispatch and
|
|
2602
|
+
// before the unknown-command error.
|
|
2603
|
+
if (await dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext })) break;
|
|
2604
|
+
|
|
3174
2605
|
// #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
|
|
3175
2606
|
// above split it so `command` here is the head ("foo"). Use
|
|
3176
2607
|
// originalCommand to reconstruct the original dotted form and suggest
|
|
@@ -3202,4 +2633,5 @@ if (require.main === module) {
|
|
|
3202
2633
|
// synthetic registry + requireModule injections.
|
|
3203
2634
|
// ADR-1244 Phase 5: export dispatchOverlayCapabilityCommand + defaultRequireFromInstallRoot for
|
|
3204
2635
|
// the third-party overlay dispatch + install-root confinement tests.
|
|
3205
|
-
module.exports = { dispatchCapabilityCommand, dispatchOverlayCapabilityCommand, defaultRequireFromInstallRoot };
|
|
2636
|
+
module.exports = { dispatchCapabilityCommand, dispatchOverlayCapabilityCommand, defaultRequireFromInstallRoot, dispatchHostCommand, HOST_COMMAND_ROUTERS };
|
|
2637
|
+
|