@paths.design/caws-cli 10.2.0 → 11.1.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 (493) hide show
  1. package/README.md +125 -374
  2. package/dist/index.js +45 -787
  3. package/dist/init/harness-detect.d.ts +18 -0
  4. package/dist/init/harness-detect.d.ts.map +1 -0
  5. package/dist/init/harness-detect.js +90 -0
  6. package/dist/init/harness-detect.js.map +1 -0
  7. package/dist/init/hook-install.d.ts +53 -0
  8. package/dist/init/hook-install.d.ts.map +1 -0
  9. package/dist/init/hook-install.js +421 -0
  10. package/dist/init/hook-install.js.map +1 -0
  11. package/dist/init/hook-packs/manifest-claude-code.d.ts +4 -0
  12. package/dist/init/hook-packs/manifest-claude-code.d.ts.map +1 -0
  13. package/dist/init/hook-packs/manifest-claude-code.js +190 -0
  14. package/dist/init/hook-packs/manifest-claude-code.js.map +1 -0
  15. package/dist/init/hook-packs/register.d.ts +19 -0
  16. package/dist/init/hook-packs/register.d.ts.map +1 -0
  17. package/dist/init/hook-packs/register.js +37 -0
  18. package/dist/init/hook-packs/register.js.map +1 -0
  19. package/dist/init/hook-packs/types.d.ts +123 -0
  20. package/dist/init/hook-packs/types.d.ts.map +1 -0
  21. package/dist/init/hook-packs/types.js +29 -0
  22. package/dist/init/hook-packs/types.js.map +1 -0
  23. package/dist/shell/binding/resolve-binding.d.ts +4 -0
  24. package/dist/shell/binding/resolve-binding.d.ts.map +1 -0
  25. package/dist/shell/binding/resolve-binding.js +228 -0
  26. package/dist/shell/binding/resolve-binding.js.map +1 -0
  27. package/dist/shell/binding/types.d.ts +42 -0
  28. package/dist/shell/binding/types.d.ts.map +1 -0
  29. package/dist/shell/binding/types.js +21 -0
  30. package/dist/shell/binding/types.js.map +1 -0
  31. package/dist/shell/commands/claim.d.ts +14 -0
  32. package/dist/shell/commands/claim.d.ts.map +1 -0
  33. package/dist/shell/commands/claim.js +197 -0
  34. package/dist/shell/commands/claim.js.map +1 -0
  35. package/dist/shell/commands/doctor.d.ts +13 -0
  36. package/dist/shell/commands/doctor.d.ts.map +1 -0
  37. package/dist/shell/commands/doctor.js +97 -0
  38. package/dist/shell/commands/doctor.js.map +1 -0
  39. package/dist/shell/commands/evidence.d.ts +28 -0
  40. package/dist/shell/commands/evidence.d.ts.map +1 -0
  41. package/dist/shell/commands/evidence.js +166 -0
  42. package/dist/shell/commands/evidence.js.map +1 -0
  43. package/dist/shell/commands/gates.d.ts +19 -0
  44. package/dist/shell/commands/gates.d.ts.map +1 -0
  45. package/dist/shell/commands/gates.js +208 -0
  46. package/dist/shell/commands/gates.js.map +1 -0
  47. package/dist/shell/commands/init.d.ts +17 -0
  48. package/dist/shell/commands/init.d.ts.map +1 -0
  49. package/dist/shell/commands/init.js +168 -0
  50. package/dist/shell/commands/init.js.map +1 -0
  51. package/dist/shell/commands/scope.d.ts +11 -0
  52. package/dist/shell/commands/scope.d.ts.map +1 -0
  53. package/dist/shell/commands/scope.js +92 -0
  54. package/dist/shell/commands/scope.js.map +1 -0
  55. package/dist/shell/commands/specs.d.ts +41 -0
  56. package/dist/shell/commands/specs.d.ts.map +1 -0
  57. package/dist/shell/commands/specs.js +264 -0
  58. package/dist/shell/commands/specs.js.map +1 -0
  59. package/dist/shell/commands/status.d.ts +15 -0
  60. package/dist/shell/commands/status.d.ts.map +1 -0
  61. package/dist/shell/commands/status.js +106 -0
  62. package/dist/shell/commands/status.js.map +1 -0
  63. package/dist/shell/commands/waiver.d.ts +38 -0
  64. package/dist/shell/commands/waiver.d.ts.map +1 -0
  65. package/dist/shell/commands/waiver.js +240 -0
  66. package/dist/shell/commands/waiver.js.map +1 -0
  67. package/dist/shell/commands/worktree.d.ts +38 -0
  68. package/dist/shell/commands/worktree.d.ts.map +1 -0
  69. package/dist/shell/commands/worktree.js +286 -0
  70. package/dist/shell/commands/worktree.js.map +1 -0
  71. package/dist/shell/gates/disposition.d.ts +23 -0
  72. package/dist/shell/gates/disposition.d.ts.map +1 -0
  73. package/dist/shell/gates/disposition.js +117 -0
  74. package/dist/shell/gates/disposition.js.map +1 -0
  75. package/dist/shell/gates/gate-result-contract.d.ts +39 -0
  76. package/dist/shell/gates/gate-result-contract.d.ts.map +1 -0
  77. package/dist/shell/gates/gate-result-contract.js +150 -0
  78. package/dist/shell/gates/gate-result-contract.js.map +1 -0
  79. package/dist/shell/gates/local-evaluators/budget-limit.d.ts +24 -0
  80. package/dist/shell/gates/local-evaluators/budget-limit.d.ts.map +1 -0
  81. package/dist/shell/gates/local-evaluators/budget-limit.js +67 -0
  82. package/dist/shell/gates/local-evaluators/budget-limit.js.map +1 -0
  83. package/dist/shell/gates/local-evaluators/diff-helpers.d.ts +25 -0
  84. package/dist/shell/gates/local-evaluators/diff-helpers.d.ts.map +1 -0
  85. package/dist/shell/gates/local-evaluators/diff-helpers.js +74 -0
  86. package/dist/shell/gates/local-evaluators/diff-helpers.js.map +1 -0
  87. package/dist/shell/gates/local-evaluators/index.d.ts +28 -0
  88. package/dist/shell/gates/local-evaluators/index.d.ts.map +1 -0
  89. package/dist/shell/gates/local-evaluators/index.js +67 -0
  90. package/dist/shell/gates/local-evaluators/index.js.map +1 -0
  91. package/dist/shell/gates/local-evaluators/scope-boundary.d.ts +23 -0
  92. package/dist/shell/gates/local-evaluators/scope-boundary.d.ts.map +1 -0
  93. package/dist/shell/gates/local-evaluators/scope-boundary.js +67 -0
  94. package/dist/shell/gates/local-evaluators/scope-boundary.js.map +1 -0
  95. package/dist/shell/gates/local-evaluators/spec-completeness.d.ts +12 -0
  96. package/dist/shell/gates/local-evaluators/spec-completeness.d.ts.map +1 -0
  97. package/dist/shell/gates/local-evaluators/spec-completeness.js +73 -0
  98. package/dist/shell/gates/local-evaluators/spec-completeness.js.map +1 -0
  99. package/dist/shell/gates/quality-gates-adapter.d.ts +55 -0
  100. package/dist/shell/gates/quality-gates-adapter.d.ts.map +1 -0
  101. package/dist/shell/gates/quality-gates-adapter.js +161 -0
  102. package/dist/shell/gates/quality-gates-adapter.js.map +1 -0
  103. package/dist/shell/gates/waiver-filter.d.ts +58 -0
  104. package/dist/shell/gates/waiver-filter.d.ts.map +1 -0
  105. package/dist/shell/gates/waiver-filter.js +119 -0
  106. package/dist/shell/gates/waiver-filter.js.map +1 -0
  107. package/dist/shell/index.d.ts +54 -0
  108. package/dist/shell/index.d.ts.map +1 -0
  109. package/dist/shell/index.js +85 -0
  110. package/dist/shell/index.js.map +1 -0
  111. package/dist/shell/register.d.ts +11 -0
  112. package/dist/shell/register.d.ts.map +1 -0
  113. package/dist/shell/register.js +464 -0
  114. package/dist/shell/register.js.map +1 -0
  115. package/dist/shell/render/claim.d.ts +22 -0
  116. package/dist/shell/render/claim.d.ts.map +1 -0
  117. package/dist/shell/render/claim.js +75 -0
  118. package/dist/shell/render/claim.js.map +1 -0
  119. package/dist/shell/render/decision.d.ts +15 -0
  120. package/dist/shell/render/decision.d.ts.map +1 -0
  121. package/dist/shell/render/decision.js +66 -0
  122. package/dist/shell/render/decision.js.map +1 -0
  123. package/dist/shell/render/diagnostic.d.ts +19 -0
  124. package/dist/shell/render/diagnostic.d.ts.map +1 -0
  125. package/dist/shell/render/diagnostic.js +76 -0
  126. package/dist/shell/render/diagnostic.js.map +1 -0
  127. package/dist/shell/render/finding.d.ts +15 -0
  128. package/dist/shell/render/finding.d.ts.map +1 -0
  129. package/dist/shell/render/finding.js +57 -0
  130. package/dist/shell/render/finding.js.map +1 -0
  131. package/dist/shell/render/gates.d.ts +3 -0
  132. package/dist/shell/render/gates.d.ts.map +1 -0
  133. package/dist/shell/render/gates.js +56 -0
  134. package/dist/shell/render/gates.js.map +1 -0
  135. package/dist/shell/render/init-hook-pack.d.ts +16 -0
  136. package/dist/shell/render/init-hook-pack.d.ts.map +1 -0
  137. package/dist/shell/render/init-hook-pack.js +206 -0
  138. package/dist/shell/render/init-hook-pack.js.map +1 -0
  139. package/dist/shell/render/init.d.ts +11 -0
  140. package/dist/shell/render/init.d.ts.map +1 -0
  141. package/dist/shell/render/init.js +32 -0
  142. package/dist/shell/render/init.js.map +1 -0
  143. package/dist/shell/render/status.d.ts +26 -0
  144. package/dist/shell/render/status.d.ts.map +1 -0
  145. package/dist/shell/render/status.js +143 -0
  146. package/dist/shell/render/status.js.map +1 -0
  147. package/dist/shell/render/waiver.d.ts +21 -0
  148. package/dist/shell/render/waiver.d.ts.map +1 -0
  149. package/dist/shell/render/waiver.js +94 -0
  150. package/dist/shell/render/waiver.js.map +1 -0
  151. package/dist/shell/rules.d.ts +37 -0
  152. package/dist/shell/rules.d.ts.map +1 -0
  153. package/dist/shell/rules.js +51 -0
  154. package/dist/shell/rules.js.map +1 -0
  155. package/dist/shell/session/actor.d.ts +14 -0
  156. package/dist/shell/session/actor.d.ts.map +1 -0
  157. package/dist/shell/session/actor.js +34 -0
  158. package/dist/shell/session/actor.js.map +1 -0
  159. package/dist/shell/session/resolve-session.d.ts +5 -0
  160. package/dist/shell/session/resolve-session.d.ts.map +1 -0
  161. package/dist/shell/session/resolve-session.js +239 -0
  162. package/dist/shell/session/resolve-session.js.map +1 -0
  163. package/dist/shell/session/types.d.ts +56 -0
  164. package/dist/shell/session/types.d.ts.map +1 -0
  165. package/dist/shell/session/types.js +15 -0
  166. package/dist/shell/session/types.js.map +1 -0
  167. package/dist/store/agents-store.d.ts +3 -0
  168. package/dist/store/agents-store.d.ts.map +1 -0
  169. package/dist/store/agents-store.js +63 -0
  170. package/dist/store/agents-store.js.map +1 -0
  171. package/dist/store/apply-patch.d.ts +16 -0
  172. package/dist/store/apply-patch.d.ts.map +1 -0
  173. package/dist/store/apply-patch.js +191 -0
  174. package/dist/store/apply-patch.js.map +1 -0
  175. package/dist/store/atomic-write.d.ts +34 -0
  176. package/dist/store/atomic-write.d.ts.map +1 -0
  177. package/dist/store/atomic-write.js +174 -0
  178. package/dist/store/atomic-write.js.map +1 -0
  179. package/dist/store/doctor-snapshot.d.ts +20 -0
  180. package/dist/store/doctor-snapshot.d.ts.map +1 -0
  181. package/dist/store/doctor-snapshot.js +176 -0
  182. package/dist/store/doctor-snapshot.js.map +1 -0
  183. package/dist/store/events-store.d.ts +33 -0
  184. package/dist/store/events-store.d.ts.map +1 -0
  185. package/dist/store/events-store.js +297 -0
  186. package/dist/store/events-store.js.map +1 -0
  187. package/dist/store/index.d.ts +21 -0
  188. package/dist/store/index.d.ts.map +1 -0
  189. package/dist/store/index.js +47 -0
  190. package/dist/store/index.js.map +1 -0
  191. package/dist/store/init-store.d.ts +21 -0
  192. package/dist/store/init-store.d.ts.map +1 -0
  193. package/dist/store/init-store.js +295 -0
  194. package/dist/store/init-store.js.map +1 -0
  195. package/dist/store/json-store.d.ts +3 -0
  196. package/dist/store/json-store.d.ts.map +1 -0
  197. package/dist/store/json-store.js +65 -0
  198. package/dist/store/json-store.js.map +1 -0
  199. package/dist/store/lifecycle-lock.d.ts +34 -0
  200. package/dist/store/lifecycle-lock.d.ts.map +1 -0
  201. package/dist/store/lifecycle-lock.js +168 -0
  202. package/dist/store/lifecycle-lock.js.map +1 -0
  203. package/dist/store/lifecycle-transaction.d.ts +79 -0
  204. package/dist/store/lifecycle-transaction.d.ts.map +1 -0
  205. package/dist/store/lifecycle-transaction.js +319 -0
  206. package/dist/store/lifecycle-transaction.js.map +1 -0
  207. package/dist/store/policy-store.d.ts +3 -0
  208. package/dist/store/policy-store.d.ts.map +1 -0
  209. package/dist/store/policy-store.js +65 -0
  210. package/dist/store/policy-store.js.map +1 -0
  211. package/dist/store/repo-root.d.ts +46 -0
  212. package/dist/store/repo-root.d.ts.map +1 -0
  213. package/dist/store/repo-root.js +145 -0
  214. package/dist/store/repo-root.js.map +1 -0
  215. package/dist/store/rules.d.ts +69 -0
  216. package/dist/store/rules.d.ts.map +1 -0
  217. package/dist/store/rules.js +95 -0
  218. package/dist/store/rules.js.map +1 -0
  219. package/dist/store/specs-store.d.ts +3 -0
  220. package/dist/store/specs-store.d.ts.map +1 -0
  221. package/dist/store/specs-store.js +131 -0
  222. package/dist/store/specs-store.js.map +1 -0
  223. package/dist/store/specs-writer.d.ts +61 -0
  224. package/dist/store/specs-writer.d.ts.map +1 -0
  225. package/dist/store/specs-writer.js +506 -0
  226. package/dist/store/specs-writer.js.map +1 -0
  227. package/dist/store/types.d.ts +84 -0
  228. package/dist/store/types.d.ts.map +1 -0
  229. package/dist/store/types.js +14 -0
  230. package/dist/store/types.js.map +1 -0
  231. package/dist/store/waivers-store.d.ts +25 -0
  232. package/dist/store/waivers-store.d.ts.map +1 -0
  233. package/dist/store/waivers-store.js +232 -0
  234. package/dist/store/waivers-store.js.map +1 -0
  235. package/dist/store/worktrees-store.d.ts +3 -0
  236. package/dist/store/worktrees-store.d.ts.map +1 -0
  237. package/dist/store/worktrees-store.js +62 -0
  238. package/dist/store/worktrees-store.js.map +1 -0
  239. package/dist/store/worktrees-writer.d.ts +77 -0
  240. package/dist/store/worktrees-writer.d.ts.map +1 -0
  241. package/dist/store/worktrees-writer.js +674 -0
  242. package/dist/store/worktrees-writer.js.map +1 -0
  243. package/dist/store/yaml-patch.d.ts +7 -0
  244. package/dist/store/yaml-patch.d.ts.map +1 -0
  245. package/dist/store/yaml-patch.js +250 -0
  246. package/dist/store/yaml-patch.js.map +1 -0
  247. package/dist/store/yaml-store.d.ts +9 -0
  248. package/dist/store/yaml-store.d.ts.map +1 -0
  249. package/dist/store/yaml-store.js +121 -0
  250. package/dist/store/yaml-store.js.map +1 -0
  251. package/package.json +15 -13
  252. package/dist/budget-derivation.js +0 -751
  253. package/dist/cicd-optimizer.js +0 -504
  254. package/dist/commands/agents.js +0 -124
  255. package/dist/commands/archive.js +0 -500
  256. package/dist/commands/burnup.js +0 -198
  257. package/dist/commands/diagnose.js +0 -525
  258. package/dist/commands/evaluate.js +0 -314
  259. package/dist/commands/gates.js +0 -149
  260. package/dist/commands/init.js +0 -857
  261. package/dist/commands/iterate.js +0 -417
  262. package/dist/commands/mode.js +0 -269
  263. package/dist/commands/parallel.js +0 -242
  264. package/dist/commands/plan.js +0 -438
  265. package/dist/commands/provenance.js +0 -1143
  266. package/dist/commands/quality-monitor.js +0 -284
  267. package/dist/commands/scope.js +0 -264
  268. package/dist/commands/session.js +0 -312
  269. package/dist/commands/sidecar.js +0 -74
  270. package/dist/commands/specs.js +0 -1656
  271. package/dist/commands/status.js +0 -1172
  272. package/dist/commands/templates.js +0 -237
  273. package/dist/commands/tool.js +0 -136
  274. package/dist/commands/tutorial.js +0 -480
  275. package/dist/commands/validate.js +0 -357
  276. package/dist/commands/verify-acs.js +0 -443
  277. package/dist/commands/waivers.js +0 -599
  278. package/dist/commands/workflow.js +0 -243
  279. package/dist/commands/worktree.js +0 -502
  280. package/dist/config/lite-scope.js +0 -158
  281. package/dist/config/modes.js +0 -347
  282. package/dist/constants/spec-types.js +0 -65
  283. package/dist/gates/budget-limit.js +0 -121
  284. package/dist/gates/feedback.js +0 -260
  285. package/dist/gates/format.js +0 -179
  286. package/dist/gates/god-object.js +0 -117
  287. package/dist/gates/pipeline.js +0 -167
  288. package/dist/gates/scope-boundary.js +0 -112
  289. package/dist/gates/spec-completeness.js +0 -109
  290. package/dist/gates/todo-detection.js +0 -205
  291. package/dist/generators/jest-config-generator.js +0 -242
  292. package/dist/generators/working-spec.js +0 -237
  293. package/dist/minimal-cli.js +0 -88
  294. package/dist/parallel/parallel-manager.js +0 -433
  295. package/dist/policy/PolicyManager.js +0 -470
  296. package/dist/scaffold/claude-hooks.js +0 -443
  297. package/dist/scaffold/cursor-hooks.js +0 -177
  298. package/dist/scaffold/git-hooks.js +0 -928
  299. package/dist/scaffold/index.js +0 -794
  300. package/dist/session/session-manager.js +0 -653
  301. package/dist/sidecars/index.js +0 -33
  302. package/dist/sidecars/listeners.js +0 -40
  303. package/dist/sidecars/provenance-summary.js +0 -238
  304. package/dist/sidecars/quality-gaps.js +0 -258
  305. package/dist/sidecars/schema.js +0 -149
  306. package/dist/sidecars/spec-drift.js +0 -151
  307. package/dist/sidecars/waiver-draft.js +0 -176
  308. package/dist/spec/SpecFileManager.js +0 -419
  309. package/dist/templates/.caws/schemas/policy.schema.json +0 -117
  310. package/dist/templates/.caws/schemas/scope.schema.json +0 -52
  311. package/dist/templates/.caws/schemas/waivers.schema.json +0 -106
  312. package/dist/templates/.caws/schemas/working-spec.schema.json +0 -340
  313. package/dist/templates/.caws/schemas/worktrees.schema.json +0 -38
  314. package/dist/templates/.caws/templates/working-spec.template.yml +0 -80
  315. package/dist/templates/.caws/tools/README.md +0 -18
  316. package/dist/templates/.caws/tools/scope-guard.js +0 -203
  317. package/dist/templates/.caws/tools-allow.json +0 -331
  318. package/dist/templates/.caws/waivers.yml +0 -19
  319. package/dist/templates/.claude/README.md +0 -190
  320. package/dist/templates/.claude/hooks/audit.sh +0 -121
  321. package/dist/templates/.claude/hooks/block-dangerous.sh +0 -203
  322. package/dist/templates/.claude/hooks/classify_command.py +0 -592
  323. package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
  324. package/dist/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
  325. package/dist/templates/.claude/hooks/naming-check.sh +0 -100
  326. package/dist/templates/.claude/hooks/protected-paths.sh +0 -39
  327. package/dist/templates/.claude/hooks/quality-check.sh +0 -81
  328. package/dist/templates/.claude/hooks/scan-secrets.sh +0 -85
  329. package/dist/templates/.claude/hooks/scope-guard.sh +0 -381
  330. package/dist/templates/.claude/hooks/session-caws-status.sh +0 -117
  331. package/dist/templates/.claude/hooks/session-log.sh +0 -634
  332. package/dist/templates/.claude/hooks/simplification-guard.sh +0 -92
  333. package/dist/templates/.claude/hooks/stop-worktree-check.sh +0 -46
  334. package/dist/templates/.claude/hooks/test_classify_command.py +0 -370
  335. package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
  336. package/dist/templates/.claude/hooks/validate-spec.sh +0 -76
  337. package/dist/templates/.claude/hooks/worktree-guard.sh +0 -220
  338. package/dist/templates/.claude/hooks/worktree-write-guard.sh +0 -190
  339. package/dist/templates/.claude/rules/git-safety.md +0 -26
  340. package/dist/templates/.claude/rules/worktree-isolation.md +0 -101
  341. package/dist/templates/.claude/settings.json +0 -141
  342. package/dist/templates/.cursor/README.md +0 -299
  343. package/dist/templates/.cursor/hooks/audit.sh +0 -55
  344. package/dist/templates/.cursor/hooks/block-dangerous.sh +0 -84
  345. package/dist/templates/.cursor/hooks/caws-quality-check.sh +0 -52
  346. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
  347. package/dist/templates/.cursor/hooks/format.sh +0 -38
  348. package/dist/templates/.cursor/hooks/naming-check.sh +0 -64
  349. package/dist/templates/.cursor/hooks/scan-secrets.sh +0 -51
  350. package/dist/templates/.cursor/hooks/scope-guard.sh +0 -52
  351. package/dist/templates/.cursor/hooks/session-log.sh +0 -924
  352. package/dist/templates/.cursor/hooks/validate-spec.sh +0 -83
  353. package/dist/templates/.cursor/hooks.json +0 -76
  354. package/dist/templates/.cursor/rules/00-claims-verification.mdc +0 -144
  355. package/dist/templates/.cursor/rules/01-working-style.mdc +0 -50
  356. package/dist/templates/.cursor/rules/02-quality-gates.mdc +0 -368
  357. package/dist/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
  358. package/dist/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
  359. package/dist/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
  360. package/dist/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
  361. package/dist/templates/.cursor/rules/07-process-ops.mdc +0 -20
  362. package/dist/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
  363. package/dist/templates/.cursor/rules/09-docstrings.mdc +0 -89
  364. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
  365. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
  366. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
  367. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
  368. package/dist/templates/.cursor/rules/README.md +0 -148
  369. package/dist/templates/.github/copilot-instructions.md +0 -82
  370. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
  371. package/dist/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
  372. package/dist/templates/.junie/guidelines.md +0 -73
  373. package/dist/templates/.vscode/launch.json +0 -17
  374. package/dist/templates/.vscode/settings.json +0 -95
  375. package/dist/templates/.windsurf/rules/caws-quality-standards.md +0 -54
  376. package/dist/templates/.windsurf/workflows/caws-guided-development.md +0 -92
  377. package/dist/templates/CLAUDE.md +0 -196
  378. package/dist/templates/COMMIT_CONVENTIONS.md +0 -86
  379. package/dist/templates/OIDC_SETUP.md +0 -300
  380. package/dist/templates/agents.md +0 -171
  381. package/dist/templates/codemod/README.md +0 -1
  382. package/dist/templates/codemod/test.js +0 -93
  383. package/dist/templates/docs/README.md +0 -151
  384. package/dist/templates/scripts/new_feature.sh +0 -80
  385. package/dist/templates/scripts/quality-gates/check-god-objects.js +0 -146
  386. package/dist/templates/scripts/quality-gates/run-quality-gates.js +0 -50
  387. package/dist/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
  388. package/dist/test-analysis.js +0 -786
  389. package/dist/tool-interface.js +0 -314
  390. package/dist/tool-loader.js +0 -303
  391. package/dist/tool-validator.js +0 -393
  392. package/dist/utils/agent-display.js +0 -210
  393. package/dist/utils/agent-session.js +0 -344
  394. package/dist/utils/async-utils.js +0 -188
  395. package/dist/utils/command-wrapper.js +0 -200
  396. package/dist/utils/event-log.js +0 -584
  397. package/dist/utils/event-renderer.js +0 -521
  398. package/dist/utils/finalization.js +0 -230
  399. package/dist/utils/git-lock.js +0 -119
  400. package/dist/utils/gitignore-updater.js +0 -158
  401. package/dist/utils/ide-detection.js +0 -133
  402. package/dist/utils/lifecycle-events.js +0 -94
  403. package/dist/utils/project-analysis.js +0 -367
  404. package/dist/utils/promise-utils.js +0 -72
  405. package/dist/utils/quality-gates-errors.js +0 -520
  406. package/dist/utils/quality-gates-utils.js +0 -387
  407. package/dist/utils/schema-validator.js +0 -50
  408. package/dist/utils/spec-resolver.js +0 -711
  409. package/dist/utils/typescript-detector.js +0 -369
  410. package/dist/utils/working-state.js +0 -530
  411. package/dist/utils/yaml-validation.js +0 -156
  412. package/dist/validation/spec-validation.js +0 -924
  413. package/dist/waivers-manager.js +0 -732
  414. package/dist/worktree/worktree-manager.js +0 -1735
  415. package/templates/.caws/schemas/policy.schema.json +0 -117
  416. package/templates/.caws/schemas/scope.schema.json +0 -52
  417. package/templates/.caws/schemas/waivers.schema.json +0 -106
  418. package/templates/.caws/schemas/working-spec.schema.json +0 -340
  419. package/templates/.caws/schemas/worktrees.schema.json +0 -38
  420. package/templates/.caws/templates/working-spec.template.yml +0 -80
  421. package/templates/.caws/tools/README.md +0 -18
  422. package/templates/.caws/tools/scope-guard.js +0 -203
  423. package/templates/.caws/tools-allow.json +0 -331
  424. package/templates/.caws/waivers.yml +0 -19
  425. package/templates/.claude/README.md +0 -190
  426. package/templates/.claude/hooks/audit.sh +0 -121
  427. package/templates/.claude/hooks/block-dangerous.sh +0 -203
  428. package/templates/.claude/hooks/classify_command.py +0 -592
  429. package/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
  430. package/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
  431. package/templates/.claude/hooks/naming-check.sh +0 -100
  432. package/templates/.claude/hooks/protected-paths.sh +0 -39
  433. package/templates/.claude/hooks/quality-check.sh +0 -81
  434. package/templates/.claude/hooks/scan-secrets.sh +0 -85
  435. package/templates/.claude/hooks/scope-guard.sh +0 -381
  436. package/templates/.claude/hooks/session-caws-status.sh +0 -117
  437. package/templates/.claude/hooks/session-log.sh +0 -634
  438. package/templates/.claude/hooks/simplification-guard.sh +0 -92
  439. package/templates/.claude/hooks/stop-worktree-check.sh +0 -46
  440. package/templates/.claude/hooks/test_classify_command.py +0 -370
  441. package/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
  442. package/templates/.claude/hooks/validate-spec.sh +0 -76
  443. package/templates/.claude/hooks/worktree-guard.sh +0 -220
  444. package/templates/.claude/hooks/worktree-write-guard.sh +0 -190
  445. package/templates/.claude/rules/git-safety.md +0 -26
  446. package/templates/.claude/rules/worktree-isolation.md +0 -101
  447. package/templates/.claude/settings.json +0 -141
  448. package/templates/.cursor/README.md +0 -299
  449. package/templates/.cursor/hooks/audit.sh +0 -55
  450. package/templates/.cursor/hooks/block-dangerous.sh +0 -84
  451. package/templates/.cursor/hooks/caws-quality-check.sh +0 -52
  452. package/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
  453. package/templates/.cursor/hooks/format.sh +0 -38
  454. package/templates/.cursor/hooks/naming-check.sh +0 -64
  455. package/templates/.cursor/hooks/scan-secrets.sh +0 -51
  456. package/templates/.cursor/hooks/scope-guard.sh +0 -52
  457. package/templates/.cursor/hooks/session-log.sh +0 -924
  458. package/templates/.cursor/hooks/validate-spec.sh +0 -83
  459. package/templates/.cursor/hooks.json +0 -76
  460. package/templates/.cursor/rules/00-claims-verification.mdc +0 -144
  461. package/templates/.cursor/rules/01-working-style.mdc +0 -50
  462. package/templates/.cursor/rules/02-quality-gates.mdc +0 -368
  463. package/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
  464. package/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
  465. package/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
  466. package/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
  467. package/templates/.cursor/rules/07-process-ops.mdc +0 -20
  468. package/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
  469. package/templates/.cursor/rules/09-docstrings.mdc +0 -89
  470. package/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
  471. package/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
  472. package/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
  473. package/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
  474. package/templates/.cursor/rules/README.md +0 -148
  475. package/templates/.github/copilot-instructions.md +0 -82
  476. package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
  477. package/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
  478. package/templates/.junie/guidelines.md +0 -73
  479. package/templates/.vscode/launch.json +0 -17
  480. package/templates/.vscode/settings.json +0 -95
  481. package/templates/.windsurf/rules/caws-quality-standards.md +0 -54
  482. package/templates/.windsurf/workflows/caws-guided-development.md +0 -92
  483. package/templates/CLAUDE.md +0 -196
  484. package/templates/COMMIT_CONVENTIONS.md +0 -86
  485. package/templates/OIDC_SETUP.md +0 -300
  486. package/templates/agents.md +0 -171
  487. package/templates/codemod/README.md +0 -1
  488. package/templates/codemod/test.js +0 -93
  489. package/templates/docs/README.md +0 -151
  490. package/templates/scripts/new_feature.sh +0 -80
  491. package/templates/scripts/quality-gates/check-god-objects.js +0 -146
  492. package/templates/scripts/quality-gates/run-quality-gates.js +0 -50
  493. package/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
@@ -1,1656 +0,0 @@
1
- /**
2
- * @fileoverview CAWS Specs Command
3
- * Manage multiple spec files for better organization and discoverability
4
- * @author @darianrosebrook
5
- */
6
-
7
- const fs = require('fs-extra');
8
- const path = require('path');
9
- const yaml = require('js-yaml');
10
- const chalk = require('chalk');
11
- const { safeAsync, outputResult } = require('../error-handler');
12
- const { question, closeReadline } = require('../utils/promise-utils');
13
- const { SPEC_TYPES } = require('../constants/spec-types');
14
-
15
- // Import suggestFeatureBreakdown from spec-resolver
16
- const { suggestFeatureBreakdown } = require('../utils/spec-resolver');
17
- const { findProjectRoot } = require('../utils/detection');
18
- const { loadRegistry: loadWorktreeRegistry, getRepoRoot } = require('../worktree/worktree-manager');
19
- const { getAgentSessionId, refreshAgentClaim } = require('../utils/agent-session');
20
- const { initializeState, saveState, deleteState } = require('../utils/working-state');
21
- const { appendEvent } = require('../utils/event-log');
22
-
23
- /**
24
- * Check if a spec is referenced by any active worktree.
25
- * Returns the list of worktree names that reference it, or empty array.
26
- * @param {string} specId - Spec identifier to check
27
- * @returns {string[]} Names of worktrees referencing this spec
28
- */
29
- function getWorktreesReferencingSpec(specId) {
30
- try {
31
- const root = getRepoRoot();
32
- const registry = loadWorktreeRegistry(root);
33
- const matches = [];
34
- for (const [name, entry] of Object.entries(registry.worktrees || {})) {
35
- if (
36
- entry.specId === specId &&
37
- entry.status !== 'destroyed' &&
38
- entry.status !== 'merged'
39
- ) {
40
- matches.push(name);
41
- }
42
- }
43
- return matches;
44
- } catch {
45
- // If worktree registry can't be loaded (e.g., no .caws dir), no conflict
46
- return [];
47
- }
48
- }
49
-
50
- /**
51
- * Specs directory structure — anchored to the CAWS project root,
52
- * not process.cwd(), so the CLI works from subdirectories and monorepos.
53
- */
54
- function getSpecsDir() {
55
- return path.join(findProjectRoot(), '.caws', 'specs');
56
- }
57
- function getSpecsRegistry() {
58
- return path.join(findProjectRoot(), '.caws', 'specs', 'registry.json');
59
- }
60
-
61
- function detectCurrentWorktreeName() {
62
- const cwd = process.cwd().replace(/\\/g, '/');
63
- const worktreeMatch = cwd.match(/\/\.caws\/worktrees\/([^/]+)(?:\/|$)/);
64
- if (worktreeMatch) {
65
- return worktreeMatch[1];
66
- }
67
-
68
- try {
69
- const root = getRepoRoot();
70
- const branch = require('child_process')
71
- .execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
72
- cwd: root,
73
- encoding: 'utf8',
74
- stdio: 'pipe',
75
- })
76
- .trim();
77
- const registry = loadWorktreeRegistry(root);
78
- for (const [name, entry] of Object.entries(registry.worktrees || {})) {
79
- if (entry.branch === branch && entry.status !== 'destroyed' && entry.status !== 'merged') {
80
- return name;
81
- }
82
- }
83
- } catch {
84
- // Best-effort only; specs can still be created outside a worktree.
85
- }
86
-
87
- return null;
88
- }
89
- // Legacy constants kept for backward compatibility in tests
90
- const SPECS_DIR = '.caws/specs';
91
- const SPECS_REGISTRY = '.caws/specs/registry.json';
92
-
93
- /**
94
- * Load specs registry
95
- * @returns {Promise<Object>} Registry data
96
- */
97
- async function loadSpecsRegistry() {
98
- try {
99
- const registryPath = getSpecsRegistry();
100
- if (!(await fs.pathExists(registryPath))) {
101
- return {
102
- version: '1.0.0',
103
- specs: {},
104
- lastUpdated: new Date().toISOString(),
105
- };
106
- }
107
-
108
- const registry = JSON.parse(await fs.readFile(registryPath, 'utf8'));
109
- return registry;
110
- } catch (error) {
111
- return {
112
- version: '1.0.0',
113
- specs: {},
114
- lastUpdated: new Date().toISOString(),
115
- };
116
- }
117
- }
118
-
119
- /**
120
- * Save specs registry
121
- * @param {Object} registry - Registry data
122
- * @returns {Promise<void>}
123
- */
124
- async function saveSpecsRegistry(registry) {
125
- const registryPath = getSpecsRegistry();
126
- await fs.ensureDir(path.dirname(registryPath));
127
- registry.lastUpdated = new Date().toISOString();
128
- await fs.writeFile(registryPath, JSON.stringify(registry, null, 2));
129
- }
130
-
131
- /**
132
- * Read and validate a spec YAML file that was just written.
133
- * This catches malformed YAML and duplicate keys before registry sync.
134
- * @param {string} filePath - Absolute path to the spec file
135
- * @returns {Promise<Object>} Parsed spec object
136
- */
137
- async function validateAndReadSpecFile(filePath) {
138
- const writtenContent = await fs.readFile(filePath, 'utf8');
139
- const parsed = yaml.load(writtenContent);
140
-
141
- if (!parsed || typeof parsed !== 'object') {
142
- throw new Error('Failed to parse written spec file - invalid YAML structure');
143
- }
144
-
145
- const { validateWorkingSpec } = require('../validation/spec-validation');
146
- const validation = validateWorkingSpec(parsed);
147
-
148
- if (!validation.valid) {
149
- const errorMessages = validation.errors
150
- .map((e) => `${e.instancePath}: ${e.message}`)
151
- .join('; ');
152
- throw new Error(`Spec validation failed: ${errorMessages}`);
153
- }
154
-
155
- return parsed;
156
- }
157
-
158
- /**
159
- * Build the registry entry from the parsed spec content instead of caller assumptions.
160
- * @param {Object} spec - Parsed spec object
161
- * @param {string} fileName - Registry path for the spec
162
- * @param {string|null} owner - Session owner for the registry entry
163
- * @returns {Object} Registry entry
164
- */
165
- function buildRegistryEntryFromSpec(spec, fileName, owner = null) {
166
- return {
167
- path: fileName,
168
- type: spec.type || 'feature',
169
- status: spec.status || 'draft',
170
- created_at: spec.created_at || new Date().toISOString(),
171
- updated_at: spec.updated_at || new Date().toISOString(),
172
- owner,
173
- };
174
- }
175
-
176
- /**
177
- * Backfill legacy sparse specs so write-time validation can succeed when
178
- * update/merge flows touch older files created before the stricter schema.
179
- * @param {Object} spec - Spec content to normalize
180
- * @returns {Object} Normalized spec content
181
- */
182
- function normalizeSpecForValidation(spec = {}) {
183
- const normalizedRiskTier =
184
- typeof spec.risk_tier === 'string'
185
- ? parseInt(spec.risk_tier.replace(/^T/i, ''), 10) || 3
186
- : spec.risk_tier || 3;
187
-
188
- const acceptanceVal = Array.isArray(spec.acceptance)
189
- ? spec.acceptance
190
- : Array.isArray(spec.acceptance_criteria)
191
- ? spec.acceptance_criteria
192
- : [];
193
-
194
- const defaults = {
195
- type: 'feature',
196
- status: 'draft',
197
- risk_tier: normalizedRiskTier,
198
- mode: 'standard',
199
- blast_radius: { modules: [], data_migration: false },
200
- operational_rollback_slo: '5m',
201
- scope: { in: ['src/', 'tests/'], out: ['node_modules/', 'dist/', 'build/'] },
202
- invariants: ['System maintains data consistency'],
203
- acceptance: [],
204
- acceptance_criteria: [],
205
- non_functional: { a11y: [], perf: {}, security: [] },
206
- contracts: [],
207
- };
208
-
209
- return {
210
- ...defaults,
211
- ...spec,
212
- risk_tier: normalizedRiskTier,
213
- blast_radius: { ...defaults.blast_radius, ...(spec.blast_radius || {}) },
214
- scope: { ...defaults.scope, ...(spec.scope || {}) },
215
- non_functional: { ...defaults.non_functional, ...(spec.non_functional || {}) },
216
- acceptance: acceptanceVal,
217
- acceptance_criteria: Array.isArray(spec.acceptance_criteria)
218
- ? spec.acceptance_criteria
219
- : acceptanceVal,
220
- };
221
- }
222
-
223
- /**
224
- * List all spec files in the specs directory
225
- * @returns {Promise<Array>} Array of spec file info
226
- */
227
- // Files under this subdir of `.caws/specs/` are treated as archived,
228
- // regardless of what the YAML's `status` field says (CAWSFIX-29 invariant:
229
- // directory location is authoritative for archive state).
230
- const ARCHIVE_SUBDIR = '.archive';
231
-
232
- function isArchivePath(relPath) {
233
- if (!relPath) return false;
234
- const normalized = relPath.replace(/\\/g, '/');
235
- return normalized === ARCHIVE_SUBDIR || normalized.startsWith(`${ARCHIVE_SUBDIR}/`);
236
- }
237
-
238
- async function listSpecFiles() {
239
- const specsDir = getSpecsDir();
240
- if (!(await fs.pathExists(specsDir))) {
241
- return [];
242
- }
243
-
244
- const files = await fs.readdir(specsDir, { recursive: true });
245
- const yamlFiles = files.filter((file) => file.endsWith('.yaml') || file.endsWith('.yml'));
246
-
247
- const specs = [];
248
- for (const file of yamlFiles) {
249
- const filePath = path.join(specsDir, file);
250
- try {
251
- const content = await fs.readFile(filePath, 'utf8');
252
- const spec = yaml.load(content);
253
- const inArchive = isArchivePath(file);
254
-
255
- specs.push({
256
- id: spec.id || path.basename(file, path.extname(file)),
257
- path: file,
258
- type: spec.type || 'feature',
259
- title: spec.title || 'Untitled',
260
- status: inArchive ? 'archived' : spec.status || 'draft',
261
- risk_tier: spec.risk_tier || 'T3',
262
- mode: spec.mode || 'development',
263
- created_at: spec.created_at || new Date().toISOString(),
264
- updated_at: spec.updated_at || new Date().toISOString(),
265
- });
266
- } catch (error) {
267
- // Skip invalid spec files
268
- console.warn(`Warning: Could not parse spec file ${file}: ${error.message}`);
269
- }
270
- }
271
-
272
- return specs;
273
- }
274
-
275
- /**
276
- * Create a new spec file
277
- * @param {string} id - Spec identifier
278
- * @param {Object} options - Creation options
279
- * @returns {Promise<Object>} Created spec info
280
- */
281
- async function createSpec(id, options = {}) {
282
- const {
283
- type = 'feature',
284
- title = `New ${type}`,
285
- risk_tier = 3, // Default to numeric 3 (low-risk)
286
- mode = 'development',
287
- template = null,
288
- force = false, // Override existing specs
289
- interactive = false, // Ask for confirmation on conflicts
290
- } = options;
291
-
292
- // Convert string tiers to numeric (handle both 'T3' and 3)
293
- let numericRiskTier = risk_tier;
294
- if (typeof risk_tier === 'string') {
295
- const tierMap = { T1: 1, T2: 2, T3: 3 };
296
- numericRiskTier = tierMap[risk_tier] || 3; // Default to 3 if invalid
297
- }
298
-
299
- // Check for existing spec
300
- const specsDir = getSpecsDir();
301
- const existingSpecPath = path.join(specsDir, `${id}.yaml`);
302
- const specExists = await fs.pathExists(existingSpecPath);
303
-
304
- // CAWSFIX-30: archive-collision guard. An id that lives in `.archive/`
305
- // is still a taken id — surface it before going further. Detection is
306
- // filesystem-driven (not registry-driven) so manually-moved legacy
307
- // specs are also caught.
308
- const archivedSpecPath = path.join(specsDir, ARCHIVE_SUBDIR, `${id}.yaml`);
309
- const archivedExists = !specExists && (await fs.pathExists(archivedSpecPath));
310
- if (archivedExists && !force) {
311
- console.error(
312
- chalk.red(
313
- `Spec '${id}' already exists in archive: ${path.relative(findProjectRoot(), archivedSpecPath)}`
314
- )
315
- );
316
- console.error(
317
- chalk.yellow(
318
- `Use --force to remove the archived copy and resurrect the id, ` +
319
- `or pick a different id.`
320
- )
321
- );
322
- throw new Error(
323
- `Spec '${id}' collides with archived spec at .archive/${id}.yaml. ` +
324
- `Use --force to resurrect or choose another id.`
325
- );
326
- }
327
- if (archivedExists && force) {
328
- // Resurrection: drop the archived YAML and any registry pointer so
329
- // the rest of createSpec can write a fresh draft cleanly.
330
- await fs.remove(archivedSpecPath);
331
- const registry = await loadSpecsRegistry();
332
- if (registry.specs[id]) {
333
- delete registry.specs[id];
334
- await saveSpecsRegistry(registry);
335
- }
336
- }
337
-
338
- // Handle conflict resolution
339
- let answer = null;
340
-
341
- if (specExists && !force) {
342
- if (interactive) {
343
- console.log(chalk.yellow(`Spec '${id}' already exists.`));
344
- console.log(chalk.gray(` Path: ${existingSpecPath}`));
345
-
346
- // Load existing spec to show details
347
- try {
348
- const existingContent = await fs.readFile(existingSpecPath, 'utf8');
349
- const existingSpec = yaml.load(existingContent);
350
- console.log(chalk.gray(` Title: ${existingSpec.title || 'Untitled'}`));
351
- console.log(chalk.gray(` Status: ${existingSpec.status || 'draft'}`));
352
- console.log(
353
- chalk.gray(
354
- ` Created: ${new Date(existingSpec.created_at || Date.now()).toLocaleDateString()}`
355
- )
356
- );
357
- } catch (error) {
358
- console.log(chalk.gray(` (Could not load existing spec details)`));
359
- }
360
-
361
- // Ask for conflict resolution
362
- answer = await askConflictResolution();
363
-
364
- if (answer === 'cancel') {
365
- console.log(chalk.blue('Spec creation canceled.'));
366
- return null;
367
- } else if (answer === 'rename') {
368
- // Generate new name with valid PREFIX-NUMBER format
369
- // Extract prefix from existing ID or use default
370
- const prefixMatch = id.match(/^([A-Z]+)-\d+$/);
371
- const prefix = prefixMatch ? prefixMatch[1] : 'FEAT';
372
- // Generate sequential number based on timestamp
373
- const number = Date.now().toString().slice(-6); // Last 6 digits of timestamp
374
- const newId = `${prefix}-${number}`;
375
- console.log(chalk.blue(`Creating spec with new name: ${newId}`));
376
- return await createSpec(newId, { ...options, interactive: false });
377
- } else if (answer === 'merge') {
378
- // Merge new spec data with existing spec
379
- console.log(chalk.blue('Merging with existing spec...'));
380
- return await mergeSpec(id, options);
381
- } else if (answer === 'override') {
382
- console.log(chalk.yellow('Overriding existing spec...'));
383
- }
384
- } else {
385
- console.error(chalk.red(`Spec '${id}' already exists.`));
386
- console.error(
387
- chalk.yellow('Use --force to override, or --interactive for conflict resolution.')
388
- );
389
- throw new Error(`Spec '${id}' already exists. Use --force to override.`);
390
- }
391
- }
392
-
393
- // If we got here via override choice, check ownership and worktree associations
394
- if (specExists && (force || answer === 'override')) {
395
- // Check session ownership — only the creator session can override
396
- const registry = await loadSpecsRegistry();
397
- const existingEntry = registry.specs[id];
398
- const currentSession = getAgentSessionId(findProjectRoot());
399
- if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
400
- throw new Error(
401
- `Cannot override spec '${id}': owned by another session (${existingEntry.owner}). ` +
402
- `Only the creator session can override a spec. Create a new spec with a different ID instead.`
403
- );
404
- }
405
-
406
- // Check for active worktree associations
407
- const referencingWorktrees = getWorktreesReferencingSpec(id);
408
- if (referencingWorktrees.length > 0) {
409
- const names = referencingWorktrees.join(', ');
410
- throw new Error(
411
- `Cannot override spec '${id}': active worktree(s) [${names}] reference it. ` +
412
- `Destroy the worktree(s) first with 'caws worktree destroy <name>', or create a new spec with a different ID.`
413
- );
414
- }
415
- console.log(chalk.yellow('Overriding existing spec...'));
416
- }
417
-
418
- // Ensure specs directory exists
419
- await fs.ensureDir(specsDir);
420
-
421
- // Generate spec content with all required fields
422
- // Merge template carefully to preserve required fields and structure
423
- const defaultSpec = {
424
- id, // Always use the provided id parameter
425
- type,
426
- title,
427
- status: 'draft',
428
- risk_tier: numericRiskTier,
429
- mode,
430
- created_at: new Date().toISOString(),
431
- updated_at: new Date().toISOString(),
432
- // Required fields for validation
433
- blast_radius: {
434
- modules: [],
435
- data_migration: false,
436
- },
437
- operational_rollback_slo: '5m',
438
- scope: {
439
- in: ['src/', 'tests/'],
440
- out: ['node_modules/', 'dist/', 'build/'],
441
- },
442
- invariants: ['System maintains data consistency'],
443
- acceptance: [], // Note: validation expects 'acceptance', not 'acceptance_criteria'
444
- acceptance_criteria: [], // Keep for backward compatibility
445
- non_functional: {
446
- a11y: [],
447
- perf: {},
448
- security: [],
449
- },
450
- contracts: [],
451
- };
452
-
453
- const detectedWorktree = detectCurrentWorktreeName();
454
- if (detectedWorktree) {
455
- defaultSpec.worktree = detectedWorktree;
456
- }
457
-
458
- // Merge template, but preserve required structure
459
- // Map template.criteria to acceptance if present
460
- const templateAcceptance = template?.criteria || template?.acceptance;
461
-
462
- const specContent = {
463
- ...defaultSpec,
464
- ...(template || {}),
465
- // Always preserve these critical fields
466
- id, // Never allow template to override id
467
- // Map criteria to acceptance if template uses criteria
468
- acceptance: templateAcceptance || defaultSpec.acceptance,
469
- acceptance_criteria: templateAcceptance || defaultSpec.acceptance_criteria,
470
- // Deep merge scope if template provides it
471
- scope: template?.scope
472
- ? {
473
- in: template.scope.in || defaultSpec.scope.in,
474
- out: template.scope.out || defaultSpec.scope.out,
475
- }
476
- : defaultSpec.scope,
477
- // Deep merge blast_radius if template provides it
478
- blast_radius: template?.blast_radius
479
- ? {
480
- modules: template.blast_radius.modules || defaultSpec.blast_radius.modules,
481
- data_migration:
482
- template.blast_radius.data_migration !== undefined
483
- ? template.blast_radius.data_migration
484
- : defaultSpec.blast_radius.data_migration,
485
- }
486
- : defaultSpec.blast_radius,
487
- // Deep merge non_functional if template provides it
488
- non_functional: template?.non_functional
489
- ? {
490
- a11y: template.non_functional.a11y || defaultSpec.non_functional.a11y,
491
- perf: template.non_functional.perf || defaultSpec.non_functional.perf,
492
- security: template.non_functional.security || defaultSpec.non_functional.security,
493
- }
494
- : defaultSpec.non_functional,
495
- };
496
-
497
- // Create file path
498
- const fileName = `${id}.yaml`;
499
- const filePath = path.join(specsDir, fileName);
500
-
501
- // Write spec file
502
- const yamlContent = yaml.dump(specContent, { indent: 2 });
503
- await fs.writeFile(filePath, yamlContent);
504
-
505
- // Validate written file (YAML syntax and structure)
506
- let parsedSpec;
507
- try {
508
- parsedSpec = await validateAndReadSpecFile(filePath);
509
- } catch (error) {
510
- // Clean up invalid file if it exists
511
- if (await fs.pathExists(filePath)) {
512
- await fs.remove(filePath);
513
- }
514
-
515
- // Re-throw with helpful message
516
- if (error.message.includes('YAMLException') || error.message.includes('yaml')) {
517
- throw new Error(
518
- `Failed to create valid spec: YAML syntax error. ${error.message}\n` +
519
- 'Consider using the interactive mode: caws specs create <id> --interactive'
520
- );
521
- }
522
- throw error;
523
- }
524
-
525
- // Update registry
526
- const registry = await loadSpecsRegistry();
527
- registry.specs[id] = buildRegistryEntryFromSpec(
528
- parsedSpec,
529
- fileName,
530
- getAgentSessionId(findProjectRoot())
531
- );
532
- await saveSpecsRegistry(registry);
533
-
534
- // Initialize working state for new spec
535
- try {
536
- const initialState = initializeState(id);
537
- saveState(id, initialState, findProjectRoot());
538
- } catch { /* non-fatal */ }
539
-
540
- // CAWSFIX-06: warn when a feature spec is created without contracts.
541
- // Contract-first development is a CAWS value proposition; empty `contracts`
542
- // on a feature-type spec is discouraged but not fatal. Emit a non-fatal
543
- // warning to stderr so agents and humans notice and can update the spec.
544
- //
545
- // Note: the spec's acceptance text uses "mode=feature" colloquially, but in
546
- // CAWS the discriminator is the `type` field (feature/fix/refactor/chore),
547
- // not the `mode` field (development/pilot/etc.). We key off `type` to match
548
- // the --type CLI flag and the schema.
549
- const specType = parsedSpec.type || type;
550
- const specContracts = Array.isArray(parsedSpec.contracts) ? parsedSpec.contracts : [];
551
- if (specType === 'feature' && specContracts.length === 0) {
552
- console.warn(
553
- chalk.yellow(
554
- `⚠ Spec ${id} has mode=feature but no contracts. ` +
555
- `mode=feature without contracts is discouraged — ` +
556
- `run 'caws specs update ${id}' to add a contract reference.`
557
- )
558
- );
559
- }
560
-
561
- // EVLOG-001: emit spec_created event alongside state write.
562
- //
563
- // Spec-lifecycle events (spec_created / spec_closed / spec_deleted) are
564
- // **informational redundancy** with the spec file + registry, which are
565
- // the true sources of truth for spec identity. In contrast, the
566
- // validation/evaluation/gates/verify_acs events are the ONLY record of
567
- // those verification runs and losing them is real data loss.
568
- //
569
- // So we deliberately wrap spec-lifecycle emits in try/catch: a
570
- // filesystem error here (test mocks, readonly fs, etc.) must not crash
571
- // the spec create/close/delete flow, because the spec file itself is
572
- // already persisted by the time we get here. This is a principled
573
- // divergence from the strict contract for the observation events —
574
- // see docs/internal/EVENTS_LOG_MIGRATION.md §4.5 and EVLOG-001 spec.
575
- try {
576
- await appendEvent(
577
- {
578
- actor: 'cli',
579
- event: 'spec_created',
580
- spec_id: id,
581
- data: {
582
- id,
583
- type: parsedSpec.type || type,
584
- title: parsedSpec.title || title,
585
- risk_tier: parsedSpec.risk_tier || numericRiskTier,
586
- mode: parsedSpec.mode || mode,
587
- },
588
- },
589
- { projectRoot: findProjectRoot() }
590
- );
591
- } catch (err) {
592
- // Surface on stderr but don't propagate — the spec is already created.
593
-
594
- console.error(`event-log: failed to record spec_created for ${id}: ${err.message}`);
595
- }
596
-
597
- // CAWSFIX-31: refresh the current agent's claim so agents.json reflects
598
- // the current spec context. Best-effort — failures must not break the op.
599
- refreshAgentClaim(findProjectRoot(), { specId: id });
600
-
601
- return {
602
- id,
603
- path: fileName,
604
- type: parsedSpec.type || type,
605
- title: parsedSpec.title || title,
606
- status: parsedSpec.status || 'draft',
607
- risk_tier: parsedSpec.risk_tier || numericRiskTier,
608
- mode: parsedSpec.mode || mode,
609
- created_at: parsedSpec.created_at || specContent.created_at,
610
- updated_at: parsedSpec.updated_at || specContent.updated_at,
611
- };
612
- }
613
-
614
- /**
615
- * Load a specific spec file
616
- * @param {string} id - Spec identifier
617
- * @returns {Promise<Object|null>} Spec data or null
618
- */
619
- async function loadSpec(id) {
620
- const registry = await loadSpecsRegistry();
621
-
622
- if (!registry.specs[id]) {
623
- return null;
624
- }
625
-
626
- const specPath = path.join(getSpecsDir(), registry.specs[id].path);
627
-
628
- try {
629
- const content = await fs.readFile(specPath, 'utf8');
630
- return yaml.load(content);
631
- } catch (error) {
632
- throw new Error(`Failed to load spec '${id}' from ${specPath}: ${error.message}`);
633
- }
634
- }
635
-
636
- /**
637
- * Update a spec file
638
- * @param {string} id - Spec identifier
639
- * @param {Object} updates - Updates to apply
640
- * @returns {Promise<boolean>} Success status
641
- */
642
- async function updateSpec(id, updates = {}) {
643
- const spec = await loadSpec(id);
644
-
645
- if (!spec) {
646
- return false;
647
- }
648
-
649
- // Validate status if being updated
650
- if (updates.status) {
651
- const { SPEC_STATUSES } = require('../constants/spec-types');
652
- if (!SPEC_STATUSES[updates.status]) {
653
- throw new Error(
654
- `Invalid status '${updates.status}'. Valid values: ${Object.keys(SPEC_STATUSES).join(', ')}`
655
- );
656
- }
657
- }
658
-
659
- // Apply updates
660
- const updatedSpec = {
661
- ...spec,
662
- ...updates,
663
- updated_at: new Date().toISOString(),
664
- };
665
- const normalizedSpec = normalizeSpecForValidation(updatedSpec);
666
-
667
- // Write back to file
668
- const registry = await loadSpecsRegistry();
669
- const specPath = path.join(getSpecsDir(), registry.specs[id].path);
670
- const previousContent = await fs.readFile(specPath, 'utf8');
671
- await fs.writeFile(specPath, yaml.dump(normalizedSpec, { indent: 2 }));
672
-
673
- let parsedSpec;
674
- try {
675
- parsedSpec = await validateAndReadSpecFile(specPath);
676
- } catch (error) {
677
- await fs.writeFile(specPath, previousContent);
678
- throw new Error(`Failed to update spec '${id}': ${error.message}`);
679
- }
680
-
681
- registry.specs[id] = buildRegistryEntryFromSpec(
682
- parsedSpec,
683
- registry.specs[id].path,
684
- registry.specs[id].owner || null
685
- );
686
- await saveSpecsRegistry(registry);
687
-
688
- return true;
689
- }
690
-
691
- /**
692
- * Merge new spec data with an existing spec
693
- * Combines acceptance criteria, updates metadata, preserves history
694
- * @param {string} id - Spec identifier
695
- * @param {Object} options - Options including new spec data to merge
696
- * @returns {Promise<Object>} Merged spec
697
- */
698
- async function mergeSpec(id, options = {}) {
699
- const existingSpec = await loadSpec(id);
700
- if (!existingSpec) {
701
- throw new Error(`Spec '${id}' not found`);
702
- }
703
-
704
- console.log(chalk.blue(`\nMerging into existing spec: ${id}`));
705
- console.log(chalk.gray('==============================================\n'));
706
-
707
- // Show existing spec summary
708
- console.log(chalk.gray(`Existing spec:`));
709
- console.log(chalk.gray(` Title: ${existingSpec.title}`));
710
- console.log(chalk.gray(` Status: ${existingSpec.status}`));
711
- console.log(
712
- chalk.gray(` Acceptance Criteria: ${existingSpec.acceptance_criteria?.length || 0}`)
713
- );
714
- console.log('');
715
-
716
- // Prepare merge data from options
717
- const {
718
- title: newTitle,
719
- description: newDescription,
720
- acceptance_criteria: newCriteria,
721
- mode: newMode,
722
- risk_tier: newRiskTier,
723
- } = options;
724
-
725
- const mergedSpec = { ...existingSpec };
726
-
727
- // Track what was merged
728
- const mergeLog = [];
729
-
730
- // Merge title (prefer new if provided)
731
- if (newTitle && newTitle !== existingSpec.title) {
732
- mergedSpec.title = newTitle;
733
- mergeLog.push(`Title updated: "${existingSpec.title}" → "${newTitle}"`);
734
- }
735
-
736
- // Merge description
737
- if (newDescription) {
738
- if (existingSpec.description) {
739
- mergedSpec.description = `${existingSpec.description}\n\n---\n\n${newDescription}`;
740
- mergeLog.push('Description appended');
741
- } else {
742
- mergedSpec.description = newDescription;
743
- mergeLog.push('Description added');
744
- }
745
- }
746
-
747
- // Merge acceptance criteria (append new ones, avoid duplicates)
748
- if (newCriteria && Array.isArray(newCriteria) && newCriteria.length > 0) {
749
- const existingCriteria = existingSpec.acceptance_criteria || [];
750
- const existingIds = new Set(existingCriteria.map((c) => c.id));
751
-
752
- const criteriaToAdd = newCriteria.filter((c) => !existingIds.has(c.id));
753
- if (criteriaToAdd.length > 0) {
754
- mergedSpec.acceptance_criteria = [...existingCriteria, ...criteriaToAdd];
755
- mergeLog.push(`Added ${criteriaToAdd.length} new acceptance criteria`);
756
- }
757
-
758
- // Also update the 'acceptance' array if it exists
759
- if (existingSpec.acceptance) {
760
- const existingAcceptIds = new Set(existingSpec.acceptance.map((a) => a.id));
761
- const acceptToAdd = newCriteria.filter((c) => !existingAcceptIds.has(c.id));
762
- if (acceptToAdd.length > 0) {
763
- mergedSpec.acceptance = [...existingSpec.acceptance, ...acceptToAdd];
764
- }
765
- }
766
- }
767
-
768
- // Merge mode (prefer higher tier if both provided)
769
- if (newMode && newMode !== existingSpec.mode) {
770
- // Mode priority: crisis > standard > minimal
771
- const modePriority = { minimal: 1, standard: 2, crisis: 3 };
772
- if ((modePriority[newMode] || 0) > (modePriority[existingSpec.mode] || 0)) {
773
- mergedSpec.mode = newMode;
774
- mergeLog.push(`Mode upgraded: ${existingSpec.mode} → ${newMode}`);
775
- }
776
- }
777
-
778
- // Merge risk tier (prefer higher risk if both provided)
779
- if (newRiskTier && newRiskTier !== existingSpec.risk_tier) {
780
- // Risk priority: T1 > T2 > T3
781
- const riskPriority = { T3: 1, T2: 2, T1: 3, 3: 1, 2: 2, 1: 3 };
782
- if ((riskPriority[newRiskTier] || 0) > (riskPriority[existingSpec.risk_tier] || 0)) {
783
- mergedSpec.risk_tier = newRiskTier;
784
- mergeLog.push(`Risk tier updated: ${existingSpec.risk_tier} → ${newRiskTier}`);
785
- }
786
- }
787
-
788
- // Update metadata
789
- mergedSpec.updated_at = new Date().toISOString();
790
-
791
- // Add merge history entry
792
- if (!mergedSpec.history) {
793
- mergedSpec.history = [];
794
- }
795
- mergedSpec.history.push({
796
- action: 'merge',
797
- timestamp: new Date().toISOString(),
798
- changes: mergeLog,
799
- });
800
-
801
- // Save merged spec
802
- await updateSpec(id, mergedSpec);
803
-
804
- // Display merge results
805
- console.log(chalk.green('Merge completed:'));
806
- if (mergeLog.length > 0) {
807
- mergeLog.forEach((change) => {
808
- console.log(chalk.gray(` - ${change}`));
809
- });
810
- } else {
811
- console.log(chalk.gray(' - No changes needed (specs were identical)'));
812
- }
813
- console.log('');
814
-
815
- return mergedSpec;
816
- }
817
-
818
- /**
819
- * Delete a spec file
820
- * @param {string} id - Spec identifier
821
- * @returns {Promise<boolean>} Success status
822
- */
823
- async function deleteSpec(id) {
824
- const registry = await loadSpecsRegistry();
825
-
826
- if (!registry.specs[id]) {
827
- return false;
828
- }
829
-
830
- // Block deletion if owned by another session
831
- const currentSession = getAgentSessionId(findProjectRoot());
832
- const existingEntry = registry.specs[id];
833
- if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
834
- throw new Error(
835
- `Cannot delete spec '${id}': owned by another session (${existingEntry.owner}). ` +
836
- `Only the creator session can delete a spec.`
837
- );
838
- }
839
-
840
- // Block deletion if active worktrees reference this spec
841
- const referencingWorktrees = getWorktreesReferencingSpec(id);
842
- if (referencingWorktrees.length > 0) {
843
- const names = referencingWorktrees.join(', ');
844
- throw new Error(
845
- `Cannot delete spec '${id}': active worktree(s) [${names}] reference it. ` +
846
- `Destroy the worktree(s) first with 'caws worktree destroy <name>'.`
847
- );
848
- }
849
-
850
- const specPath = path.join(getSpecsDir(), registry.specs[id].path);
851
-
852
- // Remove file
853
- await fs.remove(specPath);
854
-
855
- // Clean up working state
856
- try { deleteState(id, findProjectRoot()); } catch { /* non-fatal */ }
857
-
858
- // Update registry
859
- delete registry.specs[id];
860
- await saveSpecsRegistry(registry);
861
-
862
- // EVLOG-001: emit spec_deleted event in best-effort mode. See the
863
- // createSpec commentary for why spec-lifecycle events diverge from
864
- // the strict fail-loud contract used by the observation events.
865
- try {
866
- await appendEvent(
867
- { actor: 'cli', event: 'spec_deleted', spec_id: id, data: { id } },
868
- { projectRoot: findProjectRoot() }
869
- );
870
- } catch (err) {
871
-
872
- console.error(`event-log: failed to record spec_deleted for ${id}: ${err.message}`);
873
- }
874
-
875
- // CAWSFIX-31: every lifecycle verb refreshes for consistency, even
876
- // delete (no other cleanup runs on delete; this is signal-of-presence).
877
- refreshAgentClaim(findProjectRoot(), { specId: id });
878
-
879
- return true;
880
- }
881
-
882
- /**
883
- * Close a spec (sets status to 'closed', removing scope enforcement).
884
- * @param {string} id - Spec identifier
885
- * @returns {Promise<boolean>} Success status
886
- */
887
- async function closeSpec(id) {
888
- const spec = await loadSpec(id);
889
- if (!spec) {
890
- return false;
891
- }
892
-
893
- const currentStatus = spec.status || 'draft';
894
- if (currentStatus === 'closed') {
895
- console.log(chalk.yellow(`Spec '${id}' is already closed.`));
896
- return true;
897
- }
898
- if (currentStatus === 'archived') {
899
- console.log(chalk.yellow(`Spec '${id}' is archived and cannot be closed.`));
900
- return false;
901
- }
902
-
903
- // Block closure if owned by another session
904
- const registry = await loadSpecsRegistry();
905
- const existingEntry = registry.specs[id];
906
- const currentSession = getAgentSessionId(findProjectRoot());
907
- if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
908
- console.error(
909
- chalk.red(
910
- `Cannot close spec '${id}': owned by another session (${existingEntry.owner}). ` +
911
- `Only the creator session can close a spec.`
912
- )
913
- );
914
- return false;
915
- }
916
-
917
- // Block closure if active worktrees reference this spec (closing removes scope enforcement)
918
- const referencingWorktrees = getWorktreesReferencingSpec(id);
919
- if (referencingWorktrees.length > 0) {
920
- const names = referencingWorktrees.join(', ');
921
- console.error(
922
- chalk.red(
923
- `Cannot close spec '${id}': active worktree(s) [${names}] reference it. ` +
924
- `Closing would remove scope enforcement while work is in progress. ` +
925
- `Destroy the worktree(s) first with 'caws worktree destroy <name>'.`
926
- )
927
- );
928
- return false;
929
- }
930
-
931
- // CAWSFIX-15: status-only flip uses targeted line-replace so the diff
932
- // stays a single line. Full `updateSpec` reserializes the whole YAML,
933
- // reordering fields and injecting `*ref_0` anchors for the
934
- // acceptance/acceptance_criteria alias — ~20 lines of noise for what
935
- // should be a one-word change.
936
- const specPath = path.join(getSpecsDir(), registry.specs[id].path);
937
- const original = await fs.readFile(specPath, 'utf8');
938
- const nowIso = new Date().toISOString();
939
- let patched = original.replace(/^status:\s*\S+\s*$/m, 'status: closed');
940
- patched = patched.replace(/^updated_at:.*$/m, `updated_at: '${nowIso}'`);
941
- let ok = false;
942
- if (patched !== original) {
943
- await fs.writeFile(specPath, patched);
944
- registry.specs[id] = {
945
- ...registry.specs[id],
946
- status: 'closed',
947
- updated_at: nowIso,
948
- };
949
- await saveSpecsRegistry(registry);
950
- ok = true;
951
- }
952
-
953
- // EVLOG-001: emit spec_closed event after the status update succeeds.
954
- // Records the prior status so the renderer can reconstruct the lifecycle.
955
- // Best-effort mode — see createSpec commentary.
956
- if (ok) {
957
- try {
958
- await appendEvent(
959
- {
960
- actor: 'cli',
961
- event: 'spec_closed',
962
- spec_id: id,
963
- data: { id, prior_status: currentStatus },
964
- },
965
- { projectRoot: findProjectRoot() }
966
- );
967
- } catch (err) {
968
-
969
- console.error(`event-log: failed to record spec_closed for ${id}: ${err.message}`);
970
- }
971
-
972
- // CAWSFIX-31: refresh agent claim — also fires on no-op closes
973
- // (already-closed specs return ok=true after an early exit) only
974
- // when the status actually flipped, since this is the signal that
975
- // matters: the agent is currently working on this spec.
976
- refreshAgentClaim(findProjectRoot(), { specId: id });
977
- }
978
-
979
- return ok;
980
- }
981
-
982
- /**
983
- * Archive a spec: move its YAML to `.caws/specs/.archive/<id>.yaml`,
984
- * flip status to `archived`, update the registry, and emit a `spec_archived`
985
- * event. The archive directory is the canonical truth for archive state —
986
- * the listing layer (listSpecFiles) treats any file under `.archive/` as
987
- * archived regardless of the YAML literal.
988
- *
989
- * @param {string} id - Spec identifier
990
- * @returns {Promise<boolean>} true on success (including idempotent no-ops),
991
- * false on validation/lookup failure.
992
- */
993
- async function archiveSpec(id) {
994
- // Path-traversal guard: ids must be plain filenames, not paths.
995
- // Reject before touching any filesystem state.
996
- if (!id || typeof id !== 'string' || path.basename(id) !== id || id.includes('..')) {
997
- console.error(chalk.red(`Invalid spec id '${id}': must be a plain identifier`));
998
- return false;
999
- }
1000
-
1001
- const registry = await loadSpecsRegistry();
1002
- const entry = registry.specs[id];
1003
- if (!entry) {
1004
- console.error(chalk.red(`Spec '${id}' not found`));
1005
- return false;
1006
- }
1007
-
1008
- // Block if owned by another session (mirror closeSpec/deleteSpec).
1009
- const currentSession = getAgentSessionId(findProjectRoot());
1010
- if (entry.owner && currentSession && entry.owner !== currentSession) {
1011
- console.error(
1012
- chalk.red(
1013
- `Cannot archive spec '${id}': owned by another session (${entry.owner}). ` +
1014
- `Only the creator session can archive a spec.`
1015
- )
1016
- );
1017
- return false;
1018
- }
1019
-
1020
- // Block if active worktrees still reference the spec — archiving removes
1021
- // scope enforcement and would invalidate in-flight work.
1022
- const referencingWorktrees = getWorktreesReferencingSpec(id);
1023
- if (referencingWorktrees.length > 0) {
1024
- const names = referencingWorktrees.join(', ');
1025
- console.error(
1026
- chalk.red(
1027
- `Cannot archive spec '${id}': active worktree(s) [${names}] reference it. ` +
1028
- `Destroy the worktree(s) first with 'caws worktree destroy <name>'.`
1029
- )
1030
- );
1031
- return false;
1032
- }
1033
-
1034
- const specsDir = getSpecsDir();
1035
- const priorPath = entry.path;
1036
- const currentSpecPath = path.join(specsDir, priorPath);
1037
-
1038
- // If the file is already in the archive directory, the canonical
1039
- // location is satisfied — just ensure the registry status agrees and exit.
1040
- if (isArchivePath(priorPath)) {
1041
- if (entry.status !== 'archived') {
1042
- registry.specs[id] = { ...entry, status: 'archived' };
1043
- await saveSpecsRegistry(registry);
1044
- }
1045
- console.log(chalk.yellow(`Spec '${id}' is already archived.`));
1046
- return true;
1047
- }
1048
-
1049
- if (!(await fs.pathExists(currentSpecPath))) {
1050
- console.error(
1051
- chalk.red(`Cannot archive spec '${id}': file missing at ${currentSpecPath}`)
1052
- );
1053
- return false;
1054
- }
1055
-
1056
- // CAWSFIX-15-style targeted rewrite: only `status:` and `updated_at:`
1057
- // lines move. Comments, ordering, and YAML aliases survive untouched.
1058
- const original = await fs.readFile(currentSpecPath, 'utf8');
1059
- const priorStatus = entry.status || 'draft';
1060
- const nowIso = new Date().toISOString();
1061
- let patched = original.replace(/^status:\s*\S+\s*$/m, 'status: archived');
1062
- patched = patched.replace(/^updated_at:.*$/m, `updated_at: '${nowIso}'`);
1063
-
1064
- const archiveDir = path.join(specsDir, ARCHIVE_SUBDIR);
1065
- await fs.ensureDir(archiveDir);
1066
- const newRelPath = `${ARCHIVE_SUBDIR}/${id}.yaml`;
1067
- const newAbsPath = path.join(specsDir, newRelPath);
1068
-
1069
- // Write the patched content to the archive location, then remove the
1070
- // original. fs-extra's writeFile is atomic-enough for single-file moves
1071
- // on the same filesystem; we avoid `move` because we already mutated content.
1072
- await fs.writeFile(newAbsPath, patched);
1073
- await fs.remove(currentSpecPath);
1074
-
1075
- registry.specs[id] = {
1076
- ...entry,
1077
- path: newRelPath,
1078
- status: 'archived',
1079
- updated_at: nowIso,
1080
- };
1081
- await saveSpecsRegistry(registry);
1082
-
1083
- // Best-effort event emission, matching spec_closed/spec_deleted policy:
1084
- // event-log failure does not roll back the archive operation.
1085
- try {
1086
- await appendEvent(
1087
- {
1088
- actor: 'cli',
1089
- event: 'spec_archived',
1090
- spec_id: id,
1091
- data: { id, prior_status: priorStatus, prior_path: priorPath },
1092
- },
1093
- { projectRoot: findProjectRoot() }
1094
- );
1095
- } catch (err) {
1096
- console.error(`event-log: failed to record spec_archived for ${id}: ${err.message}`);
1097
- }
1098
-
1099
- // CAWSFIX-31: refresh agent claim after a successful archive transition.
1100
- refreshAgentClaim(findProjectRoot(), { specId: id });
1101
-
1102
- return true;
1103
- }
1104
-
1105
- /**
1106
- * Display specs in a formatted table
1107
- * @param {Array} specs - Array of spec objects
1108
- */
1109
- function displaySpecsTable(specs) {
1110
- console.log(chalk.bold.cyan('\nCAWS Specs'));
1111
- console.log(chalk.cyan('==============================================\n'));
1112
-
1113
- if (specs.length === 0) {
1114
- console.log(chalk.gray(' No specs found. Create one with: caws specs create <id>'));
1115
- return;
1116
- }
1117
-
1118
- // Header
1119
- console.log(chalk.bold('ID'.padEnd(15) + 'Type'.padEnd(10) + 'Status'.padEnd(12) + 'Title'));
1120
- console.log(chalk.gray('-'.repeat(80)));
1121
-
1122
- // Sort specs by type and status priority
1123
- const statusPriority = { active: 0, draft: 1, completed: 2, closed: 3, archived: 4 };
1124
- const sortedSpecs = specs.sort((a, b) => {
1125
- const typeDiff = a.type.localeCompare(b.type);
1126
- if (typeDiff !== 0) return typeDiff;
1127
- return (statusPriority[a.status] || 999) - (statusPriority[b.status] || 999);
1128
- });
1129
-
1130
- sortedSpecs.forEach((spec) => {
1131
- const specType = SPEC_TYPES[spec.type] || SPEC_TYPES.feature;
1132
- const typeColor = specType.color;
1133
-
1134
- const statusColor =
1135
- spec.status === 'active'
1136
- ? chalk.green
1137
- : spec.status === 'draft'
1138
- ? chalk.yellow
1139
- : spec.status === 'completed'
1140
- ? chalk.blue
1141
- : chalk.gray;
1142
-
1143
- console.log(
1144
- spec.id.padEnd(15) +
1145
- typeColor(spec.type.padEnd(9)) +
1146
- statusColor(spec.status.padEnd(11)) +
1147
- chalk.white(spec.title)
1148
- );
1149
- });
1150
-
1151
- console.log('');
1152
- }
1153
-
1154
- /**
1155
- * Display detailed spec information
1156
- * @param {Object} spec - Spec object
1157
- */
1158
- function displaySpecDetails(spec) {
1159
- const specType = SPEC_TYPES[spec.type] || SPEC_TYPES.feature;
1160
- const typeColor = specType.color;
1161
-
1162
- console.log(chalk.bold.cyan(`\nSpec Details: ${spec.id}`));
1163
- console.log(chalk.cyan('==============================================\n'));
1164
-
1165
- console.log(`${specType.icon} ${typeColor(spec.type.toUpperCase())} - ${spec.title}`);
1166
- console.log(
1167
- chalk.gray(` Status: ${spec.status} | Risk Tier: ${spec.risk_tier} | Mode: ${spec.mode}`)
1168
- );
1169
- console.log(chalk.gray(` Created: ${new Date(spec.created_at).toLocaleDateString()}`));
1170
- console.log(chalk.gray(` Updated: ${new Date(spec.updated_at).toLocaleDateString()}`));
1171
-
1172
- if (spec.description) {
1173
- console.log(chalk.gray(`\n Description: ${spec.description}`));
1174
- }
1175
-
1176
- if (spec.acceptance_criteria && spec.acceptance_criteria.length > 0) {
1177
- console.log(chalk.gray(`\n Acceptance Criteria (${spec.acceptance_criteria.length}):`));
1178
- spec.acceptance_criteria.forEach((criterion, index) => {
1179
- const status = criterion.completed ? chalk.green('[done]') : chalk.red('[ ]');
1180
- console.log(
1181
- chalk.gray(` ${status} ${criterion.description || criterion.title || `A${index + 1}`}`)
1182
- );
1183
- });
1184
- }
1185
-
1186
- if (spec.contracts && spec.contracts.length > 0) {
1187
- console.log(chalk.gray(`\n Contracts (${spec.contracts.length}):`));
1188
- spec.contracts.forEach((contract) => {
1189
- console.log(chalk.gray(` ${contract.type}: ${contract.path}`));
1190
- });
1191
- }
1192
-
1193
- console.log('');
1194
- }
1195
-
1196
- /**
1197
- * Migrate from legacy working-spec.yaml to feature-specific specs
1198
- * @param {Object} options - Migration options
1199
- * @param {Function} [createSpecFn] - Function to create specs (for testing)
1200
- * @returns {Promise<Object>} Migration result
1201
- */
1202
- async function migrateFromLegacy(options = {}, createSpecFn = createSpec) {
1203
- const fs = require('fs-extra');
1204
- const path = require('path');
1205
- const yaml = require('js-yaml');
1206
- const chalk = require('chalk');
1207
-
1208
- const legacyPath = path.join(findProjectRoot(), '.caws', 'working-spec.yaml');
1209
-
1210
- if (!(await fs.pathExists(legacyPath))) {
1211
- throw new Error('No legacy working-spec.yaml found to migrate');
1212
- }
1213
-
1214
- console.log(chalk.blue('Migrating from legacy single-spec to multi-spec...'));
1215
-
1216
- const legacyContent = await fs.readFile(legacyPath, 'utf8');
1217
- const legacySpec = yaml.load(legacyContent);
1218
-
1219
- if (!legacySpec) {
1220
- throw new Error('Legacy working-spec.yaml is empty or invalid');
1221
- }
1222
-
1223
- if (!legacySpec.acceptance || !Array.isArray(legacySpec.acceptance)) {
1224
- throw new Error('Legacy working-spec.yaml must have an acceptance array');
1225
- }
1226
-
1227
- // Suggest feature breakdown based on acceptance criteria
1228
- const features = suggestFeatureBreakdown(legacySpec);
1229
-
1230
- console.log(chalk.green(`\nFound ${features.length} potential features to extract:`));
1231
- features.forEach((feature, index) => {
1232
- console.log(chalk.yellow(` ${index + 1}. ${feature.id} - ${feature.title}`));
1233
- console.log(chalk.gray(` Scope: ${feature.scope.in.join(', ')}`));
1234
- });
1235
-
1236
- // Interactive selection or use provided feature IDs
1237
- let selectedFeatures = features;
1238
-
1239
- if (options.interactive) {
1240
- selectedFeatures = await selectFeaturesInteractively(features);
1241
- if (selectedFeatures.length === 0) {
1242
- console.log(chalk.yellow('No features selected. Migration cancelled.'));
1243
- return { migrated: 0, total: features.length, createdSpecs: [], legacySpec: legacySpec.id };
1244
- }
1245
- console.log(chalk.blue(`\nMigrating ${selectedFeatures.length} selected features`));
1246
- }
1247
-
1248
- if (options.features && options.features.length > 0) {
1249
- // Filter by original feature IDs (before transformation)
1250
- selectedFeatures = features.filter((f) => options.features.includes(f.id));
1251
- if (selectedFeatures.length === 0) {
1252
- const errorMsg = `No features found matching: ${options.features.join(', ')}. Available features: ${features.map((f) => f.id).join(', ')}`;
1253
- console.log(chalk.yellow(`${errorMsg}`));
1254
- throw new Error(errorMsg);
1255
- } else {
1256
- console.log(chalk.blue(`\nMigrating selected features: ${options.features.join(', ')}`));
1257
- }
1258
- }
1259
-
1260
- // Create each feature spec
1261
- const createdSpecs = [];
1262
- let featureCounter = 1;
1263
- for (const feature of selectedFeatures) {
1264
- try {
1265
- // Transform feature ID to proper format (PREFIX-NUMBER) if needed
1266
- let specId = feature.id;
1267
- if (!/^[A-Z]+-\d+$/.test(specId)) {
1268
- // Convert 'auth' -> 'FEAT-001', 'payment' -> 'FEAT-002', etc.
1269
- const prefix = specId.toUpperCase().replace(/[^A-Z0-9]/g, '');
1270
- specId = `${prefix || 'FEAT'}-${String(featureCounter).padStart(3, '0')}`;
1271
- featureCounter++;
1272
- }
1273
-
1274
- await createSpecFn(specId, {
1275
- type: 'feature',
1276
- title: feature.title,
1277
- risk_tier: 'T3', // Default tier
1278
- mode: 'development',
1279
- template: feature,
1280
- });
1281
-
1282
- createdSpecs.push(specId);
1283
- console.log(chalk.green(` Created spec: ${specId}`));
1284
- } catch (error) {
1285
- // Log full error details for debugging
1286
- console.log(chalk.red(` Failed to create spec ${feature.id}: ${error.message}`));
1287
- if (process.env.DEBUG_MIGRATION) {
1288
- console.log(chalk.gray(` Error details: ${error.stack}`));
1289
- }
1290
- }
1291
- }
1292
-
1293
- console.log(
1294
- chalk.green(`\nMigration completed! Created ${createdSpecs.length} feature specs.`)
1295
- );
1296
-
1297
- if (createdSpecs.length > 0) {
1298
- console.log(chalk.blue('\nNext steps:'));
1299
- console.log(chalk.gray(' 1. Review and customize each feature spec'));
1300
- console.log(chalk.gray(' 2. Update agents to use --spec-id <feature-id>'));
1301
- console.log(chalk.gray(' 3. Consider archiving legacy working-spec.yaml when ready'));
1302
- console.log(chalk.blue('\n Example: caws validate --spec-id user-auth'));
1303
- }
1304
-
1305
- return {
1306
- migrated: createdSpecs.length,
1307
- total: selectedFeatures.length,
1308
- createdSpecs,
1309
- legacySpec: legacySpec.id,
1310
- };
1311
- }
1312
-
1313
- /**
1314
- * Interactive feature selection for migration
1315
- * @param {Array} features - Array of suggested features
1316
- * @returns {Promise<Array>} Selected features
1317
- */
1318
- async function selectFeaturesInteractively(features) {
1319
- const readline = require('readline');
1320
- const rl = readline.createInterface({
1321
- input: process.stdin,
1322
- output: process.stdout,
1323
- });
1324
-
1325
- console.log(chalk.cyan('\nSelect features to migrate:\n'));
1326
- features.forEach((f, i) => {
1327
- const scope = f.scope?.in?.join(', ') || 'N/A';
1328
- console.log(` ${chalk.yellow(i + 1)}. ${chalk.bold(f.id || f.name)} - ${f.title || f.description}`);
1329
- console.log(chalk.gray(` Scope: ${scope}`));
1330
- });
1331
- console.log(chalk.cyan(`\nEnter numbers separated by commas, or 'all' for all features:`));
1332
- console.log(chalk.gray(`Example: 1,3,5 or all`));
1333
-
1334
- try {
1335
- const answer = await question(rl, '> ');
1336
- const trimmed = answer.trim().toLowerCase();
1337
-
1338
- if (trimmed === 'all' || trimmed === '*') {
1339
- return features;
1340
- }
1341
-
1342
- if (trimmed === '' || trimmed === 'none' || trimmed === 'q' || trimmed === 'quit') {
1343
- return [];
1344
- }
1345
-
1346
- // Parse comma-separated numbers
1347
- const indices = trimmed
1348
- .split(',')
1349
- .map(n => parseInt(n.trim(), 10) - 1)
1350
- .filter(i => !isNaN(i) && i >= 0 && i < features.length);
1351
-
1352
- // Remove duplicates and sort
1353
- const uniqueIndices = [...new Set(indices)].sort((a, b) => a - b);
1354
-
1355
- return features.filter((_, i) => uniqueIndices.includes(i));
1356
- } finally {
1357
- await closeReadline(rl);
1358
- }
1359
- }
1360
-
1361
- /**
1362
- * Ask user how to resolve spec creation conflicts
1363
- * @returns {Promise<string>} User's choice: 'cancel', 'rename', 'merge', 'override'
1364
- */
1365
- async function askConflictResolution() {
1366
- const readline = require('readline');
1367
-
1368
- console.log(chalk.blue('\nConflict Resolution Options:'));
1369
- console.log(chalk.gray(" 1. Cancel - Don't create the spec"));
1370
- console.log(chalk.gray(' 2. Rename - Create with auto-generated name'));
1371
- console.log(chalk.gray(' 3. Merge - Merge with existing spec (not implemented)'));
1372
- console.log(chalk.gray(' 4. Override - Replace existing spec (use --force)'));
1373
- console.log(chalk.yellow('\nEnter your choice (1-4) or the option name:'));
1374
-
1375
- const rl = readline.createInterface({
1376
- input: process.stdin,
1377
- output: process.stdout,
1378
- });
1379
-
1380
- try {
1381
- const answer = await question(rl, '> ');
1382
- const trimmed = answer.trim().toLowerCase();
1383
-
1384
- // Handle numeric choices
1385
- if (trimmed === '1' || trimmed === 'cancel') {
1386
- return 'cancel';
1387
- } else if (trimmed === '2' || trimmed === 'rename') {
1388
- return 'rename';
1389
- } else if (trimmed === '3' || trimmed === 'merge') {
1390
- return 'merge';
1391
- } else if (trimmed === '4' || trimmed === 'override') {
1392
- return 'override';
1393
- } else {
1394
- console.log(chalk.red('Invalid choice. Defaulting to cancel.'));
1395
- return 'cancel';
1396
- }
1397
- } finally {
1398
- await closeReadline(rl);
1399
- }
1400
- }
1401
-
1402
- /**
1403
- * Specs command handler
1404
- * @param {string} action - Action to perform (list, create, show, update, delete, conflicts, migrate)
1405
- * @param {Object} options - Command options
1406
- */
1407
- async function specsCommand(action, options = {}) {
1408
- return safeAsync(
1409
- async () => {
1410
- switch (action) {
1411
- case 'list': {
1412
- const specs = await listSpecFiles();
1413
- displaySpecsTable(specs);
1414
-
1415
- return outputResult({
1416
- command: 'specs list',
1417
- count: specs.length,
1418
- specs: specs.map((s) => ({ id: s.id, type: s.type, status: s.status })),
1419
- });
1420
- }
1421
-
1422
- case 'conflicts': {
1423
- const { checkScopeConflicts } = require('../utils/spec-resolver');
1424
- const registry = await loadSpecsRegistry();
1425
- const specIds = Object.keys(registry.specs ?? {});
1426
-
1427
- if (specIds.length < 2) {
1428
- console.log(chalk.blue('No scope conflicts possible with fewer than 2 specs'));
1429
- return outputResult({
1430
- command: 'specs conflicts',
1431
- conflictCount: 0,
1432
- conflicts: [],
1433
- });
1434
- }
1435
-
1436
- console.log(chalk.blue(`Checking scope conflicts between ${specIds.length} specs...`));
1437
- const conflicts = await checkScopeConflicts(specIds);
1438
-
1439
- if (conflicts.length === 0) {
1440
- console.log(chalk.green('No scope conflicts detected'));
1441
- } else {
1442
- console.log(
1443
- chalk.yellow(
1444
- `Found ${conflicts.length} scope conflict${conflicts.length > 1 ? 's' : ''}:`
1445
- )
1446
- );
1447
- conflicts.forEach((conflict) => {
1448
- console.log(chalk.red(` ${conflict.spec1} ↔ ${conflict.spec2}:`));
1449
- conflict.conflicts.forEach((pathConflict) => {
1450
- console.log(chalk.gray(` ${pathConflict}`));
1451
- });
1452
- });
1453
- console.log(
1454
- chalk.blue('\nTip: Use non-overlapping scope.in paths to avoid conflicts')
1455
- );
1456
- }
1457
-
1458
- return outputResult({
1459
- command: 'specs conflicts',
1460
- conflictCount: conflicts.length,
1461
- conflicts,
1462
- });
1463
- }
1464
-
1465
- case 'migrate': {
1466
- // Allow tests to inject createSpec function
1467
- const createSpecFn = options._createSpecFn || createSpec;
1468
- const migrationOptions = { ...options };
1469
- delete migrationOptions._createSpecFn; // Remove test-only option
1470
- const result = await migrateFromLegacy(migrationOptions, createSpecFn);
1471
-
1472
- return outputResult({
1473
- command: 'specs migrate',
1474
- ...result,
1475
- });
1476
- }
1477
-
1478
- case 'create': {
1479
- if (!options.id) {
1480
- throw new Error('Spec ID is required. Usage: caws specs create <id>');
1481
- }
1482
-
1483
- const newSpec = await createSpec(options.id, {
1484
- type: options.type,
1485
- title: options.title,
1486
- risk_tier: options.tier,
1487
- mode: options.mode,
1488
- force: options.force,
1489
- interactive: options.interactive,
1490
- });
1491
-
1492
- if (!newSpec) {
1493
- // User canceled or creation failed
1494
- return outputResult({
1495
- command: 'specs create',
1496
- canceled: true,
1497
- message: 'Spec creation was canceled or failed',
1498
- });
1499
- }
1500
-
1501
- console.log(chalk.green(`Created spec: ${newSpec.id}`));
1502
- displaySpecDetails(newSpec);
1503
-
1504
- return outputResult({
1505
- command: 'specs create',
1506
- spec: newSpec,
1507
- });
1508
- }
1509
-
1510
- case 'show': {
1511
- if (!options.id) {
1512
- throw new Error('Spec ID is required. Usage: caws specs show <id>');
1513
- }
1514
-
1515
- const spec = await loadSpec(options.id);
1516
- if (!spec) {
1517
- throw new Error(`Spec '${options.id}' not found`);
1518
- }
1519
-
1520
- displaySpecDetails(spec);
1521
-
1522
- return outputResult({
1523
- command: 'specs show',
1524
- spec: { id: spec.id, type: spec.type, status: spec.status },
1525
- });
1526
- }
1527
-
1528
- case 'update': {
1529
- if (!options.id) {
1530
- throw new Error('Spec ID is required. Usage: caws specs update <id>');
1531
- }
1532
-
1533
- const updates = {};
1534
- if (options.status) updates.status = options.status;
1535
- if (options.title) updates.title = options.title;
1536
- if (options.description) updates.description = options.description;
1537
-
1538
- const updated = await updateSpec(options.id, updates);
1539
- if (!updated) {
1540
- throw new Error(`Spec '${options.id}' not found`);
1541
- }
1542
-
1543
- console.log(chalk.green(`Updated spec: ${options.id}`));
1544
-
1545
- return outputResult({
1546
- command: 'specs update',
1547
- spec: options.id,
1548
- updates,
1549
- });
1550
- }
1551
-
1552
- case 'delete': {
1553
- if (!options.id) {
1554
- throw new Error('Spec ID is required. Usage: caws specs delete <id>');
1555
- }
1556
-
1557
- const deleted = await deleteSpec(options.id);
1558
- if (!deleted) {
1559
- throw new Error(`Spec '${options.id}' not found`);
1560
- }
1561
-
1562
- console.log(chalk.green(`Deleted spec: ${options.id}`));
1563
-
1564
- return outputResult({
1565
- command: 'specs delete',
1566
- spec: options.id,
1567
- });
1568
- }
1569
-
1570
- case 'close': {
1571
- if (!options.id) {
1572
- throw new Error('Spec ID is required. Usage: caws specs close <id>');
1573
- }
1574
-
1575
- const closed = await closeSpec(options.id);
1576
- if (!closed) {
1577
- throw new Error(`Could not close spec '${options.id}'`);
1578
- }
1579
-
1580
- console.log(chalk.green(`Closed spec: ${options.id} -- scope restrictions removed`));
1581
-
1582
- return outputResult({
1583
- command: 'specs close',
1584
- spec: options.id,
1585
- });
1586
- }
1587
-
1588
- case 'archive': {
1589
- if (!options.id) {
1590
- throw new Error('Spec ID is required. Usage: caws specs archive <id>');
1591
- }
1592
-
1593
- const archived = await archiveSpec(options.id);
1594
- if (!archived) {
1595
- throw new Error(`Could not archive spec '${options.id}'`);
1596
- }
1597
-
1598
- console.log(
1599
- chalk.green(
1600
- `Archived spec: ${options.id} -- moved to .caws/specs/.archive/`
1601
- )
1602
- );
1603
-
1604
- return outputResult({
1605
- command: 'specs archive',
1606
- spec: options.id,
1607
- });
1608
- }
1609
-
1610
- case 'types': {
1611
- console.log(chalk.bold.cyan('\nAvailable Spec Types'));
1612
- console.log(chalk.cyan('==============================================\n'));
1613
-
1614
- Object.entries(SPEC_TYPES).forEach(([type, info]) => {
1615
- console.log(`${info.icon} ${info.color(type.padEnd(10))} - ${info.description}`);
1616
- });
1617
-
1618
- console.log('');
1619
-
1620
- return outputResult({
1621
- command: 'specs types',
1622
- types: Object.keys(SPEC_TYPES),
1623
- });
1624
- }
1625
-
1626
- default:
1627
- throw new Error(
1628
- `Unknown specs action: ${action}. Use: list, create, show, update, delete, close, archive, conflicts, migrate, types`
1629
- );
1630
- }
1631
- },
1632
- `specs ${action}`,
1633
- true
1634
- );
1635
- }
1636
-
1637
- module.exports = {
1638
- specsCommand,
1639
- loadSpecsRegistry,
1640
- saveSpecsRegistry,
1641
- listSpecFiles,
1642
- createSpec,
1643
- loadSpec,
1644
- updateSpec,
1645
- deleteSpec,
1646
- closeSpec,
1647
- archiveSpec,
1648
- displaySpecsTable,
1649
- displaySpecDetails,
1650
- askConflictResolution,
1651
- isArchivePath,
1652
- SPECS_DIR,
1653
- SPECS_REGISTRY,
1654
- ARCHIVE_SUBDIR,
1655
- SPEC_TYPES,
1656
- };