@paths.design/caws-cli 9.3.2 → 10.0.1

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 (273) hide show
  1. package/README.md +58 -27
  2. package/dist/commands/archive.js +67 -28
  3. package/dist/commands/burnup.js +20 -11
  4. package/dist/commands/diagnose.js +34 -22
  5. package/dist/commands/evaluate.js +27 -15
  6. package/dist/commands/gates.js +122 -0
  7. package/dist/commands/init.js +143 -15
  8. package/dist/commands/iterate.js +77 -4
  9. package/dist/commands/parallel.js +4 -0
  10. package/dist/commands/plan.js +9 -19
  11. package/dist/commands/provenance.js +53 -17
  12. package/dist/commands/quality-monitor.js +64 -45
  13. package/dist/commands/sidecar.js +71 -0
  14. package/dist/commands/specs.js +233 -44
  15. package/dist/commands/status.js +113 -9
  16. package/dist/commands/tutorial.js +10 -9
  17. package/dist/commands/validate.js +49 -6
  18. package/dist/commands/verify-acs.js +35 -78
  19. package/dist/commands/waivers.js +69 -12
  20. package/dist/commands/worktree.js +50 -25
  21. package/dist/error-handler.js +2 -13
  22. package/dist/gates/budget-limit.js +116 -0
  23. package/dist/gates/feedback.js +260 -0
  24. package/dist/gates/format.js +179 -0
  25. package/dist/gates/god-object.js +117 -0
  26. package/dist/gates/pipeline.js +167 -0
  27. package/dist/gates/scope-boundary.js +93 -0
  28. package/dist/gates/spec-completeness.js +102 -0
  29. package/dist/gates/todo-detection.js +205 -0
  30. package/dist/index.js +130 -151
  31. package/dist/parallel/parallel-manager.js +3 -3
  32. package/dist/policy/PolicyManager.js +42 -10
  33. package/dist/scaffold/claude-hooks.js +24 -1
  34. package/dist/scaffold/git-hooks.js +45 -102
  35. package/dist/scaffold/index.js +4 -3
  36. package/dist/session/session-manager.js +71 -14
  37. package/dist/sidecars/index.js +33 -0
  38. package/dist/sidecars/listeners.js +40 -0
  39. package/dist/sidecars/provenance-summary.js +238 -0
  40. package/dist/sidecars/quality-gaps.js +258 -0
  41. package/dist/sidecars/schema.js +149 -0
  42. package/dist/sidecars/spec-drift.js +151 -0
  43. package/dist/sidecars/waiver-draft.js +176 -0
  44. package/dist/templates/.caws/schemas/policy.schema.json +50 -0
  45. package/dist/templates/.caws/schemas/waivers.schema.json +30 -24
  46. package/dist/templates/.caws/schemas/working-spec.schema.json +51 -8
  47. package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
  48. package/dist/templates/.caws/templates/working-spec.template.yml +7 -3
  49. package/dist/templates/.claude/hooks/audit.sh +0 -0
  50. package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
  51. package/dist/templates/.claude/hooks/classify_command.py +592 -0
  52. package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  53. package/dist/templates/.claude/hooks/quality-check.sh +23 -10
  54. package/dist/templates/.claude/hooks/scope-guard.sh +34 -32
  55. package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
  56. package/dist/templates/.claude/hooks/session-log.sh +76 -3
  57. package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  58. package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
  59. package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  60. package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
  61. package/dist/templates/.claude/hooks/worktree-write-guard.sh +1 -1
  62. package/dist/templates/.claude/settings.json +26 -0
  63. package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  64. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  65. package/dist/templates/.cursor/hooks/session-log.sh +924 -0
  66. package/dist/templates/.cursor/hooks.json +25 -0
  67. package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  68. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  69. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  70. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  71. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  72. package/dist/templates/.github/copilot-instructions.md +5 -5
  73. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  74. package/dist/templates/.junie/guidelines.md +2 -2
  75. package/dist/templates/.vscode/settings.json +3 -1
  76. package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  77. package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  78. package/dist/templates/CLAUDE.md +43 -8
  79. package/dist/templates/agents.md +29 -9
  80. package/dist/templates/docs/README.md +8 -7
  81. package/dist/templates/scripts/new_feature.sh +80 -0
  82. package/dist/test-analysis.js +43 -30
  83. package/dist/tool-loader.js +1 -1
  84. package/dist/utils/agent-session.js +202 -0
  85. package/dist/utils/detection.js +8 -2
  86. package/dist/utils/finalization.js +7 -6
  87. package/dist/utils/gitignore-updater.js +3 -0
  88. package/dist/utils/lifecycle-events.js +94 -0
  89. package/dist/utils/quality-gates-utils.js +29 -44
  90. package/dist/utils/schema-validator.js +42 -0
  91. package/dist/utils/spec-resolver.js +93 -21
  92. package/dist/utils/working-state.js +505 -0
  93. package/dist/validation/spec-validation.js +92 -22
  94. package/dist/waivers-manager.js +60 -6
  95. package/dist/worktree/worktree-manager.js +390 -93
  96. package/package.json +6 -6
  97. package/templates/.caws/schemas/policy.schema.json +50 -0
  98. package/templates/.caws/schemas/waivers.schema.json +30 -24
  99. package/templates/.caws/schemas/working-spec.schema.json +51 -8
  100. package/templates/.caws/schemas/worktrees.schema.json +3 -1
  101. package/templates/.caws/templates/working-spec.template.yml +7 -3
  102. package/templates/.claude/hooks/block-dangerous.sh +52 -11
  103. package/templates/.claude/hooks/classify_command.py +592 -0
  104. package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  105. package/templates/.claude/hooks/quality-check.sh +23 -10
  106. package/templates/.claude/hooks/scope-guard.sh +34 -32
  107. package/templates/.claude/hooks/session-caws-status.sh +2 -2
  108. package/templates/.claude/hooks/session-log.sh +76 -3
  109. package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  110. package/templates/.claude/hooks/test_classify_command.py +370 -0
  111. package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  112. package/templates/.claude/hooks/worktree-guard.sh +2 -2
  113. package/templates/.claude/hooks/worktree-write-guard.sh +1 -1
  114. package/templates/.claude/settings.json +26 -0
  115. package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  116. package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  117. package/templates/.cursor/hooks/session-log.sh +924 -0
  118. package/templates/.cursor/hooks.json +25 -0
  119. package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  120. package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  121. package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  122. package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  123. package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  124. package/templates/.github/copilot-instructions.md +5 -5
  125. package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  126. package/templates/.junie/guidelines.md +2 -2
  127. package/templates/.vscode/settings.json +3 -1
  128. package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  129. package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  130. package/templates/CLAUDE.md +43 -8
  131. package/templates/{AGENTS.md → agents.md} +29 -9
  132. package/templates/docs/README.md +8 -7
  133. package/templates/scripts/new_feature.sh +80 -0
  134. package/dist/budget-derivation.d.ts +0 -74
  135. package/dist/budget-derivation.d.ts.map +0 -1
  136. package/dist/cicd-optimizer.d.ts +0 -142
  137. package/dist/cicd-optimizer.d.ts.map +0 -1
  138. package/dist/commands/archive.d.ts +0 -51
  139. package/dist/commands/archive.d.ts.map +0 -1
  140. package/dist/commands/burnup.d.ts +0 -6
  141. package/dist/commands/burnup.d.ts.map +0 -1
  142. package/dist/commands/diagnose.d.ts +0 -52
  143. package/dist/commands/diagnose.d.ts.map +0 -1
  144. package/dist/commands/evaluate.d.ts +0 -8
  145. package/dist/commands/evaluate.d.ts.map +0 -1
  146. package/dist/commands/init.d.ts +0 -5
  147. package/dist/commands/init.d.ts.map +0 -1
  148. package/dist/commands/iterate.d.ts +0 -8
  149. package/dist/commands/iterate.d.ts.map +0 -1
  150. package/dist/commands/mode.d.ts +0 -25
  151. package/dist/commands/mode.d.ts.map +0 -1
  152. package/dist/commands/parallel.d.ts +0 -7
  153. package/dist/commands/parallel.d.ts.map +0 -1
  154. package/dist/commands/plan.d.ts +0 -49
  155. package/dist/commands/plan.d.ts.map +0 -1
  156. package/dist/commands/provenance.d.ts +0 -32
  157. package/dist/commands/provenance.d.ts.map +0 -1
  158. package/dist/commands/quality-gates.d.ts +0 -6
  159. package/dist/commands/quality-gates.d.ts.map +0 -1
  160. package/dist/commands/quality-gates.js +0 -444
  161. package/dist/commands/quality-monitor.d.ts +0 -17
  162. package/dist/commands/quality-monitor.d.ts.map +0 -1
  163. package/dist/commands/session.d.ts +0 -7
  164. package/dist/commands/session.d.ts.map +0 -1
  165. package/dist/commands/specs.d.ts +0 -77
  166. package/dist/commands/specs.d.ts.map +0 -1
  167. package/dist/commands/status.d.ts +0 -44
  168. package/dist/commands/status.d.ts.map +0 -1
  169. package/dist/commands/templates.d.ts +0 -74
  170. package/dist/commands/templates.d.ts.map +0 -1
  171. package/dist/commands/tool.d.ts +0 -13
  172. package/dist/commands/tool.d.ts.map +0 -1
  173. package/dist/commands/troubleshoot.d.ts +0 -8
  174. package/dist/commands/troubleshoot.d.ts.map +0 -1
  175. package/dist/commands/troubleshoot.js +0 -104
  176. package/dist/commands/tutorial.d.ts +0 -55
  177. package/dist/commands/tutorial.d.ts.map +0 -1
  178. package/dist/commands/validate.d.ts +0 -15
  179. package/dist/commands/validate.d.ts.map +0 -1
  180. package/dist/commands/waivers.d.ts +0 -8
  181. package/dist/commands/waivers.d.ts.map +0 -1
  182. package/dist/commands/workflow.d.ts +0 -85
  183. package/dist/commands/workflow.d.ts.map +0 -1
  184. package/dist/commands/worktree.d.ts +0 -7
  185. package/dist/commands/worktree.d.ts.map +0 -1
  186. package/dist/config/index.d.ts +0 -29
  187. package/dist/config/index.d.ts.map +0 -1
  188. package/dist/config/lite-scope.d.ts +0 -33
  189. package/dist/config/lite-scope.d.ts.map +0 -1
  190. package/dist/config/modes.d.ts +0 -264
  191. package/dist/config/modes.d.ts.map +0 -1
  192. package/dist/constants/spec-types.d.ts +0 -93
  193. package/dist/constants/spec-types.d.ts.map +0 -1
  194. package/dist/error-handler.d.ts +0 -151
  195. package/dist/error-handler.d.ts.map +0 -1
  196. package/dist/generators/jest-config-generator.d.ts +0 -32
  197. package/dist/generators/jest-config-generator.d.ts.map +0 -1
  198. package/dist/generators/jest-config.d.ts +0 -32
  199. package/dist/generators/jest-config.d.ts.map +0 -1
  200. package/dist/generators/jest-config.js +0 -242
  201. package/dist/generators/working-spec.d.ts +0 -13
  202. package/dist/generators/working-spec.d.ts.map +0 -1
  203. package/dist/index-new.d.ts +0 -5
  204. package/dist/index-new.d.ts.map +0 -1
  205. package/dist/index-new.js +0 -317
  206. package/dist/index.d.ts +0 -5
  207. package/dist/index.d.ts.map +0 -1
  208. package/dist/index.js.backup +0 -4711
  209. package/dist/minimal-cli.d.ts +0 -3
  210. package/dist/minimal-cli.d.ts.map +0 -1
  211. package/dist/parallel/parallel-manager.d.ts +0 -67
  212. package/dist/parallel/parallel-manager.d.ts.map +0 -1
  213. package/dist/policy/PolicyManager.d.ts +0 -104
  214. package/dist/policy/PolicyManager.d.ts.map +0 -1
  215. package/dist/scaffold/claude-hooks.d.ts +0 -28
  216. package/dist/scaffold/claude-hooks.d.ts.map +0 -1
  217. package/dist/scaffold/cursor-hooks.d.ts +0 -7
  218. package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
  219. package/dist/scaffold/git-hooks.d.ts +0 -38
  220. package/dist/scaffold/git-hooks.d.ts.map +0 -1
  221. package/dist/scaffold/index.d.ts +0 -17
  222. package/dist/scaffold/index.d.ts.map +0 -1
  223. package/dist/session/session-manager.d.ts +0 -94
  224. package/dist/session/session-manager.d.ts.map +0 -1
  225. package/dist/spec/SpecFileManager.d.ts +0 -146
  226. package/dist/spec/SpecFileManager.d.ts.map +0 -1
  227. package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
  228. package/dist/templates/.github/copilot/instructions.md +0 -311
  229. package/dist/test-analysis.d.ts +0 -231
  230. package/dist/test-analysis.d.ts.map +0 -1
  231. package/dist/tool-interface.d.ts +0 -236
  232. package/dist/tool-interface.d.ts.map +0 -1
  233. package/dist/tool-loader.d.ts +0 -77
  234. package/dist/tool-loader.d.ts.map +0 -1
  235. package/dist/tool-validator.d.ts +0 -72
  236. package/dist/tool-validator.d.ts.map +0 -1
  237. package/dist/utils/async-utils.d.ts +0 -73
  238. package/dist/utils/async-utils.d.ts.map +0 -1
  239. package/dist/utils/command-wrapper.d.ts +0 -66
  240. package/dist/utils/command-wrapper.d.ts.map +0 -1
  241. package/dist/utils/detection.d.ts +0 -14
  242. package/dist/utils/detection.d.ts.map +0 -1
  243. package/dist/utils/error-categories.d.ts +0 -52
  244. package/dist/utils/error-categories.d.ts.map +0 -1
  245. package/dist/utils/finalization.d.ts +0 -17
  246. package/dist/utils/finalization.d.ts.map +0 -1
  247. package/dist/utils/git-lock.d.ts +0 -13
  248. package/dist/utils/git-lock.d.ts.map +0 -1
  249. package/dist/utils/gitignore-updater.d.ts +0 -39
  250. package/dist/utils/gitignore-updater.d.ts.map +0 -1
  251. package/dist/utils/ide-detection.d.ts +0 -89
  252. package/dist/utils/ide-detection.d.ts.map +0 -1
  253. package/dist/utils/project-analysis.d.ts +0 -34
  254. package/dist/utils/project-analysis.d.ts.map +0 -1
  255. package/dist/utils/promise-utils.d.ts +0 -30
  256. package/dist/utils/promise-utils.d.ts.map +0 -1
  257. package/dist/utils/quality-gates-utils.d.ts +0 -49
  258. package/dist/utils/quality-gates-utils.d.ts.map +0 -1
  259. package/dist/utils/quality-gates.d.ts +0 -49
  260. package/dist/utils/quality-gates.d.ts.map +0 -1
  261. package/dist/utils/quality-gates.js +0 -402
  262. package/dist/utils/spec-resolver.d.ts +0 -80
  263. package/dist/utils/spec-resolver.d.ts.map +0 -1
  264. package/dist/utils/typescript-detector.d.ts +0 -66
  265. package/dist/utils/typescript-detector.d.ts.map +0 -1
  266. package/dist/utils/yaml-validation.d.ts +0 -32
  267. package/dist/utils/yaml-validation.d.ts.map +0 -1
  268. package/dist/validation/spec-validation.d.ts +0 -43
  269. package/dist/validation/spec-validation.d.ts.map +0 -1
  270. package/dist/waivers-manager.d.ts +0 -167
  271. package/dist/waivers-manager.d.ts.map +0 -1
  272. package/dist/worktree/worktree-manager.d.ts +0 -54
  273. package/dist/worktree/worktree-manager.d.ts.map +0 -1
@@ -8,9 +8,8 @@
8
8
  */
9
9
 
10
10
  const chalk = require('chalk');
11
- const fs = require('fs');
12
11
  const path = require('path');
13
- const yaml = require('js-yaml');
12
+ const { resolveSpec } = require('../utils/spec-resolver');
14
13
 
15
14
  /**
16
15
  * Analyze quality impact of an action
@@ -79,52 +78,58 @@ function analyzeQualityImpact(action, files = [], context = {}) {
79
78
  analysis.recommendations = ['Run CAWS evaluation to assess impact'];
80
79
  }
81
80
 
82
- // Load working spec to check risk tier
83
- try {
84
- const specPath = path.join(process.cwd(), '.caws/working-spec.yaml');
85
- if (fs.existsSync(specPath)) {
86
- const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
87
- const projectTier = spec.risk_tier;
88
-
89
- analysis.project_tier = projectTier;
90
-
91
- // Add context-specific recommendations for high-risk projects
92
- if (projectTier <= 2) {
93
- analysis.recommendations.unshift(
94
- 'High-risk project (Tier ' + projectTier + '): Run comprehensive validation'
95
- );
96
- if (analysis.risk_level === 'low') {
97
- analysis.risk_level = 'medium';
98
- } else if (analysis.risk_level === 'medium') {
99
- analysis.risk_level = 'high';
100
- }
101
- }
81
+ // Add context-based recommendations
82
+ if (context.project_tier) {
83
+ analysis.project_tier = context.project_tier;
84
+ }
102
85
 
103
- // Add tier-specific quality gates
104
- if (projectTier === 1) {
105
- analysis.quality_gates = [
106
- 'Branch coverage ≥ 90%',
107
- 'Mutation score 70%',
108
- 'All contract tests passing',
109
- 'Manual code review required',
110
- ];
111
- } else if (projectTier === 2) {
112
- analysis.quality_gates = [
113
- 'Branch coverage ≥ 80%',
114
- 'Mutation score ≥ 50%',
115
- 'Contract tests passing (if applicable)',
116
- ];
117
- } else {
118
- analysis.quality_gates = ['Branch coverage ≥ 70%', 'Mutation score ≥ 30%'];
119
- }
86
+ return analysis;
87
+ }
88
+
89
+ /**
90
+ * Enrich analysis with project tier and resolved spec metadata.
91
+ * @param {Object} analysis
92
+ * @param {Object|null} resolvedSpec
93
+ * @returns {Object}
94
+ */
95
+ function applySpecContext(analysis, resolvedSpec) {
96
+ if (!resolvedSpec?.spec) {
97
+ return analysis;
98
+ }
99
+
100
+ const projectTier = resolvedSpec.spec.risk_tier;
101
+ analysis.project_tier = projectTier;
102
+ analysis.spec_id = resolvedSpec.spec.id || null;
103
+ analysis.spec_path = resolvedSpec.path
104
+ ? path.relative(process.cwd(), resolvedSpec.path)
105
+ : null;
106
+
107
+ if (projectTier <= 2) {
108
+ analysis.recommendations.unshift(
109
+ 'High-risk project (Tier ' + projectTier + '): Run comprehensive validation'
110
+ );
111
+ if (analysis.risk_level === 'low') {
112
+ analysis.risk_level = 'medium';
113
+ } else if (analysis.risk_level === 'medium') {
114
+ analysis.risk_level = 'high';
120
115
  }
121
- } catch (error) {
122
- // Ignore if we can't load spec
123
116
  }
124
117
 
125
- // Add context-based recommendations
126
- if (context.project_tier) {
127
- analysis.project_tier = context.project_tier;
118
+ if (projectTier === 1) {
119
+ analysis.quality_gates = [
120
+ 'Branch coverage ≥ 90%',
121
+ 'Mutation score ≥ 70%',
122
+ 'All contract tests passing',
123
+ 'Manual code review required',
124
+ ];
125
+ } else if (projectTier === 2) {
126
+ analysis.quality_gates = [
127
+ 'Branch coverage ≥ 80%',
128
+ 'Mutation score ≥ 50%',
129
+ 'Contract tests passing (if applicable)',
130
+ ];
131
+ } else {
132
+ analysis.quality_gates = ['Branch coverage ≥ 70%', 'Mutation score ≥ 30%'];
128
133
  }
129
134
 
130
135
  return analysis;
@@ -170,7 +175,18 @@ async function qualityMonitorCommand(action, options = {}) {
170
175
  }
171
176
 
172
177
  // Analyze quality impact
173
- const analysis = analyzeQualityImpact(action, files, context);
178
+ let analysis = analyzeQualityImpact(action, files, context);
179
+
180
+ try {
181
+ const resolved = await resolveSpec({
182
+ specId: options.specId,
183
+ warnLegacy: false,
184
+ interactive: false,
185
+ });
186
+ analysis = applySpecContext(analysis, resolved);
187
+ } catch {
188
+ // Best-effort enrichment only
189
+ }
174
190
 
175
191
  // Display results
176
192
  console.log(chalk.bold('\nCAWS Quality Monitor\n'));
@@ -219,6 +235,9 @@ async function qualityMonitorCommand(action, options = {}) {
219
235
  // Project tier
220
236
  if (analysis.project_tier) {
221
237
  console.log(chalk.bold(`\nProject Tier: ${analysis.project_tier}`));
238
+ if (analysis.spec_id) {
239
+ console.log(chalk.gray(`Spec: ${analysis.spec_id}${analysis.spec_path ? ` (${analysis.spec_path})` : ''}`));
240
+ }
222
241
  }
223
242
 
224
243
  // Quality gates
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @fileoverview CAWS Sidecar Command
3
+ * CLI interface for bounded governance sidecars — advisory analysis modules
4
+ * that consume working state and produce structured recommendations.
5
+ * @author @darianrosebrook
6
+ */
7
+
8
+ const chalk = require('chalk');
9
+ const { resolveSpec } = require('../utils/spec-resolver');
10
+ const { loadState } = require('../utils/working-state');
11
+ const { commandWrapper } = require('../utils/command-wrapper');
12
+ const { SIDECARS, formatSidecarText } = require('../sidecars');
13
+
14
+ /**
15
+ * Run a sidecar analysis.
16
+ * @param {string} subcommand - Sidecar name (drift, gaps, waiver-draft, provenance)
17
+ * @param {Object} options - Command options
18
+ * @param {string} [options.specId] - Target spec ID
19
+ * @param {boolean} [options.json] - Output as JSON
20
+ * @param {string} [options.gate] - Gate name filter (waiver-draft only)
21
+ */
22
+ async function sidecarCommand(subcommand, options = {}) {
23
+ return commandWrapper(
24
+ async () => {
25
+ const sidecar = SIDECARS[subcommand];
26
+ if (!sidecar) {
27
+ const available = Object.keys(SIDECARS).join(', ');
28
+ console.error(chalk.red(`Unknown sidecar: ${subcommand}`));
29
+ console.error(chalk.yellow(`Available: ${available}`));
30
+ process.exit(1);
31
+ }
32
+
33
+ // Resolve spec
34
+ let spec = null;
35
+ try {
36
+ const resolved = await resolveSpec({
37
+ specId: options.specId,
38
+ warnLegacy: false,
39
+ quiet: Boolean(options.json),
40
+ });
41
+ spec = resolved.spec;
42
+ } catch (err) {
43
+ console.error(chalk.red(`Could not resolve spec: ${err.message}`));
44
+ console.error(chalk.yellow('Use --spec-id <id> to target a specific spec.'));
45
+ process.exit(1);
46
+ }
47
+
48
+ // Load working state (may be null — sidecars handle that)
49
+ const state = loadState(spec.id);
50
+
51
+ // Build sidecar-specific options
52
+ const sidecarOptions = {};
53
+ if (options.gate) sidecarOptions.gateName = options.gate;
54
+
55
+ // Run sidecar
56
+ const result = sidecar.fn(state, spec, sidecarOptions);
57
+
58
+ // Output
59
+ if (options.json) {
60
+ console.log(JSON.stringify(result, null, 2));
61
+ } else {
62
+ console.log(formatSidecarText(result));
63
+ }
64
+
65
+ return result;
66
+ },
67
+ { commandName: `sidecar ${subcommand}` }
68
+ );
69
+ }
70
+
71
+ module.exports = { sidecarCommand };
@@ -15,6 +15,36 @@ 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
+
22
+ /**
23
+ * Check if a spec is referenced by any active worktree.
24
+ * Returns the list of worktree names that reference it, or empty array.
25
+ * @param {string} specId - Spec identifier to check
26
+ * @returns {string[]} Names of worktrees referencing this spec
27
+ */
28
+ function getWorktreesReferencingSpec(specId) {
29
+ try {
30
+ const root = getRepoRoot();
31
+ const registry = loadWorktreeRegistry(root);
32
+ const matches = [];
33
+ for (const [name, entry] of Object.entries(registry.worktrees || {})) {
34
+ if (
35
+ entry.specId === specId &&
36
+ entry.status !== 'destroyed' &&
37
+ entry.status !== 'merged'
38
+ ) {
39
+ matches.push(name);
40
+ }
41
+ }
42
+ return matches;
43
+ } catch {
44
+ // If worktree registry can't be loaded (e.g., no .caws dir), no conflict
45
+ return [];
46
+ }
47
+ }
18
48
 
19
49
  /**
20
50
  * Specs directory structure — anchored to the CAWS project root,
@@ -68,6 +98,98 @@ async function saveSpecsRegistry(registry) {
68
98
  await fs.writeFile(registryPath, JSON.stringify(registry, null, 2));
69
99
  }
70
100
 
101
+ /**
102
+ * Read and validate a spec YAML file that was just written.
103
+ * This catches malformed YAML and duplicate keys before registry sync.
104
+ * @param {string} filePath - Absolute path to the spec file
105
+ * @returns {Promise<Object>} Parsed spec object
106
+ */
107
+ async function validateAndReadSpecFile(filePath) {
108
+ const writtenContent = await fs.readFile(filePath, 'utf8');
109
+ const parsed = yaml.load(writtenContent);
110
+
111
+ if (!parsed || typeof parsed !== 'object') {
112
+ throw new Error('Failed to parse written spec file - invalid YAML structure');
113
+ }
114
+
115
+ const { validateWorkingSpec } = require('../validation/spec-validation');
116
+ const validation = validateWorkingSpec(parsed);
117
+
118
+ if (!validation.valid) {
119
+ const errorMessages = validation.errors
120
+ .map((e) => `${e.instancePath}: ${e.message}`)
121
+ .join('; ');
122
+ throw new Error(`Spec validation failed: ${errorMessages}`);
123
+ }
124
+
125
+ return parsed;
126
+ }
127
+
128
+ /**
129
+ * Build the registry entry from the parsed spec content instead of caller assumptions.
130
+ * @param {Object} spec - Parsed spec object
131
+ * @param {string} fileName - Registry path for the spec
132
+ * @param {string|null} owner - Session owner for the registry entry
133
+ * @returns {Object} Registry entry
134
+ */
135
+ function buildRegistryEntryFromSpec(spec, fileName, owner = null) {
136
+ return {
137
+ path: fileName,
138
+ type: spec.type || 'feature',
139
+ status: spec.status || 'draft',
140
+ created_at: spec.created_at || new Date().toISOString(),
141
+ updated_at: spec.updated_at || new Date().toISOString(),
142
+ owner,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Backfill legacy sparse specs so write-time validation can succeed when
148
+ * update/merge flows touch older files created before the stricter schema.
149
+ * @param {Object} spec - Spec content to normalize
150
+ * @returns {Object} Normalized spec content
151
+ */
152
+ function normalizeSpecForValidation(spec = {}) {
153
+ const normalizedRiskTier =
154
+ typeof spec.risk_tier === 'string'
155
+ ? parseInt(spec.risk_tier.replace(/^T/i, ''), 10) || 3
156
+ : spec.risk_tier || 3;
157
+
158
+ const acceptanceVal = Array.isArray(spec.acceptance)
159
+ ? spec.acceptance
160
+ : Array.isArray(spec.acceptance_criteria)
161
+ ? spec.acceptance_criteria
162
+ : [];
163
+
164
+ const defaults = {
165
+ type: 'feature',
166
+ status: 'draft',
167
+ risk_tier: normalizedRiskTier,
168
+ mode: 'standard',
169
+ blast_radius: { modules: [], data_migration: false },
170
+ operational_rollback_slo: '5m',
171
+ scope: { in: ['src/', 'tests/'], out: ['node_modules/', 'dist/', 'build/'] },
172
+ invariants: ['System maintains data consistency'],
173
+ acceptance: [],
174
+ acceptance_criteria: [],
175
+ non_functional: { a11y: [], perf: {}, security: [] },
176
+ contracts: [],
177
+ };
178
+
179
+ return {
180
+ ...defaults,
181
+ ...spec,
182
+ risk_tier: normalizedRiskTier,
183
+ blast_radius: { ...defaults.blast_radius, ...(spec.blast_radius || {}) },
184
+ scope: { ...defaults.scope, ...(spec.scope || {}) },
185
+ non_functional: { ...defaults.non_functional, ...(spec.non_functional || {}) },
186
+ acceptance: acceptanceVal,
187
+ acceptance_criteria: Array.isArray(spec.acceptance_criteria)
188
+ ? spec.acceptance_criteria
189
+ : acceptanceVal,
190
+ };
191
+ }
192
+
71
193
  /**
72
194
  * List all spec files in the specs directory
73
195
  * @returns {Promise<Array>} Array of spec file info
@@ -192,8 +314,28 @@ async function createSpec(id, options = {}) {
192
314
  }
193
315
  }
194
316
 
195
- // If we got here via override choice, proceed with creation
317
+ // If we got here via override choice, check ownership and worktree associations
196
318
  if (specExists && (force || answer === 'override')) {
319
+ // Check session ownership — only the creator session can override
320
+ const registry = await loadSpecsRegistry();
321
+ const existingEntry = registry.specs[id];
322
+ const currentSession = getAgentSessionId(findProjectRoot());
323
+ if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
324
+ throw new Error(
325
+ `Cannot override spec '${id}': owned by another session (${existingEntry.owner}). ` +
326
+ `Only the creator session can override a spec. Create a new spec with a different ID instead.`
327
+ );
328
+ }
329
+
330
+ // Check for active worktree associations
331
+ const referencingWorktrees = getWorktreesReferencingSpec(id);
332
+ if (referencingWorktrees.length > 0) {
333
+ const names = referencingWorktrees.join(', ');
334
+ throw new Error(
335
+ `Cannot override spec '${id}': active worktree(s) [${names}] reference it. ` +
336
+ `Destroy the worktree(s) first with 'caws worktree destroy <name>', or create a new spec with a different ID.`
337
+ );
338
+ }
197
339
  console.log(chalk.yellow('Overriding existing spec...'));
198
340
  }
199
341
 
@@ -280,27 +422,9 @@ async function createSpec(id, options = {}) {
280
422
  await fs.writeFile(filePath, yamlContent);
281
423
 
282
424
  // Validate written file (YAML syntax and structure)
425
+ let parsedSpec;
283
426
  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
- }
427
+ parsedSpec = await validateAndReadSpecFile(filePath);
304
428
  } catch (error) {
305
429
  // Clean up invalid file if it exists
306
430
  if (await fs.pathExists(filePath)) {
@@ -319,25 +443,29 @@ async function createSpec(id, options = {}) {
319
443
 
320
444
  // Update registry
321
445
  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
- };
446
+ registry.specs[id] = buildRegistryEntryFromSpec(
447
+ parsedSpec,
448
+ fileName,
449
+ getAgentSessionId(findProjectRoot())
450
+ );
329
451
  await saveSpecsRegistry(registry);
330
452
 
453
+ // Initialize working state for new spec
454
+ try {
455
+ const initialState = initializeState(id);
456
+ saveState(id, initialState, findProjectRoot());
457
+ } catch { /* non-fatal */ }
458
+
331
459
  return {
332
460
  id,
333
461
  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,
462
+ type: parsedSpec.type || type,
463
+ title: parsedSpec.title || title,
464
+ status: parsedSpec.status || 'draft',
465
+ risk_tier: parsedSpec.risk_tier || numericRiskTier,
466
+ mode: parsedSpec.mode || mode,
467
+ created_at: parsedSpec.created_at || specContent.created_at,
468
+ updated_at: parsedSpec.updated_at || specContent.updated_at,
341
469
  };
342
470
  }
343
471
 
@@ -359,7 +487,7 @@ async function loadSpec(id) {
359
487
  const content = await fs.readFile(specPath, 'utf8');
360
488
  return yaml.load(content);
361
489
  } catch (error) {
362
- return null;
490
+ throw new Error(`Failed to load spec '${id}' from ${specPath}: ${error.message}`);
363
491
  }
364
492
  }
365
493
 
@@ -392,18 +520,28 @@ async function updateSpec(id, updates = {}) {
392
520
  ...updates,
393
521
  updated_at: new Date().toISOString(),
394
522
  };
523
+ const normalizedSpec = normalizeSpecForValidation(updatedSpec);
395
524
 
396
- // Update registry
525
+ // Write back to file
397
526
  const registry = await loadSpecsRegistry();
398
- registry.specs[id].updated_at = updatedSpec.updated_at;
399
- if (updates.status) {
400
- registry.specs[id].status = updates.status;
527
+ const specPath = path.join(getSpecsDir(), registry.specs[id].path);
528
+ const previousContent = await fs.readFile(specPath, 'utf8');
529
+ await fs.writeFile(specPath, yaml.dump(normalizedSpec, { indent: 2 }));
530
+
531
+ let parsedSpec;
532
+ try {
533
+ parsedSpec = await validateAndReadSpecFile(specPath);
534
+ } catch (error) {
535
+ await fs.writeFile(specPath, previousContent);
536
+ throw new Error(`Failed to update spec '${id}': ${error.message}`);
401
537
  }
402
- await saveSpecsRegistry(registry);
403
538
 
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 }));
539
+ registry.specs[id] = buildRegistryEntryFromSpec(
540
+ parsedSpec,
541
+ registry.specs[id].path,
542
+ registry.specs[id].owner || null
543
+ );
544
+ await saveSpecsRegistry(registry);
407
545
 
408
546
  return true;
409
547
  }
@@ -547,11 +685,34 @@ async function deleteSpec(id) {
547
685
  return false;
548
686
  }
549
687
 
688
+ // Block deletion if owned by another session
689
+ const currentSession = getAgentSessionId(findProjectRoot());
690
+ const existingEntry = registry.specs[id];
691
+ if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
692
+ throw new Error(
693
+ `Cannot delete spec '${id}': owned by another session (${existingEntry.owner}). ` +
694
+ `Only the creator session can delete a spec.`
695
+ );
696
+ }
697
+
698
+ // Block deletion if active worktrees reference this spec
699
+ const referencingWorktrees = getWorktreesReferencingSpec(id);
700
+ if (referencingWorktrees.length > 0) {
701
+ const names = referencingWorktrees.join(', ');
702
+ throw new Error(
703
+ `Cannot delete spec '${id}': active worktree(s) [${names}] reference it. ` +
704
+ `Destroy the worktree(s) first with 'caws worktree destroy <name>'.`
705
+ );
706
+ }
707
+
550
708
  const specPath = path.join(getSpecsDir(), registry.specs[id].path);
551
709
 
552
710
  // Remove file
553
711
  await fs.remove(specPath);
554
712
 
713
+ // Clean up working state
714
+ try { deleteState(id, findProjectRoot()); } catch { /* non-fatal */ }
715
+
555
716
  // Update registry
556
717
  delete registry.specs[id];
557
718
  await saveSpecsRegistry(registry);
@@ -580,6 +741,34 @@ async function closeSpec(id) {
580
741
  return false;
581
742
  }
582
743
 
744
+ // Block closure if owned by another session
745
+ const registry = await loadSpecsRegistry();
746
+ const existingEntry = registry.specs[id];
747
+ const currentSession = getAgentSessionId(findProjectRoot());
748
+ if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
749
+ console.error(
750
+ chalk.red(
751
+ `Cannot close spec '${id}': owned by another session (${existingEntry.owner}). ` +
752
+ `Only the creator session can close a spec.`
753
+ )
754
+ );
755
+ return false;
756
+ }
757
+
758
+ // Block closure if active worktrees reference this spec (closing removes scope enforcement)
759
+ const referencingWorktrees = getWorktreesReferencingSpec(id);
760
+ if (referencingWorktrees.length > 0) {
761
+ const names = referencingWorktrees.join(', ');
762
+ console.error(
763
+ chalk.red(
764
+ `Cannot close spec '${id}': active worktree(s) [${names}] reference it. ` +
765
+ `Closing would remove scope enforcement while work is in progress. ` +
766
+ `Destroy the worktree(s) first with 'caws worktree destroy <name>'.`
767
+ )
768
+ );
769
+ return false;
770
+ }
771
+
583
772
  return await updateSpec(id, { status: 'closed' });
584
773
  }
585
774