@paths.design/caws-cli 8.1.0 → 8.2.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 (78) hide show
  1. package/README.md +5 -6
  2. package/dist/commands/archive.d.ts +1 -1
  3. package/dist/commands/archive.d.ts.map +1 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +185 -39
  6. package/dist/commands/mode.d.ts +2 -1
  7. package/dist/commands/mode.d.ts.map +1 -1
  8. package/dist/commands/provenance.d.ts.map +1 -1
  9. package/dist/commands/specs.d.ts.map +1 -1
  10. package/dist/commands/worktree.d.ts +7 -0
  11. package/dist/commands/worktree.d.ts.map +1 -0
  12. package/dist/commands/worktree.js +136 -0
  13. package/dist/config/lite-scope.d.ts +33 -0
  14. package/dist/config/lite-scope.d.ts.map +1 -0
  15. package/dist/config/lite-scope.js +158 -0
  16. package/dist/config/modes.d.ts +90 -51
  17. package/dist/config/modes.d.ts.map +1 -1
  18. package/dist/config/modes.js +26 -0
  19. package/dist/error-handler.d.ts +3 -16
  20. package/dist/error-handler.d.ts.map +1 -1
  21. package/dist/generators/jest-config-generator.d.ts +32 -0
  22. package/dist/generators/jest-config-generator.d.ts.map +1 -0
  23. package/dist/index.js +36 -0
  24. package/dist/scaffold/claude-hooks.d.ts +28 -0
  25. package/dist/scaffold/claude-hooks.d.ts.map +1 -0
  26. package/dist/scaffold/claude-hooks.js +28 -0
  27. package/dist/scaffold/index.d.ts +2 -0
  28. package/dist/scaffold/index.d.ts.map +1 -1
  29. package/dist/scaffold/index.js +90 -88
  30. package/dist/templates/.caws/schemas/scope.schema.json +52 -0
  31. package/dist/templates/.caws/schemas/working-spec.schema.json +1 -1
  32. package/dist/templates/.caws/schemas/worktrees.schema.json +36 -0
  33. package/dist/templates/.claude/hooks/block-dangerous.sh +33 -0
  34. package/dist/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
  35. package/dist/templates/.claude/hooks/scope-guard.sh +93 -6
  36. package/dist/templates/.claude/hooks/simplification-guard.sh +92 -0
  37. package/dist/templates/.cursor/README.md +0 -3
  38. package/dist/templates/.github/copilot-instructions.md +82 -0
  39. package/dist/templates/.junie/guidelines.md +73 -0
  40. package/dist/templates/.vscode/launch.json +0 -27
  41. package/dist/templates/.windsurf/rules/caws-quality-standards.md +54 -0
  42. package/dist/templates/CLAUDE.md +101 -0
  43. package/dist/templates/agents.md +73 -1016
  44. package/dist/templates/docs/README.md +5 -5
  45. package/dist/test-analysis.d.ts +50 -1
  46. package/dist/test-analysis.d.ts.map +1 -1
  47. package/dist/utils/error-categories.d.ts +52 -0
  48. package/dist/utils/error-categories.d.ts.map +1 -0
  49. package/dist/utils/gitignore-updater.d.ts +1 -1
  50. package/dist/utils/gitignore-updater.d.ts.map +1 -1
  51. package/dist/utils/gitignore-updater.js +4 -0
  52. package/dist/utils/ide-detection.js +133 -0
  53. package/dist/utils/quality-gates-utils.d.ts +49 -0
  54. package/dist/utils/quality-gates-utils.d.ts.map +1 -0
  55. package/dist/utils/typescript-detector.d.ts +8 -5
  56. package/dist/utils/typescript-detector.d.ts.map +1 -1
  57. package/dist/validation/spec-validation.d.ts.map +1 -1
  58. package/dist/worktree/worktree-manager.d.ts +54 -0
  59. package/dist/worktree/worktree-manager.d.ts.map +1 -0
  60. package/dist/worktree/worktree-manager.js +378 -0
  61. package/package.json +5 -1
  62. package/templates/.caws/schemas/scope.schema.json +52 -0
  63. package/templates/.caws/schemas/working-spec.schema.json +1 -1
  64. package/templates/.caws/schemas/worktrees.schema.json +36 -0
  65. package/templates/.claude/hooks/block-dangerous.sh +33 -0
  66. package/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
  67. package/templates/.claude/hooks/scope-guard.sh +93 -6
  68. package/templates/.claude/hooks/simplification-guard.sh +92 -0
  69. package/templates/.cursor/README.md +0 -3
  70. package/templates/.github/copilot-instructions.md +82 -0
  71. package/templates/.junie/guidelines.md +73 -0
  72. package/templates/.vscode/launch.json +0 -27
  73. package/templates/.windsurf/rules/caws-quality-standards.md +54 -0
  74. package/templates/AGENTS.md +104 -0
  75. package/templates/CLAUDE.md +101 -0
  76. package/templates/docs/README.md +5 -5
  77. package/templates/.github/copilot/instructions.md +0 -311
  78. package/templates/agents.md +0 -1047
@@ -19,6 +19,9 @@ const { updateGitignore } = require('../utils/gitignore-updater');
19
19
  // Import Claude Code hooks scaffolding
20
20
  const { scaffoldClaudeHooks } = require('./claude-hooks');
21
21
 
22
+ // Import IDE detection utilities
23
+ const { IDE_REGISTRY, parseIDESelection, getRecommendedIDEs } = require('../utils/ide-detection');
24
+
22
25
  // CLI version from package.json
23
26
  const CLI_VERSION = require('../../package.json').version;
24
27
 
@@ -55,12 +58,20 @@ function findTemplateDir() {
55
58
  async function scaffoldIDEIntegrations(targetDir, options) {
56
59
  const templateDir = findTemplateDir() || path.join(__dirname, '../../templates');
57
60
 
58
- console.log(chalk.cyan('🎨 Setting up IDE integrations...'));
61
+ // Determine which IDEs to install
62
+ const selectedIDEs = options.ides || [];
63
+ if (selectedIDEs.length === 0) {
64
+ console.log(chalk.gray('Skipping IDE setup (none selected)'));
65
+ return { added: 0, skipped: 0 };
66
+ }
67
+
68
+ const ideNames = selectedIDEs.map((id) => IDE_REGISTRY[id]?.name || id).join(', ');
69
+ console.log(chalk.cyan(`Setting up IDE integrations: ${ideNames}`));
59
70
 
60
71
  let addedCount = 0;
61
72
  let skippedCount = 0;
62
73
 
63
- // Setup git hooks with provenance integration
74
+ // Setup git hooks with provenance integration (always -- not IDE-specific)
64
75
  try {
65
76
  const gitHooksResult = await scaffoldGitHooks(targetDir, {
66
77
  provenance: true,
@@ -72,71 +83,63 @@ async function scaffoldIDEIntegrations(targetDir, options) {
72
83
  addedCount += gitHooksResult.added;
73
84
  skippedCount += gitHooksResult.skipped;
74
85
  } catch (error) {
75
- console.log(chalk.yellow(`⚠️ Git hooks setup failed: ${error.message}`));
86
+ console.log(chalk.yellow(`Warning: Git hooks setup failed: ${error.message}`));
76
87
  }
77
88
 
78
- // List of IDE integration templates to copy
79
- const ideTemplates = [
80
- // VS Code
81
- {
82
- src: '.vscode/settings.json',
83
- dest: '.vscode/settings.json',
84
- desc: 'VS Code workspace settings',
85
- },
86
- {
87
- src: '.vscode/launch.json',
88
- dest: '.vscode/launch.json',
89
- desc: 'VS Code debug configurations',
90
- },
91
-
92
- // IntelliJ IDEA
93
- {
94
- src: '.idea/runConfigurations/CAWS_Validate.xml',
95
- dest: '.idea/runConfigurations/CAWS_Validate.xml',
96
- desc: 'IntelliJ run configuration for CAWS validate',
97
- },
98
- {
99
- src: '.idea/runConfigurations/CAWS_Evaluate.xml',
100
- dest: '.idea/runConfigurations/CAWS_Evaluate.xml',
101
- desc: 'IntelliJ run configuration for CAWS evaluate',
102
- },
103
-
104
- // Windsurf
105
- {
106
- src: '.windsurf/workflows/caws-guided-development.md',
107
- dest: '.windsurf/workflows/caws-guided-development.md',
108
- desc: 'Windsurf workflow for CAWS-guided development',
109
- },
110
-
111
- // GitHub Copilot
112
- {
113
- src: '.github/copilot/instructions.md',
114
- dest: '.github/copilot/instructions.md',
115
- desc: 'GitHub Copilot CAWS integration instructions',
116
- },
117
-
118
- // Git hooks are handled separately by scaffoldGitHooks
119
-
120
- // Cursor hooks (already handled by scaffoldCursorHooks, but ensure README is copied)
121
- {
122
- src: '.cursor/README.md',
123
- dest: '.cursor/README.md',
124
- desc: 'Cursor integration documentation',
125
- },
126
-
127
- // Claude Code hooks
128
- {
129
- src: '.claude/README.md',
130
- dest: '.claude/README.md',
131
- desc: 'Claude Code integration documentation',
132
- },
133
- ];
89
+ // Build IDE templates list dynamically based on selection
90
+ const ideTemplates = [];
134
91
 
135
- // Setup Claude Code hooks
136
- try {
137
- await scaffoldClaudeHooks(targetDir, ['safety', 'quality', 'scope', 'audit']);
138
- } catch (error) {
139
- console.log(chalk.yellow(`⚠️ Claude Code hooks setup failed: ${error.message}`));
92
+ if (selectedIDEs.includes('vscode')) {
93
+ ideTemplates.push(
94
+ { src: '.vscode/settings.json', dest: '.vscode/settings.json', desc: 'VS Code workspace settings' },
95
+ { src: '.vscode/launch.json', dest: '.vscode/launch.json', desc: 'VS Code debug configurations' }
96
+ );
97
+ }
98
+
99
+ if (selectedIDEs.includes('intellij')) {
100
+ ideTemplates.push(
101
+ { src: '.idea/runConfigurations/CAWS_Validate.xml', dest: '.idea/runConfigurations/CAWS_Validate.xml', desc: 'IntelliJ run configuration for CAWS validate' },
102
+ { src: '.idea/runConfigurations/CAWS_Evaluate.xml', dest: '.idea/runConfigurations/CAWS_Evaluate.xml', desc: 'IntelliJ run configuration for CAWS evaluate' }
103
+ );
104
+ }
105
+
106
+ if (selectedIDEs.includes('junie')) {
107
+ ideTemplates.push(
108
+ { src: '.junie/guidelines.md', dest: '.junie/guidelines.md', desc: 'JetBrains Junie AI agent guidelines' }
109
+ );
110
+ }
111
+
112
+ if (selectedIDEs.includes('windsurf')) {
113
+ ideTemplates.push(
114
+ { src: '.windsurf/workflows/caws-guided-development.md', dest: '.windsurf/workflows/caws-guided-development.md', desc: 'Windsurf workflow for CAWS-guided development' },
115
+ { src: '.windsurf/rules/caws-quality-standards.md', dest: '.windsurf/rules/caws-quality-standards.md', desc: 'Windsurf CAWS quality rules' }
116
+ );
117
+ }
118
+
119
+ if (selectedIDEs.includes('copilot')) {
120
+ ideTemplates.push(
121
+ { src: '.github/copilot-instructions.md', dest: '.github/copilot-instructions.md', desc: 'GitHub Copilot CAWS integration instructions' }
122
+ );
123
+ }
124
+
125
+ if (selectedIDEs.includes('cursor')) {
126
+ ideTemplates.push(
127
+ { src: '.cursor/README.md', dest: '.cursor/README.md', desc: 'Cursor integration documentation' }
128
+ );
129
+ }
130
+
131
+ if (selectedIDEs.includes('claude')) {
132
+ ideTemplates.push(
133
+ { src: '.claude/README.md', dest: '.claude/README.md', desc: 'Claude Code integration documentation' },
134
+ { src: 'CLAUDE.md', dest: 'CLAUDE.md', desc: 'Claude Code project instructions' }
135
+ );
136
+
137
+ // Setup Claude Code hooks
138
+ try {
139
+ await scaffoldClaudeHooks(targetDir, ['safety', 'quality', 'scope', 'audit']);
140
+ } catch (error) {
141
+ console.log(chalk.yellow(`Warning: Claude Code hooks setup failed: ${error.message}`));
142
+ }
140
143
  }
141
144
 
142
145
  for (const template of ideTemplates) {
@@ -144,48 +147,43 @@ async function scaffoldIDEIntegrations(targetDir, options) {
144
147
  const destPath = path.join(targetDir, template.dest);
145
148
 
146
149
  try {
147
- // Check if source exists
148
150
  if (!(await fs.pathExists(srcPath))) {
149
151
  if (!template.optional) {
150
- console.log(chalk.yellow(`⚠️ Template not found: ${template.src}`));
152
+ console.log(chalk.yellow(`Warning: Template not found: ${template.src}`));
151
153
  }
152
154
  continue;
153
155
  }
154
156
 
155
- // Check if destination already exists
156
157
  const destExists = await fs.pathExists(destPath);
157
158
 
158
159
  if (destExists && !options.force) {
159
- console.log(chalk.gray(`⏭️ Skipped ${template.desc} (already exists)`));
160
+ console.log(chalk.gray(`Skipped ${template.desc} (already exists)`));
160
161
  skippedCount++;
161
162
  continue;
162
163
  }
163
164
 
164
- // Ensure destination directory exists
165
165
  await fs.ensureDir(path.dirname(destPath));
166
-
167
- // Copy the file
168
166
  await fs.copy(srcPath, destPath);
169
167
 
170
- // Make scripts executable if they're in hooks or cursor directories
171
- if (destPath.includes('.git/hooks/') || destPath.includes('.cursor/hooks/')) {
168
+ if (destPath.includes('.git/hooks/') || destPath.includes('.cursor/hooks/') || destPath.includes('.claude/hooks/')) {
172
169
  try {
173
170
  await fs.chmod(destPath, '755');
174
- } catch (error) {
171
+ } catch (_) {
175
172
  // Ignore chmod errors on some systems
176
173
  }
177
174
  }
178
175
 
179
- console.log(chalk.green(`✅ Added ${template.desc}`));
176
+ console.log(chalk.green(`Added ${template.desc}`));
180
177
  addedCount++;
181
178
  } catch (error) {
182
- console.log(chalk.red(`❌ Failed to add ${template.desc}: ${error.message}`));
179
+ console.log(chalk.red(`Failed to add ${template.desc}: ${error.message}`));
183
180
  }
184
181
  }
185
182
 
186
183
  if (addedCount > 0) {
187
- console.log(chalk.green(`\n🎨 IDE integrations: ${addedCount} added, ${skippedCount} skipped`));
188
- console.log(chalk.blue('💡 Restart your IDE to activate the new integrations'));
184
+ console.log(chalk.green(`\nIDE integrations: ${addedCount} added, ${skippedCount} skipped`));
185
+ console.log(chalk.gray(` Installed: ${ideNames}`));
186
+ console.log(chalk.blue('Restart your IDE to activate the new integrations'));
189
187
  }
190
188
 
191
189
  return { added: addedCount, skipped: skippedCount };
@@ -370,15 +368,19 @@ async function scaffoldProject(options) {
370
368
  });
371
369
  }
372
370
 
373
- // Add IDE integrations for comprehensive development experience
374
- enhancements.push({
375
- name: 'ide-integrations',
376
- description: 'IDE integrations (VS Code, IntelliJ, Windsurf, Git hooks)',
377
- required: false,
378
- customHandler: async (targetDir, options) => {
379
- return await scaffoldIDEIntegrations(targetDir, options);
380
- },
381
- });
371
+ // Add IDE integrations for selected IDEs
372
+ const selectedIDEs = options.ide ? parseIDESelection(options.ide) : getRecommendedIDEs();
373
+ if (selectedIDEs.length > 0) {
374
+ const ideNames = selectedIDEs.map((id) => IDE_REGISTRY[id]?.name || id).join(', ');
375
+ enhancements.push({
376
+ name: 'ide-integrations',
377
+ description: `IDE integrations (${ideNames})`,
378
+ required: false,
379
+ customHandler: async (targetDir, opts) => {
380
+ return await scaffoldIDEIntegrations(targetDir, { ...opts, ides: selectedIDEs });
381
+ },
382
+ });
383
+ }
382
384
 
383
385
  // Add quality gates package and configuration if requested
384
386
  // Note: These are optional - git hooks fall back to CAWS CLI if package isn't installed
@@ -558,7 +560,7 @@ async function scaffoldProject(options) {
558
560
  !fs.existsSync(path.join(currentDir, 'caws.md'))
559
561
  ) {
560
562
  enhancements.push({
561
- name: 'agents.md',
563
+ name: 'AGENTS.md',
562
564
  description: 'CAWS agent workflow guide',
563
565
  required: false,
564
566
  });
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "CAWS Lite Scope Configuration",
4
+ "description": "Scope configuration for CAWS lite mode — guardrails without YAML specs",
5
+ "type": "object",
6
+ "required": ["version", "allowedDirectories"],
7
+ "properties": {
8
+ "version": {
9
+ "type": "integer",
10
+ "const": 1,
11
+ "description": "Schema version"
12
+ },
13
+ "allowedDirectories": {
14
+ "type": "array",
15
+ "items": { "type": "string" },
16
+ "minItems": 1,
17
+ "description": "Directories the agent is allowed to modify (e.g., src/, tests/)"
18
+ },
19
+ "bannedPatterns": {
20
+ "type": "object",
21
+ "properties": {
22
+ "files": {
23
+ "type": "array",
24
+ "items": { "type": "string" },
25
+ "description": "Glob patterns for banned file names (e.g., *-enhanced.*, *-final.*)"
26
+ },
27
+ "directories": {
28
+ "type": "array",
29
+ "items": { "type": "string" },
30
+ "description": "Glob patterns for banned directory names (e.g., *venv*, .venv)"
31
+ },
32
+ "docs": {
33
+ "type": "array",
34
+ "items": { "type": "string" },
35
+ "description": "Glob patterns for banned doc file names (e.g., *-summary.md)"
36
+ }
37
+ },
38
+ "additionalProperties": false
39
+ },
40
+ "maxNewFilesPerCommit": {
41
+ "type": "integer",
42
+ "minimum": 1,
43
+ "maximum": 100,
44
+ "description": "Maximum number of new files allowed per commit (prevents file sprawl)"
45
+ },
46
+ "designatedVenvPath": {
47
+ "type": "string",
48
+ "description": "The only allowed virtual environment path (e.g., .venv)"
49
+ }
50
+ },
51
+ "additionalProperties": false
52
+ }
@@ -16,7 +16,7 @@
16
16
  "contracts"
17
17
  ],
18
18
  "properties": {
19
- "id": { "type": "string", "pattern": "^(PROJ|FEAT|FIX|ARCH)-\\d{4}$" },
19
+ "id": { "type": "string", "pattern": "^[A-Z]{2,6}-\\d{3,4}$" },
20
20
  "title": { "type": "string", "minLength": 10, "maxLength": 200 },
21
21
  "risk_tier": { "type": ["integer", "string"], "enum": [1, 2, 3, "1", "2", "3"] },
22
22
  "mode": { "type": "string", "enum": ["feature", "refactor", "fix", "doc", "chore"] },
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "CAWS Worktree Registry",
4
+ "description": "Registry of git worktrees managed by CAWS for agent scope isolation",
5
+ "type": "object",
6
+ "required": ["version", "worktrees"],
7
+ "properties": {
8
+ "version": {
9
+ "type": "integer",
10
+ "const": 1
11
+ },
12
+ "worktrees": {
13
+ "type": "object",
14
+ "additionalProperties": {
15
+ "type": "object",
16
+ "required": ["name", "path", "branch", "baseBranch", "createdAt", "status"],
17
+ "properties": {
18
+ "name": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" },
19
+ "path": { "type": "string" },
20
+ "branch": { "type": "string" },
21
+ "baseBranch": { "type": "string" },
22
+ "scope": { "type": ["string", "null"] },
23
+ "specId": { "type": ["string", "null"] },
24
+ "createdAt": { "type": "string", "format": "date-time" },
25
+ "destroyedAt": { "type": "string", "format": "date-time" },
26
+ "status": {
27
+ "type": "string",
28
+ "enum": ["active", "orphaned", "missing", "destroyed"]
29
+ }
30
+ },
31
+ "additionalProperties": false
32
+ }
33
+ }
34
+ },
35
+ "additionalProperties": false
36
+ }
@@ -65,11 +65,44 @@ DANGEROUS_PATTERNS=(
65
65
  'reboot'
66
66
  'init 0'
67
67
  'init 6'
68
+
69
+ # Git destructive operations
70
+ 'git init'
71
+ 'git reset --hard'
72
+ 'git push --force'
73
+ 'git push -f '
74
+ 'git push --force-with-lease'
75
+ 'git clean -f'
76
+ 'git checkout \.'
77
+ 'git restore \.'
78
+
79
+ # Virtual environment creation (prevents venv sprawl)
80
+ 'python -m venv'
81
+ 'python3 -m venv'
82
+ 'virtualenv '
83
+ 'conda create'
68
84
  )
69
85
 
70
86
  # Check command against dangerous patterns
71
87
  for pattern in "${DANGEROUS_PATTERNS[@]}"; do
72
88
  if echo "$COMMAND" | grep -qiE "$pattern"; then
89
+ # Allow git init in worktree context
90
+ if [[ "$pattern" == "git init" ]] && [[ "${CAWS_WORKTREE_CONTEXT:-0}" == "1" ]]; then
91
+ continue
92
+ fi
93
+
94
+ # Allow venv commands if target matches designated venv path from scope.json
95
+ if echo "$pattern" | grep -qE '(python.*venv|virtualenv|conda create)'; then
96
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
97
+ SCOPE_FILE="$PROJECT_DIR/.caws/scope.json"
98
+ if [[ -f "$SCOPE_FILE" ]] && command -v node >/dev/null 2>&1; then
99
+ DESIGNATED_VENV=$(node -e "try { const s = JSON.parse(require('fs').readFileSync('$SCOPE_FILE','utf8')); console.log(s.designatedVenvPath || ''); } catch(e) { console.log(''); }" 2>/dev/null || echo "")
100
+ if [[ -n "$DESIGNATED_VENV" ]] && echo "$COMMAND" | grep -qF "$DESIGNATED_VENV"; then
101
+ continue
102
+ fi
103
+ fi
104
+ fi
105
+
73
106
  # Output to stderr for Claude to see
74
107
  echo "BLOCKED: Command matches dangerous pattern: $pattern" >&2
75
108
  echo "Command was: $COMMAND" >&2
@@ -0,0 +1,117 @@
1
+ #!/bin/bash
2
+ # CAWS Lite-Mode Sprawl Check Hook
3
+ # Checks for file sprawl patterns (banned names, venv dirs, doc sprawl)
4
+ # @author @darianrosebrook
5
+
6
+ set -euo pipefail
7
+
8
+ # Read JSON input from Claude Code
9
+ INPUT=$(cat)
10
+
11
+ # Extract tool info
12
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
13
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
14
+
15
+ # Only check Write operations (new file creation)
16
+ if [[ "$TOOL_NAME" != "Write" ]]; then
17
+ exit 0
18
+ fi
19
+
20
+ if [[ -z "$FILE_PATH" ]]; then
21
+ exit 0
22
+ fi
23
+
24
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
25
+ SCOPE_FILE="$PROJECT_DIR/.caws/scope.json"
26
+
27
+ # Only active in lite mode (scope.json present, no working-spec.yaml)
28
+ if [[ ! -f "$SCOPE_FILE" ]]; then
29
+ exit 0
30
+ fi
31
+
32
+ # Get relative path
33
+ # Get relative path (portable — macOS realpath lacks --relative-to)
34
+ if [[ "$FILE_PATH" == "$PROJECT_DIR"/* ]]; then
35
+ REL_PATH="${FILE_PATH#$PROJECT_DIR/}"
36
+ else
37
+ REL_PATH="$FILE_PATH"
38
+ fi
39
+ BASENAME=$(basename "$REL_PATH")
40
+
41
+ # Use Node.js to check banned patterns
42
+ if command -v node >/dev/null 2>&1; then
43
+ SPRAWL_CHECK=$(node -e "
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+ try {
47
+ const scope = JSON.parse(fs.readFileSync('$SCOPE_FILE', 'utf8'));
48
+ const filePath = '$REL_PATH';
49
+ const basename = '$BASENAME';
50
+ const banned = scope.bannedPatterns || {};
51
+
52
+ function matchGlob(str, pattern) {
53
+ const regex = new RegExp('^' + pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.') + '$');
54
+ return regex.test(str);
55
+ }
56
+
57
+ // Check banned file patterns
58
+ for (const p of (banned.files || [])) {
59
+ if (matchGlob(basename, p)) {
60
+ console.log('banned_file:' + p);
61
+ process.exit(0);
62
+ }
63
+ }
64
+
65
+ // Check banned doc patterns
66
+ for (const p of (banned.docs || [])) {
67
+ if (matchGlob(basename, p)) {
68
+ console.log('banned_doc:' + p);
69
+ process.exit(0);
70
+ }
71
+ }
72
+
73
+ // Check banned directory patterns
74
+ const parts = filePath.split('/');
75
+ for (const part of parts) {
76
+ for (const p of (banned.directories || [])) {
77
+ if (matchGlob(part, p)) {
78
+ console.log('banned_dir:' + p + ':' + part);
79
+ process.exit(0);
80
+ }
81
+ }
82
+ }
83
+
84
+ console.log('ok');
85
+ } catch (error) {
86
+ console.log('error:' + error.message);
87
+ }
88
+ " 2>&1)
89
+
90
+ if [[ "$SPRAWL_CHECK" == banned_file:* ]]; then
91
+ PATTERN="${SPRAWL_CHECK#banned_file:}"
92
+ echo "BLOCKED: File name matches banned sprawl pattern: $PATTERN" >&2
93
+ echo "File: $REL_PATH" >&2
94
+ echo "Banned patterns prevent shadow files like *-enhanced.*, *-final.*, *-v2.*, *-copy.*" >&2
95
+ echo "Instead, modify the original file directly." >&2
96
+ exit 2
97
+ fi
98
+
99
+ if [[ "$SPRAWL_CHECK" == banned_doc:* ]]; then
100
+ PATTERN="${SPRAWL_CHECK#banned_doc:}"
101
+ echo "BLOCKED: Doc file matches banned sprawl pattern: $PATTERN" >&2
102
+ echo "File: $REL_PATH" >&2
103
+ echo "Avoid creating many summary/recap/plan files. Update existing documentation instead." >&2
104
+ exit 2
105
+ fi
106
+
107
+ if [[ "$SPRAWL_CHECK" == banned_dir:* ]]; then
108
+ IFS=':' read -r _ PATTERN DIR_NAME <<< "$SPRAWL_CHECK"
109
+ echo "BLOCKED: Directory matches banned pattern: $PATTERN (directory: $DIR_NAME)" >&2
110
+ echo "File: $REL_PATH" >&2
111
+ echo "Use the designated venv path instead of creating new virtual environments." >&2
112
+ exit 2
113
+ fi
114
+ fi
115
+
116
+ # Allow the operation
117
+ exit 0
@@ -23,14 +23,101 @@ fi
23
23
 
24
24
  PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
25
25
  SPEC_FILE="$PROJECT_DIR/.caws/working-spec.yaml"
26
+ SCOPE_FILE="$PROJECT_DIR/.caws/scope.json"
26
27
 
27
- # Check if spec file exists
28
- if [[ ! -f "$SPEC_FILE" ]]; then
28
+ # Check if spec file or scope.json exists
29
+ if [[ ! -f "$SPEC_FILE" ]] && [[ ! -f "$SCOPE_FILE" ]]; then
29
30
  exit 0
30
31
  fi
31
32
 
32
- # Get relative path from project root
33
- REL_PATH=$(realpath --relative-to="$PROJECT_DIR" "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
33
+ # Get relative path from project root (portable — macOS realpath lacks --relative-to)
34
+ if [[ "$FILE_PATH" == "$PROJECT_DIR"/* ]]; then
35
+ REL_PATH="${FILE_PATH#$PROJECT_DIR/}"
36
+ else
37
+ REL_PATH="$FILE_PATH"
38
+ fi
39
+
40
+ # Lite mode: check scope.json if no working-spec.yaml
41
+ if [[ ! -f "$SPEC_FILE" ]] && [[ -f "$SCOPE_FILE" ]]; then
42
+ if command -v node >/dev/null 2>&1; then
43
+ LITE_CHECK=$(node -e "
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+ try {
47
+ const scope = JSON.parse(fs.readFileSync('$SCOPE_FILE', 'utf8'));
48
+ const filePath = '$REL_PATH';
49
+ const dirs = scope.allowedDirectories || [];
50
+ const banned = scope.bannedPatterns || {};
51
+
52
+ // Check banned file patterns
53
+ const basename = path.basename(filePath);
54
+ const bannedFiles = banned.files || [];
55
+ for (const pattern of bannedFiles) {
56
+ const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
57
+ if (regex.test(basename)) {
58
+ console.log('banned:' + pattern);
59
+ process.exit(0);
60
+ }
61
+ }
62
+
63
+ // Check banned doc patterns
64
+ const bannedDocs = banned.docs || [];
65
+ for (const pattern of bannedDocs) {
66
+ const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
67
+ if (regex.test(basename)) {
68
+ console.log('banned:' + pattern);
69
+ process.exit(0);
70
+ }
71
+ }
72
+
73
+ // Check allowed directories
74
+ if (dirs.length > 0) {
75
+ const normalized = filePath.replace(/\\\\\\\\/g, '/');
76
+ let found = false;
77
+ for (const dir of dirs) {
78
+ const d = dir.replace(/\\/$/, '');
79
+ if (normalized.startsWith(d + '/') || normalized === d) { found = true; break; }
80
+ }
81
+ // Allow root-level files and .caws/ directory
82
+ if (!normalized.includes('/') || normalized.startsWith('.caws/')) found = true;
83
+ if (!found) {
84
+ console.log('not_allowed');
85
+ process.exit(0);
86
+ }
87
+ }
88
+ console.log('allowed');
89
+ } catch (error) {
90
+ console.log('error:' + error.message);
91
+ }
92
+ " 2>&1)
93
+
94
+ if [[ "$LITE_CHECK" == banned:* ]]; then
95
+ PATTERN="${LITE_CHECK#banned:}"
96
+ echo '{
97
+ "hookSpecificOutput": {
98
+ "hookEventName": "PreToolUse",
99
+ "permissionDecision": "ask",
100
+ "permissionDecisionReason": "This file ('"$REL_PATH"') matches a banned pattern ('"$PATTERN"') in .caws/scope.json. Creating files with this pattern is blocked to prevent file sprawl."
101
+ }
102
+ }'
103
+ exit 0
104
+ fi
105
+
106
+ if [[ "$LITE_CHECK" == "not_allowed" ]]; then
107
+ echo '{
108
+ "hookSpecificOutput": {
109
+ "hookEventName": "PreToolUse",
110
+ "permissionDecision": "ask",
111
+ "permissionDecisionReason": "This file ('"$REL_PATH"') is outside the allowed directories in .caws/scope.json. Please confirm this edit is intentional."
112
+ }
113
+ }'
114
+ exit 0
115
+ fi
116
+
117
+ # File is allowed - exit normally
118
+ exit 0
119
+ fi
120
+ fi
34
121
 
35
122
  # Use Node.js to parse YAML and check scope
36
123
  if command -v node >/dev/null 2>&1; then
@@ -44,7 +131,7 @@ if command -v node >/dev/null 2>&1; then
44
131
  const filePath = '$REL_PATH';
45
132
 
46
133
  // Check if file is explicitly out of scope
47
- const outOfScope = spec.scope?.out_of_scope || [];
134
+ const outOfScope = spec.scope?.out || [];
48
135
  for (const pattern of outOfScope) {
49
136
  // Simple glob-like matching
50
137
  const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
@@ -55,7 +142,7 @@ if command -v node >/dev/null 2>&1; then
55
142
  }
56
143
 
57
144
  // Check if file is in scope (if scope is explicitly defined)
58
- const inScope = spec.scope?.files || spec.scope?.directories || [];
145
+ const inScope = spec.scope?.in || [];
59
146
  if (inScope.length > 0) {
60
147
  let found = false;
61
148
  for (const pattern of inScope) {