@opengsd/gsd-core 1.9.1 → 1.11.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 +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +12 -10
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +46 -0
- package/agents/gsd-planner.md +49 -54
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +26 -73
- package/bin/install.js +1272 -1238
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +2 -2
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/plan-phase.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1009 -115
- package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
- package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
- package/gsd-core/bin/lib/api-coverage.cjs +123 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +926 -202
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +608 -148
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +507 -24
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +114 -38
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +94 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +665 -99
- package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +76 -0
- package/gsd-core/bin/lib/config.cjs +22 -2
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +217 -40
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +239 -32
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1325 -169
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +805 -264
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +160 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
- package/gsd-core/bin/lib/io.cjs +38 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +821 -109
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +507 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +258 -58
- package/gsd-core/bin/lib/phase.cjs +891 -156
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +405 -84
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
- package/gsd-core/bin/lib/smart-entry.cjs +154 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +424 -105
- package/gsd-core/bin/lib/state.cjs +1927 -401
- package/gsd-core/bin/lib/surface.cjs +35 -10
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
- package/gsd-core/bin/lib/uat.cjs +706 -64
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +33 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -20
- package/gsd-core/bin/lib/verify.cjs +368 -880
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
- package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
- package/gsd-core/bin/lib/workstream.cjs +8 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +8 -7
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +33 -70
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +74 -166
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +160 -95
- package/gsd-core/workflows/debug.md +16 -17
- package/gsd-core/workflows/diagnose-issues.md +56 -8
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +103 -187
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +7 -6
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +35 -47
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +27 -240
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +89 -209
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +45 -159
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +55 -405
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +41 -13
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +41 -20
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +106 -13
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +53 -31
- package/gsd-core/workflows/ui-phase.md +13 -12
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +19 -8
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +17 -65
- package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
- package/hooks/dist/gsd-check-update-worker.js +64 -12
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +45 -24
- package/hooks/dist/gsd-statusline.js +90 -6
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +277 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +517 -0
- package/hooks/gsd-check-update-worker.js +64 -12
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +607 -26
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +45 -24
- package/hooks/gsd-statusline.js +90 -6
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +277 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +31 -10
- package/pi/gsd.cjs +71 -12
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +68 -6
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +439 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +150 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +40 -3
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +52 -6
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +2 -2
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +2 -2
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +1 -2
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -577
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/gen-emitted-baseline.cjs +0 -145
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1050
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* resolve-model <agent-type> Get model for agent based on profile
|
|
21
21
|
* find-phase <phase> Find phase directory by number
|
|
22
22
|
* commit <message> [--files f1 f2] [--no-verify] Commit planning docs
|
|
23
|
+
* commit-docs-guard enable|disable Opt-in .git/hooks/pre-commit guard
|
|
24
|
+
* that refuses a commit staging
|
|
25
|
+
* .planning/ when commit_docs is false
|
|
23
26
|
* commit-to-subrepo <msg> --files f1 f2 Route commits to sub-repos
|
|
24
27
|
* verify-summary <path> Verify a SUMMARY.md file
|
|
25
28
|
* generate-slug <text> Convert text to URL-safe slug
|
|
@@ -68,6 +71,14 @@
|
|
|
68
71
|
* milestone complete <version> Archive milestone, create MILESTONES.md
|
|
69
72
|
* [--name <name>]
|
|
70
73
|
* [--no-archive-phases] Skip moving phase dirs to milestones/vX.Y-phases/ (archived by default)
|
|
74
|
+
* [--archive-quick] Move .planning/quick/* dirs to milestones/vX.Y-quick/ + reset the
|
|
75
|
+
* Quick Tasks Completed table (#2142; opt-in, default OFF)
|
|
76
|
+
*
|
|
77
|
+
* milestone archive-quick <version> Move .planning/quick/* dirs to milestones/vX.Y-quick/ + reset the
|
|
78
|
+
* Quick Tasks Completed table, WITHOUT the milestone complete close-out
|
|
79
|
+
* (no ROADMAP/REQUIREMENTS/MILESTONES.md writes, no completion guards);
|
|
80
|
+
* safe against an already-completed milestone (#2142 escalation)
|
|
81
|
+
* [--dry-run] Preview what would move, mutates nothing
|
|
71
82
|
*
|
|
72
83
|
* User Story Validation:
|
|
73
84
|
* user-story validate --story "..." Validate "As a / I want to / so that" format
|
|
@@ -80,6 +91,7 @@
|
|
|
80
91
|
* drift-guard severity --status <S> Classify a symbol verdict into { severity, hardBlock }
|
|
81
92
|
* [--authority <A>] Status: VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE
|
|
82
93
|
* Authority: grep|intel|treesitter|lsp|scip (default: config-resolved)
|
|
94
|
+
* drift-guard phase-status [--phase N] Compare STATE.md vs ROADMAP.md phase status
|
|
83
95
|
*
|
|
84
96
|
* Validation:
|
|
85
97
|
* validate consistency Check phase numbering, disk/roadmap sync
|
|
@@ -259,11 +271,13 @@ try {
|
|
|
259
271
|
}
|
|
260
272
|
} catch { /* advisory — never block */ }
|
|
261
273
|
|
|
262
|
-
const {
|
|
263
|
-
const { resolveActiveWorkstream, applyResolvedWorkstreamEnv } = require('./lib/active-workstream-store.cjs');
|
|
274
|
+
const { resolveActiveWorkstream, applyResolvedWorkstreamEnv, peekActiveWorkstream } = require('./lib/active-workstream-store.cjs');
|
|
264
275
|
const state = require('./lib/state.cjs');
|
|
265
276
|
const phase = require('./lib/phase.cjs');
|
|
266
277
|
const roadmap = require('./lib/roadmap.cjs');
|
|
278
|
+
// #3024: resolve skills root for the sync-skills workflow (install.js is not
|
|
279
|
+
// shipped in installed trees; gsd-tools IS shipped, so the workflow calls this).
|
|
280
|
+
const { getGlobalSkillsBase, isRegisteredRuntimeId } = require('./lib/runtime-homes.cjs');
|
|
267
281
|
// #1561 — assumption-delta advisory checkpoint detector (pure function).
|
|
268
282
|
const { detectAssumptionDelta } = require('./lib/assumption-delta.cjs');
|
|
269
283
|
const verify = require('./lib/verify.cjs');
|
|
@@ -302,7 +316,7 @@ const { routeCheckCommand } = require('./lib/check-command-router.cjs');
|
|
|
302
316
|
const { routeTaskCommand } = require('./lib/task-command-router.cjs');
|
|
303
317
|
const { parseNamedArgs, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
|
|
304
318
|
const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
|
|
305
|
-
const { getEffectiveAuthority, classifyDriftSeverity } = require('./lib/plan-drift-guard.cjs');
|
|
319
|
+
const { getEffectiveAuthority, classifyDriftSeverity, comparePhaseStatus } = require('./lib/plan-drift-guard.cjs');
|
|
306
320
|
|
|
307
321
|
// ─── Bridge collapsed (Phase 4) ────────────────────────────────────────────────
|
|
308
322
|
// Non-family commands now run through their CJS handlers directly. Keep the
|
|
@@ -918,6 +932,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
918
932
|
commands.cmdCheckCommit(cwd, raw);
|
|
919
933
|
}
|
|
920
934
|
|
|
935
|
+
function routeCommitDocsGuard({ args, cwd, raw, error }) {
|
|
936
|
+
const subcommand = args[1];
|
|
937
|
+
if (subcommand === 'enable') {
|
|
938
|
+
commands.cmdCommitDocsGuardEnable(cwd, raw);
|
|
939
|
+
} else if (subcommand === 'disable') {
|
|
940
|
+
commands.cmdCommitDocsGuardDisable(cwd, raw);
|
|
941
|
+
} else {
|
|
942
|
+
error('Unknown commit-docs-guard subcommand. Available: enable, disable', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
921
946
|
function routeCommitToSubrepo({ args, cwd, raw, error }) {
|
|
922
947
|
const message = args[1];
|
|
923
948
|
const filesIndex = args.indexOf('--files');
|
|
@@ -1044,6 +1069,37 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1044
1069
|
commands.cmdCurrentTimestamp(args[1] || 'full', raw);
|
|
1045
1070
|
}
|
|
1046
1071
|
|
|
1072
|
+
function routeSkillsRoot({ args, raw, error }) {
|
|
1073
|
+
// #3024: resolve the global skills base directory for a runtime.
|
|
1074
|
+
// The sync-skills workflow previously shelled out to install.js --skills-root,
|
|
1075
|
+
// but install.js is not shipped in installed trees. gsd-tools IS shipped, so
|
|
1076
|
+
// the workflow now calls `gsd-tools query skills-root <runtime>` instead.
|
|
1077
|
+
const runtime = args[1];
|
|
1078
|
+
if (!runtime) {
|
|
1079
|
+
error('Usage: gsd-tools query skills-root <runtime>');
|
|
1080
|
+
}
|
|
1081
|
+
// Defect B (#3024): validate the runtime id against the shipped capability
|
|
1082
|
+
// registry's canonical runtime set BEFORE resolving anything.
|
|
1083
|
+
// getGlobalSkillsBase falls through getGlobalConfigDir's unknown-runtime
|
|
1084
|
+
// branch to claude's skills root for ANY id it doesn't recognize, so an
|
|
1085
|
+
// unknown, empty/whitespace-only, path-traversal, or shell-metacharacter
|
|
1086
|
+
// runtime arg would otherwise silently resolve to claude's path instead of
|
|
1087
|
+
// failing loudly. isRegisteredRuntimeId does an own-property lookup (not a
|
|
1088
|
+
// bare index), rejecting `__proto__`/`constructor`/`prototype` runtime
|
|
1089
|
+
// ids, and is the SAME validator install.js's `--skills-root` entry point
|
|
1090
|
+
// calls, so the two shipped entry points can never diverge on which
|
|
1091
|
+
// runtime ids they accept.
|
|
1092
|
+
if (!isRegisteredRuntimeId(runtime)) {
|
|
1093
|
+
error(`Unknown runtime "${runtime}" — must be a registered runtime id`);
|
|
1094
|
+
}
|
|
1095
|
+
const trimmedRuntime = typeof runtime === 'string' ? runtime.trim() : '';
|
|
1096
|
+
const skillsRoot = getGlobalSkillsBase(trimmedRuntime);
|
|
1097
|
+
if (skillsRoot === null) {
|
|
1098
|
+
error(`No skills root found for runtime "${trimmedRuntime}"`);
|
|
1099
|
+
}
|
|
1100
|
+
output({ skills_root: skillsRoot }, raw, skillsRoot);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1047
1103
|
function routeProjectInstructionFile({ args, cwd, raw, error }) {
|
|
1048
1104
|
// #1529: pure runtime→filename projection. Backs the
|
|
1049
1105
|
// `gsd_run query project-instruction-file --runtime <r>` call in
|
|
@@ -1145,16 +1201,33 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1145
1201
|
const cp = require('node:child_process');
|
|
1146
1202
|
const fsx = require('node:fs');
|
|
1147
1203
|
const os = require('node:os');
|
|
1148
|
-
const { REVIEWER_LANES } = require('./lib/review-lane-descriptor.cjs');
|
|
1204
|
+
const { REVIEWER_LANES, mergeReviewerLanes } = require('./lib/review-lane-descriptor.cjs');
|
|
1149
1205
|
const { resolveLanePlan } = require('./lib/review-lane-invocation.cjs');
|
|
1150
1206
|
const runner = require('./lib/review-lane-runner.cjs');
|
|
1151
1207
|
const cfgLoader = require('./lib/config-loader.cjs');
|
|
1208
|
+
const capabilityLoader = require('./lib/capability-loader.cjs');
|
|
1152
1209
|
|
|
1153
1210
|
const flag = (name) => {
|
|
1154
1211
|
const i = args.indexOf(name);
|
|
1155
1212
|
return i !== -1 && args[i + 1] && !String(args[i + 1]).startsWith('--') ? args[i + 1] : null;
|
|
1156
1213
|
};
|
|
1157
1214
|
const sub = args[1];
|
|
1215
|
+
// Fail fast on an unrecognized subcommand. Without this check, `sub` fell through
|
|
1216
|
+
// to the `sub !== 'invoke'` usage-error branch far below (after loading the
|
|
1217
|
+
// capability registry AND building a per-lane plan for every lane — which itself
|
|
1218
|
+
// spawns one child `query resolve-execution` process per lane via `effortFor`,
|
|
1219
|
+
// up to 12 subprocess spawns for the default lane set) before ever reporting the
|
|
1220
|
+
// error. That made an invalid subcommand slow instead of instant, and under bench
|
|
1221
|
+
// load (many sequential node spawns) `review-lane bogus` could exceed a caller's
|
|
1222
|
+
// spawn timeout and be killed before writing anything to stderr — the CI-observed
|
|
1223
|
+
// failure was empty stdout AND stderr, not the expected usage message (#3148).
|
|
1224
|
+
// `plan`/`invoke` are the only subs that need the expensive plan-building path
|
|
1225
|
+
// below; `sections`/`flags` return earlier still. Anything else errors here, before
|
|
1226
|
+
// any of that work starts.
|
|
1227
|
+
if (!['plan', 'invoke', 'sections', 'flags'].includes(sub)) {
|
|
1228
|
+
error("Usage: review-lane <plan|invoke|sections|flags> [--selected a,b] [--run-dir D] [--repo-root R]");
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1158
1231
|
const runDir = flag('--run-dir') || '.';
|
|
1159
1232
|
const repoRoot = flag('--repo-root') || cwd;
|
|
1160
1233
|
|
|
@@ -1176,8 +1249,30 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1176
1249
|
|
|
1177
1250
|
const selected = (flag('--selected') || '')
|
|
1178
1251
|
.split(',').map((s) => s.trim()).filter(Boolean);
|
|
1179
|
-
|
|
1180
|
-
|
|
1252
|
+
// ADR-2782 D8 (#2927): the lane map is first-party ∪ INSTALLED overlay
|
|
1253
|
+
// `reviewer` bodies, first-party winning on slug collision. Before this merge
|
|
1254
|
+
// the map was built from the frozen REVIEWER_LANES array alone, so an installed,
|
|
1255
|
+
// consented third-party reviewer lane was roster-visible (deriveReviewerSlugs)
|
|
1256
|
+
// and disclosed at install (collectReviewerLaneSurfaces) but never selectable,
|
|
1257
|
+
// plannable, or invocable — `sections`/`flags`/`plan`/`invoke` all consumed this
|
|
1258
|
+
// one map. The overlay body is field-identical to a ReviewerLane (ADR-2782 D1,
|
|
1259
|
+
// "no translation layer"), so `mergeReviewerLanes` is a pure merge, not a
|
|
1260
|
+
// projection. loadRegistry is TOTAL and never throws on a malformed overlay
|
|
1261
|
+
// (it skips the cap with a warning), and mergeReviewerLanes is total in turn,
|
|
1262
|
+
// so a bad third-party manifest cannot take the first-party lanes down with it.
|
|
1263
|
+
// `includeInstalled` is what merges project + global overlay caps into the
|
|
1264
|
+
// registry; without it the base is first-party-only and this is a no-op.
|
|
1265
|
+
let mergedLanes = REVIEWER_LANES;
|
|
1266
|
+
try {
|
|
1267
|
+
const registry = capabilityLoader.loadRegistry({ includeInstalled: true, cwd });
|
|
1268
|
+
mergedLanes = mergeReviewerLanes(REVIEWER_LANES, registry);
|
|
1269
|
+
} catch {
|
|
1270
|
+
// A registry load failure must never block first-party review. Degrade to the
|
|
1271
|
+
// static set — identical to pre-fix behavior — rather than crashing review-lane.
|
|
1272
|
+
mergedLanes = REVIEWER_LANES;
|
|
1273
|
+
}
|
|
1274
|
+
const laneBySlug = new Map(mergedLanes.map((l) => [l.slug, l]));
|
|
1275
|
+
const chosen = selected.length ? selected : mergedLanes.map((l) => l.slug);
|
|
1181
1276
|
|
|
1182
1277
|
if (sub === 'sections') {
|
|
1183
1278
|
const rows = chosen
|
|
@@ -1212,30 +1307,45 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1212
1307
|
return;
|
|
1213
1308
|
}
|
|
1214
1309
|
|
|
1215
|
-
// Effort argv is resolved per lane by the host's own execution policy,
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
//
|
|
1310
|
+
// Effort argv is resolved per lane by the host's own execution policy, through the SAME
|
|
1311
|
+
// `resolve-execution` surface the bash legs used (`--host <slug>`), so the host's negotiated
|
|
1312
|
+
// effortSurface still decides whether an argument is emitted and the catalog still owns the
|
|
1313
|
+
// syntax (ADR-1239 #2481, ADR-443's escalation ladder). `cmdResolveExecution` writes to
|
|
1314
|
+
// stdout and exits, so it cannot be called in-process for a value — this spawns the same
|
|
1315
|
+
// bounded query the legs did, once per selected lane. A lane whose slug is not a known host
|
|
1316
|
+
// resolves to no effort argument at all.
|
|
1317
|
+
//
|
|
1318
|
+
// NOT `--raw` and NOT `--pick` (#2295). `--raw` prints only the resolved EFFORT ('low') with
|
|
1319
|
+
// no host-specific rendering at all. `--pick effort_argv_string` used to be the answer — the
|
|
1320
|
+
// rendered array re-joined into a string ('-c model_reasoning_effort=low') — but the caller
|
|
1321
|
+
// then had to `.split(/\s+/)` that string back apart to get an argv array, and re-splitting a
|
|
1322
|
+
// string the callee just joined is a lossy round trip: any argv element that legitimately
|
|
1323
|
+
// contains a space would come back split into two argv elements, corrupting the very argv it
|
|
1324
|
+
// was rendered to preserve. Reading the UNPICKED object instead gives both `effort_argv` (a
|
|
1325
|
+
// real string array, used verbatim, no re-splitting) and `effort_argv_value` (the bare level,
|
|
1326
|
+
// #2295's `plan.effort`) from the one spawn.
|
|
1327
|
+
const EMPTY_EFFORT = { argv: [], value: null };
|
|
1224
1328
|
const effortFor = (slug) => {
|
|
1225
1329
|
try {
|
|
1226
1330
|
const r = cp.spawnSync(
|
|
1227
1331
|
process.execPath,
|
|
1228
|
-
[__filename, 'query', 'resolve-execution', 'gsd-plan-checker',
|
|
1229
|
-
// NOT `--raw`: that prints the resolved EFFORT ('low'), ignoring --pick. The picked
|
|
1230
|
-
// field is what carries the host-specific syntax ('--effort low' for claude,
|
|
1231
|
-
// '-c model_reasoning_effort=low' for codex), which is the whole point of asking.
|
|
1232
|
-
'--host', slug, '--pick', 'effort_argv_string'],
|
|
1332
|
+
[__filename, 'query', 'resolve-execution', 'gsd-plan-checker', '--host', slug],
|
|
1233
1333
|
{ cwd, encoding: 'utf8', timeout: 15000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 },
|
|
1234
1334
|
);
|
|
1235
|
-
if (r.status !== 0) return
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1335
|
+
if (r.status !== 0) return EMPTY_EFFORT;
|
|
1336
|
+
let parsed;
|
|
1337
|
+
try {
|
|
1338
|
+
parsed = JSON.parse(String(r.stdout || ''));
|
|
1339
|
+
} catch { return EMPTY_EFFORT; }
|
|
1340
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return EMPTY_EFFORT;
|
|
1341
|
+
const argv = Array.isArray(parsed.effort_argv)
|
|
1342
|
+
? parsed.effort_argv.filter((a) => typeof a === 'string' && a !== '')
|
|
1343
|
+
: [];
|
|
1344
|
+
const value = typeof parsed.effort_argv_value === 'string' && parsed.effort_argv_value
|
|
1345
|
+
? parsed.effort_argv_value
|
|
1346
|
+
: null;
|
|
1347
|
+
return { argv, value };
|
|
1348
|
+
} catch { return EMPTY_EFFORT; }
|
|
1239
1349
|
};
|
|
1240
1350
|
|
|
1241
1351
|
/**
|
|
@@ -1268,7 +1378,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1268
1378
|
// so losing all of them to one bad manifest is strictly worse. Belt and braces on purpose.
|
|
1269
1379
|
let r;
|
|
1270
1380
|
try {
|
|
1271
|
-
|
|
1381
|
+
const effort = effortFor(slug);
|
|
1382
|
+
r = resolveLanePlan({ lane, configGet, runDir, repoRoot, effortArgs: effort.argv, effortValue: effort.value });
|
|
1272
1383
|
} catch (e) {
|
|
1273
1384
|
return { slug, ok: false, reason: 'malformed_lane', detail: `resolver threw: ${e && e.message ? e.message : String(e)}` };
|
|
1274
1385
|
}
|
|
@@ -1307,13 +1418,40 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1307
1418
|
// cannot be interrupted by --test-force-exit and hangs a whole CI chunk to its 10-minute kill.
|
|
1308
1419
|
const deps = {
|
|
1309
1420
|
spawn: (binary, argv, opts) => {
|
|
1310
|
-
|
|
1421
|
+
// #3086: on Windows, reviewer CLIs (gemini, codex, etc.) are installed
|
|
1422
|
+
// as .cmd shims. spawnSync with a bare name + shell:false fails with
|
|
1423
|
+
// ENOENT (CreateProcess cannot start .cmd). Apply the same #2667 shim
|
|
1424
|
+
// gate used in runWithTimeout: detect .cmd/.bat and mediate through
|
|
1425
|
+
// cmd.exe /d /s /c with an explicit argv array (no shell:true).
|
|
1426
|
+
//
|
|
1427
|
+
// #3275: descriptors declare BARE names, so the gate above never saw an
|
|
1428
|
+
// extension — resolve through the shared PATH+PATHEXT resolver FIRST
|
|
1429
|
+
// (the same one `hasBinary` uses, so probe and spawn can never disagree
|
|
1430
|
+
// about what the lane's binary is). POSIX keeps the bare name: Node's own
|
|
1431
|
+
// PATH search already worked there, and the #3275 acceptance contract
|
|
1432
|
+
// holds macOS/Linux behavior unchanged. A name that resolves to nothing
|
|
1433
|
+
// falls back to the declared name so the ENOENT still surfaces (#3086).
|
|
1434
|
+
// #3411: the resolve-then-mediate pair is one seam call now. Both halves had
|
|
1435
|
+
// private copies here; `projectSpawnInvocation` owns them, so a fix to either
|
|
1436
|
+
// reaches every spawn site instead of only this one.
|
|
1437
|
+
//
|
|
1438
|
+
// Unlike execTool, this lane adopts the RESOLVED path even for a non-batch
|
|
1439
|
+
// binary: that is the behavior #3445 shipped and `deps.hasBinary` answers
|
|
1440
|
+
// from the same resolver, so probe and spawn must agree on the exact file.
|
|
1441
|
+
const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
|
|
1442
|
+
const { command: spawnBinary, args: spawnArgv, windowsVerbatimArguments } = projectSpawnInvocation(binary, argv);
|
|
1443
|
+
const r = cp.spawnSync(spawnBinary, spawnArgv, {
|
|
1311
1444
|
input: opts.input,
|
|
1312
1445
|
encoding: 'utf8',
|
|
1313
1446
|
timeout: opts.timeoutMs,
|
|
1314
1447
|
killSignal: 'SIGKILL',
|
|
1315
1448
|
maxBuffer: 64 * 1024 * 1024,
|
|
1316
1449
|
shell: false, // argv array only — never a shell string (no interpolation of config values)
|
|
1450
|
+
// #2483: a lane's declared env pairs merged OVER this process's environment, for this
|
|
1451
|
+
// child only. Passing a fresh object leaves `process.env` untouched, so nothing leaks
|
|
1452
|
+
// into the orchestrating session or into the next lane.
|
|
1453
|
+
...(opts.env ? { env: { ...process.env, ...opts.env } } : {}),
|
|
1454
|
+
...(windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
1317
1455
|
});
|
|
1318
1456
|
return {
|
|
1319
1457
|
status: r.status,
|
|
@@ -1342,24 +1480,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1342
1480
|
// all (a probe that costs a process is a probe you avoid running, which is how the original
|
|
1343
1481
|
// Kimi probe ended up unbounded), and `shell: true` with an args array is deprecated in
|
|
1344
1482
|
// Node 26 (DEP0190) because the arguments are concatenated rather than escaped.
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
for (const dir of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
1353
|
-
for (const ext of exts) {
|
|
1354
|
-
const candidate = path.join(dir, name + ext);
|
|
1355
|
-
try {
|
|
1356
|
-
const st = fsx.statSync(candidate);
|
|
1357
|
-
if (st.isFile()) return true;
|
|
1358
|
-
} catch { /* next candidate */ }
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
return false;
|
|
1362
|
-
},
|
|
1483
|
+
//
|
|
1484
|
+
// #3275: the scan lives in `resolveSpawnBinary` now, SHARED with `deps.spawn`
|
|
1485
|
+
// above. Two private copies of "what is this declared binary?" is how the
|
|
1486
|
+
// defect hid: the probe resolved WITH PATHEXT while spawn resolved WITHOUT,
|
|
1487
|
+
// so a lane reported available for a spawn that could never start. One
|
|
1488
|
+
// resolver, both seams — if one changes, the other changes with it.
|
|
1489
|
+
hasBinary: (name) => resolveSpawnBinary(name) !== null,
|
|
1363
1490
|
configGet,
|
|
1364
1491
|
homeDir: os.homedir(),
|
|
1365
1492
|
warn: (m) => process.stderr.write(`${m}\n`),
|
|
@@ -1411,12 +1538,14 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1411
1538
|
`model override (it declares no modelConfigKey). The review will use the CLI's own default.\n`,
|
|
1412
1539
|
);
|
|
1413
1540
|
}
|
|
1541
|
+
const instanceEffort = effortFor(entry.slug);
|
|
1414
1542
|
const overridden = resolveLanePlan({
|
|
1415
1543
|
lane,
|
|
1416
1544
|
configGet: (k) => (key && k === key ? instanceModel : configGet(k)),
|
|
1417
1545
|
runDir,
|
|
1418
1546
|
repoRoot,
|
|
1419
|
-
effortArgs:
|
|
1547
|
+
effortArgs: instanceEffort.argv,
|
|
1548
|
+
effortValue: instanceEffort.value,
|
|
1420
1549
|
});
|
|
1421
1550
|
if (overridden.ok) {
|
|
1422
1551
|
// Preserve any instance retargeting already applied above.
|
|
@@ -1493,7 +1622,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1493
1622
|
function routeDispatchShouldFlatten({ args, cwd, raw, error }) {
|
|
1494
1623
|
// #1708 / #853: typed query replacing the `RUNTIME === 'codex'` prose rule.
|
|
1495
1624
|
//
|
|
1496
|
-
// Resolves the current runtime (GSD_RUNTIME > config.runtime >
|
|
1625
|
+
// Resolves the current runtime (GSD_RUNTIME > config.runtime > per-install
|
|
1626
|
+
// .gsd-runtime marker > 'claude'),
|
|
1497
1627
|
// looks up registry.runtimes[id].runtime.hostIntegration.dispatch, and
|
|
1498
1628
|
// calls shouldFlattenDispatch(dispatch) from host-integration.cjs.
|
|
1499
1629
|
//
|
|
@@ -1547,7 +1677,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1547
1677
|
// #853) for exactly this reason — it replaced a `RUNTIME === 'codex'`
|
|
1548
1678
|
// prose rule.
|
|
1549
1679
|
//
|
|
1550
|
-
// Resolves the current runtime (GSD_RUNTIME > config.runtime >
|
|
1680
|
+
// Resolves the current runtime (GSD_RUNTIME > config.runtime > per-install
|
|
1681
|
+
// .gsd-runtime marker > 'claude'),
|
|
1551
1682
|
// reads registry.runtimes[id].runtime.hostIntegration.dispatch.isolation,
|
|
1552
1683
|
// and validates it against the closed vocabulary.
|
|
1553
1684
|
//
|
|
@@ -1564,12 +1695,101 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1564
1695
|
// `orchestrator-worktree`. It requires `--cwd-target` (the GSD-created
|
|
1565
1696
|
// worktree path) and optionally `--prompt`; without a target there is
|
|
1566
1697
|
// nothing to bind, so `exec` is null.
|
|
1567
|
-
|
|
1698
|
+
//
|
|
1699
|
+
// #3045 CORE REDESIGN: this is now the SOLE resolver of "what isolation
|
|
1700
|
+
// applies to this dispatch", and — as an unconditional side effect — it
|
|
1701
|
+
// PERSISTS that resolved decision (mode + harnessFlag + phase/plan
|
|
1702
|
+
// identifiers, written together in one atomic write) to the sentinel the
|
|
1703
|
+
// guard hooks read. Previously the sentinel was written by prose-gated
|
|
1704
|
+
// shell blocks in `executor-isolation-dispatch.md` that a model was told
|
|
1705
|
+
// to "read and run" — a prose-gated writer for a guard against
|
|
1706
|
+
// prose-gated values is the same defect class the guard exists to close.
|
|
1707
|
+
// The workflow MUST call this query to learn ISOLATION at all, so
|
|
1708
|
+
// recording here is structurally unskippable. `--phase`/`--plan` are
|
|
1709
|
+
// optional identifiers threaded through from the caller (workflow shell
|
|
1710
|
+
// variables); `--force-isolation <mode>` lets a caller that has
|
|
1711
|
+
// additional context this resolver cannot see (the #2474 per-plan
|
|
1712
|
+
// submodule intersection, computed in shell in
|
|
1713
|
+
// `per-plan-worktree-gate.md`) override the naturally-resolved mode
|
|
1714
|
+
// while still going through this single write path. Best-effort: a
|
|
1715
|
+
// sentinel write failure here must never fail the wave.
|
|
1716
|
+
const decision = resolveDispatchIsolationDecision({ args, cwd });
|
|
1717
|
+
const runtimeId = decision.runtimeId;
|
|
1718
|
+
let { isolation, exec, harnessFlag } = decision;
|
|
1719
|
+
|
|
1720
|
+
// `--force-isolation <mode>` overrides the naturally-resolved mode with
|
|
1721
|
+
// context this resolver has no way to see on its own (e.g. the #2474
|
|
1722
|
+
// per-plan submodule intersection). Invalid/unrecognized values are
|
|
1723
|
+
// ignored rather than erroring — this is a best-effort recording call,
|
|
1724
|
+
// not a hard usage gate. Forcing to 'none' clears harnessFlag/exec since
|
|
1725
|
+
// neither applies to sequential dispatch.
|
|
1726
|
+
const forceIdx = args.indexOf('--force-isolation');
|
|
1727
|
+
const forcedIsolation = forceIdx !== -1 ? args[forceIdx + 1] : undefined;
|
|
1728
|
+
if (forcedIsolation && DISPATCH_ISOLATION_VOCABULARY.has(forcedIsolation)) {
|
|
1729
|
+
isolation = forcedIsolation;
|
|
1730
|
+
if (isolation === 'none') {
|
|
1731
|
+
harnessFlag = null;
|
|
1732
|
+
exec = null;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
const phaseIdx = args.indexOf('--phase');
|
|
1737
|
+
const phaseArg = phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--')
|
|
1738
|
+
? args[phaseIdx + 1]
|
|
1739
|
+
: null;
|
|
1740
|
+
const planIdx = args.indexOf('--plan');
|
|
1741
|
+
const planArg = planIdx !== -1 && args[planIdx + 1] && !args[planIdx + 1].startsWith('--')
|
|
1742
|
+
? args[planIdx + 1]
|
|
1743
|
+
: null;
|
|
1744
|
+
|
|
1745
|
+
// Side-effect write (#3045 CORE REDESIGN) — see the doc comment above.
|
|
1746
|
+
// Never allowed to affect this query's own stdout contract or throw.
|
|
1747
|
+
try {
|
|
1748
|
+
writeDispatchIsolationSentinel(cwd, { isolation, harnessFlag, phase: phaseArg, plan: planArg });
|
|
1749
|
+
} catch {
|
|
1750
|
+
// writeDispatchIsolationSentinel already swallows its own errors into
|
|
1751
|
+
// a { recorded: false } result; this catch is defense in depth only.
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
if (args.indexOf('--json') !== -1) {
|
|
1755
|
+
output({ runtime: runtimeId, isolation, exec, harnessFlag }, raw);
|
|
1756
|
+
} else {
|
|
1757
|
+
process.stdout.write(isolation);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
const DISPATCH_ISOLATION_VOCABULARY = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Shared, side-effect-free resolution of the negotiated dispatch isolation:
|
|
1765
|
+
* runtime (GSD_RUNTIME > config.runtime > per-install .gsd-runtime marker >
|
|
1766
|
+
* 'claude') → declared
|
|
1767
|
+
* `dispatch.isolation` → harness-flag / orchestrator-exec degrade rules.
|
|
1768
|
+
* Extracted (#2486) so `routeDispatchIsolation` (the #3045 recording
|
|
1769
|
+
* dispatch path) and `routeInspectDispatchIsolation` (the read-only
|
|
1770
|
+
* inspection path) share exactly one negotiation implementation and cannot
|
|
1771
|
+
* drift apart. Resolution only — the caller decides whether the decision is
|
|
1772
|
+
* recorded to the sentinel.
|
|
1773
|
+
*/
|
|
1774
|
+
function resolveDispatchIsolationDecision({ args, cwd }) {
|
|
1568
1775
|
let isolation = 'none';
|
|
1569
1776
|
let runtimeId = null;
|
|
1570
1777
|
let exec = null;
|
|
1571
1778
|
let harnessFlag = null;
|
|
1572
1779
|
try {
|
|
1780
|
+
// Deliberately `resolveRuntime`, NOT `resolveActiveRuntime`/`loadConfig`:
|
|
1781
|
+
// loadConfig normalizes and rewrites legacy keys back to disk, and this
|
|
1782
|
+
// resolver backs the sentinel-free `inspect-dispatch-isolation` verb,
|
|
1783
|
+
// which must never write. resolveRuntime reads config.json directly.
|
|
1784
|
+
//
|
|
1785
|
+
// KNOWN LIMITATION, tracked separately: resolveRuntime stops at
|
|
1786
|
+
// GSD_RUNTIME > config.runtime > 'claude' and does not consult the
|
|
1787
|
+
// per-install `.gsd-runtime` marker, so on a non-Claude install whose
|
|
1788
|
+
// project config carries no `runtime` key this resolves 'claude'. That is
|
|
1789
|
+
// open-gsd/gsd-core#2395 — a pre-existing defect in the canonical
|
|
1790
|
+
// resolver, not introduced here, and deliberately NOT fixed in this PR
|
|
1791
|
+
// (its blast radius reaches every consumer of that resolver, so it is
|
|
1792
|
+
// being handled on its own).
|
|
1573
1793
|
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
1574
1794
|
runtimeId = resolveRuntime(cwd);
|
|
1575
1795
|
|
|
@@ -1578,7 +1798,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1578
1798
|
? registry.runtimes[runtimeId]
|
|
1579
1799
|
: null;
|
|
1580
1800
|
const declared = runtimeEntry?.runtime?.hostIntegration?.dispatch?.isolation ?? null;
|
|
1581
|
-
if (typeof declared === 'string' &&
|
|
1801
|
+
if (typeof declared === 'string' && DISPATCH_ISOLATION_VOCABULARY.has(declared)) {
|
|
1582
1802
|
isolation = declared;
|
|
1583
1803
|
}
|
|
1584
1804
|
|
|
@@ -1621,7 +1841,52 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1621
1841
|
exec = null;
|
|
1622
1842
|
harnessFlag = null;
|
|
1623
1843
|
}
|
|
1844
|
+
return { runtimeId, isolation, exec, harnessFlag };
|
|
1845
|
+
}
|
|
1624
1846
|
|
|
1847
|
+
function routeInspectDispatchIsolation({ args, cwd, raw }) {
|
|
1848
|
+
// #2486: sentinel-free sibling of `dispatch-isolation` for INSPECTION
|
|
1849
|
+
// surfaces — /gsd:health's W025 check and /gsd:settings' Worktrees
|
|
1850
|
+
// branching. The dispatch verb above intentionally records its resolved
|
|
1851
|
+
// decision to the isolation sentinel as an unconditional side effect
|
|
1852
|
+
// (#3045 CORE REDESIGN): correct for executor dispatch, where the record
|
|
1853
|
+
// must be structurally unskippable — but wrong for a read-only
|
|
1854
|
+
// diagnostic. A health check that records a phase:null/plan:null
|
|
1855
|
+
// sentinel can hard-block every executor dispatch for the sentinel's
|
|
1856
|
+
// lifetime, across sessions sharing the main checkout. Inspection
|
|
1857
|
+
// surfaces call this verb instead. Two claims, both narrower than
|
|
1858
|
+
// "side-effect-free", and both exactly true (#2486 review, Majors 2 & 4):
|
|
1859
|
+
//
|
|
1860
|
+
// 1. SENTINEL-FREE, not write-free. This route writes nothing itself, and
|
|
1861
|
+
// in particular never writes .gsd/dispatch-isolation-sentinel.json —
|
|
1862
|
+
// the only write that can hard-block a later executor dispatch. It is
|
|
1863
|
+
// NOT an unconditional claim of total filesystem purity: like every
|
|
1864
|
+
// gsd-tools invocation, it runs the shared bootstrap and
|
|
1865
|
+
// active-workstream resolution first. As of #3579's root-cause fix
|
|
1866
|
+
// that bootstrap resolves via the non-mutating peekActiveWorkstream
|
|
1867
|
+
// (never unlinks); an actual stale/invalid pointer is still
|
|
1868
|
+
// self-healed, but only by whichever verb's own getActiveWorkstream
|
|
1869
|
+
// call later consumes it for real — this inspection route makes no
|
|
1870
|
+
// such call, so it is now also side-effect-free on the pointer file.
|
|
1871
|
+
//
|
|
1872
|
+
// 2. SHARED NEGOTIATION, for the arguments this verb accepts. Both verbs
|
|
1873
|
+
// call resolveDispatchIsolationDecision, so the natural resolution
|
|
1874
|
+
// cannot drift. It is NOT a claim of byte-identical output for every
|
|
1875
|
+
// argv: routeDispatchIsolation applies --force-isolation AFTER the
|
|
1876
|
+
// shared helper returns, so the same argv could otherwise yield
|
|
1877
|
+
// 'none' there and the declared capability here. Rather than let a
|
|
1878
|
+
// caller receive a silently different answer, this verb REJECTS the
|
|
1879
|
+
// recording-only knobs outright — they exist to be recorded, and a
|
|
1880
|
+
// read has nothing to record.
|
|
1881
|
+
const RECORDING_ONLY_ARGS = ['--force-isolation', '--phase', '--plan'];
|
|
1882
|
+
const rejected = RECORDING_ONLY_ARGS.filter((flag) => args.indexOf(flag) !== -1);
|
|
1883
|
+
if (rejected.length > 0) {
|
|
1884
|
+
error(
|
|
1885
|
+
`inspect-dispatch-isolation: ${rejected.join(', ')} ${rejected.length === 1 ? 'is a' : 'are'} recording-only argument${rejected.length === 1 ? '' : 's'} and cannot be used on the inspection verb — it resolves the runtime's DECLARED capability and records nothing. Use 'query dispatch-isolation' if you need the override applied and the decision recorded.`,
|
|
1886
|
+
ERROR_REASON.USAGE,
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
const { runtimeId, isolation, exec, harnessFlag } = resolveDispatchIsolationDecision({ args, cwd });
|
|
1625
1890
|
if (args.indexOf('--json') !== -1) {
|
|
1626
1891
|
output({ runtime: runtimeId, isolation, exec, harnessFlag }, raw);
|
|
1627
1892
|
} else {
|
|
@@ -1629,6 +1894,110 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1629
1894
|
}
|
|
1630
1895
|
}
|
|
1631
1896
|
|
|
1897
|
+
/**
|
|
1898
|
+
* Atomically persist the resolved dispatch-isolation decision to the
|
|
1899
|
+
* run-scoped sentinel both isolation guard hooks read
|
|
1900
|
+
* (hooks/gsd-agent-isolation-guard.js, hooks/gsd-cursor-subagent-start.js;
|
|
1901
|
+
* shared reader hooks/lib/isolation-sentinel.js). Extracted so
|
|
1902
|
+
* `routeDispatchIsolation` (the #3045 CORE REDESIGN primary write path)
|
|
1903
|
+
* and `routeRecordDispatchIsolation` (the explicit verb, kept for the
|
|
1904
|
+
* per-plan degrade call site and back-compat/tests) share exactly one
|
|
1905
|
+
* write implementation. Never throws — returns `{ recorded, path, error? }`.
|
|
1906
|
+
*/
|
|
1907
|
+
function writeDispatchIsolationSentinel(cwd, { isolation, harnessFlag = null, phase = null, plan = null }) {
|
|
1908
|
+
const nodePath = require('path');
|
|
1909
|
+
const nodeFs = require('fs');
|
|
1910
|
+
const sentinelDir = nodePath.join(cwd, '.gsd');
|
|
1911
|
+
const sentinelPath = nodePath.join(sentinelDir, 'dispatch-isolation-sentinel.json');
|
|
1912
|
+
const payload = {
|
|
1913
|
+
isolation,
|
|
1914
|
+
harness_flag: harnessFlag || null,
|
|
1915
|
+
phase: phase || null,
|
|
1916
|
+
plan: plan || null,
|
|
1917
|
+
written_at: Date.now(),
|
|
1918
|
+
};
|
|
1919
|
+
try {
|
|
1920
|
+
nodeFs.mkdirSync(sentinelDir, { recursive: true });
|
|
1921
|
+
// Atomic write: unique temp file + rename, so a concurrent reader (a
|
|
1922
|
+
// guard hook firing mid-write) never observes a partially-written
|
|
1923
|
+
// sentinel. Unique per-process+time so concurrent orchestrator-worktree
|
|
1924
|
+
// invocations sharing the same sentinelDir never collide on the temp name.
|
|
1925
|
+
const tmpPath = `${sentinelPath}.tmp-${process.pid}-${Date.now()}`;
|
|
1926
|
+
nodeFs.writeFileSync(tmpPath, JSON.stringify(payload));
|
|
1927
|
+
nodeFs.renameSync(tmpPath, sentinelPath);
|
|
1928
|
+
return { recorded: true, path: '.gsd/dispatch-isolation-sentinel.json' };
|
|
1929
|
+
} catch (err) {
|
|
1930
|
+
return { recorded: false, path: '.gsd/dispatch-isolation-sentinel.json', error: err && err.message };
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
function routeRecordDispatchIsolation({ args, cwd, raw, error }) {
|
|
1935
|
+
// #3045: `routeDispatchIsolation` (the `dispatch-isolation` query) is now
|
|
1936
|
+
// the PRIMARY write path for the sentinel (CORE REDESIGN) — it records
|
|
1937
|
+
// as an unconditional side effect of resolving ISOLATION, which the
|
|
1938
|
+
// workflow must call to learn the value at all. This verb remains as an
|
|
1939
|
+
// explicit fallback for callers that resolve isolation through some
|
|
1940
|
+
// other means (or need to force a specific value, e.g. a caller with no
|
|
1941
|
+
// access to `--force-isolation` context) and for direct test coverage of
|
|
1942
|
+
// the write primitive. Both verbs share exactly one write implementation
|
|
1943
|
+
// (`writeDispatchIsolationSentinel`) so there is only one atomic-write
|
|
1944
|
+
// code path to reason about.
|
|
1945
|
+
//
|
|
1946
|
+
// Best-effort: a write failure here must never fail the workflow — the
|
|
1947
|
+
// guard hooks' own sentinel-absent path degrades to a conservative
|
|
1948
|
+
// registry+config check, so a missing sentinel is safe, just less precise.
|
|
1949
|
+
//
|
|
1950
|
+
// Output: { recorded: true|false, path, error? }
|
|
1951
|
+
const VALID_ISOLATION = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
|
|
1952
|
+
const isoIdx = args.indexOf('--isolation');
|
|
1953
|
+
const isolation = isoIdx !== -1 ? args[isoIdx + 1] : undefined;
|
|
1954
|
+
if (!isolation || !VALID_ISOLATION.has(isolation)) {
|
|
1955
|
+
error(
|
|
1956
|
+
'Usage: record-dispatch-isolation --isolation <harness-worktree|orchestrator-worktree|none> ' +
|
|
1957
|
+
'[--harness-flag <flag>|--harness-flag=<flag>] [--phase <n>] [--plan <id>]',
|
|
1958
|
+
ERROR_REASON.USAGE,
|
|
1959
|
+
);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
// #3045 MAJOR: the space-separated form rejects any value starting with
|
|
1963
|
+
// `--` (to avoid swallowing a missing value followed by another flag),
|
|
1964
|
+
// but that is exactly the shape of Cursor's real `harnessIsolationFlag`
|
|
1965
|
+
// — it declares the bare CLI flag `--worktree`
|
|
1966
|
+
// (gsd-core/bin/lib/capability-registry.cjs), which could therefore
|
|
1967
|
+
// never be persisted. (Windsurf declares NO `harnessIsolationFlag` at
|
|
1968
|
+
// all — its `hostIntegration.dispatch.isolation` is `none`; per
|
|
1969
|
+
// ADR-1239 it "genuinely cannot benefit" from worktree isolation
|
|
1970
|
+
// because it lacks named/concurrent subagent dispatch, so this is not a
|
|
1971
|
+
// gap to close for Windsurf.) The `--harness-flag=<value>` equals form
|
|
1972
|
+
// (mirrors the `--cwd=<path>` convention already used by this
|
|
1973
|
+
// dispatcher's top-level arg parsing above) carries the value
|
|
1974
|
+
// unambiguously and is never subject to that guard — any future runtime
|
|
1975
|
+
// whose registered flag happens to be bare-CLI-shaped benefits the same
|
|
1976
|
+
// way Cursor's does.
|
|
1977
|
+
let harnessFlag = null;
|
|
1978
|
+
const flagEqArg = args.find((a) => a.startsWith('--harness-flag='));
|
|
1979
|
+
if (flagEqArg) {
|
|
1980
|
+
const value = flagEqArg.slice('--harness-flag='.length);
|
|
1981
|
+
harnessFlag = value.length > 0 ? value : null;
|
|
1982
|
+
} else {
|
|
1983
|
+
const flagIdx = args.indexOf('--harness-flag');
|
|
1984
|
+
harnessFlag = flagIdx !== -1 && args[flagIdx + 1] && !args[flagIdx + 1].startsWith('--')
|
|
1985
|
+
? args[flagIdx + 1]
|
|
1986
|
+
: null;
|
|
1987
|
+
}
|
|
1988
|
+
const phaseIdx = args.indexOf('--phase');
|
|
1989
|
+
const phase = phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--')
|
|
1990
|
+
? args[phaseIdx + 1]
|
|
1991
|
+
: null;
|
|
1992
|
+
const planIdx = args.indexOf('--plan');
|
|
1993
|
+
const plan = planIdx !== -1 && args[planIdx + 1] && !args[planIdx + 1].startsWith('--')
|
|
1994
|
+
? args[planIdx + 1]
|
|
1995
|
+
: null;
|
|
1996
|
+
|
|
1997
|
+
const result = writeDispatchIsolationSentinel(cwd, { isolation, harnessFlag, phase, plan });
|
|
1998
|
+
output(result, raw);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1632
2001
|
function routeResolveDispatchType({ args, cwd, raw, error }) {
|
|
1633
2002
|
// #2508 Phase 4 Option A: resolve a requested GSD subagent name to the
|
|
1634
2003
|
// type an Agent() call should use on the current runtime. On
|
|
@@ -1668,6 +2037,50 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1668
2037
|
}
|
|
1669
2038
|
}
|
|
1670
2039
|
|
|
2040
|
+
function routeResolveAgent({ args, cwd, raw, error }) {
|
|
2041
|
+
// #1689: resolve a per-plan agent_hint specialist name to the subagent_type
|
|
2042
|
+
// an Agent() call should use. Returns the name unchanged when a
|
|
2043
|
+
// matching agent file exists in the active runtime's agent dir(s);
|
|
2044
|
+
// 'gsd-executor' when the name is absent, blank, or does not resolve.
|
|
2045
|
+
// Fail-closed is the fallback (gsd-executor) — never echo an
|
|
2046
|
+
// unvalidated name, which would make Agent() error and block the wave.
|
|
2047
|
+
//
|
|
2048
|
+
// Output:
|
|
2049
|
+
// --raw (default) -> prints the resolved type (the hint, or 'gsd-executor')
|
|
2050
|
+
// --json -> prints { runtime, requested, resolved, fallback }
|
|
2051
|
+
const FALLBACK = 'gsd-executor';
|
|
2052
|
+
try {
|
|
2053
|
+
const nameIdx = args.indexOf('--name');
|
|
2054
|
+
const requested = nameIdx !== -1 ? args[nameIdx + 1] : '';
|
|
2055
|
+
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
2056
|
+
const runtimeId = resolveRuntime(cwd);
|
|
2057
|
+
const { resolveAgentHint } = require('./lib/agent-install-check.cjs');
|
|
2058
|
+
let resolved = FALLBACK;
|
|
2059
|
+
let resolvedOk = false; // true only when resolveAgentHint returned a hit
|
|
2060
|
+
if (requested && !requested.startsWith('-')) {
|
|
2061
|
+
const hit = resolveAgentHint(requested, runtimeId, cwd);
|
|
2062
|
+
if (hit !== null) {
|
|
2063
|
+
resolved = hit;
|
|
2064
|
+
resolvedOk = true;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
// `fallback` = we did NOT honor a resolvable hint (absent/flag-shaped name,
|
|
2068
|
+
// the named agent did not resolve, or resolution errored). Requesting
|
|
2069
|
+
// gsd-executor explicitly and resolving to it is NOT a fallback.
|
|
2070
|
+
const fellBack = !resolvedOk;
|
|
2071
|
+
const jsonIdx = args.indexOf('--json');
|
|
2072
|
+
if (jsonIdx !== -1) {
|
|
2073
|
+
output({ runtime: runtimeId, requested: requested || null, resolved, fallback: fellBack }, raw);
|
|
2074
|
+
} else {
|
|
2075
|
+
process.stdout.write(String(resolved));
|
|
2076
|
+
}
|
|
2077
|
+
} catch {
|
|
2078
|
+
// Fail-closed: degrade to the legacy executor on any error so dispatch
|
|
2079
|
+
// never blocks on resolution.
|
|
2080
|
+
process.stdout.write(FALLBACK);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1671
2084
|
function routeAgentSkills({ args, cwd, raw, error }) {
|
|
1672
2085
|
// --json emits typed IR { agent_type, block, skills_count } for test assertions
|
|
1673
2086
|
// (#455). Default (no flag) outputs raw XML so workflow shell expansions work.
|
|
@@ -1768,9 +2181,20 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1768
2181
|
const force = args.includes('--force');
|
|
1769
2182
|
// #2118: --dry-run prints a preview plan without mutating.
|
|
1770
2183
|
const dryRun = args.includes('--dry-run');
|
|
1771
|
-
|
|
2184
|
+
// #2142: quick-task archival is opt-in (default OFF) — unlike
|
|
2185
|
+
// --no-archive-phases' inverted shape, absence of this flag means
|
|
2186
|
+
// "do nothing" rather than "skip a default-on behavior".
|
|
2187
|
+
const archiveQuick = args.includes('--archive-quick');
|
|
2188
|
+
milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun, archiveQuick }, raw);
|
|
2189
|
+
} else if (subcommand === 'archive-quick') {
|
|
2190
|
+
// #2142 escalation: narrow archival-only entry point (does NOT
|
|
2191
|
+
// touch ROADMAP/REQUIREMENTS/MILESTONES.md, runs no completion
|
|
2192
|
+
// guards) — safe to call against an already-completed milestone,
|
|
2193
|
+
// unlike `milestone complete --archive-quick`.
|
|
2194
|
+
const dryRun = args.includes('--dry-run');
|
|
2195
|
+
milestone.cmdQuickArchive(cwd, args[2], { dryRun }, raw);
|
|
1772
2196
|
} else {
|
|
1773
|
-
error('Unknown milestone subcommand. Available: complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2197
|
+
error('Unknown milestone subcommand. Available: complete, archive-quick', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1774
2198
|
}
|
|
1775
2199
|
}
|
|
1776
2200
|
|
|
@@ -2033,6 +2457,86 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2033
2457
|
teamsStatus.cmdTeamsStatus(cwd, { active: args.includes('--active') });
|
|
2034
2458
|
}
|
|
2035
2459
|
|
|
2460
|
+
// #3023 follow-up (adversarial review finding): the shared hook bundle's
|
|
2461
|
+
// directory name is runtime-descriptor-driven (bin/install.js
|
|
2462
|
+
// `hostBehaviors.sharedHooksDirName`; default 'hooks', pi renames it to
|
|
2463
|
+
// 'gsd-hooks'). A hardcoded 'hooks' literal in GSD_PREFIX_MANAGED_DIRS left
|
|
2464
|
+
// this scan blind to a renamed bundle: `fs.existsSync(configDir/hooks)` is
|
|
2465
|
+
// false for a pi install, so the ENTIRE gsd-hooks/ tree — including any
|
|
2466
|
+
// user-added file inside it — was invisible to detect-custom-files and
|
|
2467
|
+
// therefore never backed up before the next clean-install wipe (silent
|
|
2468
|
+
// data loss).
|
|
2469
|
+
//
|
|
2470
|
+
// Resolution order, mirroring bin/install.js's own resolveSharedHooksDirName:
|
|
2471
|
+
// 1. Read the per-install runtime marker written by the installer at
|
|
2472
|
+
// <configDir>/gsd-core/.gsd-runtime (#2297).
|
|
2473
|
+
// 2. Look up that runtime's `hostBehaviors.sharedHooksDirName` in the
|
|
2474
|
+
// SHIPPED capability registry (./lib/capability-registry.cjs — a data
|
|
2475
|
+
// module in the same installed tree as this file). Deliberately NOT
|
|
2476
|
+
// `require('bin/install.js')`: that file is never shipped into an
|
|
2477
|
+
// installed tree (the #3024/#2071 bug class), so only the shipped data
|
|
2478
|
+
// module is read here.
|
|
2479
|
+
//
|
|
2480
|
+
// Asymmetric fallback: when the runtime or its descriptor cannot be
|
|
2481
|
+
// determined (an install predating the marker, an unreadable/corrupt
|
|
2482
|
+
// registry, or an unrecognized runtime id) this does NOT guess a single
|
|
2483
|
+
// name — it returns every known candidate name instead. Over-scanning is
|
|
2484
|
+
// safe here: a candidate directory that does not exist is silently skipped
|
|
2485
|
+
// by the caller's `fs.existsSync` guard, and a file already tracked in the
|
|
2486
|
+
// manifest is never reported as custom. Under-scanning is the actual bug
|
|
2487
|
+
// being fixed: it would make a user's file vanish on the next wipe without
|
|
2488
|
+
// ever being backed up.
|
|
2489
|
+
function resolveSharedHooksDirCandidates(configDir) {
|
|
2490
|
+
const DEFAULT_NAME = 'hooks';
|
|
2491
|
+
// A resolved name is joined onto configDir and read back — reject
|
|
2492
|
+
// anything that isn't a plain, separator-free segment so a corrupt
|
|
2493
|
+
// registry value can never walk the scan outside the config root.
|
|
2494
|
+
const isSafeSegment = (name) =>
|
|
2495
|
+
typeof name === 'string' &&
|
|
2496
|
+
name.trim() !== '' &&
|
|
2497
|
+
name.trim() === name &&
|
|
2498
|
+
name !== '.' &&
|
|
2499
|
+
name !== '..' &&
|
|
2500
|
+
!name.includes('/') &&
|
|
2501
|
+
!name.includes('\\');
|
|
2502
|
+
|
|
2503
|
+
let registry = null;
|
|
2504
|
+
try {
|
|
2505
|
+
registry = require('./lib/capability-registry.cjs');
|
|
2506
|
+
} catch {
|
|
2507
|
+
registry = null;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
const knownNames = new Set([DEFAULT_NAME]);
|
|
2511
|
+
if (registry && registry.runtimes && typeof registry.runtimes === 'object') {
|
|
2512
|
+
for (const desc of Object.values(registry.runtimes)) {
|
|
2513
|
+
const name = desc && desc.runtime && desc.runtime.hostBehaviors &&
|
|
2514
|
+
desc.runtime.hostBehaviors.sharedHooksDirName;
|
|
2515
|
+
if (isSafeSegment(name)) knownNames.add(name);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
let runtimeId = null;
|
|
2520
|
+
try {
|
|
2521
|
+
const markerPath = path.join(configDir, 'gsd-core', '.gsd-runtime');
|
|
2522
|
+
const raw = fs.readFileSync(markerPath, 'utf8').trim();
|
|
2523
|
+
runtimeId = raw || null;
|
|
2524
|
+
} catch {
|
|
2525
|
+
runtimeId = null;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
if (runtimeId && registry && registry.runtimes && registry.runtimes[runtimeId]) {
|
|
2529
|
+
const desc = registry.runtimes[runtimeId];
|
|
2530
|
+
const name = desc && desc.runtime && desc.runtime.hostBehaviors &&
|
|
2531
|
+
desc.runtime.hostBehaviors.sharedHooksDirName;
|
|
2532
|
+
return [isSafeSegment(name) ? name : DEFAULT_NAME];
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// Runtime undeterminable: scan every known candidate (see asymmetric
|
|
2536
|
+
// fallback comment above).
|
|
2537
|
+
return Array.from(knownNames);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2036
2540
|
async function routeDetectCustomFiles({ args, cwd, raw, error }) {
|
|
2037
2541
|
const configDirIdx = args.indexOf('--config-dir');
|
|
2038
2542
|
const configDir = configDirIdx !== -1 ? args[configDirIdx + 1] : null;
|
|
@@ -2073,7 +2577,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2073
2577
|
];
|
|
2074
2578
|
const GSD_PREFIX_MANAGED_DIRS = [
|
|
2075
2579
|
'agents',
|
|
2076
|
-
|
|
2580
|
+
...resolveSharedHooksDirCandidates(resolvedConfigDir),
|
|
2077
2581
|
'skills',
|
|
2078
2582
|
];
|
|
2079
2583
|
|
|
@@ -2605,6 +3109,133 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2605
3109
|
}
|
|
2606
3110
|
}
|
|
2607
3111
|
|
|
3112
|
+
// `gsd_run query context-predicates` — selector surface for the CONTEXT.md
|
|
3113
|
+
// predicate fact-store (ADR-1671, #2928 Phase 1 row S9). Parses the
|
|
3114
|
+
// repo-root CONTEXT.md LIVE via the compiled context-predicates.cjs on
|
|
3115
|
+
// every call — it never reads the committed docs/CONTEXT-INDEX.json (that
|
|
3116
|
+
// artifact is a CI drift-guard byproduct, not a query source, so it can
|
|
3117
|
+
// never go stale relative to the live predicates it answers about).
|
|
3118
|
+
//
|
|
3119
|
+
// Selectors: --class <CLASS>, --prefix <dotted.prefix>, --contains <text>.
|
|
3120
|
+
// At least one is required. When more than one is given they are ANDed
|
|
3121
|
+
// together — the same documented precedence selectPredicates() itself
|
|
3122
|
+
// implements (see context-predicates.cjs doc comment: "Select predicates
|
|
3123
|
+
// by one or more optional criteria (ANDed together)"); no selector is
|
|
3124
|
+
// silently dropped or overridden by another.
|
|
3125
|
+
//
|
|
3126
|
+
// Flag parsing mirrors routePromptBudget's Map-based flagMap: the three
|
|
3127
|
+
// known flags are recognized in both the space-separated `--flag value`
|
|
3128
|
+
// form and the inline-assignment `--flag=value` form (the latter is the
|
|
3129
|
+
// escape hatch for a flag-shaped selector value, e.g. `--contains=--dry-run`
|
|
3130
|
+
// — #2928 review finding C; the space-separated form has no such escape by
|
|
3131
|
+
// design, since a following `--...` token always reads as a missing value).
|
|
3132
|
+
// `--class=` (empty value) and `--class==A` (double-equals typo shape)
|
|
3133
|
+
// are rejected the same way under either form. On a duplicate flag the
|
|
3134
|
+
// FIRST occurrence wins (`Map.set` only fires when the key is absent),
|
|
3135
|
+
// which is deterministic across repeated invocations with identical argv.
|
|
3136
|
+
//
|
|
3137
|
+
// Prototype-pollution safety: selector values are only ever compared via
|
|
3138
|
+
// `===`/`.startsWith()`/`.includes()` against ordinary string fields — this
|
|
3139
|
+
// route never uses a user-supplied string as an object property key
|
|
3140
|
+
// (`obj[userValue] = ...`), so `--class __proto__` / `constructor` /
|
|
3141
|
+
// `prototype` are just non-matching ordinary strings, not property-access
|
|
3142
|
+
// vectors. `flagMap` itself is a `Map`, immune to prototype pollution by
|
|
3143
|
+
// construction.
|
|
3144
|
+
function routeContextPredicates({ args, cwd, raw, error }) {
|
|
3145
|
+
const { parsePredicates, selectPredicates } = require('./lib/context-predicates.cjs');
|
|
3146
|
+
|
|
3147
|
+
const KNOWN_FLAGS = new Set(['--class', '--prefix', '--contains']);
|
|
3148
|
+
const flagMap = new Map();
|
|
3149
|
+
for (let i = 1; i < args.length; i++) {
|
|
3150
|
+
const current = args[i];
|
|
3151
|
+
if (typeof current !== 'string' || !current.startsWith('--')) continue;
|
|
3152
|
+
|
|
3153
|
+
// Inline-assignment escape hatch (`--flag=value`, mirrors the `--config-dir=`/
|
|
3154
|
+
// `--runtime=` convention in routeUpdateContext elsewhere in this file). This is
|
|
3155
|
+
// the ONLY way to pass a flag-shaped selector value (e.g. searching CONTEXT.md
|
|
3156
|
+
// for the literal substring "--dry-run"): the space-separated form below always
|
|
3157
|
+
// treats a following `--...` token as a missing value, by design, so it has no
|
|
3158
|
+
// escape hatch on its own (#2928 review finding C).
|
|
3159
|
+
const eqFlag = [...KNOWN_FLAGS].find((f) => current.startsWith(`${f}=`));
|
|
3160
|
+
if (eqFlag) {
|
|
3161
|
+
const value = current.slice(eqFlag.length + 1);
|
|
3162
|
+
// Reject an empty value (`--class=`) and the `--class==A` double-equals typo
|
|
3163
|
+
// shape (a value starting with `=`) the same way the pre-existing malformed-
|
|
3164
|
+
// assignment behavior did — never silently accept "=A" as a literal value.
|
|
3165
|
+
if (value === '' || value.startsWith('=')) {
|
|
3166
|
+
error(`context-predicates: ${eqFlag} requires a non-empty value`, ERROR_REASON.USAGE);
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
if (!flagMap.has(eqFlag)) flagMap.set(eqFlag, value);
|
|
3170
|
+
continue;
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
if (!KNOWN_FLAGS.has(current)) {
|
|
3174
|
+
error(`Unknown flag for context-predicates: ${current}`, ERROR_REASON.USAGE);
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
const next = args[i + 1];
|
|
3178
|
+
if (next === undefined || next.startsWith('--')) {
|
|
3179
|
+
if (!flagMap.has(current)) flagMap.set(current, null);
|
|
3180
|
+
continue;
|
|
3181
|
+
}
|
|
3182
|
+
if (!flagMap.has(current)) flagMap.set(current, next);
|
|
3183
|
+
i++;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
const hasClass = flagMap.has('--class');
|
|
3187
|
+
const hasPrefix = flagMap.has('--prefix');
|
|
3188
|
+
const hasContains = flagMap.has('--contains');
|
|
3189
|
+
|
|
3190
|
+
if (!hasClass && !hasPrefix && !hasContains) {
|
|
3191
|
+
error(
|
|
3192
|
+
'Usage: gsd-tools query context-predicates --class <CLASS> | --prefix <dotted.prefix> | --contains <text> ' +
|
|
3193
|
+
'(selectors are ANDed when combined)',
|
|
3194
|
+
ERROR_REASON.USAGE,
|
|
3195
|
+
);
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
const requireNonEmpty = (flagName, rawValue) => {
|
|
3200
|
+
if (rawValue === null || rawValue === undefined || rawValue.trim() === '') {
|
|
3201
|
+
error(`context-predicates: ${flagName} requires a non-empty value`, ERROR_REASON.USAGE);
|
|
3202
|
+
return null;
|
|
3203
|
+
}
|
|
3204
|
+
return rawValue;
|
|
3205
|
+
};
|
|
3206
|
+
|
|
3207
|
+
const opts = {};
|
|
3208
|
+
if (hasClass) {
|
|
3209
|
+
const v = requireNonEmpty('--class', flagMap.get('--class'));
|
|
3210
|
+
if (v === null) return;
|
|
3211
|
+
opts.klass = v;
|
|
3212
|
+
}
|
|
3213
|
+
if (hasPrefix) {
|
|
3214
|
+
const v = requireNonEmpty('--prefix', flagMap.get('--prefix'));
|
|
3215
|
+
if (v === null) return;
|
|
3216
|
+
opts.prefix = v;
|
|
3217
|
+
}
|
|
3218
|
+
if (hasContains) {
|
|
3219
|
+
const v = requireNonEmpty('--contains', flagMap.get('--contains'));
|
|
3220
|
+
if (v === null) return;
|
|
3221
|
+
opts.contains = v;
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
const contextMdPath = path.join(__dirname, '..', '..', 'CONTEXT.md');
|
|
3225
|
+
let markdown;
|
|
3226
|
+
try {
|
|
3227
|
+
markdown = fs.readFileSync(contextMdPath, 'utf8');
|
|
3228
|
+
} catch (err) {
|
|
3229
|
+
error(`context-predicates: cannot read ${contextMdPath}: ${err && err.message}`, ERROR_REASON.USAGE);
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
const { predicates } = parsePredicates(markdown);
|
|
3234
|
+
const matches = selectPredicates(predicates, opts);
|
|
3235
|
+
|
|
3236
|
+
output({ matched: matches.length, predicates: matches }, raw);
|
|
3237
|
+
}
|
|
3238
|
+
|
|
2608
3239
|
function routeUpdateContext({ args, cwd, raw, error }) {
|
|
2609
3240
|
// #498: resolve the installed GSD version, scope, runtime, and config dir
|
|
2610
3241
|
// for /gsd:update. Replaces ~280 lines of inline bash in update.md with a
|
|
@@ -2838,13 +3469,189 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2838
3469
|
return;
|
|
2839
3470
|
}
|
|
2840
3471
|
|
|
3472
|
+
if (subcommand === 'phase-status') {
|
|
3473
|
+
// #1956: deterministic STATE.md-vs-ROADMAP.md phase-status drift.
|
|
3474
|
+
const { planningDir } = require('./lib/planning-workspace.cjs');
|
|
3475
|
+
const { stateExtractField, stateCurrentPositionSlice } = require('./lib/state-document.cjs');
|
|
3476
|
+
const { findRoadmapProgressTable } = require('./lib/roadmap-parser.cjs');
|
|
3477
|
+
const { phaseKeyFromProse } = require('./lib/phase-id.cjs');
|
|
3478
|
+
// STATE.md's YAML frontmatter carries its own lowercase `status:`
|
|
3479
|
+
// scalar ahead of the body's `## Current Position` prose "Status:"
|
|
3480
|
+
// line; stateExtractField's non-scoped regex would otherwise match
|
|
3481
|
+
// that frontmatter line first (it comes first in the file) and
|
|
3482
|
+
// silently report the wrong value. Strip frontmatter so extraction
|
|
3483
|
+
// is scoped to the body.
|
|
3484
|
+
const { stripFrontmatter } = require('./lib/frontmatter.cjs');
|
|
3485
|
+
|
|
3486
|
+
const phaseIdx = args.indexOf('--phase');
|
|
3487
|
+
const phaseArg = (phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--'))
|
|
3488
|
+
? args[phaseIdx + 1]
|
|
3489
|
+
: undefined;
|
|
3490
|
+
|
|
3491
|
+
const dir = planningDir(cwd);
|
|
3492
|
+
const statePath = path.join(dir, 'STATE.md');
|
|
3493
|
+
const roadmapPath = path.join(dir, 'ROADMAP.md');
|
|
3494
|
+
|
|
3495
|
+
let stateContent = null;
|
|
3496
|
+
try {
|
|
3497
|
+
stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
3498
|
+
} catch {
|
|
3499
|
+
// missing_state below
|
|
3500
|
+
}
|
|
3501
|
+
if (stateContent === null) {
|
|
3502
|
+
const phase = phaseArg !== undefined ? phaseKeyFromProse(phaseArg) : null;
|
|
3503
|
+
output({
|
|
3504
|
+
verdict: 'uncheckable',
|
|
3505
|
+
reason: 'missing_state',
|
|
3506
|
+
phase,
|
|
3507
|
+
stateStatus: null,
|
|
3508
|
+
roadmapStatus: null,
|
|
3509
|
+
authority: 'STATE.md',
|
|
3510
|
+
}, raw);
|
|
3511
|
+
return;
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
let roadmapContent = null;
|
|
3515
|
+
try {
|
|
3516
|
+
roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
3517
|
+
} catch {
|
|
3518
|
+
// missing_roadmap below
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
const stateBody = stripFrontmatter(stateContent);
|
|
3522
|
+
// #1956 fix: scope extraction to `## Current Position` (or `###`
|
|
3523
|
+
// in the bootstrap template) so a historical `Phase:` / `Status:`
|
|
3524
|
+
// line in an archive section (e.g. `## Session Continuity
|
|
3525
|
+
// Archive`) can't shadow the real one — same #2956 scope state.cts
|
|
3526
|
+
// uses for current_phase, via the shared owner in
|
|
3527
|
+
// state-document.cjs.
|
|
3528
|
+
//
|
|
3529
|
+
// Deliberately NO whole-body fallback here. `state.cts`'s WRITE
|
|
3530
|
+
// path falls back to the whole body when no Current Position
|
|
3531
|
+
// heading is found (legacy behavior it must preserve for
|
|
3532
|
+
// backward-compatible writes) — but that fallback is wrong for a
|
|
3533
|
+
// READ that feeds a drift finding: a STATE.md with no Current
|
|
3534
|
+
// Position heading is exactly the shape that let a stray historical
|
|
3535
|
+
// `Status:` line elsewhere in the body shadow the real value and
|
|
3536
|
+
// fabricate a 'drifted' verdict. A guess is worse than an
|
|
3537
|
+
// abstention for a drift detector, so an absent Current Position
|
|
3538
|
+
// section reports 'uncheckable' instead of guessing from the whole
|
|
3539
|
+
// document.
|
|
3540
|
+
const currentPositionBody = stateCurrentPositionSlice(stateBody);
|
|
3541
|
+
if (currentPositionBody === null) {
|
|
3542
|
+
const phase = phaseArg !== undefined ? phaseKeyFromProse(phaseArg) : null;
|
|
3543
|
+
output({
|
|
3544
|
+
verdict: 'uncheckable',
|
|
3545
|
+
reason: 'no_current_position',
|
|
3546
|
+
phase,
|
|
3547
|
+
stateStatus: null,
|
|
3548
|
+
roadmapStatus: null,
|
|
3549
|
+
authority: 'STATE.md',
|
|
3550
|
+
}, raw);
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
// Resolve the target phase: --phase if given, else whatever
|
|
3555
|
+
// STATE.md's Current Position reports as current.
|
|
3556
|
+
const phase = phaseArg !== undefined
|
|
3557
|
+
? phaseKeyFromProse(phaseArg)
|
|
3558
|
+
: phaseKeyFromProse(stateExtractField(currentPositionBody, 'Phase'));
|
|
3559
|
+
|
|
3560
|
+
if (roadmapContent === null) {
|
|
3561
|
+
output({
|
|
3562
|
+
verdict: 'uncheckable',
|
|
3563
|
+
reason: 'missing_roadmap',
|
|
3564
|
+
phase,
|
|
3565
|
+
stateStatus: null,
|
|
3566
|
+
roadmapStatus: null,
|
|
3567
|
+
authority: 'STATE.md',
|
|
3568
|
+
}, raw);
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
const stateStatus = stateExtractField(currentPositionBody, 'Status');
|
|
3573
|
+
|
|
3574
|
+
// #1956/#2012: scoped to `## Progress` first (decoy-avoidance) —
|
|
3575
|
+
// see findRoadmapProgressTable's doc comment (roadmap-parser.cts).
|
|
3576
|
+
const table = findRoadmapProgressTable(roadmapContent);
|
|
3577
|
+
const matchedRow = table
|
|
3578
|
+
? table.rows.find((row) => phaseKeyFromProse(row.Phase) === phase && phase !== null)
|
|
3579
|
+
: undefined;
|
|
3580
|
+
|
|
3581
|
+
if (!matchedRow) {
|
|
3582
|
+
const result = comparePhaseStatus({ stateStatus, roadmapStatus: null });
|
|
3583
|
+
output({
|
|
3584
|
+
verdict: 'uncheckable',
|
|
3585
|
+
reason: 'phase_not_in_roadmap',
|
|
3586
|
+
phase,
|
|
3587
|
+
stateStatus,
|
|
3588
|
+
roadmapStatus: null,
|
|
3589
|
+
stateRank: result.stateRank,
|
|
3590
|
+
roadmapRank: result.roadmapRank,
|
|
3591
|
+
authority: 'STATE.md',
|
|
3592
|
+
}, raw);
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
const roadmapStatus = matchedRow.Status;
|
|
3597
|
+
const result = comparePhaseStatus({ stateStatus, roadmapStatus });
|
|
3598
|
+
output({
|
|
3599
|
+
verdict: result.verdict,
|
|
3600
|
+
phase,
|
|
3601
|
+
stateStatus,
|
|
3602
|
+
roadmapStatus,
|
|
3603
|
+
stateRank: result.stateRank,
|
|
3604
|
+
roadmapRank: result.roadmapRank,
|
|
3605
|
+
authority: 'STATE.md',
|
|
3606
|
+
}, raw);
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
|
|
2841
3610
|
error(
|
|
2842
|
-
`Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity`,
|
|
3611
|
+
`Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity, phase-status`,
|
|
2843
3612
|
ERROR_REASON.SDK_UNKNOWN_COMMAND,
|
|
2844
3613
|
);
|
|
2845
3614
|
}
|
|
2846
3615
|
|
|
2847
3616
|
|
|
3617
|
+
/**
|
|
3618
|
+
* #3275: resolve a DECLARED command name to the file a spawn can actually start.
|
|
3619
|
+
*
|
|
3620
|
+
* Lane descriptors (src/review-lane-descriptor.cts) declare BARE, platform-unaware
|
|
3621
|
+
* binary names ('codex', 'gemini', 'kimi', 'agy'), and `review-lane invoke`'s
|
|
3622
|
+
* `deps.spawn` + `deps.hasBinary` both need the on-disk form of that name. Before
|
|
3623
|
+
* this helper existed they disagreed: `hasBinary` scanned PATH WITH PATHEXT (so
|
|
3624
|
+
* probes reported lanes AVAILABLE on Windows) while `spawn` received the bare name
|
|
3625
|
+
* — `CreateProcess` performs no PATHEXT resolution, so every spawn-transport lane
|
|
3626
|
+
* ENOENT'd there, and the #3086 `.cmd`/`.bat` cmd.exe mediation gate never fired
|
|
3627
|
+
* because the declared name never carried an extension. One shared resolver is the
|
|
3628
|
+
* only shape that cannot drift back apart.
|
|
3629
|
+
*
|
|
3630
|
+
* win32: tries PATHEXT entries ONLY — never the bare name. npm global installs
|
|
3631
|
+
* drop an EXTENSIONLESS POSIX sh shim (`...\npm\codex`) next to `codex.CMD`; a
|
|
3632
|
+
* bare-name-first scan resolves to it, the mediation gate sees no `.cmd`, and the
|
|
3633
|
+
* ENOENT returns unchanged (field-reported on Windows 11 — see the #3275 issue
|
|
3634
|
+
* comment pinning exactly this pitfall).
|
|
3635
|
+
*
|
|
3636
|
+
* POSIX: answers EXISTENCE only (the old `hasBinary` contract, preserved
|
|
3637
|
+
* byte-for-byte) by scanning PATH for the bare name. `deps.spawn` does NOT consult
|
|
3638
|
+
* this on POSIX — the bare name goes to spawnSync unchanged and Node's own PATH
|
|
3639
|
+
* search does the work, so macOS/Linux behavior is untouched (#3275 acceptance).
|
|
3640
|
+
*
|
|
3641
|
+
* Path-like names (any '/' or '\') bypass the PATH scan: the name is already an
|
|
3642
|
+
* address, so it passes through when the file exists and is a file.
|
|
3643
|
+
*
|
|
3644
|
+
* #3411: the scan itself now lives in the declared platform seam
|
|
3645
|
+
* (`src/shell-command-projection.cts` → `resolveExecutableBinary`). This function is
|
|
3646
|
+
* the `bin/` entry point onto it and holds no copy of the logic — `CONTEXT.md`
|
|
3647
|
+
* declares that file "All OS-facing I/O; single platform seam", and a private
|
|
3648
|
+
* duplicate here is what made it untrue.
|
|
3649
|
+
*/
|
|
3650
|
+
function resolveSpawnBinary(name, platform = process.platform, env = process.env) {
|
|
3651
|
+
const { resolveExecutableBinary } = require('./lib/shell-command-projection.cjs');
|
|
3652
|
+
return resolveExecutableBinary(name, { platform, env });
|
|
3653
|
+
}
|
|
3654
|
+
|
|
2848
3655
|
const HOST_COMMAND_ROUTERS = {
|
|
2849
3656
|
// Each entry wraps its `route*Command` router so it receives the module-scope
|
|
2850
3657
|
// lib the old `case` arm passed, plus the per-dispatch context
|
|
@@ -2894,6 +3701,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
2894
3701
|
'find-phase': routeFindPhase,
|
|
2895
3702
|
'commit': routeCommit,
|
|
2896
3703
|
'check-commit': routeCheckCommit,
|
|
3704
|
+
'commit-docs-guard': routeCommitDocsGuard,
|
|
2897
3705
|
'commit-to-subrepo': routeCommitToSubrepo,
|
|
2898
3706
|
'pr-subrepo': routePrSubrepo,
|
|
2899
3707
|
'verify-summary': routeVerifySummary,
|
|
@@ -2912,7 +3720,10 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
2912
3720
|
'normalize-test-command': routeNormalizeTestCommand,
|
|
2913
3721
|
'dispatch-should-flatten': routeDispatchShouldFlatten,
|
|
2914
3722
|
'dispatch-isolation': routeDispatchIsolation,
|
|
3723
|
+
'inspect-dispatch-isolation': routeInspectDispatchIsolation,
|
|
3724
|
+
'record-dispatch-isolation': routeRecordDispatchIsolation,
|
|
2915
3725
|
'resolve-dispatch-type': routeResolveDispatchType,
|
|
3726
|
+
'resolve-agent': routeResolveAgent,
|
|
2916
3727
|
'agent-skills': routeAgentSkills,
|
|
2917
3728
|
'skill-manifest': routeSkillManifest,
|
|
2918
3729
|
'history-digest': routeHistoryDigest,
|
|
@@ -2940,6 +3751,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
2940
3751
|
'restore-custom-files': routeRestoreCustomFiles,
|
|
2941
3752
|
'from-gsd2': routeFromGsd2,
|
|
2942
3753
|
'prompt-budget': routePromptBudget,
|
|
3754
|
+
'context-predicates': routeContextPredicates,
|
|
2943
3755
|
'review-lane': routeReviewLane,
|
|
2944
3756
|
'update-context': routeUpdateContext,
|
|
2945
3757
|
'classify-confidence': routeClassifyConfidence,
|
|
@@ -2948,6 +3760,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
2948
3760
|
'user-story': routeUserStory,
|
|
2949
3761
|
'drift-guard': routeDriftGuard,
|
|
2950
3762
|
'windows': routeWindows,
|
|
3763
|
+
'skills-root': routeSkillsRoot,
|
|
2951
3764
|
};
|
|
2952
3765
|
|
|
2953
3766
|
// Returns true when consumed (suppress "Unknown command"), false to fall
|
|
@@ -3143,6 +3956,121 @@ function runWithTimeout(argv) {
|
|
|
3143
3956
|
|
|
3144
3957
|
// ─── CLI Router ───────────────────────────────────────────────────────────────
|
|
3145
3958
|
|
|
3959
|
+
// Top-level usage string — emitted by `gsd-tools` (no args) and by
|
|
3960
|
+
// `gsd-tools --help` / any `--help` request below.
|
|
3961
|
+
// CR feedback: the command list must enumerate every top-level command
|
|
3962
|
+
// supported by the dispatcher so `--help` is actually useful for
|
|
3963
|
+
// discovery; previously it was a partial subset that didn't include
|
|
3964
|
+
// phase / roadmap / milestone / progress / etc.
|
|
3965
|
+
//
|
|
3966
|
+
// Module-scoped (not function-local) so it can be exported and compared
|
|
3967
|
+
// against HOST_COMMAND_ROUTERS in a parity test (DEFECT.GENERATIVE-FIX) —
|
|
3968
|
+
// this string and HOST_COMMAND_ROUTERS/SKIP_ROOT_RESOLUTION are three
|
|
3969
|
+
// independently hand-maintained sites and nothing previously caught them
|
|
3970
|
+
// drifting apart when a query command was added to only one or two.
|
|
3971
|
+
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
|
|
3972
|
+
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-docs-guard, commit-to-subrepo, pr-subrepo, ' +
|
|
3973
|
+
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, ' +
|
|
3974
|
+
'context-predicates, current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
|
3975
|
+
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
|
3976
|
+
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
|
3977
|
+
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
|
3978
|
+
'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, scaffold, smart-entry, state, ' +
|
|
3979
|
+
'config-set-model-profile, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
|
|
3980
|
+
'resolve-execution, review-lane, skill-manifest, skills-root, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
|
|
3981
|
+
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
|
3982
|
+
'Global flags:\n' +
|
|
3983
|
+
' --raw Emit raw output without post-processing\n' +
|
|
3984
|
+
' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
|
|
3985
|
+
' --cwd <path> Override working directory for project-root resolution\n' +
|
|
3986
|
+
' --ws <name> Override active workstream (or set GSD_WORKSTREAM)\n' +
|
|
3987
|
+
' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n\n' +
|
|
3988
|
+
'For command-specific argument requirements, invoke the command without args ' +
|
|
3989
|
+
'(e.g. `gsd-tools phase add`) — the resulting error lists what is required.';
|
|
3990
|
+
|
|
3991
|
+
// Multi-repo guard: resolve project root for commands that read/write .planning/.
|
|
3992
|
+
// Skip for pure-utility commands that don't touch .planning/ to avoid unnecessary
|
|
3993
|
+
// filesystem traversal on every invocation.
|
|
3994
|
+
// 'loop' and 'capability' are intentionally NOT in SKIP_ROOT_RESOLUTION.
|
|
3995
|
+
// Both are registry/config queries that resolve activation via
|
|
3996
|
+
// .planning/config.json; they need the project root (cwd) for correct
|
|
3997
|
+
// `when` key resolution. If one is ever moved to SKIP_ROOT_RESOLUTION,
|
|
3998
|
+
// move the other at the same time (keep them consistent).
|
|
3999
|
+
//
|
|
4000
|
+
// Module-scoped for the same reason as TOP_LEVEL_USAGE above — kept
|
|
4001
|
+
// module-private and exposed to the dispatch-table/help-string/skip-list
|
|
4002
|
+
// parity test only through the read-only skipsRootResolution() predicate
|
|
4003
|
+
// below (never as the live Set itself; see that function's doc comment).
|
|
4004
|
+
const SKIP_ROOT_RESOLUTION = new Set([
|
|
4005
|
+
'generate-slug', 'current-timestamp', 'verify-path-exists',
|
|
4006
|
+
// #2844: verify-summary was previously skipped, leaving relative file-claim
|
|
4007
|
+
// paths resolved against the raw process.cwd() — invoking from a subdirectory
|
|
4008
|
+
// manufactured "missing files" on an otherwise-correct SUMMARY. It now goes
|
|
4009
|
+
// through findProjectRoot so claims resolve against the project root.
|
|
4010
|
+
'template', 'frontmatter', 'detect-custom-files',
|
|
4011
|
+
// #1854: restore-custom-files operates on a runtime config dir passed
|
|
4012
|
+
// explicitly via --config-dir; it never reads .planning/.
|
|
4013
|
+
'restore-custom-files',
|
|
4014
|
+
'worktree', 'prompt-budget',
|
|
4015
|
+
// context-predicates is a pure repo-root CONTEXT.md read (like
|
|
4016
|
+
// prompt-budget); it never touches .planning/, so it needs no project
|
|
4017
|
+
// root resolution and must work from any cwd (including one with no
|
|
4018
|
+
// .planning/ directory at all).
|
|
4019
|
+
'context-predicates',
|
|
4020
|
+
'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
|
|
4021
|
+
'user-story', // pure string validation — no .planning/ access needed
|
|
4022
|
+
// #1529: pure runtime→filename projection via getProjectInstructionFile; no
|
|
4023
|
+
// .planning/ access needed, and resolving project root would break workflow
|
|
4024
|
+
// invocations that run before .planning/ exists (new-project Step 1).
|
|
4025
|
+
'project-instruction-file',
|
|
4026
|
+
// #1579: eval.score is pure arithmetic (covered/total + infra weights); it
|
|
4027
|
+
// needs no .planning/ access, so skip the findProjectRoot traversal.
|
|
4028
|
+
'eval',
|
|
4029
|
+
]);
|
|
4030
|
+
|
|
4031
|
+
// Read-only accessor for SKIP_ROOT_RESOLUTION (DEFECT.MUTABLE-EXPORTED-SET,
|
|
4032
|
+
// #2928 review). The Set above stays module-private and mutable internally
|
|
4033
|
+
// (main() only ever calls .has() on it), but exporting the live Set directly
|
|
4034
|
+
// would let any importer call .add()/.delete() on it — Object.freeze() does
|
|
4035
|
+
// not lock Set.prototype.add/delete, so freezing the instance would not have
|
|
4036
|
+
// closed this — and silently change dispatch behavior for every caller in the
|
|
4037
|
+
// process. Export this predicate instead; it exposes membership without
|
|
4038
|
+
// exposing a mutation surface.
|
|
4039
|
+
function skipsRootResolution(command) {
|
|
4040
|
+
return SKIP_ROOT_RESOLUTION.has(command);
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
/**
|
|
4044
|
+
* Resolve the worktree root for a given cwd, warning to stderr when git
|
|
4045
|
+
* could not determine it (reason 'git_timed_out') rather than silently
|
|
4046
|
+
* trusting a best-effort fallback (#3050). Extracted from main() so it can
|
|
4047
|
+
* be driven directly in tests via injected deps.
|
|
4048
|
+
*
|
|
4049
|
+
* @param {string} cwd
|
|
4050
|
+
* @param {{ existsSync?: (p: string) => boolean, resolveWorktreeRoot?: (cwd: string) => { root: string, reason: string }, writeWarning?: (msg: string) => void }} [deps]
|
|
4051
|
+
* @returns {string} resolved cwd
|
|
4052
|
+
*/
|
|
4053
|
+
function resolveMainWorktreeCwd(cwd, deps = {}) {
|
|
4054
|
+
const existsSync = deps.existsSync || fs.existsSync;
|
|
4055
|
+
const resolveWorktreeRoot = deps.resolveWorktreeRoot || require('./lib/worktree-safety.cjs').resolveWorktreeRoot;
|
|
4056
|
+
const writeWarning = deps.writeWarning || ((msg) => process.stderr.write(msg));
|
|
4057
|
+
|
|
4058
|
+
if (existsSync(path.join(cwd, '.planning'))) {
|
|
4059
|
+
return cwd;
|
|
4060
|
+
}
|
|
4061
|
+
const { root: worktreeRoot, reason: worktreeRootReason } = resolveWorktreeRoot(cwd);
|
|
4062
|
+
if (worktreeRootReason === 'git_timed_out') {
|
|
4063
|
+
writeWarning(
|
|
4064
|
+
'WARNING: could not determine the git worktree root (git timed out). ' +
|
|
4065
|
+
'Planning artifacts (STATE.md, ROADMAP.md, etc.) may be written to the ' +
|
|
4066
|
+
`wrong tree — proceeding with "${worktreeRoot}" as a best-effort fallback. ` +
|
|
4067
|
+
'Retry the command; if this persists, check for a stalled filesystem mount ' +
|
|
4068
|
+
'or a stale git index lock (.git/index.lock) in this worktree.\n'
|
|
4069
|
+
);
|
|
4070
|
+
}
|
|
4071
|
+
return worktreeRoot;
|
|
4072
|
+
}
|
|
4073
|
+
|
|
3146
4074
|
async function main() {
|
|
3147
4075
|
let args = process.argv.slice(2);
|
|
3148
4076
|
|
|
@@ -3200,20 +4128,27 @@ async function main() {
|
|
|
3200
4128
|
// Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
|
|
3201
4129
|
// However, in monorepo worktrees where the subdirectory itself owns .planning/,
|
|
3202
4130
|
// skip worktree resolution — the CWD is already the correct project root.
|
|
3203
|
-
|
|
3204
|
-
if (!fs.existsSync(path.join(cwd, '.planning'))) {
|
|
3205
|
-
const worktreeRoot = resolveWorktreeRoot(cwd);
|
|
3206
|
-
if (worktreeRoot !== cwd) {
|
|
3207
|
-
cwd = worktreeRoot;
|
|
3208
|
-
}
|
|
3209
|
-
}
|
|
4131
|
+
cwd = resolveMainWorktreeCwd(cwd);
|
|
3210
4132
|
|
|
3211
4133
|
// Optional workstream override for parallel milestone work.
|
|
3212
4134
|
// Priority: --ws flag > GSD_WORKSTREAM env var > session/shared pointer > null.
|
|
3213
4135
|
let workstreamContext = null;
|
|
3214
4136
|
try {
|
|
4137
|
+
// #3579 root-cause fix: this bootstrap resolution only decides whether to
|
|
4138
|
+
// populate GSD_WORKSTREAM env for downstream routing — it is a check, not
|
|
4139
|
+
// the consuming read. Using the mutating getActiveWorkstream here
|
|
4140
|
+
// self-healed (cleared) a present-but-unresolvable pointer BEFORE the
|
|
4141
|
+
// dispatched command's own resolution/diagnostic ran, so a second read in
|
|
4142
|
+
// the same process (e.g. a subcommand's own getActiveWorkstream call, or
|
|
4143
|
+
// a fail-safe guard's diagnoseUnresolvedActiveWorkstream) observed
|
|
4144
|
+
// already-cleared state — silently falling through to a fallback marker
|
|
4145
|
+
// it should never have inherited (isolation violation), or losing the
|
|
4146
|
+
// evidence a diagnostic needed to explain why nothing resolved. peek
|
|
4147
|
+
// shares the identical resolution logic and only differs by never
|
|
4148
|
+
// calling adapter.clear(); self-heal still happens, exactly once, at
|
|
4149
|
+
// whichever call site actually consumes the workstream for real.
|
|
3215
4150
|
workstreamContext = resolveActiveWorkstream(cwd, args, process.env, {
|
|
3216
|
-
getStored:
|
|
4151
|
+
getStored: peekActiveWorkstream,
|
|
3217
4152
|
});
|
|
3218
4153
|
args = workstreamContext.args;
|
|
3219
4154
|
// Set env var so all modules (planningDir, planningPaths) auto-resolve workstream paths.
|
|
@@ -3276,30 +4211,6 @@ async function main() {
|
|
|
3276
4211
|
}
|
|
3277
4212
|
}
|
|
3278
4213
|
|
|
3279
|
-
// Top-level usage string — emitted by `gsd-tools` (no args) and by
|
|
3280
|
-
// `gsd-tools --help` / any `--help` request below.
|
|
3281
|
-
// CR feedback: the command list must enumerate every top-level command
|
|
3282
|
-
// supported by the dispatcher so `--help` is actually useful for
|
|
3283
|
-
// discovery; previously it was a partial subset that didn't include
|
|
3284
|
-
// phase / roadmap / milestone / progress / etc.
|
|
3285
|
-
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
|
|
3286
|
-
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, pr-subrepo, ' +
|
|
3287
|
-
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, ' +
|
|
3288
|
-
'current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
|
3289
|
-
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
|
3290
|
-
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
|
3291
|
-
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
|
3292
|
-
'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, scaffold, smart-entry, state, ' +
|
|
3293
|
-
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
|
3294
|
-
'Global flags:\n' +
|
|
3295
|
-
' --raw Emit raw output without post-processing\n' +
|
|
3296
|
-
' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
|
|
3297
|
-
' --cwd <path> Override working directory for project-root resolution\n' +
|
|
3298
|
-
' --ws <name> Override active workstream (or set GSD_WORKSTREAM)\n' +
|
|
3299
|
-
' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n\n' +
|
|
3300
|
-
'For command-specific argument requirements, invoke the command without args ' +
|
|
3301
|
-
'(e.g. `gsd-tools phase add`) — the resulting error lists what is required.';
|
|
3302
|
-
|
|
3303
4214
|
if (!command) {
|
|
3304
4215
|
error(TOP_LEVEL_USAGE);
|
|
3305
4216
|
}
|
|
@@ -3326,35 +4237,6 @@ async function main() {
|
|
|
3326
4237
|
}
|
|
3327
4238
|
}
|
|
3328
4239
|
|
|
3329
|
-
// Multi-repo guard: resolve project root for commands that read/write .planning/.
|
|
3330
|
-
// Skip for pure-utility commands that don't touch .planning/ to avoid unnecessary
|
|
3331
|
-
// filesystem traversal on every invocation.
|
|
3332
|
-
// 'loop' and 'capability' are intentionally NOT in SKIP_ROOT_RESOLUTION.
|
|
3333
|
-
// Both are registry/config queries that resolve activation via
|
|
3334
|
-
// .planning/config.json; they need the project root (cwd) for correct
|
|
3335
|
-
// `when` key resolution. If one is ever moved to SKIP_ROOT_RESOLUTION,
|
|
3336
|
-
// move the other at the same time (keep them consistent).
|
|
3337
|
-
const SKIP_ROOT_RESOLUTION = new Set([
|
|
3338
|
-
'generate-slug', 'current-timestamp', 'verify-path-exists',
|
|
3339
|
-
// #2844: verify-summary was previously skipped, leaving relative file-claim
|
|
3340
|
-
// paths resolved against the raw process.cwd() — invoking from a subdirectory
|
|
3341
|
-
// manufactured "missing files" on an otherwise-correct SUMMARY. It now goes
|
|
3342
|
-
// through findProjectRoot so claims resolve against the project root.
|
|
3343
|
-
'template', 'frontmatter', 'detect-custom-files',
|
|
3344
|
-
// #1854: restore-custom-files operates on a runtime config dir passed
|
|
3345
|
-
// explicitly via --config-dir; it never reads .planning/.
|
|
3346
|
-
'restore-custom-files',
|
|
3347
|
-
'worktree', 'prompt-budget',
|
|
3348
|
-
'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
|
|
3349
|
-
'user-story', // pure string validation — no .planning/ access needed
|
|
3350
|
-
// #1529: pure runtime→filename projection via getProjectInstructionFile; no
|
|
3351
|
-
// .planning/ access needed, and resolving project root would break workflow
|
|
3352
|
-
// invocations that run before .planning/ exists (new-project Step 1).
|
|
3353
|
-
'project-instruction-file',
|
|
3354
|
-
// #1579: eval.score is pure arithmetic (covered/total + infra weights); it
|
|
3355
|
-
// needs no .planning/ access, so skip the findProjectRoot traversal.
|
|
3356
|
-
'eval',
|
|
3357
|
-
]);
|
|
3358
4240
|
if (!SKIP_ROOT_RESOLUTION.has(command)) {
|
|
3359
4241
|
cwd = findProjectRoot(cwd);
|
|
3360
4242
|
}
|
|
@@ -3511,5 +4393,17 @@ if (require.main === module) {
|
|
|
3511
4393
|
// synthetic registry + requireModule injections.
|
|
3512
4394
|
// ADR-1244 Phase 5: export dispatchOverlayCapabilityCommand + defaultRequireFromInstallRoot for
|
|
3513
4395
|
// the third-party overlay dispatch + install-root confinement tests.
|
|
3514
|
-
module.exports = {
|
|
4396
|
+
module.exports = {
|
|
4397
|
+
dispatchCapabilityCommand,
|
|
4398
|
+
dispatchOverlayCapabilityCommand,
|
|
4399
|
+
defaultRequireFromInstallRoot,
|
|
4400
|
+
dispatchHostCommand,
|
|
4401
|
+
HOST_COMMAND_ROUTERS,
|
|
4402
|
+
TOP_LEVEL_USAGE,
|
|
4403
|
+
skipsRootResolution,
|
|
4404
|
+
resolveMainWorktreeCwd,
|
|
4405
|
+
// #3275: exported for tests — the shared PATH+PATHEXT resolver behind
|
|
4406
|
+
// review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
|
|
4407
|
+
resolveSpawnBinary,
|
|
4408
|
+
};
|
|
3515
4409
|
|