@paths.design/caws-cli 9.3.2 → 10.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 (286) hide show
  1. package/README.md +71 -32
  2. package/dist/budget-derivation.js +221 -74
  3. package/dist/commands/archive.js +67 -28
  4. package/dist/commands/burnup.js +20 -11
  5. package/dist/commands/diagnose.js +34 -22
  6. package/dist/commands/evaluate.js +41 -15
  7. package/dist/commands/gates.js +149 -0
  8. package/dist/commands/init.js +150 -19
  9. package/dist/commands/iterate.js +81 -4
  10. package/dist/commands/parallel.js +4 -0
  11. package/dist/commands/plan.js +9 -19
  12. package/dist/commands/provenance.js +53 -17
  13. package/dist/commands/quality-monitor.js +64 -45
  14. package/dist/commands/scope.js +264 -0
  15. package/dist/commands/sidecar.js +74 -0
  16. package/dist/commands/specs.js +381 -45
  17. package/dist/commands/status.js +117 -9
  18. package/dist/commands/templates.js +0 -8
  19. package/dist/commands/tutorial.js +10 -9
  20. package/dist/commands/validate.js +70 -6
  21. package/dist/commands/verify-acs.js +48 -76
  22. package/dist/commands/waivers.js +212 -13
  23. package/dist/commands/worktree.js +131 -26
  24. package/dist/error-handler.js +2 -13
  25. package/dist/gates/budget-limit.js +121 -0
  26. package/dist/gates/feedback.js +260 -0
  27. package/dist/gates/format.js +179 -0
  28. package/dist/gates/god-object.js +117 -0
  29. package/dist/gates/pipeline.js +167 -0
  30. package/dist/gates/scope-boundary.js +93 -0
  31. package/dist/gates/spec-completeness.js +109 -0
  32. package/dist/gates/todo-detection.js +205 -0
  33. package/dist/index.js +157 -151
  34. package/dist/parallel/parallel-manager.js +3 -3
  35. package/dist/policy/PolicyManager.js +51 -17
  36. package/dist/scaffold/claude-hooks.js +24 -1
  37. package/dist/scaffold/git-hooks.js +45 -102
  38. package/dist/scaffold/index.js +4 -3
  39. package/dist/session/session-manager.js +105 -14
  40. package/dist/sidecars/index.js +33 -0
  41. package/dist/sidecars/listeners.js +40 -0
  42. package/dist/sidecars/provenance-summary.js +238 -0
  43. package/dist/sidecars/quality-gaps.js +258 -0
  44. package/dist/sidecars/schema.js +149 -0
  45. package/dist/sidecars/spec-drift.js +151 -0
  46. package/dist/sidecars/waiver-draft.js +176 -0
  47. package/dist/templates/.caws/schemas/policy.schema.json +112 -0
  48. package/dist/templates/.caws/schemas/scope.schema.json +3 -3
  49. package/dist/templates/.caws/schemas/waivers.schema.json +96 -20
  50. package/dist/templates/.caws/schemas/working-spec.schema.json +264 -57
  51. package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
  52. package/dist/templates/.caws/templates/working-spec.template.yml +10 -4
  53. package/dist/templates/.caws/tools/scope-guard.js +66 -15
  54. package/dist/templates/.claude/README.md +1 -1
  55. package/dist/templates/.claude/hooks/audit.sh +0 -0
  56. package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
  57. package/dist/templates/.claude/hooks/classify_command.py +592 -0
  58. package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  59. package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
  60. package/dist/templates/.claude/hooks/quality-check.sh +23 -10
  61. package/dist/templates/.claude/hooks/scope-guard.sh +136 -55
  62. package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
  63. package/dist/templates/.claude/hooks/session-log.sh +76 -3
  64. package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  65. package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
  66. package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  67. package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
  68. package/dist/templates/.claude/hooks/worktree-write-guard.sh +97 -4
  69. package/dist/templates/.claude/settings.json +31 -0
  70. package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  71. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  72. package/dist/templates/.cursor/hooks/session-log.sh +924 -0
  73. package/dist/templates/.cursor/hooks.json +25 -0
  74. package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  75. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  76. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  77. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  78. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  79. package/dist/templates/.github/copilot-instructions.md +5 -5
  80. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  81. package/dist/templates/.junie/guidelines.md +2 -2
  82. package/dist/templates/.vscode/settings.json +3 -1
  83. package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  84. package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  85. package/dist/templates/CLAUDE.md +77 -8
  86. package/dist/templates/agents.md +50 -9
  87. package/dist/templates/docs/README.md +8 -7
  88. package/dist/templates/scripts/new_feature.sh +80 -0
  89. package/dist/test-analysis.js +43 -30
  90. package/dist/tool-loader.js +1 -1
  91. package/dist/utils/agent-session.js +202 -0
  92. package/dist/utils/detection.js +8 -2
  93. package/dist/utils/event-log.js +584 -0
  94. package/dist/utils/event-renderer.js +521 -0
  95. package/dist/utils/finalization.js +7 -6
  96. package/dist/utils/gitignore-updater.js +3 -0
  97. package/dist/utils/lifecycle-events.js +94 -0
  98. package/dist/utils/quality-gates-utils.js +29 -44
  99. package/dist/utils/schema-validator.js +50 -0
  100. package/dist/utils/spec-resolver.js +93 -21
  101. package/dist/utils/working-state.js +530 -0
  102. package/dist/validation/spec-validation.js +191 -31
  103. package/dist/waivers-manager.js +144 -6
  104. package/dist/worktree/worktree-manager.js +598 -95
  105. package/package.json +9 -8
  106. package/templates/.caws/schemas/policy.schema.json +112 -0
  107. package/templates/.caws/schemas/scope.schema.json +3 -3
  108. package/templates/.caws/schemas/waivers.schema.json +96 -20
  109. package/templates/.caws/schemas/working-spec.schema.json +264 -57
  110. package/templates/.caws/schemas/worktrees.schema.json +3 -1
  111. package/templates/.caws/templates/working-spec.template.yml +10 -4
  112. package/templates/.caws/tools/scope-guard.js +66 -15
  113. package/templates/.claude/README.md +1 -1
  114. package/templates/.claude/hooks/block-dangerous.sh +52 -11
  115. package/templates/.claude/hooks/classify_command.py +592 -0
  116. package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  117. package/templates/.claude/hooks/protected-paths.sh +39 -0
  118. package/templates/.claude/hooks/quality-check.sh +23 -10
  119. package/templates/.claude/hooks/scope-guard.sh +136 -55
  120. package/templates/.claude/hooks/session-caws-status.sh +2 -2
  121. package/templates/.claude/hooks/session-log.sh +76 -3
  122. package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  123. package/templates/.claude/hooks/test_classify_command.py +370 -0
  124. package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  125. package/templates/.claude/hooks/worktree-guard.sh +2 -2
  126. package/templates/.claude/hooks/worktree-write-guard.sh +97 -4
  127. package/templates/.claude/settings.json +31 -0
  128. package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  129. package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  130. package/templates/.cursor/hooks/session-log.sh +924 -0
  131. package/templates/.cursor/hooks.json +25 -0
  132. package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  133. package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  134. package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  135. package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  136. package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  137. package/templates/.github/copilot-instructions.md +5 -5
  138. package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  139. package/templates/.junie/guidelines.md +2 -2
  140. package/templates/.vscode/settings.json +3 -1
  141. package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  142. package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  143. package/templates/CLAUDE.md +77 -8
  144. package/templates/{AGENTS.md → agents.md} +50 -9
  145. package/templates/docs/README.md +8 -7
  146. package/templates/scripts/new_feature.sh +80 -0
  147. package/dist/budget-derivation.d.ts +0 -74
  148. package/dist/budget-derivation.d.ts.map +0 -1
  149. package/dist/cicd-optimizer.d.ts +0 -142
  150. package/dist/cicd-optimizer.d.ts.map +0 -1
  151. package/dist/commands/archive.d.ts +0 -51
  152. package/dist/commands/archive.d.ts.map +0 -1
  153. package/dist/commands/burnup.d.ts +0 -6
  154. package/dist/commands/burnup.d.ts.map +0 -1
  155. package/dist/commands/diagnose.d.ts +0 -52
  156. package/dist/commands/diagnose.d.ts.map +0 -1
  157. package/dist/commands/evaluate.d.ts +0 -8
  158. package/dist/commands/evaluate.d.ts.map +0 -1
  159. package/dist/commands/init.d.ts +0 -5
  160. package/dist/commands/init.d.ts.map +0 -1
  161. package/dist/commands/iterate.d.ts +0 -8
  162. package/dist/commands/iterate.d.ts.map +0 -1
  163. package/dist/commands/mode.d.ts +0 -25
  164. package/dist/commands/mode.d.ts.map +0 -1
  165. package/dist/commands/parallel.d.ts +0 -7
  166. package/dist/commands/parallel.d.ts.map +0 -1
  167. package/dist/commands/plan.d.ts +0 -49
  168. package/dist/commands/plan.d.ts.map +0 -1
  169. package/dist/commands/provenance.d.ts +0 -32
  170. package/dist/commands/provenance.d.ts.map +0 -1
  171. package/dist/commands/quality-gates.d.ts +0 -6
  172. package/dist/commands/quality-gates.d.ts.map +0 -1
  173. package/dist/commands/quality-gates.js +0 -444
  174. package/dist/commands/quality-monitor.d.ts +0 -17
  175. package/dist/commands/quality-monitor.d.ts.map +0 -1
  176. package/dist/commands/session.d.ts +0 -7
  177. package/dist/commands/session.d.ts.map +0 -1
  178. package/dist/commands/specs.d.ts +0 -77
  179. package/dist/commands/specs.d.ts.map +0 -1
  180. package/dist/commands/status.d.ts +0 -44
  181. package/dist/commands/status.d.ts.map +0 -1
  182. package/dist/commands/templates.d.ts +0 -74
  183. package/dist/commands/templates.d.ts.map +0 -1
  184. package/dist/commands/tool.d.ts +0 -13
  185. package/dist/commands/tool.d.ts.map +0 -1
  186. package/dist/commands/troubleshoot.d.ts +0 -8
  187. package/dist/commands/troubleshoot.d.ts.map +0 -1
  188. package/dist/commands/troubleshoot.js +0 -104
  189. package/dist/commands/tutorial.d.ts +0 -55
  190. package/dist/commands/tutorial.d.ts.map +0 -1
  191. package/dist/commands/validate.d.ts +0 -15
  192. package/dist/commands/validate.d.ts.map +0 -1
  193. package/dist/commands/waivers.d.ts +0 -8
  194. package/dist/commands/waivers.d.ts.map +0 -1
  195. package/dist/commands/workflow.d.ts +0 -85
  196. package/dist/commands/workflow.d.ts.map +0 -1
  197. package/dist/commands/worktree.d.ts +0 -7
  198. package/dist/commands/worktree.d.ts.map +0 -1
  199. package/dist/config/index.d.ts +0 -29
  200. package/dist/config/index.d.ts.map +0 -1
  201. package/dist/config/lite-scope.d.ts +0 -33
  202. package/dist/config/lite-scope.d.ts.map +0 -1
  203. package/dist/config/modes.d.ts +0 -264
  204. package/dist/config/modes.d.ts.map +0 -1
  205. package/dist/constants/spec-types.d.ts +0 -93
  206. package/dist/constants/spec-types.d.ts.map +0 -1
  207. package/dist/error-handler.d.ts +0 -151
  208. package/dist/error-handler.d.ts.map +0 -1
  209. package/dist/generators/jest-config-generator.d.ts +0 -32
  210. package/dist/generators/jest-config-generator.d.ts.map +0 -1
  211. package/dist/generators/jest-config.d.ts +0 -32
  212. package/dist/generators/jest-config.d.ts.map +0 -1
  213. package/dist/generators/jest-config.js +0 -242
  214. package/dist/generators/working-spec.d.ts +0 -13
  215. package/dist/generators/working-spec.d.ts.map +0 -1
  216. package/dist/index-new.d.ts +0 -5
  217. package/dist/index-new.d.ts.map +0 -1
  218. package/dist/index-new.js +0 -317
  219. package/dist/index.d.ts +0 -5
  220. package/dist/index.d.ts.map +0 -1
  221. package/dist/index.js.backup +0 -4711
  222. package/dist/minimal-cli.d.ts +0 -3
  223. package/dist/minimal-cli.d.ts.map +0 -1
  224. package/dist/parallel/parallel-manager.d.ts +0 -67
  225. package/dist/parallel/parallel-manager.d.ts.map +0 -1
  226. package/dist/policy/PolicyManager.d.ts +0 -104
  227. package/dist/policy/PolicyManager.d.ts.map +0 -1
  228. package/dist/scaffold/claude-hooks.d.ts +0 -28
  229. package/dist/scaffold/claude-hooks.d.ts.map +0 -1
  230. package/dist/scaffold/cursor-hooks.d.ts +0 -7
  231. package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
  232. package/dist/scaffold/git-hooks.d.ts +0 -38
  233. package/dist/scaffold/git-hooks.d.ts.map +0 -1
  234. package/dist/scaffold/index.d.ts +0 -17
  235. package/dist/scaffold/index.d.ts.map +0 -1
  236. package/dist/session/session-manager.d.ts +0 -94
  237. package/dist/session/session-manager.d.ts.map +0 -1
  238. package/dist/spec/SpecFileManager.d.ts +0 -146
  239. package/dist/spec/SpecFileManager.d.ts.map +0 -1
  240. package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
  241. package/dist/templates/.github/copilot/instructions.md +0 -311
  242. package/dist/test-analysis.d.ts +0 -231
  243. package/dist/test-analysis.d.ts.map +0 -1
  244. package/dist/tool-interface.d.ts +0 -236
  245. package/dist/tool-interface.d.ts.map +0 -1
  246. package/dist/tool-loader.d.ts +0 -77
  247. package/dist/tool-loader.d.ts.map +0 -1
  248. package/dist/tool-validator.d.ts +0 -72
  249. package/dist/tool-validator.d.ts.map +0 -1
  250. package/dist/utils/async-utils.d.ts +0 -73
  251. package/dist/utils/async-utils.d.ts.map +0 -1
  252. package/dist/utils/command-wrapper.d.ts +0 -66
  253. package/dist/utils/command-wrapper.d.ts.map +0 -1
  254. package/dist/utils/detection.d.ts +0 -14
  255. package/dist/utils/detection.d.ts.map +0 -1
  256. package/dist/utils/error-categories.d.ts +0 -52
  257. package/dist/utils/error-categories.d.ts.map +0 -1
  258. package/dist/utils/finalization.d.ts +0 -17
  259. package/dist/utils/finalization.d.ts.map +0 -1
  260. package/dist/utils/git-lock.d.ts +0 -13
  261. package/dist/utils/git-lock.d.ts.map +0 -1
  262. package/dist/utils/gitignore-updater.d.ts +0 -39
  263. package/dist/utils/gitignore-updater.d.ts.map +0 -1
  264. package/dist/utils/ide-detection.d.ts +0 -89
  265. package/dist/utils/ide-detection.d.ts.map +0 -1
  266. package/dist/utils/project-analysis.d.ts +0 -34
  267. package/dist/utils/project-analysis.d.ts.map +0 -1
  268. package/dist/utils/promise-utils.d.ts +0 -30
  269. package/dist/utils/promise-utils.d.ts.map +0 -1
  270. package/dist/utils/quality-gates-utils.d.ts +0 -49
  271. package/dist/utils/quality-gates-utils.d.ts.map +0 -1
  272. package/dist/utils/quality-gates.d.ts +0 -49
  273. package/dist/utils/quality-gates.d.ts.map +0 -1
  274. package/dist/utils/quality-gates.js +0 -402
  275. package/dist/utils/spec-resolver.d.ts +0 -80
  276. package/dist/utils/spec-resolver.d.ts.map +0 -1
  277. package/dist/utils/typescript-detector.d.ts +0 -66
  278. package/dist/utils/typescript-detector.d.ts.map +0 -1
  279. package/dist/utils/yaml-validation.d.ts +0 -32
  280. package/dist/utils/yaml-validation.d.ts.map +0 -1
  281. package/dist/validation/spec-validation.d.ts +0 -43
  282. package/dist/validation/spec-validation.d.ts.map +0 -1
  283. package/dist/waivers-manager.d.ts +0 -167
  284. package/dist/waivers-manager.d.ts.map +0 -1
  285. package/dist/worktree/worktree-manager.d.ts +0 -54
  286. package/dist/worktree/worktree-manager.d.ts.map +0 -1
@@ -15,6 +15,37 @@ const { SPEC_TYPES } = require('../constants/spec-types');
15
15
  // Import suggestFeatureBreakdown from spec-resolver
16
16
  const { suggestFeatureBreakdown } = require('../utils/spec-resolver');
17
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
+ }
18
49
 
19
50
  /**
20
51
  * Specs directory structure — anchored to the CAWS project root,
@@ -26,6 +57,35 @@ function getSpecsDir() {
26
57
  function getSpecsRegistry() {
27
58
  return path.join(findProjectRoot(), '.caws', 'specs', 'registry.json');
28
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
+ }
29
89
  // Legacy constants kept for backward compatibility in tests
30
90
  const SPECS_DIR = '.caws/specs';
31
91
  const SPECS_REGISTRY = '.caws/specs/registry.json';
@@ -68,6 +128,98 @@ async function saveSpecsRegistry(registry) {
68
128
  await fs.writeFile(registryPath, JSON.stringify(registry, null, 2));
69
129
  }
70
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
+
71
223
  /**
72
224
  * List all spec files in the specs directory
73
225
  * @returns {Promise<Array>} Array of spec file info
@@ -192,8 +344,28 @@ async function createSpec(id, options = {}) {
192
344
  }
193
345
  }
194
346
 
195
- // If we got here via override choice, proceed with creation
347
+ // If we got here via override choice, check ownership and worktree associations
196
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
+ }
197
369
  console.log(chalk.yellow('Overriding existing spec...'));
198
370
  }
199
371
 
@@ -232,6 +404,11 @@ async function createSpec(id, options = {}) {
232
404
  contracts: [],
233
405
  };
234
406
 
407
+ const detectedWorktree = detectCurrentWorktreeName();
408
+ if (detectedWorktree) {
409
+ defaultSpec.worktree = detectedWorktree;
410
+ }
411
+
235
412
  // Merge template, but preserve required structure
236
413
  // Map template.criteria to acceptance if present
237
414
  const templateAcceptance = template?.criteria || template?.acceptance;
@@ -280,27 +457,9 @@ async function createSpec(id, options = {}) {
280
457
  await fs.writeFile(filePath, yamlContent);
281
458
 
282
459
  // Validate written file (YAML syntax and structure)
460
+ let parsedSpec;
283
461
  try {
284
- const writtenContent = await fs.readFile(filePath, 'utf8');
285
- const parsed = yaml.load(writtenContent);
286
-
287
- // Validate YAML syntax was preserved
288
- if (!parsed || typeof parsed !== 'object') {
289
- await fs.remove(filePath);
290
- throw new Error('Failed to parse written spec file - invalid YAML structure');
291
- }
292
-
293
- // Validate spec structure using CAWS validation
294
- const { validateWorkingSpec } = require('../validation/spec-validation');
295
- const validation = validateWorkingSpec(parsed);
296
-
297
- if (!validation.valid) {
298
- await fs.remove(filePath);
299
- const errorMessages = validation.errors
300
- .map((e) => `${e.instancePath}: ${e.message}`)
301
- .join('; ');
302
- throw new Error(`Spec validation failed: ${errorMessages}`);
303
- }
462
+ parsedSpec = await validateAndReadSpecFile(filePath);
304
463
  } catch (error) {
305
464
  // Clean up invalid file if it exists
306
465
  if (await fs.pathExists(filePath)) {
@@ -319,25 +478,86 @@ async function createSpec(id, options = {}) {
319
478
 
320
479
  // Update registry
321
480
  const registry = await loadSpecsRegistry();
322
- registry.specs[id] = {
323
- path: fileName,
324
- type,
325
- status: 'draft',
326
- created_at: specContent.created_at,
327
- updated_at: specContent.updated_at,
328
- };
481
+ registry.specs[id] = buildRegistryEntryFromSpec(
482
+ parsedSpec,
483
+ fileName,
484
+ getAgentSessionId(findProjectRoot())
485
+ );
329
486
  await saveSpecsRegistry(registry);
330
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
+
331
551
  return {
332
552
  id,
333
553
  path: fileName,
334
- type,
335
- title,
336
- status: 'draft',
337
- risk_tier: numericRiskTier,
338
- mode,
339
- created_at: specContent.created_at,
340
- updated_at: specContent.updated_at,
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,
341
561
  };
342
562
  }
343
563
 
@@ -359,7 +579,7 @@ async function loadSpec(id) {
359
579
  const content = await fs.readFile(specPath, 'utf8');
360
580
  return yaml.load(content);
361
581
  } catch (error) {
362
- return null;
582
+ throw new Error(`Failed to load spec '${id}' from ${specPath}: ${error.message}`);
363
583
  }
364
584
  }
365
585
 
@@ -392,18 +612,28 @@ async function updateSpec(id, updates = {}) {
392
612
  ...updates,
393
613
  updated_at: new Date().toISOString(),
394
614
  };
615
+ const normalizedSpec = normalizeSpecForValidation(updatedSpec);
395
616
 
396
- // Update registry
617
+ // Write back to file
397
618
  const registry = await loadSpecsRegistry();
398
- registry.specs[id].updated_at = updatedSpec.updated_at;
399
- if (updates.status) {
400
- registry.specs[id].status = updates.status;
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}`);
401
629
  }
402
- await saveSpecsRegistry(registry);
403
630
 
404
- // Write back to file
405
- const specPath = path.join(getSpecsDir(), registry.specs[id].path);
406
- await fs.writeFile(specPath, yaml.dump(updatedSpec, { indent: 2 }));
631
+ registry.specs[id] = buildRegistryEntryFromSpec(
632
+ parsedSpec,
633
+ registry.specs[id].path,
634
+ registry.specs[id].owner || null
635
+ );
636
+ await saveSpecsRegistry(registry);
407
637
 
408
638
  return true;
409
639
  }
@@ -547,15 +777,51 @@ async function deleteSpec(id) {
547
777
  return false;
548
778
  }
549
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
+
550
800
  const specPath = path.join(getSpecsDir(), registry.specs[id].path);
551
801
 
552
802
  // Remove file
553
803
  await fs.remove(specPath);
554
804
 
805
+ // Clean up working state
806
+ try { deleteState(id, findProjectRoot()); } catch { /* non-fatal */ }
807
+
555
808
  // Update registry
556
809
  delete registry.specs[id];
557
810
  await saveSpecsRegistry(registry);
558
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
+
559
825
  return true;
560
826
  }
561
827
 
@@ -580,7 +846,77 @@ async function closeSpec(id) {
580
846
  return false;
581
847
  }
582
848
 
583
- return await updateSpec(id, { status: 'closed' });
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;
584
920
  }
585
921
 
586
922
  /**