@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
@@ -29,43 +29,6 @@ const CONFIG = {
29
29
  },
30
30
  };
31
31
 
32
- /**
33
- * Check if a waiver applies to the given gate
34
- * @param {string} gate - Gate name to check
35
- * @returns {Object} Waiver check result
36
- */
37
- function checkWaiver(gate) {
38
- try {
39
- const waiversPath = path.join(process.cwd(), '.caws/waivers.yml');
40
- if (!fs.existsSync(waiversPath)) {
41
- return { waived: false, reason: 'No waivers file found' };
42
- }
43
-
44
- const waiversConfig = yaml.load(fs.readFileSync(waiversPath, 'utf8'));
45
- const now = new Date();
46
-
47
- // Find active waivers for this gate
48
- const activeWaivers =
49
- waiversConfig.waivers?.filter((waiver) => {
50
- const expiresAt = new Date(waiver.expires_at);
51
- return waiver.gates.includes(gate) && expiresAt > now && waiver.status === 'active';
52
- }) || [];
53
-
54
- if (activeWaivers.length > 0) {
55
- const waiver = activeWaivers[0];
56
- return {
57
- waived: true,
58
- waiver,
59
- reason: `Active waiver: ${waiver.title} (expires: ${waiver.expires_at})`,
60
- };
61
- }
62
-
63
- return { waived: false, reason: 'No active waivers found' };
64
- } catch (error) {
65
- return { waived: false, reason: `Waiver check failed: ${error.message}` };
66
- }
67
- }
68
-
69
32
  /**
70
33
  * Detect if project is in crisis response mode
71
34
  * @returns {boolean} True if in crisis mode
@@ -73,12 +36,32 @@ function checkWaiver(gate) {
73
36
  function detectCrisisMode() {
74
37
  try {
75
38
  const crisisIndicators = [
76
- // Check for crisis response in working spec
39
+ // Check for crisis response in any active spec (feature specs then legacy)
77
40
  () => {
78
- const specPath = path.join(process.cwd(), '.caws/working-spec.yaml');
79
- if (fs.existsSync(specPath)) {
80
- const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
81
- return spec.mode === 'crisis' || spec.crisis_mode === true;
41
+ // Check feature specs first
42
+ const specsDir = path.join(process.cwd(), '.caws/specs');
43
+ if (fs.existsSync(specsDir)) {
44
+ try {
45
+ const files = fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
46
+ for (const file of files) {
47
+ const spec = yaml.load(fs.readFileSync(path.join(specsDir, file), 'utf8'));
48
+ if (spec && (spec.mode === 'crisis' || spec.crisis_mode === true)) {
49
+ return true;
50
+ }
51
+ }
52
+ } catch {
53
+ // Fall through to legacy check
54
+ }
55
+ }
56
+ // Legacy fallback
57
+ const legacyPath = path.join(process.cwd(), '.caws/working-spec.yaml');
58
+ if (fs.existsSync(legacyPath)) {
59
+ try {
60
+ const spec = yaml.load(fs.readFileSync(legacyPath, 'utf8'));
61
+ return spec.mode === 'crisis' || spec.crisis_mode === true;
62
+ } catch {
63
+ // Ignore
64
+ }
82
65
  }
83
66
  return false;
84
67
  },
@@ -110,7 +93,10 @@ function detectCrisisMode() {
110
93
  */
111
94
  function getStagedFiles() {
112
95
  try {
113
- const stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' })
96
+ const stagedFiles = execSync('git diff --cached --name-only', {
97
+ encoding: 'utf8',
98
+ stdio: ['ignore', 'pipe', 'pipe'],
99
+ })
114
100
  .trim()
115
101
  .split('\n')
116
102
  .filter((file) => file.trim() !== '');
@@ -395,7 +381,6 @@ module.exports = {
395
381
  getStagedFiles,
396
382
  checkGodObjects,
397
383
  checkHiddenTodos,
398
- checkWaiver,
399
384
  detectCrisisMode,
400
385
  runQualityGates,
401
386
  CONFIG,
@@ -0,0 +1,50 @@
1
+ const Ajv = require('ajv');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+
5
+ const ajv = new Ajv({ allErrors: true, allowUnionTypes: true, strict: false, validateFormats: false });
6
+ const cache = new Map();
7
+
8
+ function createValidator(schemaPath) {
9
+ const resolved = path.resolve(schemaPath);
10
+ if (cache.has(resolved)) return cache.get(resolved);
11
+ let schema;
12
+ try {
13
+ schema = JSON.parse(fs.readFileSync(resolved, 'utf8'));
14
+ } catch (err) {
15
+ throw new Error(`Failed to parse schema file ${resolved}: ${err.message}`);
16
+ }
17
+ // Remove $schema keyword — Ajv handles validation without it and
18
+ // the 2020-12 meta-schema URI is not bundled with the base Ajv package.
19
+ delete schema.$schema;
20
+ const validate = ajv.compile(schema);
21
+ const validator = (data) => {
22
+ const valid = validate(data);
23
+ return {
24
+ valid,
25
+ errors: valid ? [] : validate.errors.map(e => ({
26
+ path: e.instancePath,
27
+ message: e.message,
28
+ params: e.params,
29
+ })),
30
+ };
31
+ };
32
+ cache.set(resolved, validator);
33
+ return validator;
34
+ }
35
+
36
+ function getSchemaPath(schemaName, projectRoot) {
37
+ // Order: flat repo layout (`.caws/<name>.schema.json`) wins so
38
+ // repos that tightened a schema in-place (e.g. CAWSFIX-03) are
39
+ // the authoritative source. Nested `.caws/schemas/<name>.schema.json`
40
+ // is the legacy layout kept for back-compat. Bundled template is
41
+ // the last-resort fallback used by globally-installed CLIs and
42
+ // projects without a local copy.
43
+ const flatPath = path.join(projectRoot, '.caws', schemaName);
44
+ if (fs.existsSync(flatPath)) return flatPath;
45
+ const nestedPath = path.join(projectRoot, '.caws', 'schemas', schemaName);
46
+ if (fs.existsSync(nestedPath)) return nestedPath;
47
+ return path.join(__dirname, '../../templates/.caws/schemas', schemaName);
48
+ }
49
+
50
+ module.exports = { createValidator, getSchemaPath };
@@ -13,6 +13,34 @@ const chalk = require('chalk');
13
13
  // Import SPEC_TYPES from constants for consistent display
14
14
  const { SPEC_TYPES } = require('../constants/spec-types');
15
15
  const { findProjectRoot } = require('./detection');
16
+ const { createValidator, getSchemaPath } = require('./schema-validator');
17
+
18
+ /**
19
+ * Validate a spec object against the working-spec schema.
20
+ * Throws with schema errors included in the message.
21
+ * @param {Object} spec - Parsed spec object
22
+ * @param {string} specPath - Path to the spec file (for error context)
23
+ */
24
+ function validateSpecSchema(spec, specPath) {
25
+ try {
26
+ const schemaPath = getSchemaPath('working-spec.schema.json', getProjectRoot());
27
+ const validate = createValidator(schemaPath);
28
+ const result = validate(spec);
29
+ if (!result.valid) {
30
+ const errorDetails = result.errors
31
+ .map(e => ` ${e.path}: ${e.message}`)
32
+ .join('\n');
33
+ // Schema violations are warnings, not fatal — the spec is still loadable.
34
+ // Commands like validate will report these; other commands shouldn't be blocked.
35
+ if (process.env.CAWS_QUIET !== '1') {
36
+ console.warn(`Schema warnings for ${specPath}:\n${errorDetails}`);
37
+ }
38
+ }
39
+ } catch (schemaErr) {
40
+ // Schema loading/compilation errors are non-fatal — warn and continue
41
+ console.warn('Could not validate spec schema:', schemaErr.message);
42
+ }
43
+ }
16
44
 
17
45
  /**
18
46
  * Spec resolution priority:
@@ -42,10 +70,11 @@ function getProjectRoot() {
42
70
  * @param {string} [options.specFile] - Explicit file path override
43
71
  * @param {boolean} [options.warnLegacy=true] - Warn when falling back to legacy spec
44
72
  * @param {boolean} [options.interactive=false] - Use interactive spec selection for multiple specs
73
+ * @param {boolean} [options.quiet=false] - Suppress informational logging for machine-readable output
45
74
  * @returns {Promise<{path: string, type: 'feature' | 'legacy', spec: Object}>}
46
75
  */
47
76
  async function resolveSpec(options = {}) {
48
- const { specId, specFile, warnLegacy = true, interactive = false } = options;
77
+ const { specId, specFile, warnLegacy = true, interactive = false, quiet = false } = options;
49
78
 
50
79
  // 1. Explicit file path takes highest priority
51
80
  if (specFile) {
@@ -54,7 +83,13 @@ async function resolveSpec(options = {}) {
54
83
  if (await fs.pathExists(explicitPath)) {
55
84
  const yaml = require('js-yaml');
56
85
  const content = await fs.readFile(explicitPath, 'utf8');
57
- const spec = yaml.load(content);
86
+ let spec;
87
+ try {
88
+ spec = yaml.load(content);
89
+ } catch (yamlError) {
90
+ throw new Error(`Invalid YAML in spec file ${explicitPath}: ${yamlError.message}`);
91
+ }
92
+ validateSpecSchema(spec, explicitPath);
58
93
 
59
94
  return {
60
95
  path: explicitPath,
@@ -74,8 +109,11 @@ async function resolveSpec(options = {}) {
74
109
  const yaml = require('js-yaml');
75
110
  const content = await fs.readFile(featurePath, 'utf8');
76
111
  const spec = yaml.load(content);
112
+ validateSpecSchema(spec, featurePath);
77
113
 
78
- console.log(chalk.green(`Using feature-specific spec: ${specId}`));
114
+ if (!quiet) {
115
+ console.log(chalk.green(`Using feature-specific spec: ${specId}`));
116
+ }
79
117
 
80
118
  return {
81
119
  path: featurePath,
@@ -102,8 +140,11 @@ async function resolveSpec(options = {}) {
102
140
  const yaml = require('js-yaml');
103
141
  const content = await fs.readFile(singleSpecPath, 'utf8');
104
142
  const spec = yaml.load(content);
143
+ validateSpecSchema(spec, singleSpecPath);
105
144
 
106
- console.log(chalk.blue(`Auto-detected single spec: ${singleSpecId}`));
145
+ if (!quiet) {
146
+ console.log(chalk.blue(`Auto-detected single spec: ${singleSpecId}`));
147
+ }
107
148
 
108
149
  return {
109
150
  path: singleSpecPath,
@@ -113,19 +154,23 @@ async function resolveSpec(options = {}) {
113
154
  }
114
155
  } else if (specIds.length > 1) {
115
156
  // Multiple specs - require explicit selection with enhanced guidance
116
- console.error(chalk.red('Multiple specs detected. Please specify which one:'));
157
+ if (!quiet) {
158
+ console.error(chalk.red('Multiple specs detected. Please specify which one:'));
159
+ }
117
160
 
118
161
  // Show specs with details
119
162
  const specsInfo = [];
120
163
  for (const id of specIds) {
121
- const specPath = path.join(SPECS_DIR, registry.specs[id].path);
164
+ const specPath = path.join(getProjectRoot(), SPECS_DIR, registry.specs[id].path);
122
165
  try {
123
166
  const content = await fs.readFile(specPath, 'utf8');
124
167
  let spec;
125
168
  try {
126
169
  spec = yaml.load(content);
127
170
  } catch (yamlError) {
128
- console.log(chalk.yellow(` - ${id} (YAML syntax error: ${yamlError.message})`));
171
+ if (!quiet) {
172
+ console.log(chalk.yellow(` - ${id} (YAML syntax error: ${yamlError.message})`));
173
+ }
129
174
  specsInfo.push({ id, type: 'unknown', status: 'unknown', title: 'YAML error' });
130
175
  continue;
131
176
  }
@@ -135,14 +180,18 @@ async function resolveSpec(options = {}) {
135
180
  status === 'active' ? chalk.green : status === 'completed' ? chalk.blue : chalk.yellow;
136
181
  const typeColor = SPEC_TYPES[type] ? SPEC_TYPES[type].color : chalk.white;
137
182
 
138
- console.log(
139
- chalk.yellow(
140
- ` - ${id} ${typeColor(`(${type})`)} ${statusColor(`[${status}]`)} - ${spec.title || 'Untitled'}`
141
- )
142
- );
183
+ if (!quiet) {
184
+ console.log(
185
+ chalk.yellow(
186
+ ` - ${id} ${typeColor(`(${type})`)} ${statusColor(`[${status}]`)} - ${spec.title || 'Untitled'}`
187
+ )
188
+ );
189
+ }
143
190
  specsInfo.push({ id, type, status, title: spec.title || 'Untitled' });
144
191
  } catch (error) {
145
- console.log(chalk.yellow(` - ${id} (error loading details: ${error.message})`));
192
+ if (!quiet) {
193
+ console.log(chalk.yellow(` - ${id} (error loading details: ${error.message})`));
194
+ }
146
195
  specsInfo.push({ id, type: 'unknown', status: 'unknown', title: 'Error loading' });
147
196
  }
148
197
  }
@@ -157,14 +206,17 @@ async function resolveSpec(options = {}) {
157
206
  specId: selectedSpecId,
158
207
  warnLegacy,
159
208
  interactive: false, // Prevent infinite recursion
209
+ quiet,
160
210
  });
161
211
  } catch (error) {
162
212
  throw new Error(`Interactive selection failed: ${error.message}`);
163
213
  }
164
214
  }
165
215
 
166
- console.log(chalk.blue('\n Usage: caws <command> --spec-id <spec-id>'));
167
- console.log(chalk.gray(` Example: caws validate --spec-id ${specIds[0]}`));
216
+ if (!quiet) {
217
+ console.log(chalk.blue('\n Usage: caws <command> --spec-id <spec-id>'));
218
+ console.log(chalk.gray(` Example: caws validate --spec-id ${specIds[0]}`));
219
+ }
168
220
 
169
221
  // Suggest most likely spec (active first, then by type priority)
170
222
  const priorityOrder = { active: 0, draft: 1, completed: 2 };
@@ -182,11 +234,15 @@ async function resolveSpec(options = {}) {
182
234
  return aTypePriority - bTypePriority;
183
235
  });
184
236
 
185
- console.log(chalk.green('\nQuick suggestion:'));
186
- console.log(chalk.gray(` Try: caws <command> --spec-id ${sortedSpecs[0]}`));
237
+ if (!quiet) {
238
+ console.log(chalk.green('\nQuick suggestion:'));
239
+ console.log(chalk.gray(` Try: caws <command> --spec-id ${sortedSpecs[0]}`));
240
+ }
187
241
 
188
242
  // Interactive mode suggestion
189
- console.log(chalk.blue('\n Interactive mode: caws <command> --interactive-spec-selection'));
243
+ if (!quiet) {
244
+ console.log(chalk.blue('\n Interactive mode: caws <command> --interactive-spec-selection'));
245
+ }
190
246
 
191
247
  throw new Error('Spec ID required when multiple specs exist');
192
248
  }
@@ -198,8 +254,9 @@ async function resolveSpec(options = {}) {
198
254
  const yaml = require('js-yaml');
199
255
  const content = await fs.readFile(legacyPath, 'utf8');
200
256
  const spec = yaml.load(content);
257
+ validateSpecSchema(spec, legacyPath);
201
258
 
202
- if (warnLegacy) {
259
+ if (warnLegacy && !quiet) {
203
260
  console.log(chalk.yellow('Using legacy working-spec.yaml'));
204
261
  console.log(chalk.gray(' For multi-agent workflows, use feature-specific specs:'));
205
262
  console.log(chalk.blue(' caws specs create <feature-id>'));
@@ -236,7 +293,19 @@ async function loadSpecsRegistry() {
236
293
 
237
294
  try {
238
295
  const registry = await fs.readJson(registryPath);
239
- return registry;
296
+ const sanitizedSpecs = {};
297
+
298
+ for (const [id, entry] of Object.entries(registry.specs || {})) {
299
+ const specPath = path.join(getProjectRoot(), SPECS_DIR, entry.path);
300
+ if (await fs.pathExists(specPath)) {
301
+ sanitizedSpecs[id] = entry;
302
+ }
303
+ }
304
+
305
+ return {
306
+ ...registry,
307
+ specs: sanitizedSpecs,
308
+ };
240
309
  } catch (error) {
241
310
  return {
242
311
  version: '1.0.0',
@@ -379,7 +448,10 @@ async function checkScopeConflicts(specIds) {
379
448
 
380
449
  // Load all specs and their scopes
381
450
  for (const id of specIds) {
382
- const specPath = path.join(SPECS_DIR, registry.specs[id].path);
451
+ const entry = registry.specs[id];
452
+ if (!entry) continue;
453
+
454
+ const specPath = path.join(getProjectRoot(), SPECS_DIR, entry.path);
383
455
 
384
456
  try {
385
457
  const content = await fs.readFile(specPath, 'utf8');