@opengsd/gsd-core 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +85 -5
- package/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +5 -3
- package/agents/gsd-code-reviewer.md +8 -6
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +3 -3
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +17 -12
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +2 -2
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +15 -9
- package/agents/gsd-planner.md +15 -11
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +21 -30
- package/agents/gsd-ui-auditor.md +166 -37
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +37 -14
- package/bin/install.js +764 -287
- package/commands/gsd/add-tests.md +6 -1
- package/commands/gsd/ai-integration-phase.md +6 -1
- package/commands/gsd/audit-fix.md +5 -0
- package/commands/gsd/audit-milestone.md +6 -1
- package/commands/gsd/autonomous.md +7 -2
- package/commands/gsd/capture.md +9 -5
- package/commands/gsd/code-review.md +7 -2
- package/commands/gsd/complete-milestone.md +4 -0
- package/commands/gsd/config.md +7 -3
- package/commands/gsd/debug.md +11 -7
- package/commands/gsd/discuss-phase.md +7 -3
- package/commands/gsd/docs-update.md +12 -7
- package/commands/gsd/eval-review.md +6 -1
- package/commands/gsd/execute-phase.md +12 -7
- package/commands/gsd/extract-learnings.md +5 -0
- package/commands/gsd/fast.md +4 -0
- package/commands/gsd/forensics.md +5 -1
- package/commands/gsd/graphify.md +10 -6
- package/commands/gsd/health.md +5 -0
- package/commands/gsd/help.md +7 -2
- package/commands/gsd/import.md +7 -3
- package/commands/gsd/inbox.md +5 -0
- package/commands/gsd/ingest-docs.md +5 -1
- package/commands/gsd/manager.md +6 -1
- package/commands/gsd/map-codebase.md +7 -3
- package/commands/gsd/mempalace-capture.md +12 -4
- package/commands/gsd/mempalace-recall.md +5 -1
- package/commands/gsd/milestone-summary.md +5 -1
- package/commands/gsd/mvp-phase.md +8 -3
- package/commands/gsd/new-milestone.md +6 -1
- package/commands/gsd/new-project.md +5 -0
- package/commands/gsd/next.md +6 -1
- package/commands/gsd/ns-context.md +4 -0
- package/commands/gsd/ns-ideate.md +4 -0
- package/commands/gsd/ns-manage.md +4 -0
- package/commands/gsd/ns-project.md +4 -0
- package/commands/gsd/ns-review.md +4 -0
- package/commands/gsd/ns-workflow.md +4 -0
- package/commands/gsd/onboard.md +6 -1
- package/commands/gsd/pause-work.md +5 -1
- package/commands/gsd/phase.md +8 -4
- package/commands/gsd/plan-phase.md +6 -1
- package/commands/gsd/plan-review-convergence.md +11 -7
- package/commands/gsd/pr-branch.md +4 -0
- package/commands/gsd/profile-user.md +5 -1
- package/commands/gsd/progress.md +7 -2
- package/commands/gsd/quick-batch.md +21 -9
- package/commands/gsd/quick.md +12 -7
- package/commands/gsd/review.md +7 -4
- package/commands/gsd/secure-phase.md +6 -1
- package/commands/gsd/ship.md +5 -0
- package/commands/gsd/sketch.md +7 -2
- package/commands/gsd/spec-phase.md +5 -1
- package/commands/gsd/spike.md +8 -3
- package/commands/gsd/surface.md +5 -1
- package/commands/gsd/thread.md +4 -0
- package/commands/gsd/ui-phase.md +6 -1
- package/commands/gsd/ui-review.md +6 -1
- package/commands/gsd/ultraplan-phase.md +5 -1
- package/commands/gsd/undo.md +5 -1
- package/commands/gsd/update.md +6 -2
- package/commands/gsd/validate-phase.md +6 -1
- package/commands/gsd/verify-work.md +6 -1
- package/commands/gsd/workspace.md +7 -3
- package/gsd-core/bin/gsd-tools.cjs +477 -78
- package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
- package/gsd-core/bin/lib/audit.cjs +144 -42
- package/gsd-core/bin/lib/broken-windows.cjs +13 -13
- package/gsd-core/bin/lib/capability-activation.cjs +9 -4
- package/gsd-core/bin/lib/capability-registry.cjs +197 -222
- package/gsd-core/bin/lib/capability-validator.cjs +16 -1
- package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
- package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
- package/gsd-core/bin/lib/cli-exit.cjs +12 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
- package/gsd-core/bin/lib/command-aliases.cjs +7 -0
- package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
- package/gsd-core/bin/lib/commands.cjs +343 -207
- package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +76 -19
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/coverage.cjs +4 -8
- package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/drift.cjs +177 -42
- package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
- package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
- package/gsd-core/bin/lib/frontmatter.cjs +426 -234
- package/gsd-core/bin/lib/gap-checker.cjs +72 -29
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
- package/gsd-core/bin/lib/gate-args.cjs +53 -0
- package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
- package/gsd-core/bin/lib/gate-config.cjs +46 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
- package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
- package/gsd-core/bin/lib/gate-exit.cjs +27 -0
- package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
- package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
- package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
- package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
- package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
- package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
- package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
- package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
- package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
- package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
- package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
- package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +364 -132
- package/gsd-core/bin/lib/install-engine.cjs +30 -33
- package/gsd-core/bin/lib/install-profiles.cjs +7 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/io.cjs +122 -3
- package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
- package/gsd-core/bin/lib/milestone.cjs +37 -6
- package/gsd-core/bin/lib/model-resolver.cjs +171 -62
- package/gsd-core/bin/lib/observability/event.cjs +1 -1
- package/gsd-core/bin/lib/observability/logger.cjs +46 -1
- package/gsd-core/bin/lib/pattern.cjs +10 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
- package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +110 -8
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase-status.cjs +360 -0
- package/gsd-core/bin/lib/phase.cjs +489 -88
- package/gsd-core/bin/lib/plan-document.cjs +142 -20
- package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
- package/gsd-core/bin/lib/planning-document.cjs +692 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/profile-output.cjs +6 -3
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
- package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
- package/gsd-core/bin/lib/report-parser.cjs +269 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
- package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
- package/gsd-core/bin/lib/roadmap.cjs +405 -88
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
- package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
- package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
- package/gsd-core/bin/lib/state-contract.cjs +15 -18
- package/gsd-core/bin/lib/state-document.cjs +100 -22
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +256 -105
- package/gsd-core/bin/lib/surface.cjs +19 -2
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
- package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
- package/gsd-core/bin/lib/uat.cjs +432 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/vendor/README.md +31 -9
- package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
- package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
- package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
- package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +1378 -274
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
- package/gsd-core/bin/lib/verify.cjs +357 -531
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
- package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
- package/gsd-core/bin/lib/workstream.cjs +11 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/references/autonomous-smart-discuss.md +2 -1
- package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-mvp-tdd.md +5 -10
- package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +7 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/offer-next.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +12 -3
- package/gsd-core/references/spidr-splitting.md +1 -1
- package/gsd-core/references/tdd.md +37 -8
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verifier-phase-gates.md +5 -2
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -3
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/UAT.md +17 -1
- package/gsd-core/templates/config.json +2 -11
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +8 -7
- package/gsd-core/workflows/add-tests.md +4 -3
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +13 -4
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +4 -3
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +43 -23
- package/gsd-core/workflows/check-todos.md +7 -6
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
- package/gsd-core/workflows/code-review-fix.md +112 -25
- package/gsd-core/workflows/code-review.md +146 -135
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
- package/gsd-core/workflows/complete-milestone.md +13 -8
- package/gsd-core/workflows/debug.md +32 -7
- package/gsd-core/workflows/diagnose-issues.md +3 -2
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
- package/gsd-core/workflows/discuss-phase.md +4 -3
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +4 -3
- package/gsd-core/workflows/eval-review.md +14 -5
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +96 -178
- package/gsd-core/workflows/execute-plan.md +18 -23
- package/gsd-core/workflows/explore.md +4 -4
- package/gsd-core/workflows/extract-learnings.md +4 -2
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +3 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +4 -3
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +4 -3
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +6 -4
- package/gsd-core/workflows/map-codebase.md +5 -4
- package/gsd-core/workflows/milestone-summary.md +3 -2
- package/gsd-core/workflows/mvp-phase.md +14 -14
- package/gsd-core/workflows/new-milestone.md +11 -11
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +2 -2
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +50 -24
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +19 -49
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +15 -10
- package/gsd-core/workflows/quick.md +62 -39
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +3 -2
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +3 -2
- package/gsd-core/workflows/review.md +33 -17
- package/gsd-core/workflows/scan.md +3 -2
- package/gsd-core/workflows/secure-phase.md +13 -13
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +14 -13
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +2 -2
- package/gsd-core/workflows/transition.md +13 -23
- package/gsd-core/workflows/ui-phase.md +48 -11
- package/gsd-core/workflows/ui-review.md +21 -6
- package/gsd-core/workflows/ultraplan-phase.md +3 -2
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +12 -13
- package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
- package/gsd-core/workflows/verify-work.md +129 -55
- package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
- package/hooks/dist/gsd-check-update-worker.js +8 -0
- package/hooks/dist/gsd-check-update.js +8 -0
- package/hooks/dist/gsd-context-monitor.js +31 -8
- package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
- package/hooks/dist/gsd-secret-read-guard.js +161 -4
- package/hooks/dist/gsd-statusline.js +85 -20
- package/hooks/dist/gsd-update-banner.js +8 -0
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
- package/hooks/dist/gsd-workflow-guard.js +5 -4
- package/hooks/dist/gsd-worktree-path-guard.js +6 -2
- package/hooks/dist/lib/cli-exit.js +12 -0
- package/hooks/dist/lib/git-probe.js +17 -1
- package/hooks/dist/lib/isolation-sentinel.js +2 -2
- package/hooks/gsd-agent-isolation-guard.js +32 -0
- package/hooks/gsd-check-update-worker.js +8 -0
- package/hooks/gsd-check-update.js +8 -0
- package/hooks/gsd-context-monitor.js +31 -8
- package/hooks/gsd-cursor-subagent-start.js +8 -0
- package/hooks/gsd-secret-read-guard.js +161 -4
- package/hooks/gsd-statusline.js +85 -20
- package/hooks/gsd-update-banner.js +8 -0
- package/hooks/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-windsurf-pre-write.js +11 -2
- package/hooks/gsd-workflow-guard.js +5 -4
- package/hooks/gsd-worktree-path-guard.js +6 -2
- package/hooks/hooks.json +5 -5
- package/hooks/lib/cli-exit.js +12 -0
- package/hooks/lib/git-probe.js +17 -1
- package/hooks/lib/isolation-sentinel.js +2 -2
- package/package.json +22 -4
- package/scripts/build-hooks.js +15 -6
- package/scripts/changeset/parse.cjs +52 -4
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/ci-timeout-report.cjs +770 -4
- package/scripts/command-contract-helpers.cjs +15 -8
- package/scripts/docs-guard-registry.cjs +34 -0
- package/scripts/gen-features.cjs +13 -8
- package/scripts/gen-hooks-cli-exit.cjs +12 -28
- package/scripts/gen-loop-host-contract.cjs +79 -1
- package/scripts/gen-platform-conformance-tier.cjs +187 -1
- package/scripts/gen-plugin-skills.cjs +87 -1
- package/scripts/gen-research-agents.cjs +24 -31
- package/scripts/gen-scripts-cli-exit.cjs +30 -3
- package/scripts/gen-test-timings.cjs +32 -7
- package/scripts/lib/cli-exit.cjs +12 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
- package/scripts/lib/ndjson-reporter.cjs +31 -5
- package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
- package/scripts/lib/registration-ledger-preload.cjs +155 -0
- package/scripts/lib/vendor-bundle.cjs +59 -0
- package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-completion-predicate-drift.cjs +18 -19
- package/scripts/lint-descriptions.cjs +7 -3
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
- package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
- package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-enumeration-drift.cjs +12 -8
- package/scripts/lint-phase-id-drift.cjs +319 -5
- package/scripts/lint-planning-document-positive-control.cjs +329 -0
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +3 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-skill-deps.cjs +7 -3
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +38 -9
- package/scripts/lint-test-file-count.cjs +34 -1
- package/scripts/lint-vendored-deps.cjs +41 -5
- package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
- package/scripts/mutation-matrix.cjs +50 -5
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/require-issue-link-policy.cjs +6 -2
- package/scripts/sync-runtime-launcher.cjs +184 -2
- package/scripts/verify-npm-publish.cjs +76 -20
- package/skills/gsd-add-tests/SKILL.md +6 -1
- package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
- package/skills/gsd-audit-fix/SKILL.md +5 -0
- package/skills/gsd-audit-milestone/SKILL.md +6 -1
- package/skills/gsd-autonomous/SKILL.md +7 -2
- package/skills/gsd-capture/SKILL.md +9 -5
- package/skills/gsd-code-review/SKILL.md +7 -2
- package/skills/gsd-complete-milestone/SKILL.md +4 -0
- package/skills/gsd-config/SKILL.md +8 -4
- package/skills/gsd-debug/SKILL.md +11 -7
- package/skills/gsd-discuss-phase/SKILL.md +7 -3
- package/skills/gsd-docs-update/SKILL.md +12 -7
- package/skills/gsd-eval-review/SKILL.md +6 -1
- package/skills/gsd-execute-phase/SKILL.md +12 -7
- package/skills/gsd-extract-learnings/SKILL.md +5 -0
- package/skills/gsd-fast/SKILL.md +4 -0
- package/skills/gsd-forensics/SKILL.md +5 -1
- package/skills/gsd-graphify/SKILL.md +10 -6
- package/skills/gsd-health/SKILL.md +5 -0
- package/skills/gsd-help/SKILL.md +7 -2
- package/skills/gsd-import/SKILL.md +7 -3
- package/skills/gsd-inbox/SKILL.md +5 -0
- package/skills/gsd-ingest-docs/SKILL.md +5 -1
- package/skills/gsd-manager/SKILL.md +6 -1
- package/skills/gsd-map-codebase/SKILL.md +7 -3
- package/skills/gsd-mempalace-capture/SKILL.md +12 -4
- package/skills/gsd-mempalace-recall/SKILL.md +5 -1
- package/skills/gsd-milestone-summary/SKILL.md +5 -1
- package/skills/gsd-mvp-phase/SKILL.md +8 -3
- package/skills/gsd-new-milestone/SKILL.md +6 -1
- package/skills/gsd-new-project/SKILL.md +5 -0
- package/skills/gsd-next/SKILL.md +6 -1
- package/skills/gsd-ns-context/SKILL.md +4 -0
- package/skills/gsd-ns-ideate/SKILL.md +4 -0
- package/skills/gsd-ns-manage/SKILL.md +4 -0
- package/skills/gsd-ns-project/SKILL.md +4 -0
- package/skills/gsd-ns-review/SKILL.md +4 -0
- package/skills/gsd-ns-workflow/SKILL.md +4 -0
- package/skills/gsd-onboard/SKILL.md +6 -1
- package/skills/gsd-pause-work/SKILL.md +5 -1
- package/skills/gsd-phase/SKILL.md +8 -4
- package/skills/gsd-plan-phase/SKILL.md +6 -1
- package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
- package/skills/gsd-pr-branch/SKILL.md +4 -0
- package/skills/gsd-profile-user/SKILL.md +5 -1
- package/skills/gsd-progress/SKILL.md +7 -2
- package/skills/gsd-quick/SKILL.md +16 -10
- package/skills/gsd-quick-batch/SKILL.md +21 -9
- package/skills/gsd-review/SKILL.md +7 -4
- package/skills/gsd-review-backlog/SKILL.md +3 -2
- package/skills/gsd-secure-phase/SKILL.md +6 -1
- package/skills/gsd-ship/SKILL.md +5 -0
- package/skills/gsd-sketch/SKILL.md +7 -2
- package/skills/gsd-spec-phase/SKILL.md +5 -1
- package/skills/gsd-spike/SKILL.md +8 -3
- package/skills/gsd-surface/SKILL.md +5 -1
- package/skills/gsd-thread/SKILL.md +4 -0
- package/skills/gsd-ui-phase/SKILL.md +6 -1
- package/skills/gsd-ui-review/SKILL.md +6 -1
- package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
- package/skills/gsd-undo/SKILL.md +5 -1
- package/skills/gsd-update/SKILL.md +6 -2
- package/skills/gsd-validate-phase/SKILL.md +6 -1
- package/skills/gsd-verify-work/SKILL.md +6 -1
- package/skills/gsd-workspace/SKILL.md +7 -3
- package/skills/gsd-workstreams/SKILL.md +6 -6
- package/vscode/package.json +1 -1
|
@@ -20,6 +20,17 @@ const security_cjs_1 = require("./security.cjs");
|
|
|
20
20
|
// providing a deterministic failure path when git stalls (locked index, hung
|
|
21
21
|
// remote, stalled NFS mount, etc.). Callers can override via deps.timeout.
|
|
22
22
|
const DEFAULT_GIT_TIMEOUT_MS = 10000;
|
|
23
|
+
// #4721: the wave's `git merge --no-ff` is the one call in this module that runs
|
|
24
|
+
// the commit-family hooks (`pre-merge-commit`, `prepare-commit-msg`,
|
|
25
|
+
// `commit-msg`, `post-merge`), and a repo whose pre-merge hook is a test-suite
|
|
26
|
+
// gate routinely runs for minutes. That is hook runtime, not "git stalling", so
|
|
27
|
+
// the merge gets its own budget instead of inheriting DEFAULT_GIT_TIMEOUT_MS —
|
|
28
|
+
// raising the shared default would be the wrong lever, because every other
|
|
29
|
+
// caller in the module is exactly what the 10 s comment above describes.
|
|
30
|
+
// (`worktree add` runs `post-checkout` and every ref update runs
|
|
31
|
+
// `reference-transaction`; those are plumbing-cheap and stay on the default.)
|
|
32
|
+
// Callers override via deps.mergeTimeoutMs.
|
|
33
|
+
const DEFAULT_MERGE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
23
34
|
// #3021: accept the Workflow tool's worktree-wf_<runid>-<n> naming convention
|
|
24
35
|
// (claude-orchestration's isolation:"worktree" emission) alongside the
|
|
25
36
|
// existing agent-<id> / worktree-agent-<id> shapes.
|
|
@@ -54,7 +65,14 @@ function parseWorktreeEntries(porcelain) {
|
|
|
54
65
|
continue;
|
|
55
66
|
const branchLine = lines.find((l) => l.startsWith('branch refs/heads/'));
|
|
56
67
|
const branch = branchLine ? branchLine.slice('branch refs/heads/'.length).trim() : null;
|
|
57
|
-
|
|
68
|
+
// #4415: `prunable` appears either bare or with a reason. Keep the reason
|
|
69
|
+
// when git gives one, and fall back to a non-empty marker when it does not,
|
|
70
|
+
// so the field stays a truthful "git says stale" boolean either way.
|
|
71
|
+
const prunableLine = lines.find((l) => l === 'prunable' || l.startsWith('prunable '));
|
|
72
|
+
const prunable = prunableLine
|
|
73
|
+
? (prunableLine.slice('prunable'.length).trim() || 'prunable')
|
|
74
|
+
: null;
|
|
75
|
+
entries.push({ path: worktreePath, branch, prunable });
|
|
58
76
|
}
|
|
59
77
|
return entries;
|
|
60
78
|
}
|
|
@@ -481,6 +499,98 @@ function repoRootStillMidMerge(execGit, repoRoot) {
|
|
|
481
499
|
return false; // ref not found — repoRoot is not mid-merge
|
|
482
500
|
return true; // any other exit code (e.g. a fatal git error) — fail closed
|
|
483
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* #4721: after a merge that was KILLED — at its budget or by a signal — and
|
|
504
|
+
* did not leave MERGE_HEAD behind, undo whatever it staged in repoRoot's index
|
|
505
|
+
* and re-apply any work it had autostashed. (A kill that lands once MERGE_HEAD
|
|
506
|
+
* exists — inside `commit-msg`, say — is the ordinary #2852 path: `git merge
|
|
507
|
+
* --abort` restores the tree and re-applies an autostash itself, unstaged, as
|
|
508
|
+
* it does for any aborted autostashed merge.)
|
|
509
|
+
*
|
|
510
|
+
* Why the staged set is attributable to the merge — on this path only: `git
|
|
511
|
+
* merge` refuses to start when the index already differs from HEAD ("your
|
|
512
|
+
* local changes … would be overwritten", even for paths the branch never
|
|
513
|
+
* touches), and a refusal is an immediate exit with a code, never a kill. The
|
|
514
|
+
* one way for a KILLED merge to leave a dirty index with no MERGE_HEAD is a
|
|
515
|
+
* kill between populating the index and writing MERGE_HEAD — i.e. during a
|
|
516
|
+
* merge hook. The exception is `merge.autoStash`: git then parks the pre-existing
|
|
517
|
+
* work in MERGE_AUTOSTASH and starts anyway, and a killed merge never
|
|
518
|
+
* re-applies it. Handled below; it is why the reset runs even on a clean
|
|
519
|
+
* index.
|
|
520
|
+
*
|
|
521
|
+
* `git reset --merge` (no commit → HEAD) is the restore: it resets the index
|
|
522
|
+
* to HEAD and updates the worktree only for the paths the index changed,
|
|
523
|
+
* keeping unrelated unstaged edits intact, and it refuses rather than clobbers
|
|
524
|
+
* when an unstaged edit overlaps a staged path. It also moves a pending
|
|
525
|
+
* MERGE_AUTOSTASH into the stash list ("Autostash exists; creating a new stash
|
|
526
|
+
* entry"), which `git stash pop --index` then re-applies — the same outcome
|
|
527
|
+
* `git merge --abort` gives an autostashed merge that could be aborted.
|
|
528
|
+
*
|
|
529
|
+
* Returns `halt: true` only when repoRoot is still (or unverifiably) dirty —
|
|
530
|
+
* the same repo-level carve-out `repoRootStillMidMerge` uses, and for the same
|
|
531
|
+
* reason: every remaining entry's merge would run against a dirty index.
|
|
532
|
+
*/
|
|
533
|
+
function restoreMergeResidue(execGit, repoRoot, branch) {
|
|
534
|
+
const stagedPaths = (raw) => raw
|
|
535
|
+
.split('\n')
|
|
536
|
+
.map((line) => decodeGitQuotedPath(line.trim()))
|
|
537
|
+
.filter((p) => p.length > 0);
|
|
538
|
+
const leftStaged = (paths) => ({
|
|
539
|
+
halt: true,
|
|
540
|
+
warnings: paths.map((p) => ({ code: WAVE_CLEANUP_WARNING.MERGE_RESIDUE_LEFT_STAGED, branch, path: p })),
|
|
541
|
+
});
|
|
542
|
+
const staged = execGit(['diff', '--cached', '--name-only'], { cwd: repoRoot });
|
|
543
|
+
if (!gitResultOk(staged)) {
|
|
544
|
+
// Cannot tell whether the index is dirty — fail closed, same as an
|
|
545
|
+
// unverifiable MERGE_HEAD check. A null path marks "the check itself could
|
|
546
|
+
// not run", the convention SCOPE_CHECK_UNAVAILABLE already uses.
|
|
547
|
+
return leftStaged([null]);
|
|
548
|
+
}
|
|
549
|
+
const before = stagedPaths(staged.stdout || '');
|
|
550
|
+
// Exit 0 = git parked pre-existing work here before starting the merge;
|
|
551
|
+
// exit 1 = no autostash. Anything else is unknown: do not pop blind, but do
|
|
552
|
+
// say so — the reset below will have moved any stash into the list unread.
|
|
553
|
+
const autostash = execGit(['rev-parse', '--verify', '-q', 'MERGE_AUTOSTASH'], { cwd: repoRoot });
|
|
554
|
+
const hadAutostash = !autostash.timedOut && autostash.exitCode === 0;
|
|
555
|
+
const autostashUnknown = !!autostash.timedOut || (autostash.exitCode !== 0 && autostash.exitCode !== 1);
|
|
556
|
+
if (before.length === 0 && !hadAutostash && !autostashUnknown)
|
|
557
|
+
return { halt: false, warnings: [] };
|
|
558
|
+
const reset = execGit(['reset', '--merge'], { cwd: repoRoot });
|
|
559
|
+
const recheck = gitResultOk(reset) ? execGit(['diff', '--cached', '--name-only'], { cwd: repoRoot }) : null;
|
|
560
|
+
if (!recheck || !gitResultOk(recheck)) {
|
|
561
|
+
// The reset failed, or its result could not be re-read: report the set we
|
|
562
|
+
// know was staged, and halt.
|
|
563
|
+
return leftStaged(before.length > 0 ? before : [null]);
|
|
564
|
+
}
|
|
565
|
+
const after = stagedPaths(recheck.stdout || '');
|
|
566
|
+
if (after.length > 0)
|
|
567
|
+
return leftStaged(after);
|
|
568
|
+
const warnings = before.map((p) => ({ code: WAVE_CLEANUP_WARNING.MERGE_RESIDUE_RESTORED, branch, path: p }));
|
|
569
|
+
if (hadAutostash) {
|
|
570
|
+
const pop = execGit(['stash', 'pop', '--index'], { cwd: repoRoot });
|
|
571
|
+
if (!gitResultOk(pop)) {
|
|
572
|
+
warnings.push({ code: WAVE_CLEANUP_WARNING.MERGE_AUTOSTASH_UNRESTORED, branch, path: null });
|
|
573
|
+
// A failed pop keeps the stash entry, but it can leave conflict entries
|
|
574
|
+
// (`UU`) and partially applied paths behind it — and the next merge then
|
|
575
|
+
// fails with "you have unmerged files" (caught in review). Re-read rather
|
|
576
|
+
// than assume: a dirty index here halts exactly as an unrestorable
|
|
577
|
+
// residue does.
|
|
578
|
+
const afterPop = execGit(['diff', '--cached', '--name-only'], { cwd: repoRoot });
|
|
579
|
+
if (!gitResultOk(afterPop))
|
|
580
|
+
return { halt: true, warnings: [...warnings, ...leftStaged([null]).warnings] };
|
|
581
|
+
const dirty = stagedPaths(afterPop.stdout || '');
|
|
582
|
+
if (dirty.length > 0)
|
|
583
|
+
return { halt: true, warnings: [...warnings, ...leftStaged(dirty).warnings] };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else if (autostashUnknown) {
|
|
587
|
+
warnings.push({ code: WAVE_CLEANUP_WARNING.MERGE_AUTOSTASH_UNRESTORED, branch, path: null });
|
|
588
|
+
}
|
|
589
|
+
// Not a halt: the index is verified clean, or holds only the operator's own
|
|
590
|
+
// re-applied work (a successful `--index` pop), which the next merge
|
|
591
|
+
// autostashes again under the same config.
|
|
592
|
+
return { halt: false, warnings };
|
|
593
|
+
}
|
|
484
594
|
// #2596: the single definition of "this file is an executor-written SUMMARY
|
|
485
595
|
// artifact". Shared by `defaultFindSummaryFiles` (which walks for them to
|
|
486
596
|
// rescue) and the scope advisory below (which must never flag them) — a plan's
|
|
@@ -646,14 +756,25 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
|
|
|
646
756
|
const readFileSync = deps.readFileSync || ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
|
|
647
757
|
const mkdirSync = deps.mkdirSync || ((d, o) => node_fs_1.default.mkdirSync(d, o));
|
|
648
758
|
const copyFileSync = deps.copyFileSync || node_fs_1.default.copyFileSync;
|
|
649
|
-
|
|
759
|
+
// #4758: resolve the manifest's worktree_path against repoRoot once, at this
|
|
760
|
+
// boundary, so every reader of the field here — the fs walker below and the
|
|
761
|
+
// `git -C` calls — resolves it the way the caller's git consumers already do
|
|
762
|
+
// (`-C <path>` with `{ cwd: repoRoot }`). A relative value passed to the
|
|
763
|
+
// walker verbatim made `path.join` emit a relative directory whose reads
|
|
764
|
+
// resolved against process.cwd(), silently walking the wrong tree.
|
|
765
|
+
// With this resolution, the remaining deps-injectable readers (existsSync,
|
|
766
|
+
// readFileSync, mkdirSync, copyFileSync) only ever see absolute paths:
|
|
767
|
+
// `dest` is built from repoRoot, `absPath` from the walker's join off this
|
|
768
|
+
// resolved base.
|
|
769
|
+
const resolvedWorktreePath = node_path_1.default.resolve(repoRoot, worktreePath);
|
|
770
|
+
const summaryPaths = findSummaryFiles(resolvedWorktreePath);
|
|
650
771
|
const rescuedRelPaths = new Set();
|
|
651
772
|
const failures = [];
|
|
652
773
|
for (const absPath of summaryPaths) {
|
|
653
774
|
// relPath is the path relative to the worktree root (e.g. ".planning/q1-SUMMARY.md")
|
|
654
775
|
// Normalize to forward slashes so the Set comparison against `git status --porcelain`
|
|
655
776
|
// output works on Windows too (git always emits forward slashes in porcelain output).
|
|
656
|
-
const relPath = (0, shell_command_projection_cjs_1.posixNormalize)(absPath.slice(
|
|
777
|
+
const relPath = (0, shell_command_projection_cjs_1.posixNormalize)(absPath.slice(resolvedWorktreePath.length).replace(/^[/\\]/, ''));
|
|
657
778
|
// #706: skip rescue when the SUMMARY is already committed on the branch.
|
|
658
779
|
// Use `git cat-file -e HEAD:<relPath>` (not `ls-files --error-unmatch`) so
|
|
659
780
|
// the check is against the committed tree, not the index. ls-files also
|
|
@@ -671,7 +792,7 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
|
|
|
671
792
|
// copy when the main tree already holds identical content (which is also what
|
|
672
793
|
// guards the #706 merge collision). A divergent dest is overwritten, but only
|
|
673
794
|
// uncommitted main-tree content could be lost (committed content is git-recoverable).
|
|
674
|
-
const catFileResult = execGit(['-C',
|
|
795
|
+
const catFileResult = execGit(['-C', resolvedWorktreePath, 'cat-file', '-e', `HEAD:${relPath}`], { cwd: repoRoot });
|
|
675
796
|
if (catFileResult.exitCode === 0) {
|
|
676
797
|
// exit 0 → the SUMMARY is committed on HEAD; the merge will carry it, so skip rescue.
|
|
677
798
|
continue;
|
|
@@ -719,6 +840,42 @@ const WAVE_CLEANUP_WARNING = Object.freeze({
|
|
|
719
840
|
SCOPE_OUT_OF_DECLARED: 'scope_out_of_declared',
|
|
720
841
|
/** The scope diff could not be computed, so conformance is unknown. */
|
|
721
842
|
SCOPE_CHECK_UNAVAILABLE: 'scope_check_unavailable',
|
|
843
|
+
/**
|
|
844
|
+
* #4721: a killed merge (at its budget, or by a signal) left this path
|
|
845
|
+
* staged in repoRoot's index with no MERGE_HEAD, and `git reset --merge`
|
|
846
|
+
* restored it to HEAD. Informational — repoRoot is clean again.
|
|
847
|
+
*/
|
|
848
|
+
MERGE_RESIDUE_RESTORED: 'merge_residue_restored',
|
|
849
|
+
/**
|
|
850
|
+
* #4721: a killed merge left this path staged in repoRoot's index with no
|
|
851
|
+
* MERGE_HEAD and it could NOT be restored (null path: the index could not
|
|
852
|
+
* be read at all) — or a failed autostash pop left it unmerged. repoRoot is
|
|
853
|
+
* dirty; committing from it would squash the executor's history into one
|
|
854
|
+
* parent. The wave halts.
|
|
855
|
+
*/
|
|
856
|
+
MERGE_RESIDUE_LEFT_STAGED: 'merge_residue_left_staged',
|
|
857
|
+
/**
|
|
858
|
+
* #4721: the killed merge had parked pre-existing work in MERGE_AUTOSTASH
|
|
859
|
+
* (`merge.autoStash`), and re-applying it failed or could not be verified.
|
|
860
|
+
* The work is in the stash list, not lost. Path is always null. On its own
|
|
861
|
+
* the index is clean and the wave continues; when a failed pop left
|
|
862
|
+
* unmerged entries it is accompanied by MERGE_RESIDUE_LEFT_STAGED rows and
|
|
863
|
+
* the wave halts.
|
|
864
|
+
*/
|
|
865
|
+
MERGE_AUTOSTASH_UNRESTORED: 'merge_autostash_unrestored',
|
|
866
|
+
/**
|
|
867
|
+
* #4415: an entry was merged on the evidence that its checkout was already
|
|
868
|
+
* gone, rather than on a clean read of a present worktree.
|
|
869
|
+
*
|
|
870
|
+
* Emitted because "the harness cleanly removed a finished executor" and
|
|
871
|
+
* "something else removed this path" are the same signature to this code —
|
|
872
|
+
* git still registers the path -> branch binding, and `statSync` reports
|
|
873
|
+
* ENOENT, in both cases. Before this path existed, EVERY anomalous absence
|
|
874
|
+
* blocked loudly, which gave an operator something to investigate; accepting
|
|
875
|
+
* the routine case silently would take that signal away from the case that is
|
|
876
|
+
* not routine. Advisory, never a gate: the entry still merged.
|
|
877
|
+
*/
|
|
878
|
+
ACCEPTED_ABSENT_WORKTREE: 'accepted_absent_worktree',
|
|
722
879
|
});
|
|
723
880
|
/**
|
|
724
881
|
* The literal directory prefix a declared path covers, or `null` when the
|
|
@@ -834,6 +991,127 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
834
991
|
const pending = [];
|
|
835
992
|
const allWarnings = [];
|
|
836
993
|
let ok = true;
|
|
994
|
+
// #4415: two questions, two sources, each asked only what it can actually prove.
|
|
995
|
+
//
|
|
996
|
+
// IDENTITY — "is the checkout registered at this path the branch the manifest
|
|
997
|
+
// names?" — comes from `git worktree list --porcelain`.
|
|
998
|
+
// REMOVAL — "is the directory actually gone, as opposed to unreadable?" —
|
|
999
|
+
// comes from `statSync`'s errno.
|
|
1000
|
+
//
|
|
1001
|
+
// Neither source can answer the other's question, and both mistakes have been
|
|
1002
|
+
// measured rather than reasoned about:
|
|
1003
|
+
//
|
|
1004
|
+
// 1. An earlier cut inferred removal from `fs.existsSync` returning false and,
|
|
1005
|
+
// with no checkout left to read, fell back to `refs/heads/<branch>` for
|
|
1006
|
+
// identity. Git never loses the binding: after `rm -rf` it still prints
|
|
1007
|
+
// `worktree <path>` + `branch refs/heads/<branch>`. The ref fallback weakened
|
|
1008
|
+
// identity from "the checkout registered here is on this branch" to "a branch
|
|
1009
|
+
// by this name exists", which let a foreign sibling branch through the gate.
|
|
1010
|
+
//
|
|
1011
|
+
// 2. `prunable` is NOT a removal test. Measured: with a parent directory at mode
|
|
1012
|
+
// 000, git emits `prunable gitdir file points to non-existent location` for a
|
|
1013
|
+
// checkout that is still there — it cannot traverse the parent, so it reports
|
|
1014
|
+
// the gitdir file as missing. Accepting `prunable` as "removed" would merge
|
|
1015
|
+
// over uncommitted work in an unreadable worktree, which is the very thing the
|
|
1016
|
+
// dirty check exists to refuse. `statSync` separates them: ENOENT is gone,
|
|
1017
|
+
// EACCES/EIO is unreadable.
|
|
1018
|
+
//
|
|
1019
|
+
// The identity read is a SNAPSHOT taken before the loop, and that is load-bearing:
|
|
1020
|
+
// `git worktree prune` is repository-wide, so the first absent entry's teardown
|
|
1021
|
+
// clears EVERY stale registration, including those of entries not yet evaluated.
|
|
1022
|
+
// Measured: two removed worktrees, one `prune`, and both registrations are gone.
|
|
1023
|
+
// Reading the list per entry would therefore merge the first harness-removed
|
|
1024
|
+
// worktree of a wave and block the rest as `branch_mismatch` — worse than the bug
|
|
1025
|
+
// this PR fixes, since a wave of parallel executors is the normal case.
|
|
1026
|
+
//
|
|
1027
|
+
// Captured LAZILY, on the first entry that actually needs identity, and reused for
|
|
1028
|
+
// the rest of the wave. Laziness is what keeps the read off the happy path — a wave
|
|
1029
|
+
// whose worktrees are all present never spends the subprocess — and it is still
|
|
1030
|
+
// early enough to be a true pre-prune snapshot, because every teardown that prunes
|
|
1031
|
+
// consults this predicate first.
|
|
1032
|
+
let worktreeListSnapshot = null;
|
|
1033
|
+
const snapshotWorktreeList = () => {
|
|
1034
|
+
if (!worktreeListSnapshot)
|
|
1035
|
+
worktreeListSnapshot = readWorktreeList(plan.repoRoot, { execGit });
|
|
1036
|
+
return worktreeListSnapshot;
|
|
1037
|
+
};
|
|
1038
|
+
const resolveAgainstRepoRoot = (worktreePath) => node_path_1.default.resolve(plan.repoRoot, worktreePath);
|
|
1039
|
+
const findRegistered = (listed, target) => (listed.ok
|
|
1040
|
+
? listed.entries.find((listedEntry) => resolveAgainstRepoRoot(listedEntry.path) === target)
|
|
1041
|
+
: undefined);
|
|
1042
|
+
// `worktree_path` comes from the manifest verbatim and may be relative, while the
|
|
1043
|
+
// porcelain always reports absolute paths; git resolves the manifest form against
|
|
1044
|
+
// repoRoot (every call passes `-C <path>` with `cwd: plan.repoRoot`), so match it
|
|
1045
|
+
// the same way.
|
|
1046
|
+
const registeredFor = (worktreePath) => {
|
|
1047
|
+
const target = resolveAgainstRepoRoot(worktreePath);
|
|
1048
|
+
const fromSnapshot = findRegistered(snapshotWorktreeList(), target);
|
|
1049
|
+
if (fromSnapshot)
|
|
1050
|
+
return fromSnapshot;
|
|
1051
|
+
// Absent from the snapshot: it may have been registered after the wave began.
|
|
1052
|
+
// A list that cannot be read yields no entry, which blocks — the fail-safe way.
|
|
1053
|
+
return findRegistered(readWorktreeList(plan.repoRoot, { execGit }), target);
|
|
1054
|
+
};
|
|
1055
|
+
const statSyncRaw = deps.statSync || node_fs_1.default.statSync;
|
|
1056
|
+
// Only ENOENT is removal. A path that stats successfully is present; any other
|
|
1057
|
+
// errno means it could not be read, and an unreadable checkout blocked before
|
|
1058
|
+
// this PR and must keep blocking.
|
|
1059
|
+
const confirmedGone = (worktreePath) => {
|
|
1060
|
+
try {
|
|
1061
|
+
statSyncRaw(resolveAgainstRepoRoot(worktreePath));
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
catch (err) {
|
|
1065
|
+
return err?.code === 'ENOENT';
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
// Carries git's `prunable` reason for the most recent acceptance, so the
|
|
1069
|
+
// warning below can quote git rather than paraphrase it. Set only on the
|
|
1070
|
+
// accepting call; callers that reject never read it.
|
|
1071
|
+
let lastAcceptedPrunableReason = null;
|
|
1072
|
+
const absentAndIdentified = (worktreePath, branch) => {
|
|
1073
|
+
const registered = registeredFor(worktreePath);
|
|
1074
|
+
if (!registered || registered.branch !== branch)
|
|
1075
|
+
return false;
|
|
1076
|
+
if (!confirmedGone(worktreePath))
|
|
1077
|
+
return false;
|
|
1078
|
+
lastAcceptedPrunableReason = registered.prunable;
|
|
1079
|
+
return true;
|
|
1080
|
+
};
|
|
1081
|
+
/**
|
|
1082
|
+
* #4415 (maintainer review round 3): record that an entry took the absent path.
|
|
1083
|
+
*
|
|
1084
|
+
* Two Medium findings close here together. This code cannot distinguish "the
|
|
1085
|
+
* harness cleanly removed a finished executor" from "an operator or an external
|
|
1086
|
+
* process removed this path" — both leave git's registration intact and both
|
|
1087
|
+
* stat ENOENT. Before the absent path existed, every anomalous absence blocked
|
|
1088
|
+
* loudly; accepting the routine case silently would have removed that signal
|
|
1089
|
+
* from the case that is not routine, reporting `merged_removed`/`ok`
|
|
1090
|
+
* indistinguishably from an ordinary merge. The module already carries an
|
|
1091
|
+
* advisory channel for a materially less risky condition (scope conformance) a
|
|
1092
|
+
* few lines below, so withholding one here was inconsistent with its own
|
|
1093
|
+
* pattern.
|
|
1094
|
+
*
|
|
1095
|
+
* It also gives `WorktreeEntry.prunable` its consumer. The field was parsed and
|
|
1096
|
+
* documented as "worth surfacing to an operator" and then never read — dead
|
|
1097
|
+
* weight, and its bare-marker test asserted an outcome driven by other code.
|
|
1098
|
+
* Quoting git's own reason here is what that parsing was for.
|
|
1099
|
+
*/
|
|
1100
|
+
const noteAcceptedAbsent = (result, entry) => {
|
|
1101
|
+
// The parser normalises a bare `prunable` line to the literal 'prunable' so the
|
|
1102
|
+
// field stays truthy either way. That sentinel is the marker echoed back, not a
|
|
1103
|
+
// reason, so it is reported as "no reason given" rather than quoted at an
|
|
1104
|
+
// operator as though git had said something.
|
|
1105
|
+
const reason = lastAcceptedPrunableReason === 'prunable' ? null : lastAcceptedPrunableReason;
|
|
1106
|
+
const warning = {
|
|
1107
|
+
code: WAVE_CLEANUP_WARNING.ACCEPTED_ABSENT_WORKTREE,
|
|
1108
|
+
branch: entry.branch,
|
|
1109
|
+
path: entry.worktree_path,
|
|
1110
|
+
detail: reason,
|
|
1111
|
+
};
|
|
1112
|
+
result.warnings.push(warning);
|
|
1113
|
+
allWarnings.push(warning);
|
|
1114
|
+
};
|
|
837
1115
|
// #2852: every per-entry failure site marks the SAME shape — status='blocked',
|
|
838
1116
|
// a reason code, the captured stderr, push to results, flip the overall `ok`
|
|
839
1117
|
// flag — and then either `continue` (isolate, the default) or, for the one
|
|
@@ -855,12 +1133,46 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
855
1133
|
stderr: '',
|
|
856
1134
|
warnings: [],
|
|
857
1135
|
};
|
|
1136
|
+
// #4415: the harness may have already removed this worktree. Claude Code
|
|
1137
|
+
// removes a subagent's worktree the moment the subagent finishes with a
|
|
1138
|
+
// clean tree, and an executor that committed everything — SUMMARY.md
|
|
1139
|
+
// included, under `commit_docs: true` — is exactly that case, so by wave
|
|
1140
|
+
// cleanup the directory is routinely gone while the branch it left behind
|
|
1141
|
+
// is intact and mergeable.
|
|
1142
|
+
//
|
|
1143
|
+
// `git -C <gone> rev-parse` fails, and that failure was indistinguishable
|
|
1144
|
+
// from a genuine mismatch, so the entry blocked as `branch_mismatch` and
|
|
1145
|
+
// nothing merged. Disambiguate at the point of failure rather than ahead of
|
|
1146
|
+
// it: a SUCCESSFUL read still decides identity exactly as before (a present
|
|
1147
|
+
// worktree on the wrong branch blocks, unchanged), and only a FAILED read
|
|
1148
|
+
// asks git and the filesystem why.
|
|
1149
|
+
let worktreeAbsent = false;
|
|
858
1150
|
const branchCheck = execGit(['-C', entry.worktree_path, 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: plan.repoRoot });
|
|
859
|
-
if (!gitResultOk(branchCheck)
|
|
1151
|
+
if (!gitResultOk(branchCheck)) {
|
|
1152
|
+
// The in-worktree read failed. Ask git WHY, instead of asking the
|
|
1153
|
+
// filesystem WHETHER: identity is still on record in the porcelain output,
|
|
1154
|
+
// so the #3677 swap control keeps its teeth here rather than degrading to
|
|
1155
|
+
// "some branch by this name exists".
|
|
1156
|
+
//
|
|
1157
|
+
// Blocked unless git still binds this path to the branch the manifest names
|
|
1158
|
+
// AND the directory is confirmed gone (ENOENT). Each way of failing that is a
|
|
1159
|
+
// genuine mismatch: a different branch registered at the path is the swap the
|
|
1160
|
+
// control exists to catch; a path git does not list at all is an entry naming
|
|
1161
|
+
// something git has no record of; and a path that stats, or that fails to stat
|
|
1162
|
+
// for any reason other than ENOENT, is a checkout that is present or merely
|
|
1163
|
+
// unreadable — which blocked before this PR and must keep blocking.
|
|
1164
|
+
if (!absentAndIdentified(entry.worktree_path, entry.branch)) {
|
|
1165
|
+
blockEntry(result, 'branch_mismatch', branchCheck?.stderr || '');
|
|
1166
|
+
// #2852: isolate — this entry's problem does not touch repoRoot's git state,
|
|
1167
|
+
// so every remaining entry is still independently evaluated.
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
worktreeAbsent = true;
|
|
1171
|
+
noteAcceptedAbsent(result, entry);
|
|
1172
|
+
}
|
|
1173
|
+
else if (branchCheck.stdout.trim() !== entry.branch) {
|
|
860
1174
|
blockEntry(result, 'branch_mismatch', branchCheck?.stderr || '');
|
|
861
|
-
// #2852: isolate
|
|
862
|
-
// so every remaining entry is still independently evaluated.
|
|
863
|
-
continue;
|
|
1175
|
+
continue; // #2852: isolate
|
|
864
1176
|
}
|
|
865
1177
|
const mergeBase = execGit(['merge-base', 'HEAD', entry.branch], { cwd: plan.repoRoot });
|
|
866
1178
|
const allowedBases = Array.isArray(entry.allowed_bases) && entry.allowed_bases.length > 0
|
|
@@ -928,38 +1240,101 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
928
1240
|
result.warnings.push(...scopeWarnings);
|
|
929
1241
|
allWarnings.push(...scopeWarnings);
|
|
930
1242
|
}
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
.
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
//
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1243
|
+
// #4415: both steps below read the worktree directory. The rescue exists to
|
|
1244
|
+
// save work the executor left UNCOMMITTED; the dirty check exists to refuse
|
|
1245
|
+
// to merge over it. Against an absent path they fail differently, and only
|
|
1246
|
+
// one of them is loud: the default SUMMARY finder catches the unreadable
|
|
1247
|
+
// directory and simply returns no files, while `git -C <gone> status` errors
|
|
1248
|
+
// — and THAT is what surfaced as `worktree_dirty`, a block with nothing
|
|
1249
|
+
// merged. (Corrected in Codex review round 2: an earlier version of this
|
|
1250
|
+
// comment claimed both reads error.)
|
|
1251
|
+
//
|
|
1252
|
+
// The harness removes a worktree only when its tree is clean, so in the case
|
|
1253
|
+
// this fix targets there is genuinely nothing to rescue. That is a property
|
|
1254
|
+
// of the harness, NOT something checked here: this code cannot tell who
|
|
1255
|
+
// removed the directory, and a forced or manual `rm -rf` of a DIRTY worktree
|
|
1256
|
+
// would already have destroyed an uncommitted SUMMARY before cleanup ran.
|
|
1257
|
+
// What is claimed is only the narrow thing true either way — a missing
|
|
1258
|
+
// source cannot be read, so skipping the read loses nothing that still
|
|
1259
|
+
// exists. (Codex review round 1.)
|
|
1260
|
+
if (!worktreeAbsent) {
|
|
1261
|
+
// Safety net: rescue uncommitted SUMMARY.md artifacts before the dirty check.
|
|
1262
|
+
// The executor leaves <quick_id>-SUMMARY.md uncommitted by contract — the
|
|
1263
|
+
// orchestrator commits it. Mirrors quick.md shell fallback (#2296, #2070, #2838, #3804).
|
|
1264
|
+
//
|
|
1265
|
+
// Destructured in place, not hoisted: every path that reaches the consumer
|
|
1266
|
+
// below has already run this line. (An earlier cut hoisted it on the
|
|
1267
|
+
// reasoning that the nested block created another route in; Codex review
|
|
1268
|
+
// round 2 showed that is not so — flipping `worktreeAbsent` SKIPS the
|
|
1269
|
+
// consumer rather than reaching it unassigned.)
|
|
1270
|
+
const { rescuedRelPaths, failures: rescueFailures } = rescueSummaryArtifacts(entry.worktree_path, plan.repoRoot, deps);
|
|
1271
|
+
if (rescueFailures.length > 0) {
|
|
1272
|
+
blockEntry(result, 'summary_rescue_failed', rescueFailures.map((f) => `${f.relPath}: ${f.error}`).join('; '));
|
|
1273
|
+
continue; // #2852: isolate
|
|
1274
|
+
}
|
|
1275
|
+
const worktreeStatus = execGit(['-C', entry.worktree_path, 'status', '--porcelain', '--untracked-files=all'], { cwd: plan.repoRoot });
|
|
1276
|
+
if (!gitResultOk(worktreeStatus)) {
|
|
1277
|
+
// #4415 (Codex review round 1): the harness can remove the worktree
|
|
1278
|
+
// between the branch read and here — while the repoRoot-side base,
|
|
1279
|
+
// deletion and scope checks run. `worktreeAbsent` records what was true
|
|
1280
|
+
// at IDENTIFICATION time, not now, so without this a mid-entry removal
|
|
1281
|
+
// failed `status` and blocked `worktree_dirty` with nothing merged: the
|
|
1282
|
+
// same bug as the branch read, one window later. Same disambiguation,
|
|
1283
|
+
// applied at the same point — the read failed, so ask why.
|
|
1284
|
+
//
|
|
1285
|
+
// Deliberately NOT extended to a rescue FAILURE above: a copy that
|
|
1286
|
+
// errored part-way can mean an uncommitted SUMMARY was genuinely lost,
|
|
1287
|
+
// and that must keep blocking. A rescue that simply finds nothing to
|
|
1288
|
+
// copy reports no failure and falls through to here.
|
|
1289
|
+
//
|
|
1290
|
+
// Identity was already established by the successful branch read above, so
|
|
1291
|
+
// the question here is only staleness — but it is asked of git, on the same
|
|
1292
|
+
// terms as the identification site, because a `status` failure is no more
|
|
1293
|
+
// self-explaining than a `rev-parse` failure was.
|
|
1294
|
+
if (!absentAndIdentified(entry.worktree_path, entry.branch)) {
|
|
1295
|
+
blockEntry(result, 'worktree_dirty', worktreeStatus?.stderr || '');
|
|
1296
|
+
continue; // #2852: isolate
|
|
1297
|
+
}
|
|
1298
|
+
worktreeAbsent = true;
|
|
1299
|
+
noteAcceptedAbsent(result, entry);
|
|
1300
|
+
}
|
|
1301
|
+
if (!worktreeAbsent) {
|
|
1302
|
+
// Filter rescued SUMMARY paths out of the porcelain output before deciding dirty.
|
|
1303
|
+
// A line like "?? .planning/q1-SUMMARY.md" should not block when the SUMMARY
|
|
1304
|
+
// has already been rescued into the main tree.
|
|
1305
|
+
const dirtyLines = (worktreeStatus.stdout || '')
|
|
1306
|
+
.split('\n')
|
|
1307
|
+
.filter((line) => {
|
|
1308
|
+
if (!line.trim())
|
|
1309
|
+
return false;
|
|
1310
|
+
// porcelain v1 format: "XY path" (3-char prefix + space + path)
|
|
1311
|
+
const filePath = line.slice(3).trim();
|
|
1312
|
+
return !rescuedRelPaths.has(filePath);
|
|
1313
|
+
});
|
|
1314
|
+
if (dirtyLines.length > 0) {
|
|
1315
|
+
blockEntry(result, 'worktree_dirty', dirtyLines.join('\n'));
|
|
1316
|
+
continue; // #2852: isolate
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
959
1319
|
}
|
|
960
|
-
|
|
1320
|
+
// #4721: the merge runs user hooks, so it carries its own budget — see
|
|
1321
|
+
// DEFAULT_MERGE_TIMEOUT_MS. Every other call in this gauntlet keeps the
|
|
1322
|
+
// module default.
|
|
1323
|
+
const mergeTimeoutMs = deps.mergeTimeoutMs ?? DEFAULT_MERGE_TIMEOUT_MS;
|
|
1324
|
+
const merge = execGit(['merge', entry.branch, '--no-ff', '--no-edit', '-m', `chore: merge executor worktree (${entry.branch})`], { cwd: plan.repoRoot, timeout: mergeTimeoutMs });
|
|
961
1325
|
if (!gitResultOk(merge)) {
|
|
962
|
-
|
|
1326
|
+
if (merge?.timedOut) {
|
|
1327
|
+
// #4721: say "timeout" when it was one. The captured output is whatever
|
|
1328
|
+
// the hook printed before git was killed, which read as a git error under
|
|
1329
|
+
// the old `merge_failed` label and made a healthy executor branch look
|
|
1330
|
+
// broken. The hook itself is a child of the killed git process and may
|
|
1331
|
+
// still be running.
|
|
1332
|
+
const partial = (merge.stderr || merge.stdout || '').trim();
|
|
1333
|
+
blockEntry(result, 'merge_timed_out', `git merge did not finish within ${mergeTimeoutMs} ms and was killed (a merge hook such as pre-merge-commit may still be running; raise deps.mergeTimeoutMs or shorten the hook)${partial ? `; output before the kill: ${partial}` : ''}`);
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
blockEntry(result, 'merge_failed', merge?.stderr || merge?.stdout || '');
|
|
1337
|
+
}
|
|
963
1338
|
// #2852: a failed --no-ff merge MIGHT leave repoRoot itself mid-merge
|
|
964
1339
|
// (MERGE_HEAD set, conflict markers in the tree) — unlike every other block
|
|
965
1340
|
// reason above, that specific state is NOT scoped to this one entry: a second
|
|
@@ -978,21 +1353,111 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
|
|
|
978
1353
|
pending.push(...entries.slice(i + 1));
|
|
979
1354
|
break;
|
|
980
1355
|
}
|
|
1356
|
+
// #4721: "no MERGE_HEAD" is not "tree never touched". A merge killed while
|
|
1357
|
+
// its pre-merge-commit hook ran has already written the merged tree into
|
|
1358
|
+
// repoRoot's index (and set ORIG_HEAD) but never got to write MERGE_HEAD,
|
|
1359
|
+
// so the #2852 check above reads it as clean while the executor's whole
|
|
1360
|
+
// diff sits staged against the old HEAD. `git merge --abort` cannot see
|
|
1361
|
+
// that state either. Left alone, the next `git merge` in this wave would
|
|
1362
|
+
// refuse ("your local changes would be overwritten") or, worse, an
|
|
1363
|
+
// orchestrator that trusts the block reason and commits from repoRoot
|
|
1364
|
+
// squashes the executor's history into one parent. Restore it; if that
|
|
1365
|
+
// cannot be verified, halt the wave exactly as the mid-merge case does.
|
|
1366
|
+
//
|
|
1367
|
+
// ONLY when git was KILLED — at its budget, or by a signal from outside.
|
|
1368
|
+
// A merge git REFUSED (no MERGE_HEAD either) leaves the index exactly as
|
|
1369
|
+
// it found it — and "your local changes would be overwritten" is precisely
|
|
1370
|
+
// the refusal a pre-existing dirty index earns, so on that path anything
|
|
1371
|
+
// staged is the operator's own work and must not be touched (caught in
|
|
1372
|
+
// review). A kill is the one shape that stages a tree git never finished
|
|
1373
|
+
// with, and an external SIGTERM produces the same state as the timeout
|
|
1374
|
+
// without `timedOut` (caught in review too). The seam normalizes a
|
|
1375
|
+
// signal death to exitCode 1 and carries the signal alongside, so the
|
|
1376
|
+
// signal — never the exit code — is the tell; a refused merge has none.
|
|
1377
|
+
const mergeKilled = !!merge?.timedOut || !!merge?.signal;
|
|
1378
|
+
if (mergeKilled) {
|
|
1379
|
+
const residue = restoreMergeResidue(execGit, plan.repoRoot, entry.branch);
|
|
1380
|
+
result.warnings.push(...residue.warnings);
|
|
1381
|
+
allWarnings.push(...residue.warnings);
|
|
1382
|
+
if (residue.halt) {
|
|
1383
|
+
pending.push(...entries.slice(i + 1));
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
981
1387
|
continue; // #2852: isolate — repoRoot is not (or no longer) mid-merge
|
|
982
1388
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
|
|
1389
|
+
if (worktreeAbsent) {
|
|
1390
|
+
// #4415 (Codex review round 2): this entry was accepted WITHOUT the rescue
|
|
1391
|
+
// and dirty checks, on the evidence that it had no checkout. Never issue
|
|
1392
|
+
// `worktree remove --force` for it. If a registered checkout has since
|
|
1393
|
+
// reappeared at that path — recreated between the checks and here — a
|
|
1394
|
+
// forced removal would delete contents that never passed either check,
|
|
1395
|
+
// which is strictly worse than the bug this PR fixes.
|
|
1396
|
+
//
|
|
1397
|
+
// Re-confirm absence immediately before tearing down (maintainer review,
|
|
1398
|
+
// Major). Presence was classified once, at identification, and everything
|
|
1399
|
+
// between then and here — the base, deletion and scope gates, and the merge
|
|
1400
|
+
// itself — is a window in which a worktree can reappear. "Prune only" was
|
|
1401
|
+
// offered as sufficient on its own, on the argument that prune leaves a live
|
|
1402
|
+
// checkout alone and the `branch -D` below would then fail visibly. That
|
|
1403
|
+
// argument holds only while prune's own staleness check is not fooled by the
|
|
1404
|
+
// same filesystem-visibility gap that produced the false absence one call
|
|
1405
|
+
// earlier. If it is, prune clears the admin entry, `branch -D` then SUCCEEDS,
|
|
1406
|
+
// and a live, unreviewed, un-rescued worktree loses its branch — destroying
|
|
1407
|
+
// state, where the pre-fix bug only ever blocked. That asymmetry is why this
|
|
1408
|
+
// check is worth a `statSync`: the failure it prevents is unrecoverable, and
|
|
1409
|
+
// the check costs no subprocess.
|
|
1410
|
+
if (!confirmedGone(entry.worktree_path)) {
|
|
1411
|
+
blockEntry(result, 'worktree_remove_failed', `worktree ${entry.worktree_path} reappeared after being accepted as absent; refusing to prune or delete its branch`);
|
|
1412
|
+
continue; // #2852: isolate — the merge already landed on repoRoot
|
|
1413
|
+
}
|
|
1414
|
+
// Prune only: it clears the admin entry when the directory really is gone.
|
|
1415
|
+
const prune = execGit(['worktree', 'prune'], { cwd: plan.repoRoot });
|
|
1416
|
+
if (!gitResultOk(prune)) {
|
|
1417
|
+
blockEntry(result, 'worktree_remove_failed', prune?.stderr || '');
|
|
1418
|
+
continue; // #2852: isolate — the merge already landed on repoRoot
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
let remove = execGit(['worktree', 'remove', entry.worktree_path, '--force'], { cwd: plan.repoRoot });
|
|
1423
|
+
if (!gitResultOk(remove)) {
|
|
1424
|
+
// Locked worktrees require unlock before remove (or --force --force).
|
|
1425
|
+
// Attempt: git worktree unlock <path> (ignore failure — already unlocked is ok)
|
|
1426
|
+
// then retry git worktree remove --force. (#3707)
|
|
1427
|
+
execGit(['worktree', 'unlock', entry.worktree_path], { cwd: plan.repoRoot });
|
|
1428
|
+
remove = execGit(['worktree', 'remove', entry.worktree_path, '--force'], { cwd: plan.repoRoot });
|
|
1429
|
+
}
|
|
1430
|
+
if (!gitResultOk(remove)) {
|
|
1431
|
+
// #4415: a remove that fails only because the path is already gone ("is
|
|
1432
|
+
// not a working tree") used to surface as `worktree_remove_failed` AFTER
|
|
1433
|
+
// the merge had already landed, leaving the branch undeleted and the
|
|
1434
|
+
// operator to run `git worktree prune` + `git branch -D` + `rm -rf` by
|
|
1435
|
+
// hand every wave. What is actually left behind is the admin entry under
|
|
1436
|
+
// .git/worktrees, which is exactly what `prune` clears. Staleness is
|
|
1437
|
+
// re-read here rather than reusing the branch-step answer: the harness
|
|
1438
|
+
// removes worktrees on subagent completion, which can land in between.
|
|
1439
|
+
// Asked of git, so a `remove` that failed for any reason OTHER than the
|
|
1440
|
+
// path being gone — a lock this did not clear, a permissions error — still
|
|
1441
|
+
// blocks instead of being tidied away by a prune.
|
|
1442
|
+
if (!absentAndIdentified(entry.worktree_path, entry.branch)) {
|
|
1443
|
+
blockEntry(result, 'worktree_remove_failed', remove?.stderr || '');
|
|
1444
|
+
// #2852: isolate — the merge already landed on repoRoot; only this entry's
|
|
1445
|
+
// worktree/branch teardown is affected.
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
// NB: `git worktree prune` is repository-wide maintenance, not an
|
|
1449
|
+
// entry-scoped operation — it clears every stale admin entry, not only this
|
|
1450
|
+
// one. NOT harmless, and an earlier version of this comment was wrong to
|
|
1451
|
+
// say so (Codex review round 4): because identity now comes from the
|
|
1452
|
+
// registration, a prune here destroys the evidence later entries in the same
|
|
1453
|
+
// wave need. That is why the identity read is a snapshot taken before the
|
|
1454
|
+
// loop; see `worktreeListSnapshot`.
|
|
1455
|
+
const prune = execGit(['worktree', 'prune'], { cwd: plan.repoRoot });
|
|
1456
|
+
if (!gitResultOk(prune)) {
|
|
1457
|
+
blockEntry(result, 'worktree_remove_failed', prune?.stderr || '');
|
|
1458
|
+
continue; // #2852: isolate
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
996
1461
|
}
|
|
997
1462
|
const branchDelete = execGit(['branch', '-D', entry.branch], { cwd: plan.repoRoot });
|
|
998
1463
|
if (!gitResultOk(branchDelete)) {
|
|
@@ -1946,6 +2411,269 @@ function cmdWorktreeReapOrphans(cwd, deps = {}) {
|
|
|
1946
2411
|
}
|
|
1947
2412
|
write(`${JSON.stringify({ ok: true, reaped: result.filter((r) => r.status === 'reaped').length, entries: result }, null, 2)}\n`);
|
|
1948
2413
|
}
|
|
2414
|
+
// ─── Worker lifecycle records (#4624) ────────────────────────────────────────
|
|
2415
|
+
// Durable per-worker launch/terminal state for the orchestrator-worktree
|
|
2416
|
+
// backend. The dispatch fragment spawns external executor processes with a
|
|
2417
|
+
// bare background `wait`; when the orchestrator's turn ends before a worker
|
|
2418
|
+
// finishes, nothing records the launch or guarantees reconciliation, and a
|
|
2419
|
+
// resumed session has no state to recover — it re-derives everything from
|
|
2420
|
+
// manual PID/log discovery. These records persist the launch identity, the
|
|
2421
|
+
// result location, and the terminal outcome as a small JSON file beside the
|
|
2422
|
+
// worktree (NOT inside it — cleanup removes the worktree; the record must
|
|
2423
|
+
// survive it), so `worker-status` can answer "who was dispatched, is it
|
|
2424
|
+
// still running, did it finish its artifacts, does it need reconciliation"
|
|
2425
|
+
// deterministically on resume.
|
|
2426
|
+
/** Path of the lifecycle record for a worktree: a SIBLING of the worktree dir. */
|
|
2427
|
+
function workerRecordPath(worktreePath) {
|
|
2428
|
+
return `${worktreePath}.worker.json`;
|
|
2429
|
+
}
|
|
2430
|
+
const RENAME_RETRY_ERRNOS = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
2431
|
+
// local/require-fs-op-fallback: a concurrent reader or antivirus scanner can
|
|
2432
|
+
// transiently hold the rename target open on Windows (DEFECT.WINDOWS-FS-OPS) —
|
|
2433
|
+
// bounded retry on the transient errnos, per the house pattern.
|
|
2434
|
+
function renameWithRetry(tmp, target) {
|
|
2435
|
+
let lastErr;
|
|
2436
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
2437
|
+
try {
|
|
2438
|
+
node_fs_1.default.renameSync(tmp, target);
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
catch (err) {
|
|
2442
|
+
lastErr = err;
|
|
2443
|
+
const code = err.code;
|
|
2444
|
+
if (code && RENAME_RETRY_ERRNOS.has(code) && attempt < 3) {
|
|
2445
|
+
const until = Date.now() + 25 * (attempt + 1);
|
|
2446
|
+
while (Date.now() < until) { /* bounded spin: transient locks clear in <100ms */ }
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
throw err;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
throw lastErr;
|
|
2453
|
+
}
|
|
2454
|
+
function writeWorkerRecord(recordPath, record, deps = {}) {
|
|
2455
|
+
const writeFile = deps.writeFile || ((p, d) => node_fs_1.default.writeFileSync(p, d));
|
|
2456
|
+
const tmp = `${recordPath}.tmp-${process.pid}`;
|
|
2457
|
+
writeFile(tmp, `${JSON.stringify(record, null, 2)}\n`);
|
|
2458
|
+
renameWithRetry(tmp, recordPath);
|
|
2459
|
+
}
|
|
2460
|
+
function cmdWorktreeWorkerRecord(cwd, args = [], deps = {}) {
|
|
2461
|
+
const flag = (name) => {
|
|
2462
|
+
const i = args.indexOf(name);
|
|
2463
|
+
if (i < 0 || i + 1 >= args.length)
|
|
2464
|
+
return '';
|
|
2465
|
+
return args[i + 1];
|
|
2466
|
+
};
|
|
2467
|
+
const write = deps.write || ((s) => process.stdout.write(s));
|
|
2468
|
+
const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
|
|
2469
|
+
const worktreePath = flag('--path');
|
|
2470
|
+
const pidText = flag('--pid');
|
|
2471
|
+
const plan = flag('--plan');
|
|
2472
|
+
const summaryPath = flag('--summary-path');
|
|
2473
|
+
const logFile = flag('--log-file');
|
|
2474
|
+
if (!worktreePath || !pidText || !plan || !summaryPath) {
|
|
2475
|
+
writeErr('Usage: worktree worker-record --path <worktree> --pid <pid> --plan <plan_number> --summary-path <path> [--log-file <path>]\n');
|
|
2476
|
+
process.exitCode = 2;
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
const pid = Number(pidText);
|
|
2480
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
2481
|
+
writeErr(`[gsd] worktree.worker-record: invalid --pid: ${pidText}\n`);
|
|
2482
|
+
process.exitCode = 2;
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const resolvedWorktree = node_path_1.default.resolve(cwd, worktreePath);
|
|
2486
|
+
const recordPath = workerRecordPath(resolvedWorktree);
|
|
2487
|
+
const readFile = deps.readFile || ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
|
|
2488
|
+
let existing = null;
|
|
2489
|
+
try {
|
|
2490
|
+
existing = JSON.parse(readFile(recordPath));
|
|
2491
|
+
}
|
|
2492
|
+
catch { /* no record yet — first dispatch for this worktree */ }
|
|
2493
|
+
if (existing && existing.state === 'running') {
|
|
2494
|
+
// Duplicate-dispatch guard (#4624): a running record means a resumed
|
|
2495
|
+
// session re-entered the dispatch step. The worker must be reconciled
|
|
2496
|
+
// (worker-status → completion-reconciliation), never re-spawned.
|
|
2497
|
+
const hint = 'A worker is already recorded RUNNING for this worktree. Reconcile it (worktree worker-status, then execute-phase/steps/completion-reconciliation.md) before any new dispatch — never re-dispatch a recorded plan.';
|
|
2498
|
+
writeErr(`[gsd] worktree.worker-record: already_running — ${hint}\n`);
|
|
2499
|
+
write(`${JSON.stringify({ ok: false, reason: 'already_running', hint, record: existing }, null, 2)}\n`);
|
|
2500
|
+
process.exitCode = 1;
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const record = {
|
|
2504
|
+
agentId: node_path_1.default.basename(resolvedWorktree),
|
|
2505
|
+
pid,
|
|
2506
|
+
plan,
|
|
2507
|
+
worktreePath: resolvedWorktree,
|
|
2508
|
+
summaryPath: node_path_1.default.resolve(cwd, summaryPath),
|
|
2509
|
+
logFile: logFile ? node_path_1.default.resolve(cwd, logFile) : '',
|
|
2510
|
+
startedAt: new Date().toISOString(),
|
|
2511
|
+
state: 'running',
|
|
2512
|
+
exitCode: null,
|
|
2513
|
+
note: '',
|
|
2514
|
+
completedAt: null,
|
|
2515
|
+
};
|
|
2516
|
+
try {
|
|
2517
|
+
writeWorkerRecord(recordPath, record, deps);
|
|
2518
|
+
}
|
|
2519
|
+
catch (err) {
|
|
2520
|
+
writeErr(`[gsd] worktree.worker-record: write_failed — ${err.message}\n`);
|
|
2521
|
+
write(`${JSON.stringify({ ok: false, reason: 'write_failed', error: err.message }, null, 2)}\n`);
|
|
2522
|
+
process.exitCode = 1;
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
write(`${JSON.stringify({ ok: true, record }, null, 2)}\n`);
|
|
2526
|
+
}
|
|
2527
|
+
function workerStatusView(record, deps = {}) {
|
|
2528
|
+
// Only ESRCH is dead (defaultIsPidAlive contract): a verdict feeding a
|
|
2529
|
+
// merge/reconcile decision must fail toward ALIVE on unrecognized errnos.
|
|
2530
|
+
const pidAlive = deps.isPidAlive || defaultIsPidAlive;
|
|
2531
|
+
const exists = deps.existsSync || node_fs_1.default.existsSync;
|
|
2532
|
+
const summaryExists = exists(record.summaryPath);
|
|
2533
|
+
const alive = record.state === 'running' && pidAlive(record.pid);
|
|
2534
|
+
return {
|
|
2535
|
+
agentId: record.agentId,
|
|
2536
|
+
pid: record.pid,
|
|
2537
|
+
plan: record.plan,
|
|
2538
|
+
worktreePath: record.worktreePath,
|
|
2539
|
+
summaryPath: record.summaryPath,
|
|
2540
|
+
logFile: record.logFile,
|
|
2541
|
+
state: record.state,
|
|
2542
|
+
exitCode: record.exitCode,
|
|
2543
|
+
note: record.note,
|
|
2544
|
+
startedAt: record.startedAt,
|
|
2545
|
+
completedAt: record.completedAt,
|
|
2546
|
+
pidAlive: record.state === 'running' ? alive : null,
|
|
2547
|
+
summaryExists,
|
|
2548
|
+
needsReconciliation: record.state === 'running' && !alive,
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
function readWorkerRecordsFromRoot(root, readFile, readdir) {
|
|
2552
|
+
let entries = [];
|
|
2553
|
+
try {
|
|
2554
|
+
entries = readdir(root).filter((f) => f.endsWith('.worker.json'));
|
|
2555
|
+
}
|
|
2556
|
+
catch { /* root missing — no workers ever recorded */
|
|
2557
|
+
return [];
|
|
2558
|
+
}
|
|
2559
|
+
const out = [];
|
|
2560
|
+
for (const entry of entries) {
|
|
2561
|
+
const recordPath = node_path_1.default.join(root, entry);
|
|
2562
|
+
try {
|
|
2563
|
+
out.push({ path: recordPath, record: JSON.parse(readFile(recordPath)) });
|
|
2564
|
+
}
|
|
2565
|
+
catch { // a torn/partial record must not hide the others
|
|
2566
|
+
out.push({ path: recordPath, record: null });
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
return out;
|
|
2570
|
+
}
|
|
2571
|
+
function cmdWorktreeWorkerStatus(cwd, args = [], deps = {}) {
|
|
2572
|
+
const flag = (name) => {
|
|
2573
|
+
const i = args.indexOf(name);
|
|
2574
|
+
if (i < 0 || i + 1 >= args.length)
|
|
2575
|
+
return '';
|
|
2576
|
+
return args[i + 1];
|
|
2577
|
+
};
|
|
2578
|
+
const write = deps.write || ((s) => process.stdout.write(s));
|
|
2579
|
+
const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
|
|
2580
|
+
const worktreePath = flag('--path');
|
|
2581
|
+
const root = flag('--root');
|
|
2582
|
+
if (!worktreePath === !root) { // exactly one of the two
|
|
2583
|
+
writeErr('Usage: worktree worker-status (--path <worktree> | --root <worktrees-dir>)\n');
|
|
2584
|
+
process.exitCode = 2;
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const readFile = deps.readFile || ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
|
|
2588
|
+
const readdir = deps.readdir || ((p) => node_fs_1.default.readdirSync(p));
|
|
2589
|
+
const views = [];
|
|
2590
|
+
if (worktreePath) {
|
|
2591
|
+
const recordPath = workerRecordPath(node_path_1.default.resolve(cwd, worktreePath));
|
|
2592
|
+
let record = null;
|
|
2593
|
+
let unreadable = false;
|
|
2594
|
+
try {
|
|
2595
|
+
record = JSON.parse(readFile(recordPath));
|
|
2596
|
+
}
|
|
2597
|
+
catch {
|
|
2598
|
+
// A record that exists but cannot be parsed is NOT "never dispatched" —
|
|
2599
|
+
// conflating the two invites a re-dispatch. Surface it as a candidate.
|
|
2600
|
+
unreadable = node_fs_1.default.existsSync(recordPath);
|
|
2601
|
+
}
|
|
2602
|
+
if (unreadable) {
|
|
2603
|
+
// exists but unparseable — a reconciliation candidate, never "never dispatched"
|
|
2604
|
+
views.push({ state: 'unreadable', needsReconciliation: true, recordPath });
|
|
2605
|
+
}
|
|
2606
|
+
else if (record) {
|
|
2607
|
+
views.push(workerStatusView(record, deps));
|
|
2608
|
+
}
|
|
2609
|
+
else {
|
|
2610
|
+
// no record file: genuinely never dispatched (found:false)
|
|
2611
|
+
write(`${JSON.stringify({ ok: true, found: false, workers: [] }, null, 2)}\n`);
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
else {
|
|
2616
|
+
for (const { record } of readWorkerRecordsFromRoot(node_path_1.default.resolve(cwd, root), readFile, readdir)) {
|
|
2617
|
+
if (!record) { // torn record is itself a reconciliation candidate
|
|
2618
|
+
views.push({ state: 'unreadable', needsReconciliation: true });
|
|
2619
|
+
continue;
|
|
2620
|
+
}
|
|
2621
|
+
views.push(workerStatusView(record, deps));
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
write(`${JSON.stringify({ ok: true, found: true, workers: views }, null, 2)}\n`);
|
|
2625
|
+
}
|
|
2626
|
+
function cmdWorktreeWorkerComplete(cwd, args = [], deps = {}) {
|
|
2627
|
+
const flag = (name) => {
|
|
2628
|
+
const i = args.indexOf(name);
|
|
2629
|
+
if (i < 0 || i + 1 >= args.length)
|
|
2630
|
+
return '';
|
|
2631
|
+
return args[i + 1];
|
|
2632
|
+
};
|
|
2633
|
+
const write = deps.write || ((s) => process.stdout.write(s));
|
|
2634
|
+
const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
|
|
2635
|
+
const worktreePath = flag('--path');
|
|
2636
|
+
const exitText = flag('--exit-code');
|
|
2637
|
+
const note = flag('--note');
|
|
2638
|
+
if (!worktreePath || !exitText) {
|
|
2639
|
+
writeErr('Usage: worktree worker-complete --path <worktree> --exit-code <n> [--note <recovery info>]\n');
|
|
2640
|
+
process.exitCode = 2;
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
const exitCode = Number(exitText);
|
|
2644
|
+
if (!Number.isInteger(exitCode)) {
|
|
2645
|
+
writeErr(`[gsd] worktree.worker-complete: invalid --exit-code: ${exitText}\n`);
|
|
2646
|
+
process.exitCode = 2;
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
const recordPath = workerRecordPath(node_path_1.default.resolve(cwd, worktreePath));
|
|
2650
|
+
const readFile = deps.readFile || ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
|
|
2651
|
+
let record;
|
|
2652
|
+
try {
|
|
2653
|
+
record = JSON.parse(readFile(recordPath));
|
|
2654
|
+
}
|
|
2655
|
+
catch (err) {
|
|
2656
|
+
writeErr(`[gsd] worktree.worker-complete: no_record — ${err.message}\n`);
|
|
2657
|
+
write(`${JSON.stringify({ ok: false, reason: 'no_record', error: err.message }, null, 2)}\n`);
|
|
2658
|
+
process.exitCode = 1;
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
const alreadyComplete = record.state === 'complete';
|
|
2662
|
+
record.state = 'complete';
|
|
2663
|
+
record.exitCode = exitCode;
|
|
2664
|
+
record.note = note || record.note || '';
|
|
2665
|
+
record.completedAt = record.completedAt || new Date().toISOString();
|
|
2666
|
+
try {
|
|
2667
|
+
writeWorkerRecord(recordPath, record, deps);
|
|
2668
|
+
}
|
|
2669
|
+
catch (err) {
|
|
2670
|
+
writeErr(`[gsd] worktree.worker-complete: write_failed — ${err.message}\n`);
|
|
2671
|
+
write(`${JSON.stringify({ ok: false, reason: 'write_failed', error: err.message }, null, 2)}\n`);
|
|
2672
|
+
process.exitCode = 1;
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
write(`${JSON.stringify({ ok: true, alreadyComplete, record }, null, 2)}\n`);
|
|
2676
|
+
}
|
|
1949
2677
|
// Unused exports kept for API compatibility
|
|
1950
2678
|
void parseWorktreeListPaths;
|
|
1951
2679
|
// ─── Moved from core.cjs (ADR-857 T0 #1268 rehome-core-squatters) ─────────────
|
|
@@ -2012,6 +2740,7 @@ module.exports = {
|
|
|
2012
2740
|
planWorktreeWaveCleanup,
|
|
2013
2741
|
executeWorktreeWaveCleanupPlan,
|
|
2014
2742
|
WAVE_CLEANUP_WARNING,
|
|
2743
|
+
DEFAULT_MERGE_TIMEOUT_MS,
|
|
2015
2744
|
planWaveScopeConformance,
|
|
2016
2745
|
isSummaryArtifactRelPath,
|
|
2017
2746
|
cmdWorktreeCleanupWave,
|
|
@@ -2022,6 +2751,10 @@ module.exports = {
|
|
|
2022
2751
|
cmdWorktreeCreate,
|
|
2023
2752
|
reapOrphanWorktrees,
|
|
2024
2753
|
cmdWorktreeReapOrphans,
|
|
2754
|
+
workerRecordPath,
|
|
2755
|
+
cmdWorktreeWorkerRecord,
|
|
2756
|
+
cmdWorktreeWorkerStatus,
|
|
2757
|
+
cmdWorktreeWorkerComplete,
|
|
2025
2758
|
resolveWorktreeRoot,
|
|
2026
2759
|
pruneOrphanedWorktrees,
|
|
2027
2760
|
};
|