@paths.design/caws-cli 10.1.0 → 11.0.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.
Files changed (419) hide show
  1. package/README.md +125 -374
  2. package/dist/index.js +43 -756
  3. package/dist/shell/binding/resolve-binding.d.ts +4 -0
  4. package/dist/shell/binding/resolve-binding.d.ts.map +1 -0
  5. package/dist/shell/binding/resolve-binding.js +228 -0
  6. package/dist/shell/binding/resolve-binding.js.map +1 -0
  7. package/dist/shell/binding/types.d.ts +42 -0
  8. package/dist/shell/binding/types.d.ts.map +1 -0
  9. package/dist/shell/binding/types.js +21 -0
  10. package/dist/shell/binding/types.js.map +1 -0
  11. package/dist/shell/commands/claim.d.ts +14 -0
  12. package/dist/shell/commands/claim.d.ts.map +1 -0
  13. package/dist/shell/commands/claim.js +197 -0
  14. package/dist/shell/commands/claim.js.map +1 -0
  15. package/dist/shell/commands/doctor.d.ts +13 -0
  16. package/dist/shell/commands/doctor.d.ts.map +1 -0
  17. package/dist/shell/commands/doctor.js +97 -0
  18. package/dist/shell/commands/doctor.js.map +1 -0
  19. package/dist/shell/commands/evidence.d.ts +28 -0
  20. package/dist/shell/commands/evidence.d.ts.map +1 -0
  21. package/dist/shell/commands/evidence.js +166 -0
  22. package/dist/shell/commands/evidence.js.map +1 -0
  23. package/dist/shell/commands/gates.d.ts +19 -0
  24. package/dist/shell/commands/gates.d.ts.map +1 -0
  25. package/dist/shell/commands/gates.js +181 -0
  26. package/dist/shell/commands/gates.js.map +1 -0
  27. package/dist/shell/commands/init.d.ts +8 -0
  28. package/dist/shell/commands/init.d.ts.map +1 -0
  29. package/dist/shell/commands/init.js +64 -0
  30. package/dist/shell/commands/init.js.map +1 -0
  31. package/dist/shell/commands/scope.d.ts +11 -0
  32. package/dist/shell/commands/scope.d.ts.map +1 -0
  33. package/dist/shell/commands/scope.js +92 -0
  34. package/dist/shell/commands/scope.js.map +1 -0
  35. package/dist/shell/commands/status.d.ts +15 -0
  36. package/dist/shell/commands/status.d.ts.map +1 -0
  37. package/dist/shell/commands/status.js +106 -0
  38. package/dist/shell/commands/status.js.map +1 -0
  39. package/dist/shell/commands/waiver.d.ts +38 -0
  40. package/dist/shell/commands/waiver.d.ts.map +1 -0
  41. package/dist/shell/commands/waiver.js +240 -0
  42. package/dist/shell/commands/waiver.js.map +1 -0
  43. package/dist/shell/gates/disposition.d.ts +23 -0
  44. package/dist/shell/gates/disposition.d.ts.map +1 -0
  45. package/dist/shell/gates/disposition.js +87 -0
  46. package/dist/shell/gates/disposition.js.map +1 -0
  47. package/dist/shell/gates/gate-result-contract.d.ts +39 -0
  48. package/dist/shell/gates/gate-result-contract.d.ts.map +1 -0
  49. package/dist/shell/gates/gate-result-contract.js +150 -0
  50. package/dist/shell/gates/gate-result-contract.js.map +1 -0
  51. package/dist/shell/gates/quality-gates-adapter.d.ts +55 -0
  52. package/dist/shell/gates/quality-gates-adapter.d.ts.map +1 -0
  53. package/dist/shell/gates/quality-gates-adapter.js +161 -0
  54. package/dist/shell/gates/quality-gates-adapter.js.map +1 -0
  55. package/dist/shell/gates/waiver-filter.d.ts +58 -0
  56. package/dist/shell/gates/waiver-filter.d.ts.map +1 -0
  57. package/dist/shell/gates/waiver-filter.js +119 -0
  58. package/dist/shell/gates/waiver-filter.js.map +1 -0
  59. package/dist/shell/index.d.ts +50 -0
  60. package/dist/shell/index.d.ts.map +1 -0
  61. package/dist/shell/index.js +73 -0
  62. package/dist/shell/index.js.map +1 -0
  63. package/dist/shell/register.d.ts +11 -0
  64. package/dist/shell/register.d.ts.map +1 -0
  65. package/dist/shell/register.js +274 -0
  66. package/dist/shell/register.js.map +1 -0
  67. package/dist/shell/render/claim.d.ts +22 -0
  68. package/dist/shell/render/claim.d.ts.map +1 -0
  69. package/dist/shell/render/claim.js +75 -0
  70. package/dist/shell/render/claim.js.map +1 -0
  71. package/dist/shell/render/decision.d.ts +15 -0
  72. package/dist/shell/render/decision.d.ts.map +1 -0
  73. package/dist/shell/render/decision.js +66 -0
  74. package/dist/shell/render/decision.js.map +1 -0
  75. package/dist/shell/render/diagnostic.d.ts +19 -0
  76. package/dist/shell/render/diagnostic.d.ts.map +1 -0
  77. package/dist/shell/render/diagnostic.js +76 -0
  78. package/dist/shell/render/diagnostic.js.map +1 -0
  79. package/dist/shell/render/finding.d.ts +15 -0
  80. package/dist/shell/render/finding.d.ts.map +1 -0
  81. package/dist/shell/render/finding.js +57 -0
  82. package/dist/shell/render/finding.js.map +1 -0
  83. package/dist/shell/render/gates.d.ts +3 -0
  84. package/dist/shell/render/gates.d.ts.map +1 -0
  85. package/dist/shell/render/gates.js +56 -0
  86. package/dist/shell/render/gates.js.map +1 -0
  87. package/dist/shell/render/init.d.ts +11 -0
  88. package/dist/shell/render/init.d.ts.map +1 -0
  89. package/dist/shell/render/init.js +32 -0
  90. package/dist/shell/render/init.js.map +1 -0
  91. package/dist/shell/render/status.d.ts +26 -0
  92. package/dist/shell/render/status.d.ts.map +1 -0
  93. package/dist/shell/render/status.js +143 -0
  94. package/dist/shell/render/status.js.map +1 -0
  95. package/dist/shell/render/waiver.d.ts +21 -0
  96. package/dist/shell/render/waiver.d.ts.map +1 -0
  97. package/dist/shell/render/waiver.js +94 -0
  98. package/dist/shell/render/waiver.js.map +1 -0
  99. package/dist/shell/rules.d.ts +37 -0
  100. package/dist/shell/rules.d.ts.map +1 -0
  101. package/dist/shell/rules.js +51 -0
  102. package/dist/shell/rules.js.map +1 -0
  103. package/dist/shell/session/actor.d.ts +14 -0
  104. package/dist/shell/session/actor.d.ts.map +1 -0
  105. package/dist/shell/session/actor.js +34 -0
  106. package/dist/shell/session/actor.js.map +1 -0
  107. package/dist/shell/session/resolve-session.d.ts +5 -0
  108. package/dist/shell/session/resolve-session.d.ts.map +1 -0
  109. package/dist/shell/session/resolve-session.js +239 -0
  110. package/dist/shell/session/resolve-session.js.map +1 -0
  111. package/dist/shell/session/types.d.ts +56 -0
  112. package/dist/shell/session/types.d.ts.map +1 -0
  113. package/dist/shell/session/types.js +15 -0
  114. package/dist/shell/session/types.js.map +1 -0
  115. package/dist/store/agents-store.d.ts +3 -0
  116. package/dist/store/agents-store.d.ts.map +1 -0
  117. package/dist/store/agents-store.js +63 -0
  118. package/dist/store/agents-store.js.map +1 -0
  119. package/dist/store/apply-patch.d.ts +16 -0
  120. package/dist/store/apply-patch.d.ts.map +1 -0
  121. package/dist/store/apply-patch.js +191 -0
  122. package/dist/store/apply-patch.js.map +1 -0
  123. package/dist/store/atomic-write.d.ts +16 -0
  124. package/dist/store/atomic-write.d.ts.map +1 -0
  125. package/dist/store/atomic-write.js +132 -0
  126. package/dist/store/atomic-write.js.map +1 -0
  127. package/dist/store/doctor-snapshot.d.ts +20 -0
  128. package/dist/store/doctor-snapshot.d.ts.map +1 -0
  129. package/dist/store/doctor-snapshot.js +176 -0
  130. package/dist/store/doctor-snapshot.js.map +1 -0
  131. package/dist/store/events-store.d.ts +33 -0
  132. package/dist/store/events-store.d.ts.map +1 -0
  133. package/dist/store/events-store.js +297 -0
  134. package/dist/store/events-store.js.map +1 -0
  135. package/dist/store/index.d.ts +21 -0
  136. package/dist/store/index.d.ts.map +1 -0
  137. package/dist/store/index.js +47 -0
  138. package/dist/store/index.js.map +1 -0
  139. package/dist/store/init-store.d.ts +21 -0
  140. package/dist/store/init-store.d.ts.map +1 -0
  141. package/dist/store/init-store.js +295 -0
  142. package/dist/store/init-store.js.map +1 -0
  143. package/dist/store/json-store.d.ts +3 -0
  144. package/dist/store/json-store.d.ts.map +1 -0
  145. package/dist/store/json-store.js +65 -0
  146. package/dist/store/json-store.js.map +1 -0
  147. package/dist/store/policy-store.d.ts +3 -0
  148. package/dist/store/policy-store.d.ts.map +1 -0
  149. package/dist/store/policy-store.js +65 -0
  150. package/dist/store/policy-store.js.map +1 -0
  151. package/dist/store/repo-root.d.ts +46 -0
  152. package/dist/store/repo-root.d.ts.map +1 -0
  153. package/dist/store/repo-root.js +145 -0
  154. package/dist/store/repo-root.js.map +1 -0
  155. package/dist/store/rules.d.ts +53 -0
  156. package/dist/store/rules.d.ts.map +1 -0
  157. package/dist/store/rules.js +78 -0
  158. package/dist/store/rules.js.map +1 -0
  159. package/dist/store/specs-store.d.ts +3 -0
  160. package/dist/store/specs-store.d.ts.map +1 -0
  161. package/dist/store/specs-store.js +131 -0
  162. package/dist/store/specs-store.js.map +1 -0
  163. package/dist/store/types.d.ts +84 -0
  164. package/dist/store/types.d.ts.map +1 -0
  165. package/dist/store/types.js +14 -0
  166. package/dist/store/types.js.map +1 -0
  167. package/dist/store/waivers-store.d.ts +25 -0
  168. package/dist/store/waivers-store.d.ts.map +1 -0
  169. package/dist/store/waivers-store.js +232 -0
  170. package/dist/store/waivers-store.js.map +1 -0
  171. package/dist/store/worktrees-store.d.ts +3 -0
  172. package/dist/store/worktrees-store.d.ts.map +1 -0
  173. package/dist/store/worktrees-store.js +62 -0
  174. package/dist/store/worktrees-store.js.map +1 -0
  175. package/dist/store/yaml-store.d.ts +9 -0
  176. package/dist/store/yaml-store.d.ts.map +1 -0
  177. package/dist/store/yaml-store.js +121 -0
  178. package/dist/store/yaml-store.js.map +1 -0
  179. package/package.json +15 -13
  180. package/dist/budget-derivation.js +0 -751
  181. package/dist/cicd-optimizer.js +0 -504
  182. package/dist/commands/archive.js +0 -500
  183. package/dist/commands/burnup.js +0 -198
  184. package/dist/commands/diagnose.js +0 -525
  185. package/dist/commands/evaluate.js +0 -314
  186. package/dist/commands/gates.js +0 -149
  187. package/dist/commands/init.js +0 -857
  188. package/dist/commands/iterate.js +0 -417
  189. package/dist/commands/mode.js +0 -269
  190. package/dist/commands/parallel.js +0 -242
  191. package/dist/commands/plan.js +0 -438
  192. package/dist/commands/provenance.js +0 -1143
  193. package/dist/commands/quality-monitor.js +0 -284
  194. package/dist/commands/scope.js +0 -264
  195. package/dist/commands/session.js +0 -312
  196. package/dist/commands/sidecar.js +0 -74
  197. package/dist/commands/specs.js +0 -1448
  198. package/dist/commands/status.js +0 -1151
  199. package/dist/commands/templates.js +0 -237
  200. package/dist/commands/tool.js +0 -136
  201. package/dist/commands/tutorial.js +0 -480
  202. package/dist/commands/validate.js +0 -357
  203. package/dist/commands/verify-acs.js +0 -443
  204. package/dist/commands/waivers.js +0 -599
  205. package/dist/commands/workflow.js +0 -243
  206. package/dist/commands/worktree.js +0 -386
  207. package/dist/config/lite-scope.js +0 -158
  208. package/dist/config/modes.js +0 -347
  209. package/dist/constants/spec-types.js +0 -65
  210. package/dist/gates/budget-limit.js +0 -121
  211. package/dist/gates/feedback.js +0 -260
  212. package/dist/gates/format.js +0 -179
  213. package/dist/gates/god-object.js +0 -117
  214. package/dist/gates/pipeline.js +0 -167
  215. package/dist/gates/scope-boundary.js +0 -93
  216. package/dist/gates/spec-completeness.js +0 -109
  217. package/dist/gates/todo-detection.js +0 -205
  218. package/dist/generators/jest-config-generator.js +0 -242
  219. package/dist/generators/working-spec.js +0 -237
  220. package/dist/minimal-cli.js +0 -88
  221. package/dist/parallel/parallel-manager.js +0 -433
  222. package/dist/policy/PolicyManager.js +0 -465
  223. package/dist/scaffold/claude-hooks.js +0 -443
  224. package/dist/scaffold/cursor-hooks.js +0 -177
  225. package/dist/scaffold/git-hooks.js +0 -928
  226. package/dist/scaffold/index.js +0 -794
  227. package/dist/session/session-manager.js +0 -653
  228. package/dist/sidecars/index.js +0 -33
  229. package/dist/sidecars/listeners.js +0 -40
  230. package/dist/sidecars/provenance-summary.js +0 -238
  231. package/dist/sidecars/quality-gaps.js +0 -258
  232. package/dist/sidecars/schema.js +0 -149
  233. package/dist/sidecars/spec-drift.js +0 -151
  234. package/dist/sidecars/waiver-draft.js +0 -176
  235. package/dist/spec/SpecFileManager.js +0 -419
  236. package/dist/templates/.caws/schemas/policy.schema.json +0 -112
  237. package/dist/templates/.caws/schemas/scope.schema.json +0 -52
  238. package/dist/templates/.caws/schemas/waivers.schema.json +0 -106
  239. package/dist/templates/.caws/schemas/working-spec.schema.json +0 -340
  240. package/dist/templates/.caws/schemas/worktrees.schema.json +0 -38
  241. package/dist/templates/.caws/templates/working-spec.template.yml +0 -80
  242. package/dist/templates/.caws/tools/README.md +0 -18
  243. package/dist/templates/.caws/tools/scope-guard.js +0 -203
  244. package/dist/templates/.caws/tools-allow.json +0 -331
  245. package/dist/templates/.caws/waivers.yml +0 -19
  246. package/dist/templates/.claude/README.md +0 -190
  247. package/dist/templates/.claude/hooks/audit.sh +0 -121
  248. package/dist/templates/.claude/hooks/block-dangerous.sh +0 -203
  249. package/dist/templates/.claude/hooks/classify_command.py +0 -592
  250. package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
  251. package/dist/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
  252. package/dist/templates/.claude/hooks/naming-check.sh +0 -100
  253. package/dist/templates/.claude/hooks/protected-paths.sh +0 -39
  254. package/dist/templates/.claude/hooks/quality-check.sh +0 -81
  255. package/dist/templates/.claude/hooks/scan-secrets.sh +0 -85
  256. package/dist/templates/.claude/hooks/scope-guard.sh +0 -381
  257. package/dist/templates/.claude/hooks/session-caws-status.sh +0 -117
  258. package/dist/templates/.claude/hooks/session-log.sh +0 -634
  259. package/dist/templates/.claude/hooks/simplification-guard.sh +0 -92
  260. package/dist/templates/.claude/hooks/stop-worktree-check.sh +0 -46
  261. package/dist/templates/.claude/hooks/test_classify_command.py +0 -370
  262. package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
  263. package/dist/templates/.claude/hooks/validate-spec.sh +0 -76
  264. package/dist/templates/.claude/hooks/worktree-guard.sh +0 -220
  265. package/dist/templates/.claude/hooks/worktree-write-guard.sh +0 -190
  266. package/dist/templates/.claude/rules/git-safety.md +0 -26
  267. package/dist/templates/.claude/rules/worktree-isolation.md +0 -83
  268. package/dist/templates/.claude/settings.json +0 -141
  269. package/dist/templates/.cursor/README.md +0 -299
  270. package/dist/templates/.cursor/hooks/audit.sh +0 -55
  271. package/dist/templates/.cursor/hooks/block-dangerous.sh +0 -84
  272. package/dist/templates/.cursor/hooks/caws-quality-check.sh +0 -52
  273. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
  274. package/dist/templates/.cursor/hooks/format.sh +0 -38
  275. package/dist/templates/.cursor/hooks/naming-check.sh +0 -64
  276. package/dist/templates/.cursor/hooks/scan-secrets.sh +0 -51
  277. package/dist/templates/.cursor/hooks/scope-guard.sh +0 -52
  278. package/dist/templates/.cursor/hooks/session-log.sh +0 -924
  279. package/dist/templates/.cursor/hooks/validate-spec.sh +0 -83
  280. package/dist/templates/.cursor/hooks.json +0 -76
  281. package/dist/templates/.cursor/rules/00-claims-verification.mdc +0 -144
  282. package/dist/templates/.cursor/rules/01-working-style.mdc +0 -50
  283. package/dist/templates/.cursor/rules/02-quality-gates.mdc +0 -368
  284. package/dist/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
  285. package/dist/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
  286. package/dist/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
  287. package/dist/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
  288. package/dist/templates/.cursor/rules/07-process-ops.mdc +0 -20
  289. package/dist/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
  290. package/dist/templates/.cursor/rules/09-docstrings.mdc +0 -89
  291. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
  292. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
  293. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
  294. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
  295. package/dist/templates/.cursor/rules/README.md +0 -148
  296. package/dist/templates/.github/copilot-instructions.md +0 -82
  297. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
  298. package/dist/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
  299. package/dist/templates/.junie/guidelines.md +0 -73
  300. package/dist/templates/.vscode/launch.json +0 -17
  301. package/dist/templates/.vscode/settings.json +0 -95
  302. package/dist/templates/.windsurf/rules/caws-quality-standards.md +0 -54
  303. package/dist/templates/.windsurf/workflows/caws-guided-development.md +0 -92
  304. package/dist/templates/CLAUDE.md +0 -174
  305. package/dist/templates/COMMIT_CONVENTIONS.md +0 -86
  306. package/dist/templates/OIDC_SETUP.md +0 -300
  307. package/dist/templates/agents.md +0 -145
  308. package/dist/templates/codemod/README.md +0 -1
  309. package/dist/templates/codemod/test.js +0 -93
  310. package/dist/templates/docs/README.md +0 -151
  311. package/dist/templates/scripts/new_feature.sh +0 -80
  312. package/dist/templates/scripts/quality-gates/check-god-objects.js +0 -146
  313. package/dist/templates/scripts/quality-gates/run-quality-gates.js +0 -50
  314. package/dist/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
  315. package/dist/test-analysis.js +0 -786
  316. package/dist/tool-interface.js +0 -314
  317. package/dist/tool-loader.js +0 -303
  318. package/dist/tool-validator.js +0 -393
  319. package/dist/utils/agent-session.js +0 -202
  320. package/dist/utils/async-utils.js +0 -188
  321. package/dist/utils/command-wrapper.js +0 -200
  322. package/dist/utils/event-log.js +0 -584
  323. package/dist/utils/event-renderer.js +0 -521
  324. package/dist/utils/finalization.js +0 -230
  325. package/dist/utils/git-lock.js +0 -119
  326. package/dist/utils/gitignore-updater.js +0 -158
  327. package/dist/utils/ide-detection.js +0 -133
  328. package/dist/utils/lifecycle-events.js +0 -94
  329. package/dist/utils/project-analysis.js +0 -367
  330. package/dist/utils/promise-utils.js +0 -72
  331. package/dist/utils/quality-gates-errors.js +0 -520
  332. package/dist/utils/quality-gates-utils.js +0 -387
  333. package/dist/utils/schema-validator.js +0 -50
  334. package/dist/utils/spec-resolver.js +0 -711
  335. package/dist/utils/typescript-detector.js +0 -369
  336. package/dist/utils/working-state.js +0 -530
  337. package/dist/utils/yaml-validation.js +0 -156
  338. package/dist/validation/spec-validation.js +0 -921
  339. package/dist/waivers-manager.js +0 -732
  340. package/dist/worktree/worktree-manager.js +0 -1374
  341. package/templates/.caws/schemas/policy.schema.json +0 -112
  342. package/templates/.caws/schemas/scope.schema.json +0 -52
  343. package/templates/.caws/schemas/waivers.schema.json +0 -106
  344. package/templates/.caws/schemas/working-spec.schema.json +0 -340
  345. package/templates/.caws/schemas/worktrees.schema.json +0 -38
  346. package/templates/.caws/templates/working-spec.template.yml +0 -80
  347. package/templates/.caws/tools/README.md +0 -18
  348. package/templates/.caws/tools/scope-guard.js +0 -203
  349. package/templates/.caws/tools-allow.json +0 -331
  350. package/templates/.caws/waivers.yml +0 -19
  351. package/templates/.claude/README.md +0 -190
  352. package/templates/.claude/hooks/audit.sh +0 -121
  353. package/templates/.claude/hooks/block-dangerous.sh +0 -203
  354. package/templates/.claude/hooks/classify_command.py +0 -592
  355. package/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
  356. package/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
  357. package/templates/.claude/hooks/naming-check.sh +0 -100
  358. package/templates/.claude/hooks/protected-paths.sh +0 -39
  359. package/templates/.claude/hooks/quality-check.sh +0 -81
  360. package/templates/.claude/hooks/scan-secrets.sh +0 -85
  361. package/templates/.claude/hooks/scope-guard.sh +0 -381
  362. package/templates/.claude/hooks/session-caws-status.sh +0 -117
  363. package/templates/.claude/hooks/session-log.sh +0 -634
  364. package/templates/.claude/hooks/simplification-guard.sh +0 -92
  365. package/templates/.claude/hooks/stop-worktree-check.sh +0 -46
  366. package/templates/.claude/hooks/test_classify_command.py +0 -370
  367. package/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
  368. package/templates/.claude/hooks/validate-spec.sh +0 -76
  369. package/templates/.claude/hooks/worktree-guard.sh +0 -220
  370. package/templates/.claude/hooks/worktree-write-guard.sh +0 -190
  371. package/templates/.claude/rules/git-safety.md +0 -26
  372. package/templates/.claude/rules/worktree-isolation.md +0 -83
  373. package/templates/.claude/settings.json +0 -141
  374. package/templates/.cursor/README.md +0 -299
  375. package/templates/.cursor/hooks/audit.sh +0 -55
  376. package/templates/.cursor/hooks/block-dangerous.sh +0 -84
  377. package/templates/.cursor/hooks/caws-quality-check.sh +0 -52
  378. package/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
  379. package/templates/.cursor/hooks/format.sh +0 -38
  380. package/templates/.cursor/hooks/naming-check.sh +0 -64
  381. package/templates/.cursor/hooks/scan-secrets.sh +0 -51
  382. package/templates/.cursor/hooks/scope-guard.sh +0 -52
  383. package/templates/.cursor/hooks/session-log.sh +0 -924
  384. package/templates/.cursor/hooks/validate-spec.sh +0 -83
  385. package/templates/.cursor/hooks.json +0 -76
  386. package/templates/.cursor/rules/00-claims-verification.mdc +0 -144
  387. package/templates/.cursor/rules/01-working-style.mdc +0 -50
  388. package/templates/.cursor/rules/02-quality-gates.mdc +0 -368
  389. package/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
  390. package/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
  391. package/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
  392. package/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
  393. package/templates/.cursor/rules/07-process-ops.mdc +0 -20
  394. package/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
  395. package/templates/.cursor/rules/09-docstrings.mdc +0 -89
  396. package/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
  397. package/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
  398. package/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
  399. package/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
  400. package/templates/.cursor/rules/README.md +0 -148
  401. package/templates/.github/copilot-instructions.md +0 -82
  402. package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
  403. package/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
  404. package/templates/.junie/guidelines.md +0 -73
  405. package/templates/.vscode/launch.json +0 -17
  406. package/templates/.vscode/settings.json +0 -95
  407. package/templates/.windsurf/rules/caws-quality-standards.md +0 -54
  408. package/templates/.windsurf/workflows/caws-guided-development.md +0 -92
  409. package/templates/CLAUDE.md +0 -174
  410. package/templates/COMMIT_CONVENTIONS.md +0 -86
  411. package/templates/OIDC_SETUP.md +0 -300
  412. package/templates/agents.md +0 -145
  413. package/templates/codemod/README.md +0 -1
  414. package/templates/codemod/test.js +0 -93
  415. package/templates/docs/README.md +0 -151
  416. package/templates/scripts/new_feature.sh +0 -80
  417. package/templates/scripts/quality-gates/check-god-objects.js +0 -146
  418. package/templates/scripts/quality-gates/run-quality-gates.js +0 -50
  419. package/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
@@ -1,1374 +0,0 @@
1
- /**
2
- * @fileoverview CAWS Git Worktree Manager
3
- * Provides CRUD operations for git worktrees with scope isolation
4
- * @author @darianrosebrook
5
- */
6
-
7
- const { execFileSync } = require('child_process');
8
- const fs = require('fs-extra');
9
- const path = require('path');
10
- const chalk = require('chalk');
11
- const { createValidator, getSchemaPath } = require('../utils/schema-validator');
12
- const { getAgentSessionId } = require('../utils/agent-session');
13
- const { lifecycle, EVENTS } = require('../utils/lifecycle-events');
14
-
15
- const WORKTREES_DIR = '.caws/worktrees';
16
- const REGISTRY_FILE = '.caws/worktrees.json';
17
- const BRANCH_PREFIX = 'caws/';
18
-
19
- function findFeatureSpecPath(root, specId) {
20
- if (!specId) return null;
21
-
22
- const candidates = [
23
- path.join(root, '.caws', 'specs', `${specId}.yaml`),
24
- path.join(root, '.caws', 'specs', `${specId}.yml`),
25
- ];
26
-
27
- return candidates.find((candidate) => fs.existsSync(candidate)) || null;
28
- }
29
-
30
- function writeSpecWithWorktree(filePath, worktreeName) {
31
- const yaml = require('js-yaml');
32
- const content = fs.readFileSync(filePath, 'utf8');
33
- const parsed = yaml.load(content);
34
- if (!parsed || typeof parsed !== 'object') {
35
- return content;
36
- }
37
-
38
- parsed.worktree = worktreeName;
39
- return yaml.dump(parsed, { lineWidth: 120, noRefs: true });
40
- }
41
-
42
- function hasPathChanges(root, relativePath) {
43
- try {
44
- const output = execFileSync(
45
- 'git',
46
- ['status', '--porcelain', '--', relativePath],
47
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
48
- ).trim();
49
- return output.length > 0;
50
- } catch {
51
- return false;
52
- }
53
- }
54
-
55
- function ensureCanonicalSpecCommitted(root, specPath, specId, worktreeName) {
56
- const relativeSpecPath = path.relative(root, specPath);
57
- const nextContent = writeSpecWithWorktree(specPath, worktreeName);
58
- const currentContent = fs.readFileSync(specPath, 'utf8');
59
-
60
- if (currentContent !== nextContent) {
61
- fs.writeFileSync(specPath, nextContent);
62
- }
63
-
64
- if (!hasPathChanges(root, relativeSpecPath)) {
65
- return false;
66
- }
67
-
68
- execFileSync('git', ['add', '--', relativeSpecPath], {
69
- cwd: root,
70
- stdio: 'pipe',
71
- });
72
- execFileSync(
73
- 'git',
74
- ['commit', '-m', `chore(caws): bind spec ${specId} to worktree ${worktreeName}`, '--', relativeSpecPath],
75
- {
76
- cwd: root,
77
- stdio: 'pipe',
78
- }
79
- );
80
- return true;
81
- }
82
-
83
- function materializeWorktreeSpec(root, cawsDest, specId, worktreeName, scope) {
84
- if (!specId) return;
85
-
86
- const canonicalSpecPath = findFeatureSpecPath(root, specId);
87
- const workingSpecPath = path.join(cawsDest, 'working-spec.yaml');
88
-
89
- if (!canonicalSpecPath) {
90
- console.warn(
91
- chalk.yellow(`Warning: spec '${specId}' not found in .caws/specs/ — generating default working spec for worktree`)
92
- );
93
- }
94
-
95
- if (canonicalSpecPath) {
96
- const destSpecsDir = path.join(cawsDest, 'specs');
97
- const destSpecPath = path.join(destSpecsDir, path.basename(canonicalSpecPath));
98
- fs.ensureDirSync(destSpecsDir);
99
-
100
- // Keep a canonical feature-spec copy inside the worktree and align
101
- // working-spec.yaml to that exact content for legacy-compatible commands.
102
- const specContent = writeSpecWithWorktree(canonicalSpecPath, worktreeName);
103
- fs.writeFileSync(destSpecPath, specContent);
104
- fs.writeFileSync(workingSpecPath, specContent);
105
- return;
106
- }
107
-
108
- const { generateWorkingSpec } = require('../generators/working-spec');
109
- let specContent = generateWorkingSpec({
110
- projectId: specId,
111
- projectTitle: `Worktree: ${worktreeName}`,
112
- projectDescription: `Isolated worktree for ${worktreeName}`,
113
- riskTier: 3,
114
- projectMode: 'feature',
115
- scopeIn: scope || 'src/',
116
- scopeOut: 'node_modules/, dist/, build/',
117
- maxFiles: 25,
118
- maxLoc: 1000,
119
- blastModules: scope || 'src',
120
- dataMigration: false,
121
- rollbackSlo: '5m',
122
- projectThreats: '',
123
- projectInvariants: 'System maintains data consistency',
124
- acceptanceCriteria: 'Given current state, when action occurs, then expected result',
125
- a11yRequirements: 'keyboard',
126
- perfBudget: 250,
127
- securityRequirements: 'validation',
128
- contractType: '',
129
- contractPath: '',
130
- observabilityLogs: '',
131
- observabilityMetrics: '',
132
- observabilityTraces: '',
133
- migrationPlan: '',
134
- rollbackPlan: '',
135
- needsOverride: false,
136
- isExperimental: false,
137
- aiConfidence: 0.8,
138
- uncertaintyAreas: '',
139
- complexityFactors: '',
140
- });
141
-
142
- try {
143
- const yaml = require('js-yaml');
144
- const parsed = yaml.load(specContent);
145
- if (parsed && typeof parsed === 'object') {
146
- parsed.worktree = worktreeName;
147
- specContent = yaml.dump(parsed, { lineWidth: 120, noRefs: true });
148
- }
149
- } catch {
150
- // Keep generated spec content if augmentation fails.
151
- }
152
-
153
- fs.ensureDirSync(path.dirname(workingSpecPath));
154
- fs.writeFileSync(workingSpecPath, specContent);
155
- }
156
-
157
- function parseSpecIdFromYamlFile(filePath) {
158
- try {
159
- const yaml = require('js-yaml');
160
- const doc = yaml.load(fs.readFileSync(filePath, 'utf8'));
161
- if (doc && typeof doc.id === 'string' && doc.id.trim()) {
162
- return doc.id.trim();
163
- }
164
- } catch {
165
- // Ignore malformed YAML during inference
166
- }
167
- return null;
168
- }
169
-
170
- /**
171
- * Scan .caws/specs/ for a spec that declares `worktree: <name>`.
172
- * Returns the spec's id if found, null otherwise.
173
- * This enables auto-binding: when a spec already names the worktree
174
- * it expects, the registry entry gets the specId automatically.
175
- * @param {string} root - Repository root
176
- * @param {string} worktreeName - Worktree name to match
177
- * @returns {string|null} Spec ID or null
178
- */
179
- function findSpecByWorktreeName(root, worktreeName) {
180
- const yaml = require('js-yaml');
181
- const specsDir = path.join(root, '.caws', 'specs');
182
- if (!fs.existsSync(specsDir)) return null;
183
-
184
- const specFiles = fs.readdirSync(specsDir)
185
- .filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'));
186
-
187
- for (const specFile of specFiles) {
188
- try {
189
- const doc = yaml.load(fs.readFileSync(path.join(specsDir, specFile), 'utf8'));
190
- if (doc && doc.worktree === worktreeName && typeof doc.id === 'string') {
191
- return doc.id.trim();
192
- }
193
- } catch {
194
- // Skip malformed spec files
195
- }
196
- }
197
- return null;
198
- }
199
-
200
- function inferSpecIdForWorktree(worktreePath) {
201
- if (!worktreePath) return null;
202
-
203
- const specsDir = path.join(worktreePath, '.caws', 'specs');
204
- if (fs.existsSync(specsDir)) {
205
- const specFiles = fs.readdirSync(specsDir)
206
- .filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'))
207
- .sort();
208
-
209
- for (const specFile of specFiles) {
210
- const inferred = parseSpecIdFromYamlFile(path.join(specsDir, specFile));
211
- if (inferred) {
212
- return inferred;
213
- }
214
- }
215
- }
216
-
217
- return parseSpecIdFromYamlFile(path.join(worktreePath, '.caws', 'working-spec.yaml'));
218
- }
219
-
220
- /**
221
- * Get the last commit info for a branch
222
- * @param {string} branch - Branch name
223
- * @param {string} root - Repository root
224
- * @returns {{ age: string, timestamp: Date, sha: string } | null}
225
- */
226
- function getLastCommitInfo(branch, root) {
227
- try {
228
- const output = execFileSync(
229
- 'git',
230
- ['log', branch, '-1', '--format=%H%n%aI%n%ar'],
231
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
232
- ).trim();
233
- const [sha, iso, age] = output.split('\n');
234
- return { sha, timestamp: new Date(iso), age };
235
- } catch {
236
- return null;
237
- }
238
- }
239
-
240
- /**
241
- * Check if a branch has been merged into another branch
242
- * @param {string} branch - Branch to check
243
- * @param {string} target - Target branch (e.g., "main")
244
- * @param {string} root - Repository root
245
- * @returns {boolean}
246
- */
247
- function isBranchMerged(branch, target, root) {
248
- try {
249
- const merged = execFileSync(
250
- 'git',
251
- ['branch', '--merged', target, '--list', branch],
252
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
253
- ).trim();
254
- return merged.length > 0;
255
- } catch {
256
- return false;
257
- }
258
- }
259
-
260
- /**
261
- * Check if a branch has divergent commits from target (commits on branch not on target).
262
- * @param {string} branch - Branch to check
263
- * @param {string} target - Target branch (e.g., "main")
264
- * @param {string} root - Repository root
265
- * @returns {boolean}
266
- */
267
- function hasDivergentCommits(branch, target, root) {
268
- try {
269
- const count = execFileSync(
270
- 'git',
271
- ['rev-list', '--count', `${target}..${branch}`],
272
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
273
- ).trim();
274
- return parseInt(count, 10) > 0;
275
- } catch {
276
- return false;
277
- }
278
- }
279
-
280
- /**
281
- * Check if a worktree directory has dirty (uncommitted) files.
282
- * @param {string} worktreePath - Path to the worktree
283
- * @returns {boolean}
284
- */
285
- function hasDirtyFiles(worktreePath) {
286
- try {
287
- const status = execFileSync(
288
- 'git',
289
- ['status', '--porcelain'],
290
- { cwd: worktreePath, encoding: 'utf8', stdio: 'pipe' }
291
- ).trim();
292
- return status.length > 0;
293
- } catch {
294
- return false;
295
- }
296
- }
297
-
298
- /**
299
- * Get the canonical git repository root (main worktree, not a linked worktree).
300
- *
301
- * `git rev-parse --show-toplevel` returns the root of whichever worktree
302
- * the CWD is inside. In a linked worktree that is NOT the main repo root,
303
- * so CAWS would read the wrong (or missing) .caws/worktrees.json.
304
- *
305
- * `--git-common-dir` always resolves to the main repo's .git directory,
306
- * even from inside a linked worktree. Its parent is the canonical repo root.
307
- *
308
- * @returns {string} Absolute path to the main repo root
309
- */
310
- function getRepoRoot() {
311
- const gitCommonDir = execFileSync(
312
- 'git',
313
- ['rev-parse', '--path-format=absolute', '--git-common-dir'],
314
- { encoding: 'utf8' }
315
- ).trim();
316
- // gitCommonDir is /path/to/main-repo/.git — parent is the repo root
317
- return path.dirname(gitCommonDir);
318
- }
319
-
320
- /**
321
- * Get current branch name
322
- * @returns {string}
323
- */
324
- function getCurrentBranch() {
325
- return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
326
- encoding: 'utf8',
327
- }).trim();
328
- }
329
-
330
- // Track whether we've already warned about schema violations this process.
331
- // loadRegistry() is called multiple times per command; warning every time
332
- // floods stderr and contributes to Claude Code context-window exhaustion.
333
- let _schemaWarned = false;
334
-
335
- /**
336
- * Load the worktree registry
337
- * @param {string} root - Repository root
338
- * @returns {Object} Registry object
339
- */
340
- function loadRegistry(root) {
341
- const registryPath = path.join(root, REGISTRY_FILE);
342
- try {
343
- if (fs.existsSync(registryPath)) {
344
- const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
345
- try {
346
- const validate = createValidator(getSchemaPath('worktrees.schema.json', root));
347
- const result = validate(data);
348
- if (!result.valid && !_schemaWarned) {
349
- _schemaWarned = true;
350
- console.warn('Worktree registry has schema violations:', result.errors);
351
- }
352
- } catch (schemaErr) {
353
- if (!_schemaWarned) {
354
- _schemaWarned = true;
355
- console.warn('Could not validate worktree registry schema:', schemaErr.message);
356
- }
357
- }
358
- return data;
359
- }
360
- } catch {
361
- // Corrupted registry, start fresh
362
- }
363
- return { version: 1, worktrees: {} };
364
- }
365
-
366
- /**
367
- * Save the worktree registry
368
- * @param {string} root - Repository root
369
- * @param {Object} registry - Registry object
370
- */
371
- function saveRegistry(root, registry) {
372
- // Auto-prune destroyed entries whose branch and directory are both gone.
373
- // This prevents the registry from accumulating ghost entries over time.
374
- for (const [name, entry] of Object.entries(registry.worktrees || {})) {
375
- if (entry.status !== 'destroyed') continue;
376
- const dirGone = !fs.existsSync(entry.path);
377
- let branchGone = true;
378
- if (entry.branch) {
379
- try {
380
- execFileSync('git', ['rev-parse', '--verify', entry.branch], {
381
- cwd: root, stdio: 'pipe',
382
- });
383
- branchGone = false;
384
- } catch {
385
- branchGone = true;
386
- }
387
- }
388
- if (dirGone && branchGone) {
389
- delete registry.worktrees[name];
390
- }
391
- }
392
-
393
- const registryPath = path.join(root, REGISTRY_FILE);
394
- fs.ensureDirSync(path.dirname(registryPath));
395
- fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
396
- }
397
-
398
- /**
399
- * Discover git worktrees under .caws/worktrees/ that are not in the registry.
400
- * @param {string} root - Repository root
401
- * @param {Object} registry - Current registry object
402
- * @returns {Array<{ name: string, path: string, branch: string }>}
403
- */
404
- function discoverUnregisteredWorktrees(root, registry) {
405
- const unregistered = [];
406
- try {
407
- const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
408
- cwd: root,
409
- encoding: 'utf8',
410
- stdio: 'pipe',
411
- });
412
- let worktreesDir;
413
- try {
414
- worktreesDir = fs.realpathSync(path.resolve(root, WORKTREES_DIR));
415
- } catch {
416
- // Directory might not exist yet
417
- worktreesDir = path.resolve(root, WORKTREES_DIR);
418
- }
419
-
420
- const blocks = output.split('\n\n').filter(Boolean);
421
- for (const block of blocks) {
422
- const lines = block.split('\n');
423
- const wtLine = lines.find((l) => l.startsWith('worktree '));
424
- const branchLine = lines.find((l) => l.startsWith('branch '));
425
- if (!wtLine) continue;
426
-
427
- const wtPath = wtLine.replace('worktree ', '');
428
- let resolvedPath;
429
- try {
430
- resolvedPath = fs.realpathSync(wtPath);
431
- } catch {
432
- resolvedPath = path.resolve(wtPath);
433
- }
434
-
435
- // Only consider worktrees under .caws/worktrees/
436
- if (!resolvedPath.startsWith(worktreesDir + path.sep)) continue;
437
-
438
- const name = path.basename(resolvedPath);
439
- if (registry.worktrees[name]) continue;
440
-
441
- const branch = branchLine
442
- ? branchLine.replace('branch refs/heads/', '')
443
- : `${BRANCH_PREFIX}${name}`;
444
- unregistered.push({ name, path: resolvedPath, branch });
445
- }
446
- } catch {
447
- // git worktree list failed
448
- }
449
- return unregistered;
450
- }
451
-
452
- /**
453
- * Auto-register an unregistered worktree. Infers baseBranch via merge-base.
454
- * @param {string} root - Repository root
455
- * @param {Object} registry - Registry object (mutated in place)
456
- * @param {{ name: string, path: string, branch: string }} discovered
457
- * @returns {Object} The registered entry
458
- */
459
- function autoRegisterWorktree(root, registry, discovered) {
460
- let baseBranch = 'main';
461
- try {
462
- execFileSync(
463
- 'git',
464
- ['merge-base', discovered.branch, 'main'],
465
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
466
- );
467
- } catch {
468
- try {
469
- execFileSync(
470
- 'git',
471
- ['merge-base', discovered.branch, 'master'],
472
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
473
- );
474
- baseBranch = 'master';
475
- } catch {
476
- // Keep 'main' as default
477
- }
478
- }
479
-
480
- const entry = {
481
- name: discovered.name,
482
- path: discovered.path,
483
- branch: discovered.branch,
484
- baseBranch,
485
- scope: null,
486
- specId: inferSpecIdForWorktree(discovered.path),
487
- owner: null,
488
- createdAt: new Date().toISOString(),
489
- status: 'active',
490
- autoRegistered: true,
491
- };
492
-
493
- registry.worktrees[discovered.name] = entry;
494
- saveRegistry(root, registry);
495
- return entry;
496
- }
497
-
498
- /**
499
- * Create a new git worktree with scope isolation
500
- * @param {string} name - Worktree name
501
- * @param {Object} options - Creation options
502
- * @param {string} [options.scope] - Sparse checkout pattern (e.g., "src/auth/**")
503
- * @param {string} [options.baseBranch] - Base branch to create from
504
- * @param {string} [options.specId] - Associated spec ID for standard+ modes
505
- * @returns {Object} Created worktree info
506
- */
507
- function createWorktree(name, options = {}) {
508
- const root = getRepoRoot();
509
- const { scope, baseBranch, specId } = options;
510
-
511
- // Validate name
512
- if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
513
- throw new Error('Worktree name must contain only letters, numbers, hyphens, and underscores');
514
- }
515
-
516
- const registry = loadRegistry(root);
517
-
518
- // Check for duplicate in registry
519
- if (registry.worktrees[name]) {
520
- const existing = registry.worktrees[name];
521
- if (existing.status !== 'destroyed') {
522
- const ownerInfo = existing.owner ? ` (owned by session ${existing.owner})` : '';
523
- throw new Error(
524
- `Worktree '${name}' already exists with status '${existing.status}'${ownerInfo}.\n` +
525
- `Use 'caws worktree destroy ${name}' first, or choose a different name.`
526
- );
527
- }
528
- // Destroyed entries: check if another session owns the branch
529
- if (existing.owner && existing.owner !== getAgentSessionId(root)) {
530
- // Branch may still be in use by the owning session for merge
531
- try {
532
- const branchExists = execFileSync('git', ['rev-parse', '--verify', BRANCH_PREFIX + name], {
533
- cwd: root, stdio: 'pipe',
534
- }).toString().trim();
535
- if (branchExists) {
536
- throw new Error(
537
- `Worktree '${name}' was destroyed but branch '${BRANCH_PREFIX}${name}' still exists ` +
538
- `(owned by session ${existing.owner}).\n` +
539
- `The owning session may still need this branch for merging.\n` +
540
- `Choose a different name, or delete the branch first: git branch -d ${BRANCH_PREFIX}${name}`
541
- );
542
- }
543
- } catch (e) {
544
- if (e.message.includes('owned by session')) throw e;
545
- // Branch doesn't exist — safe to reuse the name
546
- }
547
- }
548
- }
549
-
550
- const worktreePath = path.join(root, WORKTREES_DIR, name);
551
- const branchName = BRANCH_PREFIX + name;
552
- const base = baseBranch || getCurrentBranch();
553
- const canonicalSpecPath = findFeatureSpecPath(root, specId);
554
-
555
- // Check if the branch already exists in git (even if not in registry)
556
- // This catches cases where another agent created the branch outside CAWS
557
- try {
558
- execFileSync('git', ['rev-parse', '--verify', branchName], {
559
- cwd: root, stdio: 'pipe',
560
- });
561
- // Branch exists — refuse unless it's fully merged into base
562
- const currentSession = getAgentSessionId(root);
563
- const registryOwner = registry.worktrees[name]?.owner;
564
- if (registryOwner && registryOwner !== currentSession) {
565
- throw new Error(
566
- `Branch '${branchName}' already exists and is owned by another session (${registryOwner}).\n` +
567
- `Another agent may be using this branch. Choose a different worktree name.`
568
- );
569
- }
570
- // Branch exists but no owner conflict — warn and reuse
571
- console.warn(`Warning: Branch '${branchName}' already exists, reusing it.`);
572
- } catch (e) {
573
- if (e.message.includes('already exists and is owned')) throw e;
574
- // Branch doesn't exist — this is the normal path
575
- }
576
-
577
- // Create the worktree directory
578
- fs.ensureDirSync(path.dirname(worktreePath));
579
-
580
- if (canonicalSpecPath) {
581
- ensureCanonicalSpecCommitted(root, canonicalSpecPath, specId, name);
582
- }
583
-
584
- // Create git worktree with new branch
585
- try {
586
- execFileSync('git', ['worktree', 'add', '-b', branchName, worktreePath, base], {
587
- cwd: root,
588
- stdio: 'pipe',
589
- });
590
- } catch (error) {
591
- // Branch already exists (caught above and allowed) — attach to it
592
- if (error.message.includes('already exists')) {
593
- execFileSync('git', ['worktree', 'add', worktreePath, branchName], {
594
- cwd: root,
595
- stdio: 'pipe',
596
- });
597
- } else {
598
- throw new Error(`Failed to create worktree: ${error.message}`);
599
- }
600
- }
601
-
602
- // Set up sparse checkout if scope is provided
603
- if (scope) {
604
- try {
605
- // Parse scope patterns (comma-separated)
606
- const patterns = scope.split(',').map((p) => p.trim());
607
-
608
- // Detect glob characters — cone mode only accepts directory paths,
609
- // not glob patterns like "core/reasoning/**" or "*.py".
610
- const hasGlobs = patterns.some((p) => /[*?[\]]/.test(p));
611
- const coneFlag = hasGlobs ? '--no-cone' : '--cone';
612
-
613
- execFileSync('git', ['sparse-checkout', 'init', coneFlag], {
614
- cwd: worktreePath,
615
- stdio: 'pipe',
616
- });
617
-
618
- execFileSync('git', ['sparse-checkout', 'set', ...patterns], {
619
- cwd: worktreePath,
620
- stdio: 'pipe',
621
- });
622
- } catch (error) {
623
- console.warn(chalk.yellow(`Sparse checkout setup failed: ${error.message}`));
624
- console.warn(chalk.blue('Worktree created but without sparse checkout'));
625
- }
626
- }
627
-
628
- // Copy .caws/ config into worktree
629
- const cawsSource = path.join(root, '.caws');
630
- const cawsDest = path.join(worktreePath, '.caws');
631
- if (fs.existsSync(cawsSource)) {
632
- try {
633
- fs.copySync(cawsSource, cawsDest, {
634
- filter: (src) => {
635
- // Don't copy worktrees directory or registry into the worktree
636
- const rel = path.relative(cawsSource, src);
637
- return !rel.startsWith('worktrees') && rel !== 'worktrees.json';
638
- },
639
- });
640
- } catch {
641
- // Non-fatal
642
- }
643
- }
644
-
645
- // Auto-bind specId: if no explicit --spec-id was passed, scan .caws/specs/
646
- // for a spec that declares `worktree: <name>`. This establishes the mutual
647
- // reference that the scope guard uses to treat one spec as authoritative.
648
- let resolvedSpecId = specId || null;
649
- if (!resolvedSpecId) {
650
- resolvedSpecId = findSpecByWorktreeName(root, name);
651
- if (resolvedSpecId) {
652
- console.log(chalk.gray(` Auto-bound spec: ${resolvedSpecId}`));
653
- }
654
- }
655
-
656
- // Materialize a worktree-local working spec. Prefer the canonical feature
657
- // spec when it exists so isolated worktrees stay aligned with the main
658
- // registry/resolver model.
659
- if (resolvedSpecId) {
660
- try {
661
- materializeWorktreeSpec(root, cawsDest, resolvedSpecId, name, scope);
662
- } catch (error) {
663
- console.warn(
664
- chalk.yellow(`Could not materialize spec '${resolvedSpecId}' for worktree '${name}': ${error.message}`)
665
- );
666
- // Non-fatal: spec generation is optional
667
- }
668
- }
669
-
670
- // Register worktree
671
- const entry = {
672
- name,
673
- path: worktreePath,
674
- branch: branchName,
675
- baseBranch: base,
676
- scope: scope || null,
677
- specId: resolvedSpecId,
678
- owner: options.owner || getAgentSessionId(root) || null,
679
- createdAt: new Date().toISOString(),
680
- status: 'fresh',
681
- };
682
-
683
- registry.worktrees[name] = entry;
684
- saveRegistry(root, registry);
685
-
686
- return entry;
687
- }
688
-
689
- /**
690
- * Reconcile registry state against git worktree list and filesystem.
691
- *
692
- * Non-destructive read that classifies every known worktree entry
693
- * (from registry + git discovery) into one of:
694
- * active — directory exists AND in git worktree list
695
- * orphaned — directory exists but NOT in git worktree list
696
- * missing — directory gone, branch may or may not exist
697
- * destroyed — explicitly destroyed via CAWS
698
- * unregistered — in git worktree list but not in registry
699
- * stale-merged — missing + branch already merged to base
700
- *
701
- * Does NOT mutate the registry. Callers decide what to persist.
702
- *
703
- * @param {string} root - Repository root
704
- * @returns {{ entries: Array, gitWorktrees: string[] }}
705
- */
706
- function reconcileRegistry(root) {
707
- const registry = loadRegistry(root);
708
-
709
- let gitWorktrees = [];
710
- try {
711
- const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
712
- cwd: root,
713
- encoding: 'utf8',
714
- stdio: 'pipe',
715
- });
716
- gitWorktrees = output
717
- .split('\n\n')
718
- .filter(Boolean)
719
- .map((block) => {
720
- const lines = block.split('\n');
721
- const worktreeLine = lines.find((l) => l.startsWith('worktree '));
722
- return worktreeLine ? worktreeLine.replace('worktree ', '') : null;
723
- })
724
- .filter(Boolean);
725
- } catch {
726
- // Git worktree list failed
727
- }
728
-
729
- const entries = Object.values(registry.worktrees).map((entry) => {
730
- const exists = fs.existsSync(entry.path);
731
- const inGit = gitWorktrees.some(
732
- (wt) => path.resolve(wt) === path.resolve(entry.path)
733
- );
734
-
735
- const merged = entry.branch && entry.baseBranch
736
- ? isBranchMerged(entry.branch, entry.baseBranch, root)
737
- : false;
738
- const divergent = entry.branch && entry.baseBranch
739
- ? hasDivergentCommits(entry.branch, entry.baseBranch, root)
740
- : false;
741
- const dirty = exists ? hasDirtyFiles(entry.path) : false;
742
-
743
- let status;
744
- if (entry.status === 'destroyed') {
745
- status = 'destroyed';
746
- } else if (exists && inGit) {
747
- // Worktree directory exists and is tracked by git
748
- if (divergent || dirty) {
749
- // Has commits beyond base or uncommitted work → active
750
- status = 'active';
751
- } else if (merged) {
752
- // No divergent commits, branch aligned with base.
753
- // Use stored status as history to distinguish fresh vs merged:
754
- // - stored 'fresh' → never had divergent commits → still fresh
755
- // - stored 'active' → had work that's now merged → merged
756
- if (entry.status === 'active') {
757
- status = 'merged';
758
- } else {
759
- status = 'fresh';
760
- }
761
- } else {
762
- status = 'fresh';
763
- }
764
- } else if (exists) {
765
- status = 'orphaned';
766
- } else {
767
- status = merged ? 'stale-merged' : 'missing';
768
- }
769
-
770
- const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
771
-
772
- return { ...entry, status, lastCommit, merged, divergent, dirty };
773
- });
774
-
775
- // Append unregistered worktrees discovered from git
776
- const unregistered = discoverUnregisteredWorktrees(root, registry);
777
- for (const discovered of unregistered) {
778
- const lastCommit = getLastCommitInfo(discovered.branch, root);
779
- entries.push({
780
- name: discovered.name,
781
- path: discovered.path,
782
- branch: discovered.branch,
783
- baseBranch: null,
784
- scope: null,
785
- specId: null,
786
- owner: null,
787
- createdAt: null,
788
- status: 'unregistered',
789
- lastCommit,
790
- merged: false,
791
- });
792
- }
793
-
794
- return { entries, gitWorktrees };
795
- }
796
-
797
- /**
798
- * Repair registry drift caused by manual git operations outside CAWS.
799
- *
800
- * Scans registry vs git vs filesystem, classifies each entry, and optionally
801
- * prunes stale entries. Reports the delta before persisting.
802
- *
803
- * @param {Object} options
804
- * @param {boolean} [options.prune=false] - Remove destroyed, stale-merged, and missing entries
805
- * @param {boolean} [options.dryRun=false] - Report only, do not persist
806
- * @param {boolean} [options.force=false] - Allow pruning entries owned by other sessions
807
- * @returns {{ repaired: Array, pruned: Array, skipped: Array }}
808
- */
809
- function repairWorktrees(options = {}) {
810
- const { prune: shouldPrune = false, dryRun = false, force = false } = options;
811
- const root = getRepoRoot();
812
- const registry = loadRegistry(root);
813
- const { entries } = reconcileRegistry(root);
814
- const currentSession = getAgentSessionId(root);
815
-
816
- const repaired = [];
817
- const pruned = [];
818
- const skipped = [];
819
-
820
- for (const entry of entries) {
821
- const regEntry = registry.worktrees[entry.name];
822
-
823
- if (entry.status === 'unregistered') {
824
- if (!dryRun) {
825
- autoRegisterWorktree(root, registry, entry);
826
- }
827
- repaired.push({ name: entry.name, action: 'registered', status: entry.status });
828
- continue;
829
- }
830
-
831
- if (!regEntry) continue;
832
-
833
- // Update registry status to match filesystem reality
834
- const wasAlive = regEntry.status === 'active' || regEntry.status === 'fresh';
835
- const nowDead = entry.status === 'missing' || entry.status === 'stale-merged';
836
- if (wasAlive && nowDead) {
837
- repaired.push({
838
- name: entry.name,
839
- action: 'status-updated',
840
- from: regEntry.status,
841
- to: entry.status,
842
- owner: entry.owner || null,
843
- });
844
- }
845
-
846
- // Determine if entry is prunable (destroyed, stale-merged, or missing)
847
- const isPrunable = entry.status === 'destroyed' ||
848
- entry.status === 'stale-merged' ||
849
- entry.status === 'missing';
850
-
851
- if (!isPrunable) continue;
852
-
853
- // Ownership check: refuse to prune another session's entries without --force
854
- const isOwnedByOther = entry.owner && currentSession && entry.owner !== currentSession;
855
-
856
- if (shouldPrune && isPrunable) {
857
- if (isOwnedByOther && !force) {
858
- skipped.push({
859
- name: entry.name,
860
- reason: `owned by another session (${entry.owner}). Use --force to override`,
861
- owner: entry.owner,
862
- });
863
- } else {
864
- if (!dryRun) {
865
- delete registry.worktrees[entry.name];
866
- }
867
- pruned.push({ name: entry.name, status: entry.status, owner: entry.owner || null });
868
- }
869
- } else if (!shouldPrune && isPrunable) {
870
- skipped.push({
871
- name: entry.name,
872
- reason: entry.status + ' (use --prune to remove)',
873
- owner: entry.owner || null,
874
- });
875
- }
876
- }
877
-
878
- if (!dryRun) {
879
- saveRegistry(root, registry);
880
- try {
881
- execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
882
- } catch {
883
- // Non-fatal
884
- }
885
- }
886
-
887
- return { repaired, pruned, skipped };
888
- }
889
-
890
- /**
891
- * List all registered worktrees with filesystem validation.
892
- * Delegates to reconcileRegistry() for state classification.
893
- * Persists status transitions (fresh → active, active → merged) so
894
- * future calls can distinguish "never had work" from "work was merged back".
895
- * @returns {Array} Worktree entries with status
896
- */
897
- function listWorktrees() {
898
- const root = getRepoRoot();
899
- const registry = loadRegistry(root);
900
- const { entries } = reconcileRegistry(root);
901
-
902
- // Persist status transitions so future reconcile can use stored status as history
903
- let dirty = false;
904
- for (const entry of entries) {
905
- const regEntry = registry.worktrees[entry.name];
906
- if (regEntry && regEntry.status !== entry.status &&
907
- entry.status !== 'unregistered') {
908
- regEntry.status = entry.status;
909
- dirty = true;
910
- }
911
- }
912
- if (dirty) {
913
- saveRegistry(root, registry);
914
- }
915
-
916
- return entries;
917
- }
918
-
919
- /**
920
- * Destroy a worktree
921
- * @param {string} name - Worktree name
922
- * @param {Object} options - Destruction options
923
- * @param {boolean} [options.deleteBranch] - Also delete the branch
924
- * @param {boolean} [options.force] - Force removal even if dirty
925
- */
926
- function destroyWorktree(name, options = {}) {
927
- const root = getRepoRoot();
928
- // Ensure CWD is not inside the worktree we're about to destroy.
929
- // If CWD is the worktree directory, removing it crashes subsequent commands.
930
- try { process.chdir(root); } catch { /* non-fatal */ }
931
- const registry = loadRegistry(root);
932
- const { deleteBranch = false, force = false } = options;
933
-
934
- let entry = registry.worktrees[name];
935
- if (!entry) {
936
- // Fallback: scan git for unregistered worktree and auto-register
937
- const unregistered = discoverUnregisteredWorktrees(root, registry);
938
- const discovered = unregistered.find((u) => u.name === name);
939
- if (discovered) {
940
- console.log(chalk.yellow(`Worktree '${name}' not in registry but found in git. Auto-registering.`));
941
- entry = autoRegisterWorktree(root, registry, discovered);
942
- } else {
943
- throw new Error(`Worktree '${name}' not found in registry or git worktree list`);
944
- }
945
- }
946
-
947
- // Ownership check: refuse to destroy another agent's worktree without --force
948
- const currentSession = getAgentSessionId(root);
949
- const isLiveStatus = entry.status === 'active' || entry.status === 'fresh' || entry.status === 'merged';
950
- if (
951
- !force &&
952
- isLiveStatus &&
953
- entry.owner &&
954
- currentSession &&
955
- entry.owner !== currentSession
956
- ) {
957
- const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
958
- const recency = lastCommit ? ` (last commit: ${lastCommit.age})` : '';
959
- throw new Error(
960
- `Worktree '${name}' belongs to another session${recency}.\n` +
961
- ` Owner: ${entry.owner}\n` +
962
- ` You: ${currentSession}\n` +
963
- `Another agent may be actively working here.\n` +
964
- `Do NOT destroy worktrees you did not create. Ask the user if cleanup is needed.`
965
- );
966
- }
967
-
968
- // Even with --force, warn loudly when destroying another session's worktree
969
- if (
970
- force &&
971
- isLiveStatus &&
972
- entry.owner &&
973
- currentSession &&
974
- entry.owner !== currentSession
975
- ) {
976
- const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
977
- const recency = lastCommit ? ` (last commit: ${lastCommit.age})` : '';
978
- console.log(chalk.red(`\n ⚠ WARNING: Force-destroying worktree '${name}' owned by another session${recency}`));
979
- console.log(chalk.red(` Owner: ${entry.owner}`));
980
- console.log(chalk.red(` You: ${currentSession}`));
981
- console.log(chalk.red(` If the other agent is still running, this WILL break their work.\n`));
982
- }
983
-
984
- // Auto-force when the branch is already merged to its base branch.
985
- // Dirty files in a merged worktree are definitionally stale.
986
- const merged = entry.branch && entry.baseBranch
987
- ? isBranchMerged(entry.branch, entry.baseBranch, root)
988
- : false;
989
- const effectiveForce = force || merged;
990
- if (merged && !force) {
991
- console.log(chalk.gray(` Branch ${entry.branch} already merged to ${entry.baseBranch}, auto-forcing cleanup`));
992
- }
993
-
994
- // Remove git worktree — handle already-deleted directories gracefully
995
- const dirExists = fs.existsSync(entry.path);
996
- if (dirExists) {
997
- try {
998
- const args = ['worktree', 'remove'];
999
- if (effectiveForce) args.push('--force');
1000
- args.push(entry.path);
1001
- execFileSync('git', args, { cwd: root, stdio: 'pipe' });
1002
- } catch (error) {
1003
- if (effectiveForce) {
1004
- // Force cleanup: remove directory manually
1005
- fs.removeSync(entry.path);
1006
- } else {
1007
- throw new Error(`Failed to remove worktree: ${error.message}. Use --force to override.`);
1008
- }
1009
- }
1010
- } else {
1011
- // Directory already gone — just clean up git's tracking
1012
- console.log(` Worktree directory already removed, cleaning up registry`);
1013
- }
1014
-
1015
- // Always prune git's worktree list to stay in sync
1016
- try {
1017
- execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
1018
- } catch {
1019
- // Non-fatal
1020
- }
1021
-
1022
- // Optionally delete branch
1023
- if (deleteBranch && entry.branch) {
1024
- try {
1025
- execFileSync('git', ['branch', '-d', entry.branch], { cwd: root, stdio: 'pipe' });
1026
- } catch {
1027
- if (effectiveForce) {
1028
- try {
1029
- execFileSync('git', ['branch', '-D', entry.branch], { cwd: root, stdio: 'pipe' });
1030
- } catch {
1031
- // Non-fatal
1032
- }
1033
- }
1034
- }
1035
- }
1036
-
1037
- // Update registry
1038
- const wasAlreadyDestroyed = registry.worktrees[name].status === 'destroyed';
1039
- registry.worktrees[name].status = 'destroyed';
1040
- registry.worktrees[name].destroyedAt = new Date().toISOString();
1041
- saveRegistry(root, registry);
1042
-
1043
- // CAWSFIX-18: auto-commit the registry so the working tree stays clean
1044
- if (!wasAlreadyDestroyed) {
1045
- try {
1046
- const status = execFileSync('git', ['status', '--porcelain', '.caws/worktrees.json'], {
1047
- cwd: root, stdio: ['pipe', 'pipe', 'pipe'],
1048
- }).toString().trim();
1049
- if (status) {
1050
- const otherActive = Object.values(registry.worktrees || {}).some(
1051
- (e) => e.status === 'active' || e.status === 'fresh'
1052
- );
1053
- const prefix = otherActive ? 'wip(checkpoint)' : 'chore(worktree)';
1054
- execFileSync('git', ['add', '.caws/worktrees.json'], { cwd: root, stdio: 'pipe' });
1055
- execFileSync('git', ['commit', '-m', `${prefix}: record destroyed ${name}`], {
1056
- cwd: root, stdio: 'pipe',
1057
- });
1058
- }
1059
- } catch (err) {
1060
- console.warn(chalk.yellow(` Warning: could not auto-commit .caws/worktrees.json: ${err.message}`));
1061
- }
1062
- }
1063
- }
1064
-
1065
- /**
1066
- * Merge a worktree branch back to base in one operation.
1067
- * Sequence: dry-run conflict check → destroy worktree → merge → cleanup.
1068
- * @param {string} name - Worktree name
1069
- * @param {Object} options - Merge options
1070
- * @param {boolean} [options.dryRun] - Preview conflicts without merging
1071
- * @param {boolean} [options.deleteBranch] - Delete branch after merge
1072
- * @param {string} [options.message] - Custom merge commit message
1073
- * @returns {Object} Merge result
1074
- */
1075
- function mergeWorktree(name, options = {}) {
1076
- const root = getRepoRoot();
1077
- const registry = loadRegistry(root);
1078
- const { dryRun = false, deleteBranch = true, message } = options;
1079
-
1080
- let entry = registry.worktrees[name];
1081
- if (!entry) {
1082
- // Fallback: scan git for unregistered worktree and auto-register
1083
- const unregistered = discoverUnregisteredWorktrees(root, registry);
1084
- const discovered = unregistered.find((u) => u.name === name);
1085
- if (discovered) {
1086
- console.log(chalk.yellow(`Worktree '${name}' not in registry but found in git. Auto-registering.`));
1087
- entry = autoRegisterWorktree(root, registry, discovered);
1088
- } else {
1089
- throw new Error(`Worktree '${name}' not found in registry or git worktree list`);
1090
- }
1091
- }
1092
-
1093
- const baseBranch = entry.baseBranch || 'main';
1094
-
1095
- // Check for uncommitted work in the worktree.
1096
- // Ignore .caws/ changes (provenance chain, registry) — these are
1097
- // infrastructure artifacts written by git hooks, not user work.
1098
- // The post-commit hook appends to .caws/provenance/chain.json after
1099
- // every commit, which immediately dirties the tree and blocks merges.
1100
- if (fs.existsSync(entry.path)) {
1101
- try {
1102
- const rawStatus = execFileSync(
1103
- 'git',
1104
- ['status', '--porcelain'],
1105
- { cwd: entry.path, encoding: 'utf8', stdio: 'pipe' }
1106
- );
1107
- // Filter out .caws/ infrastructure changes (provenance, registry).
1108
- // Git porcelain format: "XY PATH" — 2 status chars, space, path.
1109
- // IMPORTANT: do NOT .trim() the raw output — it strips the leading
1110
- // space from " M file" (unstaged), corrupting the XY prefix and
1111
- // breaking substring(3) path extraction.
1112
- const statusLines = rawStatus.split('\n').filter(l => l.length > 0);
1113
- const userChanges = statusLines
1114
- .filter(line => {
1115
- const filePath = line.substring(3);
1116
- return !filePath.startsWith('.caws/');
1117
- }).join('\n');
1118
- if (userChanges) {
1119
- throw new Error(
1120
- `Worktree '${name}' has uncommitted changes:\n${userChanges}\n` +
1121
- `Commit or discard changes before merging.`
1122
- );
1123
- }
1124
- } catch (error) {
1125
- if (error.message.includes('uncommitted changes')) throw error;
1126
- // Non-fatal: status check failed, proceed cautiously
1127
- }
1128
- }
1129
-
1130
- // Dry-run: check for conflicts using git merge-tree (new-style, git 2.38+)
1131
- let conflicts = [];
1132
- try {
1133
- // New-style merge-tree: takes two branches, computes merge-base automatically
1134
- execFileSync(
1135
- 'git',
1136
- ['merge-tree', '--write-tree', baseBranch, entry.branch],
1137
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
1138
- );
1139
- // Exit 0 = clean merge, no conflicts
1140
- } catch (mergeTreeError) {
1141
- // Exit 1 = conflicts detected; parse them from output
1142
- const output = (mergeTreeError.stdout || '') + (mergeTreeError.stderr || '');
1143
- const conflictLines = output.split('\n').filter(
1144
- (l) => l.includes('CONFLICT') || l.includes('conflict')
1145
- );
1146
- if (mergeTreeError.status === 1 && conflictLines.length > 0) {
1147
- conflicts = conflictLines;
1148
- } else if (mergeTreeError.status === 1) {
1149
- conflicts = ['Merge conflicts detected (run merge manually to inspect)'];
1150
- }
1151
- // Other exit codes (e.g., merge-tree not supported) = can't detect, proceed
1152
- }
1153
-
1154
- if (dryRun) {
1155
- return {
1156
- name,
1157
- branch: entry.branch,
1158
- baseBranch,
1159
- conflicts,
1160
- wouldMerge: conflicts.length === 0,
1161
- };
1162
- }
1163
-
1164
- // Emit merge:pre event
1165
- try {
1166
- lifecycle.emit(EVENTS.MERGE_PRE, {
1167
- worktreeName: name, branch: entry.branch, baseBranch, conflicts,
1168
- timestamp: new Date().toISOString(),
1169
- });
1170
- } catch { /* non-fatal */ }
1171
-
1172
- // Ensure CWD is the repo root BEFORE destroying the worktree.
1173
- // If the caller's CWD is inside the worktree directory, destroying it
1174
- // removes the CWD out from under the process, causing all subsequent
1175
- // git commands to fail with "Unable to read current working directory".
1176
- try { process.chdir(root); } catch { /* non-fatal */ }
1177
-
1178
- // Destroy the worktree (auto-forces since we're about to merge)
1179
- destroyWorktree(name, { deleteBranch: false, force: true });
1180
-
1181
- // Switch to base branch (use cwd: root since getCurrentBranch has no cwd param)
1182
- const currentBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
1183
- cwd: root, encoding: 'utf8', stdio: 'pipe',
1184
- }).trim();
1185
- if (currentBranch !== baseBranch) {
1186
- execFileSync('git', ['checkout', baseBranch], { cwd: root, stdio: 'pipe' });
1187
- }
1188
-
1189
- // Merge
1190
- // Use --no-verify to skip pre-commit/commit-msg hooks during merge.
1191
- // The worktree commits were already validated by those hooks when originally
1192
- // committed. Re-running them here adds seconds of blocking time (especially
1193
- // in projects with heavy hooks like quality gates, YAML validation, etc.)
1194
- // and can trigger OAuth token expiry races in long-running sessions.
1195
- const mergeMessage = message || `merge(worktree): ${name}`;
1196
- try {
1197
- execFileSync(
1198
- 'git',
1199
- ['merge', '--no-ff', '--no-verify', entry.branch, '-m', mergeMessage],
1200
- { cwd: root, stdio: 'pipe' }
1201
- );
1202
- } catch (error) {
1203
- const failResult = {
1204
- name, branch: entry.branch, baseBranch, merged: false,
1205
- conflicts: [`Merge failed: ${error.message}`],
1206
- message: 'Merge conflicts detected. Resolve with git and commit.',
1207
- };
1208
- try {
1209
- lifecycle.emit(EVENTS.MERGE_POST, { ...failResult, timestamp: new Date().toISOString() });
1210
- } catch { /* non-fatal */ }
1211
- return failResult;
1212
- }
1213
-
1214
- // Delete branch after successful merge
1215
- if (deleteBranch) {
1216
- try {
1217
- execFileSync('git', ['branch', '-d', entry.branch], { cwd: root, stdio: 'pipe' });
1218
- } catch {
1219
- // Non-fatal
1220
- }
1221
- }
1222
-
1223
- // Auto-close the bound spec if one exists. A worktree merge is the
1224
- // lifecycle signal that the spec's work is done; leaving the spec
1225
- // `active` after merge accumulates stale-active entries (D6). Direct
1226
- // YAML status flip bypasses the ownership + worktree-reference checks
1227
- // in `closeSpec` — the caller has already proven authority by merging.
1228
- let autoClosedSpecId = null;
1229
- if (entry.specId) {
1230
- autoClosedSpecId = autoCloseBoundSpec(root, entry.specId);
1231
- }
1232
-
1233
- const mergeResult = {
1234
- name, branch: entry.branch, baseBranch, merged: true, conflicts: [],
1235
- specId: entry.specId || null, autoClosedSpecId,
1236
- };
1237
- try {
1238
- lifecycle.emit(EVENTS.MERGE_POST, { ...mergeResult, timestamp: new Date().toISOString() });
1239
- } catch { /* non-fatal */ }
1240
- return mergeResult;
1241
- }
1242
-
1243
- /**
1244
- * Flip a spec's status to `closed` by rewriting just the `status:` line.
1245
- * Idempotent: no-op when the spec is already closed or the file is missing.
1246
- * Returns the spec ID on success, null if skipped or failed.
1247
- * @param {string} root - Repo root
1248
- * @param {string} specId - Spec identifier (e.g. CAWSFIX-14)
1249
- * @returns {string|null}
1250
- */
1251
- function autoCloseBoundSpec(root, specId) {
1252
- try {
1253
- const specPath = findFeatureSpecPath(root, specId);
1254
- if (!specPath || !fs.existsSync(specPath)) return null;
1255
- const original = fs.readFileSync(specPath, 'utf8');
1256
- // Idempotent: already closed → no-op, no write, no diff.
1257
- if (/^status:\s*closed\s*$/m.test(original)) return specId;
1258
- const patched = original.replace(/^status:\s*active\s*$/m, 'status: closed');
1259
- if (patched === original) return null; // status was e.g. draft/archived
1260
- fs.writeFileSync(specPath, patched, 'utf8');
1261
- return specId;
1262
- } catch {
1263
- return null;
1264
- }
1265
- }
1266
-
1267
- /**
1268
- * Prune stale worktree entries
1269
- * @param {Object} options - Prune options
1270
- * @param {number} [options.maxAgeDays] - Remove entries older than this many days
1271
- * @param {number} [options.recentCommitMinutes] - Protect branches with commits newer than this (default: 60)
1272
- * @param {boolean} [options.force] - Allow pruning entries owned by other sessions
1273
- * @returns {{ pruned: Array, skipped: Array }} Pruned and skipped entries
1274
- */
1275
- function pruneWorktrees(options = {}) {
1276
- const root = getRepoRoot();
1277
- const registry = loadRegistry(root);
1278
- const { maxAgeDays = 30, recentCommitMinutes = 60, force = false } = options;
1279
- const currentSession = getAgentSessionId(root);
1280
-
1281
- const now = new Date();
1282
- const pruned = [];
1283
- const skipped = [];
1284
-
1285
- for (const [name, entry] of Object.entries(registry.worktrees)) {
1286
- const created = new Date(entry.createdAt);
1287
- const ageDays = (now - created) / (1000 * 60 * 60 * 24);
1288
- const dirExists = fs.existsSync(entry.path);
1289
-
1290
- const shouldPrune =
1291
- // Always prune destroyed entries
1292
- entry.status === 'destroyed' ||
1293
- // Prune active/fresh entries whose directory is gone (filesystem-registry desync)
1294
- ((entry.status === 'active' || entry.status === 'fresh') && !dirExists) ||
1295
- // Prune old missing entries
1296
- (!dirExists && ageDays > maxAgeDays);
1297
-
1298
- if (shouldPrune) {
1299
- // Ownership check: skip entries owned by other sessions unless --force
1300
- const isOwnedByOther = entry.owner && currentSession && entry.owner !== currentSession;
1301
- if (isOwnedByOther && entry.status !== 'destroyed' && !force) {
1302
- skipped.push({
1303
- name,
1304
- reason: `owned by another session (${entry.owner})`,
1305
- entry,
1306
- });
1307
- continue;
1308
- }
1309
-
1310
- // Before pruning a non-destroyed entry, check for recent commits (skip if --force)
1311
- if (!force && entry.status !== 'destroyed' && entry.branch) {
1312
- const lastCommit = getLastCommitInfo(entry.branch, root);
1313
- if (lastCommit) {
1314
- const commitAgeMinutes = (now - lastCommit.timestamp) / (1000 * 60);
1315
- if (commitAgeMinutes < recentCommitMinutes) {
1316
- skipped.push({ name, reason: `recent commit (${lastCommit.age})`, entry });
1317
- continue;
1318
- }
1319
- }
1320
- }
1321
-
1322
- // Clean up filesystem if still exists
1323
- if (dirExists) {
1324
- try {
1325
- execFileSync('git', ['worktree', 'remove', '--force', entry.path], {
1326
- cwd: root,
1327
- stdio: 'pipe',
1328
- });
1329
- } catch {
1330
- fs.removeSync(entry.path);
1331
- }
1332
- }
1333
- pruned.push(entry);
1334
- delete registry.worktrees[name];
1335
- }
1336
- }
1337
-
1338
- // Prune git's worktree list
1339
- try {
1340
- execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
1341
- } catch {
1342
- // Non-fatal
1343
- }
1344
-
1345
- saveRegistry(root, registry);
1346
- return { pruned, skipped };
1347
- }
1348
-
1349
- module.exports = {
1350
- createWorktree,
1351
- listWorktrees,
1352
- destroyWorktree,
1353
- mergeWorktree,
1354
- autoCloseBoundSpec,
1355
- pruneWorktrees,
1356
- repairWorktrees,
1357
- reconcileRegistry,
1358
- loadRegistry,
1359
- saveRegistry,
1360
- getRepoRoot,
1361
- getLastCommitInfo,
1362
- isBranchMerged,
1363
- hasDivergentCommits,
1364
- hasDirtyFiles,
1365
- discoverUnregisteredWorktrees,
1366
- autoRegisterWorktree,
1367
- WORKTREES_DIR,
1368
- REGISTRY_FILE,
1369
- BRANCH_PREFIX,
1370
- findFeatureSpecPath,
1371
- materializeWorktreeSpec,
1372
- inferSpecIdForWorktree,
1373
- findSpecByWorktreeName,
1374
- };