@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
|
@@ -35,6 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
35
35
|
const node_path_1 = __importDefault(require("node:path"));
|
|
36
36
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
37
37
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
38
|
+
const security_cjs_1 = require("./security.cjs");
|
|
38
39
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
40
|
const io = require("./io.cjs");
|
|
40
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -174,8 +175,11 @@ function resolvePhaseDirForArg(cwd, phaseArg) {
|
|
|
174
175
|
*/
|
|
175
176
|
function resolveConfinedPath(cwd, relFile) {
|
|
176
177
|
const root = node_path_1.default.resolve(cwd);
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
// ADR-4650 decision 6: lexical family — resolving the symlink here would
|
|
179
|
+
// undo the refuse-don't-resolve posture documented above; `lstatSync` below
|
|
180
|
+
// is the gate that actually refuses a symlink.
|
|
181
|
+
const resolved = (0, security_cjs_1.tryWithinRootLexical)(relFile, root);
|
|
182
|
+
if (resolved === null)
|
|
179
183
|
return null;
|
|
180
184
|
try {
|
|
181
185
|
if (!node_fs_1.default.lstatSync(resolved).isFile())
|
|
@@ -321,6 +325,32 @@ function loadWindowsOrDegrade(cwd, windowsOverride) {
|
|
|
321
325
|
}
|
|
322
326
|
return { ok: true, windows, ledger };
|
|
323
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* #3780: lazy (module-load cost, see file header) require of the ledger's
|
|
330
|
+
* serialization seam — the same `.planning/.WINDOWS.lock` the windows cmd*
|
|
331
|
+
* writers hold, so this router's own read-compute-write cycles on
|
|
332
|
+
* WINDOWS.md cannot lose updates against them. Required from the REAL
|
|
333
|
+
* compiled module, never through the injectable `windowsOverride` seam:
|
|
334
|
+
* the lock is infrastructure, not a parser stand-in.
|
|
335
|
+
*/
|
|
336
|
+
function withLedgerLock(cwd, fn) {
|
|
337
|
+
let lockMod;
|
|
338
|
+
try {
|
|
339
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
340
|
+
lockMod = require('./broken-windows.cjs');
|
|
341
|
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// Ledger module unavailable (the #1953 degrade world — see the row-86
|
|
345
|
+
// notesLedgerUnavailableWithoutBrokenWindows contract): no lock-holding
|
|
346
|
+
// writer can exist either, because every WINDOWS.md writer requires this
|
|
347
|
+
// same module. Run the body unlocked and let loadWindowsOrDegrade
|
|
348
|
+
// produce the canonical degrade note — wrapping that world in lock
|
|
349
|
+
// ceremony would only rewrite the note the degrade contract pins.
|
|
350
|
+
return fn();
|
|
351
|
+
}
|
|
352
|
+
return lockMod.withLedgerLock(cwd, fn);
|
|
353
|
+
}
|
|
324
354
|
/**
|
|
325
355
|
* Strict-mode window append (step 9 of `evaluate`). Degrades to
|
|
326
356
|
* `{ recorded: false, note }` per `loadWindowsOrDegrade` — never an error,
|
|
@@ -329,6 +359,23 @@ function loadWindowsOrDegrade(cwd, windowsOverride) {
|
|
|
329
359
|
* file/line) and does not append a second one.
|
|
330
360
|
*/
|
|
331
361
|
function recordStrictWindow(cwd, padded, target, windowsOverride) {
|
|
362
|
+
// #3780: hold the same cross-process ledger lock the windows cmd* writers
|
|
363
|
+
// hold — this site's read-compute-write on WINDOWS.md is otherwise the
|
|
364
|
+
// same lost-update race: a concurrent `gsd_run windows append` (or another
|
|
365
|
+
// evaluator) could silently overwrite this entry, or be overwritten by
|
|
366
|
+
// it. The lock comes from the real ledger module, not the injectable
|
|
367
|
+
// `windowsOverride` seam — it is infrastructure, not a parser stand-in.
|
|
368
|
+
// The wrapper keeps the #1953-defect-2 degrade contract: the lock's typed
|
|
369
|
+
// refusal degrades to `{ recorded: false, note }` like every other
|
|
370
|
+
// failure, it never throws out of this function.
|
|
371
|
+
try {
|
|
372
|
+
return withLedgerLock(cwd, () => recordStrictWindowLocked(cwd, padded, target, windowsOverride));
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
return { recorded: false, note: `failed to record broken-windows entry: ${e instanceof Error ? e.message : String(e)}` };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function recordStrictWindowLocked(cwd, padded, target, windowsOverride) {
|
|
332
379
|
const loaded = loadWindowsOrDegrade(cwd, windowsOverride);
|
|
333
380
|
if (!loaded.ok)
|
|
334
381
|
return { recorded: false, note: loaded.note };
|
|
@@ -357,6 +404,18 @@ function recordStrictWindow(cwd, padded, target, windowsOverride) {
|
|
|
357
404
|
* `accept`/`decline` never tell a user "it failed" without saying why.
|
|
358
405
|
*/
|
|
359
406
|
function resolveLedgerWindow(cwd, padded, file, line, kind, reasonText, windowsOverride) {
|
|
407
|
+
// #3780: same serialization as recordStrictWindow — a concurrent ledger
|
|
408
|
+
// writer holding a stale snapshot would silently revert this resolve (or
|
|
409
|
+
// lose its own write to this one). Same degrade contract: the lock's
|
|
410
|
+
// typed refusal degrades, never throws.
|
|
411
|
+
try {
|
|
412
|
+
return withLedgerLock(cwd, () => resolveLedgerWindowLocked(cwd, padded, file, line, kind, reasonText, windowsOverride));
|
|
413
|
+
}
|
|
414
|
+
catch (e) {
|
|
415
|
+
return { resolved: false, note: `failed to resolve broken-windows entry: ${e instanceof Error ? e.message : String(e)}` };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function resolveLedgerWindowLocked(cwd, padded, file, line, kind, reasonText, windowsOverride) {
|
|
360
419
|
const loaded = loadWindowsOrDegrade(cwd, windowsOverride);
|
|
361
420
|
if (!loaded.ok)
|
|
362
421
|
return { resolved: false, note: loaded.note };
|
|
@@ -15,6 +15,10 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
15
15
|
const node_path_1 = __importDefault(require("node:path"));
|
|
16
16
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
17
17
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
18
|
+
// ADR-4650 decision 6: lexical family — the key is validated before
|
|
19
|
+
// `fs.mkdirSync` creates the store dir, so the candidate legitimately does
|
|
20
|
+
// not exist yet at check time.
|
|
21
|
+
const security_cjs_1 = require("./security.cjs");
|
|
18
22
|
// ---------------------------------------------------------------------------
|
|
19
23
|
// Constants
|
|
20
24
|
// ---------------------------------------------------------------------------
|
|
@@ -94,15 +98,12 @@ function putResearch(cwd, key, payload, { clock = Date, homeDir = node_os_1.defa
|
|
|
94
98
|
const fetched_at = new Date(clock.now()).toISOString();
|
|
95
99
|
const entry = { content, source, provider, confidence, fetched_at, ttl, kind };
|
|
96
100
|
const dir = resolveStorePath(cwd, source, { homeDir });
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const filePath = node_path_1.default.join(dir, `${key}.json`);
|
|
100
|
-
const resolvedFile = node_path_1.default.resolve(filePath);
|
|
101
|
-
if (!resolvedFile.startsWith(resolvedDir + node_path_1.default.sep)) {
|
|
101
|
+
const containedFile = (0, security_cjs_1.tryWithinRootLexical)(`${key}.json`, dir);
|
|
102
|
+
if (containedFile === null || containedFile === node_path_1.default.resolve(dir)) {
|
|
102
103
|
throw new Error('invalid research key');
|
|
103
104
|
}
|
|
104
105
|
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
105
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(
|
|
106
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(containedFile, JSON.stringify(entry));
|
|
106
107
|
return entry;
|
|
107
108
|
}
|
|
108
109
|
// ---------------------------------------------------------------------------
|
|
@@ -120,16 +121,14 @@ function getResearch(cwd, key, { clock = Date, homeDir = node_os_1.default.homed
|
|
|
120
121
|
const tierDirs = [userDir, projectDir];
|
|
121
122
|
const candidates = [];
|
|
122
123
|
for (const dir of tierDirs) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
// Belt-and-suspenders: ensure path stays inside tier dir
|
|
126
|
-
if (!node_path_1.default.resolve(filePath).startsWith(resolvedDir + node_path_1.default.sep))
|
|
124
|
+
const containedFile = (0, security_cjs_1.tryWithinRootLexical)(`${key}.json`, dir);
|
|
125
|
+
if (containedFile === null || containedFile === node_path_1.default.resolve(dir))
|
|
127
126
|
continue;
|
|
128
|
-
if (!node_fs_1.default.existsSync(
|
|
127
|
+
if (!node_fs_1.default.existsSync(containedFile))
|
|
129
128
|
continue;
|
|
130
129
|
let entry;
|
|
131
130
|
try {
|
|
132
|
-
entry = JSON.parse(node_fs_1.default.readFileSync(
|
|
131
|
+
entry = JSON.parse(node_fs_1.default.readFileSync(containedFile, 'utf8'));
|
|
133
132
|
}
|
|
134
133
|
catch {
|
|
135
134
|
// Corrupt file in this tier — skip it
|
|
@@ -31,6 +31,8 @@ exports.resolveTimeoutMs = resolveTimeoutMs;
|
|
|
31
31
|
exports.nativeTimeoutToken = nativeTimeoutToken;
|
|
32
32
|
exports.isEmptyReview = isEmptyReview;
|
|
33
33
|
exports.fileRefPrompt = fileRefPrompt;
|
|
34
|
+
exports.artifactPaths = artifactPaths;
|
|
35
|
+
exports.resolveLaneBudget = resolveLaneBudget;
|
|
34
36
|
exports.resolveLanePlan = resolveLanePlan;
|
|
35
37
|
const review_lane_descriptor_cjs_1 = require("./review-lane-descriptor.cjs");
|
|
36
38
|
/* ------------------------------------------------------------------ *
|
|
@@ -236,6 +238,27 @@ function artifactPaths(runDir, slug) {
|
|
|
236
238
|
errPath: `${base}/gsd-review-${slug}.err`,
|
|
237
239
|
};
|
|
238
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Per-lane prompt budget (#2797 semantics, preserved exactly).
|
|
243
|
+
*
|
|
244
|
+
* `-1` is the UNSET sentinel and falls back to the central `review.max_prompt_tokens`, because
|
|
245
|
+
* `0` is a legitimate value meaning "do not trim this lane". Treating 0 as unset would silently
|
|
246
|
+
* switch a user who deliberately disabled trimming onto the global budget.
|
|
247
|
+
*
|
|
248
|
+
* Single source of truth: `gsd-core/bin/gsd-tools.cjs`'s `review-lane plan`/`invoke` and
|
|
249
|
+
* `src/reviewer-step-dispatch.cts`'s `dispatchReviewerLanes` both resolve a lane's budget through
|
|
250
|
+
* this function rather than each carrying their own copy (#4209 R3 — two verbatim copies drift).
|
|
251
|
+
*/
|
|
252
|
+
function resolveLaneBudget(lane, configGet) {
|
|
253
|
+
if (!lane.promptBudgetKey)
|
|
254
|
+
return null;
|
|
255
|
+
const per = configGet(lane.promptBudgetKey);
|
|
256
|
+
const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
257
|
+
if (isNum(per) && per !== -1)
|
|
258
|
+
return per;
|
|
259
|
+
const global = configGet('review.max_prompt_tokens');
|
|
260
|
+
return isNum(global) ? global : null;
|
|
261
|
+
}
|
|
239
262
|
/* ------------------------------------------------------------------ *
|
|
240
263
|
* Resolution
|
|
241
264
|
* ------------------------------------------------------------------ */
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reviewer Step Dispatch (#4209 Phase 1 Plan 2, ADR-2782 seam).
|
|
4
|
+
*
|
|
5
|
+
* ONE interpreter for "a step declares `supportsReviewerLanes: true`" — see
|
|
6
|
+
* `gsd-core/references/loop-hook-dispatch.md` for the canonical explanation of the trait and how
|
|
7
|
+
* `review-lane dispatch-step` re-derives it. This module trusts `trait` exactly as given: it is
|
|
8
|
+
* the CALLER's job to have derived it correctly. Every direct or lifecycle caller routes through
|
|
9
|
+
* `dispatchReviewerLanes` so selection/plan/invoke logic is owned once, not re-derived per
|
|
10
|
+
* feature. This module owns NONE of those primitives — it wires `resolveReviewerSelection`
|
|
11
|
+
* (selection) and `resolveLanePlan` (planning), the same building blocks
|
|
12
|
+
* `gsd-core/bin/gsd-tools.cjs`'s `review-lane plan` subcommand uses. Invocation (`runLane`) needs
|
|
13
|
+
* OS-aware spawn/probe plumbing this module does not own, so `deps.invoke` is the one required,
|
|
14
|
+
* caller-supplied seam (wired for real in `gsd-core/bin/gsd-tools.cjs`'s `review-lane
|
|
15
|
+
* dispatch-step` route).
|
|
16
|
+
*
|
|
17
|
+
* Fail-closed contract:
|
|
18
|
+
* - Trait not exactly `true`, or nothing selected → inert. Zero plan/invoke calls.
|
|
19
|
+
* - Missing/unsafe request-level input (paths escaping `repoRoot`, absent depth/base SHA) stops
|
|
20
|
+
* the WHOLE dispatch before any lane is planned or invoked.
|
|
21
|
+
* - An explicitly requested lane the selector could not resolve does not silently narrow the
|
|
22
|
+
* result to only what worked: lanes that DID resolve still run and their results are kept,
|
|
23
|
+
* but the aggregate `ok` is `false` so no caller mistakes a partial run for a clean one.
|
|
24
|
+
* - Once a lane is planned, a per-lane plan/budget/invoke failure never displaces or cancels a
|
|
25
|
+
* sibling lane already run.
|
|
26
|
+
* - A lane whose `promptChannel` is `'none'` (it reviews the working tree on its own terms, fed
|
|
27
|
+
* nothing — e.g. `coderabbit`) cannot receive the bounded prompt below and is rejected before
|
|
28
|
+
* plan/invoke, same as an unresolved slug.
|
|
29
|
+
*
|
|
30
|
+
* The bounded source-review prompt built here is METADATA ONLY — repository root, canonical
|
|
31
|
+
* file paths, review depth, base SHA, and four fixed prohibitions. It never embeds file
|
|
32
|
+
* contents. If the assembled prompt exceeds a lane's resolved budget, that lane's dispatch
|
|
33
|
+
* hard-fails before `invoke` runs for it — no silent truncation of the file list.
|
|
34
|
+
*/
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.SOURCE_REVIEW_PROHIBITIONS = exports.DISPATCH_REASON = void 0;
|
|
40
|
+
exports.buildSourceReviewPrompt = buildSourceReviewPrompt;
|
|
41
|
+
exports.dispatchReviewerLanes = dispatchReviewerLanes;
|
|
42
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
43
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
44
|
+
const prompt_budget_cjs_1 = require("./prompt-budget.cjs");
|
|
45
|
+
const security_cjs_1 = require("./security.cjs");
|
|
46
|
+
const review_lane_invocation_cjs_1 = require("./review-lane-invocation.cjs");
|
|
47
|
+
const review_reviewer_selection_cjs_1 = require("./review-reviewer-selection.cjs");
|
|
48
|
+
/** Closed set of request-level (not per-lane) halt reasons. Mirrors `LANE_UNAVAILABLE`'s shape. */
|
|
49
|
+
exports.DISPATCH_REASON = Object.freeze({
|
|
50
|
+
TRAIT_NOT_ENABLED: 'trait_not_enabled',
|
|
51
|
+
NO_LANES_SELECTED: 'no_lanes_selected',
|
|
52
|
+
SELECTION_FAILED: 'selection_failed',
|
|
53
|
+
INVALID_PATHS: 'invalid_paths',
|
|
54
|
+
PATH_ESCAPES_REPO_ROOT: 'path_escapes_repo_root',
|
|
55
|
+
MISSING_PROVENANCE: 'missing_provenance',
|
|
56
|
+
INVALID_PROVENANCE: 'invalid_provenance',
|
|
57
|
+
PROMPT_WRITE_FAILED: 'prompt_write_failed',
|
|
58
|
+
});
|
|
59
|
+
/** Fixed, non-negotiable prompt constraints (SAFE-03..SAFE-06). Order is the display order. */
|
|
60
|
+
exports.SOURCE_REVIEW_PROHIBITIONS = Object.freeze([
|
|
61
|
+
'Do not modify any source file.',
|
|
62
|
+
'Do not run tests.',
|
|
63
|
+
'Do not start background processes.',
|
|
64
|
+
'Do not poll or wait — return findings from a single read-only pass.',
|
|
65
|
+
]);
|
|
66
|
+
// #4209 RQ-04: a control character (newline, CR, NUL, ...) in ANY string this module embeds
|
|
67
|
+
// into the external prompt (`buildSourceReviewPrompt`) lets it inject a fabricated section —
|
|
68
|
+
// not just via `paths` (agy-F1's original finding), since `depth`, `baseSha`, and `repoRoot` land
|
|
69
|
+
// in that same markdown. Every embedded string is checked against this ONE shared boundary.
|
|
70
|
+
const CONTROL_CHAR = /[\x00-\x1f\x7f\u2028\u2029]/;
|
|
71
|
+
function defaultWritePromptFile(filePath, content) {
|
|
72
|
+
node_fs_1.default.writeFileSync(filePath, content, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validate that every path is a non-empty string resolving INSIDE `repoRoot` — blocks `..`
|
|
76
|
+
* traversal and absolute paths pointing elsewhere before any lane sees them.
|
|
77
|
+
*/
|
|
78
|
+
function validatePaths(repoRoot, paths) {
|
|
79
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
80
|
+
return { ok: false, reason: exports.DISPATCH_REASON.INVALID_PATHS };
|
|
81
|
+
}
|
|
82
|
+
const root = node_path_1.default.resolve(String(repoRoot ?? ''));
|
|
83
|
+
// repoRoot itself may be a symlink (e.g. a `/tmp`-based worktree on macOS, where `/tmp` is
|
|
84
|
+
// itself a symlink to `/private/tmp`) — realpath it once so the per-path comparison below
|
|
85
|
+
// compares like with like, not a resolved child path against an unresolved root.
|
|
86
|
+
let realRoot;
|
|
87
|
+
try {
|
|
88
|
+
realRoot = node_fs_1.default.realpathSync(root);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
realRoot = root;
|
|
92
|
+
}
|
|
93
|
+
// #4209 agy-F1: a control character (newline, CR, NUL, ...) in a path lets a maliciously
|
|
94
|
+
// named repo file inject a fabricated section into the markdown prompt built from `paths`
|
|
95
|
+
// below (buildSourceReviewPrompt) — reject it here, at the shared trust boundary, rather than
|
|
96
|
+
// relying on the incidental quoting `git diff --name-only` happens to apply upstream.
|
|
97
|
+
for (const p of paths) {
|
|
98
|
+
if (typeof p !== 'string' || p.length === 0 || CONTROL_CHAR.test(p)) {
|
|
99
|
+
return { ok: false, reason: exports.DISPATCH_REASON.INVALID_PATHS };
|
|
100
|
+
}
|
|
101
|
+
// ADR-4650 decision 6: lexical family — this is the first of the two
|
|
102
|
+
// deliberate halves (#4209 WR-05); the ENOENT-tolerant realpath half
|
|
103
|
+
// below cannot be folded into a single `tryWithinRoot` call (its
|
|
104
|
+
// ancestor-walk would accept a deleted path via the nearest existing
|
|
105
|
+
// ancestor, not the explicit `continue` this code requires).
|
|
106
|
+
const resolved = node_path_1.default.resolve(root, p);
|
|
107
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(p, root) === null) {
|
|
108
|
+
return { ok: false, reason: exports.DISPATCH_REASON.PATH_ESCAPES_REPO_ROOT };
|
|
109
|
+
}
|
|
110
|
+
// #4209 WR-05: `path.resolve` is lexical only — a symlink whose OWN path sits inside
|
|
111
|
+
// repoRoot can still point outside it, passing the check above while listing an
|
|
112
|
+
// out-of-repo file for the external lane to read. `fs.realpathSync` follows the link;
|
|
113
|
+
// ENOENT is expected and benign here (a `git diff --name-only` path can legitimately name
|
|
114
|
+
// a file already deleted in a stale worktree) and is not itself an escape.
|
|
115
|
+
let real;
|
|
116
|
+
try {
|
|
117
|
+
real = node_fs_1.default.realpathSync(resolved);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(real, realRoot) === null) {
|
|
123
|
+
return { ok: false, reason: exports.DISPATCH_REASON.PATH_ESCAPES_REPO_ROOT };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { ok: true };
|
|
127
|
+
}
|
|
128
|
+
// `resolveLaneBudget` (review-lane-invocation.cjs) resolves the number; `null` and a resolved
|
|
129
|
+
// `0` both mean unbounded (#2797) — the caller's overflow check must test both `!== null` and
|
|
130
|
+
// `!== 0`. See the call site below.
|
|
131
|
+
/**
|
|
132
|
+
* One-line depth definition for an external reviewer lane, condensed from `<depth_levels>` in
|
|
133
|
+
* `agents/gsd-code-reviewer.md` (#4209 review: a bare `quick`/`standard`/`deep` label means
|
|
134
|
+
* nothing to a third-party CLI that never sees that agent's system prompt — unlike the internal
|
|
135
|
+
* reviewer, whose own persona fully defines these three terms). Every category named here must
|
|
136
|
+
* stay a strict subset of what `<depth_levels>` actually does — `tests/reviewer-step-dispatch
|
|
137
|
+
* .test.cjs`'s "depthMeaning tracks depth_levels" tests assert each case against the real agent
|
|
138
|
+
* file, not just against this function, so the two cannot silently drift again. An unrecognised
|
|
139
|
+
* depth normalizes to `standard`'s text, matching `agents/gsd-code-reviewer.md`'s own "if depth
|
|
140
|
+
* is not one of quick/standard/deep, default to standard" rule — the raw label is not repeated
|
|
141
|
+
* here since `buildSourceReviewPrompt` already states it once, verbatim, earlier in the prompt.
|
|
142
|
+
*/
|
|
143
|
+
function depthMeaning(depth) {
|
|
144
|
+
switch (depth) {
|
|
145
|
+
case 'quick':
|
|
146
|
+
return 'pattern-scan without reading full file contents: hardcoded secrets, dangerous functions, debug artifacts, empty catch blocks, commented-out code';
|
|
147
|
+
case 'standard':
|
|
148
|
+
return 'read each changed file in context for bugs, security, and quality problems; cross-reference imports and exports';
|
|
149
|
+
case 'deep':
|
|
150
|
+
return 'standard, plus cross-file analysis: trace call chains, check type consistency at API boundaries, verify error propagation, check state mutation consistency, detect circular dependencies';
|
|
151
|
+
default:
|
|
152
|
+
return depthMeaning('standard');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Build the bounded source-review prompt. Metadata only — repoRoot, paths, depth, base SHA, and
|
|
157
|
+
* the four fixed prohibitions. NEVER embeds file contents.
|
|
158
|
+
*/
|
|
159
|
+
function buildSourceReviewPrompt(input) {
|
|
160
|
+
// Base SHA is identical for every file and already stated once above — repeating it per line
|
|
161
|
+
// (as an earlier version of this prompt did) wastes real tokens at O(files), for zero
|
|
162
|
+
// information gain, on every dispatched lane.
|
|
163
|
+
const fileLines = input.paths.map((p) => `- ${p}`).join('\n');
|
|
164
|
+
const ruleLines = exports.SOURCE_REVIEW_PROHIBITIONS.map((r, i) => `${i + 1}. ${r}`).join('\n');
|
|
165
|
+
return [
|
|
166
|
+
'## Source Review Request',
|
|
167
|
+
'',
|
|
168
|
+
`Repository root: ${input.repoRoot}`,
|
|
169
|
+
`Review depth: ${input.depth}`,
|
|
170
|
+
`Base SHA: ${input.baseSha}`,
|
|
171
|
+
'',
|
|
172
|
+
'Review the changes introduced in each file below relative to the base SHA above, at the',
|
|
173
|
+
`requested depth (${depthMeaning(input.depth)}). Report every bug, security issue, and`,
|
|
174
|
+
'code-quality problem you find. For every claim you make, cite the exact file path and line',
|
|
175
|
+
'number(s) it applies to — a claim with no file:line citation cannot be independently',
|
|
176
|
+
're-verified and will be discarded by the consolidating reviewer. Performance issues',
|
|
177
|
+
'(O(n²), memory leaks) are out of scope unless also correctness issues (e.g. an infinite',
|
|
178
|
+
'loop) — do not flag them otherwise.',
|
|
179
|
+
'',
|
|
180
|
+
'### Files in scope',
|
|
181
|
+
fileLines,
|
|
182
|
+
'',
|
|
183
|
+
'### Rules',
|
|
184
|
+
ruleLines,
|
|
185
|
+
].join('\n');
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Dispatch every selected reviewer lane for one opted-in step. See module docstring for scope.
|
|
189
|
+
*/
|
|
190
|
+
async function dispatchReviewerLanes(input, deps) {
|
|
191
|
+
if (input.trait !== true) {
|
|
192
|
+
return { dispatched: false, ok: true, reason: exports.DISPATCH_REASON.TRAIT_NOT_ENABLED, results: [] };
|
|
193
|
+
}
|
|
194
|
+
const resolveSelection = deps.resolveSelection ?? review_reviewer_selection_cjs_1.resolveReviewerSelection;
|
|
195
|
+
const selection = resolveSelection(input.selection);
|
|
196
|
+
if (selection.selected.length === 0) {
|
|
197
|
+
// Distinguish "explicitly requested but every candidate was unavailable" (a real failure —
|
|
198
|
+
// `errors` is non-empty) from "nothing was ever requested" (a clean, inert no-op).
|
|
199
|
+
const reason = selection.errors.length > 0
|
|
200
|
+
? exports.DISPATCH_REASON.SELECTION_FAILED
|
|
201
|
+
: exports.DISPATCH_REASON.NO_LANES_SELECTED;
|
|
202
|
+
return { dispatched: false, ok: selection.errors.length === 0, reason, selection, results: [] };
|
|
203
|
+
}
|
|
204
|
+
const pathCheck = validatePaths(input.repoRoot, input.paths);
|
|
205
|
+
if (!pathCheck.ok) {
|
|
206
|
+
return { dispatched: false, ok: false, reason: pathCheck.reason, selection, results: [] };
|
|
207
|
+
}
|
|
208
|
+
// #4209 RQ-04: depth/baseSha/repoRoot/runDir land in the SAME markdown prompt `paths` does
|
|
209
|
+
// (buildSourceReviewPrompt, `dispatchReviewerLanes`'s `runDir`-derived promptPath write) — a
|
|
210
|
+
// control character in any of them is the identical injection vector agy-F1 found in `paths`,
|
|
211
|
+
// so this trust boundary must reject it here too, not just for the file list.
|
|
212
|
+
if (typeof input.depth !== 'string' || input.depth.length === 0
|
|
213
|
+
|| typeof input.baseSha !== 'string' || input.baseSha.length === 0
|
|
214
|
+
|| typeof input.repoRoot !== 'string' || input.repoRoot.length === 0
|
|
215
|
+
|| typeof input.runDir !== 'string' || input.runDir.length === 0) {
|
|
216
|
+
return { dispatched: false, ok: false, reason: exports.DISPATCH_REASON.MISSING_PROVENANCE, selection, results: [] };
|
|
217
|
+
}
|
|
218
|
+
// #4209 WR-04: a present-but-malicious field (control character) is a different failure mode
|
|
219
|
+
// than an absent one — MISSING_PROVENANCE above means "the caller never supplied this"; this
|
|
220
|
+
// branch means "the caller supplied something and it's an injection attempt," which a caller
|
|
221
|
+
// handling the two reasons differently (e.g. surfacing one as a config problem, the other as
|
|
222
|
+
// a security event) must be able to tell apart.
|
|
223
|
+
if (CONTROL_CHAR.test(input.depth) || CONTROL_CHAR.test(input.baseSha)
|
|
224
|
+
|| CONTROL_CHAR.test(input.repoRoot) || CONTROL_CHAR.test(input.runDir)) {
|
|
225
|
+
return { dispatched: false, ok: false, reason: exports.DISPATCH_REASON.INVALID_PROVENANCE, selection, results: [] };
|
|
226
|
+
}
|
|
227
|
+
// #4209 WR-03 (considered, declined): gating `depth` to code-review's quick/standard/deep
|
|
228
|
+
// enum here would reject the deliberately capability-neutral case this function supports —
|
|
229
|
+
// see "a second, unrelated synthetic step context dispatches through the same function
|
|
230
|
+
// identically" below, which passes a wholly different depth vocabulary on purpose to prove
|
|
231
|
+
// this dispatcher has no code-review-specific special-casing. `depthMeaning()`'s `standard`
|
|
232
|
+
// fallback for an off-enum value is accepted, not a bug, for that reason.
|
|
233
|
+
const { configGet, getLane, plan } = deps;
|
|
234
|
+
const writePromptFile = deps.writePromptFile ?? defaultWritePromptFile;
|
|
235
|
+
const prompt = buildSourceReviewPrompt(input);
|
|
236
|
+
const estimatedTokens = (0, prompt_budget_cjs_1.estimateTokens)(prompt);
|
|
237
|
+
// Written once, before any lane's plan() runs: `promptPath` is derived from `runDir` alone
|
|
238
|
+
// (see `artifactPaths`), constant across every lane in this dispatch by construction — there
|
|
239
|
+
// is no per-lane variance to defend against, so writing it per-lane (as an earlier version of
|
|
240
|
+
// this function did) was pure redundancy, not a real safeguard.
|
|
241
|
+
// A hoisted, whole-dispatch write (see the doc comment above) that throws must not escape as
|
|
242
|
+
// an uncaught exception — no lane can succeed anyway if the shared prompt file was never
|
|
243
|
+
// written, so this is a dispatch-level halt like `validatePaths`/`MISSING_PROVENANCE` above,
|
|
244
|
+
// not a per-lane failure.
|
|
245
|
+
try {
|
|
246
|
+
writePromptFile((0, review_lane_invocation_cjs_1.artifactPaths)(input.runDir, '').promptPath, prompt);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return { dispatched: false, ok: false, reason: exports.DISPATCH_REASON.PROMPT_WRITE_FAILED, selection, results: [] };
|
|
250
|
+
}
|
|
251
|
+
const results = [];
|
|
252
|
+
// Never narrow the requested set: an explicit reviewer the selector could not resolve is
|
|
253
|
+
// already surfaced in `selection.errors` — reflect that in the aggregate `ok` even though
|
|
254
|
+
// lanes that DID resolve still run below and keep their own results.
|
|
255
|
+
let anyFailed = selection.errors.length > 0;
|
|
256
|
+
// Tracks whether any lane actually reached plan() — `dispatched` must stay false when every
|
|
257
|
+
// selected slug turned out to be unresolvable, even though a `results` entry was still pushed.
|
|
258
|
+
let planned = false;
|
|
259
|
+
for (const slug of selection.selected) {
|
|
260
|
+
const lane = getLane(slug);
|
|
261
|
+
if (!lane) {
|
|
262
|
+
results.push({ slug, ok: false, reason: 'malformed_lane', detail: 'no such declared lane' });
|
|
263
|
+
anyFailed = true;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
// #4209 review: a `promptChannel: 'none'` lane (coderabbit) is fed nothing and reviews
|
|
267
|
+
// whatever it independently sees fit (its own working-tree diff, review.md:367) rather than
|
|
268
|
+
// the bounded `paths`/`depth`/`baseSha` scope `buildSourceReviewPrompt` promises — silently
|
|
269
|
+
// dispatching it here would violate this interpreter's own scoped, metadata-only review
|
|
270
|
+
// contract. Reject before plan()/invoke() rather than let the mismatch surface as an
|
|
271
|
+
// unexplained out-of-scope review.
|
|
272
|
+
if (lane.transport === 'spawn' && lane.invoke.promptChannel === 'none') {
|
|
273
|
+
results.push({
|
|
274
|
+
slug,
|
|
275
|
+
ok: false,
|
|
276
|
+
reason: 'prompt_channel_unsupported',
|
|
277
|
+
detail: `lane '${slug}' declares promptChannel 'none' and cannot receive a scoped source-review prompt`,
|
|
278
|
+
});
|
|
279
|
+
anyFailed = true;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// A single throwing plan()/invoke() must not take down every sibling lane already collected
|
|
283
|
+
// in `results` — same rationale as gsd-tools.cjs's resolveLanePlan guard
|
|
284
|
+
// (#2494/#2605/#1698/#1936/#2073/#2176/#2589/#2794): belt and braces on purpose.
|
|
285
|
+
let planOutcome;
|
|
286
|
+
try {
|
|
287
|
+
planOutcome = plan(lane, { configGet, runDir: input.runDir, repoRoot: input.repoRoot });
|
|
288
|
+
}
|
|
289
|
+
catch (e) {
|
|
290
|
+
results.push({ slug, ok: false, reason: 'malformed_lane', detail: e instanceof Error ? e.message : String(e) });
|
|
291
|
+
anyFailed = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (!planOutcome.ok) {
|
|
295
|
+
results.push({ slug, ok: false, reason: planOutcome.reason, detail: planOutcome.detail });
|
|
296
|
+
anyFailed = true;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const budget = (0, review_lane_invocation_cjs_1.resolveLaneBudget)(lane, configGet);
|
|
300
|
+
if (budget !== null && budget !== 0 && estimatedTokens > budget) {
|
|
301
|
+
results.push({
|
|
302
|
+
slug,
|
|
303
|
+
ok: false,
|
|
304
|
+
reason: 'budget_exceeded',
|
|
305
|
+
detail: `estimated ${estimatedTokens} tokens exceeds resolved budget ${budget} for lane '${slug}'`,
|
|
306
|
+
});
|
|
307
|
+
anyFailed = true;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
planned = true;
|
|
311
|
+
let invokeOutcome;
|
|
312
|
+
try {
|
|
313
|
+
invokeOutcome = await deps.invoke(lane, planOutcome.plan);
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
results.push({ slug, ok: false, reason: 'invoke_failed', detail: e instanceof Error ? e.message : String(e) });
|
|
317
|
+
anyFailed = true;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!invokeOutcome.ok)
|
|
321
|
+
anyFailed = true;
|
|
322
|
+
results.push({
|
|
323
|
+
slug,
|
|
324
|
+
ok: invokeOutcome.ok,
|
|
325
|
+
reason: invokeOutcome.reason,
|
|
326
|
+
detail: invokeOutcome.detail,
|
|
327
|
+
reviewPath: invokeOutcome.reviewPath,
|
|
328
|
+
errPath: invokeOutcome.errPath,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
dispatched: planned,
|
|
333
|
+
ok: !anyFailed,
|
|
334
|
+
selection,
|
|
335
|
+
results,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
@@ -626,16 +626,17 @@ function hasPhaseEntries(markdown, phaseIdConvention) {
|
|
|
626
626
|
return collectTablePhaseRows(markdown).length > 0;
|
|
627
627
|
}
|
|
628
628
|
// ─── #3577: markdown-table phase listings ─────────────────────────────────────
|
|
629
|
-
// #3577: a GFM table declares phases when its header's FIRST cell is
|
|
630
|
-
// `Phase` (optionally `Phase #` / `Phase No.` / `Phase number`)
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
// their first cell (digit-bearing canonical shape —
|
|
635
|
-
// `---` delimiter rows are digit-free and excluded
|
|
636
|
-
// via stripFencedCode, matching the #3184 lesson:
|
|
637
|
-
// form is not a declared phase.
|
|
629
|
+
// #3577/#4480: a GFM table declares phases only when its header's FIRST cell is
|
|
630
|
+
// the literal `Phase` (optionally `Phase #` / `Phase No.` / `Phase number`) AND
|
|
631
|
+
// it positively identifies a `Name` or `Phase Name` column. This fails closed:
|
|
632
|
+
// ordinary progress/summary tables such as `| Phase | Status |` cannot mint a
|
|
633
|
+
// phase whose name is whichever value happens to occupy column two. Data rows
|
|
634
|
+
// carry the phase id in their first cell (digit-bearing canonical shape —
|
|
635
|
+
// `Phase`-word header cells and `---` delimiter rows are digit-free and excluded
|
|
636
|
+
// by construction). Fence-aware via stripFencedCode, matching the #3184 lesson:
|
|
637
|
+
// a fenced EXAMPLE of the table form is not a declared phase.
|
|
638
638
|
const PHASE_LISTING_HEADER_RE = /^\|?\s*phase(?:\s*(?:#|no\.?|number))?\s*\|/i;
|
|
639
|
+
const PHASE_NAME_HEADER_RE = /^(?:phase\s+)?name$/i;
|
|
639
640
|
const TABLE_PHASE_ID_RE = /^[A-Za-z]?\d[\w.-]*$/;
|
|
640
641
|
function collectTablePhaseRows(window) {
|
|
641
642
|
const unfenced = (0, markdown_sectionizer_cjs_1.stripFencedCode)(window).text;
|
|
@@ -645,8 +646,9 @@ function collectTablePhaseRows(window) {
|
|
|
645
646
|
if (!PHASE_LISTING_HEADER_RE.test(lines[i]))
|
|
646
647
|
continue;
|
|
647
648
|
const headerCells = (0, markdown_table_cjs_1.splitTableRow)(lines[i]);
|
|
648
|
-
|
|
649
|
-
|
|
649
|
+
const nameColumn = headerCells.findIndex((cell) => PHASE_NAME_HEADER_RE.test(cell));
|
|
650
|
+
if (nameColumn === -1)
|
|
651
|
+
continue;
|
|
650
652
|
if (!(0, markdown_table_cjs_1.isDelimiterRow)((0, markdown_table_cjs_1.splitTableRow)(lines[i + 1])))
|
|
651
653
|
continue;
|
|
652
654
|
for (let j = i + 2; j < lines.length; j++) {
|
|
@@ -662,7 +664,8 @@ function collectTablePhaseRows(window) {
|
|
|
662
664
|
if (!TABLE_PHASE_ID_RE.test(first))
|
|
663
665
|
continue;
|
|
664
666
|
if (!/^999\b/.test(first)) {
|
|
665
|
-
|
|
667
|
+
const name = cells[nameColumn];
|
|
668
|
+
rows.push({ id: first, name: name && name !== '' ? name : null, row: lines[j] });
|
|
666
669
|
}
|
|
667
670
|
}
|
|
668
671
|
}
|
|
@@ -1481,6 +1484,23 @@ function findRoadmapProgressTable(roadmapContent) {
|
|
|
1481
1484
|
function stripLeadingDelimiter(s) {
|
|
1482
1485
|
return s.replace(/^[\s—–:-]+/, '').trim();
|
|
1483
1486
|
}
|
|
1487
|
+
/**
|
|
1488
|
+
* #4134/#4433 (§7.2 rule 6 floor, applied symmetrically): a captured "name"
|
|
1489
|
+
* with no letter or digit anywhere is heading/bullet STRUCTURE, not a curated
|
|
1490
|
+
* name — e.g. the trailing `)` a name-then-version heading leaves after its
|
|
1491
|
+
* version token, or a malformed 🚧-bullet whose only content past the version
|
|
1492
|
+
* is punctuation (`---`, `***`, a lone `:`). #4134 fixed this for
|
|
1493
|
+
* `extractMilestoneHeadingName`'s heading path only; #4433 found the sibling
|
|
1494
|
+
* 🚧-bullet capture (`getMilestoneInfo`'s `listMatch`) and the no-STATE.md
|
|
1495
|
+
* fallback (`inProgressMatch`) both skipped straight to a bare truthiness
|
|
1496
|
+
* check, so a punctuation-only bullet name passed through as a real one. This
|
|
1497
|
+
* is now the SOLE name-validity predicate — every capture site in this file
|
|
1498
|
+
* calls it instead of re-deriving the character class. A name that merely
|
|
1499
|
+
* CONTAINS punctuation is unaffected; digits alone qualify.
|
|
1500
|
+
*/
|
|
1501
|
+
function hasNameableContent(s) {
|
|
1502
|
+
return /[\p{L}\p{N}]/u.test(s);
|
|
1503
|
+
}
|
|
1484
1504
|
/**
|
|
1485
1505
|
* #3216 (ADR-3180 §7.2's "Name extraction — pinned rule"): the sole "milestone
|
|
1486
1506
|
* heading text → version + curated name" rule. Strips everything through the
|
|
@@ -1493,6 +1513,13 @@ function stripLeadingDelimiter(s) {
|
|
|
1493
1513
|
* one implementation. Returns `null` when `headingText` carries no version
|
|
1494
1514
|
* token at all (e.g. a non-milestone heading reached this by mistake).
|
|
1495
1515
|
*
|
|
1516
|
+
* #4134 (§7.2 rule 6 floor): the rule's direction assumes version-then-name.
|
|
1517
|
+
* A name-then-version heading (`# Roadmap: Project — Name (v1.13)`) leaves a
|
|
1518
|
+
* punctuation fragment (`)`) after the token; a remainder with no letter or
|
|
1519
|
+
* digit anywhere is heading structure, not a curated name, and is refused as
|
|
1520
|
+
* `name: null` so callers report the honest rule-6 answer instead of
|
|
1521
|
+
* fabricating garbage. Names that merely CONTAIN punctuation are unaffected.
|
|
1522
|
+
*
|
|
1496
1523
|
* @param expectedVersion - When the caller already knows the exact version it
|
|
1497
1524
|
* is looking for (the STATE-anchored `getMilestoneInfo` path, which located
|
|
1498
1525
|
* this heading via `selectMilestoneHeading(roadmap, stateVersion)`), pass it
|
|
@@ -1529,7 +1556,18 @@ function extractMilestoneHeadingName(headingText, expectedVersion) {
|
|
|
1529
1556
|
// whitespace — the marker is already carried structurally by `closed`, so
|
|
1530
1557
|
// duplicating it inside `name` (e.g. "Old ✅") is redundant and wrong. Only
|
|
1531
1558
|
// these three markers, only at the end; a marker inside a name is untouched.
|
|
1532
|
-
const
|
|
1559
|
+
const candidate = stripLeadingDelimiter(afterVersion).replace(/\s*(?:[✅📋🚧]\s*)+$/, '') || null;
|
|
1560
|
+
// #4134 (§7.2 rule 6 floor): a "name" with no letter or digit anywhere is
|
|
1561
|
+
// heading structure, not a curated name. The pinned rule takes everything
|
|
1562
|
+
// AFTER the version token, so a name-then-version heading (`# Roadmap:
|
|
1563
|
+
// Project — Name (v1.13)` — the shape a first-ever ROADMAP.md drifts into)
|
|
1564
|
+
// leaves exactly `)` there, which used to be returned as a COMPLETE-scope
|
|
1565
|
+
// name and propagated into init.* output and STATE.md. Refuse it: callers
|
|
1566
|
+
// already report the honest rule-6 answer (version kept, `name: null`,
|
|
1567
|
+
// scope TRUNCATED) for an unresolvable name. A name that merely CONTAINS
|
|
1568
|
+
// punctuation is untouched — `(` is an ordinary name character (#3171) —
|
|
1569
|
+
// and digits alone qualify (`## v4.0 — 42` is the name `42`).
|
|
1570
|
+
const name = candidate !== null && hasNameableContent(candidate) ? candidate : null;
|
|
1533
1571
|
return { version, name };
|
|
1534
1572
|
}
|
|
1535
1573
|
/**
|
|
@@ -1614,7 +1652,7 @@ function getMilestoneInfo(cwd) {
|
|
|
1614
1652
|
const listMatch = roadmap.match(new RegExp(`🚧\\s*\\*?\\*?${escapedVer}\\*?\\*?\\s+([^*\\n]+)`, 'i'));
|
|
1615
1653
|
if (listMatch) {
|
|
1616
1654
|
const name = stripLeadingDelimiter(listMatch[1]);
|
|
1617
|
-
if (name)
|
|
1655
|
+
if (name && hasNameableContent(name))
|
|
1618
1656
|
return scoped({ version: stateVersion, name }, SCOPE.COMPLETE);
|
|
1619
1657
|
}
|
|
1620
1658
|
// #3216: heading selection routes through the shared owner
|
|
@@ -1648,7 +1686,10 @@ function getMilestoneInfo(cwd) {
|
|
|
1648
1686
|
// (unchanged from the pre-#3216 fallback).
|
|
1649
1687
|
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/);
|
|
1650
1688
|
if (inProgressMatch) {
|
|
1651
|
-
|
|
1689
|
+
const inProgressName = inProgressMatch[2].trim();
|
|
1690
|
+
if (hasNameableContent(inProgressName)) {
|
|
1691
|
+
return scoped({ version: 'v' + inProgressMatch[1], name: inProgressName }, SCOPE.COMPLETE);
|
|
1692
|
+
}
|
|
1652
1693
|
}
|
|
1653
1694
|
// #3216: enumerate every OPEN (non-shipped) milestone heading via the
|
|
1654
1695
|
// shared owner and take the first in document order — deletes the
|