@paths.design/caws-cli 10.1.0 → 11.0.0

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