@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
@@ -0,0 +1,505 @@
1
+ /**
2
+ * @fileoverview Working-State Layer
3
+ *
4
+ * Runtime companion to specs that tracks what an agent is currently doing.
5
+ * Persists current phase, touched files, gate results, blockers, and
6
+ * next actions to `.caws/state/<spec-id>.json`.
7
+ *
8
+ * All writes are non-fatal — if state cannot be persisted the calling
9
+ * command continues normally.
10
+ *
11
+ * @author @darianrosebrook
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { lifecycle, EVENTS } = require('./lifecycle-events');
17
+
18
+ const STATE_DIR = '.caws/state';
19
+ const STATE_SCHEMA_VERSION = 'caws.state.v1';
20
+ const MAX_HISTORY = 20;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Find the project root by walking up to the nearest .caws/ directory.
28
+ * Falls back to cwd if nothing found.
29
+ * @param {string} [startDir]
30
+ * @returns {string}
31
+ */
32
+ function findRoot(startDir) {
33
+ let dir = startDir || process.cwd();
34
+ while (dir !== path.dirname(dir)) {
35
+ if (fs.existsSync(path.join(dir, '.caws'))) return dir;
36
+ dir = path.dirname(dir);
37
+ }
38
+ return startDir || process.cwd();
39
+ }
40
+
41
+ /**
42
+ * Resolve the absolute path for a spec's state file.
43
+ * @param {string} specId
44
+ * @param {string} [projectRoot]
45
+ * @returns {string}
46
+ */
47
+ function getStatePath(specId, projectRoot) {
48
+ const root = projectRoot || findRoot();
49
+ return path.join(root, STATE_DIR, `${specId}.json`);
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Core CRUD
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Build an empty default state for a new spec.
58
+ * @param {string} specId
59
+ * @returns {object}
60
+ */
61
+ function initializeState(specId) {
62
+ return {
63
+ schema: STATE_SCHEMA_VERSION,
64
+ spec_id: specId,
65
+ updated_at: new Date().toISOString(),
66
+ phase: 'not-started',
67
+ files_touched: [],
68
+ validation: null,
69
+ evaluation: null,
70
+ gates: null,
71
+ acceptance_criteria: null,
72
+ blockers: [],
73
+ next_actions: [],
74
+ history: [],
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Load state from disk. Returns null if file does not exist.
80
+ * @param {string} specId
81
+ * @param {string} [projectRoot]
82
+ * @returns {object|null}
83
+ */
84
+ function loadState(specId, projectRoot) {
85
+ const filePath = getStatePath(specId, projectRoot);
86
+ if (!fs.existsSync(filePath)) return null;
87
+ try {
88
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Save state atomically (write-then-rename).
96
+ * @param {string} specId
97
+ * @param {object} state
98
+ * @param {string} [projectRoot]
99
+ */
100
+ function saveState(specId, state, projectRoot) {
101
+ const filePath = getStatePath(specId, projectRoot);
102
+ const dir = path.dirname(filePath);
103
+ if (!fs.existsSync(dir)) {
104
+ fs.mkdirSync(dir, { recursive: true });
105
+ }
106
+ const tmpPath = filePath + '.tmp.' + process.pid;
107
+ fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
108
+ fs.renameSync(tmpPath, filePath);
109
+ }
110
+
111
+ /**
112
+ * Delete state file for a spec.
113
+ * @param {string} specId
114
+ * @param {string} [projectRoot]
115
+ */
116
+ function deleteState(specId, projectRoot) {
117
+ const filePath = getStatePath(specId, projectRoot);
118
+ if (fs.existsSync(filePath)) {
119
+ fs.unlinkSync(filePath);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Load → patch → recompute derived fields → save.
125
+ * @param {string} specId
126
+ * @param {object} patch - Partial state to merge
127
+ * @param {object} [options]
128
+ * @param {string} [options.projectRoot]
129
+ * @param {object} [options.spec] - Spec object for derived-field computation
130
+ * @param {string} [options.command] - Command name for history entry
131
+ * @param {string} [options.summary] - Summary for history entry
132
+ * @returns {object} Updated state
133
+ */
134
+ function updateState(specId, patch, options = {}) {
135
+ const { projectRoot, spec, command, summary } = options;
136
+ let state = loadState(specId, projectRoot) || initializeState(specId);
137
+
138
+ // Merge top-level sections (replace, not deep-merge)
139
+ for (const [key, value] of Object.entries(patch)) {
140
+ if (key === 'files_touched' && Array.isArray(value)) {
141
+ // Merge file lists with dedup
142
+ const merged = new Set([...(state.files_touched || []), ...value]);
143
+ state.files_touched = [...merged];
144
+ } else {
145
+ state[key] = value;
146
+ }
147
+ }
148
+
149
+ // Append history
150
+ if (command) {
151
+ state.history = state.history || [];
152
+ state.history.push({
153
+ timestamp: new Date().toISOString(),
154
+ command,
155
+ summary: summary || '',
156
+ });
157
+ // Cap at MAX_HISTORY
158
+ if (state.history.length > MAX_HISTORY) {
159
+ state.history = state.history.slice(-MAX_HISTORY);
160
+ }
161
+ }
162
+
163
+ // Recompute derived fields
164
+ state.blockers = computeBlockers(state);
165
+ state.next_actions = computeNextActions(state, spec);
166
+ const oldPhase = state.phase;
167
+ state.phase = computePhase(state, spec);
168
+ state.updated_at = new Date().toISOString();
169
+
170
+ // Emit phase transition event
171
+ if (oldPhase && oldPhase !== state.phase) {
172
+ try {
173
+ lifecycle.emit(EVENTS.PHASE_TRANSITION, {
174
+ specId, oldPhase, newPhase: state.phase,
175
+ timestamp: state.updated_at,
176
+ });
177
+ } catch { /* non-fatal */ }
178
+ }
179
+
180
+ saveState(specId, state, projectRoot);
181
+ return state;
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Record helpers — called by individual commands
186
+ // ---------------------------------------------------------------------------
187
+
188
+ /**
189
+ * Record validation result.
190
+ * @param {string} specId
191
+ * @param {object} result
192
+ * @param {boolean} result.passed
193
+ * @param {number} [result.compliance_score]
194
+ * @param {string} [result.grade]
195
+ * @param {number} [result.error_count]
196
+ * @param {number} [result.warning_count]
197
+ * @param {string} [projectRoot]
198
+ */
199
+ function recordValidation(specId, result, projectRoot) {
200
+ const validation = {
201
+ last_run: new Date().toISOString(),
202
+ passed: result.passed,
203
+ compliance_score: result.compliance_score ?? null,
204
+ grade: result.grade ?? null,
205
+ error_count: result.error_count ?? 0,
206
+ warning_count: result.warning_count ?? 0,
207
+ };
208
+ const summaryText = result.passed
209
+ ? `Passed (Grade ${validation.grade || '?'})`
210
+ : `Failed — ${validation.error_count} error(s)`;
211
+ updateState(specId, { validation }, {
212
+ projectRoot,
213
+ command: 'validate',
214
+ summary: summaryText,
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Record evaluation result.
220
+ * @param {string} specId
221
+ * @param {object} result
222
+ * @param {number} result.score
223
+ * @param {number} result.max_score
224
+ * @param {number} result.percentage
225
+ * @param {string} result.grade
226
+ * @param {number} result.checks_passed
227
+ * @param {number} result.checks_total
228
+ * @param {string} [projectRoot]
229
+ */
230
+ function recordEvaluation(specId, result, projectRoot) {
231
+ const evaluation = {
232
+ last_run: new Date().toISOString(),
233
+ score: result.score,
234
+ max_score: result.max_score,
235
+ percentage: result.percentage,
236
+ grade: result.grade,
237
+ checks_passed: result.checks_passed,
238
+ checks_total: result.checks_total,
239
+ };
240
+ updateState(specId, { evaluation }, {
241
+ projectRoot,
242
+ command: 'evaluate',
243
+ summary: `${result.score}/${result.max_score} (${result.percentage}%) Grade ${result.grade}`,
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Record gate evaluation results.
249
+ * @param {string} specId
250
+ * @param {object} report - Report from evaluateGates()
251
+ * @param {boolean} report.passed
252
+ * @param {object} report.summary
253
+ * @param {object[]} report.gates - Individual gate results
254
+ * @param {string} [context] - Execution context (cli, commit, edit)
255
+ * @param {string} [projectRoot]
256
+ */
257
+ function recordGates(specId, report, context, projectRoot) {
258
+ const gates = {
259
+ last_run: new Date().toISOString(),
260
+ context: context || 'cli',
261
+ passed: report.passed,
262
+ summary: report.summary,
263
+ results: (report.gates || []).map(g => ({
264
+ name: g.name,
265
+ status: g.status,
266
+ mode: g.mode,
267
+ })),
268
+ };
269
+ const { blocked, warned, passed } = report.summary || {};
270
+ const summaryText = `${passed || 0} passed, ${blocked || 0} blocked, ${warned || 0} warned`;
271
+ updateState(specId, { gates }, {
272
+ projectRoot,
273
+ command: 'gates',
274
+ summary: summaryText,
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Record acceptance-criteria verification results.
280
+ * @param {string} specId
281
+ * @param {object} result
282
+ * @param {number} result.total
283
+ * @param {number} result.pass
284
+ * @param {number} result.fail
285
+ * @param {number} result.unchecked
286
+ * @param {object[]} [result.results] - Per-AC results
287
+ * @param {string} [projectRoot]
288
+ */
289
+ function recordACVerification(specId, result, projectRoot) {
290
+ const acceptance_criteria = {
291
+ last_run: new Date().toISOString(),
292
+ total: result.total,
293
+ pass: result.pass,
294
+ fail: result.fail,
295
+ unchecked: result.unchecked,
296
+ results: (result.results || []).map(r => ({
297
+ id: r.id,
298
+ status: r.status,
299
+ })),
300
+ };
301
+ const summaryText = `${result.pass}/${result.total} pass, ${result.fail} fail, ${result.unchecked} unchecked`;
302
+ updateState(specId, { acceptance_criteria }, {
303
+ projectRoot,
304
+ command: 'verify-acs',
305
+ summary: summaryText,
306
+ });
307
+ }
308
+
309
+ /**
310
+ * Merge touched files into state (additive, deduped).
311
+ * @param {string} specId
312
+ * @param {string[]} files
313
+ * @param {string} [projectRoot]
314
+ */
315
+ function mergeFilesTouched(specId, files, projectRoot) {
316
+ if (!files || files.length === 0) return;
317
+ updateState(specId, { files_touched: files }, {
318
+ projectRoot,
319
+ command: 'session',
320
+ summary: `+${files.length} file(s) touched`,
321
+ });
322
+ }
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // Derived-field computation
326
+ // ---------------------------------------------------------------------------
327
+
328
+ /**
329
+ * Derive the current workflow phase from state evidence.
330
+ * @param {object} state
331
+ * @param {object} [spec] - Spec object (for AC count)
332
+ * @returns {string}
333
+ */
334
+ function computePhase(state, _spec) {
335
+ const v = state.validation;
336
+ const e = state.evaluation;
337
+ const g = state.gates;
338
+ const ac = state.acceptance_criteria;
339
+
340
+ // Nothing has run yet
341
+ if (!v && !e && !g && !ac) return 'not-started';
342
+
343
+ // Validation failed or evaluation below 70% → still authoring the spec
344
+ if (v && !v.passed) return 'spec-authoring';
345
+ if (e && e.percentage < 70) return 'spec-authoring';
346
+
347
+ // All ACs pass, all gates pass, evaluation >= 90% → complete
348
+ if (ac && ac.total > 0 && ac.fail === 0 && ac.unchecked === 0
349
+ && g && g.passed
350
+ && e && e.percentage >= 90) {
351
+ return 'complete';
352
+ }
353
+
354
+ // All ACs pass, gates have been run → verification phase
355
+ if (ac && ac.total > 0 && ac.fail === 0 && ac.unchecked === 0 && g) {
356
+ return 'verification';
357
+ }
358
+
359
+ // Otherwise: implementation
360
+ return 'implementation';
361
+ }
362
+
363
+ /**
364
+ * Extract active blockers from state.
365
+ * @param {object} state
366
+ * @returns {object[]}
367
+ */
368
+ function computeBlockers(state) {
369
+ const blockers = [];
370
+ const now = new Date().toISOString();
371
+
372
+ // Validation failure
373
+ if (state.validation && !state.validation.passed) {
374
+ blockers.push({
375
+ type: 'validation_failure',
376
+ message: `Validation failed with ${state.validation.error_count} error(s)`,
377
+ since: state.validation.last_run || now,
378
+ });
379
+ }
380
+
381
+ // Gate failures (block-mode only)
382
+ if (state.gates && state.gates.results) {
383
+ for (const g of state.gates.results) {
384
+ if (g.status === 'fail' && g.mode === 'block') {
385
+ blockers.push({
386
+ type: 'gate_failure',
387
+ gate: g.name,
388
+ message: `Gate "${g.name}" is blocking`,
389
+ since: state.gates.last_run || now,
390
+ });
391
+ }
392
+ }
393
+ }
394
+
395
+ // AC failures
396
+ if (state.acceptance_criteria && state.acceptance_criteria.fail > 0) {
397
+ const failingIds = (state.acceptance_criteria.results || [])
398
+ .filter(r => r.status === 'FAIL')
399
+ .map(r => r.id);
400
+ blockers.push({
401
+ type: 'ac_failure',
402
+ message: `${state.acceptance_criteria.fail} acceptance criteria failing${failingIds.length ? ': ' + failingIds.join(', ') : ''}`,
403
+ since: state.acceptance_criteria.last_run || new Date().toISOString(),
404
+ });
405
+ }
406
+
407
+ return blockers;
408
+ }
409
+
410
+ /**
411
+ * Compute ordered next actions based on current state.
412
+ * @param {object} state
413
+ * @param {object} [spec]
414
+ * @returns {string[]}
415
+ */
416
+ function computeNextActions(state, _spec) {
417
+ const actions = [];
418
+
419
+ // Validation failed → fix first
420
+ if (state.validation && !state.validation.passed) {
421
+ actions.push('Fix validation errors, then run: caws validate');
422
+ }
423
+
424
+ // Gate blockers
425
+ if (state.gates && state.gates.results) {
426
+ for (const g of state.gates.results) {
427
+ if (g.status === 'fail' && g.mode === 'block') {
428
+ actions.push(`Fix gate violation: ${g.name}`);
429
+ }
430
+ }
431
+ }
432
+
433
+ // Failing ACs
434
+ if (state.acceptance_criteria) {
435
+ const failing = (state.acceptance_criteria.results || [])
436
+ .filter(r => r.status === 'FAIL')
437
+ .map(r => r.id);
438
+ if (failing.length > 0) {
439
+ actions.push(`Fix failing acceptance criteria: ${failing.join(', ')}`);
440
+ }
441
+
442
+ const unchecked = state.acceptance_criteria.unchecked || 0;
443
+ if (unchecked > 0) {
444
+ actions.push(`Add tests for ${unchecked} unchecked acceptance criteria`);
445
+ }
446
+ }
447
+
448
+ // Low evaluation
449
+ if (state.evaluation && state.evaluation.percentage < 80) {
450
+ actions.push(`Improve spec quality (currently ${state.evaluation.percentage}%), run: caws evaluate`);
451
+ }
452
+
453
+ // No validation yet
454
+ if (!state.validation) {
455
+ actions.push('Run: caws validate');
456
+ }
457
+
458
+ // No evaluation yet
459
+ if (!state.evaluation) {
460
+ actions.push('Run: caws evaluate');
461
+ }
462
+
463
+ // No AC verification yet
464
+ if (!state.acceptance_criteria) {
465
+ actions.push('Run: caws verify-acs');
466
+ }
467
+
468
+ // Everything green
469
+ if (actions.length === 0) {
470
+ actions.push('All checks passing. Ready for merge. Run: caws verify-acs --run for final verification.');
471
+ }
472
+
473
+ return actions;
474
+ }
475
+
476
+ // ---------------------------------------------------------------------------
477
+ // Exports
478
+ // ---------------------------------------------------------------------------
479
+
480
+ module.exports = {
481
+ // Core
482
+ loadState,
483
+ saveState,
484
+ deleteState,
485
+ updateState,
486
+ initializeState,
487
+ getStatePath,
488
+
489
+ // Recorders
490
+ recordValidation,
491
+ recordEvaluation,
492
+ recordGates,
493
+ recordACVerification,
494
+ mergeFilesTouched,
495
+
496
+ // Derived fields
497
+ computePhase,
498
+ computeBlockers,
499
+ computeNextActions,
500
+
501
+ // Constants
502
+ STATE_DIR,
503
+ STATE_SCHEMA_VERSION,
504
+ MAX_HISTORY,
505
+ };
@@ -4,8 +4,11 @@
4
4
  * @author @darianrosebrook
5
5
  */
6
6
 
7
+ const fs = require('fs');
8
+ const path = require('path');
7
9
  const { deriveBudget, checkBudgetCompliance } = require('../budget-derivation');
8
10
  const { execSync } = require('child_process');
11
+ const { createValidator, getSchemaPath } = require('../utils/schema-validator');
9
12
 
10
13
  /**
11
14
  * Get actual budget statistics from git history
@@ -21,27 +24,31 @@ function getActualBudgetStats(specDir) {
21
24
  try {
22
25
  baseRef = execSync('git describe --tags --abbrev=0 2>/dev/null', {
23
26
  cwd,
24
- encoding: 'utf8'
27
+ encoding: 'utf8',
28
+ stdio: ['ignore', 'pipe', 'ignore'],
25
29
  }).trim();
26
30
  } catch {
27
31
  // No tags found, use initial commit
28
32
  baseRef = execSync('git rev-list --max-parents=0 HEAD', {
29
33
  cwd,
30
- encoding: 'utf8'
34
+ encoding: 'utf8',
35
+ stdio: ['ignore', 'pipe', 'ignore'],
31
36
  }).trim();
32
37
  }
33
38
 
34
39
  // Count files changed since base ref
35
40
  const filesOutput = execSync(`git diff --name-only ${baseRef}..HEAD`, {
36
41
  cwd,
37
- encoding: 'utf8'
42
+ encoding: 'utf8',
43
+ stdio: ['ignore', 'pipe', 'ignore'],
38
44
  });
39
45
  const files_changed = filesOutput.trim().split('\n').filter(Boolean).length;
40
46
 
41
47
  // Count lines changed (added + removed)
42
48
  const numstatOutput = execSync(`git diff --numstat ${baseRef}..HEAD`, {
43
49
  cwd,
44
- encoding: 'utf8'
50
+ encoding: 'utf8',
51
+ stdio: ['ignore', 'pipe', 'ignore'],
45
52
  });
46
53
  let lines_changed = 0;
47
54
  for (const line of numstatOutput.trim().split('\n').filter(Boolean)) {
@@ -67,7 +74,25 @@ function getActualBudgetStats(specDir) {
67
74
  */
68
75
  const validateWorkingSpec = (spec, _options = {}) => {
69
76
  try {
70
- // Basic structural validation for essential fields
77
+ // First pass: AJV schema validation (non-blocking results collected as warnings)
78
+ let schemaWarnings = [];
79
+ try {
80
+ const schemaPath = getSchemaPath('working-spec.schema.json', process.cwd());
81
+ const validate = createValidator(schemaPath);
82
+ const schemaResult = validate(spec);
83
+ if (!schemaResult.valid) {
84
+ schemaWarnings = schemaResult.errors.map(e => ({
85
+ instancePath: e.path,
86
+ message: e.message,
87
+ }));
88
+ }
89
+ } catch (schemaErr) {
90
+ // Schema not available — fall through to semantic validation
91
+ }
92
+
93
+ // Second pass: semantic checks (authoritative — always runs as fallback)
94
+
95
+ // Check required fields (schema may not be available)
71
96
  const requiredFields = [
72
97
  'id',
73
98
  'title',
@@ -82,17 +107,6 @@ const validateWorkingSpec = (spec, _options = {}) => {
82
107
  'contracts',
83
108
  ];
84
109
 
85
- // For new policy-based specs, change_budget is not required
86
- // It's derived from policy.yaml + waivers
87
-
88
- // Normalize risk_tier: accept "T1"/"T2"/"T3" strings and convert to numeric
89
- if (spec.risk_tier !== undefined && typeof spec.risk_tier === 'string') {
90
- const match = spec.risk_tier.match(/^T?(\d)$/i);
91
- if (match) {
92
- spec.risk_tier = parseInt(match[1], 10);
93
- }
94
- }
95
-
96
110
  for (const field of requiredFields) {
97
111
  if (!spec[field]) {
98
112
  return {
@@ -120,6 +134,14 @@ const validateWorkingSpec = (spec, _options = {}) => {
120
134
  };
121
135
  }
122
136
 
137
+ // Normalize risk_tier: accept "T1"/"T2"/"T3" strings and convert to numeric
138
+ if (spec.risk_tier !== undefined && typeof spec.risk_tier === 'string') {
139
+ const match = spec.risk_tier.match(/^T?(\d)$/i);
140
+ if (match) {
141
+ spec.risk_tier = parseInt(match[1], 10);
142
+ }
143
+ }
144
+
123
145
  // Validate status field if present
124
146
  if (spec.status) {
125
147
  const { SPEC_STATUSES } = require('../constants/spec-types');
@@ -203,7 +225,10 @@ const validateWorkingSpec = (spec, _options = {}) => {
203
225
  };
204
226
  }
205
227
 
206
- return { valid: true };
228
+ return {
229
+ valid: true,
230
+ schemaWarnings: schemaWarnings.length > 0 ? schemaWarnings : undefined,
231
+ };
207
232
  } catch (error) {
208
233
  return {
209
234
  valid: false,
@@ -227,7 +252,30 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
227
252
  const { autoFix = false, checkBudget = false, projectRoot } = options;
228
253
 
229
254
  try {
230
- // Basic structural validation for essential fields
255
+ let errors = [];
256
+ let warnings = [];
257
+ let fixes = [];
258
+
259
+ // First pass: AJV schema validation (non-blocking — results collected as warnings)
260
+ try {
261
+ const schemaPath = getSchemaPath('working-spec.schema.json', projectRoot || process.cwd());
262
+ const validate = createValidator(schemaPath);
263
+ const schemaResult = validate(spec);
264
+ if (!schemaResult.valid) {
265
+ for (const e of schemaResult.errors) {
266
+ const fieldName = e.path ? e.path.replace(/^\//, '').split('/')[0] : '';
267
+ warnings.push({
268
+ instancePath: e.path,
269
+ message: `Schema: ${e.message}`,
270
+ suggestion: fieldName ? getFieldSuggestion(fieldName, spec) : undefined,
271
+ });
272
+ }
273
+ }
274
+ } catch (schemaErr) {
275
+ // Schema not available — non-fatal
276
+ }
277
+
278
+ // Required fields check (authoritative — always runs regardless of schema)
231
279
  const requiredFields = [
232
280
  'id',
233
281
  'title',
@@ -242,10 +290,6 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
242
290
  'contracts',
243
291
  ];
244
292
 
245
- let errors = [];
246
- let warnings = [];
247
- let fixes = [];
248
-
249
293
  for (const field of requiredFields) {
250
294
  if (!spec[field]) {
251
295
  errors.push({
@@ -257,6 +301,8 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
257
301
  }
258
302
  }
259
303
 
304
+ // Semantic checks that AJV can't express
305
+
260
306
  // Validate specific field formats
261
307
  if (spec.id && !/^[A-Z]+-\d+$/.test(spec.id)) {
262
308
  errors.push({
@@ -575,6 +621,30 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
575
621
  // Budget enforcement is derived from policy.yaml risk_tier + waivers.
576
622
  // No warning emitted — the field is valid and expected.
577
623
 
624
+ // Validate scope.json against scope.schema.json if it exists
625
+ if (projectRoot) {
626
+ const scopeJsonPath = path.join(projectRoot, '.caws', 'scope.json');
627
+ if (fs.existsSync(scopeJsonPath)) {
628
+ try {
629
+ const schemaPath = getSchemaPath('scope.schema.json', projectRoot);
630
+ const validate = createValidator(schemaPath);
631
+ const scopeData = JSON.parse(fs.readFileSync(scopeJsonPath, 'utf8'));
632
+ const scopeResult = validate(scopeData);
633
+ if (!scopeResult.valid) {
634
+ for (const err of scopeResult.errors) {
635
+ warnings.push({
636
+ instancePath: `/scope.json${err.path}`,
637
+ message: `scope.json schema violation: ${err.message}`,
638
+ suggestion: 'Fix .caws/scope.json to match scope.schema.json',
639
+ });
640
+ }
641
+ }
642
+ } catch (schemaErr) {
643
+ // Non-fatal — don't block validation on schema issues
644
+ }
645
+ }
646
+ }
647
+
578
648
  // Derive and check budget if requested
579
649
  let budgetCheck = null;
580
650
  if (checkBudget && projectRoot) {