@paths.design/caws-cli 7.0.2 → 7.0.3

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 (117) hide show
  1. package/dist/budget-derivation.js +5 -4
  2. package/dist/commands/diagnose.js +24 -19
  3. package/dist/commands/init.js +51 -4
  4. package/dist/commands/specs.js +40 -1
  5. package/dist/commands/status.js +2 -2
  6. package/dist/commands/tool.js +2 -3
  7. package/dist/config/index.js +17 -8
  8. package/dist/generators/working-spec.js +19 -6
  9. package/dist/scaffold/git-hooks.js +127 -29
  10. package/dist/scaffold/index.js +53 -7
  11. package/dist/templates/.caws/tools/README.md +20 -0
  12. package/dist/templates/.cursor/README.md +311 -0
  13. package/dist/templates/.cursor/hooks/audit.sh +55 -0
  14. package/dist/templates/.cursor/hooks/block-dangerous.sh +83 -0
  15. package/dist/templates/.cursor/hooks/caws-quality-check.sh +52 -0
  16. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +130 -0
  17. package/dist/templates/.cursor/hooks/caws-tool-validation.sh +121 -0
  18. package/dist/templates/.cursor/hooks/format.sh +38 -0
  19. package/dist/templates/.cursor/hooks/naming-check.sh +64 -0
  20. package/dist/templates/.cursor/hooks/scan-secrets.sh +46 -0
  21. package/dist/templates/.cursor/hooks/scope-guard.sh +52 -0
  22. package/dist/templates/.cursor/hooks/validate-spec.sh +83 -0
  23. package/dist/templates/.cursor/hooks.json +59 -0
  24. package/dist/templates/.cursor/rules/00-claims-verification.mdc +144 -0
  25. package/dist/templates/.cursor/rules/01-working-style.mdc +50 -0
  26. package/dist/templates/.cursor/rules/02-quality-gates.mdc +370 -0
  27. package/dist/templates/.cursor/rules/03-naming-and-refactor.mdc +33 -0
  28. package/dist/templates/.cursor/rules/04-logging-language-style.mdc +23 -0
  29. package/dist/templates/.cursor/rules/05-safe-defaults-guards.mdc +23 -0
  30. package/dist/templates/.cursor/rules/06-typescript-conventions.mdc +36 -0
  31. package/dist/templates/.cursor/rules/07-process-ops.mdc +20 -0
  32. package/dist/templates/.cursor/rules/08-solid-and-architecture.mdc +16 -0
  33. package/dist/templates/.cursor/rules/09-docstrings.mdc +89 -0
  34. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +390 -0
  35. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +385 -0
  36. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +516 -0
  37. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +588 -0
  38. package/dist/templates/.cursor/rules/README.md +148 -0
  39. package/dist/templates/.github/copilot/instructions.md +311 -0
  40. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +5 -0
  41. package/dist/templates/.idea/runConfigurations/CAWS_Validate.xml +5 -0
  42. package/dist/templates/.vscode/launch.json +56 -0
  43. package/dist/templates/.vscode/settings.json +93 -0
  44. package/dist/templates/.windsurf/workflows/caws-guided-development.md +92 -0
  45. package/dist/templates/COMMIT_CONVENTIONS.md +86 -0
  46. package/dist/templates/OIDC_SETUP.md +300 -0
  47. package/dist/templates/agents.md +1047 -0
  48. package/dist/templates/codemod/README.md +1 -0
  49. package/dist/templates/codemod/test.js +93 -0
  50. package/dist/templates/docs/README.md +150 -0
  51. package/dist/templates/scripts/quality-gates/check-god-objects.js +146 -0
  52. package/dist/templates/scripts/quality-gates/run-quality-gates.js +50 -0
  53. package/dist/templates/scripts/v3/analysis/todo_analyzer.py +1997 -0
  54. package/dist/tool-loader.js +6 -1
  55. package/dist/tool-validator.js +8 -2
  56. package/dist/utils/detection.js +4 -3
  57. package/dist/utils/git-lock.js +118 -0
  58. package/dist/utils/gitignore-updater.js +148 -0
  59. package/dist/utils/quality-gates.js +47 -7
  60. package/dist/utils/spec-resolver.js +23 -3
  61. package/dist/utils/yaml-validation.js +155 -0
  62. package/dist/validation/spec-validation.js +81 -2
  63. package/package.json +2 -2
  64. package/templates/.caws/schemas/waivers.schema.json +30 -0
  65. package/templates/.caws/schemas/working-spec.schema.json +133 -0
  66. package/templates/.caws/templates/working-spec.template.yml +74 -0
  67. package/templates/.caws/tools/README.md +20 -0
  68. package/templates/.caws/tools/scope-guard.js +208 -0
  69. package/templates/.caws/tools-allow.json +331 -0
  70. package/templates/.caws/waivers.yml +19 -0
  71. package/templates/.cursor/hooks/scope-guard.sh +2 -2
  72. package/templates/.cursor/hooks/validate-spec.sh +42 -7
  73. package/templates/apps/tools/caws/COMPLETION_REPORT.md +0 -331
  74. package/templates/apps/tools/caws/MIGRATION_SUMMARY.md +0 -360
  75. package/templates/apps/tools/caws/README.md +0 -463
  76. package/templates/apps/tools/caws/TEST_STATUS.md +0 -365
  77. package/templates/apps/tools/caws/attest.js +0 -357
  78. package/templates/apps/tools/caws/ci-optimizer.js +0 -642
  79. package/templates/apps/tools/caws/config.ts +0 -245
  80. package/templates/apps/tools/caws/cross-functional.js +0 -876
  81. package/templates/apps/tools/caws/dashboard.js +0 -1112
  82. package/templates/apps/tools/caws/flake-detector.ts +0 -362
  83. package/templates/apps/tools/caws/gates.js +0 -198
  84. package/templates/apps/tools/caws/gates.ts +0 -271
  85. package/templates/apps/tools/caws/language-adapters.ts +0 -381
  86. package/templates/apps/tools/caws/language-support.d.ts +0 -367
  87. package/templates/apps/tools/caws/language-support.d.ts.map +0 -1
  88. package/templates/apps/tools/caws/language-support.js +0 -585
  89. package/templates/apps/tools/caws/legacy-assessment.ts +0 -408
  90. package/templates/apps/tools/caws/legacy-assessor.js +0 -764
  91. package/templates/apps/tools/caws/mutant-analyzer.js +0 -734
  92. package/templates/apps/tools/caws/perf-budgets.ts +0 -349
  93. package/templates/apps/tools/caws/prompt-lint.js.backup +0 -274
  94. package/templates/apps/tools/caws/property-testing.js +0 -707
  95. package/templates/apps/tools/caws/provenance.d.ts +0 -14
  96. package/templates/apps/tools/caws/provenance.d.ts.map +0 -1
  97. package/templates/apps/tools/caws/provenance.js +0 -132
  98. package/templates/apps/tools/caws/provenance.js.backup +0 -73
  99. package/templates/apps/tools/caws/provenance.ts +0 -211
  100. package/templates/apps/tools/caws/security-provenance.ts +0 -483
  101. package/templates/apps/tools/caws/shared/base-tool.ts +0 -281
  102. package/templates/apps/tools/caws/shared/config-manager.ts +0 -366
  103. package/templates/apps/tools/caws/shared/gate-checker.ts +0 -849
  104. package/templates/apps/tools/caws/shared/types.ts +0 -444
  105. package/templates/apps/tools/caws/shared/validator.ts +0 -305
  106. package/templates/apps/tools/caws/shared/waivers-manager.ts +0 -174
  107. package/templates/apps/tools/caws/spec-test-mapper.ts +0 -391
  108. package/templates/apps/tools/caws/test-quality.js +0 -578
  109. package/templates/apps/tools/caws/validate.js +0 -76
  110. package/templates/apps/tools/caws/validate.ts +0 -228
  111. package/templates/apps/tools/caws/waivers.js +0 -344
  112. /package/{templates/apps/tools/caws → dist/templates/.caws}/schemas/waivers.schema.json +0 -0
  113. /package/{templates/apps/tools/caws → dist/templates/.caws}/schemas/working-spec.schema.json +0 -0
  114. /package/{templates/apps/tools/caws → dist/templates/.caws}/templates/working-spec.template.yml +0 -0
  115. /package/{templates/apps/tools/caws → dist/templates/.caws/tools}/scope-guard.js +0 -0
  116. /package/{templates/apps/tools/caws → dist/templates/.caws}/tools-allow.json +0 -0
  117. /package/{templates/apps/tools/caws → dist/templates/.caws}/waivers.yml +0 -0
@@ -19,8 +19,13 @@ const { safeAsync } = require('./error-handler');
19
19
  class ToolLoader extends EventEmitter {
20
20
  constructor(options = {}) {
21
21
  super();
22
+ // Check new location first, fall back to legacy location
23
+ const newToolsDir = path.join(process.cwd(), '.caws/tools');
24
+ const legacyToolsDir = path.join(process.cwd(), 'apps/tools/caws');
25
+ const defaultToolsDir = fs.existsSync(newToolsDir) ? newToolsDir : legacyToolsDir;
26
+
22
27
  this.options = {
23
- toolsDir: options.toolsDir || path.join(process.cwd(), 'apps/tools/caws'),
28
+ toolsDir: options.toolsDir || defaultToolsDir,
24
29
  cacheEnabled: options.cacheEnabled !== false,
25
30
  timeout: options.timeout || 10000,
26
31
  maxTools: options.maxTools || 50,
@@ -15,9 +15,15 @@ const crypto = require('crypto');
15
15
  */
16
16
  class ToolValidator {
17
17
  constructor(options = {}) {
18
+ // Check new location first, fall back to legacy location
19
+ const newAllowlistPath = path.join(process.cwd(), '.caws/tools-allow.json');
20
+ const legacyAllowlistPath = path.join(process.cwd(), 'apps/tools/caws/tools-allow.json');
21
+ const defaultAllowlistPath = fs.existsSync(newAllowlistPath)
22
+ ? newAllowlistPath
23
+ : legacyAllowlistPath;
24
+
18
25
  this.options = {
19
- allowlistPath:
20
- options.allowlistPath || path.join(process.cwd(), 'apps/tools/caws/tools-allow.json'),
26
+ allowlistPath: options.allowlistPath || defaultAllowlistPath,
21
27
  strictMode: options.strictMode !== false,
22
28
  maxFileSize: options.maxFileSize || 1024 * 1024, // 1MB
23
29
  ...options,
@@ -73,9 +73,10 @@ function detectCAWSSetup(cwd = process.cwd()) {
73
73
  const specFiles = files.filter((f) => f.endsWith('-spec.yaml'));
74
74
  const hasMultipleSpecs = specFiles.length > 1;
75
75
 
76
- // Check for tools directory (enhanced setup)
77
- const toolsDir = path.join(cwd, 'apps/tools/caws');
78
- const hasTools = fs.existsSync(toolsDir);
76
+ // Check for tools directory (enhanced setup) - check new location first
77
+ const toolsDir = path.join(cwd, '.caws/tools');
78
+ const legacyToolsDir = path.join(cwd, 'apps/tools/caws');
79
+ const hasTools = fs.existsSync(toolsDir) || fs.existsSync(legacyToolsDir);
79
80
 
80
81
  // Determine setup type
81
82
  let setupType = 'basic';
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @fileoverview Git Lock Detection Utilities
3
+ * Functions for detecting and handling git locks
4
+ * @author @darianrosebrook
5
+ */
6
+
7
+ const fs = require('fs-extra');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Check for git lock files
12
+ * @param {string} projectRoot - Project root directory
13
+ * @returns {Object} Lock status information
14
+ */
15
+ function checkGitLock(projectRoot) {
16
+ const lockFile = path.join(projectRoot, '.git', 'index.lock');
17
+ const headLockFile = path.join(projectRoot, '.git', 'HEAD.lock');
18
+
19
+ const result = {
20
+ locked: false,
21
+ stale: false,
22
+ lockFiles: [],
23
+ message: null,
24
+ suggestion: null,
25
+ };
26
+
27
+ // Check index.lock
28
+ if (fs.existsSync(lockFile)) {
29
+ const lockAge = Date.now() - fs.statSync(lockFile).mtimeMs;
30
+ const lockAgeMinutes = Math.floor(lockAge / 60000);
31
+
32
+ result.locked = true;
33
+ result.lockFiles.push({
34
+ path: '.git/index.lock',
35
+ age: lockAgeMinutes,
36
+ stale: lockAgeMinutes > 5,
37
+ });
38
+
39
+ if (lockAgeMinutes > 5) {
40
+ // Stale lock (older than 5 minutes)
41
+ result.stale = true;
42
+ result.message = `Stale git lock detected (${lockAgeMinutes} minutes old). This may indicate a crashed git process.`;
43
+ result.suggestion = 'Remove stale lock: rm .git/index.lock';
44
+ } else {
45
+ // Active lock
46
+ result.message =
47
+ 'Git lock detected. Another git process may be running.';
48
+ result.suggestion =
49
+ 'Wait for the other process to complete, or check for running git/editor processes';
50
+ }
51
+ }
52
+
53
+ // Check HEAD.lock
54
+ if (fs.existsSync(headLockFile)) {
55
+ const lockAge = Date.now() - fs.statSync(headLockFile).mtimeMs;
56
+ const lockAgeMinutes = Math.floor(lockAge / 60000);
57
+
58
+ result.locked = true;
59
+ result.lockFiles.push({
60
+ path: '.git/HEAD.lock',
61
+ age: lockAgeMinutes,
62
+ stale: lockAgeMinutes > 5,
63
+ });
64
+
65
+ if (lockAgeMinutes > 5) {
66
+ result.stale = true;
67
+ if (!result.message) {
68
+ result.message = `Stale git lock detected (${lockAgeMinutes} minutes old).`;
69
+ result.suggestion = 'Remove stale lock: rm .git/HEAD.lock';
70
+ }
71
+ }
72
+ }
73
+
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Format git lock error message
79
+ * @param {Object} lockStatus - Lock status from checkGitLock
80
+ * @returns {string} Formatted error message
81
+ */
82
+ function formatGitLockError(lockStatus) {
83
+ if (!lockStatus.locked) {
84
+ return null;
85
+ }
86
+
87
+ let message = '⚠️ Git lock detected\n';
88
+ message += ` ${lockStatus.message}\n`;
89
+
90
+ if (lockStatus.lockFiles.length > 0) {
91
+ message += '\n Lock files:\n';
92
+ for (const lockFile of lockStatus.lockFiles) {
93
+ message += ` - ${lockFile.path} (${lockFile.age} minutes old)`;
94
+ if (lockFile.stale) {
95
+ message += ' [STALE]';
96
+ }
97
+ message += '\n';
98
+ }
99
+ }
100
+
101
+ if (lockStatus.suggestion) {
102
+ message += `\n 💡 ${lockStatus.suggestion}\n`;
103
+ }
104
+
105
+ if (lockStatus.stale) {
106
+ message +=
107
+ '\n ⚠️ Warning: Removing stale locks may cause data loss if another process is actually running.\n';
108
+ message += ' Check for running git/editor processes before removing locks.\n';
109
+ }
110
+
111
+ return message;
112
+ }
113
+
114
+ module.exports = {
115
+ checkGitLock,
116
+ formatGitLockError,
117
+ };
118
+
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @fileoverview Gitignore Updater Utility
3
+ * Updates .gitignore to properly handle CAWS runtime files vs source files
4
+ * @author @darianrosebrook
5
+ */
6
+
7
+ const fs = require('fs-extra');
8
+ const path = require('path');
9
+ const chalk = require('chalk');
10
+
11
+ /**
12
+ * CAWS .gitignore entries
13
+ *
14
+ * Strategy: Track shared/collaborative files, ignore local-only runtime data
15
+ *
16
+ * TRACKED (shared with team):
17
+ * - .caws/working-spec.yaml (main spec)
18
+ * - .caws/specs/*.yaml (feature specs)
19
+ * - .caws/policy.yaml (team policy)
20
+ * - .caws/waivers/*.yaml (project-wide waivers)
21
+ * - .caws/provenance/ (audit trails for compliance)
22
+ * - .caws/changes/ (change tracking for team visibility)
23
+ * - .caws/archive/ (archived changes for history)
24
+ * - .caws/plans/*.md (implementation plans)
25
+ *
26
+ * IGNORED (local-only):
27
+ * - .agent/ (agent runtime tracking, local to each developer)
28
+ * - Temporary files (*.tmp, *.bak)
29
+ * - Logs (caws.log, debug logs)
30
+ * - Local overrides (caws.local.*)
31
+ */
32
+ const CAWS_GITIGNORE_ENTRIES = `
33
+ # CAWS Local Runtime Data (developer-specific, should not be tracked)
34
+ # ====================================================================
35
+ # Note: Specs, policy, waivers, provenance, and plans ARE tracked for team collaboration
36
+ # Only local agent tracking, generated tools, and temporary files are ignored
37
+
38
+ # Agent runtime tracking (local to each developer)
39
+ .agent/
40
+
41
+ # CAWS tools (now in .caws/tools/)
42
+ .caws/tools/
43
+ # Legacy location (for backward compatibility)
44
+ apps/tools/caws/
45
+
46
+ # Temporary CAWS files
47
+ **/*.caws.tmp
48
+ **/*.working-spec.bak
49
+ .caws/*.tmp
50
+ .caws/*.bak
51
+
52
+ # CAWS logs (local debugging)
53
+ caws-debug.log*
54
+ **/caws.log
55
+ .caws/*.log
56
+
57
+ # Local development overrides (developer-specific)
58
+ caws.local.*
59
+ .caws/local.*
60
+ `;
61
+
62
+ /**
63
+ * Update .gitignore to include CAWS runtime file exclusions
64
+ * @param {string} projectRoot - Project root directory
65
+ * @param {Object} options - Options
66
+ * @param {boolean} options.force - Force update even if entries exist
67
+ * @returns {Promise<boolean>} Whether .gitignore was updated
68
+ */
69
+ async function updateGitignore(projectRoot, options = {}) {
70
+ const { force = false } = options;
71
+ const gitignorePath = path.join(projectRoot, '.gitignore');
72
+
73
+ try {
74
+ // Read existing .gitignore or create empty
75
+ let existingContent = '';
76
+ if (await fs.pathExists(gitignorePath)) {
77
+ existingContent = await fs.readFile(gitignorePath, 'utf8');
78
+ }
79
+
80
+ // Check if CAWS entries already exist (check for either old or new header)
81
+ const hasCawsEntries =
82
+ existingContent.includes('# CAWS Local Runtime Data') ||
83
+ existingContent.includes('# CAWS Runtime Data');
84
+
85
+ if (hasCawsEntries && !force) {
86
+ // Already has CAWS entries, skip
87
+ return false;
88
+ }
89
+
90
+ // If old entries exist, replace them with new ones
91
+ if (existingContent.includes('# CAWS Runtime Data') && force) {
92
+ // Remove old CAWS entries (between "# CAWS Runtime Data" and next major section)
93
+ const lines = existingContent.split('\n');
94
+ const startIndex = lines.findIndex((line) => line.includes('# CAWS Runtime Data'));
95
+ if (startIndex !== -1) {
96
+ // Find the end of CAWS section (next major section starting with #)
97
+ let endIndex = startIndex + 1;
98
+ while (
99
+ endIndex < lines.length &&
100
+ (lines[endIndex].trim() === '' ||
101
+ lines[endIndex].startsWith('#') ||
102
+ lines[endIndex].startsWith('.caws/') ||
103
+ lines[endIndex].startsWith('.agent/') ||
104
+ lines[endIndex].includes('caws') ||
105
+ lines[endIndex].includes('CAWS'))
106
+ ) {
107
+ endIndex++;
108
+ }
109
+ // Remove old section and insert new one
110
+ const before = lines.slice(0, startIndex).join('\n');
111
+ const after = lines.slice(endIndex).join('\n');
112
+ existingContent = [before, after].filter(Boolean).join('\n');
113
+ }
114
+ }
115
+
116
+ // Append CAWS entries
117
+ const updatedContent = existingContent.trim() + '\n' + CAWS_GITIGNORE_ENTRIES.trim() + '\n';
118
+
119
+ await fs.writeFile(gitignorePath, updatedContent, 'utf8');
120
+
121
+ return true;
122
+ } catch (error) {
123
+ console.warn(chalk.yellow(`⚠️ Could not update .gitignore: ${error.message}`));
124
+ return false;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Verify .gitignore has proper CAWS entries
130
+ * @param {string} projectRoot - Project root directory
131
+ * @returns {Promise<boolean>} Whether .gitignore has CAWS entries
132
+ */
133
+ async function verifyGitignore(projectRoot) {
134
+ const gitignorePath = path.join(projectRoot, '.gitignore');
135
+
136
+ if (!(await fs.pathExists(gitignorePath))) {
137
+ return false;
138
+ }
139
+
140
+ const content = await fs.readFile(gitignorePath, 'utf8');
141
+ return content.includes('# CAWS Local Runtime Data') || content.includes('# CAWS Runtime Data');
142
+ }
143
+
144
+ module.exports = {
145
+ updateGitignore,
146
+ verifyGitignore,
147
+ CAWS_GITIGNORE_ENTRIES,
148
+ };
@@ -196,18 +196,58 @@ function checkHiddenTodos(stagedFiles) {
196
196
  console.log(`📁 Found ${supportedFiles.length} staged files to analyze for TODOs`);
197
197
 
198
198
  try {
199
- // Check if TODO analyzer exists
200
- const analyzerPath = path.join(process.cwd(), 'scripts/v3/analysis/todo_analyzer.py');
201
- if (!fs.existsSync(analyzerPath)) {
199
+ // Find TODO analyzer .mjs file (preferred - no Python dependency)
200
+ const possiblePaths = [
201
+ // Published npm package (priority)
202
+ path.join(
203
+ process.cwd(),
204
+ 'node_modules',
205
+ '@paths.design',
206
+ 'quality-gates',
207
+ 'todo-analyzer.mjs'
208
+ ),
209
+ // Legacy monorepo local copy
210
+ path.join(process.cwd(), 'node_modules', '@caws', 'quality-gates', 'todo-analyzer.mjs'),
211
+ // Monorepo structure (development)
212
+ path.join(process.cwd(), 'packages', 'quality-gates', 'todo-analyzer.mjs'),
213
+ // Local copy in scripts directory (if scaffolded)
214
+ path.join(process.cwd(), 'scripts', 'todo-analyzer.mjs'),
215
+ // Legacy Python analyzer (deprecated)
216
+ path.join(process.cwd(), 'scripts', 'v3', 'analysis', 'todo_analyzer.py'),
217
+ ];
218
+
219
+ let analyzerPath = null;
220
+ let usePython = false;
221
+
222
+ for (const testPath of possiblePaths) {
223
+ if (fs.existsSync(testPath)) {
224
+ analyzerPath = testPath;
225
+ usePython = testPath.endsWith('.py');
226
+ break;
227
+ }
228
+ }
229
+
230
+ if (!analyzerPath) {
202
231
  console.warn('⚠️ TODO analyzer not found - skipping TODO analysis');
232
+ console.warn(
233
+ '💡 Install @paths.design/quality-gates: npm install --save-dev @paths.design/quality-gates'
234
+ );
203
235
  return { todos: [], blocking: 0, total: 0 };
204
236
  }
205
237
 
238
+ if (usePython) {
239
+ console.warn('⚠️ Using legacy Python TODO analyzer (deprecated)');
240
+ console.warn(
241
+ '💡 Install @paths.design/quality-gates for Node.js version: npm install --save-dev @paths.design/quality-gates'
242
+ );
243
+ }
244
+
206
245
  // Run the TODO analyzer with staged files
207
- const result = execSync(
208
- `python3 ${analyzerPath} --staged-only --min-confidence ${CONFIG.todoConfidenceThreshold}`,
209
- { encoding: 'utf8', cwd: process.cwd() }
210
- );
246
+ const command = usePython
247
+ ? `python3 ${analyzerPath} --staged-only --min-confidence ${CONFIG.todoConfidenceThreshold}`
248
+ : `node ${analyzerPath} --staged-only --ci-mode --min-confidence ${CONFIG.todoConfidenceThreshold}`;
249
+
250
+ const result = execSync(command, { encoding: 'utf8', cwd: process.cwd() });
211
251
 
212
252
  // Parse the output to extract TODO count
213
253
  const lines = result.split('\n');
@@ -108,7 +108,14 @@ async function resolveSpec(options = {}) {
108
108
  const specPath = path.join(SPECS_DIR, registry.specs[id].path);
109
109
  try {
110
110
  const content = await fs.readFile(specPath, 'utf8');
111
- const spec = yaml.load(content);
111
+ let spec;
112
+ try {
113
+ spec = yaml.load(content);
114
+ } catch (yamlError) {
115
+ console.log(chalk.yellow(` - ${id} (YAML syntax error: ${yamlError.message})`));
116
+ specsInfo.push({ id, type: 'unknown', status: 'unknown', title: 'YAML error' });
117
+ continue;
118
+ }
112
119
  const status = spec.status || 'draft';
113
120
  const type = spec.type || 'feature';
114
121
  const statusColor =
@@ -122,7 +129,7 @@ async function resolveSpec(options = {}) {
122
129
  );
123
130
  specsInfo.push({ id, type, status, title: spec.title || 'Untitled' });
124
131
  } catch (error) {
125
- console.log(chalk.yellow(` - ${id} (error loading details)`));
132
+ console.log(chalk.yellow(` - ${id} (error loading details: ${error.message})`));
126
133
  specsInfo.push({ id, type: 'unknown', status: 'unknown', title: 'Error loading' });
127
134
  }
128
135
  }
@@ -363,7 +370,20 @@ async function checkScopeConflicts(specIds) {
363
370
 
364
371
  try {
365
372
  const content = await fs.readFile(specPath, 'utf8');
366
- const spec = yaml.load(content);
373
+ let spec;
374
+ try {
375
+ spec = yaml.load(content);
376
+ } catch (yamlError) {
377
+ const relativePath = path.relative(process.cwd(), specPath);
378
+ throw new Error(
379
+ `Invalid YAML syntax in ${relativePath}: ${yamlError.message}\n` +
380
+ (yamlError.mark
381
+ ? ` Line ${yamlError.mark.line + 1}, Column ${yamlError.mark.column + 1}\n`
382
+ : '') +
383
+ (yamlError.mark?.snippet ? ` ${yamlError.mark.snippet}\n` : '') +
384
+ `💡 Fix YAML syntax errors or use 'caws specs create <id>' for proper structure`
385
+ );
386
+ }
367
387
 
368
388
  specScopes.push({
369
389
  id,
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @fileoverview YAML Validation Utilities
3
+ * Functions for validating YAML syntax and structure
4
+ * @author @darianrosebrook
5
+ */
6
+
7
+ const yaml = require('js-yaml');
8
+ const fs = require('fs-extra');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Validate YAML syntax for a file
13
+ * @param {string} filePath - Path to YAML file
14
+ * @returns {Object} Validation result with valid flag and error details
15
+ */
16
+ function validateYamlSyntax(filePath) {
17
+ try {
18
+ if (!fs.existsSync(filePath)) {
19
+ return {
20
+ valid: false,
21
+ error: `File not found: ${filePath}`,
22
+ line: null,
23
+ column: null,
24
+ };
25
+ }
26
+
27
+ const content = fs.readFileSync(filePath, 'utf8');
28
+ yaml.load(content); // Will throw if invalid
29
+
30
+ return { valid: true };
31
+ } catch (error) {
32
+ return {
33
+ valid: false,
34
+ error: error.message,
35
+ line: error.mark?.line ? error.mark.line + 1 : null, // Convert to 1-based
36
+ column: error.mark?.column ? error.mark.column + 1 : null, // Convert to 1-based
37
+ snippet: error.mark?.snippet || null,
38
+ };
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Validate YAML syntax for multiple files
44
+ * @param {string[]} filePaths - Array of file paths to validate
45
+ * @returns {Object} Validation results with summary
46
+ */
47
+ function validateYamlFiles(filePaths) {
48
+ const results = {
49
+ valid: true,
50
+ files: [],
51
+ errors: [],
52
+ };
53
+
54
+ for (const filePath of filePaths) {
55
+ const validation = validateYamlSyntax(filePath);
56
+ const relativePath = path.relative(process.cwd(), filePath);
57
+
58
+ results.files.push({
59
+ path: relativePath,
60
+ ...validation,
61
+ });
62
+
63
+ if (!validation.valid) {
64
+ results.valid = false;
65
+ results.errors.push({
66
+ file: relativePath,
67
+ error: validation.error,
68
+ line: validation.line,
69
+ column: validation.column,
70
+ snippet: validation.snippet,
71
+ });
72
+ }
73
+ }
74
+
75
+ return results;
76
+ }
77
+
78
+ /**
79
+ * Find all YAML files in .caws directory
80
+ * @param {string} projectRoot - Project root directory
81
+ * @returns {string[]} Array of YAML file paths
82
+ */
83
+ function findCawsYamlFiles(projectRoot) {
84
+ const cawsDir = path.join(projectRoot, '.caws');
85
+ const yamlFiles = [];
86
+
87
+ if (!fs.existsSync(cawsDir)) {
88
+ return yamlFiles;
89
+ }
90
+
91
+ function walkDir(dir) {
92
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
93
+
94
+ for (const entry of entries) {
95
+ const fullPath = path.join(dir, entry.name);
96
+
97
+ if (entry.isDirectory()) {
98
+ walkDir(fullPath);
99
+ } else if (
100
+ entry.isFile() &&
101
+ (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml'))
102
+ ) {
103
+ yamlFiles.push(fullPath);
104
+ }
105
+ }
106
+ }
107
+
108
+ walkDir(cawsDir);
109
+ return yamlFiles;
110
+ }
111
+
112
+ /**
113
+ * Validate all CAWS YAML files in project
114
+ * @param {string} projectRoot - Project root directory
115
+ * @returns {Object} Validation results
116
+ */
117
+ function validateAllCawsYamlFiles(projectRoot) {
118
+ const yamlFiles = findCawsYamlFiles(projectRoot);
119
+ return validateYamlFiles(yamlFiles);
120
+ }
121
+
122
+ /**
123
+ * Format validation error for display
124
+ * @param {Object} error - Error object from validateYamlSyntax
125
+ * @param {string} filePath - File path
126
+ * @returns {string} Formatted error message
127
+ */
128
+ function formatYamlError(error, filePath) {
129
+ const relativePath = path.relative(process.cwd(), filePath);
130
+ let message = `❌ Invalid YAML in ${relativePath}\n`;
131
+ message += ` Error: ${error.error}\n`;
132
+
133
+ if (error.line !== null) {
134
+ message += ` Line: ${error.line}`;
135
+ if (error.column !== null) {
136
+ message += `, Column: ${error.column}`;
137
+ }
138
+ message += '\n';
139
+ }
140
+
141
+ if (error.snippet) {
142
+ message += ` ${error.snippet}\n`;
143
+ }
144
+
145
+ return message;
146
+ }
147
+
148
+ module.exports = {
149
+ validateYamlSyntax,
150
+ validateYamlFiles,
151
+ findCawsYamlFiles,
152
+ validateAllCawsYamlFiles,
153
+ formatYamlError,
154
+ };
155
+
@@ -238,6 +238,42 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
238
238
  }
239
239
  }
240
240
 
241
+ // Validate scope.out doesn't contain glob patterns
242
+ if (spec.scope && spec.scope.out && Array.isArray(spec.scope.out)) {
243
+ const globPatterns = spec.scope.out.filter(
244
+ (pattern) => pattern.includes('*') || pattern.includes('?')
245
+ );
246
+ if (globPatterns.length > 0) {
247
+ errors.push({
248
+ instancePath: '/scope/out',
249
+ message: `Unsupported glob patterns in scope.out: ${globPatterns.join(', ')}`,
250
+ suggestion:
251
+ 'Use directory paths only (e.g., __pycache__/ instead of *.pyc or **/*.pyc). Python cache files are already covered by __pycache__/',
252
+ canAutoFix: true,
253
+ });
254
+
255
+ // Auto-fix: remove glob patterns and keep only directory paths
256
+ if (autoFix) {
257
+ const fixedOut = spec.scope.out
258
+ .filter((pattern) => !pattern.includes('*') && !pattern.includes('?'))
259
+ .map((pattern) => {
260
+ // Ensure directory paths end with /
261
+ if (!pattern.includes('.') && !pattern.endsWith('/')) {
262
+ return pattern + '/';
263
+ }
264
+ return pattern;
265
+ });
266
+
267
+ fixes.push({
268
+ field: 'scope.out',
269
+ value: fixedOut,
270
+ description: `Removed glob patterns from scope.out: ${globPatterns.join(', ')}`,
271
+ reason: 'Glob patterns are not supported in scope.out',
272
+ });
273
+ }
274
+ }
275
+ }
276
+
241
277
  // Auto-fix missing scope.out
242
278
  if (spec.scope && !spec.scope.out) {
243
279
  fixes.push({
@@ -329,7 +365,7 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
329
365
  const suggestion = isChoreMode
330
366
  ? 'For infrastructure/setup work, add a minimal project_setup contract or create a waiver'
331
367
  : 'Add API contracts (OpenAPI, GraphQL, etc.) or change mode to "chore" for maintenance work';
332
-
368
+
333
369
  errors.push({
334
370
  instancePath: '/contracts',
335
371
  message: `Contracts required for Tier ${spec.risk_tier} changes`,
@@ -341,7 +377,8 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
341
377
  {
342
378
  type: 'project_setup',
343
379
  path: '.caws/working-spec.yaml',
344
- description: 'Project-level CAWS configuration. Feature-specific contracts will be added as features are developed.',
380
+ description:
381
+ 'Project-level CAWS configuration. Feature-specific contracts will be added as features are developed.',
345
382
  },
346
383
  ],
347
384
  }
@@ -392,6 +429,48 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
392
429
  }
393
430
  }
394
431
 
432
+ // Validate rollback format if present (for all tiers)
433
+ if (spec.rollback !== undefined) {
434
+ if (!Array.isArray(spec.rollback)) {
435
+ errors.push({
436
+ instancePath: '/rollback',
437
+ message: 'rollback must be an array of strings',
438
+ suggestion: 'Use format: ["Step 1", "Step 2", "Step 3"]',
439
+ canAutoFix: false,
440
+ });
441
+ } else {
442
+ // Check for duplicates
443
+ const uniqueSteps = [...new Set(spec.rollback)];
444
+ if (uniqueSteps.length !== spec.rollback.length) {
445
+ warnings.push({
446
+ instancePath: '/rollback',
447
+ message: 'Duplicate entries found in rollback array',
448
+ suggestion: 'Remove duplicate entries',
449
+ });
450
+
451
+ if (autoFix) {
452
+ fixes.push({
453
+ field: 'rollback',
454
+ value: uniqueSteps,
455
+ description: 'Removed duplicate rollback entries',
456
+ reason: 'Duplicate entries detected',
457
+ });
458
+ }
459
+ }
460
+
461
+ // Validate each entry is a string
462
+ const invalidEntries = spec.rollback.filter((entry) => typeof entry !== 'string');
463
+ if (invalidEntries.length > 0) {
464
+ errors.push({
465
+ instancePath: '/rollback',
466
+ message: `Invalid rollback entries (must be strings): ${invalidEntries.length}`,
467
+ suggestion: 'All rollback entries must be string descriptions',
468
+ canAutoFix: false,
469
+ });
470
+ }
471
+ }
472
+ }
473
+
395
474
  // Validate waiver_ids format if present
396
475
  if (spec.waiver_ids) {
397
476
  if (!Array.isArray(spec.waiver_ids)) {