@opengsd/gsd-core 1.2.0-rc.1

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.
Files changed (503) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja-JP.md +870 -0
  3. package/README.ko-KR.md +861 -0
  4. package/README.md +301 -0
  5. package/README.pt-BR.md +492 -0
  6. package/README.zh-CN.md +842 -0
  7. package/agents/gsd-advisor-researcher.md +127 -0
  8. package/agents/gsd-ai-researcher.md +133 -0
  9. package/agents/gsd-assumptions-analyzer.md +105 -0
  10. package/agents/gsd-code-fixer.md +668 -0
  11. package/agents/gsd-code-reviewer.md +387 -0
  12. package/agents/gsd-codebase-mapper.md +853 -0
  13. package/agents/gsd-debug-session-manager.md +314 -0
  14. package/agents/gsd-debugger.md +1452 -0
  15. package/agents/gsd-doc-classifier.md +168 -0
  16. package/agents/gsd-doc-synthesizer.md +204 -0
  17. package/agents/gsd-doc-verifier.md +217 -0
  18. package/agents/gsd-doc-writer.md +615 -0
  19. package/agents/gsd-domain-researcher.md +153 -0
  20. package/agents/gsd-eval-auditor.md +191 -0
  21. package/agents/gsd-eval-planner.md +154 -0
  22. package/agents/gsd-executor.md +772 -0
  23. package/agents/gsd-framework-selector.md +160 -0
  24. package/agents/gsd-integration-checker.md +470 -0
  25. package/agents/gsd-intel-updater.md +342 -0
  26. package/agents/gsd-nyquist-auditor.md +203 -0
  27. package/agents/gsd-pattern-mapper.md +335 -0
  28. package/agents/gsd-phase-researcher.md +928 -0
  29. package/agents/gsd-plan-checker.md +978 -0
  30. package/agents/gsd-planner.md +1218 -0
  31. package/agents/gsd-project-researcher.md +677 -0
  32. package/agents/gsd-research-synthesizer.md +255 -0
  33. package/agents/gsd-roadmapper.md +688 -0
  34. package/agents/gsd-security-auditor.md +155 -0
  35. package/agents/gsd-ui-auditor.md +495 -0
  36. package/agents/gsd-ui-checker.md +309 -0
  37. package/agents/gsd-ui-researcher.md +380 -0
  38. package/agents/gsd-user-profiler.md +171 -0
  39. package/agents/gsd-verifier.md +917 -0
  40. package/bin/install.js +10936 -0
  41. package/bin/lib/ui-safety-gate.cjs +107 -0
  42. package/commands/gsd/add-tests.md +42 -0
  43. package/commands/gsd/ai-integration-phase.md +37 -0
  44. package/commands/gsd/audit-fix.md +34 -0
  45. package/commands/gsd/audit-milestone.md +37 -0
  46. package/commands/gsd/audit-uat.md +24 -0
  47. package/commands/gsd/autonomous.md +46 -0
  48. package/commands/gsd/capture.md +62 -0
  49. package/commands/gsd/cleanup.md +24 -0
  50. package/commands/gsd/code-review.md +59 -0
  51. package/commands/gsd/complete-milestone.md +143 -0
  52. package/commands/gsd/config.md +56 -0
  53. package/commands/gsd/debug.md +52 -0
  54. package/commands/gsd/discuss-phase.md +76 -0
  55. package/commands/gsd/docs-update.md +49 -0
  56. package/commands/gsd/eval-review.md +33 -0
  57. package/commands/gsd/execute-phase.md +64 -0
  58. package/commands/gsd/explore.md +27 -0
  59. package/commands/gsd/extract-learnings.md +23 -0
  60. package/commands/gsd/fast.md +31 -0
  61. package/commands/gsd/forensics.md +57 -0
  62. package/commands/gsd/graphify.md +199 -0
  63. package/commands/gsd/health.md +31 -0
  64. package/commands/gsd/help.md +28 -0
  65. package/commands/gsd/import.md +41 -0
  66. package/commands/gsd/inbox.md +39 -0
  67. package/commands/gsd/ingest-docs.md +42 -0
  68. package/commands/gsd/manager.md +45 -0
  69. package/commands/gsd/map-codebase.md +83 -0
  70. package/commands/gsd/milestone-summary.md +51 -0
  71. package/commands/gsd/mvp-phase.md +45 -0
  72. package/commands/gsd/new-milestone.md +45 -0
  73. package/commands/gsd/new-project.md +47 -0
  74. package/commands/gsd/ns-context.md +23 -0
  75. package/commands/gsd/ns-ideate.md +24 -0
  76. package/commands/gsd/ns-manage.md +29 -0
  77. package/commands/gsd/ns-project.md +22 -0
  78. package/commands/gsd/ns-review.md +26 -0
  79. package/commands/gsd/ns-workflow.md +28 -0
  80. package/commands/gsd/pause-work.md +43 -0
  81. package/commands/gsd/phase.md +56 -0
  82. package/commands/gsd/plan-phase.md +62 -0
  83. package/commands/gsd/plan-review-convergence.md +59 -0
  84. package/commands/gsd/pr-branch.md +26 -0
  85. package/commands/gsd/profile-user.md +46 -0
  86. package/commands/gsd/progress.md +47 -0
  87. package/commands/gsd/quick.md +174 -0
  88. package/commands/gsd/resume-work.md +30 -0
  89. package/commands/gsd/review-backlog.md +63 -0
  90. package/commands/gsd/review.md +41 -0
  91. package/commands/gsd/secure-phase.md +36 -0
  92. package/commands/gsd/settings.md +29 -0
  93. package/commands/gsd/ship.md +24 -0
  94. package/commands/gsd/sketch.md +60 -0
  95. package/commands/gsd/spec-phase.md +63 -0
  96. package/commands/gsd/spike.md +57 -0
  97. package/commands/gsd/stats.md +19 -0
  98. package/commands/gsd/surface.md +155 -0
  99. package/commands/gsd/thread.md +24 -0
  100. package/commands/gsd/ui-phase.md +35 -0
  101. package/commands/gsd/ui-review.md +33 -0
  102. package/commands/gsd/ultraplan-phase.md +34 -0
  103. package/commands/gsd/undo.md +35 -0
  104. package/commands/gsd/update.md +48 -0
  105. package/commands/gsd/validate-phase.md +36 -0
  106. package/commands/gsd/verify-work.md +39 -0
  107. package/commands/gsd/workspace.md +52 -0
  108. package/commands/gsd/workstreams.md +70 -0
  109. package/get-shit-done/bin/check-latest-version.cjs +106 -0
  110. package/get-shit-done/bin/gsd-tools.cjs +1676 -0
  111. package/get-shit-done/bin/lib/active-workstream-store.cjs +302 -0
  112. package/get-shit-done/bin/lib/adr-parser.cjs +394 -0
  113. package/get-shit-done/bin/lib/agent-command-router.cjs +65 -0
  114. package/get-shit-done/bin/lib/artifacts.cjs +53 -0
  115. package/get-shit-done/bin/lib/audit.cjs +755 -0
  116. package/get-shit-done/bin/lib/check-command-router.cjs +333 -0
  117. package/get-shit-done/bin/lib/cjs-command-router-adapter.cjs +118 -0
  118. package/get-shit-done/bin/lib/clock.cjs +96 -0
  119. package/get-shit-done/bin/lib/clusters.cjs +135 -0
  120. package/get-shit-done/bin/lib/code-review-flags.cjs +74 -0
  121. package/get-shit-done/bin/lib/command-aliases.cjs +815 -0
  122. package/get-shit-done/bin/lib/command-arg-projection.cjs +62 -0
  123. package/get-shit-done/bin/lib/command-routing-hub.cjs +388 -0
  124. package/get-shit-done/bin/lib/commands.cjs +1188 -0
  125. package/get-shit-done/bin/lib/config-schema.cjs +31 -0
  126. package/get-shit-done/bin/lib/config.cjs +728 -0
  127. package/get-shit-done/bin/lib/configuration.cjs +248 -0
  128. package/get-shit-done/bin/lib/context-utilization.cjs +47 -0
  129. package/get-shit-done/bin/lib/core.cjs +2121 -0
  130. package/get-shit-done/bin/lib/decisions.cjs +116 -0
  131. package/get-shit-done/bin/lib/docs.cjs +270 -0
  132. package/get-shit-done/bin/lib/drift.cjs +388 -0
  133. package/get-shit-done/bin/lib/fallow-runner.cjs +109 -0
  134. package/get-shit-done/bin/lib/frontmatter.cjs +389 -0
  135. package/get-shit-done/bin/lib/gap-checker.cjs +205 -0
  136. package/get-shit-done/bin/lib/graphify.cjs +592 -0
  137. package/get-shit-done/bin/lib/gsd2-import.cjs +514 -0
  138. package/get-shit-done/bin/lib/init-command-router.cjs +58 -0
  139. package/get-shit-done/bin/lib/init.cjs +2112 -0
  140. package/get-shit-done/bin/lib/install-profiles.cjs +603 -0
  141. package/get-shit-done/bin/lib/installer-migration-authoring.cjs +117 -0
  142. package/get-shit-done/bin/lib/installer-migration-report.cjs +354 -0
  143. package/get-shit-done/bin/lib/installer-migrations/000-first-time-baseline.cjs +220 -0
  144. package/get-shit-done/bin/lib/installer-migrations/001-legacy-orphan-files.cjs +41 -0
  145. package/get-shit-done/bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs +80 -0
  146. package/get-shit-done/bin/lib/installer-migrations.cjs +778 -0
  147. package/get-shit-done/bin/lib/intel.cjs +708 -0
  148. package/get-shit-done/bin/lib/learnings.cjs +421 -0
  149. package/get-shit-done/bin/lib/milestone.cjs +314 -0
  150. package/get-shit-done/bin/lib/model-catalog.cjs +212 -0
  151. package/get-shit-done/bin/lib/model-profiles.cjs +31 -0
  152. package/get-shit-done/bin/lib/observability/event.cjs +82 -0
  153. package/get-shit-done/bin/lib/observability/logger.cjs +174 -0
  154. package/get-shit-done/bin/lib/observability/redaction.cjs +50 -0
  155. package/get-shit-done/bin/lib/package-identity.cjs +31 -0
  156. package/get-shit-done/bin/lib/phase-command-router.cjs +191 -0
  157. package/get-shit-done/bin/lib/phase-lifecycle.cjs +80 -0
  158. package/get-shit-done/bin/lib/phase.cjs +1607 -0
  159. package/get-shit-done/bin/lib/phases-command-router.cjs +39 -0
  160. package/get-shit-done/bin/lib/plan-scan.cjs +97 -0
  161. package/get-shit-done/bin/lib/planning-workspace.cjs +238 -0
  162. package/get-shit-done/bin/lib/profile-output.cjs +1141 -0
  163. package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
  164. package/get-shit-done/bin/lib/project-root.cjs +112 -0
  165. package/get-shit-done/bin/lib/prompt-budget.cjs +399 -0
  166. package/get-shit-done/bin/lib/review-reviewer-selection.cjs +125 -0
  167. package/get-shit-done/bin/lib/roadmap-command-router.cjs +28 -0
  168. package/get-shit-done/bin/lib/roadmap.cjs +650 -0
  169. package/get-shit-done/bin/lib/runtime-artifact-layout.cjs +301 -0
  170. package/get-shit-done/bin/lib/runtime-homes.cjs +222 -0
  171. package/get-shit-done/bin/lib/runtime-name-policy.cjs +83 -0
  172. package/get-shit-done/bin/lib/runtime-slash.cjs +112 -0
  173. package/get-shit-done/bin/lib/schema-detect.cjs +165 -0
  174. package/get-shit-done/bin/lib/secrets.cjs +32 -0
  175. package/get-shit-done/bin/lib/security.cjs +600 -0
  176. package/get-shit-done/bin/lib/semver-compare.cjs +35 -0
  177. package/get-shit-done/bin/lib/shell-command-projection.cjs +500 -0
  178. package/get-shit-done/bin/lib/state-command-router.cjs +252 -0
  179. package/get-shit-done/bin/lib/state-document.cjs +263 -0
  180. package/get-shit-done/bin/lib/state.cjs +2038 -0
  181. package/get-shit-done/bin/lib/surface.cjs +470 -0
  182. package/get-shit-done/bin/lib/task-command-router.cjs +81 -0
  183. package/get-shit-done/bin/lib/template.cjs +228 -0
  184. package/get-shit-done/bin/lib/uat.cjs +289 -0
  185. package/get-shit-done/bin/lib/update-context.cjs +209 -0
  186. package/get-shit-done/bin/lib/validate-command-router.cjs +83 -0
  187. package/get-shit-done/bin/lib/validate.cjs +92 -0
  188. package/get-shit-done/bin/lib/verify-command-router.cjs +40 -0
  189. package/get-shit-done/bin/lib/verify.cjs +1511 -0
  190. package/get-shit-done/bin/lib/workstream-inventory-builder.cjs +74 -0
  191. package/get-shit-done/bin/lib/workstream-inventory.cjs +146 -0
  192. package/get-shit-done/bin/lib/workstream-name-policy.cjs +94 -0
  193. package/get-shit-done/bin/lib/workstream.cjs +389 -0
  194. package/get-shit-done/bin/lib/worktree-safety.cjs +985 -0
  195. package/get-shit-done/bin/shared/config-defaults.manifest.json +97 -0
  196. package/get-shit-done/bin/shared/config-schema.manifest.json +175 -0
  197. package/get-shit-done/bin/shared/model-catalog.json +122 -0
  198. package/get-shit-done/bin/shared/runtime-aliases.manifest.json +75 -0
  199. package/get-shit-done/bin/verify-reapply-patches.cjs +352 -0
  200. package/get-shit-done/contexts/dev.md +21 -0
  201. package/get-shit-done/contexts/research.md +22 -0
  202. package/get-shit-done/contexts/review.md +23 -0
  203. package/get-shit-done/references/agent-contracts.md +79 -0
  204. package/get-shit-done/references/ai-evals.md +156 -0
  205. package/get-shit-done/references/ai-frameworks.md +186 -0
  206. package/get-shit-done/references/artifact-types.md +131 -0
  207. package/get-shit-done/references/autonomous-smart-discuss.md +277 -0
  208. package/get-shit-done/references/checkpoints.md +814 -0
  209. package/get-shit-done/references/common-bug-patterns.md +114 -0
  210. package/get-shit-done/references/context-budget.md +85 -0
  211. package/get-shit-done/references/continuation-format.md +253 -0
  212. package/get-shit-done/references/debugger-philosophy.md +76 -0
  213. package/get-shit-done/references/decimal-phase-calculation.md +64 -0
  214. package/get-shit-done/references/doc-conflict-engine.md +91 -0
  215. package/get-shit-done/references/domain-probes.md +125 -0
  216. package/get-shit-done/references/execute-mvp-tdd.md +81 -0
  217. package/get-shit-done/references/executor-examples.md +110 -0
  218. package/get-shit-done/references/few-shot-examples/plan-checker.md +73 -0
  219. package/get-shit-done/references/few-shot-examples/verifier.md +109 -0
  220. package/get-shit-done/references/gate-prompts.md +100 -0
  221. package/get-shit-done/references/gates.md +70 -0
  222. package/get-shit-done/references/git-integration.md +298 -0
  223. package/get-shit-done/references/git-planning-commit.md +40 -0
  224. package/get-shit-done/references/ios-scaffold.md +123 -0
  225. package/get-shit-done/references/mandatory-initial-read.md +2 -0
  226. package/get-shit-done/references/model-profile-resolution.md +38 -0
  227. package/get-shit-done/references/model-profiles.md +245 -0
  228. package/get-shit-done/references/mvp-concepts.md +49 -0
  229. package/get-shit-done/references/phase-argument-parsing.md +61 -0
  230. package/get-shit-done/references/planner-antipatterns.md +89 -0
  231. package/get-shit-done/references/planner-chunked.md +49 -0
  232. package/get-shit-done/references/planner-gap-closure.md +62 -0
  233. package/get-shit-done/references/planner-graphify-auto-update.md +67 -0
  234. package/get-shit-done/references/planner-human-verify-mode.md +57 -0
  235. package/get-shit-done/references/planner-interface-context.md +62 -0
  236. package/get-shit-done/references/planner-mvp-mode.md +53 -0
  237. package/get-shit-done/references/planner-reviews.md +39 -0
  238. package/get-shit-done/references/planner-revision.md +87 -0
  239. package/get-shit-done/references/planner-source-audit.md +73 -0
  240. package/get-shit-done/references/planning-config.md +471 -0
  241. package/get-shit-done/references/project-skills-discovery.md +19 -0
  242. package/get-shit-done/references/questioning.md +162 -0
  243. package/get-shit-done/references/revision-loop.md +97 -0
  244. package/get-shit-done/references/scout-codebase.md +51 -0
  245. package/get-shit-done/references/skeleton-template.md +48 -0
  246. package/get-shit-done/references/sketch-interactivity.md +41 -0
  247. package/get-shit-done/references/sketch-theme-system.md +94 -0
  248. package/get-shit-done/references/sketch-tooling.md +45 -0
  249. package/get-shit-done/references/sketch-variant-patterns.md +81 -0
  250. package/get-shit-done/references/spidr-splitting.md +69 -0
  251. package/get-shit-done/references/tdd.md +330 -0
  252. package/get-shit-done/references/thinking-models-debug.md +44 -0
  253. package/get-shit-done/references/thinking-models-execution.md +50 -0
  254. package/get-shit-done/references/thinking-models-planning.md +62 -0
  255. package/get-shit-done/references/thinking-models-research.md +50 -0
  256. package/get-shit-done/references/thinking-models-verification.md +55 -0
  257. package/get-shit-done/references/thinking-partner.md +96 -0
  258. package/get-shit-done/references/ui-brand.md +160 -0
  259. package/get-shit-done/references/universal-anti-patterns.md +63 -0
  260. package/get-shit-done/references/user-profiling.md +681 -0
  261. package/get-shit-done/references/user-story-template.md +58 -0
  262. package/get-shit-done/references/verification-overrides.md +227 -0
  263. package/get-shit-done/references/verification-patterns.md +612 -0
  264. package/get-shit-done/references/verify-mvp-mode.md +85 -0
  265. package/get-shit-done/references/workstream-flag.md +111 -0
  266. package/get-shit-done/references/worktree-path-safety.md +89 -0
  267. package/get-shit-done/templates/AI-SPEC.md +246 -0
  268. package/get-shit-done/templates/DEBUG.md +169 -0
  269. package/get-shit-done/templates/README.md +77 -0
  270. package/get-shit-done/templates/SECURITY.md +61 -0
  271. package/get-shit-done/templates/UAT.md +265 -0
  272. package/get-shit-done/templates/UI-SPEC.md +100 -0
  273. package/get-shit-done/templates/VALIDATION.md +76 -0
  274. package/get-shit-done/templates/claude-md.md +145 -0
  275. package/get-shit-done/templates/codebase/architecture.md +255 -0
  276. package/get-shit-done/templates/codebase/concerns.md +310 -0
  277. package/get-shit-done/templates/codebase/conventions.md +307 -0
  278. package/get-shit-done/templates/codebase/integrations.md +280 -0
  279. package/get-shit-done/templates/codebase/stack.md +186 -0
  280. package/get-shit-done/templates/codebase/structure.md +285 -0
  281. package/get-shit-done/templates/codebase/testing.md +480 -0
  282. package/get-shit-done/templates/config.json +62 -0
  283. package/get-shit-done/templates/context.md +352 -0
  284. package/get-shit-done/templates/continue-here.md +78 -0
  285. package/get-shit-done/templates/copilot-instructions.md +7 -0
  286. package/get-shit-done/templates/debug-subagent-prompt.md +91 -0
  287. package/get-shit-done/templates/dev-preferences.md +21 -0
  288. package/get-shit-done/templates/discovery.md +146 -0
  289. package/get-shit-done/templates/discussion-log.md +63 -0
  290. package/get-shit-done/templates/milestone-archive.md +123 -0
  291. package/get-shit-done/templates/milestone.md +115 -0
  292. package/get-shit-done/templates/phase-prompt.md +610 -0
  293. package/get-shit-done/templates/planner-subagent-prompt.md +117 -0
  294. package/get-shit-done/templates/project.md +186 -0
  295. package/get-shit-done/templates/requirements.md +231 -0
  296. package/get-shit-done/templates/research-project/ARCHITECTURE.md +204 -0
  297. package/get-shit-done/templates/research-project/FEATURES.md +147 -0
  298. package/get-shit-done/templates/research-project/PITFALLS.md +200 -0
  299. package/get-shit-done/templates/research-project/STACK.md +120 -0
  300. package/get-shit-done/templates/research-project/SUMMARY.md +170 -0
  301. package/get-shit-done/templates/research.md +592 -0
  302. package/get-shit-done/templates/retrospective.md +54 -0
  303. package/get-shit-done/templates/roadmap.md +202 -0
  304. package/get-shit-done/templates/spec.md +307 -0
  305. package/get-shit-done/templates/state.md +195 -0
  306. package/get-shit-done/templates/summary-complex.md +59 -0
  307. package/get-shit-done/templates/summary-minimal.md +41 -0
  308. package/get-shit-done/templates/summary-standard.md +48 -0
  309. package/get-shit-done/templates/summary.md +248 -0
  310. package/get-shit-done/templates/user-profile.md +146 -0
  311. package/get-shit-done/templates/user-setup.md +311 -0
  312. package/get-shit-done/templates/verification-report.md +322 -0
  313. package/get-shit-done/workflows/_runtime-launcher.snippet.sh +1 -0
  314. package/get-shit-done/workflows/add-backlog.md +91 -0
  315. package/get-shit-done/workflows/add-phase.md +113 -0
  316. package/get-shit-done/workflows/add-tests.md +355 -0
  317. package/get-shit-done/workflows/add-todo.md +161 -0
  318. package/get-shit-done/workflows/ai-integration-phase.md +295 -0
  319. package/get-shit-done/workflows/analyze-dependencies.md +96 -0
  320. package/get-shit-done/workflows/audit-fix.md +178 -0
  321. package/get-shit-done/workflows/audit-milestone.md +358 -0
  322. package/get-shit-done/workflows/audit-uat.md +110 -0
  323. package/get-shit-done/workflows/autonomous.md +795 -0
  324. package/get-shit-done/workflows/check-todos.md +180 -0
  325. package/get-shit-done/workflows/cleanup.md +155 -0
  326. package/get-shit-done/workflows/code-review-fix.md +502 -0
  327. package/get-shit-done/workflows/code-review.md +656 -0
  328. package/get-shit-done/workflows/complete-milestone.md +855 -0
  329. package/get-shit-done/workflows/debug.md +232 -0
  330. package/get-shit-done/workflows/diagnose-issues.md +241 -0
  331. package/get-shit-done/workflows/discovery-phase.md +291 -0
  332. package/get-shit-done/workflows/discuss-phase/modes/advisor.md +176 -0
  333. package/get-shit-done/workflows/discuss-phase/modes/all.md +28 -0
  334. package/get-shit-done/workflows/discuss-phase/modes/analyze.md +44 -0
  335. package/get-shit-done/workflows/discuss-phase/modes/auto.md +57 -0
  336. package/get-shit-done/workflows/discuss-phase/modes/batch.md +52 -0
  337. package/get-shit-done/workflows/discuss-phase/modes/chain.md +98 -0
  338. package/get-shit-done/workflows/discuss-phase/modes/default.md +141 -0
  339. package/get-shit-done/workflows/discuss-phase/modes/power.md +44 -0
  340. package/get-shit-done/workflows/discuss-phase/modes/text.md +55 -0
  341. package/get-shit-done/workflows/discuss-phase/templates/checkpoint.json +18 -0
  342. package/get-shit-done/workflows/discuss-phase/templates/context.md +136 -0
  343. package/get-shit-done/workflows/discuss-phase/templates/discussion-log.md +50 -0
  344. package/get-shit-done/workflows/discuss-phase-assumptions.md +675 -0
  345. package/get-shit-done/workflows/discuss-phase-power.md +291 -0
  346. package/get-shit-done/workflows/discuss-phase.md +499 -0
  347. package/get-shit-done/workflows/do.md +111 -0
  348. package/get-shit-done/workflows/docs-update.md +1162 -0
  349. package/get-shit-done/workflows/edit-phase.md +295 -0
  350. package/get-shit-done/workflows/eval-review.md +156 -0
  351. package/get-shit-done/workflows/execute-phase/steps/codebase-drift-gate.md +82 -0
  352. package/get-shit-done/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  353. package/get-shit-done/workflows/execute-phase/steps/post-merge-gate.md +117 -0
  354. package/get-shit-done/workflows/execute-phase.md +1709 -0
  355. package/get-shit-done/workflows/execute-plan.md +526 -0
  356. package/get-shit-done/workflows/explore.md +144 -0
  357. package/get-shit-done/workflows/extract-learnings.md +243 -0
  358. package/get-shit-done/workflows/fast.md +124 -0
  359. package/get-shit-done/workflows/forensics.md +279 -0
  360. package/get-shit-done/workflows/graduation.md +196 -0
  361. package/get-shit-done/workflows/health.md +224 -0
  362. package/get-shit-done/workflows/help/modes/brief.md +22 -0
  363. package/get-shit-done/workflows/help/modes/default.md +50 -0
  364. package/get-shit-done/workflows/help/modes/full.md +784 -0
  365. package/get-shit-done/workflows/help/modes/topic.md +74 -0
  366. package/get-shit-done/workflows/help.md +24 -0
  367. package/get-shit-done/workflows/import.md +254 -0
  368. package/get-shit-done/workflows/inbox.md +387 -0
  369. package/get-shit-done/workflows/ingest-docs.md +339 -0
  370. package/get-shit-done/workflows/insert-phase.md +152 -0
  371. package/get-shit-done/workflows/list-phase-assumptions.md +178 -0
  372. package/get-shit-done/workflows/list-workspaces.md +57 -0
  373. package/get-shit-done/workflows/manager.md +393 -0
  374. package/get-shit-done/workflows/map-codebase.md +444 -0
  375. package/get-shit-done/workflows/milestone-summary.md +224 -0
  376. package/get-shit-done/workflows/mvp-phase.md +222 -0
  377. package/get-shit-done/workflows/new-milestone.md +635 -0
  378. package/get-shit-done/workflows/new-project.md +1555 -0
  379. package/get-shit-done/workflows/new-workspace.md +240 -0
  380. package/get-shit-done/workflows/next.md +299 -0
  381. package/get-shit-done/workflows/node-repair.md +92 -0
  382. package/get-shit-done/workflows/note.md +158 -0
  383. package/get-shit-done/workflows/pause-work.md +244 -0
  384. package/get-shit-done/workflows/plan-milestone-gaps.md +281 -0
  385. package/get-shit-done/workflows/plan-phase.md +1809 -0
  386. package/get-shit-done/workflows/plan-review-convergence.md +346 -0
  387. package/get-shit-done/workflows/plant-seed.md +230 -0
  388. package/get-shit-done/workflows/pr-branch.md +157 -0
  389. package/get-shit-done/workflows/profile-user.md +453 -0
  390. package/get-shit-done/workflows/progress.md +699 -0
  391. package/get-shit-done/workflows/quick.md +1039 -0
  392. package/get-shit-done/workflows/reapply-patches.md +426 -0
  393. package/get-shit-done/workflows/remove-phase.md +156 -0
  394. package/get-shit-done/workflows/remove-workspace.md +108 -0
  395. package/get-shit-done/workflows/resume-project.md +332 -0
  396. package/get-shit-done/workflows/review.md +623 -0
  397. package/get-shit-done/workflows/scan.md +105 -0
  398. package/get-shit-done/workflows/secure-phase.md +180 -0
  399. package/get-shit-done/workflows/session-report.md +146 -0
  400. package/get-shit-done/workflows/settings-advanced.md +620 -0
  401. package/get-shit-done/workflows/settings-integrations.md +312 -0
  402. package/get-shit-done/workflows/settings.md +552 -0
  403. package/get-shit-done/workflows/ship.md +356 -0
  404. package/get-shit-done/workflows/sketch-wrap-up.md +286 -0
  405. package/get-shit-done/workflows/sketch.md +361 -0
  406. package/get-shit-done/workflows/spec-phase.md +262 -0
  407. package/get-shit-done/workflows/spike-wrap-up.md +307 -0
  408. package/get-shit-done/workflows/spike.md +453 -0
  409. package/get-shit-done/workflows/stats.md +80 -0
  410. package/get-shit-done/workflows/sync-skills.md +182 -0
  411. package/get-shit-done/workflows/thread.md +222 -0
  412. package/get-shit-done/workflows/transition.md +694 -0
  413. package/get-shit-done/workflows/ui-phase.md +328 -0
  414. package/get-shit-done/workflows/ui-review.md +193 -0
  415. package/get-shit-done/workflows/ultraplan-phase.md +199 -0
  416. package/get-shit-done/workflows/undo.md +314 -0
  417. package/get-shit-done/workflows/update.md +443 -0
  418. package/get-shit-done/workflows/validate-phase.md +179 -0
  419. package/get-shit-done/workflows/verify-phase.md +544 -0
  420. package/get-shit-done/workflows/verify-work.md +781 -0
  421. package/hooks/dist/gsd-check-update-worker.js +95 -0
  422. package/hooks/dist/gsd-check-update.js +64 -0
  423. package/hooks/dist/gsd-context-monitor.js +195 -0
  424. package/hooks/dist/gsd-graphify-update.sh +158 -0
  425. package/hooks/dist/gsd-phase-boundary.sh +47 -0
  426. package/hooks/dist/gsd-prompt-guard.js +97 -0
  427. package/hooks/dist/gsd-read-guard.js +101 -0
  428. package/hooks/dist/gsd-read-injection-scanner.js +203 -0
  429. package/hooks/dist/gsd-session-state.sh +59 -0
  430. package/hooks/dist/gsd-statusline.js +548 -0
  431. package/hooks/dist/gsd-update-banner.js +134 -0
  432. package/hooks/dist/gsd-validate-commit.sh +57 -0
  433. package/hooks/dist/gsd-workflow-guard.js +166 -0
  434. package/hooks/dist/lib/git-cmd.js +150 -0
  435. package/hooks/dist/lib/gsd-graphify-rebuild.sh +65 -0
  436. package/hooks/gsd-check-update-worker.js +95 -0
  437. package/hooks/gsd-check-update.js +64 -0
  438. package/hooks/gsd-context-monitor.js +195 -0
  439. package/hooks/gsd-graphify-update.sh +158 -0
  440. package/hooks/gsd-phase-boundary.sh +47 -0
  441. package/hooks/gsd-prompt-guard.js +97 -0
  442. package/hooks/gsd-read-guard.js +101 -0
  443. package/hooks/gsd-read-injection-scanner.js +203 -0
  444. package/hooks/gsd-session-state.sh +59 -0
  445. package/hooks/gsd-statusline.js +548 -0
  446. package/hooks/gsd-update-banner.js +134 -0
  447. package/hooks/gsd-validate-commit.sh +57 -0
  448. package/hooks/gsd-workflow-guard.js +166 -0
  449. package/hooks/lib/git-cmd.js +150 -0
  450. package/hooks/lib/gsd-graphify-rebuild.sh +65 -0
  451. package/hooks/managed-hooks-registry.cjs +34 -0
  452. package/package.json +102 -0
  453. package/scripts/affected-tests-lib.cjs +541 -0
  454. package/scripts/audit-workflow-script-paths.cjs +73 -0
  455. package/scripts/base64-scan.sh +339 -0
  456. package/scripts/build-hooks.js +236 -0
  457. package/scripts/changeset/README.md +129 -0
  458. package/scripts/changeset/cli.cjs +392 -0
  459. package/scripts/changeset/github-release-notes.cjs +199 -0
  460. package/scripts/changeset/lint.cjs +110 -0
  461. package/scripts/changeset/new.cjs +137 -0
  462. package/scripts/changeset/parse.cjs +114 -0
  463. package/scripts/changeset/render.cjs +34 -0
  464. package/scripts/changeset/serialize.cjs +130 -0
  465. package/scripts/check-alias-drift.cjs +108 -0
  466. package/scripts/check-env.cjs +302 -0
  467. package/scripts/check-npm-integrity.cjs +209 -0
  468. package/scripts/ci-guard-runner.cjs +16 -0
  469. package/scripts/ci-prepare-test-scope.cjs +46 -0
  470. package/scripts/ci-rebase-check.cjs +85 -0
  471. package/scripts/ci-test-scope.cjs +302 -0
  472. package/scripts/command-contract-helpers.cjs +64 -0
  473. package/scripts/diff-touches-shipped-paths.cjs +147 -0
  474. package/scripts/fix-slash-commands.cjs +147 -0
  475. package/scripts/gen-inventory-manifest.cjs +109 -0
  476. package/scripts/generate-package-identity.cjs +104 -0
  477. package/scripts/lint-command-contract.cjs +108 -0
  478. package/scripts/lint-descriptions.cjs +83 -0
  479. package/scripts/lint-docs-required.cjs +222 -0
  480. package/scripts/lint-no-source-grep-extras.cjs +81 -0
  481. package/scripts/lint-no-source-grep.cjs +174 -0
  482. package/scripts/lint-package-identity-drift.cjs +141 -0
  483. package/scripts/lint-pr-check-project-dir.cjs +98 -0
  484. package/scripts/lint-shared-module-handsync.cjs +388 -0
  485. package/scripts/lint-shell-command-projection-drift.cjs +57 -0
  486. package/scripts/lint-skill-deps.cjs +180 -0
  487. package/scripts/lint-test-file-count.allowlist.json +36 -0
  488. package/scripts/lint-test-file-count.cjs +190 -0
  489. package/scripts/pr-template-policy.cjs +268 -0
  490. package/scripts/prompt-injection-scan.sh +203 -0
  491. package/scripts/release-tarball-smoke.cjs +627 -0
  492. package/scripts/run-affected-tests.cjs +6 -0
  493. package/scripts/run-cross-platform-tests.cjs +63 -0
  494. package/scripts/run-tests.cjs +282 -0
  495. package/scripts/secret-scan-lint.sh +231 -0
  496. package/scripts/secret-scan.sh +358 -0
  497. package/scripts/setup-branch-protection.sh +236 -0
  498. package/scripts/shared-module-handsync-allowlist.json +183 -0
  499. package/scripts/strip-prose-atrefs.cjs +106 -0
  500. package/scripts/sync-rulesets.sh +34 -0
  501. package/scripts/sync-runtime-launcher.cjs +402 -0
  502. package/scripts/test-failure-reasons.cjs +34 -0
  503. package/scripts/workflow-policy.cjs +450 -0
@@ -0,0 +1,985 @@
1
+ /**
2
+ * Worktree Safety Policy Module
3
+ *
4
+ * Owns worktree-root resolution and non-destructive prune policy decisions.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execGit: execGitSeam } = require('./shell-command-projection.cjs');
10
+
11
+ // Default timeout for worktree-related git subprocess calls.
12
+ // 10 s is generous enough for normal git operations on large repos while still
13
+ // providing a deterministic failure path when git stalls (locked index, hung
14
+ // remote, stalled NFS mount, etc.). Callers can override via deps.timeout.
15
+ const DEFAULT_GIT_TIMEOUT_MS = 10000;
16
+
17
+ /**
18
+ * Execute a git command via the shell-projection seam, with a derived
19
+ * `timedOut` field. Tests inject mocks via deps.execGit using the new
20
+ * (args, opts) shape — see worktree-safety-policy.test.cjs.
21
+ *
22
+ * Return shape: { exitCode, stdout, stderr, timedOut, error, signal }
23
+ * - timedOut: true when spawnSync reports SIGTERM + ETIMEDOUT
24
+ */
25
+ function execGitDefault(args, opts = {}) {
26
+ const result = execGitSeam(args, { ...opts, timeout: opts.timeout ?? DEFAULT_GIT_TIMEOUT_MS });
27
+ const timedOut = result.signal === 'SIGTERM' && result.error?.code === 'ETIMEDOUT';
28
+ return { ...result, timedOut };
29
+ }
30
+
31
+ function parseWorktreePorcelain(porcelain) {
32
+ return parseWorktreeEntries(porcelain).filter((entry) => entry.branch).map((entry) => ({
33
+ path: entry.path,
34
+ branch: entry.branch,
35
+ }));
36
+ }
37
+
38
+ function parseWorktreeEntries(porcelain) {
39
+ const entries = [];
40
+ const blocks = String(porcelain || '').split('\n\n').filter(Boolean);
41
+ for (const block of blocks) {
42
+ const lines = block.split('\n');
43
+ const worktreeLine = lines.find((l) => l.startsWith('worktree '));
44
+ if (!worktreeLine) continue;
45
+ const worktreePath = worktreeLine.slice('worktree '.length).trim();
46
+ if (!worktreePath) continue;
47
+ const branchLine = lines.find((l) => l.startsWith('branch refs/heads/'));
48
+ const branch = branchLine ? branchLine.slice('branch refs/heads/'.length).trim() : null;
49
+ entries.push({ path: worktreePath, branch });
50
+ }
51
+ return entries;
52
+ }
53
+
54
+ function parseWorktreeListPaths(porcelain) {
55
+ return parseWorktreeEntries(porcelain).map((entry) => entry.path);
56
+ }
57
+
58
+ function readWorktreeList(repoRoot, deps = {}) {
59
+ const execGit = deps.execGit || execGitDefault;
60
+ const listResult = execGit(['worktree', 'list', '--porcelain'], { cwd: repoRoot });
61
+ if (listResult.timedOut) {
62
+ // AC2 / AC4: surface timeout as a distinct reason so callers can emit a
63
+ // structured warning rather than silently treating the failure as a generic
64
+ // list error (PRED.k302 — error-swallowing-empty-sentinel).
65
+ return {
66
+ ok: false,
67
+ reason: 'git_timed_out',
68
+ porcelain: '',
69
+ entries: [],
70
+ };
71
+ }
72
+ if (listResult.exitCode !== 0) {
73
+ const stderr = String(listResult.stderr || '');
74
+ return {
75
+ ok: false,
76
+ reason: /not a git repository|not a git repo/i.test(stderr)
77
+ ? 'not_a_git_repo'
78
+ : 'git_list_failed',
79
+ porcelain: '',
80
+ entries: [],
81
+ };
82
+ }
83
+
84
+ return {
85
+ ok: true,
86
+ reason: 'ok',
87
+ porcelain: listResult.stdout,
88
+ entries: parseWorktreeEntries(listResult.stdout),
89
+ };
90
+ }
91
+
92
+ function resolveWorktreeContext(cwd, deps = {}) {
93
+ const execGit = deps.execGit || execGitDefault;
94
+ const existsSync = deps.existsSync || fs.existsSync;
95
+
96
+ // Local .planning takes precedence over linked-worktree remapping.
97
+ if (existsSync(path.join(cwd, '.planning'))) {
98
+ return {
99
+ effectiveRoot: cwd,
100
+ mode: 'current_directory',
101
+ reason: 'has_local_planning',
102
+ };
103
+ }
104
+
105
+ const gitDir = execGit(['rev-parse', '--git-dir'], { cwd });
106
+ const commonDir = execGit(['rev-parse', '--git-common-dir'], { cwd });
107
+ if (gitDir.exitCode !== 0 || commonDir.exitCode !== 0) {
108
+ return {
109
+ effectiveRoot: cwd,
110
+ mode: 'current_directory',
111
+ reason: 'not_git_repo',
112
+ };
113
+ }
114
+
115
+ const gitDirResolved = path.resolve(cwd, gitDir.stdout);
116
+ const commonDirResolved = path.resolve(cwd, commonDir.stdout);
117
+ if (gitDirResolved !== commonDirResolved) {
118
+ return {
119
+ effectiveRoot: path.dirname(commonDirResolved),
120
+ mode: 'linked_worktree_root',
121
+ reason: 'linked_worktree',
122
+ };
123
+ }
124
+
125
+ return {
126
+ effectiveRoot: cwd,
127
+ mode: 'current_directory',
128
+ reason: 'main_worktree',
129
+ };
130
+ }
131
+
132
+ function planWorktreePrune(repoRoot, options = {}, deps = {}) {
133
+ const parsePorcelain = deps.parseWorktreePorcelain || parseWorktreePorcelain;
134
+ const destructiveModeRequested = Boolean(options.allowDestructive);
135
+ const listed = readWorktreeList(repoRoot, deps);
136
+ if (!listed.ok) {
137
+ return {
138
+ repoRoot,
139
+ action: 'skip',
140
+ reason: listed.reason,
141
+ destructiveModeRequested,
142
+ };
143
+ }
144
+
145
+ let worktrees = [];
146
+ try {
147
+ worktrees = parsePorcelain(listed.porcelain);
148
+ } catch {
149
+ // Keep historical behavior: still run metadata prune when parsing fails.
150
+ worktrees = [];
151
+ }
152
+
153
+ return {
154
+ repoRoot,
155
+ action: 'metadata_prune_only',
156
+ reason: worktrees.length === 0 ? 'no_worktrees' : 'worktrees_present',
157
+ destructiveModeRequested,
158
+ };
159
+ }
160
+
161
+ function executeWorktreePrunePlan(plan, deps = {}) {
162
+ const execGit = deps.execGit || execGitDefault;
163
+ if (!plan || plan.action === 'skip') {
164
+ return {
165
+ ok: false,
166
+ action: plan ? plan.action : 'skip',
167
+ reason: plan ? plan.reason : 'missing_plan',
168
+ pruned: [],
169
+ };
170
+ }
171
+
172
+ if (plan.action !== 'metadata_prune_only') {
173
+ return {
174
+ ok: false,
175
+ action: plan.action,
176
+ reason: 'unsupported_action',
177
+ pruned: [],
178
+ };
179
+ }
180
+
181
+ const result = execGit(['worktree', 'prune'], { cwd: plan.repoRoot });
182
+ if (result.timedOut) {
183
+ // AC4: surface timedOut as a first-class field so callers (e.g.
184
+ // pruneOrphanedWorktrees in core.cjs) can log a structured WARNING rather
185
+ // than silently ignoring it (PRED.k302 — error-swallowing-empty-sentinel).
186
+ return {
187
+ ok: false,
188
+ action: plan.action,
189
+ reason: 'git_timed_out',
190
+ timedOut: true,
191
+ pruned: [],
192
+ };
193
+ }
194
+ return {
195
+ ok: result.exitCode === 0,
196
+ action: plan.action,
197
+ reason: plan.reason,
198
+ timedOut: false,
199
+ pruned: [],
200
+ };
201
+ }
202
+
203
+ function listLinkedWorktreePaths(repoRoot, deps = {}) {
204
+ const listed = readWorktreeList(repoRoot, deps);
205
+ if (!listed.ok) {
206
+ return {
207
+ ok: false,
208
+ reason: listed.reason,
209
+ paths: [],
210
+ };
211
+ }
212
+
213
+ const allPaths = listed.entries.map((entry) => entry.path);
214
+ // git worktree list always includes the current/main worktree first.
215
+ return {
216
+ ok: true,
217
+ reason: 'ok',
218
+ paths: allPaths.slice(1),
219
+ };
220
+ }
221
+
222
+ function inspectWorktreeHealth(repoRoot, options = {}, deps = {}) {
223
+ const inventory = snapshotWorktreeInventory(repoRoot, options, deps);
224
+ if (!inventory.ok) {
225
+ return {
226
+ ok: false,
227
+ reason: inventory.reason,
228
+ findings: [],
229
+ };
230
+ }
231
+
232
+ const findings = [];
233
+ for (const entry of inventory.entries) {
234
+ if (!entry.exists) {
235
+ findings.push({
236
+ kind: 'orphan',
237
+ path: entry.path,
238
+ });
239
+ continue;
240
+ }
241
+ if (entry.isStale) {
242
+ findings.push({
243
+ kind: 'stale',
244
+ path: entry.path,
245
+ ageMinutes: entry.ageMinutes,
246
+ });
247
+ }
248
+ }
249
+
250
+ return {
251
+ ok: true,
252
+ reason: 'ok',
253
+ findings,
254
+ };
255
+ }
256
+
257
+ function snapshotWorktreeInventory(repoRoot, options = {}, deps = {}) {
258
+ const existsSync = deps.existsSync || fs.existsSync;
259
+ const statSync = deps.statSync || fs.statSync;
260
+ const staleAfterMs = options.staleAfterMs ?? (60 * 60 * 1000);
261
+ const nowMs = options.nowMs ?? Date.now();
262
+ const listed = listLinkedWorktreePaths(repoRoot, { execGit: deps.execGit || execGitDefault });
263
+ if (!listed.ok) {
264
+ return {
265
+ ok: false,
266
+ reason: listed.reason,
267
+ entries: [],
268
+ };
269
+ }
270
+
271
+ const entries = [];
272
+ for (const worktreePath of listed.paths) {
273
+ let exists = false;
274
+ let isStale = false;
275
+ let ageMinutes = null;
276
+
277
+ if (!existsSync(worktreePath)) {
278
+ entries.push({
279
+ path: worktreePath,
280
+ exists,
281
+ isStale,
282
+ ageMinutes,
283
+ });
284
+ continue;
285
+ }
286
+
287
+ exists = true;
288
+ try {
289
+ const stat = statSync(worktreePath);
290
+ const ageMs = nowMs - stat.mtimeMs;
291
+ ageMinutes = Math.round(ageMs / 60000);
292
+ if (ageMs > staleAfterMs) {
293
+ isStale = true;
294
+ }
295
+ } catch {
296
+ // Keep historical behavior: stat failures are ignored.
297
+ }
298
+ entries.push({
299
+ path: worktreePath,
300
+ exists,
301
+ isStale,
302
+ ageMinutes,
303
+ });
304
+ }
305
+
306
+ return {
307
+ ok: true,
308
+ reason: 'ok',
309
+ entries,
310
+ };
311
+ }
312
+
313
+ function normalizeCleanupManifestEntry(entry) {
314
+ if (!entry || typeof entry !== 'object') return null;
315
+ const worktreePath = typeof entry.worktree_path === 'string'
316
+ ? entry.worktree_path
317
+ : (typeof entry.path === 'string' ? entry.path : '');
318
+ const branch = typeof entry.branch === 'string' ? entry.branch : '';
319
+ const expectedBase = typeof entry.expected_base === 'string' ? entry.expected_base : '';
320
+ if (!worktreePath || !branch || !expectedBase) return null;
321
+ if (!/^worktree-agent-[A-Za-z0-9._/-]+$/.test(branch)) return null;
322
+ return {
323
+ agent_id: typeof entry.agent_id === 'string' ? entry.agent_id : null,
324
+ worktree_path: worktreePath,
325
+ branch,
326
+ expected_base: expectedBase,
327
+ };
328
+ }
329
+
330
+ function normalizeCleanupManifest(manifest) {
331
+ let parsed = manifest;
332
+ if (typeof manifest === 'string') {
333
+ try {
334
+ parsed = JSON.parse(manifest);
335
+ } catch {
336
+ return { ok: false, reason: 'invalid_manifest_json', entries: [] };
337
+ }
338
+ }
339
+
340
+ const rawEntries = Array.isArray(parsed)
341
+ ? parsed
342
+ : (Array.isArray(parsed?.worktrees) ? parsed.worktrees : []);
343
+ const seen = new Set();
344
+ const entries = [];
345
+ for (const raw of rawEntries) {
346
+ const entry = normalizeCleanupManifestEntry(raw);
347
+ if (!entry) continue;
348
+ const key = `${entry.worktree_path}\0${entry.branch}`;
349
+ if (seen.has(key)) continue;
350
+ seen.add(key);
351
+ entries.push(entry);
352
+ }
353
+
354
+ if (entries.length === 0) {
355
+ return { ok: false, reason: 'empty_manifest', entries: [] };
356
+ }
357
+
358
+ return { ok: true, reason: 'ok', entries };
359
+ }
360
+
361
+ function planWorktreeWaveCleanup(repoRoot, manifest) {
362
+ const normalized = normalizeCleanupManifest(manifest);
363
+ if (!normalized.ok) {
364
+ return {
365
+ ok: false,
366
+ repoRoot,
367
+ action: 'skip',
368
+ discovery: 'manifest',
369
+ reason: normalized.reason,
370
+ entries: [],
371
+ };
372
+ }
373
+
374
+ return {
375
+ ok: true,
376
+ repoRoot,
377
+ action: 'cleanup_wave',
378
+ discovery: 'manifest',
379
+ reason: 'manifest_entries_present',
380
+ entries: normalized.entries,
381
+ };
382
+ }
383
+
384
+ function gitResultOk(result) {
385
+ return result && result.exitCode === 0 && !result.timedOut;
386
+ }
387
+
388
+ /**
389
+ * Walk <worktreePath>/.planning/ recursively and collect absolute paths of
390
+ * all files whose names match *SUMMARY.md. Returns [] when the directory
391
+ * does not exist or cannot be read.
392
+ *
393
+ * Mirrors the shell fallback in quick.md (#2296, #2070, #2838):
394
+ * find "$WT/.planning" -name "*SUMMARY.md"
395
+ */
396
+ function defaultFindSummaryFiles(worktreePath) {
397
+ const planningDir = path.join(worktreePath, '.planning');
398
+ const results = [];
399
+ function walk(dir) {
400
+ let entries;
401
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
402
+ for (const entry of entries) {
403
+ const full = path.join(dir, entry.name);
404
+ if (entry.isDirectory()) {
405
+ walk(full);
406
+ } else if (entry.isFile() && entry.name.endsWith('SUMMARY.md')) {
407
+ results.push(full);
408
+ }
409
+ }
410
+ }
411
+ walk(planningDir);
412
+ return results;
413
+ }
414
+
415
+ /**
416
+ * Rescue uncommitted SUMMARY.md artifacts from a worktree into the main repo
417
+ * tree before the dirty-state check. Mirrors the shell-fallback rescue block
418
+ * in quick.md (lines 878–891, #2296/#2070/#2838).
419
+ *
420
+ * For each *SUMMARY.md found under <worktreePath>/.planning/:
421
+ * - compute relative path from worktree root → .planning/<id>-SUMMARY.md
422
+ * - destination = <repoRoot>/<relPath>
423
+ * - copy when dest is absent or content differs
424
+ *
425
+ * Returns a Set of worktree-relative paths (e.g. ".planning/q1-SUMMARY.md")
426
+ * that were eligible for rescue (regardless of whether a copy was needed).
427
+ * These paths are filtered out of the git-status porcelain output so a
428
+ * SUMMARY-only dirty worktree does not block cleanup.
429
+ *
430
+ * Injected deps (all optional — falls back to real FS):
431
+ * findSummaryFiles(worktreePath) → string[]
432
+ * existsSync(path) → boolean
433
+ * readFileSync(path) → string
434
+ * mkdirSync(dir, opts)
435
+ * copyFileSync(src, dest)
436
+ */
437
+ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
438
+ const findSummaryFiles = deps.findSummaryFiles || defaultFindSummaryFiles;
439
+ const existsSync = deps.existsSync || fs.existsSync;
440
+ const readFileSync = deps.readFileSync || ((p) => fs.readFileSync(p, 'utf8'));
441
+ const mkdirSync = deps.mkdirSync || ((d, o) => fs.mkdirSync(d, o));
442
+ const copyFileSync = deps.copyFileSync || fs.copyFileSync;
443
+
444
+ const summaryPaths = findSummaryFiles(worktreePath);
445
+ const rescuedRelPaths = new Set();
446
+
447
+ for (const absPath of summaryPaths) {
448
+ // relPath is the path relative to the worktree root (e.g. ".planning/q1-SUMMARY.md")
449
+ // Normalize to forward slashes so the Set comparison against `git status --porcelain`
450
+ // output works on Windows too (git always emits forward slashes in porcelain output).
451
+ const relPath = absPath.slice(worktreePath.length).replace(/^[/\\]/, '').replace(/\\/g, '/');
452
+ rescuedRelPaths.add(relPath);
453
+
454
+ const dest = path.join(repoRoot, relPath);
455
+ let needsCopy = !existsSync(dest);
456
+ if (!needsCopy) {
457
+ try {
458
+ const srcContent = readFileSync(absPath);
459
+ const destContent = readFileSync(dest);
460
+ needsCopy = srcContent !== destContent;
461
+ } catch {
462
+ needsCopy = true;
463
+ }
464
+ }
465
+ if (needsCopy) {
466
+ try {
467
+ mkdirSync(path.dirname(dest), { recursive: true });
468
+ copyFileSync(absPath, dest);
469
+ } catch {
470
+ // Best-effort rescue — if it fails the dirty check below will decide fate
471
+ }
472
+ }
473
+ }
474
+
475
+ return rescuedRelPaths;
476
+ }
477
+
478
+ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
479
+ const execGit = deps.execGit || execGitDefault;
480
+ const entries = Array.isArray(plan?.entries) ? plan.entries : [];
481
+ if (!plan || plan.action !== 'cleanup_wave' || entries.length === 0) {
482
+ return {
483
+ ok: false,
484
+ action: plan ? plan.action : 'skip',
485
+ reason: plan ? (plan.reason || 'missing_entries') : 'missing_plan',
486
+ entries: [],
487
+ pending: entries,
488
+ };
489
+ }
490
+
491
+ const results = [];
492
+ const pending = [];
493
+ let ok = true;
494
+
495
+ for (let i = 0; i < entries.length; i += 1) {
496
+ const entry = entries[i];
497
+ const result = {
498
+ ...entry,
499
+ status: 'pending',
500
+ reason: null,
501
+ stderr: '',
502
+ };
503
+
504
+ const branchCheck = execGit(['-C', entry.worktree_path, 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: plan.repoRoot });
505
+ if (!gitResultOk(branchCheck) || branchCheck.stdout.trim() !== entry.branch) {
506
+ result.status = 'blocked';
507
+ result.reason = 'branch_mismatch';
508
+ result.stderr = branchCheck?.stderr || '';
509
+ results.push(result);
510
+ pending.push(...entries.slice(i + 1));
511
+ ok = false;
512
+ break;
513
+ }
514
+
515
+ const mergeBase = execGit(['merge-base', 'HEAD', entry.branch], { cwd: plan.repoRoot });
516
+ if (!gitResultOk(mergeBase) || mergeBase.stdout.trim() !== entry.expected_base) {
517
+ result.status = 'blocked';
518
+ result.reason = 'base_mismatch';
519
+ result.stderr = mergeBase?.stderr || '';
520
+ results.push(result);
521
+ pending.push(...entries.slice(i + 1));
522
+ ok = false;
523
+ break;
524
+ }
525
+
526
+ const deletions = execGit(['diff', '--diff-filter=D', '--name-only', `HEAD...${entry.branch}`], { cwd: plan.repoRoot });
527
+ if (!gitResultOk(deletions)) {
528
+ result.status = 'blocked';
529
+ result.reason = 'deletion_check_failed';
530
+ result.stderr = deletions?.stderr || '';
531
+ results.push(result);
532
+ pending.push(...entries.slice(i + 1));
533
+ ok = false;
534
+ break;
535
+ }
536
+ if (deletions.stdout) {
537
+ result.status = 'blocked';
538
+ result.reason = 'branch_contains_deletions';
539
+ result.stderr = deletions.stdout;
540
+ results.push(result);
541
+ pending.push(...entries.slice(i + 1));
542
+ ok = false;
543
+ break;
544
+ }
545
+
546
+ // Safety net: rescue uncommitted SUMMARY.md artifacts before the dirty check.
547
+ // The executor leaves <quick_id>-SUMMARY.md uncommitted by contract — the
548
+ // orchestrator commits it. Mirrors quick.md shell fallback (#2296, #2070, #2838, #3804).
549
+ const rescuedRelPaths = rescueSummaryArtifacts(entry.worktree_path, plan.repoRoot, deps);
550
+
551
+ const worktreeStatus = execGit(['-C', entry.worktree_path, 'status', '--porcelain', '--untracked-files=all'], { cwd: plan.repoRoot });
552
+ if (!gitResultOk(worktreeStatus)) {
553
+ result.status = 'blocked';
554
+ result.reason = 'worktree_dirty';
555
+ result.stderr = worktreeStatus?.stderr || '';
556
+ results.push(result);
557
+ pending.push(...entries.slice(i + 1));
558
+ ok = false;
559
+ break;
560
+ }
561
+ // Filter rescued SUMMARY paths out of the porcelain output before deciding dirty.
562
+ // A line like "?? .planning/q1-SUMMARY.md" should not block when the SUMMARY
563
+ // has already been rescued into the main tree.
564
+ const dirtyLines = (worktreeStatus.stdout || '')
565
+ .split('\n')
566
+ .filter((line) => {
567
+ if (!line.trim()) return false;
568
+ // porcelain v1 format: "XY path" (3-char prefix + space + path)
569
+ const filePath = line.slice(3).trim();
570
+ return !rescuedRelPaths.has(filePath);
571
+ });
572
+ if (dirtyLines.length > 0) {
573
+ result.status = 'blocked';
574
+ result.reason = 'worktree_dirty';
575
+ result.stderr = dirtyLines.join('\n');
576
+ results.push(result);
577
+ pending.push(...entries.slice(i + 1));
578
+ ok = false;
579
+ break;
580
+ }
581
+
582
+ const merge = execGit(['merge', entry.branch, '--no-ff', '--no-edit', '-m', `chore: merge executor worktree (${entry.branch})`], { cwd: plan.repoRoot });
583
+ if (!gitResultOk(merge)) {
584
+ result.status = 'blocked';
585
+ result.reason = 'merge_failed';
586
+ result.stderr = merge?.stderr || merge?.stdout || '';
587
+ results.push(result);
588
+ pending.push(...entries.slice(i + 1));
589
+ ok = false;
590
+ break;
591
+ }
592
+
593
+ let remove = execGit(['worktree', 'remove', entry.worktree_path, '--force'], { cwd: plan.repoRoot });
594
+ if (!gitResultOk(remove)) {
595
+ // Locked worktrees require unlock before remove (or --force --force).
596
+ // Attempt: git worktree unlock <path> (ignore failure — already unlocked is ok)
597
+ // then retry git worktree remove --force. (#3707)
598
+ execGit(['worktree', 'unlock', entry.worktree_path], { cwd: plan.repoRoot });
599
+ remove = execGit(['worktree', 'remove', entry.worktree_path, '--force'], { cwd: plan.repoRoot });
600
+ }
601
+ if (!gitResultOk(remove)) {
602
+ result.status = 'blocked';
603
+ result.reason = 'worktree_remove_failed';
604
+ result.stderr = remove?.stderr || '';
605
+ results.push(result);
606
+ pending.push(...entries.slice(i + 1));
607
+ ok = false;
608
+ break;
609
+ }
610
+
611
+ const branchDelete = execGit(['branch', '-D', entry.branch], { cwd: plan.repoRoot });
612
+ if (!gitResultOk(branchDelete)) {
613
+ result.status = 'warning';
614
+ result.reason = 'branch_delete_failed';
615
+ result.stderr = branchDelete?.stderr || '';
616
+ ok = false;
617
+ } else {
618
+ result.status = 'merged_removed';
619
+ result.reason = 'ok';
620
+ }
621
+ results.push(result);
622
+ }
623
+
624
+ return {
625
+ ok,
626
+ action: plan.action,
627
+ reason: ok ? 'ok' : 'cleanup_blocked',
628
+ entries: results,
629
+ pending,
630
+ };
631
+ }
632
+
633
+ function cmdWorktreeCleanupWave(cwd, args = []) {
634
+ const manifestFlagIndex = args.indexOf('--manifest');
635
+ const manifestPath = manifestFlagIndex >= 0 ? args[manifestFlagIndex + 1] : '';
636
+ if (!manifestPath) {
637
+ process.stderr.write('Usage: worktree cleanup-wave --manifest <path>\n');
638
+ process.exitCode = 2;
639
+ return;
640
+ }
641
+
642
+ let manifest;
643
+ try {
644
+ manifest = fs.readFileSync(path.resolve(cwd, manifestPath), 'utf8');
645
+ } catch (err) {
646
+ process.stdout.write(`${JSON.stringify({
647
+ ok: false,
648
+ reason: 'manifest_read_failed',
649
+ error: err.message,
650
+ }, null, 2)}\n`);
651
+ process.exitCode = 1;
652
+ return;
653
+ }
654
+
655
+ const plan = planWorktreeWaveCleanup(cwd, manifest);
656
+ const result = executeWorktreeWaveCleanupPlan(plan);
657
+ const response = {
658
+ ok: result.ok,
659
+ plan: {
660
+ action: plan.action,
661
+ discovery: plan.discovery,
662
+ reason: plan.reason,
663
+ entries: plan.entries.length,
664
+ },
665
+ result,
666
+ };
667
+ process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
668
+ if (!result.ok) {
669
+ process.exitCode = 1;
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Reap orphaned linked worktrees whose lock owner process is dead, whose
675
+ * branch tip is fully merged into the default branch, and whose lock file
676
+ * mtime is older than REAP_MTIME_GUARD_MS (race guard).
677
+ *
678
+ * Invariants (Fail-closed — skip on any doubt):
679
+ * Pre: .git/worktrees/<id>/locked exists for a linked worktree
680
+ * Reap: pid dead (or unparseable) AND branch-tip ancestor of default branch
681
+ * AND lock mtime > REAP_MTIME_GUARD_MS old
682
+ * Action: worktree unlock → worktree remove --force → prune
683
+ * Post: worktree absent from git worktree list; no unmerged work lost
684
+ *
685
+ * @param {string} repoRoot - Absolute path to the primary worktree root.
686
+ * @param {object} [deps] - Optional dependency overrides for testing.
687
+ * deps.execGit - Replaces execGitDefault for all git calls.
688
+ * deps.isPidAlive - Function(pid:number):boolean (default: kill -0).
689
+ * deps.readDirSafe - Function(dir:string):string[] (default: fs.readdirSync).
690
+ * deps.readFileSafe - Function(file:string):string (default: fs.readFileSync).
691
+ * deps.mtimeSafe - Function(file:string):Date (default: fs.statSync).
692
+ * deps.reapMtimeGuardMs - Override stale-lock age threshold (default 5 min).
693
+ * @returns {Array<{path:string, status:'reaped'|'skipped', reason:string}>}
694
+ */
695
+ const REAP_MTIME_GUARD_MS = 5 * 60 * 1000; // 5 minutes
696
+
697
+ function reapOrphanWorktrees(repoRoot, deps = {}) {
698
+ const execGit = deps.execGit || execGitDefault;
699
+ const isPidAlive = deps.isPidAlive || defaultIsPidAlive;
700
+ const readDirSafe = deps.readDirSafe || defaultReadDirSafe;
701
+ const readFileSafe = deps.readFileSafe || defaultReadFileSafe;
702
+ const mtimeSafe = deps.mtimeSafe || defaultMtimeSafe;
703
+ const reapMtimeGuardMs = deps.reapMtimeGuardMs !== undefined ? deps.reapMtimeGuardMs : REAP_MTIME_GUARD_MS;
704
+
705
+ const results = [];
706
+
707
+ // 1. Discover the .git/worktrees/ admin directory.
708
+ const gitDir = execGit(['rev-parse', '--git-dir'], { cwd: repoRoot });
709
+ if (!gitResultOk(gitDir)) return results;
710
+ const gitDirPath = path.resolve(repoRoot, gitDir.stdout.trim());
711
+
712
+ const worktreesAdminDir = path.join(gitDirPath, 'worktrees');
713
+ const entries = readDirSafe(worktreesAdminDir);
714
+ if (!entries) return results;
715
+
716
+ // 2. Discover the default branch (main/master/etc) tip.
717
+ // Strategy (fail-closed):
718
+ // a. Prefer refs/remotes/origin/HEAD — the authoritative integration branch.
719
+ // b. Only fall back to 'main' / 'master' when origin/HEAD is absent AND the
720
+ // remote itself doesn't exist (i.e. local-only test fixtures). In all other
721
+ // cases, bail out rather than guess: using a wrong branch tip would allow
722
+ // `merge-base --is-ancestor` to pass against a non-authoritative ref and
723
+ // reap a worktree whose branch is NOT merged into the real default.
724
+ //
725
+ // Intentionally excludes 'HEAD': using HEAD when detached or on a feature
726
+ // branch would make every branch appear "merged" into it, causing false reaping.
727
+ const defaultBranchResult = execGit(
728
+ ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'],
729
+ { cwd: repoRoot }
730
+ );
731
+
732
+ let mainTip;
733
+ if (gitResultOk(defaultBranchResult)) {
734
+ // Remote default branch is known — use it exclusively.
735
+ const branchName = defaultBranchResult.stdout.trim().replace(/^origin\//, '');
736
+ const r = execGit(['rev-parse', `refs/remotes/origin/${branchName}`], { cwd: repoRoot });
737
+ if (!gitResultOk(r)) return results; // remote ref unresolvable — fail closed
738
+ mainTip = r.stdout.trim();
739
+ } else {
740
+ // No remote configured (local-only repo, e.g. test fixtures).
741
+ // Fall back to 'main' then 'master' — only safe because there is no remote
742
+ // integration branch to confuse with. A remote that exists but lacks
743
+ // origin/HEAD is treated as ambiguous and bails out (fail-closed).
744
+ const hasRemote = execGit(['remote'], { cwd: repoRoot });
745
+ if (gitResultOk(hasRemote) && hasRemote.stdout.trim()) {
746
+ // Remote exists but origin/HEAD not set — ambiguous; fail closed.
747
+ return results;
748
+ }
749
+ // Build candidate list: init.defaultBranch config, HEAD symref, then main, master.
750
+ const candidateBranches = [];
751
+ // Try git config init.defaultBranch first (user-configured default)
752
+ const configResult = execGit(['config', '--get', 'init.defaultBranch'], { cwd: repoRoot });
753
+ if (gitResultOk(configResult) && configResult.stdout.trim()) {
754
+ candidateBranches.push(configResult.stdout.trim());
755
+ }
756
+ // Try HEAD symref (the branch the repo is currently on — valid for local repos
757
+ // without detached HEAD; do not use when detached since it could be a feature branch)
758
+ const headSymref = execGit(['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd: repoRoot });
759
+ if (gitResultOk(headSymref) && headSymref.stdout.trim()) {
760
+ const headBranch = headSymref.stdout.trim();
761
+ if (!candidateBranches.includes(headBranch)) {
762
+ candidateBranches.push(headBranch);
763
+ }
764
+ }
765
+ // Always include main and master as universal fallbacks
766
+ for (const b of ['main', 'master']) {
767
+ if (!candidateBranches.includes(b)) candidateBranches.push(b);
768
+ }
769
+ for (const candidate of candidateBranches) {
770
+ const r = execGit(['rev-parse', candidate], { cwd: repoRoot });
771
+ if (gitResultOk(r)) {
772
+ mainTip = r.stdout.trim();
773
+ break;
774
+ }
775
+ }
776
+ if (!mainTip) return results;
777
+ }
778
+
779
+ // 3. Build a canonical-path → listed-path index from git worktree list.
780
+ // git worktree list shows paths AS PROVIDED to git worktree add.
781
+ // On macOS, os.tmpdir() may be /var/folders/... (symlink) while git writes
782
+ // /private/var/folders/... (real path) in the gitdir file. We need the
783
+ // LISTED path for git worktree unlock/remove to find the worktree.
784
+ const listedResult = execGit(['worktree', 'list', '--porcelain'], { cwd: repoRoot });
785
+ const canonicalToListed = new Map();
786
+ if (gitResultOk(listedResult)) {
787
+ // Normalize CRLF → LF before splitting: git on Windows may emit CRLF in
788
+ // porcelain output, which would break block splitting on '\n\n'.
789
+ const normalizedListed = listedResult.stdout.replace(/\r\n/g, '\n');
790
+ for (const block of normalizedListed.split('\n\n').filter(Boolean)) {
791
+ const wtLine = block.split('\n').find((l) => l.startsWith('worktree '));
792
+ if (!wtLine) continue;
793
+ const listed = wtLine.slice('worktree '.length).trim();
794
+ try {
795
+ const canonical = fs.realpathSync.native(listed);
796
+ canonicalToListed.set(canonical, listed);
797
+ } catch {
798
+ // If the path doesn't exist (already removed), skip silently.
799
+ }
800
+ }
801
+ }
802
+
803
+ // 4. Process each worktree admin entry that has a 'locked' file.
804
+ for (const entryName of entries) {
805
+ const adminDir = path.join(worktreesAdminDir, entryName);
806
+ const lockedFile = path.join(adminDir, 'locked');
807
+ const lockedContent = readFileSafe(lockedFile);
808
+ if (lockedContent === null) continue; // no lock file — not our concern
809
+
810
+ // Resolve the actual worktree path from the gitdir pointer.
811
+ // The gitdir file contains a path like "../../<name>/.git" relative to adminDir.
812
+ // Strip the trailing .git segment (cross-platform: handle both / and \).
813
+ const gitdirFile = path.join(adminDir, 'gitdir');
814
+ const gitdirContent = readFileSafe(gitdirFile);
815
+ if (!gitdirContent) continue;
816
+ const resolvedGitFile = path.resolve(adminDir, gitdirContent.trim());
817
+ const worktreePath = path.basename(resolvedGitFile) === '.git'
818
+ ? path.dirname(resolvedGitFile)
819
+ : resolvedGitFile;
820
+
821
+ // Look up the git-list path (the path git knows about) for use in
822
+ // git worktree unlock/remove commands. Falls back to worktreePath if
823
+ // not found (e.g. already removed, or no symlink ambiguity).
824
+ let gitKnownPath = worktreePath;
825
+ try {
826
+ const canonical = fs.realpathSync.native(worktreePath);
827
+ gitKnownPath = canonicalToListed.get(canonical) || worktreePath;
828
+ } catch {
829
+ // worktreePath may not exist yet (already removed); use as-is.
830
+ }
831
+
832
+ // 4a. Stale-lock guard: skip if lock is too fresh (PID recycling / race).
833
+ const lockMtime = mtimeSafe(lockedFile);
834
+ if (!lockMtime || Date.now() - lockMtime.getTime() < reapMtimeGuardMs) {
835
+ results.push({ path: worktreePath, status: 'skipped', reason: 'lock_too_fresh' });
836
+ continue;
837
+ }
838
+
839
+ // 4b. PID liveness check.
840
+ // Fail-closed: any lock content that does not parse as a numeric PID (e.g.
841
+ // "Locked by claude-code agent-xxxx") is treated as ALIVE — we cannot
842
+ // confirm the owner is dead, so we must not reap. This includes the real
843
+ // Claude Code lock format which is non-numeric text.
844
+ const pidStr = lockedContent.trim().match(/^\d+/)?.[0];
845
+ if (!pidStr) {
846
+ results.push({ path: worktreePath, status: 'skipped', reason: 'lock_owner_unknown' });
847
+ continue;
848
+ }
849
+ const pid = parseInt(pidStr, 10);
850
+ // Wrap isPidAlive in try/catch: any error (e.g. EPERM on Windows when the process
851
+ // exists but is owned by another user) must be treated as ALIVE (fail-closed).
852
+ let pidIsAlive;
853
+ try {
854
+ pidIsAlive = Number.isNaN(pid) || isPidAlive(pid);
855
+ } catch {
856
+ pidIsAlive = true; // Cannot determine liveness — treat as alive, do not reap.
857
+ }
858
+ if (pidIsAlive) {
859
+ results.push({ path: worktreePath, status: 'skipped', reason: 'pid_alive' });
860
+ continue;
861
+ }
862
+
863
+ // 4c. Ancestry guard: branch-tip must be reachable from main (fail closed).
864
+ // The admin HEAD file contains either "ref: refs/heads/<branch>" or a bare SHA.
865
+ // We read the file directly (no non-standard git ref parsing).
866
+ let branchTip;
867
+ {
868
+ const headContent = readFileSafe(path.join(adminDir, 'HEAD'));
869
+ if (!headContent) {
870
+ results.push({ path: worktreePath, status: 'skipped', reason: 'cannot_resolve_branch_tip' });
871
+ continue;
872
+ }
873
+ const trimmed = headContent.trim();
874
+ if (trimmed.startsWith('ref: refs/heads/')) {
875
+ // Symbolic ref — resolve to commit SHA via git
876
+ const branchName = trimmed.slice('ref: refs/heads/'.length);
877
+ const resolveResult = execGit(['rev-parse', `refs/heads/${branchName}`], { cwd: repoRoot });
878
+ if (!gitResultOk(resolveResult)) {
879
+ results.push({ path: worktreePath, status: 'skipped', reason: 'cannot_resolve_branch_tip' });
880
+ continue;
881
+ }
882
+ branchTip = resolveResult.stdout.trim();
883
+ } else if (/^[0-9a-f]{40}$/i.test(trimmed)) {
884
+ // Detached HEAD — bare SHA
885
+ branchTip = trimmed;
886
+ } else {
887
+ results.push({ path: worktreePath, status: 'skipped', reason: 'cannot_resolve_branch_tip' });
888
+ continue;
889
+ }
890
+ }
891
+
892
+ const ancestorCheck = execGit(
893
+ ['merge-base', '--is-ancestor', branchTip, mainTip],
894
+ { cwd: repoRoot }
895
+ );
896
+ if (!gitResultOk(ancestorCheck)) {
897
+ results.push({ path: worktreePath, status: 'skipped', reason: 'branch_not_merged' });
898
+ continue;
899
+ }
900
+
901
+ // 4d. Reap: unlock → remove --force.
902
+ // Use gitKnownPath (from git worktree list) so that git can locate the
903
+ // worktree even when the path in the gitdir file differs due to symlinks
904
+ // (e.g. macOS /var/folders vs /private/var/folders).
905
+ execGit(['worktree', 'unlock', gitKnownPath], { cwd: repoRoot }); // ignore failure (already unlocked)
906
+ const removeResult = execGit(['worktree', 'remove', gitKnownPath, '--force'], { cwd: repoRoot });
907
+ if (!gitResultOk(removeResult)) {
908
+ results.push({ path: worktreePath, status: 'skipped', reason: 'remove_failed' });
909
+ continue;
910
+ }
911
+
912
+ // Use the git-listed path so the result is consistent with what callers see
913
+ // from 'git worktree list', avoiding symlink vs real-path mismatches on macOS.
914
+ results.push({ path: gitKnownPath, status: 'reaped', reason: 'pid_dead_and_merged' });
915
+ }
916
+
917
+ // 5. Always prune stale metadata (handles missing-on-disk entries).
918
+ execGit(['worktree', 'prune'], { cwd: repoRoot });
919
+
920
+ return results;
921
+ }
922
+
923
+ // ─── reapOrphanWorktrees deps helpers ─────────────────────────────────────────
924
+
925
+ function defaultIsPidAlive(pid) {
926
+ // process.kill(pid, 0) probes process existence without sending a real signal.
927
+ // - Returns normally → process is alive.
928
+ // - Throws ESRCH → process does not exist → dead.
929
+ // - Throws EPERM → process exists but we lack permission (alive; fail-closed
930
+ // on Windows where cross-user processes throw EPERM, not ESRCH).
931
+ try {
932
+ process.kill(pid, 0);
933
+ return true;
934
+ } catch (err) {
935
+ // EPERM means the process exists but we cannot signal it.
936
+ // Treat as alive (fail-closed: do not reap a process we cannot confirm dead).
937
+ if (err && err.code === 'EPERM') return true;
938
+ return false;
939
+ }
940
+ }
941
+
942
+ function defaultReadDirSafe(dir) {
943
+ try { return fs.readdirSync(dir); } catch { return null; }
944
+ }
945
+
946
+ function defaultReadFileSafe(file) {
947
+ try { return fs.readFileSync(file, 'utf8'); } catch { return null; }
948
+ }
949
+
950
+ function defaultMtimeSafe(file) {
951
+ try { return fs.statSync(file).mtime; } catch { return null; }
952
+ }
953
+
954
+ function cmdWorktreeReapOrphans(cwd) {
955
+ let result;
956
+ try {
957
+ result = reapOrphanWorktrees(cwd);
958
+ } catch (err) {
959
+ // Surface failure as a one-line warning; keep exit-zero so workflows don't break.
960
+ process.stderr.write(`[gsd] worktree.reap-orphans failed: ${err && err.message ? err.message : String(err)}\n`);
961
+ result = [];
962
+ }
963
+ const skippedCount = result.filter((r) => r.status === 'skipped').length;
964
+ if (skippedCount > 0) {
965
+ // Surface skipped entries so operators are aware of unresolved orphans.
966
+ process.stderr.write(`[gsd] worktree.reap-orphans: ${skippedCount} orphan(s) skipped (run with DEBUG=1 for details)\n`);
967
+ }
968
+ process.stdout.write(`${JSON.stringify({ ok: true, reaped: result.filter((r) => r.status === 'reaped').length, entries: result }, null, 2)}\n`);
969
+ }
970
+
971
+ module.exports = {
972
+ resolveWorktreeContext,
973
+ parseWorktreePorcelain,
974
+ planWorktreePrune,
975
+ executeWorktreePrunePlan,
976
+ listLinkedWorktreePaths,
977
+ inspectWorktreeHealth,
978
+ snapshotWorktreeInventory,
979
+ normalizeCleanupManifest,
980
+ planWorktreeWaveCleanup,
981
+ executeWorktreeWaveCleanupPlan,
982
+ cmdWorktreeCleanupWave,
983
+ reapOrphanWorktrees,
984
+ cmdWorktreeReapOrphans,
985
+ };