@paths.design/caws-cli 10.2.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 (421) hide show
  1. package/README.md +125 -374
  2. package/dist/index.js +43 -785
  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/agents.js +0 -124
  183. package/dist/commands/archive.js +0 -500
  184. package/dist/commands/burnup.js +0 -198
  185. package/dist/commands/diagnose.js +0 -525
  186. package/dist/commands/evaluate.js +0 -314
  187. package/dist/commands/gates.js +0 -149
  188. package/dist/commands/init.js +0 -857
  189. package/dist/commands/iterate.js +0 -417
  190. package/dist/commands/mode.js +0 -269
  191. package/dist/commands/parallel.js +0 -242
  192. package/dist/commands/plan.js +0 -438
  193. package/dist/commands/provenance.js +0 -1143
  194. package/dist/commands/quality-monitor.js +0 -284
  195. package/dist/commands/scope.js +0 -264
  196. package/dist/commands/session.js +0 -312
  197. package/dist/commands/sidecar.js +0 -74
  198. package/dist/commands/specs.js +0 -1656
  199. package/dist/commands/status.js +0 -1172
  200. package/dist/commands/templates.js +0 -237
  201. package/dist/commands/tool.js +0 -136
  202. package/dist/commands/tutorial.js +0 -480
  203. package/dist/commands/validate.js +0 -357
  204. package/dist/commands/verify-acs.js +0 -443
  205. package/dist/commands/waivers.js +0 -599
  206. package/dist/commands/workflow.js +0 -243
  207. package/dist/commands/worktree.js +0 -502
  208. package/dist/config/lite-scope.js +0 -158
  209. package/dist/config/modes.js +0 -347
  210. package/dist/constants/spec-types.js +0 -65
  211. package/dist/gates/budget-limit.js +0 -121
  212. package/dist/gates/feedback.js +0 -260
  213. package/dist/gates/format.js +0 -179
  214. package/dist/gates/god-object.js +0 -117
  215. package/dist/gates/pipeline.js +0 -167
  216. package/dist/gates/scope-boundary.js +0 -112
  217. package/dist/gates/spec-completeness.js +0 -109
  218. package/dist/gates/todo-detection.js +0 -205
  219. package/dist/generators/jest-config-generator.js +0 -242
  220. package/dist/generators/working-spec.js +0 -237
  221. package/dist/minimal-cli.js +0 -88
  222. package/dist/parallel/parallel-manager.js +0 -433
  223. package/dist/policy/PolicyManager.js +0 -470
  224. package/dist/scaffold/claude-hooks.js +0 -443
  225. package/dist/scaffold/cursor-hooks.js +0 -177
  226. package/dist/scaffold/git-hooks.js +0 -928
  227. package/dist/scaffold/index.js +0 -794
  228. package/dist/session/session-manager.js +0 -653
  229. package/dist/sidecars/index.js +0 -33
  230. package/dist/sidecars/listeners.js +0 -40
  231. package/dist/sidecars/provenance-summary.js +0 -238
  232. package/dist/sidecars/quality-gaps.js +0 -258
  233. package/dist/sidecars/schema.js +0 -149
  234. package/dist/sidecars/spec-drift.js +0 -151
  235. package/dist/sidecars/waiver-draft.js +0 -176
  236. package/dist/spec/SpecFileManager.js +0 -419
  237. package/dist/templates/.caws/schemas/policy.schema.json +0 -117
  238. package/dist/templates/.caws/schemas/scope.schema.json +0 -52
  239. package/dist/templates/.caws/schemas/waivers.schema.json +0 -106
  240. package/dist/templates/.caws/schemas/working-spec.schema.json +0 -340
  241. package/dist/templates/.caws/schemas/worktrees.schema.json +0 -38
  242. package/dist/templates/.caws/templates/working-spec.template.yml +0 -80
  243. package/dist/templates/.caws/tools/README.md +0 -18
  244. package/dist/templates/.caws/tools/scope-guard.js +0 -203
  245. package/dist/templates/.caws/tools-allow.json +0 -331
  246. package/dist/templates/.caws/waivers.yml +0 -19
  247. package/dist/templates/.claude/README.md +0 -190
  248. package/dist/templates/.claude/hooks/audit.sh +0 -121
  249. package/dist/templates/.claude/hooks/block-dangerous.sh +0 -203
  250. package/dist/templates/.claude/hooks/classify_command.py +0 -592
  251. package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
  252. package/dist/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
  253. package/dist/templates/.claude/hooks/naming-check.sh +0 -100
  254. package/dist/templates/.claude/hooks/protected-paths.sh +0 -39
  255. package/dist/templates/.claude/hooks/quality-check.sh +0 -81
  256. package/dist/templates/.claude/hooks/scan-secrets.sh +0 -85
  257. package/dist/templates/.claude/hooks/scope-guard.sh +0 -381
  258. package/dist/templates/.claude/hooks/session-caws-status.sh +0 -117
  259. package/dist/templates/.claude/hooks/session-log.sh +0 -634
  260. package/dist/templates/.claude/hooks/simplification-guard.sh +0 -92
  261. package/dist/templates/.claude/hooks/stop-worktree-check.sh +0 -46
  262. package/dist/templates/.claude/hooks/test_classify_command.py +0 -370
  263. package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
  264. package/dist/templates/.claude/hooks/validate-spec.sh +0 -76
  265. package/dist/templates/.claude/hooks/worktree-guard.sh +0 -220
  266. package/dist/templates/.claude/hooks/worktree-write-guard.sh +0 -190
  267. package/dist/templates/.claude/rules/git-safety.md +0 -26
  268. package/dist/templates/.claude/rules/worktree-isolation.md +0 -101
  269. package/dist/templates/.claude/settings.json +0 -141
  270. package/dist/templates/.cursor/README.md +0 -299
  271. package/dist/templates/.cursor/hooks/audit.sh +0 -55
  272. package/dist/templates/.cursor/hooks/block-dangerous.sh +0 -84
  273. package/dist/templates/.cursor/hooks/caws-quality-check.sh +0 -52
  274. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
  275. package/dist/templates/.cursor/hooks/format.sh +0 -38
  276. package/dist/templates/.cursor/hooks/naming-check.sh +0 -64
  277. package/dist/templates/.cursor/hooks/scan-secrets.sh +0 -51
  278. package/dist/templates/.cursor/hooks/scope-guard.sh +0 -52
  279. package/dist/templates/.cursor/hooks/session-log.sh +0 -924
  280. package/dist/templates/.cursor/hooks/validate-spec.sh +0 -83
  281. package/dist/templates/.cursor/hooks.json +0 -76
  282. package/dist/templates/.cursor/rules/00-claims-verification.mdc +0 -144
  283. package/dist/templates/.cursor/rules/01-working-style.mdc +0 -50
  284. package/dist/templates/.cursor/rules/02-quality-gates.mdc +0 -368
  285. package/dist/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
  286. package/dist/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
  287. package/dist/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
  288. package/dist/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
  289. package/dist/templates/.cursor/rules/07-process-ops.mdc +0 -20
  290. package/dist/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
  291. package/dist/templates/.cursor/rules/09-docstrings.mdc +0 -89
  292. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
  293. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
  294. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
  295. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
  296. package/dist/templates/.cursor/rules/README.md +0 -148
  297. package/dist/templates/.github/copilot-instructions.md +0 -82
  298. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
  299. package/dist/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
  300. package/dist/templates/.junie/guidelines.md +0 -73
  301. package/dist/templates/.vscode/launch.json +0 -17
  302. package/dist/templates/.vscode/settings.json +0 -95
  303. package/dist/templates/.windsurf/rules/caws-quality-standards.md +0 -54
  304. package/dist/templates/.windsurf/workflows/caws-guided-development.md +0 -92
  305. package/dist/templates/CLAUDE.md +0 -196
  306. package/dist/templates/COMMIT_CONVENTIONS.md +0 -86
  307. package/dist/templates/OIDC_SETUP.md +0 -300
  308. package/dist/templates/agents.md +0 -171
  309. package/dist/templates/codemod/README.md +0 -1
  310. package/dist/templates/codemod/test.js +0 -93
  311. package/dist/templates/docs/README.md +0 -151
  312. package/dist/templates/scripts/new_feature.sh +0 -80
  313. package/dist/templates/scripts/quality-gates/check-god-objects.js +0 -146
  314. package/dist/templates/scripts/quality-gates/run-quality-gates.js +0 -50
  315. package/dist/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
  316. package/dist/test-analysis.js +0 -786
  317. package/dist/tool-interface.js +0 -314
  318. package/dist/tool-loader.js +0 -303
  319. package/dist/tool-validator.js +0 -393
  320. package/dist/utils/agent-display.js +0 -210
  321. package/dist/utils/agent-session.js +0 -344
  322. package/dist/utils/async-utils.js +0 -188
  323. package/dist/utils/command-wrapper.js +0 -200
  324. package/dist/utils/event-log.js +0 -584
  325. package/dist/utils/event-renderer.js +0 -521
  326. package/dist/utils/finalization.js +0 -230
  327. package/dist/utils/git-lock.js +0 -119
  328. package/dist/utils/gitignore-updater.js +0 -158
  329. package/dist/utils/ide-detection.js +0 -133
  330. package/dist/utils/lifecycle-events.js +0 -94
  331. package/dist/utils/project-analysis.js +0 -367
  332. package/dist/utils/promise-utils.js +0 -72
  333. package/dist/utils/quality-gates-errors.js +0 -520
  334. package/dist/utils/quality-gates-utils.js +0 -387
  335. package/dist/utils/schema-validator.js +0 -50
  336. package/dist/utils/spec-resolver.js +0 -711
  337. package/dist/utils/typescript-detector.js +0 -369
  338. package/dist/utils/working-state.js +0 -530
  339. package/dist/utils/yaml-validation.js +0 -156
  340. package/dist/validation/spec-validation.js +0 -924
  341. package/dist/waivers-manager.js +0 -732
  342. package/dist/worktree/worktree-manager.js +0 -1735
  343. package/templates/.caws/schemas/policy.schema.json +0 -117
  344. package/templates/.caws/schemas/scope.schema.json +0 -52
  345. package/templates/.caws/schemas/waivers.schema.json +0 -106
  346. package/templates/.caws/schemas/working-spec.schema.json +0 -340
  347. package/templates/.caws/schemas/worktrees.schema.json +0 -38
  348. package/templates/.caws/templates/working-spec.template.yml +0 -80
  349. package/templates/.caws/tools/README.md +0 -18
  350. package/templates/.caws/tools/scope-guard.js +0 -203
  351. package/templates/.caws/tools-allow.json +0 -331
  352. package/templates/.caws/waivers.yml +0 -19
  353. package/templates/.claude/README.md +0 -190
  354. package/templates/.claude/hooks/audit.sh +0 -121
  355. package/templates/.claude/hooks/block-dangerous.sh +0 -203
  356. package/templates/.claude/hooks/classify_command.py +0 -592
  357. package/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
  358. package/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
  359. package/templates/.claude/hooks/naming-check.sh +0 -100
  360. package/templates/.claude/hooks/protected-paths.sh +0 -39
  361. package/templates/.claude/hooks/quality-check.sh +0 -81
  362. package/templates/.claude/hooks/scan-secrets.sh +0 -85
  363. package/templates/.claude/hooks/scope-guard.sh +0 -381
  364. package/templates/.claude/hooks/session-caws-status.sh +0 -117
  365. package/templates/.claude/hooks/session-log.sh +0 -634
  366. package/templates/.claude/hooks/simplification-guard.sh +0 -92
  367. package/templates/.claude/hooks/stop-worktree-check.sh +0 -46
  368. package/templates/.claude/hooks/test_classify_command.py +0 -370
  369. package/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
  370. package/templates/.claude/hooks/validate-spec.sh +0 -76
  371. package/templates/.claude/hooks/worktree-guard.sh +0 -220
  372. package/templates/.claude/hooks/worktree-write-guard.sh +0 -190
  373. package/templates/.claude/rules/git-safety.md +0 -26
  374. package/templates/.claude/rules/worktree-isolation.md +0 -101
  375. package/templates/.claude/settings.json +0 -141
  376. package/templates/.cursor/README.md +0 -299
  377. package/templates/.cursor/hooks/audit.sh +0 -55
  378. package/templates/.cursor/hooks/block-dangerous.sh +0 -84
  379. package/templates/.cursor/hooks/caws-quality-check.sh +0 -52
  380. package/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
  381. package/templates/.cursor/hooks/format.sh +0 -38
  382. package/templates/.cursor/hooks/naming-check.sh +0 -64
  383. package/templates/.cursor/hooks/scan-secrets.sh +0 -51
  384. package/templates/.cursor/hooks/scope-guard.sh +0 -52
  385. package/templates/.cursor/hooks/session-log.sh +0 -924
  386. package/templates/.cursor/hooks/validate-spec.sh +0 -83
  387. package/templates/.cursor/hooks.json +0 -76
  388. package/templates/.cursor/rules/00-claims-verification.mdc +0 -144
  389. package/templates/.cursor/rules/01-working-style.mdc +0 -50
  390. package/templates/.cursor/rules/02-quality-gates.mdc +0 -368
  391. package/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
  392. package/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
  393. package/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
  394. package/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
  395. package/templates/.cursor/rules/07-process-ops.mdc +0 -20
  396. package/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
  397. package/templates/.cursor/rules/09-docstrings.mdc +0 -89
  398. package/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
  399. package/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
  400. package/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
  401. package/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
  402. package/templates/.cursor/rules/README.md +0 -148
  403. package/templates/.github/copilot-instructions.md +0 -82
  404. package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
  405. package/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
  406. package/templates/.junie/guidelines.md +0 -73
  407. package/templates/.vscode/launch.json +0 -17
  408. package/templates/.vscode/settings.json +0 -95
  409. package/templates/.windsurf/rules/caws-quality-standards.md +0 -54
  410. package/templates/.windsurf/workflows/caws-guided-development.md +0 -92
  411. package/templates/CLAUDE.md +0 -196
  412. package/templates/COMMIT_CONVENTIONS.md +0 -86
  413. package/templates/OIDC_SETUP.md +0 -300
  414. package/templates/agents.md +0 -171
  415. package/templates/codemod/README.md +0 -1
  416. package/templates/codemod/test.js +0 -93
  417. package/templates/docs/README.md +0 -151
  418. package/templates/scripts/new_feature.sh +0 -80
  419. package/templates/scripts/quality-gates/check-god-objects.js +0 -146
  420. package/templates/scripts/quality-gates/run-quality-gates.js +0 -50
  421. package/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
@@ -1,1735 +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 {
13
- getAgentSessionId,
14
- loadAgentRegistry,
15
- findSessionLogs,
16
- refreshAgentClaim,
17
- } = require('../utils/agent-session');
18
- const { formatClaimNotice, formatOrphanLogHint } = require('../utils/agent-display');
19
- const { lifecycle, EVENTS } = require('../utils/lifecycle-events');
20
-
21
- const WORKTREES_DIR = '.caws/worktrees';
22
- const REGISTRY_FILE = '.caws/worktrees.json';
23
- const BRANCH_PREFIX = 'caws/';
24
-
25
- function findFeatureSpecPath(root, specId) {
26
- if (!specId) return null;
27
-
28
- const candidates = [
29
- path.join(root, '.caws', 'specs', `${specId}.yaml`),
30
- path.join(root, '.caws', 'specs', `${specId}.yml`),
31
- ];
32
-
33
- return candidates.find((candidate) => fs.existsSync(candidate)) || null;
34
- }
35
-
36
- /**
37
- * Resolve a feature spec path, preferring a worktree-local copy when cwd
38
- * is inside a worktree. Falls back to the main repo.
39
- *
40
- * Why two-step: `caws worktree bind` may be invoked from inside a worktree
41
- * that was forked off a non-main base branch (Option C fork-off-sibling
42
- * pattern). In that workflow the spec is committed on the worktree's own
43
- * branch and never lands on main. The pre-CAWSFIX-25 behavior of looking
44
- * only in `root/.caws/specs/` made bind unusable there (D8 ledger entry).
45
- *
46
- * @param {string} root - Main repo root (from getRepoRoot())
47
- * @param {string} specId - Spec identifier
48
- * @param {string} [cwd=process.cwd()] - Directory to resolve from
49
- * @returns {string|null} Absolute path to the spec file, or null
50
- */
51
- function findFeatureSpecPathFromCwd(root, specId, cwd) {
52
- if (!specId) return null;
53
- const effectiveCwd = cwd || process.cwd();
54
-
55
- // Normalize both sides against symlinks before comparing. On macOS
56
- // `/tmp` and `/var/folders` are symlinks under `/private`, so the literal
57
- // `startsWith` check fails intermittently in the test fixture. Fall back
58
- // to the pre-resolution path if realpath throws (e.g., cwd removed).
59
- const resolve = (p) => {
60
- try { return fs.realpathSync(p); } catch { return p; }
61
- };
62
- const resolvedCwd = resolve(effectiveCwd);
63
- const worktreesBase = resolve(path.join(root, '.caws', 'worktrees'));
64
-
65
- if (resolvedCwd.startsWith(worktreesBase + path.sep)) {
66
- const relative = path.relative(worktreesBase, resolvedCwd);
67
- const worktreeName = relative.split(path.sep)[0];
68
- if (worktreeName) {
69
- const worktreeRoot = path.join(worktreesBase, worktreeName);
70
- const local = findFeatureSpecPath(worktreeRoot, specId);
71
- if (local) return local;
72
- }
73
- }
74
-
75
- return findFeatureSpecPath(root, specId);
76
- }
77
-
78
- function writeSpecWithWorktree(filePath, worktreeName) {
79
- const yaml = require('js-yaml');
80
- const content = fs.readFileSync(filePath, 'utf8');
81
- const parsed = yaml.load(content);
82
- if (!parsed || typeof parsed !== 'object') {
83
- return content;
84
- }
85
-
86
- // CAWSFIX-24 / D10: if the on-disk spec already declares the target
87
- // worktree and reloads to an equivalent object, return the original
88
- // bytes untouched. js-yaml.dump re-wraps folded scalars at its own
89
- // line-width preference, which otherwise produces spurious bytes-only
90
- // diffs on every bind/create. That mechanical churn (a) leaves dirty
91
- // files on main after worktree create, and (b) causes merge conflicts
92
- // when two validator invocations wrap the same title at different
93
- // widths.
94
- if (parsed.worktree === worktreeName) {
95
- return content;
96
- }
97
-
98
- parsed.worktree = worktreeName;
99
- return yaml.dump(parsed, { lineWidth: 120, noRefs: true });
100
- }
101
-
102
- function hasPathChanges(root, relativePath) {
103
- try {
104
- const output = execFileSync(
105
- 'git',
106
- ['status', '--porcelain', '--', relativePath],
107
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
108
- ).trim();
109
- return output.length > 0;
110
- } catch {
111
- return false;
112
- }
113
- }
114
-
115
- function ensureCanonicalSpecCommitted(root, specPath, specId, worktreeName) {
116
- const relativeSpecPath = path.relative(root, specPath);
117
- const nextContent = writeSpecWithWorktree(specPath, worktreeName);
118
- const currentContent = fs.readFileSync(specPath, 'utf8');
119
-
120
- if (currentContent !== nextContent) {
121
- fs.writeFileSync(specPath, nextContent);
122
- }
123
-
124
- if (!hasPathChanges(root, relativeSpecPath)) {
125
- return false;
126
- }
127
-
128
- execFileSync('git', ['add', '--', relativeSpecPath], {
129
- cwd: root,
130
- stdio: 'pipe',
131
- });
132
- execFileSync(
133
- 'git',
134
- ['commit', '-m', `chore(caws): bind spec ${specId} to worktree ${worktreeName}`, '--', relativeSpecPath],
135
- {
136
- cwd: root,
137
- stdio: 'pipe',
138
- }
139
- );
140
- return true;
141
- }
142
-
143
- function materializeWorktreeSpec(root, cawsDest, specId, worktreeName, scope) {
144
- if (!specId) return;
145
-
146
- const canonicalSpecPath = findFeatureSpecPath(root, specId);
147
- const destSpecsDir = path.join(cawsDest, 'specs');
148
-
149
- // CAWSFIX-24 / D5: never write to .caws/working-spec.yaml inside the
150
- // worktree. That file is the shared project baseline and must remain
151
- // byte-identical to what was checked out from HEAD. The feature spec
152
- // is materialized only under .caws/specs/<id>.yaml, which is what
153
- // spec-resolver and commands actually read via --spec-id / registry.
154
-
155
- if (canonicalSpecPath) {
156
- const destSpecPath = path.join(destSpecsDir, path.basename(canonicalSpecPath));
157
- fs.ensureDirSync(destSpecsDir);
158
-
159
- // Keep a canonical feature-spec copy inside the worktree.
160
- const specContent = writeSpecWithWorktree(canonicalSpecPath, worktreeName);
161
- // writeSpecWithWorktree is idempotent (CAWSFIX-24 / D10): if the spec
162
- // already has the worktree field and reloads to equivalent YAML, the
163
- // returned content matches what's on disk. Skip the write in that case
164
- // so `git status` stays clean.
165
- const existing = fs.existsSync(destSpecPath) ? fs.readFileSync(destSpecPath, 'utf8') : null;
166
- if (existing !== specContent) {
167
- fs.writeFileSync(destSpecPath, specContent);
168
- }
169
- return;
170
- }
171
-
172
- // specId given but no canonical spec found — generate a default feature
173
- // spec at .caws/specs/<specId>.yaml so the worktree has something to
174
- // resolve against. Do not touch .caws/working-spec.yaml.
175
- console.warn(
176
- chalk.yellow(`Warning: spec '${specId}' not found in .caws/specs/ — generating default feature spec for worktree`)
177
- );
178
-
179
- const { generateWorkingSpec } = require('../generators/working-spec');
180
- let specContent = generateWorkingSpec({
181
- projectId: specId,
182
- projectTitle: `Worktree: ${worktreeName}`,
183
- projectDescription: `Isolated worktree for ${worktreeName}`,
184
- riskTier: 3,
185
- projectMode: 'feature',
186
- scopeIn: scope || 'src/',
187
- scopeOut: 'node_modules/, dist/, build/',
188
- maxFiles: 25,
189
- maxLoc: 1000,
190
- blastModules: scope || 'src',
191
- dataMigration: false,
192
- rollbackSlo: '5m',
193
- projectThreats: '',
194
- projectInvariants: 'System maintains data consistency',
195
- acceptanceCriteria: 'Given current state, when action occurs, then expected result',
196
- a11yRequirements: 'keyboard',
197
- perfBudget: 250,
198
- securityRequirements: 'validation',
199
- contractType: '',
200
- contractPath: '',
201
- observabilityLogs: '',
202
- observabilityMetrics: '',
203
- observabilityTraces: '',
204
- migrationPlan: '',
205
- rollbackPlan: '',
206
- needsOverride: false,
207
- isExperimental: false,
208
- aiConfidence: 0.8,
209
- uncertaintyAreas: '',
210
- complexityFactors: '',
211
- });
212
-
213
- try {
214
- const yaml = require('js-yaml');
215
- const parsed = yaml.load(specContent);
216
- if (parsed && typeof parsed === 'object') {
217
- parsed.worktree = worktreeName;
218
- specContent = yaml.dump(parsed, { lineWidth: 120, noRefs: true });
219
- }
220
- } catch {
221
- // Keep generated spec content if augmentation fails.
222
- }
223
-
224
- fs.ensureDirSync(destSpecsDir);
225
- const generatedSpecPath = path.join(destSpecsDir, `${specId}.yaml`);
226
- fs.writeFileSync(generatedSpecPath, specContent);
227
- }
228
-
229
- function parseSpecIdFromYamlFile(filePath) {
230
- try {
231
- const yaml = require('js-yaml');
232
- const doc = yaml.load(fs.readFileSync(filePath, 'utf8'));
233
- if (doc && typeof doc.id === 'string' && doc.id.trim()) {
234
- return doc.id.trim();
235
- }
236
- } catch {
237
- // Ignore malformed YAML during inference
238
- }
239
- return null;
240
- }
241
-
242
- /**
243
- * Scan .caws/specs/ for a spec that declares `worktree: <name>`.
244
- * Returns the spec's id if found, null otherwise.
245
- * This enables auto-binding: when a spec already names the worktree
246
- * it expects, the registry entry gets the specId automatically.
247
- * @param {string} root - Repository root
248
- * @param {string} worktreeName - Worktree name to match
249
- * @returns {string|null} Spec ID or null
250
- */
251
- function findSpecByWorktreeName(root, worktreeName) {
252
- const yaml = require('js-yaml');
253
- const specsDir = path.join(root, '.caws', 'specs');
254
- if (!fs.existsSync(specsDir)) return null;
255
-
256
- const specFiles = fs.readdirSync(specsDir)
257
- .filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'));
258
-
259
- for (const specFile of specFiles) {
260
- try {
261
- const doc = yaml.load(fs.readFileSync(path.join(specsDir, specFile), 'utf8'));
262
- if (doc && doc.worktree === worktreeName && typeof doc.id === 'string') {
263
- return doc.id.trim();
264
- }
265
- } catch {
266
- // Skip malformed spec files
267
- }
268
- }
269
- return null;
270
- }
271
-
272
- function inferSpecIdForWorktree(worktreePath) {
273
- if (!worktreePath) return null;
274
-
275
- const specsDir = path.join(worktreePath, '.caws', 'specs');
276
- if (fs.existsSync(specsDir)) {
277
- const specFiles = fs.readdirSync(specsDir)
278
- .filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'))
279
- .sort();
280
-
281
- for (const specFile of specFiles) {
282
- const inferred = parseSpecIdFromYamlFile(path.join(specsDir, specFile));
283
- if (inferred) {
284
- return inferred;
285
- }
286
- }
287
- }
288
-
289
- return parseSpecIdFromYamlFile(path.join(worktreePath, '.caws', 'working-spec.yaml'));
290
- }
291
-
292
- /**
293
- * Get the last commit info for a branch
294
- * @param {string} branch - Branch name
295
- * @param {string} root - Repository root
296
- * @returns {{ age: string, timestamp: Date, sha: string } | null}
297
- */
298
- function getLastCommitInfo(branch, root) {
299
- try {
300
- const output = execFileSync(
301
- 'git',
302
- ['log', branch, '-1', '--format=%H%n%aI%n%ar'],
303
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
304
- ).trim();
305
- const [sha, iso, age] = output.split('\n');
306
- return { sha, timestamp: new Date(iso), age };
307
- } catch {
308
- return null;
309
- }
310
- }
311
-
312
- /**
313
- * Check if a branch has been merged into another branch
314
- * @param {string} branch - Branch to check
315
- * @param {string} target - Target branch (e.g., "main")
316
- * @param {string} root - Repository root
317
- * @returns {boolean}
318
- */
319
- function isBranchMerged(branch, target, root) {
320
- try {
321
- const merged = execFileSync(
322
- 'git',
323
- ['branch', '--merged', target, '--list', branch],
324
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
325
- ).trim();
326
- return merged.length > 0;
327
- } catch {
328
- return false;
329
- }
330
- }
331
-
332
- /**
333
- * Check if a branch has divergent commits from target (commits on branch not on target).
334
- * @param {string} branch - Branch to check
335
- * @param {string} target - Target branch (e.g., "main")
336
- * @param {string} root - Repository root
337
- * @returns {boolean}
338
- */
339
- function hasDivergentCommits(branch, target, root) {
340
- try {
341
- const count = execFileSync(
342
- 'git',
343
- ['rev-list', '--count', `${target}..${branch}`],
344
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
345
- ).trim();
346
- return parseInt(count, 10) > 0;
347
- } catch {
348
- return false;
349
- }
350
- }
351
-
352
- /**
353
- * Check if a worktree directory has dirty (uncommitted) files.
354
- * @param {string} worktreePath - Path to the worktree
355
- * @returns {boolean}
356
- */
357
- function hasDirtyFiles(worktreePath) {
358
- try {
359
- const status = execFileSync(
360
- 'git',
361
- ['status', '--porcelain'],
362
- { cwd: worktreePath, encoding: 'utf8', stdio: 'pipe' }
363
- ).trim();
364
- return status.length > 0;
365
- } catch {
366
- return false;
367
- }
368
- }
369
-
370
- /**
371
- * Get the canonical git repository root (main worktree, not a linked worktree).
372
- *
373
- * `git rev-parse --show-toplevel` returns the root of whichever worktree
374
- * the CWD is inside. In a linked worktree that is NOT the main repo root,
375
- * so CAWS would read the wrong (or missing) .caws/worktrees.json.
376
- *
377
- * `--git-common-dir` always resolves to the main repo's .git directory,
378
- * even from inside a linked worktree. Its parent is the canonical repo root.
379
- *
380
- * @returns {string} Absolute path to the main repo root
381
- */
382
- function getRepoRoot() {
383
- const gitCommonDir = execFileSync(
384
- 'git',
385
- ['rev-parse', '--path-format=absolute', '--git-common-dir'],
386
- { encoding: 'utf8' }
387
- ).trim();
388
- // gitCommonDir is /path/to/main-repo/.git — parent is the repo root
389
- return path.dirname(gitCommonDir);
390
- }
391
-
392
- /**
393
- * Get current branch name
394
- * @returns {string}
395
- */
396
- function getCurrentBranch() {
397
- return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
398
- encoding: 'utf8',
399
- }).trim();
400
- }
401
-
402
- // Track whether we've already warned about schema violations this process.
403
- // loadRegistry() is called multiple times per command; warning every time
404
- // floods stderr and contributes to Claude Code context-window exhaustion.
405
- let _schemaWarned = false;
406
-
407
- /**
408
- * CAWSFIX-31: Assert that the current agent session may operate on
409
- * worktree `name`. The decision is purely session-id-equality based —
410
- * never TTL, never log freshness — because rolled-over and resumed
411
- * sessions should not be auto-blocked just because their registry
412
- * entry was pruned.
413
- *
414
- * Returns `{ allowed, warning?, priorOwner? }`. The caller decides
415
- * how to react:
416
- *
417
- * - allowed=true, no warning → silent proceed (same session id, no claim, etc.)
418
- * - allowed=true, warning present → soft notice (orphan session log, no block)
419
- * - allowed=false, warning present → soft-block; surface warning, exit non-zero
420
- *
421
- * On takeover (`allowTakeover: true`), the function rewrites the
422
- * worktree entry's owner to the current session id and appends the
423
- * prior owner to a `prior_owners` audit array (with lastSeen captured
424
- * from agents.json at takeover time, or null if pruned).
425
- *
426
- * @param {string} root - Project root
427
- * @param {string} name - Worktree name
428
- * @param {object} [opts]
429
- * @param {boolean} [opts.allowTakeover=false] - Apply takeover when true
430
- * @param {string} [opts.takeoverCommandHint] - Suggested command for the warning
431
- * @returns {{ allowed: boolean, warning?: string, priorOwner?: object }}
432
- */
433
- function assertWorktreeOwnership(root, name, opts = {}) {
434
- const { allowTakeover = false, takeoverCommandHint } = opts;
435
- const registry = loadRegistry(root);
436
- const entry = registry.worktrees[name];
437
- if (!entry) {
438
- return { allowed: true };
439
- }
440
-
441
- const currentSession = getAgentSessionId(root);
442
- const owner = entry.owner;
443
-
444
- // No CAWS-tracked owner — surface session-log hint if present, but
445
- // allow the operation to proceed.
446
- if (!owner) {
447
- const branch = entry.branch || null;
448
- const logs = branch ? findSessionLogs(root, { branch }) : [];
449
- if (logs.length > 0) {
450
- return {
451
- allowed: true,
452
- warning: formatOrphanLogHint({ worktree: name, sessionLogs: logs, root }),
453
- };
454
- }
455
- return { allowed: true };
456
- }
457
-
458
- // Same session id → silent proceed. Roll-over case included: an
459
- // agent that resumed with the same session id is its own claimant.
460
- if (currentSession && owner === currentSession) {
461
- return { allowed: true };
462
- }
463
-
464
- // Foreign claim — gather context.
465
- const agentRegistry = loadAgentRegistry(root);
466
- const priorOwnerEntry = agentRegistry.agents[owner] || null;
467
- const priorOwnerLastSeen = priorOwnerEntry ? priorOwnerEntry.lastSeen : null;
468
- const priorOwnerPlatform = priorOwnerEntry ? priorOwnerEntry.platform : 'unknown';
469
-
470
- // Surface session-log pointers (by sid OR by branch).
471
- const branch = entry.branch || null;
472
- const seen = new Set();
473
- const sessionLogs = [];
474
- for (const log of findSessionLogs(root, { sessionId: owner })) {
475
- if (seen.has(log.path)) continue;
476
- seen.add(log.path);
477
- sessionLogs.push(log);
478
- }
479
- if (branch) {
480
- for (const log of findSessionLogs(root, { branch })) {
481
- if (seen.has(log.path)) continue;
482
- seen.add(log.path);
483
- sessionLogs.push(log);
484
- }
485
- }
486
-
487
- const takeoverCommand =
488
- takeoverCommandHint || `caws worktree claim ${name} --takeover`;
489
- const warning = formatClaimNotice({
490
- worktree: name,
491
- priorOwnerEntry,
492
- priorOwnerSessionId: owner,
493
- sessionLogs,
494
- root,
495
- takeoverCommand,
496
- });
497
-
498
- if (!allowTakeover) {
499
- return { allowed: false, warning };
500
- }
501
-
502
- // Takeover: rewrite owner, append prior_owners audit entry.
503
- const priorOwners = Array.isArray(entry.prior_owners) ? entry.prior_owners : [];
504
- priorOwners.push({
505
- sessionId: owner,
506
- platform: priorOwnerPlatform,
507
- lastSeen: priorOwnerLastSeen,
508
- takenOver_at: new Date().toISOString(),
509
- });
510
- registry.worktrees[name] = {
511
- ...entry,
512
- owner: currentSession || null,
513
- prior_owners: priorOwners,
514
- };
515
- saveRegistry(root, registry);
516
-
517
- // Heartbeat the new owner so agents.json reflects the takeover too.
518
- // Without this, `caws status` and `caws agents list` would show the
519
- // takeover'd worktree with an "unknown / pruned" current owner until
520
- // some other lifecycle verb fires.
521
- refreshAgentClaim(root, { worktree: name });
522
-
523
- return {
524
- allowed: true,
525
- priorOwner: {
526
- sessionId: owner,
527
- platform: priorOwnerPlatform,
528
- lastSeen: priorOwnerLastSeen,
529
- },
530
- };
531
- }
532
-
533
- /**
534
- * Load the worktree registry
535
- * @param {string} root - Repository root
536
- * @returns {Object} Registry object
537
- */
538
- function loadRegistry(root) {
539
- const registryPath = path.join(root, REGISTRY_FILE);
540
- try {
541
- if (fs.existsSync(registryPath)) {
542
- const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
543
- try {
544
- const validate = createValidator(getSchemaPath('worktrees.schema.json', root));
545
- const result = validate(data);
546
- if (!result.valid && !_schemaWarned) {
547
- _schemaWarned = true;
548
- console.warn('Worktree registry has schema violations:', result.errors);
549
- }
550
- } catch (schemaErr) {
551
- if (!_schemaWarned) {
552
- _schemaWarned = true;
553
- console.warn('Could not validate worktree registry schema:', schemaErr.message);
554
- }
555
- }
556
- return data;
557
- }
558
- } catch {
559
- // Corrupted registry, start fresh
560
- }
561
- return { version: 1, worktrees: {} };
562
- }
563
-
564
- /**
565
- * Save the worktree registry
566
- * @param {string} root - Repository root
567
- * @param {Object} registry - Registry object
568
- */
569
- function saveRegistry(root, registry) {
570
- // Auto-prune ghost entries: any registry entry whose path directory AND
571
- // stored branch are BOTH gone. Previously this only fired for entries
572
- // explicitly marked `status: destroyed`, which missed two common cases:
573
- // 1. A worktree removed via `git worktree remove` (not `caws worktree
574
- // destroy`) that later had its branch manually deleted with
575
- // `git branch -D`.
576
- // 2. A worktree whose create failed partway, leaving a registry entry
577
- // at `fresh`/`active` but no artifacts on disk.
578
- // Both are pure ghost state — no recoverable work remains in either
579
- // the directory or the branch. Pruning is safe. (CAWSFIX-25 / D7)
580
- //
581
- // Entries with ONE artifact intact (dir gone but branch still present,
582
- // or vice versa) are preserved. The branch may still hold unmerged
583
- // commits, or the directory may still hold uncommitted work — the user
584
- // should merge or explicitly destroy.
585
- for (const [name, entry] of Object.entries(registry.worktrees || {})) {
586
- const dirGone = !fs.existsSync(entry.path);
587
- let branchGone = true;
588
- if (entry.branch) {
589
- try {
590
- execFileSync('git', ['rev-parse', '--verify', entry.branch], {
591
- cwd: root, stdio: 'pipe',
592
- });
593
- branchGone = false;
594
- } catch {
595
- branchGone = true;
596
- }
597
- }
598
- if (dirGone && branchGone) {
599
- delete registry.worktrees[name];
600
- }
601
- }
602
-
603
- const registryPath = path.join(root, REGISTRY_FILE);
604
- fs.ensureDirSync(path.dirname(registryPath));
605
- fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
606
- }
607
-
608
- /**
609
- * Discover git worktrees under .caws/worktrees/ that are not in the registry.
610
- * @param {string} root - Repository root
611
- * @param {Object} registry - Current registry object
612
- * @returns {Array<{ name: string, path: string, branch: string }>}
613
- */
614
- function discoverUnregisteredWorktrees(root, registry) {
615
- const unregistered = [];
616
- try {
617
- const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
618
- cwd: root,
619
- encoding: 'utf8',
620
- stdio: 'pipe',
621
- });
622
- let worktreesDir;
623
- try {
624
- worktreesDir = fs.realpathSync(path.resolve(root, WORKTREES_DIR));
625
- } catch {
626
- // Directory might not exist yet
627
- worktreesDir = path.resolve(root, WORKTREES_DIR);
628
- }
629
-
630
- const blocks = output.split('\n\n').filter(Boolean);
631
- for (const block of blocks) {
632
- const lines = block.split('\n');
633
- const wtLine = lines.find((l) => l.startsWith('worktree '));
634
- const branchLine = lines.find((l) => l.startsWith('branch '));
635
- if (!wtLine) continue;
636
-
637
- const wtPath = wtLine.replace('worktree ', '');
638
- let resolvedPath;
639
- try {
640
- resolvedPath = fs.realpathSync(wtPath);
641
- } catch {
642
- resolvedPath = path.resolve(wtPath);
643
- }
644
-
645
- // Only consider worktrees under .caws/worktrees/
646
- if (!resolvedPath.startsWith(worktreesDir + path.sep)) continue;
647
-
648
- const name = path.basename(resolvedPath);
649
- if (registry.worktrees[name]) continue;
650
-
651
- const branch = branchLine
652
- ? branchLine.replace('branch refs/heads/', '')
653
- : `${BRANCH_PREFIX}${name}`;
654
- unregistered.push({ name, path: resolvedPath, branch });
655
- }
656
- } catch {
657
- // git worktree list failed
658
- }
659
- return unregistered;
660
- }
661
-
662
- /**
663
- * Auto-register an unregistered worktree. Infers baseBranch via merge-base.
664
- * @param {string} root - Repository root
665
- * @param {Object} registry - Registry object (mutated in place)
666
- * @param {{ name: string, path: string, branch: string }} discovered
667
- * @returns {Object} The registered entry
668
- */
669
- function autoRegisterWorktree(root, registry, discovered) {
670
- let baseBranch = 'main';
671
- try {
672
- execFileSync(
673
- 'git',
674
- ['merge-base', discovered.branch, 'main'],
675
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
676
- );
677
- } catch {
678
- try {
679
- execFileSync(
680
- 'git',
681
- ['merge-base', discovered.branch, 'master'],
682
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
683
- );
684
- baseBranch = 'master';
685
- } catch {
686
- // Keep 'main' as default
687
- }
688
- }
689
-
690
- const entry = {
691
- name: discovered.name,
692
- path: discovered.path,
693
- branch: discovered.branch,
694
- baseBranch,
695
- scope: null,
696
- specId: inferSpecIdForWorktree(discovered.path),
697
- owner: null,
698
- createdAt: new Date().toISOString(),
699
- status: 'active',
700
- autoRegistered: true,
701
- };
702
-
703
- registry.worktrees[discovered.name] = entry;
704
- saveRegistry(root, registry);
705
- return entry;
706
- }
707
-
708
- /**
709
- * Create a new git worktree with scope isolation
710
- * @param {string} name - Worktree name
711
- * @param {Object} options - Creation options
712
- * @param {string} [options.scope] - Sparse checkout pattern (e.g., "src/auth/**")
713
- * @param {string} [options.baseBranch] - Base branch to create from
714
- * @param {string} [options.specId] - Associated spec ID for standard+ modes
715
- * @returns {Object} Created worktree info
716
- */
717
- function createWorktree(name, options = {}) {
718
- const root = getRepoRoot();
719
- const { scope, baseBranch, specId } = options;
720
-
721
- // Validate name
722
- if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
723
- throw new Error('Worktree name must contain only letters, numbers, hyphens, and underscores');
724
- }
725
-
726
- const registry = loadRegistry(root);
727
-
728
- // Check for duplicate in registry
729
- if (registry.worktrees[name]) {
730
- const existing = registry.worktrees[name];
731
- if (existing.status !== 'destroyed') {
732
- const ownerInfo = existing.owner ? ` (owned by session ${existing.owner})` : '';
733
- throw new Error(
734
- `Worktree '${name}' already exists with status '${existing.status}'${ownerInfo}.\n` +
735
- `Use 'caws worktree destroy ${name}' first, or choose a different name.`
736
- );
737
- }
738
- // Destroyed entries: check if another session owns the branch
739
- if (existing.owner && existing.owner !== getAgentSessionId(root)) {
740
- // Branch may still be in use by the owning session for merge
741
- try {
742
- const branchExists = execFileSync('git', ['rev-parse', '--verify', BRANCH_PREFIX + name], {
743
- cwd: root, stdio: 'pipe',
744
- }).toString().trim();
745
- if (branchExists) {
746
- throw new Error(
747
- `Worktree '${name}' was destroyed but branch '${BRANCH_PREFIX}${name}' still exists ` +
748
- `(owned by session ${existing.owner}).\n` +
749
- `The owning session may still need this branch for merging.\n` +
750
- `Choose a different name, or delete the branch first: git branch -d ${BRANCH_PREFIX}${name}`
751
- );
752
- }
753
- } catch (e) {
754
- if (e.message.includes('owned by session')) throw e;
755
- // Branch doesn't exist — safe to reuse the name
756
- }
757
- }
758
- }
759
-
760
- const worktreePath = path.join(root, WORKTREES_DIR, name);
761
- const branchName = BRANCH_PREFIX + name;
762
- const base = baseBranch || getCurrentBranch();
763
-
764
- // CAWSFIX-27: resolve the bound specId (explicit --spec-id OR auto-bind
765
- // via worktree-name match) BEFORE creating the worktree, so the
766
- // draft→active flip + bind commit land on the base branch before the
767
- // worktree forks. Pre-CAWSFIX-27 the auto-bind path activated the spec
768
- // but never committed it, leaving main with a dirty spec after
769
- // `caws worktree create <name>` (no --spec-id).
770
- let resolvedSpecId = specId || null;
771
- if (!resolvedSpecId) {
772
- resolvedSpecId = findSpecByWorktreeName(root, name);
773
- if (resolvedSpecId) {
774
- console.log(chalk.gray(` Auto-bound spec: ${resolvedSpecId}`));
775
- }
776
- }
777
- const canonicalSpecPath = findFeatureSpecPath(root, resolvedSpecId);
778
-
779
- // Check if the branch already exists in git (even if not in registry)
780
- // This catches cases where another agent created the branch outside CAWS
781
- try {
782
- execFileSync('git', ['rev-parse', '--verify', branchName], {
783
- cwd: root, stdio: 'pipe',
784
- });
785
- // Branch exists — refuse unless it's fully merged into base
786
- const currentSession = getAgentSessionId(root);
787
- const registryOwner = registry.worktrees[name]?.owner;
788
- if (registryOwner && registryOwner !== currentSession) {
789
- throw new Error(
790
- `Branch '${branchName}' already exists and is owned by another session (${registryOwner}).\n` +
791
- `Another agent may be using this branch. Choose a different worktree name.`
792
- );
793
- }
794
- // Branch exists but no owner conflict — warn and reuse
795
- console.warn(`Warning: Branch '${branchName}' already exists, reusing it.`);
796
- } catch (e) {
797
- if (e.message.includes('already exists and is owned')) throw e;
798
- // Branch doesn't exist — this is the normal path
799
- }
800
-
801
- // Create the worktree directory
802
- fs.ensureDirSync(path.dirname(worktreePath));
803
-
804
- if (canonicalSpecPath && resolvedSpecId) {
805
- // CAWSFIX-23: flip draft→active BEFORE the bind commit so the spec
806
- // lifecycle transition lands in the same commit as the worktree field.
807
- // CAWSFIX-27: this block now handles BOTH the explicit --spec-id path
808
- // and the auto-bind (findSpecByWorktreeName) path — previously only
809
- // the explicit path committed the flip.
810
- autoActivateBoundSpec(root, resolvedSpecId);
811
- ensureCanonicalSpecCommitted(root, canonicalSpecPath, resolvedSpecId, name);
812
- }
813
-
814
- // Create git worktree with new branch
815
- try {
816
- execFileSync('git', ['worktree', 'add', '-b', branchName, worktreePath, base], {
817
- cwd: root,
818
- stdio: 'pipe',
819
- });
820
- } catch (error) {
821
- // Branch already exists (caught above and allowed) — attach to it
822
- if (error.message.includes('already exists')) {
823
- execFileSync('git', ['worktree', 'add', worktreePath, branchName], {
824
- cwd: root,
825
- stdio: 'pipe',
826
- });
827
- } else {
828
- throw new Error(`Failed to create worktree: ${error.message}`);
829
- }
830
- }
831
-
832
- // Set up sparse checkout if scope is provided
833
- if (scope) {
834
- try {
835
- // Parse scope patterns (comma-separated)
836
- const patterns = scope.split(',').map((p) => p.trim());
837
-
838
- // Detect glob characters — cone mode only accepts directory paths,
839
- // not glob patterns like "core/reasoning/**" or "*.py".
840
- const hasGlobs = patterns.some((p) => /[*?[\]]/.test(p));
841
- const coneFlag = hasGlobs ? '--no-cone' : '--cone';
842
-
843
- execFileSync('git', ['sparse-checkout', 'init', coneFlag], {
844
- cwd: worktreePath,
845
- stdio: 'pipe',
846
- });
847
-
848
- execFileSync('git', ['sparse-checkout', 'set', ...patterns], {
849
- cwd: worktreePath,
850
- stdio: 'pipe',
851
- });
852
- } catch (error) {
853
- console.warn(chalk.yellow(`Sparse checkout setup failed: ${error.message}`));
854
- console.warn(chalk.blue('Worktree created but without sparse checkout'));
855
- }
856
- }
857
-
858
- // Copy .caws/ config into worktree
859
- const cawsSource = path.join(root, '.caws');
860
- const cawsDest = path.join(worktreePath, '.caws');
861
- if (fs.existsSync(cawsSource)) {
862
- try {
863
- fs.copySync(cawsSource, cawsDest, {
864
- filter: (src) => {
865
- // Don't copy worktrees directory or registry into the worktree
866
- const rel = path.relative(cawsSource, src);
867
- return !rel.startsWith('worktrees') && rel !== 'worktrees.json';
868
- },
869
- });
870
- } catch {
871
- // Non-fatal
872
- }
873
- }
874
-
875
- // CAWSFIX-27: resolvedSpecId is now computed before the worktree is
876
- // added (see block above the `fs.ensureDirSync` call). The activation
877
- // and bind-commit already ran on the base branch, so the worktree forks
878
- // from a base that already includes the flip commit.
879
-
880
- // Materialize a worktree-local working spec. Prefer the canonical feature
881
- // spec when it exists so isolated worktrees stay aligned with the main
882
- // registry/resolver model.
883
- if (resolvedSpecId) {
884
- try {
885
- materializeWorktreeSpec(root, cawsDest, resolvedSpecId, name, scope);
886
- } catch (error) {
887
- console.warn(
888
- chalk.yellow(`Could not materialize spec '${resolvedSpecId}' for worktree '${name}': ${error.message}`)
889
- );
890
- // Non-fatal: spec generation is optional
891
- }
892
- }
893
-
894
- // Register worktree
895
- const entry = {
896
- name,
897
- path: worktreePath,
898
- branch: branchName,
899
- baseBranch: base,
900
- scope: scope || null,
901
- specId: resolvedSpecId,
902
- owner: options.owner || getAgentSessionId(root) || null,
903
- createdAt: new Date().toISOString(),
904
- status: 'fresh',
905
- };
906
-
907
- registry.worktrees[name] = entry;
908
- saveRegistry(root, registry);
909
-
910
- // CAWSFIX-32: heartbeat the current session into agents.json so the
911
- // worktree+spec context is visible to other agents and to
912
- // `caws status` / `caws agents list` immediately after create.
913
- refreshAgentClaim(root, { worktree: name, specId: resolvedSpecId || null });
914
-
915
- return entry;
916
- }
917
-
918
- /**
919
- * Reconcile registry state against git worktree list and filesystem.
920
- *
921
- * Non-destructive read that classifies every known worktree entry
922
- * (from registry + git discovery) into one of:
923
- * active — directory exists AND in git worktree list
924
- * orphaned — directory exists but NOT in git worktree list
925
- * missing — directory gone, branch may or may not exist
926
- * destroyed — explicitly destroyed via CAWS
927
- * unregistered — in git worktree list but not in registry
928
- * stale-merged — missing + branch already merged to base
929
- *
930
- * Does NOT mutate the registry. Callers decide what to persist.
931
- *
932
- * @param {string} root - Repository root
933
- * @returns {{ entries: Array, gitWorktrees: string[] }}
934
- */
935
- function reconcileRegistry(root) {
936
- const registry = loadRegistry(root);
937
-
938
- let gitWorktrees = [];
939
- try {
940
- const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
941
- cwd: root,
942
- encoding: 'utf8',
943
- stdio: 'pipe',
944
- });
945
- gitWorktrees = output
946
- .split('\n\n')
947
- .filter(Boolean)
948
- .map((block) => {
949
- const lines = block.split('\n');
950
- const worktreeLine = lines.find((l) => l.startsWith('worktree '));
951
- return worktreeLine ? worktreeLine.replace('worktree ', '') : null;
952
- })
953
- .filter(Boolean);
954
- } catch {
955
- // Git worktree list failed
956
- }
957
-
958
- const entries = Object.values(registry.worktrees).map((entry) => {
959
- const exists = fs.existsSync(entry.path);
960
- const inGit = gitWorktrees.some(
961
- (wt) => path.resolve(wt) === path.resolve(entry.path)
962
- );
963
-
964
- const merged = entry.branch && entry.baseBranch
965
- ? isBranchMerged(entry.branch, entry.baseBranch, root)
966
- : false;
967
- const divergent = entry.branch && entry.baseBranch
968
- ? hasDivergentCommits(entry.branch, entry.baseBranch, root)
969
- : false;
970
- const dirty = exists ? hasDirtyFiles(entry.path) : false;
971
-
972
- let status;
973
- if (entry.status === 'destroyed') {
974
- status = 'destroyed';
975
- } else if (exists && inGit) {
976
- // Worktree directory exists and is tracked by git
977
- if (divergent || dirty) {
978
- // Has commits beyond base or uncommitted work → active
979
- status = 'active';
980
- } else if (merged) {
981
- // No divergent commits, branch aligned with base.
982
- // Use stored status as history to distinguish fresh vs merged:
983
- // - stored 'fresh' → never had divergent commits → still fresh
984
- // - stored 'active' → had work that's now merged → merged
985
- if (entry.status === 'active') {
986
- status = 'merged';
987
- } else {
988
- status = 'fresh';
989
- }
990
- } else {
991
- status = 'fresh';
992
- }
993
- } else if (exists) {
994
- status = 'orphaned';
995
- } else {
996
- status = merged ? 'stale-merged' : 'missing';
997
- }
998
-
999
- const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
1000
-
1001
- return { ...entry, status, lastCommit, merged, divergent, dirty };
1002
- });
1003
-
1004
- // Append unregistered worktrees discovered from git
1005
- const unregistered = discoverUnregisteredWorktrees(root, registry);
1006
- for (const discovered of unregistered) {
1007
- const lastCommit = getLastCommitInfo(discovered.branch, root);
1008
- entries.push({
1009
- name: discovered.name,
1010
- path: discovered.path,
1011
- branch: discovered.branch,
1012
- baseBranch: null,
1013
- scope: null,
1014
- specId: null,
1015
- owner: null,
1016
- createdAt: null,
1017
- status: 'unregistered',
1018
- lastCommit,
1019
- merged: false,
1020
- });
1021
- }
1022
-
1023
- return { entries, gitWorktrees };
1024
- }
1025
-
1026
- /**
1027
- * Repair registry drift caused by manual git operations outside CAWS.
1028
- *
1029
- * Scans registry vs git vs filesystem, classifies each entry, and optionally
1030
- * prunes stale entries. Reports the delta before persisting.
1031
- *
1032
- * @param {Object} options
1033
- * @param {boolean} [options.prune=false] - Remove destroyed, stale-merged, and missing entries
1034
- * @param {boolean} [options.dryRun=false] - Report only, do not persist
1035
- * @param {boolean} [options.force=false] - Allow pruning entries owned by other sessions
1036
- * @returns {{ repaired: Array, pruned: Array, skipped: Array }}
1037
- */
1038
- function repairWorktrees(options = {}) {
1039
- const { prune: shouldPrune = false, dryRun = false, force = false } = options;
1040
- const root = getRepoRoot();
1041
- const registry = loadRegistry(root);
1042
- const { entries } = reconcileRegistry(root);
1043
- const currentSession = getAgentSessionId(root);
1044
-
1045
- const repaired = [];
1046
- const pruned = [];
1047
- const skipped = [];
1048
-
1049
- for (const entry of entries) {
1050
- const regEntry = registry.worktrees[entry.name];
1051
-
1052
- if (entry.status === 'unregistered') {
1053
- if (!dryRun) {
1054
- autoRegisterWorktree(root, registry, entry);
1055
- }
1056
- repaired.push({ name: entry.name, action: 'registered', status: entry.status });
1057
- continue;
1058
- }
1059
-
1060
- if (!regEntry) continue;
1061
-
1062
- // Update registry status to match filesystem reality
1063
- const wasAlive = regEntry.status === 'active' || regEntry.status === 'fresh';
1064
- const nowDead = entry.status === 'missing' || entry.status === 'stale-merged';
1065
- if (wasAlive && nowDead) {
1066
- repaired.push({
1067
- name: entry.name,
1068
- action: 'status-updated',
1069
- from: regEntry.status,
1070
- to: entry.status,
1071
- owner: entry.owner || null,
1072
- });
1073
- }
1074
-
1075
- // Determine if entry is prunable (destroyed, stale-merged, or missing)
1076
- const isPrunable = entry.status === 'destroyed' ||
1077
- entry.status === 'stale-merged' ||
1078
- entry.status === 'missing';
1079
-
1080
- if (!isPrunable) continue;
1081
-
1082
- // Ownership check: refuse to prune another session's entries without --force
1083
- const isOwnedByOther = entry.owner && currentSession && entry.owner !== currentSession;
1084
-
1085
- if (shouldPrune && isPrunable) {
1086
- if (isOwnedByOther && !force) {
1087
- skipped.push({
1088
- name: entry.name,
1089
- reason: `owned by another session (${entry.owner}). Use --force to override`,
1090
- owner: entry.owner,
1091
- });
1092
- } else {
1093
- if (!dryRun) {
1094
- delete registry.worktrees[entry.name];
1095
- }
1096
- pruned.push({ name: entry.name, status: entry.status, owner: entry.owner || null });
1097
- }
1098
- } else if (!shouldPrune && isPrunable) {
1099
- skipped.push({
1100
- name: entry.name,
1101
- reason: entry.status + ' (use --prune to remove)',
1102
- owner: entry.owner || null,
1103
- });
1104
- }
1105
- }
1106
-
1107
- if (!dryRun) {
1108
- saveRegistry(root, registry);
1109
- try {
1110
- execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
1111
- } catch {
1112
- // Non-fatal
1113
- }
1114
- }
1115
-
1116
- return { repaired, pruned, skipped };
1117
- }
1118
-
1119
- /**
1120
- * List all registered worktrees with filesystem validation.
1121
- * Delegates to reconcileRegistry() for state classification.
1122
- * Persists status transitions (fresh → active, active → merged) so
1123
- * future calls can distinguish "never had work" from "work was merged back".
1124
- * @returns {Array} Worktree entries with status
1125
- */
1126
- function listWorktrees() {
1127
- const root = getRepoRoot();
1128
- const registry = loadRegistry(root);
1129
- const { entries } = reconcileRegistry(root);
1130
-
1131
- // Persist status transitions so future reconcile can use stored status as history
1132
- let dirty = false;
1133
- for (const entry of entries) {
1134
- const regEntry = registry.worktrees[entry.name];
1135
- if (regEntry && regEntry.status !== entry.status &&
1136
- entry.status !== 'unregistered') {
1137
- regEntry.status = entry.status;
1138
- dirty = true;
1139
- }
1140
- }
1141
- if (dirty) {
1142
- saveRegistry(root, registry);
1143
- }
1144
-
1145
- return entries;
1146
- }
1147
-
1148
- /**
1149
- * Destroy a worktree
1150
- * @param {string} name - Worktree name
1151
- * @param {Object} options - Destruction options
1152
- * @param {boolean} [options.deleteBranch] - Also delete the branch
1153
- * @param {boolean} [options.force] - Force removal even if dirty
1154
- */
1155
- function destroyWorktree(name, options = {}) {
1156
- const root = getRepoRoot();
1157
- // Ensure CWD is not inside the worktree we're about to destroy.
1158
- // If CWD is the worktree directory, removing it crashes subsequent commands.
1159
- try { process.chdir(root); } catch { /* non-fatal */ }
1160
- const registry = loadRegistry(root);
1161
- const { deleteBranch = false, force = false } = options;
1162
-
1163
- let entry = registry.worktrees[name];
1164
- if (!entry) {
1165
- // Fallback: scan git for unregistered worktree and auto-register
1166
- const unregistered = discoverUnregisteredWorktrees(root, registry);
1167
- const discovered = unregistered.find((u) => u.name === name);
1168
- if (discovered) {
1169
- console.log(chalk.yellow(`Worktree '${name}' not in registry but found in git. Auto-registering.`));
1170
- entry = autoRegisterWorktree(root, registry, discovered);
1171
- } else {
1172
- throw new Error(`Worktree '${name}' not found in registry or git worktree list`);
1173
- }
1174
- }
1175
-
1176
- // Ownership check: refuse to destroy another agent's worktree without --force
1177
- const currentSession = getAgentSessionId(root);
1178
- const isLiveStatus = entry.status === 'active' || entry.status === 'fresh' || entry.status === 'merged';
1179
- if (
1180
- !force &&
1181
- isLiveStatus &&
1182
- entry.owner &&
1183
- currentSession &&
1184
- entry.owner !== currentSession
1185
- ) {
1186
- const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
1187
- const recency = lastCommit ? ` (last commit: ${lastCommit.age})` : '';
1188
- throw new Error(
1189
- `Worktree '${name}' belongs to another session${recency}.\n` +
1190
- ` Owner: ${entry.owner}\n` +
1191
- ` You: ${currentSession}\n` +
1192
- `Another agent may be actively working here.\n` +
1193
- `Do NOT destroy worktrees you did not create. Ask the user if cleanup is needed.`
1194
- );
1195
- }
1196
-
1197
- // Even with --force, warn loudly when destroying another session's worktree
1198
- if (
1199
- force &&
1200
- isLiveStatus &&
1201
- entry.owner &&
1202
- currentSession &&
1203
- entry.owner !== currentSession
1204
- ) {
1205
- const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
1206
- const recency = lastCommit ? ` (last commit: ${lastCommit.age})` : '';
1207
- console.log(chalk.red(`\n ⚠ WARNING: Force-destroying worktree '${name}' owned by another session${recency}`));
1208
- console.log(chalk.red(` Owner: ${entry.owner}`));
1209
- console.log(chalk.red(` You: ${currentSession}`));
1210
- console.log(chalk.red(` If the other agent is still running, this WILL break their work.\n`));
1211
- }
1212
-
1213
- // Auto-force when the branch is already merged to its base branch.
1214
- // Dirty files in a merged worktree are definitionally stale.
1215
- const merged = entry.branch && entry.baseBranch
1216
- ? isBranchMerged(entry.branch, entry.baseBranch, root)
1217
- : false;
1218
- const effectiveForce = force || merged;
1219
- if (merged && !force) {
1220
- console.log(chalk.gray(` Branch ${entry.branch} already merged to ${entry.baseBranch}, auto-forcing cleanup`));
1221
- }
1222
-
1223
- // Remove git worktree — handle already-deleted directories gracefully
1224
- const dirExists = fs.existsSync(entry.path);
1225
- if (dirExists) {
1226
- try {
1227
- const args = ['worktree', 'remove'];
1228
- if (effectiveForce) args.push('--force');
1229
- args.push(entry.path);
1230
- execFileSync('git', args, { cwd: root, stdio: 'pipe' });
1231
- } catch (error) {
1232
- if (effectiveForce) {
1233
- // Force cleanup: remove directory manually
1234
- fs.removeSync(entry.path);
1235
- } else {
1236
- throw new Error(`Failed to remove worktree: ${error.message}. Use --force to override.`);
1237
- }
1238
- }
1239
- } else {
1240
- // Directory already gone — just clean up git's tracking
1241
- console.log(` Worktree directory already removed, cleaning up registry`);
1242
- }
1243
-
1244
- // Always prune git's worktree list to stay in sync
1245
- try {
1246
- execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
1247
- } catch {
1248
- // Non-fatal
1249
- }
1250
-
1251
- // Optionally delete branch
1252
- if (deleteBranch && entry.branch) {
1253
- try {
1254
- execFileSync('git', ['branch', '-d', entry.branch], { cwd: root, stdio: 'pipe' });
1255
- } catch {
1256
- if (effectiveForce) {
1257
- try {
1258
- execFileSync('git', ['branch', '-D', entry.branch], { cwd: root, stdio: 'pipe' });
1259
- } catch {
1260
- // Non-fatal
1261
- }
1262
- }
1263
- }
1264
- }
1265
-
1266
- // Update registry
1267
- const wasAlreadyDestroyed = registry.worktrees[name].status === 'destroyed';
1268
- registry.worktrees[name].status = 'destroyed';
1269
- registry.worktrees[name].destroyedAt = new Date().toISOString();
1270
- saveRegistry(root, registry);
1271
-
1272
- // CAWSFIX-18: auto-commit the registry so the working tree stays clean
1273
- if (!wasAlreadyDestroyed) {
1274
- try {
1275
- const status = execFileSync('git', ['status', '--porcelain', '.caws/worktrees.json'], {
1276
- cwd: root, stdio: ['pipe', 'pipe', 'pipe'],
1277
- }).toString().trim();
1278
- if (status) {
1279
- const otherActive = Object.values(registry.worktrees || {}).some(
1280
- (e) => e.status === 'active' || e.status === 'fresh'
1281
- );
1282
- const prefix = otherActive ? 'wip(checkpoint)' : 'chore(worktree)';
1283
- execFileSync('git', ['add', '.caws/worktrees.json'], { cwd: root, stdio: 'pipe' });
1284
- execFileSync('git', ['commit', '-m', `${prefix}: record destroyed ${name}`], {
1285
- cwd: root, stdio: 'pipe',
1286
- });
1287
- }
1288
- } catch (err) {
1289
- console.warn(chalk.yellow(` Warning: could not auto-commit .caws/worktrees.json: ${err.message}`));
1290
- }
1291
- }
1292
- }
1293
-
1294
- /**
1295
- * Merge a worktree branch back to base in one operation.
1296
- * Sequence: dry-run conflict check → destroy worktree → merge → cleanup.
1297
- * @param {string} name - Worktree name
1298
- * @param {Object} options - Merge options
1299
- * @param {boolean} [options.dryRun] - Preview conflicts without merging
1300
- * @param {boolean} [options.deleteBranch] - Delete branch after merge
1301
- * @param {string} [options.message] - Custom merge commit message
1302
- * @returns {Object} Merge result
1303
- */
1304
- function mergeWorktree(name, options = {}) {
1305
- const root = getRepoRoot();
1306
- const registry = loadRegistry(root);
1307
- const { dryRun = false, deleteBranch = true, message, takeover = false } = options;
1308
-
1309
- let entry = registry.worktrees[name];
1310
- if (!entry) {
1311
- // Fallback: scan git for unregistered worktree and auto-register
1312
- const unregistered = discoverUnregisteredWorktrees(root, registry);
1313
- const discovered = unregistered.find((u) => u.name === name);
1314
- if (discovered) {
1315
- console.log(chalk.yellow(`Worktree '${name}' not in registry but found in git. Auto-registering.`));
1316
- entry = autoRegisterWorktree(root, registry, discovered);
1317
- } else {
1318
- throw new Error(`Worktree '${name}' not found in registry or git worktree list`);
1319
- }
1320
- }
1321
-
1322
- // CAWSFIX-32: assert ownership BEFORE any merge/git work. Foreign
1323
- // claim soft-blocks unless --takeover is supplied, matching the
1324
- // bind/claim semantics. Throws on refusal so the CLI command handler
1325
- // surfaces the structured warning.
1326
- const ownership = assertWorktreeOwnership(root, name, {
1327
- allowTakeover: takeover,
1328
- takeoverCommandHint: `caws worktree merge ${name} --takeover`,
1329
- });
1330
- if (!ownership.allowed) {
1331
- const err = new Error(ownership.warning);
1332
- err.claimWarning = true;
1333
- throw err;
1334
- }
1335
-
1336
- // CAWSFIX-32: heartbeat the current session into agents.json now that
1337
- // ownership is confirmed. Same-session merges need this since the
1338
- // takeover branch only fires inside assertWorktreeOwnership when a
1339
- // takeover actually occurs.
1340
- refreshAgentClaim(root, { worktree: name, specId: entry.specId || null });
1341
-
1342
- const baseBranch = entry.baseBranch || 'main';
1343
-
1344
- // Check for uncommitted work in the worktree.
1345
- // Ignore .caws/ changes (provenance chain, registry) — these are
1346
- // infrastructure artifacts written by git hooks, not user work.
1347
- // The post-commit hook appends to .caws/provenance/chain.json after
1348
- // every commit, which immediately dirties the tree and blocks merges.
1349
- if (fs.existsSync(entry.path)) {
1350
- try {
1351
- const rawStatus = execFileSync(
1352
- 'git',
1353
- ['status', '--porcelain'],
1354
- { cwd: entry.path, encoding: 'utf8', stdio: 'pipe' }
1355
- );
1356
- // Filter out .caws/ infrastructure changes (provenance, registry).
1357
- // Git porcelain format: "XY PATH" — 2 status chars, space, path.
1358
- // IMPORTANT: do NOT .trim() the raw output — it strips the leading
1359
- // space from " M file" (unstaged), corrupting the XY prefix and
1360
- // breaking substring(3) path extraction.
1361
- const statusLines = rawStatus.split('\n').filter(l => l.length > 0);
1362
- const userChanges = statusLines
1363
- .filter(line => {
1364
- const filePath = line.substring(3);
1365
- return !filePath.startsWith('.caws/');
1366
- }).join('\n');
1367
- if (userChanges) {
1368
- throw new Error(
1369
- `Worktree '${name}' has uncommitted changes:\n${userChanges}\n` +
1370
- `Commit or discard changes before merging.`
1371
- );
1372
- }
1373
- } catch (error) {
1374
- if (error.message.includes('uncommitted changes')) throw error;
1375
- // Non-fatal: status check failed, proceed cautiously
1376
- }
1377
- }
1378
-
1379
- // Dry-run: check for conflicts using git merge-tree (new-style, git 2.38+)
1380
- let conflicts = [];
1381
- try {
1382
- // New-style merge-tree: takes two branches, computes merge-base automatically
1383
- execFileSync(
1384
- 'git',
1385
- ['merge-tree', '--write-tree', baseBranch, entry.branch],
1386
- { cwd: root, encoding: 'utf8', stdio: 'pipe' }
1387
- );
1388
- // Exit 0 = clean merge, no conflicts
1389
- } catch (mergeTreeError) {
1390
- // Exit 1 = conflicts detected; parse them from output
1391
- const output = (mergeTreeError.stdout || '') + (mergeTreeError.stderr || '');
1392
- const conflictLines = output.split('\n').filter(
1393
- (l) => l.includes('CONFLICT') || l.includes('conflict')
1394
- );
1395
- if (mergeTreeError.status === 1 && conflictLines.length > 0) {
1396
- conflicts = conflictLines;
1397
- } else if (mergeTreeError.status === 1) {
1398
- conflicts = ['Merge conflicts detected (run merge manually to inspect)'];
1399
- }
1400
- // Other exit codes (e.g., merge-tree not supported) = can't detect, proceed
1401
- }
1402
-
1403
- if (dryRun) {
1404
- return {
1405
- name,
1406
- branch: entry.branch,
1407
- baseBranch,
1408
- conflicts,
1409
- wouldMerge: conflicts.length === 0,
1410
- };
1411
- }
1412
-
1413
- // Emit merge:pre event
1414
- try {
1415
- lifecycle.emit(EVENTS.MERGE_PRE, {
1416
- worktreeName: name, branch: entry.branch, baseBranch, conflicts,
1417
- timestamp: new Date().toISOString(),
1418
- });
1419
- } catch { /* non-fatal */ }
1420
-
1421
- // Ensure CWD is the repo root BEFORE destroying the worktree.
1422
- // If the caller's CWD is inside the worktree directory, destroying it
1423
- // removes the CWD out from under the process, causing all subsequent
1424
- // git commands to fail with "Unable to read current working directory".
1425
- try { process.chdir(root); } catch { /* non-fatal */ }
1426
-
1427
- // Destroy the worktree (auto-forces since we're about to merge)
1428
- destroyWorktree(name, { deleteBranch: false, force: true });
1429
-
1430
- // Switch to base branch (use cwd: root since getCurrentBranch has no cwd param)
1431
- const currentBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
1432
- cwd: root, encoding: 'utf8', stdio: 'pipe',
1433
- }).trim();
1434
- if (currentBranch !== baseBranch) {
1435
- execFileSync('git', ['checkout', baseBranch], { cwd: root, stdio: 'pipe' });
1436
- }
1437
-
1438
- // Merge
1439
- // Use --no-verify to skip pre-commit/commit-msg hooks during merge.
1440
- // The worktree commits were already validated by those hooks when originally
1441
- // committed. Re-running them here adds seconds of blocking time (especially
1442
- // in projects with heavy hooks like quality gates, YAML validation, etc.)
1443
- // and can trigger OAuth token expiry races in long-running sessions.
1444
- const mergeMessage = message || `merge(worktree): ${name}`;
1445
- try {
1446
- execFileSync(
1447
- 'git',
1448
- ['merge', '--no-ff', '--no-verify', entry.branch, '-m', mergeMessage],
1449
- { cwd: root, stdio: 'pipe' }
1450
- );
1451
- } catch (error) {
1452
- const failResult = {
1453
- name, branch: entry.branch, baseBranch, merged: false,
1454
- conflicts: [`Merge failed: ${error.message}`],
1455
- message: 'Merge conflicts detected. Resolve with git and commit.',
1456
- };
1457
- try {
1458
- lifecycle.emit(EVENTS.MERGE_POST, { ...failResult, timestamp: new Date().toISOString() });
1459
- } catch { /* non-fatal */ }
1460
- return failResult;
1461
- }
1462
-
1463
- // Delete branch after successful merge
1464
- if (deleteBranch) {
1465
- try {
1466
- execFileSync('git', ['branch', '-d', entry.branch], { cwd: root, stdio: 'pipe' });
1467
- } catch {
1468
- // Non-fatal
1469
- }
1470
- }
1471
-
1472
- // Auto-close the bound spec if one exists. A worktree merge is the
1473
- // lifecycle signal that the spec's work is done; leaving the spec
1474
- // `active` (or `draft`, pre-CAWSFIX-23) after merge accumulates stale
1475
- // entries (D6). Direct YAML status flip bypasses the ownership +
1476
- // worktree-reference checks in `closeSpec` — the caller has already
1477
- // proven authority by merging.
1478
- let autoClose = {
1479
- specId: null, acsPassing: null, acsFailureCount: 0, acsTotal: 0, acsFailureIds: [],
1480
- didWrite: false, specPath: null,
1481
- };
1482
- if (entry.specId) {
1483
- autoClose = autoCloseBoundSpec(root, entry.specId);
1484
- if (autoClose.acsPassing === false && autoClose.acsFailureCount > 0) {
1485
- console.warn(chalk.yellow(
1486
- ` ⚠ Spec ${entry.specId} closed with ${autoClose.acsFailureCount}/${autoClose.acsTotal} failing AC(s): ${autoClose.acsFailureIds.join(', ')}`
1487
- ));
1488
- console.warn(chalk.yellow(
1489
- ` Merge succeeded — the spec reflects that — but follow up to address the failing ACs.`
1490
- ));
1491
- }
1492
-
1493
- // CAWSFIX-24 / D6: if the auto-close flipped the status, commit the
1494
- // change on the base branch before returning. Leaving it uncommitted
1495
- // was the "dirty main" footgun: the next worktree merge would abort
1496
- // on "local changes would be overwritten," after the prior worktree
1497
- // was already destroyed. Use --no-verify to match the merge commit's
1498
- // hook-skip discipline (the content was verified when the merge ran).
1499
- if (autoClose.didWrite && autoClose.specPath) {
1500
- try {
1501
- const relPath = path.relative(root, autoClose.specPath);
1502
- execFileSync('git', ['add', '--', relPath], { cwd: root, stdio: 'pipe' });
1503
- execFileSync(
1504
- 'git',
1505
- ['commit', '--no-verify', '-m', `chore(caws): close ${autoClose.specId} spec post-merge`, '--', relPath],
1506
- { cwd: root, stdio: 'pipe' }
1507
- );
1508
- } catch (commitErr) {
1509
- // Non-fatal: a failed auto-commit leaves the spec dirty but the
1510
- // merge itself already succeeded. Warn so the caller can clean up.
1511
- console.warn(chalk.yellow(
1512
- ` ⚠ Auto-commit of ${autoClose.specId} close flip failed: ${commitErr.message}. Commit manually.`
1513
- ));
1514
- }
1515
- }
1516
- }
1517
-
1518
- const mergeResult = {
1519
- name, branch: entry.branch, baseBranch, merged: true, conflicts: [],
1520
- specId: entry.specId || null,
1521
- autoClosedSpecId: autoClose.specId,
1522
- acsPassing: autoClose.acsPassing,
1523
- acsFailureCount: autoClose.acsFailureCount,
1524
- acsTotal: autoClose.acsTotal,
1525
- acsFailureIds: autoClose.acsFailureIds,
1526
- };
1527
- try {
1528
- lifecycle.emit(EVENTS.MERGE_POST, { ...mergeResult, timestamp: new Date().toISOString() });
1529
- } catch { /* non-fatal */ }
1530
- return mergeResult;
1531
- }
1532
-
1533
- /**
1534
- * Flip a spec's status from `draft` to `active` by rewriting just the
1535
- * `status:` line. Called on worktree-bind so specs whose work is
1536
- * starting transition out of draft without manual intervention.
1537
- * Idempotent: no-op when the spec is already active/closed/etc.
1538
- * @param {string} root - Repo root
1539
- * @param {string} specId - Spec identifier
1540
- * @returns {string|null} specId on flip, null if no change
1541
- */
1542
- function autoActivateBoundSpec(root, specId, specPathOverride = null) {
1543
- try {
1544
- // CAWSFIX-25 / D8: callers that resolved a worktree-local spec path
1545
- // (via findFeatureSpecPathFromCwd) pass it in so the flip lands on
1546
- // the worktree's copy, not main's. Falls through to main resolution
1547
- // for backward compatibility when override is null.
1548
- const specPath = specPathOverride || findFeatureSpecPath(root, specId);
1549
- if (!specPath || !fs.existsSync(specPath)) return null;
1550
- const original = fs.readFileSync(specPath, 'utf8');
1551
- // Idempotent: already active/closed/archived → no write.
1552
- if (/^status:[ \t]*active[ \t]*$/m.test(original)) return specId;
1553
- const patched = original.replace(/^status:[ \t]*draft[ \t]*$/m, 'status: active');
1554
- if (patched === original) return null;
1555
- fs.writeFileSync(specPath, patched, 'utf8');
1556
- return specId;
1557
- } catch {
1558
- return null;
1559
- }
1560
- }
1561
-
1562
- /**
1563
- * Flip a spec's status to `closed` by rewriting just the `status:` line.
1564
- * Accepts both `draft` and `active` as source states — merge is the
1565
- * authoritative "work done" signal regardless of whether the spec ever
1566
- * transitioned through active. Runs verify-acs in collect-only mode
1567
- * before the flip and returns AC health so the caller can warn.
1568
- * @param {string} root - Repo root
1569
- * @param {string} specId - Spec identifier (e.g. CAWSFIX-14)
1570
- * @returns {{specId: string|null, acsPassing: boolean|null, acsFailureCount: number, acsTotal: number, acsFailureIds: string[]}}
1571
- */
1572
- function autoCloseBoundSpec(root, specId) {
1573
- const result = {
1574
- specId: null,
1575
- acsPassing: null,
1576
- acsFailureCount: 0,
1577
- acsTotal: 0,
1578
- acsFailureIds: [],
1579
- didWrite: false,
1580
- specPath: null,
1581
- };
1582
- try {
1583
- const specPath = findFeatureSpecPath(root, specId);
1584
- if (!specPath || !fs.existsSync(specPath)) return result;
1585
- result.specPath = specPath;
1586
- const original = fs.readFileSync(specPath, 'utf8');
1587
- // Idempotent: already closed → no-op, no write, no diff.
1588
- if (/^status:[ \t]*closed[ \t]*$/m.test(original)) {
1589
- result.specId = specId;
1590
- return result;
1591
- }
1592
- // Run verify-acs in collect-only mode before flipping. Never throws —
1593
- // any error (missing tests, unavailable runner, malformed spec) leaves
1594
- // acsPassing: null so the caller knows verification didn't run.
1595
- try {
1596
- const yaml = require('js-yaml');
1597
- const { verifySpec } = require('../commands/verify-acs');
1598
- const parsed = yaml.load(original);
1599
- if (parsed && typeof parsed === 'object') {
1600
- const verdict = verifySpec(parsed, root, { run: false });
1601
- const fails = (verdict.results || []).filter((r) => r.status === 'FAIL');
1602
- result.acsTotal = (verdict.results || []).length;
1603
- result.acsFailureCount = fails.length;
1604
- result.acsFailureIds = fails.map((r) => r.id);
1605
- result.acsPassing = fails.length === 0;
1606
- }
1607
- } catch {
1608
- // verify-acs unavailable — don't block close
1609
- }
1610
- // Flip status. Accept both draft and active as source so specs that
1611
- // never transitioned through active (D6 pre-CAWSFIX-23 drift) still close.
1612
- const patched = original.replace(/^status:[ \t]*(?:draft|active)[ \t]*$/m, 'status: closed');
1613
- if (patched === original) {
1614
- return result; // status was archived/unknown — leave alone
1615
- }
1616
- fs.writeFileSync(specPath, patched, 'utf8');
1617
- result.specId = specId;
1618
- result.didWrite = true;
1619
- return result;
1620
- } catch {
1621
- return result;
1622
- }
1623
- }
1624
-
1625
- /**
1626
- * Prune stale worktree entries
1627
- * @param {Object} options - Prune options
1628
- * @param {number} [options.maxAgeDays] - Remove entries older than this many days
1629
- * @param {number} [options.recentCommitMinutes] - Protect branches with commits newer than this (default: 60)
1630
- * @param {boolean} [options.force] - Allow pruning entries owned by other sessions
1631
- * @returns {{ pruned: Array, skipped: Array }} Pruned and skipped entries
1632
- */
1633
- function pruneWorktrees(options = {}) {
1634
- const root = getRepoRoot();
1635
- const registry = loadRegistry(root);
1636
- const { maxAgeDays = 30, recentCommitMinutes = 60, force = false } = options;
1637
- const currentSession = getAgentSessionId(root);
1638
-
1639
- const now = new Date();
1640
- const pruned = [];
1641
- const skipped = [];
1642
-
1643
- for (const [name, entry] of Object.entries(registry.worktrees)) {
1644
- const created = new Date(entry.createdAt);
1645
- const ageDays = (now - created) / (1000 * 60 * 60 * 24);
1646
- const dirExists = fs.existsSync(entry.path);
1647
-
1648
- const shouldPrune =
1649
- // Always prune destroyed entries
1650
- entry.status === 'destroyed' ||
1651
- // Prune active/fresh entries whose directory is gone (filesystem-registry desync)
1652
- ((entry.status === 'active' || entry.status === 'fresh') && !dirExists) ||
1653
- // Prune old missing entries
1654
- (!dirExists && ageDays > maxAgeDays);
1655
-
1656
- if (shouldPrune) {
1657
- // Ownership check: skip entries owned by other sessions unless --force
1658
- const isOwnedByOther = entry.owner && currentSession && entry.owner !== currentSession;
1659
- if (isOwnedByOther && entry.status !== 'destroyed' && !force) {
1660
- skipped.push({
1661
- name,
1662
- reason: `owned by another session (${entry.owner})`,
1663
- entry,
1664
- });
1665
- continue;
1666
- }
1667
-
1668
- // Before pruning a non-destroyed entry, check for recent commits (skip if --force)
1669
- if (!force && entry.status !== 'destroyed' && entry.branch) {
1670
- const lastCommit = getLastCommitInfo(entry.branch, root);
1671
- if (lastCommit) {
1672
- const commitAgeMinutes = (now - lastCommit.timestamp) / (1000 * 60);
1673
- if (commitAgeMinutes < recentCommitMinutes) {
1674
- skipped.push({ name, reason: `recent commit (${lastCommit.age})`, entry });
1675
- continue;
1676
- }
1677
- }
1678
- }
1679
-
1680
- // Clean up filesystem if still exists
1681
- if (dirExists) {
1682
- try {
1683
- execFileSync('git', ['worktree', 'remove', '--force', entry.path], {
1684
- cwd: root,
1685
- stdio: 'pipe',
1686
- });
1687
- } catch {
1688
- fs.removeSync(entry.path);
1689
- }
1690
- }
1691
- pruned.push(entry);
1692
- delete registry.worktrees[name];
1693
- }
1694
- }
1695
-
1696
- // Prune git's worktree list
1697
- try {
1698
- execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
1699
- } catch {
1700
- // Non-fatal
1701
- }
1702
-
1703
- saveRegistry(root, registry);
1704
- return { pruned, skipped };
1705
- }
1706
-
1707
- module.exports = {
1708
- createWorktree,
1709
- listWorktrees,
1710
- destroyWorktree,
1711
- mergeWorktree,
1712
- autoActivateBoundSpec,
1713
- autoCloseBoundSpec,
1714
- pruneWorktrees,
1715
- repairWorktrees,
1716
- reconcileRegistry,
1717
- loadRegistry,
1718
- saveRegistry,
1719
- assertWorktreeOwnership,
1720
- getRepoRoot,
1721
- getLastCommitInfo,
1722
- isBranchMerged,
1723
- hasDivergentCommits,
1724
- hasDirtyFiles,
1725
- discoverUnregisteredWorktrees,
1726
- autoRegisterWorktree,
1727
- WORKTREES_DIR,
1728
- REGISTRY_FILE,
1729
- BRANCH_PREFIX,
1730
- findFeatureSpecPath,
1731
- findFeatureSpecPathFromCwd,
1732
- materializeWorktreeSpec,
1733
- inferSpecIdForWorktree,
1734
- findSpecByWorktreeName,
1735
- };