@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
|
@@ -19,20 +19,20 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
|
|
|
19
19
|
// providing a deterministic failure path when git stalls (locked index, hung
|
|
20
20
|
// remote, stalled NFS mount, etc.). Callers can override via deps.timeout.
|
|
21
21
|
const DEFAULT_GIT_TIMEOUT_MS = 10000;
|
|
22
|
-
|
|
22
|
+
// #3021: accept the Workflow tool's worktree-wf_<runid>-<n> naming convention
|
|
23
|
+
// (claude-orchestration's isolation:"worktree" emission) alongside the
|
|
24
|
+
// existing agent-<id> / worktree-agent-<id> shapes.
|
|
25
|
+
const WORKTREE_AGENT_BRANCH_RE = /^((worktree-)?agent-|worktree-wf_)[A-Za-z0-9._/-]+$/;
|
|
23
26
|
const WORKTREE_AGENT_BRANCH_PATTERN = WORKTREE_AGENT_BRANCH_RE.source;
|
|
24
27
|
/**
|
|
25
|
-
* Execute a git command via the shell-projection seam,
|
|
26
|
-
* `timedOut`
|
|
28
|
+
* Execute a git command via the shell-projection seam, applying the module's
|
|
29
|
+
* default timeout. `timedOut` is now derived by the seam itself
|
|
30
|
+
* (shell-command-projection.cts's `_spawnResult`), so this is a thin
|
|
31
|
+
* passthrough. Tests inject mocks via deps.execGit using the same
|
|
27
32
|
* (args, opts) shape — see worktree-safety-policy.test.cjs.
|
|
28
|
-
*
|
|
29
|
-
* Return shape: { exitCode, stdout, stderr, timedOut, error, signal }
|
|
30
|
-
* - timedOut: true when spawnSync reports SIGTERM + ETIMEDOUT
|
|
31
33
|
*/
|
|
32
34
|
function execGitDefault(args, opts = {}) {
|
|
33
|
-
|
|
34
|
-
const timedOut = result.signal === 'SIGTERM' && result.error?.code === 'ETIMEDOUT';
|
|
35
|
-
return { ...result, timedOut };
|
|
35
|
+
return (0, shell_command_projection_cjs_1.execGit)(args, { ...opts, timeout: opts.timeout ?? DEFAULT_GIT_TIMEOUT_MS });
|
|
36
36
|
}
|
|
37
37
|
function parseWorktreePorcelain(porcelain) {
|
|
38
38
|
return parseWorktreeEntries(porcelain).filter((entry) => entry.branch !== null).map((entry) => ({
|
|
@@ -92,19 +92,39 @@ function readWorktreeList(repoRoot, deps = {}) {
|
|
|
92
92
|
entries: parseWorktreeEntries(listResult.stdout),
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Shortcut-free git-dir-vs-git-common-dir comparison: the actual primitive
|
|
97
|
+
* that distinguishes a linked worktree from the main worktree.
|
|
98
|
+
*
|
|
99
|
+
* Deliberately factored out of `resolveWorktreeContext` (#3045). That
|
|
100
|
+
* function's `has_local_planning` shortcut answers a DIFFERENT question ("is
|
|
101
|
+
* there already a usable project root right here") and must NOT be consulted
|
|
102
|
+
* for isolation detection: a git worktree created specifically to isolate an
|
|
103
|
+
* executor is a full checkout, so it normally has its OWN checked-out
|
|
104
|
+
* `.planning/` too. A caller that ran the shortcut first would read that
|
|
105
|
+
* correctly-isolated worktree as `current_directory`/`has_local_planning` —
|
|
106
|
+
* i.e. "not isolated" — a false positive that defeats the very isolation
|
|
107
|
+
* guard that needs this check (see `hooks/gsd-cursor-subagent-start.js`,
|
|
108
|
+
* #3045). `resolveWorktreeLinkage` always performs the real git-dir
|
|
109
|
+
* comparison, independent of whether `.planning` exists locally.
|
|
110
|
+
*/
|
|
111
|
+
function resolveWorktreeLinkage(cwd, deps = {}) {
|
|
96
112
|
const execGit = deps.execGit || execGitDefault;
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
const gitDir = execGit(['rev-parse', '--git-dir'], { cwd });
|
|
114
|
+
const commonDir = execGit(['rev-parse', '--git-common-dir'], { cwd });
|
|
115
|
+
// A TIMEOUT means the command never completed — it is not evidence of "not a
|
|
116
|
+
// git repository" (which completes fast, with a clean non-zero exit). Surface
|
|
117
|
+
// it under a distinct reason so callers can tell "genuinely not a repo" apart
|
|
118
|
+
// from "could not determine" (#3050). effectiveRoot still degrades to cwd
|
|
119
|
+
// (there is no safer default without a resolved git-dir), but the reason is
|
|
120
|
+
// no longer indistinguishable from the benign case.
|
|
121
|
+
if (gitDir.timedOut || commonDir.timedOut) {
|
|
100
122
|
return {
|
|
101
123
|
effectiveRoot: cwd,
|
|
102
124
|
mode: 'current_directory',
|
|
103
|
-
reason: '
|
|
125
|
+
reason: 'git_timed_out',
|
|
104
126
|
};
|
|
105
127
|
}
|
|
106
|
-
const gitDir = execGit(['rev-parse', '--git-dir'], { cwd });
|
|
107
|
-
const commonDir = execGit(['rev-parse', '--git-common-dir'], { cwd });
|
|
108
128
|
if (gitDir.exitCode !== 0 || commonDir.exitCode !== 0) {
|
|
109
129
|
return {
|
|
110
130
|
effectiveRoot: cwd,
|
|
@@ -127,6 +147,18 @@ function resolveWorktreeContext(cwd, deps = {}) {
|
|
|
127
147
|
reason: 'main_worktree',
|
|
128
148
|
};
|
|
129
149
|
}
|
|
150
|
+
function resolveWorktreeContext(cwd, deps = {}) {
|
|
151
|
+
const existsSync = deps.existsSync || node_fs_1.default.existsSync;
|
|
152
|
+
// Local .planning takes precedence over linked-worktree remapping.
|
|
153
|
+
if (existsSync(node_path_1.default.join(cwd, '.planning'))) {
|
|
154
|
+
return {
|
|
155
|
+
effectiveRoot: cwd,
|
|
156
|
+
mode: 'current_directory',
|
|
157
|
+
reason: 'has_local_planning',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return resolveWorktreeLinkage(cwd, deps);
|
|
161
|
+
}
|
|
130
162
|
function planWorktreePrune(repoRoot, options = {}, deps = {}) {
|
|
131
163
|
const parsePorcelain = deps.parseWorktreePorcelain || parseWorktreePorcelain;
|
|
132
164
|
const destructiveModeRequested = Boolean(options.allowDestructive);
|
|
@@ -140,17 +172,26 @@ function planWorktreePrune(repoRoot, options = {}, deps = {}) {
|
|
|
140
172
|
};
|
|
141
173
|
}
|
|
142
174
|
let worktrees = [];
|
|
175
|
+
let parseFailed = false;
|
|
143
176
|
try {
|
|
144
177
|
worktrees = parsePorcelain(listed.porcelain);
|
|
145
178
|
}
|
|
146
179
|
catch {
|
|
147
180
|
// Keep historical behavior: still run metadata prune when parsing fails.
|
|
181
|
+
// #3050/#3057 (B6): but the reason must NOT collide with the
|
|
182
|
+
// genuinely-empty-list case below — a parser that could not read the
|
|
183
|
+
// porcelain output is not the same fact as "there are no worktrees", and
|
|
184
|
+
// this plan drives a PRUNE, so conflating them means a prune decision made
|
|
185
|
+
// on unread data would be indistinguishable from one made on real data.
|
|
148
186
|
worktrees = [];
|
|
187
|
+
parseFailed = true;
|
|
149
188
|
}
|
|
150
189
|
return {
|
|
151
190
|
repoRoot,
|
|
152
191
|
action: 'metadata_prune_only',
|
|
153
|
-
reason:
|
|
192
|
+
reason: parseFailed
|
|
193
|
+
? 'parse_failed'
|
|
194
|
+
: (worktrees.length === 0 ? 'no_worktrees' : 'worktrees_present'),
|
|
154
195
|
destructiveModeRequested,
|
|
155
196
|
};
|
|
156
197
|
}
|
|
@@ -220,13 +261,25 @@ function inspectWorktreeHealth(repoRoot, options = {}, deps = {}) {
|
|
|
220
261
|
}
|
|
221
262
|
const findings = [];
|
|
222
263
|
for (const entry of inventory.entries) {
|
|
223
|
-
if (
|
|
264
|
+
if (entry.exists === 'absent') {
|
|
224
265
|
findings.push({
|
|
225
266
|
kind: 'orphan',
|
|
226
267
|
path: entry.path,
|
|
227
268
|
});
|
|
228
269
|
continue;
|
|
229
270
|
}
|
|
271
|
+
if (entry.exists === 'unverified') {
|
|
272
|
+
// #3050/#3057 (B5): existsSync confirmed the path is present but statSync
|
|
273
|
+
// threw, so age/staleness could not be determined. This is neither
|
|
274
|
+
// "orphan" (existsSync says it IS there) nor "healthy" (we never verified
|
|
275
|
+
// it) — surface it as its own finding so a caller can't silently treat an
|
|
276
|
+
// unverifiable worktree as confirmed present-and-not-stale.
|
|
277
|
+
findings.push({
|
|
278
|
+
kind: 'unverified',
|
|
279
|
+
path: entry.path,
|
|
280
|
+
});
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
230
283
|
if (entry.isStale) {
|
|
231
284
|
findings.push({
|
|
232
285
|
kind: 'stale',
|
|
@@ -256,7 +309,7 @@ function snapshotWorktreeInventory(repoRoot, options = {}, deps = {}) {
|
|
|
256
309
|
}
|
|
257
310
|
const entries = [];
|
|
258
311
|
for (const worktreePath of listed.paths) {
|
|
259
|
-
let exists =
|
|
312
|
+
let exists = 'absent';
|
|
260
313
|
let isStale = false;
|
|
261
314
|
let ageMinutes = null;
|
|
262
315
|
if (!existsSync(worktreePath)) {
|
|
@@ -268,9 +321,9 @@ function snapshotWorktreeInventory(repoRoot, options = {}, deps = {}) {
|
|
|
268
321
|
});
|
|
269
322
|
continue;
|
|
270
323
|
}
|
|
271
|
-
exists = true;
|
|
272
324
|
try {
|
|
273
325
|
const stat = statSync(worktreePath);
|
|
326
|
+
exists = 'present';
|
|
274
327
|
const ageMs = nowMs - stat.mtimeMs;
|
|
275
328
|
ageMinutes = Math.round(ageMs / 60000);
|
|
276
329
|
if (ageMs > staleAfterMs) {
|
|
@@ -278,7 +331,12 @@ function snapshotWorktreeInventory(repoRoot, options = {}, deps = {}) {
|
|
|
278
331
|
}
|
|
279
332
|
}
|
|
280
333
|
catch {
|
|
281
|
-
//
|
|
334
|
+
// #3050/#3057 (B5): a statSync throw means presence could not be
|
|
335
|
+
// verified — do NOT report exists:'present' (a guard that could not
|
|
336
|
+
// check must not claim the worktree is confirmed present). Distinguish
|
|
337
|
+
// from the genuinely-absent case above with a third state ('unverified')
|
|
338
|
+
// rather than silently falling through to the pre-existing 'present' default.
|
|
339
|
+
exists = 'unverified';
|
|
282
340
|
}
|
|
283
341
|
entries.push({
|
|
284
342
|
path: worktreePath,
|
|
@@ -308,13 +366,24 @@ function normalizeCleanupManifestEntry(entry) {
|
|
|
308
366
|
return null;
|
|
309
367
|
const rawAllowedBases = Array.isArray(e.allowed_bases) ? e.allowed_bases : [];
|
|
310
368
|
const allowedBases = Array.from(new Set([expectedBase, ...rawAllowedBases.filter((base) => typeof base === 'string' && base.length > 0)]));
|
|
311
|
-
|
|
369
|
+
// #2596: liberal in what we accept — non-array, or non-string / empty
|
|
370
|
+
// elements, are dropped rather than coerced. An EMPTY result omits the field
|
|
371
|
+
// entirely, so "declared nothing" is indistinguishable from "not recorded":
|
|
372
|
+
// that ambiguity is already resolved as *unknown* by the per-plan submodule
|
|
373
|
+
// gate's own `[ -z "$PLAN_FILES" ]` rule, and inventing a second rule here
|
|
374
|
+
// would make an unrecorded plan look 100% out of scope.
|
|
375
|
+
const filesModified = (Array.isArray(e.files_modified) ? e.files_modified : [])
|
|
376
|
+
.filter((f) => typeof f === 'string' && f.trim().length > 0);
|
|
377
|
+
const normalized = {
|
|
312
378
|
agent_id: typeof e.agent_id === 'string' ? e.agent_id : null,
|
|
313
379
|
worktree_path: worktreePath,
|
|
314
380
|
branch,
|
|
315
381
|
expected_base: expectedBase,
|
|
316
382
|
allowed_bases: allowedBases,
|
|
317
383
|
};
|
|
384
|
+
if (filesModified.length > 0)
|
|
385
|
+
normalized.files_modified = filesModified;
|
|
386
|
+
return normalized;
|
|
318
387
|
}
|
|
319
388
|
function normalizeCleanupManifest(manifest) {
|
|
320
389
|
let parsed = manifest;
|
|
@@ -371,6 +440,71 @@ function planWorktreeWaveCleanup(repoRoot, manifest) {
|
|
|
371
440
|
function gitResultOk(result) {
|
|
372
441
|
return !!(result && result.exitCode === 0 && !result.timedOut);
|
|
373
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* #2852: after a failed `git merge` + a `git merge --abort` attempt, determine
|
|
445
|
+
* whether `repoRoot` is STILL mid-merge — the only condition that genuinely
|
|
446
|
+
* invalidates the rest of a cleanup wave.
|
|
447
|
+
*
|
|
448
|
+
* `git merge --abort`'s own exit code is NOT a reliable signal here: git refuses
|
|
449
|
+
* many merges (e.g. "your local changes to the following files would be
|
|
450
|
+
* overwritten by merge") WITHOUT ever creating a `MERGE_HEAD`, in which case
|
|
451
|
+
* `repoRoot`'s tree was never touched and `git merge --abort` correctly fails
|
|
452
|
+
* with "fatal: There is no merge to abort (MERGE_HEAD missing)?" — a SAFE
|
|
453
|
+
* outcome, not a broken one. Trusting that exit code alone would misclassify an
|
|
454
|
+
* ordinary per-entry merge failure as a repo-level one and strand the rest of
|
|
455
|
+
* the wave (caught in review).
|
|
456
|
+
*
|
|
457
|
+
* Checked directly via `git rev-parse --verify -q MERGE_HEAD` against the git
|
|
458
|
+
* ref itself rather than the filesystem: exit 0 means a merge is genuinely still
|
|
459
|
+
* in progress (unrecoverable — halt); exit 1 (the ref simply doesn't exist) means
|
|
460
|
+
* repoRoot is clean, whether because no merge state was ever entered or because
|
|
461
|
+
* abort successfully cleared it (safe — isolate and continue). Anything else
|
|
462
|
+
* (a timeout, or an unexpected git error) is treated conservatively as "still
|
|
463
|
+
* mid-merge" — degrade to the safe/halting answer rather than throw or guess.
|
|
464
|
+
*/
|
|
465
|
+
function repoRootStillMidMerge(execGit, repoRoot) {
|
|
466
|
+
const check = execGit(['rev-parse', '--verify', '-q', 'MERGE_HEAD'], { cwd: repoRoot });
|
|
467
|
+
if (check.timedOut)
|
|
468
|
+
return true; // fail closed — cannot confirm safety
|
|
469
|
+
if (check.exitCode === 0)
|
|
470
|
+
return true; // MERGE_HEAD exists — genuinely still mid-merge
|
|
471
|
+
if (check.exitCode === 1)
|
|
472
|
+
return false; // ref not found — repoRoot is not mid-merge
|
|
473
|
+
return true; // any other exit code (e.g. a fatal git error) — fail closed
|
|
474
|
+
}
|
|
475
|
+
// #2596: the single definition of "this file is an executor-written SUMMARY
|
|
476
|
+
// artifact". Shared by `defaultFindSummaryFiles` (which walks for them to
|
|
477
|
+
// rescue) and the scope advisory below (which must never flag them) — a plan's
|
|
478
|
+
// declared `files_modified` never lists a SUMMARY, because the executor writes
|
|
479
|
+
// it by orchestration contract, so a second copy of this rule would make the
|
|
480
|
+
// advisory fire on essentially every wave.
|
|
481
|
+
const SUMMARY_ARTIFACT_DIR = '.planning';
|
|
482
|
+
const SUMMARY_ARTIFACT_SUFFIX = 'SUMMARY.md';
|
|
483
|
+
/**
|
|
484
|
+
* Normalize one path for scope comparison. Applied to BOTH sides so a declared
|
|
485
|
+
* path and a git-reported path meet in the same shape: backslashes become
|
|
486
|
+
* slashes unconditionally (a backslash path is not a Windows-only input),
|
|
487
|
+
* a leading `./` and any trailing `/` are stripped. This is the single
|
|
488
|
+
* normalizer shared by the SUMMARY-artifact predicate and the scope advisory,
|
|
489
|
+
* so the two can never disagree about what `./a\b/` means.
|
|
490
|
+
*/
|
|
491
|
+
function normalizeScopePath(raw) {
|
|
492
|
+
return String(raw || '')
|
|
493
|
+
.replace(/\\/g, '/')
|
|
494
|
+
.trim()
|
|
495
|
+
.replace(/^\.\//, '')
|
|
496
|
+
.replace(/\/+$/, '');
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* True when a worktree-relative path is a SUMMARY artifact. Input may use
|
|
500
|
+
* either separator; normalization to POSIX is unconditional (backslash paths
|
|
501
|
+
* reach Linux too).
|
|
502
|
+
*/
|
|
503
|
+
function isSummaryArtifactRelPath(relPath) {
|
|
504
|
+
const normalized = normalizeScopePath(relPath);
|
|
505
|
+
return normalized.startsWith(`${SUMMARY_ARTIFACT_DIR}/`)
|
|
506
|
+
&& normalized.endsWith(SUMMARY_ARTIFACT_SUFFIX);
|
|
507
|
+
}
|
|
374
508
|
/**
|
|
375
509
|
* Walk <worktreePath>/.planning/ recursively and collect absolute paths of
|
|
376
510
|
* all files whose names match *SUMMARY.md. Returns [] when the directory
|
|
@@ -380,7 +514,7 @@ function gitResultOk(result) {
|
|
|
380
514
|
* find "$WT/.planning" -name "*SUMMARY.md"
|
|
381
515
|
*/
|
|
382
516
|
function defaultFindSummaryFiles(worktreePath) {
|
|
383
|
-
const planningDir = node_path_1.default.join(worktreePath,
|
|
517
|
+
const planningDir = node_path_1.default.join(worktreePath, SUMMARY_ARTIFACT_DIR);
|
|
384
518
|
const results = [];
|
|
385
519
|
function walk(dir) {
|
|
386
520
|
let entries;
|
|
@@ -395,7 +529,7 @@ function defaultFindSummaryFiles(worktreePath) {
|
|
|
395
529
|
if (entry.isDirectory()) {
|
|
396
530
|
walk(full);
|
|
397
531
|
}
|
|
398
|
-
else if (entry.isFile() && entry.name.endsWith(
|
|
532
|
+
else if (entry.isFile() && entry.name.endsWith(SUMMARY_ARTIFACT_SUFFIX)) {
|
|
399
533
|
results.push(full);
|
|
400
534
|
}
|
|
401
535
|
}
|
|
@@ -498,6 +632,77 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
|
|
|
498
632
|
}
|
|
499
633
|
return { rescuedRelPaths, failures };
|
|
500
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* #2596: advisory codes emitted by the wave-cleanup gauntlet. Frozen and
|
|
637
|
+
* exported so tests assert on a code rather than on rendered prose (this repo
|
|
638
|
+
* forbids raw-text matching on test output).
|
|
639
|
+
*/
|
|
640
|
+
const WAVE_CLEANUP_WARNING = Object.freeze({
|
|
641
|
+
/** A committed path fell outside the plan's declared `files_modified`. */
|
|
642
|
+
SCOPE_OUT_OF_DECLARED: 'scope_out_of_declared',
|
|
643
|
+
/** The scope diff could not be computed, so conformance is unknown. */
|
|
644
|
+
SCOPE_CHECK_UNAVAILABLE: 'scope_check_unavailable',
|
|
645
|
+
});
|
|
646
|
+
/**
|
|
647
|
+
* The literal directory prefix a declared path covers, or `null` when the
|
|
648
|
+
* pattern begins with a glob metacharacter and therefore has no usable prefix.
|
|
649
|
+
*
|
|
650
|
+
* Deliberately literal-prefix only — NOT a glob engine. A hand-rolled
|
|
651
|
+
* `**`/`*`/`?` matcher inside a worktree-lifecycle module is an informal,
|
|
652
|
+
* undocumented pattern language living where no language belongs, and this
|
|
653
|
+
* repo forbids external deps in core. The submodule-intersection gate
|
|
654
|
+
* (`workflows/execute-phase/steps/per-plan-worktree-gate.md`) already ships
|
|
655
|
+
* exactly this glob-prefix rule; reusing it beats inventing a second one.
|
|
656
|
+
*
|
|
657
|
+
* `null` (no literal prefix, e.g. `*.md`) means "matches everything": for an
|
|
658
|
+
* ADVISORY, a false alarm costs more than a miss, so the ambiguous case
|
|
659
|
+
* suppresses rather than shouts.
|
|
660
|
+
*/
|
|
661
|
+
function declaredScopePrefix(declared) {
|
|
662
|
+
const globAt = declared.search(/[*?[]/);
|
|
663
|
+
if (globAt < 0)
|
|
664
|
+
return declared;
|
|
665
|
+
const literal = declared.slice(0, globAt).replace(/\/+$/, '');
|
|
666
|
+
return literal.length > 0 ? literal : null;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* #2596: compare a branch's actual committed paths against the plan's declared
|
|
670
|
+
* scope. Pure — no git, no IO. Returns one warning per out-of-scope path, in
|
|
671
|
+
* the order the paths were given. Returns [] when nothing usable was declared:
|
|
672
|
+
* absence of data is not evidence of over-reach.
|
|
673
|
+
*/
|
|
674
|
+
function planWaveScopeConformance(changedPaths, declaredFiles, branch) {
|
|
675
|
+
if (!Array.isArray(declaredFiles))
|
|
676
|
+
return [];
|
|
677
|
+
const prefixes = [];
|
|
678
|
+
for (const declared of declaredFiles) {
|
|
679
|
+
if (typeof declared !== 'string')
|
|
680
|
+
continue;
|
|
681
|
+
const normalized = normalizeScopePath(declared);
|
|
682
|
+
if (!normalized)
|
|
683
|
+
continue;
|
|
684
|
+
prefixes.push(declaredScopePrefix(normalized));
|
|
685
|
+
}
|
|
686
|
+
if (prefixes.length === 0)
|
|
687
|
+
return [];
|
|
688
|
+
const warnings = [];
|
|
689
|
+
const seen = new Set();
|
|
690
|
+
for (const raw of Array.isArray(changedPaths) ? changedPaths : []) {
|
|
691
|
+
if (typeof raw !== 'string')
|
|
692
|
+
continue;
|
|
693
|
+
const changed = normalizeScopePath(raw);
|
|
694
|
+
if (!changed || seen.has(changed))
|
|
695
|
+
continue;
|
|
696
|
+
seen.add(changed);
|
|
697
|
+
if (isSummaryArtifactRelPath(changed))
|
|
698
|
+
continue;
|
|
699
|
+
const covered = prefixes.some((prefix) => (prefix === null || changed === prefix || changed.startsWith(`${prefix}/`)));
|
|
700
|
+
if (covered)
|
|
701
|
+
continue;
|
|
702
|
+
warnings.push({ code: WAVE_CLEANUP_WARNING.SCOPE_OUT_OF_DECLARED, branch, path: changed });
|
|
703
|
+
}
|
|
704
|
+
return warnings;
|
|
705
|
+
}
|
|
501
706
|
function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
502
707
|
const execGit = deps.execGit || execGitDefault;
|
|
503
708
|
const entries = Array.isArray(plan?.entries) ? plan.entries : [];
|
|
@@ -508,11 +713,25 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
508
713
|
reason: plan ? (plan.reason || 'missing_entries') : 'missing_plan',
|
|
509
714
|
entries: [],
|
|
510
715
|
pending: entries,
|
|
716
|
+
warnings: [],
|
|
511
717
|
};
|
|
512
718
|
}
|
|
513
719
|
const results = [];
|
|
514
720
|
const pending = [];
|
|
721
|
+
const allWarnings = [];
|
|
515
722
|
let ok = true;
|
|
723
|
+
// #2852: every per-entry failure site marks the SAME shape — status='blocked',
|
|
724
|
+
// a reason code, the captured stderr, push to results, flip the overall `ok`
|
|
725
|
+
// flag — and then either `continue` (isolate, the default) or, for the one
|
|
726
|
+
// repo-level-failure carve-out, `break`. Factored out so the 8 call sites below
|
|
727
|
+
// don't repeat the assembly; each site still owns its own control-flow decision.
|
|
728
|
+
function blockEntry(result, reason, stderr) {
|
|
729
|
+
result.status = 'blocked';
|
|
730
|
+
result.reason = reason;
|
|
731
|
+
result.stderr = stderr;
|
|
732
|
+
results.push(result);
|
|
733
|
+
ok = false;
|
|
734
|
+
}
|
|
516
735
|
for (let i = 0; i < entries.length; i += 1) {
|
|
517
736
|
const entry = entries[i];
|
|
518
737
|
const result = {
|
|
@@ -520,71 +739,68 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
520
739
|
status: 'pending',
|
|
521
740
|
reason: null,
|
|
522
741
|
stderr: '',
|
|
742
|
+
warnings: [],
|
|
523
743
|
};
|
|
524
744
|
const branchCheck = execGit(['-C', entry.worktree_path, 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: plan.repoRoot });
|
|
525
745
|
if (!gitResultOk(branchCheck) || branchCheck.stdout.trim() !== entry.branch) {
|
|
526
|
-
result
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
pending.push(...entries.slice(i + 1));
|
|
531
|
-
ok = false;
|
|
532
|
-
break;
|
|
746
|
+
blockEntry(result, 'branch_mismatch', branchCheck?.stderr || '');
|
|
747
|
+
// #2852: isolate — this entry's problem does not touch repoRoot's git state,
|
|
748
|
+
// so every remaining entry is still independently evaluated.
|
|
749
|
+
continue;
|
|
533
750
|
}
|
|
534
751
|
const mergeBase = execGit(['merge-base', 'HEAD', entry.branch], { cwd: plan.repoRoot });
|
|
535
752
|
const allowedBases = Array.isArray(entry.allowed_bases) && entry.allowed_bases.length > 0
|
|
536
753
|
? entry.allowed_bases
|
|
537
754
|
: [entry.expected_base];
|
|
538
755
|
if (!gitResultOk(mergeBase) || !allowedBases.includes(mergeBase.stdout.trim())) {
|
|
539
|
-
result
|
|
540
|
-
|
|
541
|
-
result.stderr = mergeBase?.stderr || '';
|
|
542
|
-
results.push(result);
|
|
543
|
-
pending.push(...entries.slice(i + 1));
|
|
544
|
-
ok = false;
|
|
545
|
-
break;
|
|
756
|
+
blockEntry(result, 'base_mismatch', mergeBase?.stderr || '');
|
|
757
|
+
continue; // #2852: isolate
|
|
546
758
|
}
|
|
547
759
|
const deletions = execGit(['diff', '--diff-filter=D', '--name-only', `HEAD...${entry.branch}`], { cwd: plan.repoRoot });
|
|
548
760
|
if (!gitResultOk(deletions)) {
|
|
549
|
-
result
|
|
550
|
-
|
|
551
|
-
result.stderr = deletions?.stderr || '';
|
|
552
|
-
results.push(result);
|
|
553
|
-
pending.push(...entries.slice(i + 1));
|
|
554
|
-
ok = false;
|
|
555
|
-
break;
|
|
761
|
+
blockEntry(result, 'deletion_check_failed', deletions?.stderr || '');
|
|
762
|
+
continue; // #2852: isolate
|
|
556
763
|
}
|
|
557
764
|
if (deletions.stdout) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
765
|
+
// Unconditional: any deletion in this entry's branch blocks THIS entry. Whether
|
|
766
|
+
// that guard should have an opt-in for intentional deletions is a deferred
|
|
767
|
+
// product decision (issue #2852's own triage scoped it out — tracked in #3003);
|
|
768
|
+
// this fix only isolates the block to this one entry (#2852) instead of aborting
|
|
769
|
+
// the rest of the wave, same as every other block reason below.
|
|
770
|
+
blockEntry(result, 'branch_contains_deletions', deletions.stdout);
|
|
771
|
+
continue; // #2852: isolate
|
|
772
|
+
}
|
|
773
|
+
// #2596: advisory scope conformance — does the branch's ACTUAL committed
|
|
774
|
+
// diff stay inside the scope the plan declared? Gated on a declared scope
|
|
775
|
+
// being present: with nothing declared there is nothing to compare, so no
|
|
776
|
+
// git subprocess is spent at all (and every pre-#2596 fixture, none of
|
|
777
|
+
// which declares one, issues exactly the git calls it always did).
|
|
778
|
+
//
|
|
779
|
+
// ADVISORY ONLY. Unlike the deletions check above, a finding here does NOT
|
|
780
|
+
// call blockEntry and does NOT touch `ok` — the merge proceeds. Promotion
|
|
781
|
+
// to a hard gate is a separate, disclosed change.
|
|
782
|
+
if (Array.isArray(entry.files_modified) && entry.files_modified.length > 0) {
|
|
783
|
+
const scopeDiff = execGit(['diff', '--name-only', `HEAD...${entry.branch}`], { cwd: plan.repoRoot });
|
|
784
|
+
const scopeWarnings = !gitResultOk(scopeDiff)
|
|
785
|
+
// A broken advisory must never become a gate: record that conformance
|
|
786
|
+
// is unknown rather than blocking (or, worse, silently passing).
|
|
787
|
+
? [{ code: WAVE_CLEANUP_WARNING.SCOPE_CHECK_UNAVAILABLE, branch: entry.branch, path: null }]
|
|
788
|
+
: planWaveScopeConformance((scopeDiff.stdout || '').split('\n'), entry.files_modified, entry.branch);
|
|
789
|
+
result.warnings.push(...scopeWarnings);
|
|
790
|
+
allWarnings.push(...scopeWarnings);
|
|
565
791
|
}
|
|
566
792
|
// Safety net: rescue uncommitted SUMMARY.md artifacts before the dirty check.
|
|
567
793
|
// The executor leaves <quick_id>-SUMMARY.md uncommitted by contract — the
|
|
568
794
|
// orchestrator commits it. Mirrors quick.md shell fallback (#2296, #2070, #2838, #3804).
|
|
569
795
|
const { rescuedRelPaths, failures: rescueFailures } = rescueSummaryArtifacts(entry.worktree_path, plan.repoRoot, deps);
|
|
570
796
|
if (rescueFailures.length > 0) {
|
|
571
|
-
result.
|
|
572
|
-
|
|
573
|
-
result.stderr = rescueFailures.map((f) => `${f.relPath}: ${f.error}`).join('; ');
|
|
574
|
-
results.push(result);
|
|
575
|
-
pending.push(...entries.slice(i + 1));
|
|
576
|
-
ok = false;
|
|
577
|
-
break;
|
|
797
|
+
blockEntry(result, 'summary_rescue_failed', rescueFailures.map((f) => `${f.relPath}: ${f.error}`).join('; '));
|
|
798
|
+
continue; // #2852: isolate
|
|
578
799
|
}
|
|
579
800
|
const worktreeStatus = execGit(['-C', entry.worktree_path, 'status', '--porcelain', '--untracked-files=all'], { cwd: plan.repoRoot });
|
|
580
801
|
if (!gitResultOk(worktreeStatus)) {
|
|
581
|
-
result
|
|
582
|
-
|
|
583
|
-
result.stderr = worktreeStatus?.stderr || '';
|
|
584
|
-
results.push(result);
|
|
585
|
-
pending.push(...entries.slice(i + 1));
|
|
586
|
-
ok = false;
|
|
587
|
-
break;
|
|
802
|
+
blockEntry(result, 'worktree_dirty', worktreeStatus?.stderr || '');
|
|
803
|
+
continue; // #2852: isolate
|
|
588
804
|
}
|
|
589
805
|
// Filter rescued SUMMARY paths out of the porcelain output before deciding dirty.
|
|
590
806
|
// A line like "?? .planning/q1-SUMMARY.md" should not block when the SUMMARY
|
|
@@ -599,23 +815,31 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
599
815
|
return !rescuedRelPaths.has(filePath);
|
|
600
816
|
});
|
|
601
817
|
if (dirtyLines.length > 0) {
|
|
602
|
-
result
|
|
603
|
-
|
|
604
|
-
result.stderr = dirtyLines.join('\n');
|
|
605
|
-
results.push(result);
|
|
606
|
-
pending.push(...entries.slice(i + 1));
|
|
607
|
-
ok = false;
|
|
608
|
-
break;
|
|
818
|
+
blockEntry(result, 'worktree_dirty', dirtyLines.join('\n'));
|
|
819
|
+
continue; // #2852: isolate
|
|
609
820
|
}
|
|
610
821
|
const merge = execGit(['merge', entry.branch, '--no-ff', '--no-edit', '-m', `chore: merge executor worktree (${entry.branch})`], { cwd: plan.repoRoot });
|
|
611
822
|
if (!gitResultOk(merge)) {
|
|
612
|
-
result
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
823
|
+
blockEntry(result, 'merge_failed', merge?.stderr || merge?.stdout || '');
|
|
824
|
+
// #2852: a failed --no-ff merge MIGHT leave repoRoot itself mid-merge
|
|
825
|
+
// (MERGE_HEAD set, conflict markers in the tree) — unlike every other block
|
|
826
|
+
// reason above, that specific state is NOT scoped to this one entry: a second
|
|
827
|
+
// `git merge` cannot even start while one is in progress, so every remaining
|
|
828
|
+
// entry would be corrupted by it. But git also refuses many merges WITHOUT ever
|
|
829
|
+
// entering a merge state (e.g. "your local changes would be overwritten by
|
|
830
|
+
// merge") — in that case repoRoot's tree was never touched and this failure is
|
|
831
|
+
// scoped to this entry, same as everything else. Attempt the abort as a
|
|
832
|
+
// best-effort cleanup, then check repoRoot's ACTUAL state directly — not
|
|
833
|
+
// `git merge --abort`'s own exit code, which fails "There is no merge to abort"
|
|
834
|
+
// in the safe case too and would misclassify it as unrecoverable (caught in
|
|
835
|
+
// review). Only a repo genuinely still mid-merge afterward legitimately halts
|
|
836
|
+
// the rest of the wave (the brief's "infrastructure-level failure" carve-out).
|
|
837
|
+
execGit(['merge', '--abort'], { cwd: plan.repoRoot });
|
|
838
|
+
if (repoRootStillMidMerge(execGit, plan.repoRoot)) {
|
|
839
|
+
pending.push(...entries.slice(i + 1));
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
continue; // #2852: isolate — repoRoot is not (or no longer) mid-merge
|
|
619
843
|
}
|
|
620
844
|
let remove = execGit(['worktree', 'remove', entry.worktree_path, '--force'], { cwd: plan.repoRoot });
|
|
621
845
|
if (!gitResultOk(remove)) {
|
|
@@ -626,13 +850,10 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
626
850
|
remove = execGit(['worktree', 'remove', entry.worktree_path, '--force'], { cwd: plan.repoRoot });
|
|
627
851
|
}
|
|
628
852
|
if (!gitResultOk(remove)) {
|
|
629
|
-
result
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
pending.push(...entries.slice(i + 1));
|
|
634
|
-
ok = false;
|
|
635
|
-
break;
|
|
853
|
+
blockEntry(result, 'worktree_remove_failed', remove?.stderr || '');
|
|
854
|
+
// #2852: isolate — the merge already landed on repoRoot; only this entry's
|
|
855
|
+
// worktree/branch teardown is affected.
|
|
856
|
+
continue;
|
|
636
857
|
}
|
|
637
858
|
const branchDelete = execGit(['branch', '-D', entry.branch], { cwd: plan.repoRoot });
|
|
638
859
|
if (!gitResultOk(branchDelete)) {
|
|
@@ -653,6 +874,7 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
653
874
|
reason: ok ? 'ok' : 'cleanup_blocked',
|
|
654
875
|
entries: results,
|
|
655
876
|
pending,
|
|
877
|
+
warnings: allWarnings,
|
|
656
878
|
};
|
|
657
879
|
}
|
|
658
880
|
function cmdWorktreeCleanupWave(cwd, args = []) {
|
|
@@ -693,6 +915,15 @@ function cmdWorktreeCleanupWave(cwd, args = []) {
|
|
|
693
915
|
process.exitCode = 1;
|
|
694
916
|
}
|
|
695
917
|
}
|
|
918
|
+
/**
|
|
919
|
+
* #2596: split a `--files` value into declared paths. Whitespace-separated,
|
|
920
|
+
* matching the `PLAN_FILES` shape the per-plan worktree gate already builds
|
|
921
|
+
* with `jq -r '.files_modified // [] | join(" ")'`. Values are DATA compared
|
|
922
|
+
* against a diff — never opened, never passed to a shell.
|
|
923
|
+
*/
|
|
924
|
+
function parseDeclaredScopeFlag(raw) {
|
|
925
|
+
return String(raw || '').split(/\s+/).filter((token) => token.length > 0);
|
|
926
|
+
}
|
|
696
927
|
/**
|
|
697
928
|
* Pure planner for the per-agent wave-manifest append.
|
|
698
929
|
*
|
|
@@ -709,8 +940,9 @@ function cmdWorktreeCleanupWave(cwd, args = []) {
|
|
|
709
940
|
* rejected loudly — the reader dedups on that key, so a re-record would be
|
|
710
941
|
* silently dropped (the failure mode this verb exists to eliminate). The
|
|
711
942
|
* on-disk shape stays the existing 4-field entry (`agent_id`, `worktree_path`,
|
|
712
|
-
* `branch`, `expected_base`)
|
|
713
|
-
* `
|
|
943
|
+
* `branch`, `expected_base`) unless `--files` declares a scope, in which case
|
|
944
|
+
* an optional `files_modified` is appended (#2596); the reader still
|
|
945
|
+
* re-derives `allowed_bases`.
|
|
714
946
|
*/
|
|
715
947
|
function planWorktreeRecordAgent(manifestRaw, fields) {
|
|
716
948
|
// 1. Write-strict required-field check (loud, with which flag is missing).
|
|
@@ -740,18 +972,20 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
|
|
|
740
972
|
}
|
|
741
973
|
// 2. Shared validation: run the candidate through the reader's normalizer.
|
|
742
974
|
// If it returns null the reader would drop this entry on read — reject now.
|
|
975
|
+
const declaredScope = parseDeclaredScopeFlag(fields.files);
|
|
743
976
|
const candidate = {
|
|
744
977
|
agent_id: agentId,
|
|
745
978
|
worktree_path: worktreePath,
|
|
746
979
|
branch,
|
|
747
980
|
expected_base: base,
|
|
981
|
+
...(declaredScope.length > 0 ? { files_modified: declaredScope } : {}),
|
|
748
982
|
};
|
|
749
983
|
const entry = normalizeCleanupManifestEntry(candidate);
|
|
750
984
|
if (!entry) {
|
|
751
985
|
return {
|
|
752
986
|
ok: false,
|
|
753
987
|
reason: 'invalid_entry',
|
|
754
|
-
hint: `Entry failed cleanup-manifest validation: --path/--branch/--base must be non-empty and --branch must match ${WORKTREE_AGENT_BRANCH_PATTERN} (accepts
|
|
988
|
+
hint: `Entry failed cleanup-manifest validation: --path/--branch/--base must be non-empty and --branch must match ${WORKTREE_AGENT_BRANCH_PATTERN} (accepts agent-<id>, worktree-agent-<id>, and worktree-wf_<runid> namespaces; got branch="${branch}"). Fix the field and re-run.`,
|
|
755
989
|
entry: null,
|
|
756
990
|
manifest: null,
|
|
757
991
|
};
|
|
@@ -833,6 +1067,11 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
|
|
|
833
1067
|
branch: entry.branch,
|
|
834
1068
|
expected_base: entry.expected_base,
|
|
835
1069
|
};
|
|
1070
|
+
// #2596: only written when a scope was actually declared — conservative in
|
|
1071
|
+
// what we send, so a blank --files leaves the 4-field shape untouched.
|
|
1072
|
+
if (entry.files_modified && entry.files_modified.length > 0) {
|
|
1073
|
+
recorded.files_modified = entry.files_modified;
|
|
1074
|
+
}
|
|
836
1075
|
worktrees.push(recorded);
|
|
837
1076
|
return {
|
|
838
1077
|
ok: true,
|
|
@@ -844,7 +1083,7 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
|
|
|
844
1083
|
/**
|
|
845
1084
|
* CLI command: append a validated per-agent entry to a wave cleanup manifest.
|
|
846
1085
|
*
|
|
847
|
-
* Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha>
|
|
1086
|
+
* Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> [--files "<space-separated paths>"]
|
|
848
1087
|
*
|
|
849
1088
|
* Fails loudly (non-zero exit + recovery hint on stderr) when a field is
|
|
850
1089
|
* missing/garbled or the manifest is absent/malformed, rather than appending an
|
|
@@ -859,7 +1098,7 @@ function cmdWorktreeRecordAgent(cwd, args = [], deps = {}) {
|
|
|
859
1098
|
const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
|
|
860
1099
|
const manifestPath = flag('--manifest');
|
|
861
1100
|
if (!manifestPath) {
|
|
862
|
-
writeErr('Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha
|
|
1101
|
+
writeErr('Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> [--files "<space-separated paths>"]\n');
|
|
863
1102
|
process.exitCode = 2;
|
|
864
1103
|
return { ok: false, reason: 'usage', entry: null };
|
|
865
1104
|
}
|
|
@@ -881,6 +1120,7 @@ function cmdWorktreeRecordAgent(cwd, args = [], deps = {}) {
|
|
|
881
1120
|
worktreePath: flag('--path'),
|
|
882
1121
|
branch: flag('--branch'),
|
|
883
1122
|
base: flag('--base'),
|
|
1123
|
+
files: flag('--files'),
|
|
884
1124
|
});
|
|
885
1125
|
if (!plan.ok || plan.manifest === null) {
|
|
886
1126
|
writeErr(`[gsd] worktree.record-agent: ${plan.reason} — ${plan.hint || ''}\n`);
|
|
@@ -924,18 +1164,20 @@ function planWorktreeCreate(fields) {
|
|
|
924
1164
|
entry: null,
|
|
925
1165
|
};
|
|
926
1166
|
}
|
|
1167
|
+
const declaredScope = parseDeclaredScopeFlag(fields.files);
|
|
927
1168
|
const candidate = {
|
|
928
1169
|
agent_id: agentId,
|
|
929
1170
|
worktree_path: worktreePath,
|
|
930
1171
|
branch,
|
|
931
1172
|
expected_base: base,
|
|
1173
|
+
...(declaredScope.length > 0 ? { files_modified: declaredScope } : {}),
|
|
932
1174
|
};
|
|
933
1175
|
const entry = normalizeCleanupManifestEntry(candidate);
|
|
934
1176
|
if (!entry) {
|
|
935
1177
|
return {
|
|
936
1178
|
ok: false,
|
|
937
1179
|
reason: 'invalid_entry',
|
|
938
|
-
hint: `Entry failed cleanup-manifest validation: --path/--branch/--base must be non-empty and --branch must match ${WORKTREE_AGENT_BRANCH_PATTERN} (accepts
|
|
1180
|
+
hint: `Entry failed cleanup-manifest validation: --path/--branch/--base must be non-empty and --branch must match ${WORKTREE_AGENT_BRANCH_PATTERN} (accepts agent-<id>, worktree-agent-<id>, and worktree-wf_<runid> namespaces; got branch="${branch}"). Fix the field and re-run.`,
|
|
939
1181
|
entry: null,
|
|
940
1182
|
};
|
|
941
1183
|
}
|
|
@@ -1056,7 +1298,7 @@ function executeWorktreeCreatePlan(plan, repoRoot, deps = {}) {
|
|
|
1056
1298
|
* validated manifest entry so the worktree is immediately manageable by
|
|
1057
1299
|
* `worktree cleanup-wave` / `worktree reap-orphans`.
|
|
1058
1300
|
*
|
|
1059
|
-
* Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha>
|
|
1301
|
+
* Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir> [--files "<space-separated paths>"]
|
|
1060
1302
|
*
|
|
1061
1303
|
* #2584 FIX 1 — ORDERING CONTRACT: every manifest read/parse/shape-validate/
|
|
1062
1304
|
* plan step runs BEFORE the git side effect (step 5). The ONLY manifest
|
|
@@ -1076,7 +1318,7 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
|
|
|
1076
1318
|
const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
|
|
1077
1319
|
const manifestPath = flag('--manifest');
|
|
1078
1320
|
if (!manifestPath) {
|
|
1079
|
-
writeErr('Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha>
|
|
1321
|
+
writeErr('Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir> [--files "<space-separated paths>"]\n');
|
|
1080
1322
|
process.exitCode = 2;
|
|
1081
1323
|
return { ok: false, reason: 'usage' };
|
|
1082
1324
|
}
|
|
@@ -1141,6 +1383,7 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
|
|
|
1141
1383
|
worktreePath: flag('--path'),
|
|
1142
1384
|
branch: flag('--branch'),
|
|
1143
1385
|
base: flag('--base'),
|
|
1386
|
+
files: flag('--files'),
|
|
1144
1387
|
});
|
|
1145
1388
|
if (!plan.ok || !plan.entry) {
|
|
1146
1389
|
writeErr(`[gsd] worktree.create: ${plan.reason} — ${plan.hint || ''}\n`);
|
|
@@ -1148,25 +1391,38 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
|
|
|
1148
1391
|
process.exitCode = 1;
|
|
1149
1392
|
return { ok: false, reason: plan.reason, hint: plan.hint };
|
|
1150
1393
|
}
|
|
1151
|
-
// 3b.
|
|
1152
|
-
// deferred here from
|
|
1153
|
-
// planWorktreeCreate
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
// unconfined --path is a write primitive aimed
|
|
1394
|
+
// 3b. Mandatory root confinement (#2627 Phase 3 introduced it; #3050 made it
|
|
1395
|
+
// mandatory — the confinement Phase 2 deferred here from
|
|
1396
|
+
// planWorktreeCreate's path-traversal guard). planWorktreeCreate rejects a
|
|
1397
|
+
// literal ".." SEGMENT, but a plain absolute path outside the project
|
|
1398
|
+
// contains no ".." and passes. The orchestrator SPAWNS executor processes
|
|
1399
|
+
// into these paths, so an unconfined --path is a write primitive aimed
|
|
1400
|
+
// anywhere on the filesystem.
|
|
1157
1401
|
//
|
|
1158
1402
|
// The root is DECLARED by the caller (`--root`) rather than inferred: agent
|
|
1159
1403
|
// worktrees legitimately live outside the orchestrator's own root (a lane
|
|
1160
1404
|
// orchestrator creates siblings under the repo's .claude/worktrees/), so
|
|
1161
|
-
// there is no layout this module could derive without guessing.
|
|
1162
|
-
// `--root` the behavior is exactly as shipped in Phase 2 — the
|
|
1163
|
-
// orchestrator-worktree scheduler path always passes it.
|
|
1405
|
+
// there is no layout this module could derive without guessing.
|
|
1164
1406
|
//
|
|
1165
1407
|
// Lexical by design: the worktree does not exist yet, so there is nothing
|
|
1166
1408
|
// to realpath, and resolving only the root would not close a symlinked-leaf
|
|
1167
1409
|
// hole. Pairs with the leading-dash and ".."-segment guards above.
|
|
1410
|
+
//
|
|
1411
|
+
// #3050: confinement does not depend on the caller remembering to pass
|
|
1412
|
+
// `--root` — it used to be silently skippable, so a caller that forgot the
|
|
1413
|
+
// flag got an unconfined `--path` with no warning. Fail closed instead:
|
|
1414
|
+
// absent `--root`, this verb refuses to create anything. The one current
|
|
1415
|
+
// caller (execute-phase's orchestrator-worktree dispatch) always passes
|
|
1416
|
+
// `--root`, so this closes the gap without breaking it.
|
|
1168
1417
|
const rootFlag = flag('--root');
|
|
1169
|
-
if (rootFlag) {
|
|
1418
|
+
if (!rootFlag) {
|
|
1419
|
+
const hint = '--root is required (fail-closed root confinement, #3050). Pass --root <orchestrator-root-dir> so worktree.create can verify --path resolves inside it before creating anything.';
|
|
1420
|
+
writeErr(`[gsd] worktree.create: root_required — ${hint}\n`);
|
|
1421
|
+
write(`${JSON.stringify({ ok: false, reason: 'root_required', hint }, null, 2)}\n`);
|
|
1422
|
+
process.exitCode = 1;
|
|
1423
|
+
return { ok: false, reason: 'root_required', hint };
|
|
1424
|
+
}
|
|
1425
|
+
{
|
|
1170
1426
|
const absRoot = node_path_1.default.resolve(cwd, rootFlag);
|
|
1171
1427
|
const absWorktree = node_path_1.default.resolve(cwd, plan.entry.worktree_path);
|
|
1172
1428
|
const rel = node_path_1.default.relative(absRoot, absWorktree);
|
|
@@ -1194,6 +1450,11 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
|
|
|
1194
1450
|
branch: plan.entry.branch,
|
|
1195
1451
|
expected_base: plan.entry.expected_base,
|
|
1196
1452
|
};
|
|
1453
|
+
// #2596: only written when a scope was actually declared — conservative in
|
|
1454
|
+
// what we send, so a blank --files leaves the 4-field shape untouched.
|
|
1455
|
+
if (Array.isArray(plan.entry.files_modified) && plan.entry.files_modified.length > 0) {
|
|
1456
|
+
recorded.files_modified = plan.entry.files_modified;
|
|
1457
|
+
}
|
|
1197
1458
|
const dedupeKey = `${recorded.worktree_path}\0${recorded.branch}`;
|
|
1198
1459
|
const alreadyPresent = worktrees.some((existing) => {
|
|
1199
1460
|
const normalized = normalizeCleanupManifestEntry(existing);
|
|
@@ -1350,8 +1611,21 @@ function reapOrphanWorktrees(repoRoot, deps = {}) {
|
|
|
1350
1611
|
// worktreePath may not exist yet (already removed); use as-is.
|
|
1351
1612
|
}
|
|
1352
1613
|
// 4a. Stale-lock guard: skip if lock is too fresh (PID recycling / race).
|
|
1614
|
+
//
|
|
1615
|
+
// The two causes are reported SEPARATELY (#3057). A lock whose mtime could
|
|
1616
|
+
// not be read is not "fresh" in any sense: `lock_too_fresh` tells an
|
|
1617
|
+
// operator that waiting will resolve the skip, and waiting never resolves a
|
|
1618
|
+
// stat failure — the lock could be seconds or months old and the sweep has
|
|
1619
|
+
// no way to tell. Conflating them is the same defect this module already
|
|
1620
|
+
// fixed for `parse_failed` vs `no_worktrees` in planWorktreePrune: a
|
|
1621
|
+
// decision made on unread data must not be indistinguishable from one made
|
|
1622
|
+
// on real data.
|
|
1353
1623
|
const lockMtime = mtimeSafe(lockedFile);
|
|
1354
|
-
if (!lockMtime
|
|
1624
|
+
if (!lockMtime) {
|
|
1625
|
+
results.push({ path: worktreePath, status: 'skipped', reason: 'lock_age_unknown' });
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
if (nowMs - lockMtime.getTime() < reapMtimeGuardMs) {
|
|
1355
1629
|
results.push({ path: worktreePath, status: 'skipped', reason: 'lock_too_fresh' });
|
|
1356
1630
|
continue;
|
|
1357
1631
|
}
|
|
@@ -1362,9 +1636,26 @@ function reapOrphanWorktrees(repoRoot, deps = {}) {
|
|
|
1362
1636
|
continue;
|
|
1363
1637
|
}
|
|
1364
1638
|
const pid = parseInt(pidStr, 10);
|
|
1639
|
+
// Number.isFinite, not Number.isNaN: pidStr is captured by /^\d+/ above, so
|
|
1640
|
+
// pid can never be NaN. A 309-or-more-digit string parses to Infinity.
|
|
1641
|
+
//
|
|
1642
|
+
// NOT LOAD-BEARING FOR SAFETY — do not delete it as redundant. Fail-closed
|
|
1643
|
+
// liveness now lives in defaultIsPidAlive, which treats every non-ESRCH
|
|
1644
|
+
// outcome (including the TypeError process.kill throws for Infinity) as
|
|
1645
|
+
// ALIVE. This guard survives because it produces a more ACCURATE verdict
|
|
1646
|
+
// for garbage input: `lock_owner_unknown` says "the lock names a PID this
|
|
1647
|
+
// parse could not represent", whereas falling through would report
|
|
1648
|
+
// `pid_alive` — an assertion about an owner that was never probed.
|
|
1649
|
+
// Note this is an EARLIER, DIFFERENT gate than the process.kill range
|
|
1650
|
+
// limit: process.kill accepts up to 2147483647 and rejects 2147483648
|
|
1651
|
+
// (measured), far below the parse cliff this guard catches.
|
|
1652
|
+
if (!Number.isFinite(pid)) {
|
|
1653
|
+
results.push({ path: worktreePath, status: 'skipped', reason: 'lock_owner_unknown' });
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1365
1656
|
let pidIsAlive;
|
|
1366
1657
|
try {
|
|
1367
|
-
pidIsAlive =
|
|
1658
|
+
pidIsAlive = isPidAliveCheck(pid);
|
|
1368
1659
|
}
|
|
1369
1660
|
catch {
|
|
1370
1661
|
pidIsAlive = true; // Cannot determine liveness — treat as alive, do not reap.
|
|
@@ -1420,15 +1711,29 @@ function reapOrphanWorktrees(repoRoot, deps = {}) {
|
|
|
1420
1711
|
return results;
|
|
1421
1712
|
}
|
|
1422
1713
|
// ─── reapOrphanWorktrees deps helpers ─────────────────────────────────────────
|
|
1714
|
+
/**
|
|
1715
|
+
* Liveness probe for a lock-owner PID — FAILS CLOSED (#3057).
|
|
1716
|
+
*
|
|
1717
|
+
* `ESRCH` ("no such process") is the ONLY outcome that proves the owner is
|
|
1718
|
+
* gone. Every other failure means the probe could not determine liveness:
|
|
1719
|
+
* - `EPERM` — the process exists, we just may not signal it;
|
|
1720
|
+
* - `TypeError` / `ERR_INVALID_ARG_TYPE` — `process.kill` accepts a pid up
|
|
1721
|
+
* to 2147483647 and REJECTS 2147483648 and above (measured), so a finite
|
|
1722
|
+
* but out-of-range pid never reaches the OS at all;
|
|
1723
|
+
* - anything else — an outcome this helper does not recognise.
|
|
1724
|
+
*
|
|
1725
|
+
* The return value feeds a DESTRUCTIVE decision (`git worktree remove
|
|
1726
|
+
* --force`), so an unrecognised failure must never read as "dead". Hence the
|
|
1727
|
+
* inversion: only ESRCH returns false; everything else returns true (alive,
|
|
1728
|
+
* do not reap).
|
|
1729
|
+
*/
|
|
1423
1730
|
function defaultIsPidAlive(pid) {
|
|
1424
1731
|
try {
|
|
1425
1732
|
process.kill(pid, 0);
|
|
1426
1733
|
return true;
|
|
1427
1734
|
}
|
|
1428
1735
|
catch (err) {
|
|
1429
|
-
|
|
1430
|
-
return true;
|
|
1431
|
-
return false;
|
|
1736
|
+
return err?.code !== 'ESRCH';
|
|
1432
1737
|
}
|
|
1433
1738
|
}
|
|
1434
1739
|
function defaultReadDirSafe(dir) {
|
|
@@ -1455,36 +1760,45 @@ function defaultMtimeSafe(file) {
|
|
|
1455
1760
|
return null;
|
|
1456
1761
|
}
|
|
1457
1762
|
}
|
|
1458
|
-
function cmdWorktreeReapOrphans(cwd) {
|
|
1763
|
+
function cmdWorktreeReapOrphans(cwd, deps = {}) {
|
|
1764
|
+
const write = deps.write || ((s) => process.stdout.write(s));
|
|
1765
|
+
const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
|
|
1459
1766
|
let result;
|
|
1460
1767
|
try {
|
|
1461
|
-
result = reapOrphanWorktrees(cwd);
|
|
1768
|
+
result = reapOrphanWorktrees(cwd, deps);
|
|
1462
1769
|
}
|
|
1463
1770
|
catch (err) {
|
|
1464
1771
|
// Surface failure as a one-line warning; keep exit-zero so workflows don't break.
|
|
1465
|
-
|
|
1772
|
+
writeErr(`[gsd] worktree.reap-orphans failed: ${err && err.message ? err.message : String(err)}\n`);
|
|
1466
1773
|
result = [];
|
|
1467
1774
|
}
|
|
1468
1775
|
const skippedCount = result.filter((r) => r.status === 'skipped').length;
|
|
1469
1776
|
if (skippedCount > 0) {
|
|
1470
1777
|
// Surface skipped entries so operators are aware of unresolved orphans.
|
|
1471
|
-
|
|
1778
|
+
writeErr(`[gsd] worktree.reap-orphans: ${skippedCount} orphan(s) skipped (run with DEBUG=1 for details)\n`);
|
|
1472
1779
|
}
|
|
1473
|
-
|
|
1780
|
+
write(`${JSON.stringify({ ok: true, reaped: result.filter((r) => r.status === 'reaped').length, entries: result }, null, 2)}\n`);
|
|
1474
1781
|
}
|
|
1475
1782
|
// Unused exports kept for API compatibility
|
|
1476
1783
|
void parseWorktreeListPaths;
|
|
1477
1784
|
// ─── Moved from core.cjs (ADR-857 T0 #1268 rehome-core-squatters) ─────────────
|
|
1478
1785
|
/**
|
|
1479
|
-
* Resolve the main worktree root when running inside a git worktree
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1786
|
+
* Resolve the main worktree root when running inside a git worktree, along
|
|
1787
|
+
* with the `reason` that produced it (#3050). Callers MUST inspect `reason`
|
|
1788
|
+
* before trusting `root` unconditionally — a `reason` of `git_timed_out`
|
|
1789
|
+
* means the git subprocess used to distinguish "linked worktree" from
|
|
1790
|
+
* "not a repo" never completed, so `root` is a best-effort fallback (cwd),
|
|
1791
|
+
* not a confirmed worktree root. Degrading to cwd rather than throwing is
|
|
1792
|
+
* intentional (return degraded result on timeout; do not throw) — but the
|
|
1793
|
+
* reason must still reach the caller so it can surface the risk instead of
|
|
1794
|
+
* silently trusting the wrong root.
|
|
1482
1795
|
*/
|
|
1483
|
-
function resolveWorktreeRoot(cwd) {
|
|
1796
|
+
function resolveWorktreeRoot(cwd, deps = {}) {
|
|
1484
1797
|
const context = resolveWorktreeContext(cwd, {
|
|
1485
|
-
existsSync: node_fs_1.default.existsSync,
|
|
1798
|
+
existsSync: deps.existsSync || node_fs_1.default.existsSync,
|
|
1799
|
+
execGit: deps.execGit,
|
|
1486
1800
|
});
|
|
1487
|
-
return context.effectiveRoot;
|
|
1801
|
+
return { root: context.effectiveRoot, reason: context.reason };
|
|
1488
1802
|
}
|
|
1489
1803
|
/**
|
|
1490
1804
|
* Clear stale worktree metadata references via `git worktree prune`.
|
|
@@ -1495,12 +1809,19 @@ function resolveWorktreeRoot(cwd) {
|
|
|
1495
1809
|
* the repository; used as `cwd` for git commands.
|
|
1496
1810
|
* @returns list of worktree paths that were removed (always empty)
|
|
1497
1811
|
*/
|
|
1498
|
-
function pruneOrphanedWorktrees(repoRoot) {
|
|
1812
|
+
function pruneOrphanedWorktrees(repoRoot, deps = {}) {
|
|
1813
|
+
const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
|
|
1499
1814
|
try {
|
|
1500
|
-
|
|
1501
|
-
|
|
1815
|
+
// `...deps` comes LAST deliberately: `parseWorktreePorcelain` is a declared
|
|
1816
|
+
// member of WorktreeDeps and planWorktreePrune already reads
|
|
1817
|
+
// `deps.parseWorktreePorcelain` before falling back to the module function,
|
|
1818
|
+
// so a caller-supplied parser is an intended override, not an accident.
|
|
1819
|
+
// The hard-coded key is only a restatement of that same default. Do not
|
|
1820
|
+
// reorder the two — `tests/worktree-safety-reap.test.cjs` pins the override.
|
|
1821
|
+
const plan = planWorktreePrune(repoRoot, { allowDestructive: false }, { parseWorktreePorcelain, ...deps });
|
|
1822
|
+
const pruneResult = executeWorktreePrunePlan(plan, deps);
|
|
1502
1823
|
if (pruneResult && pruneResult.timedOut) {
|
|
1503
|
-
|
|
1824
|
+
writeErr('[gsd-tools] WARNING: worktree health check degraded' +
|
|
1504
1825
|
' — git worktree prune timed out after 10s.' +
|
|
1505
1826
|
' Orphaned worktree metadata may remain until the next successful run.\n');
|
|
1506
1827
|
}
|
|
@@ -1510,6 +1831,7 @@ function pruneOrphanedWorktrees(repoRoot) {
|
|
|
1510
1831
|
}
|
|
1511
1832
|
module.exports = {
|
|
1512
1833
|
resolveWorktreeContext,
|
|
1834
|
+
resolveWorktreeLinkage,
|
|
1513
1835
|
parseWorktreePorcelain,
|
|
1514
1836
|
planWorktreePrune,
|
|
1515
1837
|
executeWorktreePrunePlan,
|
|
@@ -1519,6 +1841,9 @@ module.exports = {
|
|
|
1519
1841
|
normalizeCleanupManifest,
|
|
1520
1842
|
planWorktreeWaveCleanup,
|
|
1521
1843
|
executeWorktreeWaveCleanupPlan,
|
|
1844
|
+
WAVE_CLEANUP_WARNING,
|
|
1845
|
+
planWaveScopeConformance,
|
|
1846
|
+
isSummaryArtifactRelPath,
|
|
1522
1847
|
cmdWorktreeCleanupWave,
|
|
1523
1848
|
planWorktreeRecordAgent,
|
|
1524
1849
|
cmdWorktreeRecordAgent,
|