@opengsd/gsd-core 1.13.0 → 1.14.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/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/bin/install.js +206 -68
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +338 -125
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +79 -67
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -1
- package/gsd-core/bin/lib/check-command-router.cjs +113 -36
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/commands.cjs +650 -72
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +137 -34
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/init.cjs +409 -47
- package/gsd-core/bin/lib/install-engine.cjs +16 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +19 -8
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
- package/gsd-core/bin/lib/phase-id.cjs +161 -22
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +167 -63
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
- package/gsd-core/bin/lib/roadmap.cjs +108 -14
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +142 -28
- package/gsd-core/bin/lib/state.cjs +223 -27
- package/gsd-core/bin/lib/surface.cjs +60 -2
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/uat.cjs +1 -1
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +47 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify.cjs +188 -23
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/planning-config.md +3 -0
- package/gsd-core/references/tdd.md +5 -2
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/workflows/add-todo.md +3 -2
- package/gsd-core/workflows/autonomous.md +13 -10
- package/gsd-core/workflows/check-todos.md +4 -2
- package/gsd-core/workflows/cleanup.md +3 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
- package/gsd-core/workflows/code-review-fix.md +3 -3
- package/gsd-core/workflows/code-review.md +156 -30
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +39 -262
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +14 -155
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase.md +53 -152
- package/gsd-core/workflows/execute-plan.md +20 -7
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/map-codebase.md +50 -3
- package/gsd-core/workflows/new-milestone.md +54 -12
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +32 -202
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase.md +22 -181
- package/gsd-core/workflows/pr-branch.md +19 -7
- package/gsd-core/workflows/quick.md +8 -1
- package/gsd-core/workflows/reapply-patches.md +77 -3
- package/gsd-core/workflows/settings.md +18 -5
- package/gsd-core/workflows/update.md +7 -5
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +20 -180
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +44 -18
- package/hooks/dist/gsd-statusline.js +11 -7
- package/hooks/dist/gsd-validate-commit.sh +34 -4
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +44 -18
- package/hooks/gsd-statusline.js +11 -7
- package/hooks/gsd-validate-commit.sh +34 -4
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +10 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +7 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
- package/scripts/lint-phase-id-drift.cjs +338 -13
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
- package/scripts/prompt-injection-scan.sh +14 -0
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -438,8 +438,9 @@ function dispatchCapabilityCommand({ command, args, cwd, raw, error, registry, r
|
|
|
438
438
|
// Step 2: confinement check — belt-and-suspenders even after the basename
|
|
439
439
|
// validation above. Resolved path must be inside libDir (not equal to it,
|
|
440
440
|
// and must start with libDir + sep so "libDir-suffix" can't sneak through).
|
|
441
|
-
const
|
|
442
|
-
|
|
441
|
+
const { tryWithinRootLexical } = require('./lib/security.cjs');
|
|
442
|
+
const resolved = tryWithinRootLexical(m, libDir);
|
|
443
|
+
if (resolved === null || resolved === path.resolve(libDir)) {
|
|
443
444
|
throw new Error('capability module path escapes bin/lib/: ' + JSON.stringify(m));
|
|
444
445
|
}
|
|
445
446
|
// Step 3: require the resolved absolute path — the SAME representation that
|
|
@@ -515,18 +516,19 @@ function defaultRequireFromInstallRoot(installRoot, m) {
|
|
|
515
516
|
if (typeof m !== 'string' || !/^[A-Za-z0-9._-]+\.cjs$/.test(m)) {
|
|
516
517
|
throw new Error('capability module must be a bare .cjs basename: ' + JSON.stringify(m));
|
|
517
518
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
if (
|
|
519
|
+
const { tryWithinRoot, tryWithinRootLexical, PathAcceptance } = require('./lib/security.cjs');
|
|
520
|
+
// Lexical containment check: a symlinked ancestor can't widen confinement.
|
|
521
|
+
const lexical = tryWithinRootLexical(m, installRoot);
|
|
522
|
+
if (lexical === null || lexical === path.resolve(installRoot)) {
|
|
522
523
|
throw new Error('capability module path escapes its install root: ' + JSON.stringify(m));
|
|
523
524
|
}
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
525
|
+
// Realpath/symlink check: the module file itself must not be a symlink
|
|
526
|
+
// pointing outside the root.
|
|
527
|
+
const real = tryWithinRoot(m, installRoot, PathAcceptance.RelativeOnly);
|
|
528
|
+
if (real === null) {
|
|
527
529
|
throw new Error('capability module resolves outside its install root (symlink): ' + JSON.stringify(m));
|
|
528
530
|
}
|
|
529
|
-
return require(
|
|
531
|
+
return require(real);
|
|
530
532
|
}
|
|
531
533
|
|
|
532
534
|
/**
|
|
@@ -784,7 +786,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
784
786
|
error,
|
|
785
787
|
output: output,
|
|
786
788
|
});
|
|
787
|
-
if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw);
|
|
789
|
+
if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw, { dryRun: args.includes('--dry-run') });
|
|
788
790
|
}
|
|
789
791
|
|
|
790
792
|
function routeConfigSetModelProfile({ args, cwd, raw }) {
|
|
@@ -950,15 +952,27 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
950
952
|
function routeCommit({ args, cwd, raw, error }) {
|
|
951
953
|
const amend = args.includes('--amend');
|
|
952
954
|
const noVerify = args.includes('--no-verify');
|
|
953
|
-
|
|
955
|
+
// #4208: `--files` and `--files-removed` are two path lists, each
|
|
956
|
+
// running from its flag to the NEXT LIST FLAG. A boolean flag
|
|
957
|
+
// inside a list (`--files a --amend b`) is skipped, not a
|
|
958
|
+
// terminator: that is what the previous slice-to-end collection
|
|
959
|
+
// did (it filtered `--` tokens and kept everything else), and a
|
|
960
|
+
// list that stopped at any `--` token silently dropped `b`
|
|
961
|
+
// (review of #4253). The previous form could not carry a second
|
|
962
|
+
// list flag at all, which is the only thing that changed.
|
|
963
|
+
// A REPEATED list flag (`--files a --files b`) merges, as the old
|
|
964
|
+
// slice-to-end parse merged it: every occurrence contributes its
|
|
965
|
+
// run, and none of them ends another's silently.
|
|
966
|
+
const firstListFlag = args.findIndex((a, i) => i > 0 && COMMIT_LIST_FLAGS.has(a));
|
|
954
967
|
// Collect all positional args between command name and first flag,
|
|
955
968
|
// then join them — handles both quoted ("multi word msg") and
|
|
956
969
|
// unquoted (multi word msg) invocations from different shells
|
|
957
|
-
const endIndex =
|
|
970
|
+
const endIndex = firstListFlag !== -1 ? firstListFlag : args.length;
|
|
958
971
|
const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
|
|
959
972
|
const message = messageArgs.join(' ') || undefined;
|
|
960
|
-
const files =
|
|
961
|
-
|
|
973
|
+
const files = collectListFlagValues(args, '--files');
|
|
974
|
+
const filesRemoved = collectListFlagValues(args, '--files-removed');
|
|
975
|
+
commands.cmdCommit(cwd, message, files, raw, amend, noVerify, filesRemoved);
|
|
962
976
|
}
|
|
963
977
|
|
|
964
978
|
function routeCheckCommit({ args, cwd, raw, error }) {
|
|
@@ -1322,7 +1336,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1322
1336
|
const fsx = require('node:fs');
|
|
1323
1337
|
const os = require('node:os');
|
|
1324
1338
|
const { REVIEWER_LANES, mergeReviewerLanes } = require('./lib/review-lane-descriptor.cjs');
|
|
1325
|
-
const { resolveLanePlan, resolveLaneEffort } = require('./lib/review-lane-invocation.cjs');
|
|
1339
|
+
const { resolveLanePlan, resolveLaneEffort, resolveLaneBudget } = require('./lib/review-lane-invocation.cjs');
|
|
1326
1340
|
const modelCatalog = require('./lib/model-catalog.cjs');
|
|
1327
1341
|
const runner = require('./lib/review-lane-runner.cjs');
|
|
1328
1342
|
const cfgLoader = require('./lib/config-loader.cjs');
|
|
@@ -1347,8 +1361,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1347
1361
|
// `plan`/`invoke` are the only subs that need the expensive plan-building path
|
|
1348
1362
|
// below; `sections`/`flags` return earlier still. Anything else errors here, before
|
|
1349
1363
|
// any of that work starts.
|
|
1350
|
-
if (!['plan', 'invoke', 'sections', 'flags'].includes(sub)) {
|
|
1351
|
-
error("Usage: review-lane <plan|invoke|sections|flags> [--selected a,b] [--run-dir D] [--repo-root R]");
|
|
1364
|
+
if (!['plan', 'invoke', 'sections', 'flags', 'dispatch-step', 'explicit-from-argv'].includes(sub)) {
|
|
1365
|
+
error("Usage: review-lane <plan|invoke|sections|flags|dispatch-step|explicit-from-argv> [--selected a,b] [--run-dir D] [--repo-root R]");
|
|
1352
1366
|
return;
|
|
1353
1367
|
}
|
|
1354
1368
|
const runDir = flag('--run-dir') || '.';
|
|
@@ -1370,6 +1384,86 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1370
1384
|
return cur;
|
|
1371
1385
|
};
|
|
1372
1386
|
|
|
1387
|
+
// Shared by `invoke` and `dispatch-step` (#4209) — both need the SAME bounded
|
|
1388
|
+
// spawn/http/fs seam `runLane` requires (RunnerDeps). Factored out so the two
|
|
1389
|
+
// callers can never disagree about how a lane's binary is resolved or how its
|
|
1390
|
+
// process is bounded; a fix to either reaches both.
|
|
1391
|
+
const buildLaneRunnerDeps = () => ({
|
|
1392
|
+
spawn: (binary, argv, opts) => {
|
|
1393
|
+
// #3086: on Windows, reviewer CLIs (gemini, codex, etc.) are installed
|
|
1394
|
+
// as .cmd shims. spawnSync with a bare name + shell:false fails with
|
|
1395
|
+
// ENOENT (CreateProcess cannot start .cmd). Apply the same #2667 shim
|
|
1396
|
+
// gate used in runWithTimeout: detect .cmd/.bat and mediate through
|
|
1397
|
+
// cmd.exe /d /s /c with an explicit argv array (no shell:true).
|
|
1398
|
+
//
|
|
1399
|
+
// #3275: descriptors declare BARE names, so the gate above never saw an
|
|
1400
|
+
// extension — resolve through the shared PATH+PATHEXT resolver FIRST
|
|
1401
|
+
// (the same one `hasBinary` uses, so probe and spawn can never disagree
|
|
1402
|
+
// about what the lane's binary is). POSIX keeps the bare name: Node's own
|
|
1403
|
+
// PATH search already worked there, and the #3275 acceptance contract
|
|
1404
|
+
// holds macOS/Linux behavior unchanged. A name that resolves to nothing
|
|
1405
|
+
// falls back to the declared name so the ENOENT still surfaces (#3086).
|
|
1406
|
+
// #3411: the resolve-then-mediate pair is one seam call now. Both halves had
|
|
1407
|
+
// private copies here; `projectSpawnInvocation` owns them, so a fix to either
|
|
1408
|
+
// reaches every spawn site instead of only this one.
|
|
1409
|
+
//
|
|
1410
|
+
// Unlike execTool, this lane adopts the RESOLVED path even for a non-batch
|
|
1411
|
+
// binary: that is the behavior #3445 shipped and `deps.hasBinary` answers
|
|
1412
|
+
// from the same resolver, so probe and spawn must agree on the exact file.
|
|
1413
|
+
const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
|
|
1414
|
+
const { command: spawnBinary, args: spawnArgv, windowsVerbatimArguments } = projectSpawnInvocation(binary, argv);
|
|
1415
|
+
const r = cp.spawnSync(spawnBinary, spawnArgv, {
|
|
1416
|
+
input: opts.input,
|
|
1417
|
+
encoding: 'utf8',
|
|
1418
|
+
timeout: opts.timeoutMs,
|
|
1419
|
+
killSignal: 'SIGKILL',
|
|
1420
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1421
|
+
shell: false, // argv array only — never a shell string (no interpolation of config values)
|
|
1422
|
+
// #2483: a lane's declared env pairs merged OVER this process's environment, for this
|
|
1423
|
+
// child only. Passing a fresh object leaves `process.env` untouched, so nothing leaks
|
|
1424
|
+
// into the orchestrating session or into the next lane.
|
|
1425
|
+
...(opts.env ? { env: { ...process.env, ...opts.env } } : {}),
|
|
1426
|
+
...(windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
1427
|
+
});
|
|
1428
|
+
return {
|
|
1429
|
+
status: r.status,
|
|
1430
|
+
stdout: r.stdout || '',
|
|
1431
|
+
stderr: r.stderr || '',
|
|
1432
|
+
errorCode: r.error && r.error.code ? r.error.code : undefined,
|
|
1433
|
+
};
|
|
1434
|
+
},
|
|
1435
|
+
httpJson: async (url, opts) => {
|
|
1436
|
+
try {
|
|
1437
|
+
const res = await fetch(url, {
|
|
1438
|
+
method: opts.method,
|
|
1439
|
+
headers: opts.body ? { 'Content-Type': 'application/json' } : undefined,
|
|
1440
|
+
body: opts.body,
|
|
1441
|
+
signal: AbortSignal.timeout(opts.timeoutMs),
|
|
1442
|
+
});
|
|
1443
|
+
return { ok: res.ok, status: res.status, body: await res.text() };
|
|
1444
|
+
} catch (e) {
|
|
1445
|
+
return { ok: false, status: 0, body: '', error: e && e.message ? e.message : String(e) };
|
|
1446
|
+
}
|
|
1447
|
+
},
|
|
1448
|
+
readFile: (p) => fsx.readFileSync(p, 'utf8'),
|
|
1449
|
+
writeFile: (p, c) => fsx.writeFileSync(p, c, 'utf8'),
|
|
1450
|
+
exists: (p) => fsx.existsSync(p),
|
|
1451
|
+
// PATH scan rather than spawning `command -v` / `where`. Two reasons: it spawns nothing at
|
|
1452
|
+
// all (a probe that costs a process is a probe you avoid running, which is how the original
|
|
1453
|
+
// Kimi probe ended up unbounded), and `shell: true` with an args array is deprecated in
|
|
1454
|
+
// Node 26 (DEP0190) because the arguments are concatenated rather than escaped.
|
|
1455
|
+
//
|
|
1456
|
+
// #3275: the scan lives in `resolveSpawnBinary` now, SHARED with `deps.spawn`
|
|
1457
|
+
// above. Two private copies of "what is this declared binary?" is how the
|
|
1458
|
+
// defect hid: the probe resolved WITH PATHEXT while spawn resolved WITHOUT,
|
|
1459
|
+
// so a lane reported available for a spawn that could never start. One
|
|
1460
|
+
// resolver, both seams — if one changes, the other changes with it.
|
|
1461
|
+
hasBinary: (name) => resolveSpawnBinary(name) !== null,
|
|
1462
|
+
configGet,
|
|
1463
|
+
homeDir: os.homedir(),
|
|
1464
|
+
warn: (m) => process.stderr.write(`${m}\n`),
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1373
1467
|
const selected = (flag('--selected') || '')
|
|
1374
1468
|
.split(',').map((s) => s.trim()).filter(Boolean);
|
|
1375
1469
|
// ADR-2782 D8 (#2927): the lane map is first-party ∪ INSTALLED overlay
|
|
@@ -1397,6 +1491,25 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1397
1491
|
const laneBySlug = new Map(mergedLanes.map((l) => [l.slug, l]));
|
|
1398
1492
|
const chosen = selected.length ? selected : mergedLanes.map((l) => l.slug);
|
|
1399
1493
|
|
|
1494
|
+
// #4209 RQ-02: match a workflow's raw CLI argv (e.g. `--codex`, `--agy`) against the SAME
|
|
1495
|
+
// merged first-party+installed-overlay roster this whole function already built above,
|
|
1496
|
+
// instead of a workflow re-deriving its own copy of `loadRegistry`/`mergeReviewerLanes` via
|
|
1497
|
+
// an inline `node -e` (a rename-only duplicate of the block starting at `mergedLanes =
|
|
1498
|
+
// REVIEWER_LANES` above — `code-review-flags.cjs`'s own header states "this is the canonical
|
|
1499
|
+
// flag-parsing surface — do not replicate inline bash parsing" for exactly this reason).
|
|
1500
|
+
// Everything after `--` is a candidate flag; matched lane slugs print sorted and comma-joined.
|
|
1501
|
+
if (sub === 'explicit-from-argv') {
|
|
1502
|
+
const sepIdx = args.indexOf('--');
|
|
1503
|
+
const candidateArgs = new Set(sepIdx === -1 ? [] : args.slice(sepIdx + 1));
|
|
1504
|
+
const slugs = [];
|
|
1505
|
+
for (const lane of mergedLanes) {
|
|
1506
|
+
const flags = Array.isArray(lane.flags) ? lane.flags : [];
|
|
1507
|
+
if (flags.some((f) => candidateArgs.has(f))) slugs.push(lane.slug);
|
|
1508
|
+
}
|
|
1509
|
+
process.stdout.write([...new Set(slugs)].sort().join(','));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1400
1513
|
if (sub === 'sections') {
|
|
1401
1514
|
const rows = chosen
|
|
1402
1515
|
.map((s) => laneBySlug.get(s))
|
|
@@ -1454,26 +1567,135 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1454
1567
|
};
|
|
1455
1568
|
const effortFor = (lane) => resolveLaneEffort(lane, configGet, renderLaneEffort);
|
|
1456
1569
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1570
|
+
// Per-lane prompt budget: `resolveLaneBudget` (review-lane-invocation.cjs) owns the #2797
|
|
1571
|
+
// resolution semantics (shared with src/reviewer-step-dispatch.cts, #4209 R3 — was two
|
|
1572
|
+
// verbatim copies). Only the budget VALUE is resolved here; assembly/trimming stay in
|
|
1573
|
+
// `prompt-budget`, which the workflow calls, handing the trimmed file back via `--prompt-file`.
|
|
1574
|
+
const budgetFor = (lane) => resolveLaneBudget(lane, configGet);
|
|
1575
|
+
|
|
1576
|
+
// #4209 (ADR-2782 seam) — the ONE interpreter route for a step that declared
|
|
1577
|
+
// `supportsReviewerLanes: true`. Wires `dispatchReviewerLanes` (src/reviewer-step-dispatch.cts)
|
|
1578
|
+
// to the SAME plan/invoke machinery `plan`/`invoke` above use, so a step opting in gets exact
|
|
1579
|
+
// parity with hand-driven `review-lane plan|invoke` rather than a second implementation.
|
|
1580
|
+
// Canonical file paths travel on stdin, never argv (see gsd-core/workflows/code-review.md's
|
|
1581
|
+
// "Files travel on stdin" note) — a 50+-file scope with long paths approaches the Windows
|
|
1582
|
+
// execFileSync argv ceiling, and stdin has no such bound.
|
|
1583
|
+
//
|
|
1584
|
+
// Returns EARLY, like `sections`/`flags` above, rather than falling into the `plans` builder
|
|
1585
|
+
// below: that builder spawns one `effortFor` child process PER LANE IN THE ROSTER (chosen
|
|
1586
|
+
// defaults to every merged lane when nothing is selected), which would burn ~12 wasted spawns
|
|
1587
|
+
// on every dispatch-step call whether or not anything was actually selected. `dispatchReviewerLanes`
|
|
1588
|
+
// builds its own per-SELECTED-lane plan below instead, bounded by the (typically 0-3) explicitly
|
|
1589
|
+
// requested slugs, not the whole roster.
|
|
1590
|
+
if (sub === 'dispatch-step') {
|
|
1591
|
+
const { dispatchReviewerLanes } = require('./lib/reviewer-step-dispatch.cjs');
|
|
1592
|
+
const explicitFlags = (flag('--explicit') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
1593
|
+
const depth = flag('--depth') || '';
|
|
1594
|
+
const baseSha = flag('--base-sha') || '';
|
|
1595
|
+
// #4209: this command IS the reusable capability/step-dispatch trait check — see
|
|
1596
|
+
// gsd-core/references/loop-hook-dispatch.md for what supportsReviewerLanes means and why
|
|
1597
|
+
// this is the one place it's resolved. Calls the SAME resolver `loop render-hooks` uses,
|
|
1598
|
+
// `resolveActiveHooksForPoint`, directly in-process — no subprocess, no JSON re-parse, and
|
|
1599
|
+
// no exposure to `io.cjs`'s `@file:` overflow protocol (which only applies to the
|
|
1600
|
+
// rendered-string envelope this path never touches).
|
|
1601
|
+
const capId = flag('--cap-id') || '';
|
|
1602
|
+
const point = flag('--point') || '';
|
|
1603
|
+
let trait = false;
|
|
1604
|
+
if (capId && point) {
|
|
1605
|
+
try {
|
|
1606
|
+
const { resolveActiveHooksForPoint } = loopResolver;
|
|
1607
|
+
const { activeHooks } = resolveActiveHooksForPoint(cwd, point);
|
|
1608
|
+
trait = activeHooks.some((h) => h && h.capId === capId && h.supportsReviewerLanes === true);
|
|
1609
|
+
} catch (e) {
|
|
1610
|
+
process.stderr.write(`Warning: reviewer-lane trait resolution failed for --cap-id ${capId} --point ${point}: ${e && e.message ? e.message : String(e)} — treating as not enabled.\n`);
|
|
1611
|
+
trait = false;
|
|
1612
|
+
}
|
|
1613
|
+
} else if (capId || point) {
|
|
1614
|
+
// #4209 RQ-03: exactly one of the two was passed — a caller with NO capability-step
|
|
1615
|
+
// context at all (neither flag) is the legitimate, silent no-op documented above, but a
|
|
1616
|
+
// caller that named a capability without its point (or vice versa) is misconfigured, not
|
|
1617
|
+
// opted out, and that must not look identical to a correct opt-out on the wire.
|
|
1618
|
+
process.stderr.write(`Warning: --cap-id and --point must both be given to resolve the reviewer-lane trait (got --cap-id=${JSON.stringify(capId)} --point=${JSON.stringify(point)}) — treating as not enabled.\n`);
|
|
1619
|
+
}
|
|
1620
|
+
// No piped stdin (interactive TTY): fail closed to empty paths instead of blocking
|
|
1621
|
+
// indefinitely on a TTY EOF the caller never sends.
|
|
1622
|
+
let stdinPaths = '';
|
|
1623
|
+
if (!process.stdin.isTTY) {
|
|
1624
|
+
try { stdinPaths = fsx.readFileSync(0, 'utf8'); } catch { stdinPaths = ''; }
|
|
1625
|
+
}
|
|
1626
|
+
const paths = stdinPaths.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
1627
|
+
|
|
1628
|
+
// Reuse the exact effort-aware, per-lane plan `plan` builds above (DISP-03: "planned
|
|
1629
|
+
// through the existing `review-lane plan` interface") rather than the interpreter's
|
|
1630
|
+
// simpler default plan callback, which does not resolve per-host effort.
|
|
1631
|
+
const planFn = (lane, ctx) => {
|
|
1632
|
+
const effort = effortFor(lane.slug);
|
|
1633
|
+
return resolveLanePlan({
|
|
1634
|
+
lane, configGet: ctx.configGet, runDir: ctx.runDir, repoRoot: ctx.repoRoot,
|
|
1635
|
+
effortArgs: effort.argv, effortValue: effort.value,
|
|
1636
|
+
});
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
const runnerDeps = buildLaneRunnerDeps();
|
|
1640
|
+
const invokeFn = async (lane, plan) => {
|
|
1641
|
+
let consentedHost;
|
|
1642
|
+
if (plan.transport === 'openai-http') {
|
|
1643
|
+
try {
|
|
1644
|
+
const consent = require('./lib/capability-consent.cjs');
|
|
1645
|
+
const projectRoot = require('./lib/project-root.cjs').consentProjectRoot(cwd);
|
|
1646
|
+
const capId = String(lane.slug).replace(/_/g, '-');
|
|
1647
|
+
consentedHost = consent.readConsentedReviewerHost({ projectRoot, id: capId });
|
|
1648
|
+
} catch { consentedHost = undefined; }
|
|
1649
|
+
}
|
|
1650
|
+
// DISP-04/05: every selected lane is invoked through this SAME `runner.runLane` seam
|
|
1651
|
+
// `invoke` uses, exactly once (the interpreter's own for-loop over `selection.selected`
|
|
1652
|
+
// never revisits a slug).
|
|
1653
|
+
return runner.runLane(plan, runnerDeps, { consentedHost, explicitlyRequested: true, repoRoot });
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
// `resolveReviewerSelection` only selects an explicit flag present in `detected`
|
|
1657
|
+
// (ADR-2782 D4: absent-safe governs discovery, never explicit selection — a slug the
|
|
1658
|
+
// roster does not declare is rejected here as an explicit-selection error). REAL
|
|
1659
|
+
// host availability (is the CLI actually installed?) is a separate, already-owned
|
|
1660
|
+
// check inside `runner.runLane`'s `probeLane` at invoke time below — duplicating a
|
|
1661
|
+
// second `command -v` probe here would let the two disagree about what "available"
|
|
1662
|
+
// means, which is the exact defect class `resolveSpawnBinary` was consolidated to
|
|
1663
|
+
// prevent (#3275).
|
|
1664
|
+
//
|
|
1665
|
+
// GUARDED ON explicitFlags.length, not unconditional: `resolveReviewerSelection`'s
|
|
1666
|
+
// precedence chain (explicit > --all > review.default_reviewers > all detected) ends,
|
|
1667
|
+
// when none of the first three apply, in `selected = [...detected]` — the SAME
|
|
1668
|
+
// "no flags means every detected reviewer" default `/gsd:review` intentionally uses.
|
|
1669
|
+
// Source review's COMP-01 contract is the opposite: no reviewer-lane flag means inert,
|
|
1670
|
+
// unchanged from before #4209. Passing a non-empty
|
|
1671
|
+
// `detected` unconditionally would silently opt every dispatch-step call with no
|
|
1672
|
+
// `--explicit` into planning+invoking the WHOLE roster via that fallback branch. An
|
|
1673
|
+
// empty `detected` when nothing was asked for makes that fallback resolve to
|
|
1674
|
+
// `[...[]]` = `[]`, so `dispatchReviewerLanes` hits its own `NO_LANES_SELECTED`
|
|
1675
|
+
// early-return before any plan/invoke call — the same fast, inert no-op the caller
|
|
1676
|
+
// gets from an absent `supportsReviewerLanes` trait.
|
|
1677
|
+
const rosterSlugs = explicitFlags.length > 0 ? [...laneBySlug.keys()] : [];
|
|
1678
|
+
|
|
1679
|
+
const dispatchResult = await dispatchReviewerLanes(
|
|
1680
|
+
{
|
|
1681
|
+
trait,
|
|
1682
|
+
selection: { explicitFlags, detected: rosterSlugs },
|
|
1683
|
+
repoRoot,
|
|
1684
|
+
paths,
|
|
1685
|
+
depth,
|
|
1686
|
+
baseSha,
|
|
1687
|
+
runDir,
|
|
1688
|
+
},
|
|
1689
|
+
{
|
|
1690
|
+
getLane: (slug) => laneBySlug.get(slug),
|
|
1691
|
+
configGet,
|
|
1692
|
+
plan: planFn,
|
|
1693
|
+
invoke: invokeFn,
|
|
1694
|
+
},
|
|
1695
|
+
);
|
|
1696
|
+
output(dispatchResult, raw);
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1477
1699
|
|
|
1478
1700
|
const plans = chosen.map((slug) => {
|
|
1479
1701
|
const lane = laneBySlug.get(slug);
|
|
@@ -1508,7 +1730,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1508
1730
|
}
|
|
1509
1731
|
|
|
1510
1732
|
if (sub !== 'invoke') {
|
|
1511
|
-
error("Usage: review-lane <plan|invoke|sections|flags> [--selected a,b] [--run-dir D] [--repo-root R]");
|
|
1733
|
+
error("Usage: review-lane <plan|invoke|sections|flags|dispatch-step|explicit-from-argv> [--selected a,b] [--run-dir D] [--repo-root R]");
|
|
1512
1734
|
return;
|
|
1513
1735
|
}
|
|
1514
1736
|
|
|
@@ -1522,81 +1744,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1522
1744
|
|
|
1523
1745
|
// EVERY spawn bounded — `DEFECT.UNBOUNDED-SUBPROCESS` (CONTEXT.md:772). A frozen sync spawn
|
|
1524
1746
|
// cannot be interrupted by --test-force-exit and hangs a whole CI chunk to its 10-minute kill.
|
|
1525
|
-
const deps =
|
|
1526
|
-
spawn: (binary, argv, opts) => {
|
|
1527
|
-
// #3086: on Windows, reviewer CLIs (gemini, codex, etc.) are installed
|
|
1528
|
-
// as .cmd shims. spawnSync with a bare name + shell:false fails with
|
|
1529
|
-
// ENOENT (CreateProcess cannot start .cmd). Apply the same #2667 shim
|
|
1530
|
-
// gate used in runWithTimeout: detect .cmd/.bat and mediate through
|
|
1531
|
-
// cmd.exe /d /s /c with an explicit argv array (no shell:true).
|
|
1532
|
-
//
|
|
1533
|
-
// #3275: descriptors declare BARE names, so the gate above never saw an
|
|
1534
|
-
// extension — resolve through the shared PATH+PATHEXT resolver FIRST
|
|
1535
|
-
// (the same one `hasBinary` uses, so probe and spawn can never disagree
|
|
1536
|
-
// about what the lane's binary is). POSIX keeps the bare name: Node's own
|
|
1537
|
-
// PATH search already worked there, and the #3275 acceptance contract
|
|
1538
|
-
// holds macOS/Linux behavior unchanged. A name that resolves to nothing
|
|
1539
|
-
// falls back to the declared name so the ENOENT still surfaces (#3086).
|
|
1540
|
-
// #3411: the resolve-then-mediate pair is one seam call now. Both halves had
|
|
1541
|
-
// private copies here; `projectSpawnInvocation` owns them, so a fix to either
|
|
1542
|
-
// reaches every spawn site instead of only this one.
|
|
1543
|
-
//
|
|
1544
|
-
// Unlike execTool, this lane adopts the RESOLVED path even for a non-batch
|
|
1545
|
-
// binary: that is the behavior #3445 shipped and `deps.hasBinary` answers
|
|
1546
|
-
// from the same resolver, so probe and spawn must agree on the exact file.
|
|
1547
|
-
const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
|
|
1548
|
-
const { command: spawnBinary, args: spawnArgv, windowsVerbatimArguments } = projectSpawnInvocation(binary, argv);
|
|
1549
|
-
const r = cp.spawnSync(spawnBinary, spawnArgv, {
|
|
1550
|
-
input: opts.input,
|
|
1551
|
-
encoding: 'utf8',
|
|
1552
|
-
timeout: opts.timeoutMs,
|
|
1553
|
-
killSignal: 'SIGKILL',
|
|
1554
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
1555
|
-
shell: false, // argv array only — never a shell string (no interpolation of config values)
|
|
1556
|
-
// #2483: a lane's declared env pairs merged OVER this process's environment, for this
|
|
1557
|
-
// child only. Passing a fresh object leaves `process.env` untouched, so nothing leaks
|
|
1558
|
-
// into the orchestrating session or into the next lane.
|
|
1559
|
-
...(opts.env ? { env: { ...process.env, ...opts.env } } : {}),
|
|
1560
|
-
...(windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
1561
|
-
});
|
|
1562
|
-
return {
|
|
1563
|
-
status: r.status,
|
|
1564
|
-
stdout: r.stdout || '',
|
|
1565
|
-
stderr: r.stderr || '',
|
|
1566
|
-
errorCode: r.error && r.error.code ? r.error.code : undefined,
|
|
1567
|
-
};
|
|
1568
|
-
},
|
|
1569
|
-
httpJson: async (url, opts) => {
|
|
1570
|
-
try {
|
|
1571
|
-
const res = await fetch(url, {
|
|
1572
|
-
method: opts.method,
|
|
1573
|
-
headers: opts.body ? { 'Content-Type': 'application/json' } : undefined,
|
|
1574
|
-
body: opts.body,
|
|
1575
|
-
signal: AbortSignal.timeout(opts.timeoutMs),
|
|
1576
|
-
});
|
|
1577
|
-
return { ok: res.ok, status: res.status, body: await res.text() };
|
|
1578
|
-
} catch (e) {
|
|
1579
|
-
return { ok: false, status: 0, body: '', error: e && e.message ? e.message : String(e) };
|
|
1580
|
-
}
|
|
1581
|
-
},
|
|
1582
|
-
readFile: (p) => fsx.readFileSync(p, 'utf8'),
|
|
1583
|
-
writeFile: (p, c) => fsx.writeFileSync(p, c, 'utf8'),
|
|
1584
|
-
exists: (p) => fsx.existsSync(p),
|
|
1585
|
-
// PATH scan rather than spawning `command -v` / `where`. Two reasons: it spawns nothing at
|
|
1586
|
-
// all (a probe that costs a process is a probe you avoid running, which is how the original
|
|
1587
|
-
// Kimi probe ended up unbounded), and `shell: true` with an args array is deprecated in
|
|
1588
|
-
// Node 26 (DEP0190) because the arguments are concatenated rather than escaped.
|
|
1589
|
-
//
|
|
1590
|
-
// #3275: the scan lives in `resolveSpawnBinary` now, SHARED with `deps.spawn`
|
|
1591
|
-
// above. Two private copies of "what is this declared binary?" is how the
|
|
1592
|
-
// defect hid: the probe resolved WITH PATHEXT while spawn resolved WITHOUT,
|
|
1593
|
-
// so a lane reported available for a spawn that could never start. One
|
|
1594
|
-
// resolver, both seams — if one changes, the other changes with it.
|
|
1595
|
-
hasBinary: (name) => resolveSpawnBinary(name) !== null,
|
|
1596
|
-
configGet,
|
|
1597
|
-
homeDir: os.homedir(),
|
|
1598
|
-
warn: (m) => process.stderr.write(`${m}\n`),
|
|
1599
|
-
};
|
|
1747
|
+
const deps = buildLaneRunnerDeps();
|
|
1600
1748
|
|
|
1601
1749
|
// ADR-1517 reviewer instances resolve THROUGH a lane rather than being lanes themselves
|
|
1602
1750
|
// (ADR-2782 D8), so they reuse this seam with three substitutions instead of duplicating the
|
|
@@ -3147,6 +3295,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3147
3295
|
const RESTORE_OUTCOME = Object.freeze({
|
|
3148
3296
|
ELIGIBLE: 'eligible',
|
|
3149
3297
|
RESTORED: 'restored',
|
|
3298
|
+
ALREADY_PRESENT: 'already_present',
|
|
3150
3299
|
SKIPPED_DESTINATION_MANAGED: 'skipped_destination_managed',
|
|
3151
3300
|
SKIPPED_DESTINATION_EXISTS: 'skipped_destination_exists',
|
|
3152
3301
|
SKIPPED_COPY_FAILED: 'skipped_copy_failed',
|
|
@@ -3205,18 +3354,30 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3205
3354
|
return out;
|
|
3206
3355
|
}
|
|
3207
3356
|
|
|
3208
|
-
// Why these three checks rather than security.cjs's `
|
|
3209
|
-
// resolves symlinks with realpathSync and then
|
|
3210
|
-
// whose target sits inside the config dir
|
|
3211
|
-
// wrong — writing through any link
|
|
3212
|
-
//
|
|
3213
|
-
//
|
|
3214
|
-
//
|
|
3357
|
+
// Why these three checks rather than security.cjs's `assertWithinRoot` /
|
|
3358
|
+
// `tryWithinRoot`: that seam resolves symlinks with realpathSync and then
|
|
3359
|
+
// tests containment, so a link whose target sits inside the config dir
|
|
3360
|
+
// passes. For a restore that is still wrong — writing through any link
|
|
3361
|
+
// overwrites whatever it points at instead of materializing a regular file
|
|
3362
|
+
// at the backed-up path. These checks reject links outright, which is
|
|
3363
|
+
// strictly stricter than assertWithinRoot/tryWithinRoot, not a
|
|
3364
|
+
// reimplementation of them. Do not "simplify" this to assertWithinRoot or
|
|
3365
|
+
// tryWithinRoot. Reviewed under epic #4636 Phase 3: the containment
|
|
3366
|
+
// DECISION now routes through the canonical lexical predicate
|
|
3367
|
+
// (`tryWithinRootLexical`, ADR-4650 decision 6); isInsideDir below still
|
|
3368
|
+
// treats target === root as NOT contained via its own extra `!==` check
|
|
3369
|
+
// (unlike every other containment implementation in this repo, which
|
|
3370
|
+
// treats target === root as contained) — that condition is this gate's
|
|
3371
|
+
// own and is layered on top of the shared predicate, not folded into it.
|
|
3215
3372
|
|
|
3216
3373
|
/** True when `target` resolves strictly inside `root`. */
|
|
3217
3374
|
function isInsideDir(root, target) {
|
|
3218
|
-
|
|
3219
|
-
|
|
3375
|
+
// Containment decision: canonical lexical predicate (ADR-4650 decision 6).
|
|
3376
|
+
// The extra `!==` condition is this gate's own: a restore must never
|
|
3377
|
+
// target the config directory itself, only something strictly inside it.
|
|
3378
|
+
if (path.resolve(target) === path.resolve(root)) return false;
|
|
3379
|
+
const { tryWithinRootLexical } = require('./lib/security.cjs');
|
|
3380
|
+
return tryWithinRootLexical(target, root) !== null;
|
|
3220
3381
|
}
|
|
3221
3382
|
|
|
3222
3383
|
/**
|
|
@@ -3414,9 +3575,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3414
3575
|
}
|
|
3415
3576
|
|
|
3416
3577
|
// An identical destination is a no-op restore, not a conflict: re-running
|
|
3417
|
-
// the restore after a successful one must stay quiet and idempotent.
|
|
3578
|
+
// the restore after a successful one must stay quiet and idempotent. It
|
|
3579
|
+
// gets its own outcome so it is excluded from eligible_count — the update
|
|
3580
|
+
// workflow drives its restore question off that count (#4558).
|
|
3581
|
+
let destExists = false;
|
|
3418
3582
|
let destDiffers = false;
|
|
3419
3583
|
if (fs.existsSync(destPath)) {
|
|
3584
|
+
destExists = true;
|
|
3420
3585
|
try {
|
|
3421
3586
|
destDiffers = !fs.readFileSync(destPath).equals(fs.readFileSync(srcPath));
|
|
3422
3587
|
} catch {
|
|
@@ -3431,6 +3596,10 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3431
3596
|
entries.push({ path: relPath, outcome: RESTORE_OUTCOME.SKIPPED_DESTINATION_EXISTS, warnings });
|
|
3432
3597
|
continue;
|
|
3433
3598
|
}
|
|
3599
|
+
if (destExists) {
|
|
3600
|
+
entries.push({ path: relPath, outcome: RESTORE_OUTCOME.ALREADY_PRESENT, warnings });
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3434
3603
|
|
|
3435
3604
|
if (!apply) {
|
|
3436
3605
|
entries.push({ path: relPath, outcome: RESTORE_OUTCOME.ELIGIBLE, warnings });
|
|
@@ -4146,6 +4315,31 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4146
4315
|
* declares that file "All OS-facing I/O; single platform seam", and a private
|
|
4147
4316
|
* duplicate here is what made it untrue.
|
|
4148
4317
|
*/
|
|
4318
|
+
const COMMIT_LIST_FLAGS = new Set(['--files', '--files-removed']);
|
|
4319
|
+
|
|
4320
|
+
// #4208 review: hoisted out of routeCommit's closure so the parser is reachable
|
|
4321
|
+
// from a test. It is the whole of the two-list argument contract, and its edge
|
|
4322
|
+
// cases (a boolean flag inside a run, a repeated list flag, either order) were
|
|
4323
|
+
// already the subject of a review round -- a parser that only the CLI can reach
|
|
4324
|
+
// can only be tested by example, one spawn at a time.
|
|
4325
|
+
//
|
|
4326
|
+
// Every occurrence of `flag` contributes a run; a run ends at the next LIST
|
|
4327
|
+
// flag and skips boolean flags on the way, so no token strictly between one
|
|
4328
|
+
// list flag and the next is ever dropped. Repeated runs of the same flag merge,
|
|
4329
|
+
// as the pre-#4208 slice-to-end parse merged them.
|
|
4330
|
+
function collectListFlagValues(args, flag) {
|
|
4331
|
+
const values = [];
|
|
4332
|
+
args.forEach((a, i) => {
|
|
4333
|
+
if (a !== flag) return;
|
|
4334
|
+
for (const b of args.slice(i + 1)) {
|
|
4335
|
+
if (COMMIT_LIST_FLAGS.has(b)) break;
|
|
4336
|
+
if (b.startsWith('--')) continue;
|
|
4337
|
+
values.push(b);
|
|
4338
|
+
}
|
|
4339
|
+
});
|
|
4340
|
+
return values;
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4149
4343
|
function resolveSpawnBinary(name, platform = process.platform, env = process.env) {
|
|
4150
4344
|
const { resolveExecutableBinary } = require('./lib/shell-command-projection.cjs');
|
|
4151
4345
|
return resolveExecutableBinary(name, { platform, env });
|
|
@@ -4187,6 +4381,21 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
4187
4381
|
// rather than a family — ADR-2346 promotes to a family only at >=3.
|
|
4188
4382
|
'estimate-check': ({ args, cwd, raw }) => estimateCli.cmdEstimateCheck(cwd, args.slice(1), raw),
|
|
4189
4383
|
'estimate-calibration': ({ args, cwd, raw }) => estimateCli.cmdEstimateCalibration(cwd, args.slice(1), raw),
|
|
4384
|
+
// #3418: writes `last_mapped_commit` into every codebase-map document that
|
|
4385
|
+
// exists, closing the loop drift.cjs was built for. A LEAF verb rather than a
|
|
4386
|
+
// `verify` subcommand on purpose -- the verify family is read-only by
|
|
4387
|
+
// contract and this one mutates; ADR-2346 promotes a leaf to a family only at
|
|
4388
|
+
// >=3 verbs, and this is one.
|
|
4389
|
+
'stamp-codebase-map': ({ args, cwd, raw, error }) => {
|
|
4390
|
+
const { files } = parseNamedArgsOrExit(args, { valueFlags: ['files'], positionals: 1 }, error);
|
|
4391
|
+
// A value flag with no value parses to `null`, same as an absent one, so
|
|
4392
|
+
// presence is read off `args`: a bare `--files` (an unquoted empty shell
|
|
4393
|
+
// variable) must hit the empty-filter refusal, not widen to all seven.
|
|
4394
|
+
const only = args.includes('--files')
|
|
4395
|
+
? String(files ?? '').split(',').map((f) => f.trim()).filter(Boolean)
|
|
4396
|
+
: undefined;
|
|
4397
|
+
verify.cmdStampCodebaseMap(cwd, raw, only);
|
|
4398
|
+
},
|
|
4190
4399
|
'estimate-calibrate': ({ args, cwd, raw }) => estimateCli.cmdEstimateCalibrate(cwd, args.slice(1), raw),
|
|
4191
4400
|
'config-new-project': routeConfigNewProject,
|
|
4192
4401
|
'config-path': routeConfigPath,
|
|
@@ -4485,7 +4694,7 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
|
|
|
4485
4694
|
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, planning, profile-questionnaire, ' +
|
|
4486
4695
|
'profile-sample, progress, project-instruction-file, prompt-budget, quick-batch, quick-tasks-append, quick-tasks-migrate, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, runtime-identity, scaffold, smart-entry, state, ' +
|
|
4487
4696
|
'config-set-model-profile, dispatch-capacity, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
|
|
4488
|
-
'resolve-execution, review-lane, skill-manifest, skills-root, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
|
|
4697
|
+
'resolve-execution, review-lane, skill-manifest, skills-root, stamp-codebase-map, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
|
|
4489
4698
|
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
|
4490
4699
|
'Global flags:\n' +
|
|
4491
4700
|
' --raw Emit raw output without post-processing\n' +
|
|
@@ -5050,6 +5259,10 @@ module.exports = {
|
|
|
5050
5259
|
// #3275: exported for tests — the shared PATH+PATHEXT resolver behind
|
|
5051
5260
|
// review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
|
|
5052
5261
|
resolveSpawnBinary,
|
|
5262
|
+
// #4208 review: exported for tests — the two-list commit parser is otherwise
|
|
5263
|
+
// reachable only by spawning the CLI, which a property test cannot afford.
|
|
5264
|
+
collectListFlagValues,
|
|
5265
|
+
COMMIT_LIST_FLAGS,
|
|
5053
5266
|
// #3714 follow-up: exported for tests — the dispatch model-pin VALUE
|
|
5054
5267
|
// policy (charset accept/render parity, max-length boundary, leading-char
|
|
5055
5268
|
// anchor) is otherwise unreachable from outside the dispatchOverlayCapabilityCommand closure.
|
|
@@ -402,7 +402,7 @@ function parseCliArgs(argv) {
|
|
|
402
402
|
}
|
|
403
403
|
function main(argv) {
|
|
404
404
|
const opts = parseCliArgs(argv);
|
|
405
|
-
const safePath = (0, security_cjs_1.requireSafePath)(opts.input, node_path_1.default.resolve(opts.projectDir), 'ADR input path',
|
|
405
|
+
const safePath = (0, security_cjs_1.requireSafePath)(opts.input, node_path_1.default.resolve(opts.projectDir), 'ADR input path', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
406
406
|
const content = node_fs_1.default.readFileSync(safePath, 'utf8');
|
|
407
407
|
const parsed = parseAdrMarkdown(content, { sourcePath: opts.input ?? undefined, format: opts.format });
|
|
408
408
|
process.stdout.write(JSON.stringify(parsed, null, 2));
|
|
@@ -31,7 +31,8 @@ exports.CANONICAL_EXACT = new Set([
|
|
|
31
31
|
'STATE-ARCHIVE.md', // state.cts's cmdStatePrune writes this at the .planning/ root
|
|
32
32
|
'milestone.lock', // #3311: milestone (phase + session) claim (src/milestone-lock.cts); persistent, unlike the transient STATE.md.lock/WAITING.json
|
|
33
33
|
'state.json', // #3227: machine-readable state contract published at step boundaries (src/state-contract.cts)
|
|
34
|
-
'skill-manifest.json', // init.cts
|
|
34
|
+
'skill-manifest.json', // init.cts cmdSkillManifest --write (project-scoped planning root, #3964)
|
|
35
|
+
'PATTERNS.md', // #4282: graduated cross-phase patterns (workflows/graduation.md, `patterns` target) -- distinct from the per-phase NN-PATTERNS.md (templates/README.md)
|
|
35
36
|
]);
|
|
36
37
|
// Pattern-match canonical file names (regex tests on the basename)
|
|
37
38
|
// Each pattern includes the name of the workflow that produces it as a comment.
|