@opengsd/gsd-core 1.13.0 → 1.15.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/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- 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 +459 -0
- package/agents/gsd-code-fixer.md +9 -8
- 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 +360 -0
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- 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-auditor.md +1 -1
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-executor.md +13 -8
- 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-intel-updater.md +1 -1
- 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-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-research-synthesizer.md +1 -1
- 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-auditor.md +155 -17
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +848 -163
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- 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 +8 -3
- 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/plan-review-convergence.md +6 -6
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/review.md +2 -3
- 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 +672 -146
- package/gsd-core/bin/lib/adr-parser.cjs +4 -2
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +119 -34
- 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 +96 -189
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -2
- package/gsd-core/bin/lib/check-command-router.cjs +213 -49
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +823 -112
- package/gsd-core/bin/lib/config-loader.cjs +66 -4
- package/gsd-core/bin/lib/config.cjs +186 -45
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +164 -45
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- 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/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +614 -86
- package/gsd-core/bin/lib/install-engine.cjs +29 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
- 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 +37 -13
- package/gsd-core/bin/lib/model-resolver.cjs +253 -53
- package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +268 -27
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +393 -88
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- 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-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +356 -42
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- 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 +181 -30
- package/gsd-core/bin/lib/state.cjs +265 -27
- package/gsd-core/bin/lib/surface.cjs +77 -3
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +9 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- 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 +315 -30
- package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
- package/gsd-core/bin/lib/verify.cjs +320 -48
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
- package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +13 -4
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +14 -2
- package/gsd-core/references/tdd.md +30 -4
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +433 -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/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +29 -16
- package/gsd-core/workflows/check-todos.md +6 -4
- package/gsd-core/workflows/cleanup.md +5 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +216 -73
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +41 -264
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +17 -158
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +83 -172
- package/gsd-core/workflows/execute-plan.md +24 -10
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +52 -5
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +56 -14
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +39 -209
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +45 -187
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +132 -20
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +29 -10
- package/gsd-core/workflows/reapply-patches.md +86 -6
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +22 -9
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +14 -12
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +101 -196
- package/hooks/dist/gsd-agent-isolation-guard.js +66 -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 +71 -19
- package/hooks/dist/gsd-statusline.js +81 -20
- package/hooks/dist/gsd-validate-commit.sh +97 -8
- 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 +66 -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 +71 -19
- package/hooks/gsd-statusline.js +81 -20
- package/hooks/gsd-validate-commit.sh +97 -8
- 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 +11 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +131 -12
- 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/command-contract-helpers.cjs +3 -0
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +35 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-loop-host-contract.cjs +69 -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 +224 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +287 -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 +47 -3
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +623 -13
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +29 -9
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
- package/scripts/prompt-injection-scan.sh +18 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- 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 +8 -3
- 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-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review/SKILL.md +2 -3
- 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
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* state signal-resume Remove WAITING.json signal
|
|
20
20
|
* resolve-model <agent-type> Get model for agent based on profile
|
|
21
21
|
* find-phase <phase> Find phase directory by number
|
|
22
|
+
* select-revert-commits (--phase P | --plan NN-MM) --range R
|
|
23
|
+
* List commits within R whose DECLARED conventional-commit
|
|
24
|
+
* scope identifies P or NN-MM (exact match; phase mode also
|
|
25
|
+
* accepts a NN-MM-scoped commit). Used by /gsd:undo (#4661).
|
|
22
26
|
* commit <message> [--files f1 f2] [--no-verify] Commit planning docs
|
|
23
27
|
* commit-docs-guard enable|disable Opt-in .git/hooks/pre-commit guard
|
|
24
28
|
* that refuses a commit staging
|
|
@@ -60,7 +64,7 @@
|
|
|
60
64
|
* roadmap update-plan-progress <N> Update progress table row from disk (PLAN vs SUMMARY counts)
|
|
61
65
|
* roadmap annotate-dependencies <N> Add wave dependency notes + cross-cutting constraints to ROADMAP.md
|
|
62
66
|
* roadmap validate Validate phase ID convention compliance
|
|
63
|
-
* roadmap upgrade [--apply] --convention milestone-prefixed Migrate phase IDs
|
|
67
|
+
* roadmap upgrade [--apply] --convention <milestone-prefixed|bracket> Migrate phase IDs (dry-run by default)
|
|
64
68
|
*
|
|
65
69
|
* Requirements Operations:
|
|
66
70
|
* requirements mark-complete <ids> Mark requirement IDs as complete in REQUIREMENTS.md
|
|
@@ -438,8 +442,9 @@ function dispatchCapabilityCommand({ command, args, cwd, raw, error, registry, r
|
|
|
438
442
|
// Step 2: confinement check — belt-and-suspenders even after the basename
|
|
439
443
|
// validation above. Resolved path must be inside libDir (not equal to it,
|
|
440
444
|
// and must start with libDir + sep so "libDir-suffix" can't sneak through).
|
|
441
|
-
const
|
|
442
|
-
|
|
445
|
+
const { tryWithinRootLexical } = require('./lib/security.cjs');
|
|
446
|
+
const resolved = tryWithinRootLexical(m, libDir);
|
|
447
|
+
if (resolved === null || resolved === path.resolve(libDir)) {
|
|
443
448
|
throw new Error('capability module path escapes bin/lib/: ' + JSON.stringify(m));
|
|
444
449
|
}
|
|
445
450
|
// Step 3: require the resolved absolute path — the SAME representation that
|
|
@@ -515,18 +520,19 @@ function defaultRequireFromInstallRoot(installRoot, m) {
|
|
|
515
520
|
if (typeof m !== 'string' || !/^[A-Za-z0-9._-]+\.cjs$/.test(m)) {
|
|
516
521
|
throw new Error('capability module must be a bare .cjs basename: ' + JSON.stringify(m));
|
|
517
522
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
if (
|
|
523
|
+
const { tryWithinRoot, tryWithinRootLexical, PathAcceptance } = require('./lib/security.cjs');
|
|
524
|
+
// Lexical containment check: a symlinked ancestor can't widen confinement.
|
|
525
|
+
const lexical = tryWithinRootLexical(m, installRoot);
|
|
526
|
+
if (lexical === null || lexical === path.resolve(installRoot)) {
|
|
522
527
|
throw new Error('capability module path escapes its install root: ' + JSON.stringify(m));
|
|
523
528
|
}
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
529
|
+
// Realpath/symlink check: the module file itself must not be a symlink
|
|
530
|
+
// pointing outside the root.
|
|
531
|
+
const real = tryWithinRoot(m, installRoot, PathAcceptance.RelativeOnly);
|
|
532
|
+
if (real === null) {
|
|
527
533
|
throw new Error('capability module resolves outside its install root (symlink): ' + JSON.stringify(m));
|
|
528
534
|
}
|
|
529
|
-
return require(
|
|
535
|
+
return require(real);
|
|
530
536
|
}
|
|
531
537
|
|
|
532
538
|
/**
|
|
@@ -784,7 +790,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
784
790
|
error,
|
|
785
791
|
output: output,
|
|
786
792
|
});
|
|
787
|
-
if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw);
|
|
793
|
+
if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw, { dryRun: args.includes('--dry-run') });
|
|
788
794
|
}
|
|
789
795
|
|
|
790
796
|
function routeConfigSetModelProfile({ args, cwd, raw }) {
|
|
@@ -947,18 +953,243 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
947
953
|
if (!handled) phase.cmdFindPhase(cwd, args[1], raw);
|
|
948
954
|
}
|
|
949
955
|
|
|
956
|
+
/**
|
|
957
|
+
* #4906 Phase 5 (issue #4661, absorbed from the original bug report):
|
|
958
|
+
* `gsd-tools.cjs query select-revert-commits --phase <id>|--plan <id>
|
|
959
|
+
* --range <git-revision-range>`.
|
|
960
|
+
*
|
|
961
|
+
* Replaces the two `git log --oneline | grep -E ...` pipelines
|
|
962
|
+
* `gsd-core/workflows/undo.md` used to run for `/gsd:undo --phase`/`--plan`
|
|
963
|
+
* selection. `--range` is the ALREADY-COMPUTED `$UNDO_RANGE` the workflow's
|
|
964
|
+
* own phase-directory anchor logic derives — this subcommand changes only
|
|
965
|
+
* the subject-matching step, never the ranging behavior.
|
|
966
|
+
*
|
|
967
|
+
* Validates `--phase`/`--range <id>` via `validatePhaseNumber`
|
|
968
|
+
* (src/security.cts) BEFORE running any git command — an invalid id is
|
|
969
|
+
* refused here, never reaching git or the selection regex (closes bug
|
|
970
|
+
* class 1 of #4661 by construction). A `--plan NN-MM` id is two
|
|
971
|
+
* phase-number-shaped segments joined by the FIRST `-`: `validatePhaseNumber`'s
|
|
972
|
+
* bracket-style alternative requires a leading letter, so a plain numeric
|
|
973
|
+
* plan id never matches it whole — each segment is validated on its own
|
|
974
|
+
* instead of forking a second grammar.
|
|
975
|
+
*
|
|
976
|
+
* Selection itself is delegated to `selectCommitsByDeclaredScope`
|
|
977
|
+
* (src/undo-commit-selection.cts), which parses each commit subject
|
|
978
|
+
* through the SAME anchored conventional-commit header regex the
|
|
979
|
+
* changelog/PR-title gate uses and compares the declared scope to the
|
|
980
|
+
* validated id by exact string equality — no regex is ever built from
|
|
981
|
+
* user input.
|
|
982
|
+
*
|
|
983
|
+
* Output: JSON `{selected, classified}` by default; with `--raw`, one
|
|
984
|
+
* `<full-sha> <subject>` line per selected commit (preserves the visual
|
|
985
|
+
* shape `git log --oneline` produced, so the workflow's downstream
|
|
986
|
+
* confirm-screen display, >50 truncation count, and `git revert
|
|
987
|
+
* --no-commit` loop over COMMITS are unchanged).
|
|
988
|
+
*/
|
|
989
|
+
function routeSelectRevertCommits({ args, cwd, raw, error, invokingCwd }) {
|
|
990
|
+
// #4906/#4465 review fix: `cwd` here has already been remapped to the
|
|
991
|
+
// MAIN worktree root by `resolveMainWorktreeCwd` (main(), applied
|
|
992
|
+
// BLANKET to every command before dispatch — correct for a router that
|
|
993
|
+
// reads `.planning/`, e.g. `find-phase`, but wrong for this one). This
|
|
994
|
+
// command reads NO `.planning/` content — it only runs `git log` — and
|
|
995
|
+
// that git operation must see the CALLER's own worktree: its HEAD, its
|
|
996
|
+
// branch, its reachable commit range. Using the remapped `cwd` instead
|
|
997
|
+
// silently resolves `--range` against a DIFFERENT repository checkout
|
|
998
|
+
// (reproduced: from a linked worktree with no local `.planning/`, a
|
|
999
|
+
// `--range HEAD~3..HEAD` that succeeds when run directly in that
|
|
1000
|
+
// worktree fails with "ambiguous argument" through this command,
|
|
1001
|
+
// because it silently ran against main's shorter HEAD instead —
|
|
1002
|
+
// tests/undo-commit-selection-4465.test.cjs, "linked worktree: the main
|
|
1003
|
+
// worktree's planning is the same repository, and is not refused").
|
|
1004
|
+
// `invokingCwd` (threaded from main() through dispatchHostCommand) is
|
|
1005
|
+
// the pre-remap value; falling back to `cwd` keeps direct callers that
|
|
1006
|
+
// omit it (e.g. `dispatchHostCommand` invoked without the new field)
|
|
1007
|
+
// working exactly as before rather than crashing on `undefined`.
|
|
1008
|
+
const gitCwd = invokingCwd || cwd;
|
|
1009
|
+
const { validatePhaseNumber } = require('./lib/security.cjs');
|
|
1010
|
+
const { normalizePhaseName } = require('./lib/phase-id.cjs');
|
|
1011
|
+
const { selectCommitsByDeclaredScope } = require('./lib/undo-commit-selection.cjs');
|
|
1012
|
+
const { execGit } = require('./lib/shell-command-projection.cjs');
|
|
1013
|
+
|
|
1014
|
+
// #4906 Phase 5 review fix: the RETIRED phase-mode grep's `0*` prefix
|
|
1015
|
+
// (`\(0*${TARGET_PHASE}(-[0-9]+)?\):`) tolerated an UNPADDED user-supplied
|
|
1016
|
+
// phase number against a zero-padded commit scope — `--phase 3` matched
|
|
1017
|
+
// `feat(03-01): ...` because `0*3` matches the literal text `03`.
|
|
1018
|
+
// `selectCommitsByDeclaredScope` compares by exact string equality, and
|
|
1019
|
+
// `gsd-core/workflows/undo.md`'s parse_arguments step passes `TARGET_PHASE`
|
|
1020
|
+
// through with NO normalization, so without this, `--phase 3` would
|
|
1021
|
+
// silently select nothing against real (zero-padded) commit scopes — a
|
|
1022
|
+
// behavior regression, not just a documented known-limit. Zero-pad via
|
|
1023
|
+
// `normalizePhaseName` (src/phase-id.cts), the canonical owner of this
|
|
1024
|
+
// exact operation (confirmed: `normalizePhaseName('3') === '03'`).
|
|
1025
|
+
//
|
|
1026
|
+
// GUARDED to digit-first ids only: `normalizePhaseName` also runs
|
|
1027
|
+
// `stripProjectCodePrefix`, which strips a leading `LETTERS-`-shaped
|
|
1028
|
+
// prefix UNCONDITIONALLY — `normalizePhaseName('PROJ-42') === '42'`,
|
|
1029
|
+
// discarding the "PROJ-" entirely (confirmed by direct call). That is
|
|
1030
|
+
// exactly the shape `validatePhaseNumber`'s bracket-style alternative
|
|
1031
|
+
// legitimately accepts as a phase id in ITS OWN right (`PROJ-42`,
|
|
1032
|
+
// `AUTH-101` — see tests/security.test.cjs). Running a letter-first id
|
|
1033
|
+
// through it would silently widen the match (a wildcard-style
|
|
1034
|
+
// over-match reintroducing bug classes 1/2). The old grep's `0*` was
|
|
1035
|
+
// never meaningful for a letter-first target anyway — `0*` before a
|
|
1036
|
+
// non-'0' character matches zero repetitions, a no-op — so a
|
|
1037
|
+
// letter-first id needs neither padding nor this normalization.
|
|
1038
|
+
// `PROJECT_CODE_PREFIX_STRIP_RE_I` (`src/phase-id.cts:33`) itself
|
|
1039
|
+
// requires `^[A-Z]`, so it can never fire on a digit-first id — the
|
|
1040
|
+
// guard below is therefore exactly the safe/unsafe boundary.
|
|
1041
|
+
const normalizePaddedPhase = (id) => (/^\d/.test(id) ? normalizePhaseName(id) : id);
|
|
1042
|
+
|
|
1043
|
+
const parsed = parseNamedArgsOrExit(
|
|
1044
|
+
args,
|
|
1045
|
+
{ valueFlags: ['phase', 'plan', 'range'], positionals: 1 },
|
|
1046
|
+
error,
|
|
1047
|
+
);
|
|
1048
|
+
const rawPhase = parsed.phase;
|
|
1049
|
+
const rawPlan = parsed.plan;
|
|
1050
|
+
const range = parsed.range;
|
|
1051
|
+
|
|
1052
|
+
if ((rawPhase && rawPlan) || (!rawPhase && !rawPlan)) {
|
|
1053
|
+
error('select-revert-commits requires exactly one of --phase <id> or --plan <id>', ERROR_REASON.USAGE);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (!range) {
|
|
1057
|
+
error('select-revert-commits requires --range <git-revision-range>', ERROR_REASON.USAGE);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
// Belt-and-suspenders alongside the --end-of-options guard below (mirrors
|
|
1061
|
+
// src/git-base-branch.cts's isSafeRevisionRef posture for the same class
|
|
1062
|
+
// of argument): refuse an option-shaped range before it ever reaches git.
|
|
1063
|
+
if (typeof range !== 'string' || range.startsWith('-')) {
|
|
1064
|
+
error(`Invalid --range: ${JSON.stringify(range)}`, ERROR_REASON.USAGE);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
let mode;
|
|
1069
|
+
let normalizedId;
|
|
1070
|
+
if (rawPhase) {
|
|
1071
|
+
mode = 'phase';
|
|
1072
|
+
const check = validatePhaseNumber(rawPhase);
|
|
1073
|
+
if (!check.valid) {
|
|
1074
|
+
error(`Invalid --phase: ${check.error}`, ERROR_REASON.USAGE);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
normalizedId = normalizePaddedPhase(check.normalized);
|
|
1078
|
+
} else {
|
|
1079
|
+
mode = 'plan';
|
|
1080
|
+
// Split on the FIRST `-` only, giving exactly two segments (phase,
|
|
1081
|
+
// plan). This is deliberately NOT a general N-segment composite-id
|
|
1082
|
+
// parser: checked against docs/reference/plan-md.md (frontmatter
|
|
1083
|
+
// `plan` field + `.planning/phases/<NN>-<slug>/<NN>-<PP>-PLAN.md`
|
|
1084
|
+
// layout) and every `--plan` usage in gsd-core/workflows/*.md, the only
|
|
1085
|
+
// plan-id shape this repo documents or emits is the plain two-segment
|
|
1086
|
+
// `NN-MM`. A THREE-segment purely-numeric shape (`NN-MM-PP`) does exist
|
|
1087
|
+
// elsewhere in the codebase, but as a MILESTONE-phase-plan composite
|
|
1088
|
+
// under the (already-deprecated-forward) `milestone-prefixed`
|
|
1089
|
+
// `phase_id_convention` — a different id space than phase-plan, which
|
|
1090
|
+
// `docs/adr/612-bracket-phase-id-convention.md:15` itself documents as
|
|
1091
|
+
// having "no deterministic parse" once a token carries both a
|
|
1092
|
+
// milestone-joined phase AND a plan (the exact ambiguity that
|
|
1093
|
+
// motivated bracket's own `[PROJECT.MM] PP-PP` grammar). No function in
|
|
1094
|
+
// `src/phase-id.cts` validates the phase-PLAN grammar end-to-end
|
|
1095
|
+
// either: `parsePhaseId` explicitly REJECTS a bare `NN-MM` token by
|
|
1096
|
+
// design (its own doc comment lists `02-04` as a rejected "ambiguous /
|
|
1097
|
+
// bare token"), and `getPhaseDirFromPhaseId`'s N-segment dash grammar
|
|
1098
|
+
// is a MILESTONE-phase(-subphase) directory-name constructor — a
|
|
1099
|
+
// structurally similar but semantically different id space (first
|
|
1100
|
+
// segment = milestone, not phase) whose return value doesn't map back
|
|
1101
|
+
// to (phase, plan) anyway. Given no suitable whole-string validator
|
|
1102
|
+
// exists and no real usage needs more than two segments, a `NN-MM-PP`
|
|
1103
|
+
// id is refused today (see
|
|
1104
|
+
// tests/undo-commit-selection.test.cjs "refuses a 3-segment plan id"),
|
|
1105
|
+
// not silently mis-parsed — extending this to N segments is a product
|
|
1106
|
+
// decision for whichever future issue actually needs it. The same
|
|
1107
|
+
// "only the plain two-segment shape is supported" standard applies to
|
|
1108
|
+
// a bracket-style PHASE segment here too: `--plan PROJ-42-01` splits
|
|
1109
|
+
// on the first `-` into phase segment `PROJ` (no internal dash) and
|
|
1110
|
+
// plan segment `42-01`, and `validatePhaseNumber`'s bracket branch
|
|
1111
|
+
// requires the internal dash on ITS side (`PROJ-42`, not bare `PROJ`),
|
|
1112
|
+
// so the phase segment fails and the whole id is refused — bracket-style
|
|
1113
|
+
// ids are supported for `--phase` only, never as a `--plan` segment (no
|
|
1114
|
+
// real usage found; see tests/undo-commit-selection.test.cjs "bracket-
|
|
1115
|
+
// style phase segment in --plan is refused").
|
|
1116
|
+
const dashIdx = rawPlan.indexOf('-');
|
|
1117
|
+
if (dashIdx === -1) {
|
|
1118
|
+
error(`Invalid --plan: "${rawPlan}" (expected NN-MM)`, ERROR_REASON.USAGE);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const phasePart = rawPlan.slice(0, dashIdx);
|
|
1122
|
+
const planPart = rawPlan.slice(dashIdx + 1);
|
|
1123
|
+
const phaseCheck = validatePhaseNumber(phasePart);
|
|
1124
|
+
if (!phaseCheck.valid) {
|
|
1125
|
+
error(`Invalid --plan: "${rawPlan}" (phase segment: ${phaseCheck.error})`, ERROR_REASON.USAGE);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const planCheck = validatePhaseNumber(planPart);
|
|
1129
|
+
if (!planCheck.valid) {
|
|
1130
|
+
error(`Invalid --plan: "${rawPlan}" (plan segment: ${planCheck.error})`, ERROR_REASON.USAGE);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
normalizedId = `${normalizePaddedPhase(phaseCheck.normalized)}-${normalizePaddedPhase(planCheck.normalized)}`;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const GIT_LOG_TIMEOUT_MS = 15000;
|
|
1137
|
+
const gitResult = execGit(
|
|
1138
|
+
['log', '--format=%H%x00%s', '--no-merges', '--no-decorate', '--end-of-options', range],
|
|
1139
|
+
{ cwd: gitCwd, timeout: GIT_LOG_TIMEOUT_MS },
|
|
1140
|
+
);
|
|
1141
|
+
if (gitResult.exitCode !== 0) {
|
|
1142
|
+
// `execGit` (src/shell-command-projection.cts) exposes `timedOut` on
|
|
1143
|
+
// its SpawnResultOutput — thread it through so a hung `git log` reads
|
|
1144
|
+
// distinctly from an ordinary non-zero exit with no stderr, which
|
|
1145
|
+
// otherwise both surfaced as the same unhelpful "(no stderr)" message.
|
|
1146
|
+
const gitFailureDetail = gitResult.timedOut
|
|
1147
|
+
? `git log timed out after ${GIT_LOG_TIMEOUT_MS}ms`
|
|
1148
|
+
: `git log failed: ${gitResult.stderr || '(no stderr)'}`;
|
|
1149
|
+
error(
|
|
1150
|
+
`select-revert-commits: ${gitFailureDetail} for range "${range}"`,
|
|
1151
|
+
ERROR_REASON.USAGE,
|
|
1152
|
+
);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const commits = (gitResult.stdout ? gitResult.stdout.split('\n') : [])
|
|
1157
|
+
.filter((line) => line.length > 0)
|
|
1158
|
+
.map((line) => {
|
|
1159
|
+
const nulIdx = line.indexOf('\0');
|
|
1160
|
+
return nulIdx === -1 ? null : { sha: line.slice(0, nulIdx), subject: line.slice(nulIdx + 1) };
|
|
1161
|
+
})
|
|
1162
|
+
.filter((c) => c !== null);
|
|
1163
|
+
|
|
1164
|
+
const result = selectCommitsByDeclaredScope(commits, normalizedId, mode);
|
|
1165
|
+
const rawLines = result.selected.map((c) => `${c.sha} ${c.subject}`).join('\n');
|
|
1166
|
+
output(result, raw, rawLines);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
950
1169
|
function routeCommit({ args, cwd, raw, error }) {
|
|
951
1170
|
const amend = args.includes('--amend');
|
|
952
1171
|
const noVerify = args.includes('--no-verify');
|
|
953
|
-
|
|
1172
|
+
// #4208: `--files` and `--files-removed` are two path lists, each
|
|
1173
|
+
// running from its flag to the NEXT LIST FLAG. A boolean flag
|
|
1174
|
+
// inside a list (`--files a --amend b`) is skipped, not a
|
|
1175
|
+
// terminator: that is what the previous slice-to-end collection
|
|
1176
|
+
// did (it filtered `--` tokens and kept everything else), and a
|
|
1177
|
+
// list that stopped at any `--` token silently dropped `b`
|
|
1178
|
+
// (review of #4253). The previous form could not carry a second
|
|
1179
|
+
// list flag at all, which is the only thing that changed.
|
|
1180
|
+
// A REPEATED list flag (`--files a --files b`) merges, as the old
|
|
1181
|
+
// slice-to-end parse merged it: every occurrence contributes its
|
|
1182
|
+
// run, and none of them ends another's silently.
|
|
1183
|
+
const firstListFlag = args.findIndex((a, i) => i > 0 && COMMIT_LIST_FLAGS.has(a));
|
|
954
1184
|
// Collect all positional args between command name and first flag,
|
|
955
1185
|
// then join them — handles both quoted ("multi word msg") and
|
|
956
1186
|
// unquoted (multi word msg) invocations from different shells
|
|
957
|
-
const endIndex =
|
|
1187
|
+
const endIndex = firstListFlag !== -1 ? firstListFlag : args.length;
|
|
958
1188
|
const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
|
|
959
1189
|
const message = messageArgs.join(' ') || undefined;
|
|
960
|
-
const files =
|
|
961
|
-
|
|
1190
|
+
const files = collectListFlagValues(args, '--files');
|
|
1191
|
+
const filesRemoved = collectListFlagValues(args, '--files-removed');
|
|
1192
|
+
commands.cmdCommit(cwd, message, files, raw, amend, noVerify, filesRemoved);
|
|
962
1193
|
}
|
|
963
1194
|
|
|
964
1195
|
function routeCheckCommit({ args, cwd, raw, error }) {
|
|
@@ -1169,7 +1400,18 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1169
1400
|
// First positional that isn't a flag also works (lenient); otherwise ignore unknown flags.
|
|
1170
1401
|
if (!a.startsWith('-') && !pifRuntime) { pifRuntime = a; }
|
|
1171
1402
|
}
|
|
1172
|
-
|
|
1403
|
+
// A retired runtime id now THROWS rather than resolving (#4709 AC#1).
|
|
1404
|
+
// Map it to the same clean single-line error routeSkillsRoot emits for
|
|
1405
|
+
// an unknown runtime — a CLI must not answer a bad flag value with a
|
|
1406
|
+
// stack trace. The thrown message already names the successor and the
|
|
1407
|
+
// retiring issue, so it is surfaced verbatim.
|
|
1408
|
+
let filename;
|
|
1409
|
+
try {
|
|
1410
|
+
filename = getProjectInstructionFile(pifRuntime);
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
if (err && err.code === 'GSD_RETIRED_RUNTIME') error(err.message);
|
|
1413
|
+
throw err;
|
|
1414
|
+
}
|
|
1173
1415
|
process.stdout.write(filename + '\n');
|
|
1174
1416
|
}
|
|
1175
1417
|
|
|
@@ -1238,9 +1480,21 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1238
1480
|
// claims. `--directory` wins outright when given explicitly;
|
|
1239
1481
|
// otherwise a supplied `--quick-id` + `--slug` pair derives the
|
|
1240
1482
|
// canonical permalink the same way `workflows/quick.md` renders it.
|
|
1483
|
+
// #4906 Phase 3 (#4958, evidence #4736): `--status` is the OPTIONAL
|
|
1484
|
+
// widening that closes the last gap between this CLI and
|
|
1485
|
+
// `workflows/quick.md`'s own Step 7c row shapes. `appendQuickTaskRow`
|
|
1486
|
+
// (markdown-table.cjs) has always accepted `status` — the schema's
|
|
1487
|
+
// `Status` column exists precisely for `$VALIDATE_MODE` runs — but
|
|
1488
|
+
// nothing on this CLI surface could set it, so quick.md's
|
|
1489
|
+
// VALIDATE_MODE branch had no escaping-safe path and kept
|
|
1490
|
+
// interpolating `${DESCRIPTION}` into raw markdown (#4736 D1: an
|
|
1491
|
+
// unescaped `|` in a task description permanently rags the table).
|
|
1492
|
+
// Omitted, `status` stays undefined and `appendQuickTaskRow` falls
|
|
1493
|
+
// back to its own `'—'` default — unchanged for every existing
|
|
1494
|
+
// caller (fast.md, quick.md's non-VALIDATE_MODE branch).
|
|
1241
1495
|
const qtaParsed = parseNamedArgsOrExit(
|
|
1242
1496
|
qtaArgs,
|
|
1243
|
-
{ valueFlags: ['task', 'quick-id', 'slug', 'directory'], positionals: 'rest' },
|
|
1497
|
+
{ valueFlags: ['task', 'quick-id', 'slug', 'directory', 'status'], positionals: 'rest' },
|
|
1244
1498
|
error,
|
|
1245
1499
|
);
|
|
1246
1500
|
const qtaTask = qtaParsed.task || args[1];
|
|
@@ -1251,6 +1505,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1251
1505
|
const qtaSlug = qtaParsed['slug'] || undefined;
|
|
1252
1506
|
const qtaDirectory = qtaParsed['directory']
|
|
1253
1507
|
|| (qtaQuickId && qtaSlug ? `[${qtaQuickId}-${qtaSlug}](./quick/${qtaQuickId}-${qtaSlug}/)` : undefined);
|
|
1508
|
+
const qtaStatus = qtaParsed['status'] || undefined;
|
|
1254
1509
|
|
|
1255
1510
|
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
|
1256
1511
|
if (!fs.existsSync(statePath)) {
|
|
@@ -1288,6 +1543,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1288
1543
|
description: qtaTask,
|
|
1289
1544
|
date,
|
|
1290
1545
|
commit,
|
|
1546
|
+
status: qtaStatus,
|
|
1291
1547
|
quickId: qtaQuickId,
|
|
1292
1548
|
directory: qtaDirectory,
|
|
1293
1549
|
});
|
|
@@ -1322,7 +1578,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1322
1578
|
const fsx = require('node:fs');
|
|
1323
1579
|
const os = require('node:os');
|
|
1324
1580
|
const { REVIEWER_LANES, mergeReviewerLanes } = require('./lib/review-lane-descriptor.cjs');
|
|
1325
|
-
const { resolveLanePlan, resolveLaneEffort } = require('./lib/review-lane-invocation.cjs');
|
|
1581
|
+
const { resolveLanePlan, resolveLaneEffort, resolveLaneBudget } = require('./lib/review-lane-invocation.cjs');
|
|
1326
1582
|
const modelCatalog = require('./lib/model-catalog.cjs');
|
|
1327
1583
|
const runner = require('./lib/review-lane-runner.cjs');
|
|
1328
1584
|
const cfgLoader = require('./lib/config-loader.cjs');
|
|
@@ -1347,8 +1603,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1347
1603
|
// `plan`/`invoke` are the only subs that need the expensive plan-building path
|
|
1348
1604
|
// below; `sections`/`flags` return earlier still. Anything else errors here, before
|
|
1349
1605
|
// 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]");
|
|
1606
|
+
if (!['plan', 'invoke', 'sections', 'flags', 'dispatch-step', 'explicit-from-argv'].includes(sub)) {
|
|
1607
|
+
error("Usage: review-lane <plan|invoke|sections|flags|dispatch-step|explicit-from-argv> [--selected a,b] [--run-dir D] [--repo-root R]");
|
|
1352
1608
|
return;
|
|
1353
1609
|
}
|
|
1354
1610
|
const runDir = flag('--run-dir') || '.';
|
|
@@ -1370,6 +1626,86 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1370
1626
|
return cur;
|
|
1371
1627
|
};
|
|
1372
1628
|
|
|
1629
|
+
// Shared by `invoke` and `dispatch-step` (#4209) — both need the SAME bounded
|
|
1630
|
+
// spawn/http/fs seam `runLane` requires (RunnerDeps). Factored out so the two
|
|
1631
|
+
// callers can never disagree about how a lane's binary is resolved or how its
|
|
1632
|
+
// process is bounded; a fix to either reaches both.
|
|
1633
|
+
const buildLaneRunnerDeps = () => ({
|
|
1634
|
+
spawn: (binary, argv, opts) => {
|
|
1635
|
+
// #3086: on Windows, reviewer CLIs (gemini, codex, etc.) are installed
|
|
1636
|
+
// as .cmd shims. spawnSync with a bare name + shell:false fails with
|
|
1637
|
+
// ENOENT (CreateProcess cannot start .cmd). Apply the same #2667 shim
|
|
1638
|
+
// gate used in runWithTimeout: detect .cmd/.bat and mediate through
|
|
1639
|
+
// cmd.exe /d /s /c with an explicit argv array (no shell:true).
|
|
1640
|
+
//
|
|
1641
|
+
// #3275: descriptors declare BARE names, so the gate above never saw an
|
|
1642
|
+
// extension — resolve through the shared PATH+PATHEXT resolver FIRST
|
|
1643
|
+
// (the same one `hasBinary` uses, so probe and spawn can never disagree
|
|
1644
|
+
// about what the lane's binary is). POSIX keeps the bare name: Node's own
|
|
1645
|
+
// PATH search already worked there, and the #3275 acceptance contract
|
|
1646
|
+
// holds macOS/Linux behavior unchanged. A name that resolves to nothing
|
|
1647
|
+
// falls back to the declared name so the ENOENT still surfaces (#3086).
|
|
1648
|
+
// #3411: the resolve-then-mediate pair is one seam call now. Both halves had
|
|
1649
|
+
// private copies here; `projectSpawnInvocation` owns them, so a fix to either
|
|
1650
|
+
// reaches every spawn site instead of only this one.
|
|
1651
|
+
//
|
|
1652
|
+
// Unlike execTool, this lane adopts the RESOLVED path even for a non-batch
|
|
1653
|
+
// binary: that is the behavior #3445 shipped and `deps.hasBinary` answers
|
|
1654
|
+
// from the same resolver, so probe and spawn must agree on the exact file.
|
|
1655
|
+
const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
|
|
1656
|
+
const { command: spawnBinary, args: spawnArgv, windowsVerbatimArguments } = projectSpawnInvocation(binary, argv);
|
|
1657
|
+
const r = cp.spawnSync(spawnBinary, spawnArgv, {
|
|
1658
|
+
input: opts.input,
|
|
1659
|
+
encoding: 'utf8',
|
|
1660
|
+
timeout: opts.timeoutMs,
|
|
1661
|
+
killSignal: 'SIGKILL',
|
|
1662
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1663
|
+
shell: false, // argv array only — never a shell string (no interpolation of config values)
|
|
1664
|
+
// #2483: a lane's declared env pairs merged OVER this process's environment, for this
|
|
1665
|
+
// child only. Passing a fresh object leaves `process.env` untouched, so nothing leaks
|
|
1666
|
+
// into the orchestrating session or into the next lane.
|
|
1667
|
+
...(opts.env ? { env: { ...process.env, ...opts.env } } : {}),
|
|
1668
|
+
...(windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
1669
|
+
});
|
|
1670
|
+
return {
|
|
1671
|
+
status: r.status,
|
|
1672
|
+
stdout: r.stdout || '',
|
|
1673
|
+
stderr: r.stderr || '',
|
|
1674
|
+
errorCode: r.error && r.error.code ? r.error.code : undefined,
|
|
1675
|
+
};
|
|
1676
|
+
},
|
|
1677
|
+
httpJson: async (url, opts) => {
|
|
1678
|
+
try {
|
|
1679
|
+
const res = await fetch(url, {
|
|
1680
|
+
method: opts.method,
|
|
1681
|
+
headers: opts.body ? { 'Content-Type': 'application/json' } : undefined,
|
|
1682
|
+
body: opts.body,
|
|
1683
|
+
signal: AbortSignal.timeout(opts.timeoutMs),
|
|
1684
|
+
});
|
|
1685
|
+
return { ok: res.ok, status: res.status, body: await res.text() };
|
|
1686
|
+
} catch (e) {
|
|
1687
|
+
return { ok: false, status: 0, body: '', error: e && e.message ? e.message : String(e) };
|
|
1688
|
+
}
|
|
1689
|
+
},
|
|
1690
|
+
readFile: (p) => fsx.readFileSync(p, 'utf8'),
|
|
1691
|
+
writeFile: (p, c) => fsx.writeFileSync(p, c, 'utf8'),
|
|
1692
|
+
exists: (p) => fsx.existsSync(p),
|
|
1693
|
+
// PATH scan rather than spawning `command -v` / `where`. Two reasons: it spawns nothing at
|
|
1694
|
+
// all (a probe that costs a process is a probe you avoid running, which is how the original
|
|
1695
|
+
// Kimi probe ended up unbounded), and `shell: true` with an args array is deprecated in
|
|
1696
|
+
// Node 26 (DEP0190) because the arguments are concatenated rather than escaped.
|
|
1697
|
+
//
|
|
1698
|
+
// #3275: the scan lives in `resolveSpawnBinary` now, SHARED with `deps.spawn`
|
|
1699
|
+
// above. Two private copies of "what is this declared binary?" is how the
|
|
1700
|
+
// defect hid: the probe resolved WITH PATHEXT while spawn resolved WITHOUT,
|
|
1701
|
+
// so a lane reported available for a spawn that could never start. One
|
|
1702
|
+
// resolver, both seams — if one changes, the other changes with it.
|
|
1703
|
+
hasBinary: (name) => resolveSpawnBinary(name) !== null,
|
|
1704
|
+
configGet,
|
|
1705
|
+
homeDir: os.homedir(),
|
|
1706
|
+
warn: (m) => process.stderr.write(`${m}\n`),
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1373
1709
|
const selected = (flag('--selected') || '')
|
|
1374
1710
|
.split(',').map((s) => s.trim()).filter(Boolean);
|
|
1375
1711
|
// ADR-2782 D8 (#2927): the lane map is first-party ∪ INSTALLED overlay
|
|
@@ -1397,6 +1733,25 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1397
1733
|
const laneBySlug = new Map(mergedLanes.map((l) => [l.slug, l]));
|
|
1398
1734
|
const chosen = selected.length ? selected : mergedLanes.map((l) => l.slug);
|
|
1399
1735
|
|
|
1736
|
+
// #4209 RQ-02: match a workflow's raw CLI argv (e.g. `--codex`, `--agy`) against the SAME
|
|
1737
|
+
// merged first-party+installed-overlay roster this whole function already built above,
|
|
1738
|
+
// instead of a workflow re-deriving its own copy of `loadRegistry`/`mergeReviewerLanes` via
|
|
1739
|
+
// an inline `node -e` (a rename-only duplicate of the block starting at `mergedLanes =
|
|
1740
|
+
// REVIEWER_LANES` above — `code-review-flags.cjs`'s own header states "this is the canonical
|
|
1741
|
+
// flag-parsing surface — do not replicate inline bash parsing" for exactly this reason).
|
|
1742
|
+
// Everything after `--` is a candidate flag; matched lane slugs print sorted and comma-joined.
|
|
1743
|
+
if (sub === 'explicit-from-argv') {
|
|
1744
|
+
const sepIdx = args.indexOf('--');
|
|
1745
|
+
const candidateArgs = new Set(sepIdx === -1 ? [] : args.slice(sepIdx + 1));
|
|
1746
|
+
const slugs = [];
|
|
1747
|
+
for (const lane of mergedLanes) {
|
|
1748
|
+
const flags = Array.isArray(lane.flags) ? lane.flags : [];
|
|
1749
|
+
if (flags.some((f) => candidateArgs.has(f))) slugs.push(lane.slug);
|
|
1750
|
+
}
|
|
1751
|
+
process.stdout.write([...new Set(slugs)].sort().join(','));
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1400
1755
|
if (sub === 'sections') {
|
|
1401
1756
|
const rows = chosen
|
|
1402
1757
|
.map((s) => laneBySlug.get(s))
|
|
@@ -1454,26 +1809,135 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1454
1809
|
};
|
|
1455
1810
|
const effortFor = (lane) => resolveLaneEffort(lane, configGet, renderLaneEffort);
|
|
1456
1811
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1812
|
+
// Per-lane prompt budget: `resolveLaneBudget` (review-lane-invocation.cjs) owns the #2797
|
|
1813
|
+
// resolution semantics (shared with src/reviewer-step-dispatch.cts, #4209 R3 — was two
|
|
1814
|
+
// verbatim copies). Only the budget VALUE is resolved here; assembly/trimming stay in
|
|
1815
|
+
// `prompt-budget`, which the workflow calls, handing the trimmed file back via `--prompt-file`.
|
|
1816
|
+
const budgetFor = (lane) => resolveLaneBudget(lane, configGet);
|
|
1817
|
+
|
|
1818
|
+
// #4209 (ADR-2782 seam) — the ONE interpreter route for a step that declared
|
|
1819
|
+
// `supportsReviewerLanes: true`. Wires `dispatchReviewerLanes` (src/reviewer-step-dispatch.cts)
|
|
1820
|
+
// to the SAME plan/invoke machinery `plan`/`invoke` above use, so a step opting in gets exact
|
|
1821
|
+
// parity with hand-driven `review-lane plan|invoke` rather than a second implementation.
|
|
1822
|
+
// Canonical file paths travel on stdin, never argv (see gsd-core/workflows/code-review.md's
|
|
1823
|
+
// "Files travel on stdin" note) — a 50+-file scope with long paths approaches the Windows
|
|
1824
|
+
// execFileSync argv ceiling, and stdin has no such bound.
|
|
1825
|
+
//
|
|
1826
|
+
// Returns EARLY, like `sections`/`flags` above, rather than falling into the `plans` builder
|
|
1827
|
+
// below: that builder spawns one `effortFor` child process PER LANE IN THE ROSTER (chosen
|
|
1828
|
+
// defaults to every merged lane when nothing is selected), which would burn ~12 wasted spawns
|
|
1829
|
+
// on every dispatch-step call whether or not anything was actually selected. `dispatchReviewerLanes`
|
|
1830
|
+
// builds its own per-SELECTED-lane plan below instead, bounded by the (typically 0-3) explicitly
|
|
1831
|
+
// requested slugs, not the whole roster.
|
|
1832
|
+
if (sub === 'dispatch-step') {
|
|
1833
|
+
const { dispatchReviewerLanes } = require('./lib/reviewer-step-dispatch.cjs');
|
|
1834
|
+
const explicitFlags = (flag('--explicit') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
1835
|
+
const depth = flag('--depth') || '';
|
|
1836
|
+
const baseSha = flag('--base-sha') || '';
|
|
1837
|
+
// #4209: this command IS the reusable capability/step-dispatch trait check — see
|
|
1838
|
+
// gsd-core/references/loop-hook-dispatch.md for what supportsReviewerLanes means and why
|
|
1839
|
+
// this is the one place it's resolved. Calls the SAME resolver `loop render-hooks` uses,
|
|
1840
|
+
// `resolveActiveHooksForPoint`, directly in-process — no subprocess, no JSON re-parse, and
|
|
1841
|
+
// no exposure to `io.cjs`'s `@file:` overflow protocol (which only applies to the
|
|
1842
|
+
// rendered-string envelope this path never touches).
|
|
1843
|
+
const capId = flag('--cap-id') || '';
|
|
1844
|
+
const point = flag('--point') || '';
|
|
1845
|
+
let trait = false;
|
|
1846
|
+
if (capId && point) {
|
|
1847
|
+
try {
|
|
1848
|
+
const { resolveActiveHooksForPoint } = loopResolver;
|
|
1849
|
+
const { activeHooks } = resolveActiveHooksForPoint(cwd, point);
|
|
1850
|
+
trait = activeHooks.some((h) => h && h.capId === capId && h.supportsReviewerLanes === true);
|
|
1851
|
+
} catch (e) {
|
|
1852
|
+
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`);
|
|
1853
|
+
trait = false;
|
|
1854
|
+
}
|
|
1855
|
+
} else if (capId || point) {
|
|
1856
|
+
// #4209 RQ-03: exactly one of the two was passed — a caller with NO capability-step
|
|
1857
|
+
// context at all (neither flag) is the legitimate, silent no-op documented above, but a
|
|
1858
|
+
// caller that named a capability without its point (or vice versa) is misconfigured, not
|
|
1859
|
+
// opted out, and that must not look identical to a correct opt-out on the wire.
|
|
1860
|
+
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`);
|
|
1861
|
+
}
|
|
1862
|
+
// No piped stdin (interactive TTY): fail closed to empty paths instead of blocking
|
|
1863
|
+
// indefinitely on a TTY EOF the caller never sends.
|
|
1864
|
+
let stdinPaths = '';
|
|
1865
|
+
if (!process.stdin.isTTY) {
|
|
1866
|
+
try { stdinPaths = fsx.readFileSync(0, 'utf8'); } catch { stdinPaths = ''; }
|
|
1867
|
+
}
|
|
1868
|
+
const paths = stdinPaths.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
1869
|
+
|
|
1870
|
+
// Reuse the exact effort-aware, per-lane plan `plan` builds above (DISP-03: "planned
|
|
1871
|
+
// through the existing `review-lane plan` interface") rather than the interpreter's
|
|
1872
|
+
// simpler default plan callback, which does not resolve per-host effort.
|
|
1873
|
+
const planFn = (lane, ctx) => {
|
|
1874
|
+
const effort = effortFor(lane.slug);
|
|
1875
|
+
return resolveLanePlan({
|
|
1876
|
+
lane, configGet: ctx.configGet, runDir: ctx.runDir, repoRoot: ctx.repoRoot,
|
|
1877
|
+
effortArgs: effort.argv, effortValue: effort.value,
|
|
1878
|
+
});
|
|
1879
|
+
};
|
|
1880
|
+
|
|
1881
|
+
const runnerDeps = buildLaneRunnerDeps();
|
|
1882
|
+
const invokeFn = async (lane, plan) => {
|
|
1883
|
+
let consentedHost;
|
|
1884
|
+
if (plan.transport === 'openai-http') {
|
|
1885
|
+
try {
|
|
1886
|
+
const consent = require('./lib/capability-consent.cjs');
|
|
1887
|
+
const projectRoot = require('./lib/project-root.cjs').consentProjectRoot(cwd);
|
|
1888
|
+
const capId = String(lane.slug).replace(/_/g, '-');
|
|
1889
|
+
consentedHost = consent.readConsentedReviewerHost({ projectRoot, id: capId });
|
|
1890
|
+
} catch { consentedHost = undefined; }
|
|
1891
|
+
}
|
|
1892
|
+
// DISP-04/05: every selected lane is invoked through this SAME `runner.runLane` seam
|
|
1893
|
+
// `invoke` uses, exactly once (the interpreter's own for-loop over `selection.selected`
|
|
1894
|
+
// never revisits a slug).
|
|
1895
|
+
return runner.runLane(plan, runnerDeps, { consentedHost, explicitlyRequested: true, repoRoot });
|
|
1896
|
+
};
|
|
1897
|
+
|
|
1898
|
+
// `resolveReviewerSelection` only selects an explicit flag present in `detected`
|
|
1899
|
+
// (ADR-2782 D4: absent-safe governs discovery, never explicit selection — a slug the
|
|
1900
|
+
// roster does not declare is rejected here as an explicit-selection error). REAL
|
|
1901
|
+
// host availability (is the CLI actually installed?) is a separate, already-owned
|
|
1902
|
+
// check inside `runner.runLane`'s `probeLane` at invoke time below — duplicating a
|
|
1903
|
+
// second `command -v` probe here would let the two disagree about what "available"
|
|
1904
|
+
// means, which is the exact defect class `resolveSpawnBinary` was consolidated to
|
|
1905
|
+
// prevent (#3275).
|
|
1906
|
+
//
|
|
1907
|
+
// GUARDED ON explicitFlags.length, not unconditional: `resolveReviewerSelection`'s
|
|
1908
|
+
// precedence chain (explicit > --all > review.default_reviewers > all detected) ends,
|
|
1909
|
+
// when none of the first three apply, in `selected = [...detected]` — the SAME
|
|
1910
|
+
// "no flags means every detected reviewer" default `/gsd:review` intentionally uses.
|
|
1911
|
+
// Source review's COMP-01 contract is the opposite: no reviewer-lane flag means inert,
|
|
1912
|
+
// unchanged from before #4209. Passing a non-empty
|
|
1913
|
+
// `detected` unconditionally would silently opt every dispatch-step call with no
|
|
1914
|
+
// `--explicit` into planning+invoking the WHOLE roster via that fallback branch. An
|
|
1915
|
+
// empty `detected` when nothing was asked for makes that fallback resolve to
|
|
1916
|
+
// `[...[]]` = `[]`, so `dispatchReviewerLanes` hits its own `NO_LANES_SELECTED`
|
|
1917
|
+
// early-return before any plan/invoke call — the same fast, inert no-op the caller
|
|
1918
|
+
// gets from an absent `supportsReviewerLanes` trait.
|
|
1919
|
+
const rosterSlugs = explicitFlags.length > 0 ? [...laneBySlug.keys()] : [];
|
|
1920
|
+
|
|
1921
|
+
const dispatchResult = await dispatchReviewerLanes(
|
|
1922
|
+
{
|
|
1923
|
+
trait,
|
|
1924
|
+
selection: { explicitFlags, detected: rosterSlugs },
|
|
1925
|
+
repoRoot,
|
|
1926
|
+
paths,
|
|
1927
|
+
depth,
|
|
1928
|
+
baseSha,
|
|
1929
|
+
runDir,
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
getLane: (slug) => laneBySlug.get(slug),
|
|
1933
|
+
configGet,
|
|
1934
|
+
plan: planFn,
|
|
1935
|
+
invoke: invokeFn,
|
|
1936
|
+
},
|
|
1937
|
+
);
|
|
1938
|
+
output(dispatchResult, raw);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1477
1941
|
|
|
1478
1942
|
const plans = chosen.map((slug) => {
|
|
1479
1943
|
const lane = laneBySlug.get(slug);
|
|
@@ -1508,7 +1972,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1508
1972
|
}
|
|
1509
1973
|
|
|
1510
1974
|
if (sub !== 'invoke') {
|
|
1511
|
-
error("Usage: review-lane <plan|invoke|sections|flags> [--selected a,b] [--run-dir D] [--repo-root R]");
|
|
1975
|
+
error("Usage: review-lane <plan|invoke|sections|flags|dispatch-step|explicit-from-argv> [--selected a,b] [--run-dir D] [--repo-root R]");
|
|
1512
1976
|
return;
|
|
1513
1977
|
}
|
|
1514
1978
|
|
|
@@ -1522,81 +1986,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1522
1986
|
|
|
1523
1987
|
// EVERY spawn bounded — `DEFECT.UNBOUNDED-SUBPROCESS` (CONTEXT.md:772). A frozen sync spawn
|
|
1524
1988
|
// 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
|
-
};
|
|
1989
|
+
const deps = buildLaneRunnerDeps();
|
|
1600
1990
|
|
|
1601
1991
|
// ADR-1517 reviewer instances resolve THROUGH a lane rather than being lanes themselves
|
|
1602
1992
|
// (ADR-2782 D8), so they reuse this seam with three substitutions instead of duplicating the
|
|
@@ -2858,6 +3248,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2858
3248
|
worktreeSafety.cmdWorktreeRecordAgent(cwd, args.slice(2));
|
|
2859
3249
|
} else if (subcommand === 'reap-orphans') {
|
|
2860
3250
|
worktreeSafety.cmdWorktreeReapOrphans(cwd);
|
|
3251
|
+
} else if (subcommand === 'worker-record') {
|
|
3252
|
+
worktreeSafety.cmdWorktreeWorkerRecord(cwd, args.slice(2));
|
|
3253
|
+
} else if (subcommand === 'worker-status') {
|
|
3254
|
+
worktreeSafety.cmdWorktreeWorkerStatus(cwd, args.slice(2));
|
|
3255
|
+
} else if (subcommand === 'worker-complete') {
|
|
3256
|
+
worktreeSafety.cmdWorktreeWorkerComplete(cwd, args.slice(2));
|
|
2861
3257
|
} else if (subcommand === 'base-check') {
|
|
2862
3258
|
require('./lib/worktree-base-ref.cjs').cmdWorktreeBaseCheck(cwd, args.slice(2));
|
|
2863
3259
|
} else if (subcommand === 'set-baseref') {
|
|
@@ -2865,7 +3261,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2865
3261
|
} else if (subcommand === 'create') {
|
|
2866
3262
|
worktreeSafety.cmdWorktreeCreate(cwd, args.slice(2));
|
|
2867
3263
|
} else {
|
|
2868
|
-
error('Unknown worktree subcommand. Available: cleanup-wave, record-agent, reap-orphans, base-check, set-baseref, create', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
3264
|
+
error('Unknown worktree subcommand. Available: cleanup-wave, record-agent, reap-orphans, base-check, set-baseref, create, worker-record, worker-status, worker-complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2869
3265
|
}
|
|
2870
3266
|
}
|
|
2871
3267
|
|
|
@@ -3147,6 +3543,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3147
3543
|
const RESTORE_OUTCOME = Object.freeze({
|
|
3148
3544
|
ELIGIBLE: 'eligible',
|
|
3149
3545
|
RESTORED: 'restored',
|
|
3546
|
+
ALREADY_PRESENT: 'already_present',
|
|
3150
3547
|
SKIPPED_DESTINATION_MANAGED: 'skipped_destination_managed',
|
|
3151
3548
|
SKIPPED_DESTINATION_EXISTS: 'skipped_destination_exists',
|
|
3152
3549
|
SKIPPED_COPY_FAILED: 'skipped_copy_failed',
|
|
@@ -3205,18 +3602,30 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3205
3602
|
return out;
|
|
3206
3603
|
}
|
|
3207
3604
|
|
|
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
|
-
//
|
|
3605
|
+
// Why these three checks rather than security.cjs's `assertWithinRoot` /
|
|
3606
|
+
// `tryWithinRoot`: that seam resolves symlinks with realpathSync and then
|
|
3607
|
+
// tests containment, so a link whose target sits inside the config dir
|
|
3608
|
+
// passes. For a restore that is still wrong — writing through any link
|
|
3609
|
+
// overwrites whatever it points at instead of materializing a regular file
|
|
3610
|
+
// at the backed-up path. These checks reject links outright, which is
|
|
3611
|
+
// strictly stricter than assertWithinRoot/tryWithinRoot, not a
|
|
3612
|
+
// reimplementation of them. Do not "simplify" this to assertWithinRoot or
|
|
3613
|
+
// tryWithinRoot. Reviewed under epic #4636 Phase 3: the containment
|
|
3614
|
+
// DECISION now routes through the canonical lexical predicate
|
|
3615
|
+
// (`tryWithinRootLexical`, ADR-4650 decision 6); isInsideDir below still
|
|
3616
|
+
// treats target === root as NOT contained via its own extra `!==` check
|
|
3617
|
+
// (unlike every other containment implementation in this repo, which
|
|
3618
|
+
// treats target === root as contained) — that condition is this gate's
|
|
3619
|
+
// own and is layered on top of the shared predicate, not folded into it.
|
|
3215
3620
|
|
|
3216
3621
|
/** True when `target` resolves strictly inside `root`. */
|
|
3217
3622
|
function isInsideDir(root, target) {
|
|
3218
|
-
|
|
3219
|
-
|
|
3623
|
+
// Containment decision: canonical lexical predicate (ADR-4650 decision 6).
|
|
3624
|
+
// The extra `!==` condition is this gate's own: a restore must never
|
|
3625
|
+
// target the config directory itself, only something strictly inside it.
|
|
3626
|
+
if (path.resolve(target) === path.resolve(root)) return false;
|
|
3627
|
+
const { tryWithinRootLexical } = require('./lib/security.cjs');
|
|
3628
|
+
return tryWithinRootLexical(target, root) !== null;
|
|
3220
3629
|
}
|
|
3221
3630
|
|
|
3222
3631
|
/**
|
|
@@ -3414,9 +3823,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3414
3823
|
}
|
|
3415
3824
|
|
|
3416
3825
|
// 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.
|
|
3826
|
+
// the restore after a successful one must stay quiet and idempotent. It
|
|
3827
|
+
// gets its own outcome so it is excluded from eligible_count — the update
|
|
3828
|
+
// workflow drives its restore question off that count (#4558).
|
|
3829
|
+
let destExists = false;
|
|
3418
3830
|
let destDiffers = false;
|
|
3419
3831
|
if (fs.existsSync(destPath)) {
|
|
3832
|
+
destExists = true;
|
|
3420
3833
|
try {
|
|
3421
3834
|
destDiffers = !fs.readFileSync(destPath).equals(fs.readFileSync(srcPath));
|
|
3422
3835
|
} catch {
|
|
@@ -3431,6 +3844,10 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3431
3844
|
entries.push({ path: relPath, outcome: RESTORE_OUTCOME.SKIPPED_DESTINATION_EXISTS, warnings });
|
|
3432
3845
|
continue;
|
|
3433
3846
|
}
|
|
3847
|
+
if (destExists) {
|
|
3848
|
+
entries.push({ path: relPath, outcome: RESTORE_OUTCOME.ALREADY_PRESENT, warnings });
|
|
3849
|
+
continue;
|
|
3850
|
+
}
|
|
3434
3851
|
|
|
3435
3852
|
if (!apply) {
|
|
3436
3853
|
entries.push({ path: relPath, outcome: RESTORE_OUTCOME.ELIGIBLE, warnings });
|
|
@@ -4146,6 +4563,31 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
4146
4563
|
* declares that file "All OS-facing I/O; single platform seam", and a private
|
|
4147
4564
|
* duplicate here is what made it untrue.
|
|
4148
4565
|
*/
|
|
4566
|
+
const COMMIT_LIST_FLAGS = new Set(['--files', '--files-removed']);
|
|
4567
|
+
|
|
4568
|
+
// #4208 review: hoisted out of routeCommit's closure so the parser is reachable
|
|
4569
|
+
// from a test. It is the whole of the two-list argument contract, and its edge
|
|
4570
|
+
// cases (a boolean flag inside a run, a repeated list flag, either order) were
|
|
4571
|
+
// already the subject of a review round -- a parser that only the CLI can reach
|
|
4572
|
+
// can only be tested by example, one spawn at a time.
|
|
4573
|
+
//
|
|
4574
|
+
// Every occurrence of `flag` contributes a run; a run ends at the next LIST
|
|
4575
|
+
// flag and skips boolean flags on the way, so no token strictly between one
|
|
4576
|
+
// list flag and the next is ever dropped. Repeated runs of the same flag merge,
|
|
4577
|
+
// as the pre-#4208 slice-to-end parse merged them.
|
|
4578
|
+
function collectListFlagValues(args, flag) {
|
|
4579
|
+
const values = [];
|
|
4580
|
+
args.forEach((a, i) => {
|
|
4581
|
+
if (a !== flag) return;
|
|
4582
|
+
for (const b of args.slice(i + 1)) {
|
|
4583
|
+
if (COMMIT_LIST_FLAGS.has(b)) break;
|
|
4584
|
+
if (b.startsWith('--')) continue;
|
|
4585
|
+
values.push(b);
|
|
4586
|
+
}
|
|
4587
|
+
});
|
|
4588
|
+
return values;
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4149
4591
|
function resolveSpawnBinary(name, platform = process.platform, env = process.env) {
|
|
4150
4592
|
const { resolveExecutableBinary } = require('./lib/shell-command-projection.cjs');
|
|
4151
4593
|
return resolveExecutableBinary(name, { platform, env });
|
|
@@ -4187,6 +4629,21 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
4187
4629
|
// rather than a family — ADR-2346 promotes to a family only at >=3.
|
|
4188
4630
|
'estimate-check': ({ args, cwd, raw }) => estimateCli.cmdEstimateCheck(cwd, args.slice(1), raw),
|
|
4189
4631
|
'estimate-calibration': ({ args, cwd, raw }) => estimateCli.cmdEstimateCalibration(cwd, args.slice(1), raw),
|
|
4632
|
+
// #3418: writes `last_mapped_commit` into every codebase-map document that
|
|
4633
|
+
// exists, closing the loop drift.cjs was built for. A LEAF verb rather than a
|
|
4634
|
+
// `verify` subcommand on purpose -- the verify family is read-only by
|
|
4635
|
+
// contract and this one mutates; ADR-2346 promotes a leaf to a family only at
|
|
4636
|
+
// >=3 verbs, and this is one.
|
|
4637
|
+
'stamp-codebase-map': ({ args, cwd, raw, error }) => {
|
|
4638
|
+
const { files } = parseNamedArgsOrExit(args, { valueFlags: ['files'], positionals: 1 }, error);
|
|
4639
|
+
// A value flag with no value parses to `null`, same as an absent one, so
|
|
4640
|
+
// presence is read off `args`: a bare `--files` (an unquoted empty shell
|
|
4641
|
+
// variable) must hit the empty-filter refusal, not widen to all seven.
|
|
4642
|
+
const only = args.includes('--files')
|
|
4643
|
+
? String(files ?? '').split(',').map((f) => f.trim()).filter(Boolean)
|
|
4644
|
+
: undefined;
|
|
4645
|
+
verify.cmdStampCodebaseMap(cwd, raw, only);
|
|
4646
|
+
},
|
|
4190
4647
|
'estimate-calibrate': ({ args, cwd, raw }) => estimateCli.cmdEstimateCalibrate(cwd, args.slice(1), raw),
|
|
4191
4648
|
'config-new-project': routeConfigNewProject,
|
|
4192
4649
|
'config-path': routeConfigPath,
|
|
@@ -4198,6 +4655,8 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
4198
4655
|
'smart-entry': routeSmartEntry,
|
|
4199
4656
|
'check': routeCheck,
|
|
4200
4657
|
'find-phase': routeFindPhase,
|
|
4658
|
+
// #4906 Phase 5 (#4661): commit-scope selection for /gsd:undo --phase/--plan.
|
|
4659
|
+
'select-revert-commits': routeSelectRevertCommits,
|
|
4201
4660
|
'commit': routeCommit,
|
|
4202
4661
|
'check-commit': routeCheckCommit,
|
|
4203
4662
|
'commit-docs-guard': routeCommitDocsGuard,
|
|
@@ -4275,7 +4734,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
4275
4734
|
// through. Prototype-pollution-safe: own-property lookup rejects
|
|
4276
4735
|
// `__proto__`/`constructor`/`prototype` command keys (same guard as
|
|
4277
4736
|
// dispatchCapabilityCommand).
|
|
4278
|
-
async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext }) {
|
|
4737
|
+
async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd }) {
|
|
4279
4738
|
if (
|
|
4280
4739
|
command === '__proto__' ||
|
|
4281
4740
|
command === 'constructor' ||
|
|
@@ -4290,7 +4749,13 @@ async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValu
|
|
|
4290
4749
|
if (typeof router !== 'function') return false;
|
|
4291
4750
|
// `await` so async host routers (e.g. capability's install/upgrade ops)
|
|
4292
4751
|
// complete before runCommand returns; sync routers pass through unchanged.
|
|
4293
|
-
|
|
4752
|
+
// `invokingCwd` (#4906/#4465): the pre-worktree-remap cwd, for the rare
|
|
4753
|
+
// router (currently only `select-revert-commits`) whose own git operation
|
|
4754
|
+
// must see the INVOKING worktree's HEAD, never the main-worktree remap
|
|
4755
|
+
// every other router legitimately wants via `cwd`. Optional — callers/tests
|
|
4756
|
+
// that omit it (every existing one) leave routers that don't read it
|
|
4757
|
+
// byte-unaffected.
|
|
4758
|
+
await router({ args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd });
|
|
4294
4759
|
return true; // consumed — don't emit "Unknown command"
|
|
4295
4760
|
}
|
|
4296
4761
|
|
|
@@ -4323,7 +4788,7 @@ async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValu
|
|
|
4323
4788
|
// keep working. No shell is spawned (argv array) — no injection surface beyond
|
|
4324
4789
|
// the old `timeout … bash -c "$CMD"`.
|
|
4325
4790
|
function runWithTimeout(argv) {
|
|
4326
|
-
const { spawn } = require('node:child_process');
|
|
4791
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
4327
4792
|
const os = require('node:os');
|
|
4328
4793
|
|
|
4329
4794
|
const USAGE = 'Usage: gsd_run run-with-timeout <seconds> [--] <command> [args...]';
|
|
@@ -4348,9 +4813,11 @@ function runWithTimeout(argv) {
|
|
|
4348
4813
|
|
|
4349
4814
|
const isWin = process.platform === 'win32';
|
|
4350
4815
|
// Detached (own process group) on POSIX so a timeout can reap the WHOLE tree —
|
|
4351
|
-
// a bare child.kill() misses grandchildren (e.g. a test runner's workers)
|
|
4352
|
-
//
|
|
4353
|
-
//
|
|
4816
|
+
// a bare child.kill() misses grandchildren (e.g. a test runner's workers).
|
|
4817
|
+
// Windows has no POSIX process groups and process.kill(-pid) is unsupported
|
|
4818
|
+
// there, so EVERY killTree attempt on Windows tree-kills via
|
|
4819
|
+
// `taskkill /PID <pid> /T /F` while the root is alive (see killTree) — by the
|
|
4820
|
+
// time the direct child exits, its descendants are already orphaned.
|
|
4354
4821
|
const detached = !isWin && secs > 0;
|
|
4355
4822
|
const spawnFailureCode = (err) =>
|
|
4356
4823
|
(err && err.code === 'ENOENT' ? 127 : err && err.code === 'EACCES' ? 126 : 125);
|
|
@@ -4374,9 +4841,26 @@ function runWithTimeout(argv) {
|
|
|
4374
4841
|
// cap's process-group kill (the wrapped child escapes reap → exit 124 never
|
|
4375
4842
|
// fires) and risks cmd.exe mis-parsing an arg like `-e "setTimeout(()=>{})"`.
|
|
4376
4843
|
// Only .cmd/.bat are the CVE-2024-27980 EINVAL cases that require mediation.
|
|
4844
|
+
// #4797: the mediation is NOT hand-rolled here — the private `/d /s /c <cmd>
|
|
4845
|
+
// ...args` copy broke on any shim path containing a space (Node quotes the
|
|
4846
|
+
// argv token; `/s` strips the FIRST and LAST quote of the /c string, so
|
|
4847
|
+
// cmd.exe took the pre-space fragment as the program). projectSpawnInvocation
|
|
4848
|
+
// (the declared single owner, #3411/#3617) wraps the WHOLE command line in
|
|
4849
|
+
// one extra quote pair with windowsVerbatimArguments — the shape that
|
|
4850
|
+
// survives spaces. One behavior delta, accepted: the seam DECLINES mediation
|
|
4851
|
+
// when the target or an arg carries CR/LF (the old block mediated anyway) —
|
|
4852
|
+
// the unmediated spawn of a .cmd then fails EINVAL, loud, at the catch below.
|
|
4377
4853
|
const winShim = isWin && /\.(cmd|bat)$/i.test(path.basename(cmd));
|
|
4378
|
-
|
|
4379
|
-
|
|
4854
|
+
let spawnCmd = cmd;
|
|
4855
|
+
let spawnArgs = cmdArgs;
|
|
4856
|
+
let spawnOpts;
|
|
4857
|
+
if (winShim) {
|
|
4858
|
+
const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
|
|
4859
|
+
const inv = projectSpawnInvocation(cmd, cmdArgs);
|
|
4860
|
+
spawnCmd = inv.command;
|
|
4861
|
+
spawnArgs = inv.args;
|
|
4862
|
+
if (inv.windowsVerbatimArguments) spawnOpts = { windowsVerbatimArguments: true };
|
|
4863
|
+
}
|
|
4380
4864
|
// Node's setTimeout delay is a 32-bit signed ms int; a larger value silently
|
|
4381
4865
|
// clamps to 1ms → a spurious immediate timeout. Cap the budget (~24.8 days).
|
|
4382
4866
|
const timerMs = Math.min(Math.round(secs * 1000), 2 ** 31 - 1);
|
|
@@ -4387,11 +4871,13 @@ function runWithTimeout(argv) {
|
|
|
4387
4871
|
return new Promise((resolve) => {
|
|
4388
4872
|
let child;
|
|
4389
4873
|
try {
|
|
4390
|
-
// #2667: on win32 `.cmd`/`.bat
|
|
4391
|
-
//
|
|
4392
|
-
//
|
|
4393
|
-
//
|
|
4394
|
-
|
|
4874
|
+
// #2667: on win32 `.cmd`/`.bat` shims, spawn cmd.exe with an explicit
|
|
4875
|
+
// argv ARRAY rather than the shim directly — preserves the array-only,
|
|
4876
|
+
// no-shell-string argv contract. #4797: the exact argv shape (quote
|
|
4877
|
+
// wrapping, verbatim arguments) is projected by projectSpawnInvocation —
|
|
4878
|
+
// see the block above. `detached` is always false on win32, so it never
|
|
4879
|
+
// co-occurs with the cmd.exe mediation.
|
|
4880
|
+
child = spawn(spawnCmd, spawnArgs, { stdio: 'inherit', detached, ...spawnOpts });
|
|
4395
4881
|
} catch (err) {
|
|
4396
4882
|
process.stderr.write(`run-with-timeout: ${cmd}: ${err && err.message ? err.message : 'failed to start'}\n`);
|
|
4397
4883
|
resolve(spawnFailureCode(err));
|
|
@@ -4403,6 +4889,27 @@ function runWithTimeout(argv) {
|
|
|
4403
4889
|
if (detached && child.pid) {
|
|
4404
4890
|
try { process.kill(-child.pid, signal); return; } catch { /* group already gone */ }
|
|
4405
4891
|
}
|
|
4892
|
+
if (isWin && child.pid) {
|
|
4893
|
+
// #4601: Windows has no POSIX process groups, so the tree kill rides
|
|
4894
|
+
// on `taskkill /T`, which walks the child's descendants the way
|
|
4895
|
+
// `process.kill(-pid)` reaches a POSIX group — this is what bounds
|
|
4896
|
+
// the wall clock when the direct child mediates (cmd.exe /c shim) or
|
|
4897
|
+
// spawns its own children. Deliberately NOT gated on the SIGKILL
|
|
4898
|
+
// stage: child.kill on Windows is TerminateProcess regardless of
|
|
4899
|
+
// signal, so by the time the direct child exits its descendants are
|
|
4900
|
+
// orphaned and no taskkill can reach them — the tree kill must ride
|
|
4901
|
+
// the FIRST attempt, while the root is still alive. /F is required:
|
|
4902
|
+
// without it taskkill posts WM_CLOSE, which a headless CLI never
|
|
4903
|
+
// pumps. Spawned as an argv array per the no-shell-for-argv-array
|
|
4904
|
+
// contract, and bounded — a non-zero/absent status means taskkill
|
|
4905
|
+
// lost a race with an exiting process, and we fall through to the
|
|
4906
|
+
// direct kill so the attempt is never weaker than before.
|
|
4907
|
+
const reap = spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
|
|
4908
|
+
encoding: 'utf8',
|
|
4909
|
+
timeout: 15000, // taskkill /T is sub-second in practice; bounded so a wedged taskkill can't hang the gate
|
|
4910
|
+
});
|
|
4911
|
+
if (reap.status === 0) return;
|
|
4912
|
+
}
|
|
4406
4913
|
child.kill(signal);
|
|
4407
4914
|
} catch { /* already exited */ }
|
|
4408
4915
|
};
|
|
@@ -4485,7 +4992,7 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
|
|
|
4485
4992
|
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, planning, profile-questionnaire, ' +
|
|
4486
4993
|
'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
4994
|
'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, ' +
|
|
4995
|
+
'resolve-execution, review-lane, select-revert-commits, skill-manifest, skills-root, stamp-codebase-map, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
|
|
4489
4996
|
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
|
4490
4997
|
'Global flags:\n' +
|
|
4491
4998
|
' --raw Emit raw output without post-processing\n' +
|
|
@@ -4707,10 +5214,25 @@ async function main() {
|
|
|
4707
5214
|
cwd = resolvedProjectDir;
|
|
4708
5215
|
projectDirExplicit = true;
|
|
4709
5216
|
}
|
|
5217
|
+
// #4894: verification derives its root from a phase directory, not `cwd`, so
|
|
5218
|
+
// hand it the validated explicit root. Always (re)set — `null` when the flag is
|
|
5219
|
+
// absent — so a prior in-process main() call can never leak an override.
|
|
5220
|
+
projectRoot.setExplicitProjectRoot(projectDirExplicit ? cwd : null);
|
|
4710
5221
|
|
|
4711
5222
|
// Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
|
|
4712
5223
|
// However, in monorepo worktrees where the subdirectory itself owns .planning/,
|
|
4713
5224
|
// skip worktree resolution — the CWD is already the correct project root.
|
|
5225
|
+
//
|
|
5226
|
+
// #4906/#4465: this remap is a BLANKET rewrite applied to every command, before
|
|
5227
|
+
// `command` is even known — most commands need it (they read `.planning/`, which
|
|
5228
|
+
// in a linked worktree lives only in the main one). A command whose own git
|
|
5229
|
+
// operation must run against the INVOKING worktree's OWN HEAD/branch (never the
|
|
5230
|
+
// main worktree's) needs the PRE-remap value instead — preserved here so it can be
|
|
5231
|
+
// threaded through dispatchHostCommand as `invokingCwd` (see routeSelectRevertCommits,
|
|
5232
|
+
// the first consumer: its `git log` must see the caller's own reachable history, not
|
|
5233
|
+
// main's, or a linked-worktree revert range silently resolves against the wrong repo
|
|
5234
|
+
// state — reproduced in tests/undo-commit-selection-4465.test.cjs's linked-worktree case).
|
|
5235
|
+
const preWorktreeRemapCwd = cwd;
|
|
4714
5236
|
cwd = resolveMainWorktreeCwd(cwd);
|
|
4715
5237
|
|
|
4716
5238
|
// Optional workstream override for parallel milestone work.
|
|
@@ -4836,7 +5358,7 @@ async function main() {
|
|
|
4836
5358
|
// false "output was not JSON" (negative space N8).
|
|
4837
5359
|
if (pickField) {
|
|
4838
5360
|
const captured = await captureStdoutSyncWrites(async () => {
|
|
4839
|
-
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
5361
|
+
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd);
|
|
4840
5362
|
});
|
|
4841
5363
|
const resolved = resolveAtFileOutput(captured);
|
|
4842
5364
|
let obj;
|
|
@@ -4868,7 +5390,7 @@ async function main() {
|
|
|
4868
5390
|
// every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
|
|
4869
5391
|
// that breaks on PowerShell and other non-bash shells.
|
|
4870
5392
|
const captured = await captureStdoutSyncWrites(async () => {
|
|
4871
|
-
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
5393
|
+
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd);
|
|
4872
5394
|
});
|
|
4873
5395
|
fs.writeSync(1, resolveAtFileOutput(captured));
|
|
4874
5396
|
}
|
|
@@ -4984,7 +5506,7 @@ function extractField(obj, fieldPath) {
|
|
|
4984
5506
|
return { found: true, value: current };
|
|
4985
5507
|
}
|
|
4986
5508
|
|
|
4987
|
-
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) {
|
|
5509
|
+
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null, invokingCwd) {
|
|
4988
5510
|
switch (command) {
|
|
4989
5511
|
|
|
4990
5512
|
default: {
|
|
@@ -5005,7 +5527,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
5005
5527
|
// commands (state, …) routed via their `route*Command` router instead of
|
|
5006
5528
|
// a hardcoded `case` arm. Tried after capability/overlay dispatch and
|
|
5007
5529
|
// before the unknown-command error.
|
|
5008
|
-
if (await dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext })) break;
|
|
5530
|
+
if (await dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd })) break;
|
|
5009
5531
|
|
|
5010
5532
|
// #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
|
|
5011
5533
|
// above split it so `command` here is the head ("foo"). Use
|
|
@@ -5050,6 +5572,10 @@ module.exports = {
|
|
|
5050
5572
|
// #3275: exported for tests — the shared PATH+PATHEXT resolver behind
|
|
5051
5573
|
// review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
|
|
5052
5574
|
resolveSpawnBinary,
|
|
5575
|
+
// #4208 review: exported for tests — the two-list commit parser is otherwise
|
|
5576
|
+
// reachable only by spawning the CLI, which a property test cannot afford.
|
|
5577
|
+
collectListFlagValues,
|
|
5578
|
+
COMMIT_LIST_FLAGS,
|
|
5053
5579
|
// #3714 follow-up: exported for tests — the dispatch model-pin VALUE
|
|
5054
5580
|
// policy (charset accept/render parity, max-length boundary, leading-char
|
|
5055
5581
|
// anchor) is otherwise unreachable from outside the dispatchOverlayCapabilityCommand closure.
|