@paths.design/caws-cli 7.0.1 → 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 (121) hide show
  1. package/dist/budget-derivation.js +5 -4
  2. package/dist/commands/diagnose.js +26 -20
  3. package/dist/commands/init.js +72 -5
  4. package/dist/commands/specs.js +40 -1
  5. package/dist/commands/status.js +2 -2
  6. package/dist/commands/templates.js +10 -0
  7. package/dist/commands/tool.js +2 -3
  8. package/dist/commands/validate.js +12 -0
  9. package/dist/config/index.js +17 -8
  10. package/dist/generators/working-spec.js +42 -9
  11. package/dist/index.js +3 -1
  12. package/dist/scaffold/cursor-hooks.js +10 -2
  13. package/dist/scaffold/git-hooks.js +189 -32
  14. package/dist/scaffold/index.js +105 -17
  15. package/dist/templates/.caws/tools/README.md +20 -0
  16. package/dist/templates/.cursor/README.md +311 -0
  17. package/dist/templates/.cursor/hooks/audit.sh +55 -0
  18. package/dist/templates/.cursor/hooks/block-dangerous.sh +83 -0
  19. package/dist/templates/.cursor/hooks/caws-quality-check.sh +52 -0
  20. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +130 -0
  21. package/dist/templates/.cursor/hooks/caws-tool-validation.sh +121 -0
  22. package/dist/templates/.cursor/hooks/format.sh +38 -0
  23. package/dist/templates/.cursor/hooks/naming-check.sh +64 -0
  24. package/dist/templates/.cursor/hooks/scan-secrets.sh +46 -0
  25. package/dist/templates/.cursor/hooks/scope-guard.sh +52 -0
  26. package/dist/templates/.cursor/hooks/validate-spec.sh +83 -0
  27. package/dist/templates/.cursor/hooks.json +59 -0
  28. package/dist/templates/.cursor/rules/00-claims-verification.mdc +144 -0
  29. package/dist/templates/.cursor/rules/01-working-style.mdc +50 -0
  30. package/dist/templates/.cursor/rules/02-quality-gates.mdc +370 -0
  31. package/dist/templates/.cursor/rules/03-naming-and-refactor.mdc +33 -0
  32. package/dist/templates/.cursor/rules/04-logging-language-style.mdc +23 -0
  33. package/dist/templates/.cursor/rules/05-safe-defaults-guards.mdc +23 -0
  34. package/dist/templates/.cursor/rules/06-typescript-conventions.mdc +36 -0
  35. package/dist/templates/.cursor/rules/07-process-ops.mdc +20 -0
  36. package/dist/templates/.cursor/rules/08-solid-and-architecture.mdc +16 -0
  37. package/dist/templates/.cursor/rules/09-docstrings.mdc +89 -0
  38. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +390 -0
  39. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +385 -0
  40. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +516 -0
  41. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +588 -0
  42. package/dist/templates/.cursor/rules/README.md +148 -0
  43. package/dist/templates/.github/copilot/instructions.md +311 -0
  44. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +5 -0
  45. package/dist/templates/.idea/runConfigurations/CAWS_Validate.xml +5 -0
  46. package/dist/templates/.vscode/launch.json +56 -0
  47. package/dist/templates/.vscode/settings.json +93 -0
  48. package/dist/templates/.windsurf/workflows/caws-guided-development.md +92 -0
  49. package/dist/templates/COMMIT_CONVENTIONS.md +86 -0
  50. package/dist/templates/OIDC_SETUP.md +300 -0
  51. package/dist/templates/agents.md +1047 -0
  52. package/dist/templates/codemod/README.md +1 -0
  53. package/dist/templates/codemod/test.js +93 -0
  54. package/dist/templates/docs/README.md +150 -0
  55. package/dist/templates/scripts/quality-gates/check-god-objects.js +146 -0
  56. package/dist/templates/scripts/quality-gates/run-quality-gates.js +50 -0
  57. package/dist/templates/scripts/v3/analysis/todo_analyzer.py +1997 -0
  58. package/dist/tool-loader.js +6 -1
  59. package/dist/tool-validator.js +8 -2
  60. package/dist/utils/detection.js +34 -6
  61. package/dist/utils/git-lock.js +118 -0
  62. package/dist/utils/gitignore-updater.js +148 -0
  63. package/dist/utils/quality-gates.js +47 -7
  64. package/dist/utils/spec-resolver.js +23 -3
  65. package/dist/utils/yaml-validation.js +155 -0
  66. package/dist/validation/spec-validation.js +105 -2
  67. package/package.json +2 -2
  68. package/templates/.caws/schemas/waivers.schema.json +30 -0
  69. package/templates/.caws/schemas/working-spec.schema.json +133 -0
  70. package/templates/.caws/templates/working-spec.template.yml +74 -0
  71. package/templates/.caws/tools/README.md +20 -0
  72. package/templates/.caws/tools/scope-guard.js +208 -0
  73. package/templates/.caws/tools-allow.json +331 -0
  74. package/templates/.caws/waivers.yml +19 -0
  75. package/templates/.cursor/hooks/scope-guard.sh +2 -2
  76. package/templates/.cursor/hooks/validate-spec.sh +42 -7
  77. package/templates/apps/tools/caws/COMPLETION_REPORT.md +0 -331
  78. package/templates/apps/tools/caws/MIGRATION_SUMMARY.md +0 -360
  79. package/templates/apps/tools/caws/README.md +0 -463
  80. package/templates/apps/tools/caws/TEST_STATUS.md +0 -365
  81. package/templates/apps/tools/caws/attest.js +0 -357
  82. package/templates/apps/tools/caws/ci-optimizer.js +0 -642
  83. package/templates/apps/tools/caws/config.ts +0 -245
  84. package/templates/apps/tools/caws/cross-functional.js +0 -876
  85. package/templates/apps/tools/caws/dashboard.js +0 -1112
  86. package/templates/apps/tools/caws/flake-detector.ts +0 -362
  87. package/templates/apps/tools/caws/gates.js +0 -198
  88. package/templates/apps/tools/caws/gates.ts +0 -271
  89. package/templates/apps/tools/caws/language-adapters.ts +0 -381
  90. package/templates/apps/tools/caws/language-support.d.ts +0 -367
  91. package/templates/apps/tools/caws/language-support.d.ts.map +0 -1
  92. package/templates/apps/tools/caws/language-support.js +0 -585
  93. package/templates/apps/tools/caws/legacy-assessment.ts +0 -408
  94. package/templates/apps/tools/caws/legacy-assessor.js +0 -764
  95. package/templates/apps/tools/caws/mutant-analyzer.js +0 -734
  96. package/templates/apps/tools/caws/perf-budgets.ts +0 -349
  97. package/templates/apps/tools/caws/prompt-lint.js.backup +0 -274
  98. package/templates/apps/tools/caws/property-testing.js +0 -707
  99. package/templates/apps/tools/caws/provenance.d.ts +0 -14
  100. package/templates/apps/tools/caws/provenance.d.ts.map +0 -1
  101. package/templates/apps/tools/caws/provenance.js +0 -132
  102. package/templates/apps/tools/caws/provenance.js.backup +0 -73
  103. package/templates/apps/tools/caws/provenance.ts +0 -211
  104. package/templates/apps/tools/caws/security-provenance.ts +0 -483
  105. package/templates/apps/tools/caws/shared/base-tool.ts +0 -281
  106. package/templates/apps/tools/caws/shared/config-manager.ts +0 -366
  107. package/templates/apps/tools/caws/shared/gate-checker.ts +0 -849
  108. package/templates/apps/tools/caws/shared/types.ts +0 -444
  109. package/templates/apps/tools/caws/shared/validator.ts +0 -305
  110. package/templates/apps/tools/caws/shared/waivers-manager.ts +0 -174
  111. package/templates/apps/tools/caws/spec-test-mapper.ts +0 -391
  112. package/templates/apps/tools/caws/test-quality.js +0 -578
  113. package/templates/apps/tools/caws/validate.js +0 -76
  114. package/templates/apps/tools/caws/validate.ts +0 -228
  115. package/templates/apps/tools/caws/waivers.js +0 -344
  116. /package/{templates/apps/tools/caws → dist/templates/.caws}/schemas/waivers.schema.json +0 -0
  117. /package/{templates/apps/tools/caws → dist/templates/.caws}/schemas/working-spec.schema.json +0 -0
  118. /package/{templates/apps/tools/caws → dist/templates/.caws}/templates/working-spec.template.yml +0 -0
  119. /package/{templates/apps/tools/caws → dist/templates/.caws/tools}/scope-guard.js +0 -0
  120. /package/{templates/apps/tools/caws → dist/templates/.caws}/tools-allow.json +0 -0
  121. /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,
@@ -8,6 +8,25 @@ const fs = require('fs-extra');
8
8
  const path = require('path');
9
9
  const chalk = require('chalk');
10
10
 
11
+ /**
12
+ * Find the package root directory by looking for package.json
13
+ * Works in both development (src/) and production (dist/) scenarios
14
+ * @param {string} startDir - Directory to start searching from (defaults to __dirname)
15
+ * @returns {string} Package root directory path
16
+ */
17
+ function findPackageRoot(startDir = __dirname) {
18
+ let packageRoot = startDir;
19
+ for (let i = 0; i < 5; i++) {
20
+ const packageJsonPath = path.join(packageRoot, 'package.json');
21
+ if (fs.existsSync(packageJsonPath)) {
22
+ return packageRoot;
23
+ }
24
+ packageRoot = path.dirname(packageRoot);
25
+ }
26
+ // Fallback to startDir if package.json not found
27
+ return startDir;
28
+ }
29
+
11
30
  /**
12
31
  * Detect CAWS setup in a directory
13
32
  * @param {string} cwd - Current working directory
@@ -54,9 +73,10 @@ function detectCAWSSetup(cwd = process.cwd()) {
54
73
  const specFiles = files.filter((f) => f.endsWith('-spec.yaml'));
55
74
  const hasMultipleSpecs = specFiles.length > 1;
56
75
 
57
- // Check for tools directory (enhanced setup)
58
- const toolsDir = path.join(cwd, 'apps/tools/caws');
59
- 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);
60
80
 
61
81
  // Determine setup type
62
82
  let setupType = 'basic';
@@ -93,10 +113,17 @@ function detectCAWSSetup(cwd = process.cwd()) {
93
113
 
94
114
  // Check for template directory - try multiple possible locations
95
115
  let templateDir = null;
116
+
117
+ // Find package root using shared utility
118
+ const packageRoot = findPackageRoot(__dirname);
119
+
96
120
  const possibleTemplatePaths = [
97
- // FIRST: Try bundled templates (for npm-installed CLI)
98
- { path: path.resolve(__dirname, '../templates'), source: 'bundled with CLI' },
99
- { path: path.resolve(__dirname, 'templates'), source: 'bundled with CLI (fallback)' },
121
+ // FIRST: Try bundled templates relative to package root (works in dev and global install)
122
+ { path: path.join(packageRoot, 'templates'), source: 'bundled with CLI (package root)' },
123
+ // Fallback: Try relative to current file location (for development)
124
+ { path: path.resolve(__dirname, '../../templates'), source: 'bundled with CLI (dev fallback)' },
125
+ { path: path.resolve(__dirname, '../templates'), source: 'bundled with CLI (legacy fallback)' },
126
+ { path: path.resolve(__dirname, 'templates'), source: 'bundled with CLI (legacy fallback 2)' },
100
127
  // Try relative to current working directory (for monorepo setups)
101
128
  { path: path.resolve(cwd, '../caws-template'), source: 'monorepo parent directory' },
102
129
  { path: path.resolve(cwd, '../../caws-template'), source: 'monorepo grandparent' },
@@ -171,4 +198,5 @@ function detectCAWSSetup(cwd = process.cwd()) {
171
198
 
172
199
  module.exports = {
173
200
  detectCAWSSetup,
201
+ findPackageRoot,
174
202
  };
@@ -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
+