@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
|
@@ -15,6 +15,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
15
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.PATH_ACTION_REASON = void 0;
|
|
18
19
|
exports.toPosixPath = toPosixPath;
|
|
19
20
|
exports.toNativePath = toNativePath;
|
|
20
21
|
exports.posixNormalize = posixNormalize;
|
|
@@ -35,11 +36,15 @@ exports.projectCodexHookTomlCommand = projectCodexHookTomlCommand;
|
|
|
35
36
|
exports.escapePowerShellSingleQuoted = escapePowerShellSingleQuoted;
|
|
36
37
|
exports.escapePosixDoubleQuoted = escapePosixDoubleQuoted;
|
|
37
38
|
exports.escapeSingleQuotedShellLiteral = escapeSingleQuotedShellLiteral;
|
|
39
|
+
exports.projectPathExportLine = projectPathExportLine;
|
|
38
40
|
exports.renderShellActionLines = renderShellActionLines;
|
|
39
41
|
exports.projectPathActionProjection = projectPathActionProjection;
|
|
40
42
|
exports.projectPersistentPathExportActions = projectPersistentPathExportActions;
|
|
43
|
+
exports.isSpawnTimeout = isSpawnTimeout;
|
|
41
44
|
exports.execGit = execGit;
|
|
42
45
|
exports.execNpm = execNpm;
|
|
46
|
+
exports.resolveExecutableBinary = resolveExecutableBinary;
|
|
47
|
+
exports.projectSpawnInvocation = projectSpawnInvocation;
|
|
43
48
|
exports.execTool = execTool;
|
|
44
49
|
exports.resolveGsdToolsPath = resolveGsdToolsPath;
|
|
45
50
|
exports.dispatchGsdCommand = dispatchGsdCommand;
|
|
@@ -55,6 +60,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
55
60
|
// can intercept calls from this seam — destructured imports capture references
|
|
56
61
|
// at load time and become un-mockable.
|
|
57
62
|
const node_child_process_1 = __importDefault(require("node:child_process"));
|
|
63
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
58
64
|
/**
|
|
59
65
|
* Convert a filesystem path to POSIX form (forward slashes) by translating the
|
|
60
66
|
* platform-native separator. Single seam for native→POSIX conversion.
|
|
@@ -303,7 +309,7 @@ function isManagedHookCommand(commandText, opts = {}) {
|
|
|
303
309
|
return false;
|
|
304
310
|
}
|
|
305
311
|
for (const basename of managedBasenames) {
|
|
306
|
-
const escapedBasename =
|
|
312
|
+
const escapedBasename = (0, pattern_cjs_1.escapeRegex)(basename);
|
|
307
313
|
const pattern = new RegExp(`(^|[\\\\/\\s"'` + '`' + `])${escapedBasename}(?=$|[\\s"'` + '`' + `])`);
|
|
308
314
|
if (pattern.test(normalizedCommand))
|
|
309
315
|
return true;
|
|
@@ -351,8 +357,34 @@ function projectLegacySettingsHookCommand({ absoluteRunner, scriptPath, scriptTo
|
|
|
351
357
|
platform,
|
|
352
358
|
});
|
|
353
359
|
}
|
|
360
|
+
// Implements the TOML v1.0.0 basic-string escaping grammar (toml.md, "Basic
|
|
361
|
+
// strings" section, https://toml.io/en/v1.0.0#string): a basic string must
|
|
362
|
+
// escape the quotation mark, backslash, and control characters other than
|
|
363
|
+
// tab (U+0000-U+0008, U+000A-U+001F, U+007F). Compact escapes are used where
|
|
364
|
+
// TOML defines them (\b \t \n \f \r \" \\); every other character in the
|
|
365
|
+
// required ranges falls back to \uXXXX. See #3118 — an earlier version
|
|
366
|
+
// escaped only backslash and quote, so a raw newline/CR/NUL in a value
|
|
367
|
+
// produced an unparseable config.toml.
|
|
368
|
+
const TOML_COMPACT_ESCAPES = {
|
|
369
|
+
'\x08': '\\b',
|
|
370
|
+
'\x09': '\\t',
|
|
371
|
+
'\x0A': '\\n',
|
|
372
|
+
'\x0C': '\\f',
|
|
373
|
+
'\x0D': '\\r',
|
|
374
|
+
};
|
|
375
|
+
// U+0000-U+0008, U+000A-U+001F, U+007F — control characters other than tab
|
|
376
|
+
// (U+0009), which the grammar permits unescaped.
|
|
377
|
+
const TOML_MUST_ESCAPE_CONTROL_CHARS = /[\x00-\x08\x0A-\x1F\x7F]/g;
|
|
354
378
|
function escapeTomlDoubleQuotedString(value) {
|
|
355
|
-
return String(value)
|
|
379
|
+
return String(value)
|
|
380
|
+
.replace(/\\/g, '\\\\')
|
|
381
|
+
.replace(/"/g, '\\"')
|
|
382
|
+
.replace(TOML_MUST_ESCAPE_CONTROL_CHARS, (ch) => {
|
|
383
|
+
const compact = TOML_COMPACT_ESCAPES[ch];
|
|
384
|
+
if (compact)
|
|
385
|
+
return compact;
|
|
386
|
+
return `\\u${ch.codePointAt(0).toString(16).padStart(4, '0')}`;
|
|
387
|
+
});
|
|
356
388
|
}
|
|
357
389
|
function projectCodexHookTomlCommand({ absoluteRunner, scriptPath, platform = process.platform }) {
|
|
358
390
|
const command = projectManagedHookCommand({
|
|
@@ -372,6 +404,27 @@ function escapePosixDoubleQuoted(value) {
|
|
|
372
404
|
function escapeSingleQuotedShellLiteral(value) {
|
|
373
405
|
return String(value).replace(/'/g, "'\\''");
|
|
374
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* The `export PATH="<dir>:$PATH"` line every persistence lane appends, plus the escaped directory
|
|
409
|
+
* token it embeds. One builder because three lanes emit this line: a lane that re-escapes it
|
|
410
|
+
* locally is how #3118 shipped a `$(…)` into ~/.bashrc, where it ran on every new shell. The
|
|
411
|
+
* escaping is for the line's FINAL context — a double-quoted string in an rc file — not for
|
|
412
|
+
* whatever transport (an `echo`, a paste) it passes through on the way there.
|
|
413
|
+
*/
|
|
414
|
+
function projectPathExportLine(targetDir) {
|
|
415
|
+
const escapedDir = escapePosixDoubleQuoted(String(targetDir));
|
|
416
|
+
return { escapedDir, line: `export PATH="${escapedDir}:$PATH"` };
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Why a PATH suggestion produced no actions. An empty `shellActions` alone folds two different
|
|
420
|
+
* facts together — "no target directory was given" and "this target directory cannot be
|
|
421
|
+
* expressed as a shell command" — and a caller that cannot tell them apart prints a header with
|
|
422
|
+
* nothing under it (#3118).
|
|
423
|
+
*/
|
|
424
|
+
exports.PATH_ACTION_REASON = Object.freeze({
|
|
425
|
+
NO_TARGET_DIR: 'no_target_dir',
|
|
426
|
+
WIN32_RESERVED_QUOTE: 'win32_reserved_quote',
|
|
427
|
+
});
|
|
375
428
|
function renderShellActionLines(shellActions = []) {
|
|
376
429
|
return shellActions.map((action) => {
|
|
377
430
|
if (!action || !action.command)
|
|
@@ -381,12 +434,18 @@ function renderShellActionLines(shellActions = []) {
|
|
|
381
434
|
}
|
|
382
435
|
function projectPathActionProjection({ mode = 'repair', targetDir, platform = process.platform, }) {
|
|
383
436
|
if (!targetDir)
|
|
384
|
-
return { shellActions: [], actionLines: [] };
|
|
437
|
+
return { shellActions: [], actionLines: [], reason: exports.PATH_ACTION_REASON.NO_TARGET_DIR };
|
|
438
|
+
// #3118: `"` is reserved on Windows, so a path containing one cannot exist — and it would close
|
|
439
|
+
// cmd's quoted region in the `powershell -Command "…"` lane below, turning the rest into cmd
|
|
440
|
+
// input. There is no correct command to suggest for an impossible path: fail closed rather than
|
|
441
|
+
// emit one whose quoting can be broken.
|
|
442
|
+
if (platform === 'win32' && String(targetDir).includes('"'))
|
|
443
|
+
return { shellActions: [], actionLines: [], reason: exports.PATH_ACTION_REASON.WIN32_RESERVED_QUOTE };
|
|
385
444
|
const isWin32 = platform === 'win32';
|
|
386
445
|
let shellActions;
|
|
387
446
|
if (isWin32) {
|
|
388
447
|
const psTargetDir = escapePowerShellSingleQuoted(targetDir);
|
|
389
|
-
const
|
|
448
|
+
const bashExportLine = escapeSingleQuotedShellLiteral(projectPathExportLine(posixNormalize(String(targetDir))).line);
|
|
390
449
|
shellActions = [
|
|
391
450
|
{
|
|
392
451
|
label: 'PowerShell',
|
|
@@ -401,42 +460,51 @@ function projectPathActionProjection({ mode = 'repair', targetDir, platform = pr
|
|
|
401
460
|
{
|
|
402
461
|
label: 'Git Bash',
|
|
403
462
|
shell: 'bash',
|
|
404
|
-
command: `echo '
|
|
463
|
+
command: `echo '${bashExportLine}' >> ~/.bashrc`,
|
|
405
464
|
},
|
|
406
465
|
];
|
|
407
466
|
}
|
|
408
467
|
else if (mode === 'persist') {
|
|
409
|
-
const
|
|
468
|
+
const exportLine = escapeSingleQuotedShellLiteral(projectPathExportLine(targetDir).line);
|
|
469
|
+
const fishTargetDir = escapeSingleQuotedShellLiteral(String(targetDir));
|
|
410
470
|
shellActions = [
|
|
411
471
|
{
|
|
412
472
|
label: 'zsh',
|
|
413
473
|
shell: 'zsh',
|
|
414
|
-
command: `echo '
|
|
474
|
+
command: `echo '${exportLine}' >> ~/.zshrc`,
|
|
415
475
|
},
|
|
416
476
|
{
|
|
417
477
|
label: 'bash',
|
|
418
478
|
shell: 'bash',
|
|
419
|
-
command: `echo '
|
|
479
|
+
command: `echo '${exportLine}' >> ~/.bashrc`,
|
|
420
480
|
},
|
|
421
481
|
// #323: fish has no `export`/`$PATH`-list syntax. `fish_add_path` is the
|
|
422
482
|
// fish-native API (>= fish 3.2, 2021) that persists to the universal
|
|
423
483
|
// variable store and de-duplicates. The directory is single-quoted with
|
|
424
484
|
// the same POSIX literal escaping as the zsh/bash siblings — `'\''` is
|
|
425
485
|
// also a valid escaped single quote in fish between quote spans.
|
|
486
|
+
//
|
|
487
|
+
// #3118 review MINOR: a `targetDir` with a leading `-` (e.g. `-v`) is a
|
|
488
|
+
// legal directory name, but fish's argparse-based option scanning
|
|
489
|
+
// treats a leading-dash token as a flag REGARDLESS of quoting, so
|
|
490
|
+
// `fish_add_path '-v'` misparses it and prints "No paths to add, not
|
|
491
|
+
// setting anything." (exit 1) instead of adding the path. `--` is
|
|
492
|
+
// fish's standard end-of-options separator; verified empirically
|
|
493
|
+
// against a real fish 4.8.1 install that `fish_add_path -- '-v'`
|
|
494
|
+
// succeeds where the unseparated form fails.
|
|
426
495
|
{
|
|
427
496
|
label: 'fish',
|
|
428
497
|
shell: 'fish',
|
|
429
|
-
command: `fish_add_path '${
|
|
498
|
+
command: `fish_add_path -- '${fishTargetDir}'`,
|
|
430
499
|
},
|
|
431
500
|
];
|
|
432
501
|
}
|
|
433
502
|
else {
|
|
434
|
-
const posixTargetDir = escapePosixDoubleQuoted(targetDir);
|
|
435
503
|
shellActions = [
|
|
436
504
|
{
|
|
437
505
|
label: null,
|
|
438
506
|
shell: 'posix',
|
|
439
|
-
command:
|
|
507
|
+
command: projectPathExportLine(targetDir).line,
|
|
440
508
|
},
|
|
441
509
|
];
|
|
442
510
|
}
|
|
@@ -451,18 +519,46 @@ function projectPersistentPathExportActions({ targetDir, platform = process.plat
|
|
|
451
519
|
targetDir,
|
|
452
520
|
platform,
|
|
453
521
|
});
|
|
454
|
-
return
|
|
522
|
+
return projected.reason === undefined
|
|
523
|
+
? { shellActions: projected.shellActions }
|
|
524
|
+
: { shellActions: projected.shellActions, reason: projected.reason };
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Returns true when a spawn/exec result indicates the subprocess was killed
|
|
528
|
+
* by a timeout, i.e. it never completed and reported a real answer. This is
|
|
529
|
+
* the single shared definition of "did this subprocess time out" — worktree
|
|
530
|
+
* safety (src/worktree-safety.cts) and worktree base-ref detection
|
|
531
|
+
* (src/worktree-base-ref.cts) both call this instead of maintaining their
|
|
532
|
+
* own copies (#3050 — "Generative Fix Divergence").
|
|
533
|
+
*
|
|
534
|
+
* Only `error.code === 'ETIMEDOUT'` is checked. Node.js guarantees this
|
|
535
|
+
* cross-platform when `spawnSync`'s `timeout` option fires. The `signal ===
|
|
536
|
+
* 'SIGTERM'` check some earlier code paired with it is platform-fragile —
|
|
537
|
+
* Windows does not necessarily report SIGTERM the same way — and pairing it
|
|
538
|
+
* in as a REQUIRED conjunct risks a false NEGATIVE (a timeout that silently
|
|
539
|
+
* fails to trip the guard) on that platform. There is no false-positive risk
|
|
540
|
+
* from dropping it: an externally-delivered SIGTERM (not a timeout) leaves
|
|
541
|
+
* `error` null, so `error.code === 'ETIMEDOUT'` alone still won't match it.
|
|
542
|
+
*/
|
|
543
|
+
function isSpawnTimeout(result) {
|
|
544
|
+
return result.error?.code === 'ETIMEDOUT';
|
|
455
545
|
}
|
|
456
546
|
function _spawnResult(result, program) {
|
|
457
547
|
if (result.error && result.error.code === 'ENOENT') {
|
|
458
|
-
return { exitCode: 127, stdout: '', stderr: `${program}: not found`, signal: null, error: result.error };
|
|
548
|
+
return { exitCode: 127, stdout: '', stderr: `${program}: not found`, signal: null, error: result.error, timedOut: false };
|
|
459
549
|
}
|
|
550
|
+
const signal = result.signal ?? null;
|
|
551
|
+
const error = result.error ?? null;
|
|
460
552
|
return {
|
|
461
553
|
exitCode: result.status ?? 1,
|
|
462
554
|
stdout: (result.stdout ?? '').toString().trim(),
|
|
463
555
|
stderr: (result.stderr ?? '').toString().trim(),
|
|
464
|
-
signal
|
|
465
|
-
error
|
|
556
|
+
signal,
|
|
557
|
+
error,
|
|
558
|
+
// Reuse the single shared timeout predicate (isSpawnTimeout, below) rather
|
|
559
|
+
// than re-deriving it here — see that function's docstring for why only
|
|
560
|
+
// error.code === 'ETIMEDOUT' is checked (not signal === 'SIGTERM').
|
|
561
|
+
timedOut: isSpawnTimeout({ error }),
|
|
466
562
|
};
|
|
467
563
|
}
|
|
468
564
|
function execGit(args, opts = {}) {
|
|
@@ -496,15 +592,283 @@ function execNpm(args, opts = {}) {
|
|
|
496
592
|
});
|
|
497
593
|
return _spawnResult(result, 'npm');
|
|
498
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Default PATHEXT when Windows does not supply one. Matches the value
|
|
597
|
+
* `gsd-tools.cjs`'s `resolveSpawnBinary` shipped in #3275; kept identical so the
|
|
598
|
+
* delegation is a behavior-preserving move rather than a redefinition.
|
|
599
|
+
*/
|
|
600
|
+
const DEFAULT_PATHEXT = '.EXE;.CMD;.BAT;.COM';
|
|
601
|
+
/** Windows extensions that must be mediated through cmd.exe rather than spawned. */
|
|
602
|
+
const CMD_MEDIATED_EXT = /\.(cmd|bat)$/i;
|
|
603
|
+
function _isFile(candidate, requireExecutable = false, platform = process.platform) {
|
|
604
|
+
try {
|
|
605
|
+
if (!node_fs_1.default.statSync(candidate).isFile())
|
|
606
|
+
return false;
|
|
607
|
+
if (requireExecutable && platform !== 'win32')
|
|
608
|
+
node_fs_1.default.accessSync(candidate, node_fs_1.default.constants.X_OK);
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// Missing, unreadable (EACCES), a broken link, or (when requireExecutable
|
|
613
|
+
// is set) not executable — all mean "not this one".
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Read an environment variable by name, case-insensitively.
|
|
619
|
+
*
|
|
620
|
+
* Windows environment variable names are case-insensitive and conventionally
|
|
621
|
+
* cased `Path` / `ComSpec`, and `process.env` is a case-insensitive proxy that
|
|
622
|
+
* hides the difference. Spreading it (`{ ...process.env, ...opts.env }`, which
|
|
623
|
+
* `execTool` does whenever a caller supplies `opts.env`) produces a PLAIN object
|
|
624
|
+
* that keeps the OS's actual casing and loses the proxy — so an exact-case
|
|
625
|
+
* `env['PATH']` lookup returns undefined there and the PATH scan silently sees
|
|
626
|
+
* nothing. Caught by the Windows CI lane on #3617; the #3445 tests never hit it
|
|
627
|
+
* because they pass uppercase keys explicitly.
|
|
628
|
+
*
|
|
629
|
+
* Exact match wins when present, so a caller who sets the canonical name pays
|
|
630
|
+
* no scan.
|
|
631
|
+
*/
|
|
632
|
+
function _envGet(env, name) {
|
|
633
|
+
const exact = env[name];
|
|
634
|
+
if (exact !== undefined)
|
|
635
|
+
return exact;
|
|
636
|
+
const lower = name.toLowerCase();
|
|
637
|
+
for (const key of Object.keys(env)) {
|
|
638
|
+
if (key.toLowerCase() === lower)
|
|
639
|
+
return env[key];
|
|
640
|
+
}
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
/** cmd.exe's own quoting rule inside a `/c` string: a literal quote is doubled. */
|
|
644
|
+
function _cmdQuoteToken(token) {
|
|
645
|
+
return `"${token.replace(/"/g, '""')}"`;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Build a single verbatim command-line string for `cmd.exe /c` where every
|
|
649
|
+
* token is force-quoted, then wrap the whole thing in one more outer pair.
|
|
650
|
+
* cmd.exe strips exactly one outer quote pair when the string begins with a
|
|
651
|
+
* quote and contains at least two — so the outer wrap disappears and what's
|
|
652
|
+
* left is a sequence of individually-quoted tokens. Force-quoting is the
|
|
653
|
+
* point: an unquoted `a&b` is split by cmd's own metacharacter parsing, but a
|
|
654
|
+
* quoted `"a&b"` is one literal argument.
|
|
655
|
+
*/
|
|
656
|
+
function _buildVerbatimCmdLine(target, args) {
|
|
657
|
+
return `"${[target, ...args].map(_cmdQuoteToken).join(' ')}"`;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* #3411: resolve a DECLARED command name to the file a spawn can actually start.
|
|
661
|
+
* The single canonical answer to "where is this binary?" for the whole tree.
|
|
662
|
+
*
|
|
663
|
+
* This is the seam `CONTEXT.md` declares as "All OS-facing I/O; single platform
|
|
664
|
+
* seam". Four divergent implementations of this logic existed (#3411): `execNpm`'s
|
|
665
|
+
* `shell:true`, `execTool`'s absence of any handling, `gsd-tools.cjs`'s private
|
|
666
|
+
* scan, and `fallow-runner.cts`'s own candidate array. #3275 folded two of them
|
|
667
|
+
* together in `bin/`; this lifts that resolver into the seam so `bin/` delegates
|
|
668
|
+
* instead of owning a copy.
|
|
669
|
+
*
|
|
670
|
+
* **win32** tries PATHEXT entries ONLY — never the bare name. npm global installs
|
|
671
|
+
* drop an EXTENSIONLESS POSIX sh shim (`...\npm\codex`) next to `codex.CMD`; a
|
|
672
|
+
* bare-name-first scan resolves to it, the cmd.exe mediation gate sees no `.cmd`,
|
|
673
|
+
* and the ENOENT returns unchanged (field-reported on Windows 11 — see #3275).
|
|
674
|
+
* A name that ALREADY carries a PATHEXT-listed extension is tried as-is first, so
|
|
675
|
+
* `foo.exe` resolves to `foo.exe` rather than being probed as `foo.exe.EXE`; a
|
|
676
|
+
* suffix that is not in PATHEXT (`foo.txt`) is not an extension and only feeds the
|
|
677
|
+
* append loop.
|
|
678
|
+
*
|
|
679
|
+
* **POSIX** answers EXISTENCE by scanning PATH for the bare name. `execTool` does
|
|
680
|
+
* NOT consult this on POSIX — the bare name goes to spawnSync unchanged and Node's
|
|
681
|
+
* own PATH search does the work, so macOS/Linux behavior is untouched (#3275
|
|
682
|
+
* acceptance contract).
|
|
683
|
+
*
|
|
684
|
+
* Path-like names (any `/` or `\`) bypass the PATH scan: the name is already an
|
|
685
|
+
* address, so it passes through when it names an existing file.
|
|
686
|
+
*
|
|
687
|
+
* **`opts.prependPaths`** — directories searched BEFORE `env.PATH`, in array
|
|
688
|
+
* order (e.g. a project-local `node_modules/.bin`). Defaults to `[]`, so
|
|
689
|
+
* Phase 1's callers (`execTool`, and `gsd-tools.cjs`'s `resolveSpawnBinary` /
|
|
690
|
+
* `deps.spawn` / `hasBinary`), which set neither new option, are byte-identical
|
|
691
|
+
* to today. The existing per-directory candidate logic (win32 as-is-then-append-
|
|
692
|
+
* PATHEXT; POSIX bare name) applies to prepended directories exactly as it does
|
|
693
|
+
* to `PATH` segments — there is no special-casing.
|
|
694
|
+
*
|
|
695
|
+
* **`opts.requireExecutable`** — when `true` and the platform is not `win32`,
|
|
696
|
+
* a candidate must additionally pass `fs.accessSync(candidate, fs.constants.X_OK)`
|
|
697
|
+
* to count as a match. On `win32` this is a no-op (mode bits do not mean
|
|
698
|
+
* execute on Windows — the same carve-out `fallow-runner`'s prior private
|
|
699
|
+
* resolver already had). Defaults to `false`, so `accessSync` is never called
|
|
700
|
+
* unless a caller opts in. It is opt-in rather than the default because making
|
|
701
|
+
* `X_OK` unconditional would break #3445's suite: those tests stage candidates
|
|
702
|
+
* with plain `fs.writeFileSync` and never set an exec bit (the repo bans
|
|
703
|
+
* `chmod` in tests), so every one of them would resolve to `null` on POSIX.
|
|
704
|
+
*
|
|
705
|
+
* **`opts.pathOverride`** — "search THIS PATH, but read everything else —
|
|
706
|
+
* PATHEXT included — from the ambient environment." When set (`!== undefined`),
|
|
707
|
+
* the PATH search segments come from splitting `pathOverride` on `path.delimiter`
|
|
708
|
+
* instead of from `env.PATH`; `pathOverride: ''` means an explicit EMPTY search
|
|
709
|
+
* path (zero segments), never a fallback to `env.PATH` — use `!== undefined`,
|
|
710
|
+
* not truthiness, to tell "caller supplied a PATH string" apart from "caller
|
|
711
|
+
* supplied nothing". `opts.prependPaths` still comes first. PATHEXT resolution
|
|
712
|
+
* is UNAFFECTED by this option — it still reads from `env` (which defaults to
|
|
713
|
+
* `process.env`) exactly as it does when `pathOverride` is omitted. This exists
|
|
714
|
+
* so a caller that already has its own search path in hand (e.g.
|
|
715
|
+
* `resolveFallowBinary`'s `envPath`) does not have to hand-thread PATHEXT
|
|
716
|
+
* alongside it — a private PATHEXT read is the very shape
|
|
717
|
+
* `local/no-private-binary-resolution` forbids outside this seam.
|
|
718
|
+
*
|
|
719
|
+
* @returns the resolved path, or `null` when nothing matched. Callers fall back to
|
|
720
|
+
* the declared name on `null` so a genuine ENOENT still surfaces (#3086).
|
|
721
|
+
*/
|
|
722
|
+
function resolveExecutableBinary(name, opts = {}) {
|
|
723
|
+
if (!name)
|
|
724
|
+
return null;
|
|
725
|
+
const requireExecutable = opts.requireExecutable ?? false;
|
|
726
|
+
const platform = opts.platform ?? process.platform;
|
|
727
|
+
if (name.includes('/') || name.includes('\\')) {
|
|
728
|
+
return _isFile(name, requireExecutable, platform) ? name : null;
|
|
729
|
+
}
|
|
730
|
+
const env = opts.env ?? process.env;
|
|
731
|
+
const rawPath = opts.pathOverride !== undefined ? opts.pathOverride : (_envGet(env, 'PATH') || '');
|
|
732
|
+
const pathSegments = String(rawPath).split(node_path_1.default.delimiter).filter(Boolean);
|
|
733
|
+
const segments = [...(opts.prependPaths ?? []), ...pathSegments];
|
|
734
|
+
if (platform !== 'win32') {
|
|
735
|
+
for (const dir of segments) {
|
|
736
|
+
const candidate = node_path_1.default.join(dir, name);
|
|
737
|
+
if (_isFile(candidate, requireExecutable, platform))
|
|
738
|
+
return candidate;
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
const exts = String(_envGet(env, 'PATHEXT') || DEFAULT_PATHEXT).split(';').filter(Boolean);
|
|
743
|
+
// A name already ending in a PATHEXT-listed extension is an address, not a stem:
|
|
744
|
+
// probing `foo.exe` as `foo.exe.EXE` would miss the file sitting right there.
|
|
745
|
+
// Compared case-insensitively because PATHEXT casing is not guaranteed.
|
|
746
|
+
const lower = name.toLowerCase();
|
|
747
|
+
const carriesKnownExt = exts.some((ext) => lower.endsWith(ext.toLowerCase()));
|
|
748
|
+
for (const dir of segments) {
|
|
749
|
+
if (carriesKnownExt) {
|
|
750
|
+
const asIs = node_path_1.default.join(dir, name);
|
|
751
|
+
if (_isFile(asIs, requireExecutable, platform))
|
|
752
|
+
return asIs;
|
|
753
|
+
}
|
|
754
|
+
for (const ext of exts) {
|
|
755
|
+
const candidate = node_path_1.default.join(dir, name + ext);
|
|
756
|
+
if (_isFile(candidate, requireExecutable, platform))
|
|
757
|
+
return candidate;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* #3411: project a declared `(command, args)` into the pair `spawnSync` can
|
|
764
|
+
* actually execute on this platform.
|
|
765
|
+
*
|
|
766
|
+
* Resolution alone does not make Windows work: `CreateProcess` cannot execute a
|
|
767
|
+
* `.cmd`/`.bat` at all, so the cmd.exe mediation is inseparable from the lookup.
|
|
768
|
+
* Exporting only the resolver would leave every caller to re-derive that half —
|
|
769
|
+
* which is precisely how #3411's four copies accumulated.
|
|
770
|
+
*
|
|
771
|
+
* cmd.exe is invoked as an ordinary program with an EXPLICIT argv array, never via
|
|
772
|
+
* `shell: true`. `shell:true` on Windows is the mechanism behind CVE-2024-27980
|
|
773
|
+
* (argument injection through `.bat`/`.cmd`), and Node 26 deprecates it with an
|
|
774
|
+
* args array (DEP0190) because arguments are concatenated rather than escaped.
|
|
775
|
+
*
|
|
776
|
+
* Mediation fires when the target — the resolved path, or the declared name when
|
|
777
|
+
* resolution found nothing — carries a `.cmd`/`.bat` extension. A BARE name that
|
|
778
|
+
* resolved to nothing is passed through verbatim so the spawn fails with ENOENT;
|
|
779
|
+
* mediating it would turn `{exitCode:127, 'foo: not found'}` into cmd.exe's exit
|
|
780
|
+
* 9009 and silently change the not-found contract callers depend on.
|
|
781
|
+
*
|
|
782
|
+
* POSIX is a strict no-op: the declared command is returned unchanged and the
|
|
783
|
+
* environment is never consulted.
|
|
784
|
+
*
|
|
785
|
+
* The mediated command line is built VERBATIM rather than left to libuv: libuv's
|
|
786
|
+
* `quote_cmd_arg` only force-quotes an argument that contains a space, tab, or
|
|
787
|
+
* quote — it does not know about cmd.exe metacharacters (`&`, `|`, `>`, `<`,
|
|
788
|
+
* `^`, ...) at all, so an argument like `a&calc` reaches cmd.exe unquoted and
|
|
789
|
+
* gets re-parsed as two commands (the CVE-2024-27980 argument-injection class).
|
|
790
|
+
* Node's own CVE-2024-27980 escaping does not help here because it only fires
|
|
791
|
+
* when the spawned FILE itself is a `.bat`/`.cmd` — in this seam the spawned
|
|
792
|
+
* file is `cmd.exe`, not the target. Building the line ourselves and passing
|
|
793
|
+
* `windowsVerbatimArguments: true` (the shape Rust's std uses for the sibling
|
|
794
|
+
* CVE-2024-24576) means every token is force-quoted inside one outer pair, so
|
|
795
|
+
* a metacharacter inside a quoted token can never split the command line.
|
|
796
|
+
*
|
|
797
|
+
* KNOWN LIMIT: `%VAR%` still expands inside a cmd `/c` string, and there is no
|
|
798
|
+
* escape for `%` outside a batch file — an argument containing `%FOO%` is
|
|
799
|
+
* substituted with the environment value regardless of quoting. That is an
|
|
800
|
+
* information-disclosure limit, not arbitrary execution, and it's the same
|
|
801
|
+
* limit Rust's std documents for its own `CommandExt::raw_arg` escape hatch.
|
|
802
|
+
*
|
|
803
|
+
* CALLER CHOICE: this function's return value carries two independently
|
|
804
|
+
* adoptable pieces of information, and a caller may take either, both, or
|
|
805
|
+
* neither. `windowsVerbatimArguments: true` marks the cases where mediation
|
|
806
|
+
* was REQUIRED — the caller MUST adopt `command`+`args` together, since a
|
|
807
|
+
* `.cmd`/`.bat` genuinely cannot be spawned any other way. A merely-resolved
|
|
808
|
+
* `.exe` path (no mediation flag set) is only an OFFER: a caller may decline
|
|
809
|
+
* it and keep spawning the declared name instead, to hold its own observable
|
|
810
|
+
* contract stable. `execTool` (this file, below) is exactly such a caller —
|
|
811
|
+
* it adopts the mediated pair when `windowsVerbatimArguments` is set, but
|
|
812
|
+
* otherwise passes the declared `program`/`args` through untouched.
|
|
813
|
+
*/
|
|
814
|
+
function projectSpawnInvocation(command, args = [], opts = {}) {
|
|
815
|
+
const platform = opts.platform ?? process.platform;
|
|
816
|
+
if (platform !== 'win32')
|
|
817
|
+
return { command, args, resolved: null };
|
|
818
|
+
const env = opts.env ?? process.env;
|
|
819
|
+
const resolved = resolveExecutableBinary(command, { platform, env });
|
|
820
|
+
// Mediate against the resolved path when we have one, else against the declared
|
|
821
|
+
// name. An unresolved name is mediated ONLY when it already declares .cmd/.bat:
|
|
822
|
+
// PATH-only resolution misses a batch file sitting in the current directory,
|
|
823
|
+
// which `cmd.exe /c` still finds — the behavior gsd-tools.cjs shipped before
|
|
824
|
+
// this consolidation, preserved here rather than silently narrowed.
|
|
825
|
+
const target = resolved ?? command;
|
|
826
|
+
if (!CMD_MEDIATED_EXT.test(node_path_1.default.basename(target))) {
|
|
827
|
+
// A BARE name that resolved to nothing is passed through untouched so the
|
|
828
|
+
// spawn fails with ENOENT. Mediating it would turn {exitCode:127,
|
|
829
|
+
// '<name>: not found'} into cmd.exe's exit 9009 and silently change the
|
|
830
|
+
// not-found contract `_spawnResult` and its 53 dependent files rely on.
|
|
831
|
+
return resolved ? { command: resolved, args, resolved } : { command, args, resolved: null };
|
|
832
|
+
}
|
|
833
|
+
// A CR/LF cannot be represented inside a Windows command line at all — cmd.exe
|
|
834
|
+
// treats it as a line terminator, so mediating it would silently truncate the
|
|
835
|
+
// argument rather than pass it through. Fail visibly instead: fall back to the
|
|
836
|
+
// unmediated shape so the spawn either fails with ENOENT (bare unresolved name)
|
|
837
|
+
// or hands the raw string to CreateProcess, whichever the caller was already
|
|
838
|
+
// prepared to see for a non-.cmd/.bat target.
|
|
839
|
+
if (/[\r\n]/.test(target) || args.some((a) => /[\r\n]/.test(a))) {
|
|
840
|
+
return resolved ? { command: resolved, args, resolved } : { command, args, resolved: null };
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
command: String(_envGet(env, 'ComSpec') || 'cmd.exe'),
|
|
844
|
+
args: ['/d', '/s', '/c', _buildVerbatimCmdLine(target, args)],
|
|
845
|
+
resolved,
|
|
846
|
+
windowsVerbatimArguments: true,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
499
849
|
function execTool(program, args, opts = {}) {
|
|
500
|
-
|
|
850
|
+
// #3411: Windows cannot spawn a .cmd/.bat at all — CreateProcess refuses it —
|
|
851
|
+
// so those are mediated through cmd.exe. Everything else keeps the DECLARED
|
|
852
|
+
// program name: libuv's CreateProcess path already performs PATH + PATHEXT
|
|
853
|
+
// search, so resolving a .exe here would buy nothing and would change what
|
|
854
|
+
// this seam's 167 dependents observe being spawned. `tests/graphify.test.cjs`
|
|
855
|
+
// pins that contract by spying on spawnSync's first argument. POSIX never
|
|
856
|
+
// reaches the mediation branch at all.
|
|
857
|
+
const spawnEnv = opts.env ? { ...process.env, ...opts.env } : undefined;
|
|
858
|
+
const invocation = projectSpawnInvocation(program, args, { env: spawnEnv ?? process.env });
|
|
859
|
+
const mediated = invocation.windowsVerbatimArguments === true;
|
|
860
|
+
const result = node_child_process_1.default.spawnSync(mediated ? invocation.command : program, mediated ? invocation.args : args, {
|
|
501
861
|
cwd: opts.cwd,
|
|
502
|
-
env:
|
|
862
|
+
env: spawnEnv,
|
|
503
863
|
encoding: 'utf-8',
|
|
504
864
|
stdio: 'pipe',
|
|
505
865
|
timeout: opts.timeout ?? 30_000,
|
|
506
866
|
windowsHide: true,
|
|
867
|
+
...(mediated ? { windowsVerbatimArguments: true } : {}),
|
|
507
868
|
});
|
|
869
|
+
// Stamp the DECLARED name, never the resolved path: `_spawnResult` renders
|
|
870
|
+
// `${program}: not found`, and callers across 53 files match on the string they
|
|
871
|
+
// passed. Resolution must not leak an absolute path into that message.
|
|
508
872
|
return _spawnResult(result, program);
|
|
509
873
|
}
|
|
510
874
|
/**
|
|
@@ -549,9 +913,9 @@ function resolveGsdToolsPath() {
|
|
|
549
913
|
* NEVER throws. Degrades to `{ ok:false, ... }` on:
|
|
550
914
|
* - a missing/invalid "family" (validated locally, no subprocess spawned)
|
|
551
915
|
* - ENOENT / a missing gsd-tools.cjs (via the injectable `gsdToolsPath`)
|
|
552
|
-
* - a wall-clock timeout (`timedOut:true`,
|
|
553
|
-
*
|
|
554
|
-
*
|
|
916
|
+
* - a wall-clock timeout (`timedOut:true`, via the shared `isSpawnTimeout`
|
|
917
|
+
* predicate defined above in this file — also used by worktree-safety.cts
|
|
918
|
+
* and worktree-base-ref.cts)
|
|
555
919
|
* - any other unanticipated throw from the underlying spawn (defensive
|
|
556
920
|
* try/catch — execTool itself is spawnSync-based and does not throw).
|
|
557
921
|
*/
|
|
@@ -592,16 +956,9 @@ function dispatchGsdCommand({ family, subcommand, args = [], cwd, timeout = 30_0
|
|
|
592
956
|
timedOut: false,
|
|
593
957
|
};
|
|
594
958
|
}
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
// to `NodeJS.ErrnoException | null` directly (paired with optional chaining)
|
|
599
|
-
// trips a typescript-eslint no-unnecessary-type-assertion false positive for
|
|
600
|
-
// this exact narrowing shape (all of ErrnoException's extra fields over Error
|
|
601
|
-
// are optional).
|
|
602
|
-
const timedOut = result.signal === 'SIGTERM'
|
|
603
|
-
&& result.error !== null
|
|
604
|
-
&& result.error.code === 'ETIMEDOUT';
|
|
959
|
+
// Delegates to the single shared predicate defined above in this file
|
|
960
|
+
// (#3050 — "Generative Fix Divergence") instead of a local inline copy.
|
|
961
|
+
const timedOut = isSpawnTimeout(result);
|
|
605
962
|
return {
|
|
606
963
|
ok: result.exitCode === 0 && !timedOut,
|
|
607
964
|
stdout: result.stdout,
|
|
@@ -618,6 +975,7 @@ function probeTty(opts = {}) {
|
|
|
618
975
|
const ttyPath = node_child_process_1.default.execFileSync('tty', [], {
|
|
619
976
|
encoding: 'utf-8',
|
|
620
977
|
stdio: ['inherit', 'pipe', 'ignore'],
|
|
978
|
+
timeout: 5_000,
|
|
621
979
|
}).trim();
|
|
622
980
|
if (!ttyPath || ttyPath === 'not a tty')
|
|
623
981
|
return null;
|