@paths.design/caws-cli 8.1.0 → 8.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) 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/quality-gates.js +1 -1
  10. package/dist/commands/specs.d.ts.map +1 -1
  11. package/dist/commands/worktree.d.ts +7 -0
  12. package/dist/commands/worktree.d.ts.map +1 -0
  13. package/dist/commands/worktree.js +136 -0
  14. package/dist/config/lite-scope.d.ts +33 -0
  15. package/dist/config/lite-scope.d.ts.map +1 -0
  16. package/dist/config/lite-scope.js +158 -0
  17. package/dist/config/modes.d.ts +90 -51
  18. package/dist/config/modes.d.ts.map +1 -1
  19. package/dist/config/modes.js +26 -0
  20. package/dist/error-handler.d.ts +3 -16
  21. package/dist/error-handler.d.ts.map +1 -1
  22. package/dist/generators/jest-config-generator.d.ts +32 -0
  23. package/dist/generators/jest-config-generator.d.ts.map +1 -0
  24. package/dist/index.js +36 -0
  25. package/dist/scaffold/claude-hooks.d.ts +28 -0
  26. package/dist/scaffold/claude-hooks.d.ts.map +1 -0
  27. package/dist/scaffold/claude-hooks.js +28 -0
  28. package/dist/scaffold/index.d.ts +2 -0
  29. package/dist/scaffold/index.d.ts.map +1 -1
  30. package/dist/scaffold/index.js +90 -88
  31. package/dist/templates/.caws/schemas/scope.schema.json +52 -0
  32. package/dist/templates/.caws/schemas/working-spec.schema.json +1 -1
  33. package/dist/templates/.caws/schemas/worktrees.schema.json +36 -0
  34. package/dist/templates/.claude/hooks/block-dangerous.sh +33 -0
  35. package/dist/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
  36. package/dist/templates/.claude/hooks/scope-guard.sh +93 -6
  37. package/dist/templates/.claude/hooks/simplification-guard.sh +92 -0
  38. package/dist/templates/.cursor/README.md +0 -3
  39. package/dist/templates/.github/copilot-instructions.md +82 -0
  40. package/dist/templates/.junie/guidelines.md +73 -0
  41. package/dist/templates/.vscode/launch.json +0 -27
  42. package/dist/templates/.windsurf/rules/caws-quality-standards.md +54 -0
  43. package/dist/templates/CLAUDE.md +101 -0
  44. package/dist/templates/agents.md +73 -1016
  45. package/dist/templates/docs/README.md +5 -5
  46. package/dist/test-analysis.d.ts +50 -1
  47. package/dist/test-analysis.d.ts.map +1 -1
  48. package/dist/utils/error-categories.d.ts +52 -0
  49. package/dist/utils/error-categories.d.ts.map +1 -0
  50. package/dist/utils/gitignore-updater.d.ts +1 -1
  51. package/dist/utils/gitignore-updater.d.ts.map +1 -1
  52. package/dist/utils/gitignore-updater.js +4 -0
  53. package/dist/utils/ide-detection.js +133 -0
  54. package/dist/utils/quality-gates-utils.d.ts +49 -0
  55. package/dist/utils/quality-gates-utils.d.ts.map +1 -0
  56. package/dist/utils/typescript-detector.d.ts +8 -5
  57. package/dist/utils/typescript-detector.d.ts.map +1 -1
  58. package/dist/validation/spec-validation.d.ts.map +1 -1
  59. package/dist/worktree/worktree-manager.d.ts +54 -0
  60. package/dist/worktree/worktree-manager.d.ts.map +1 -0
  61. package/dist/worktree/worktree-manager.js +378 -0
  62. package/package.json +5 -1
  63. package/templates/.caws/schemas/scope.schema.json +52 -0
  64. package/templates/.caws/schemas/working-spec.schema.json +1 -1
  65. package/templates/.caws/schemas/worktrees.schema.json +36 -0
  66. package/templates/.claude/hooks/block-dangerous.sh +33 -0
  67. package/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
  68. package/templates/.claude/hooks/scope-guard.sh +93 -6
  69. package/templates/.claude/hooks/simplification-guard.sh +92 -0
  70. package/templates/.cursor/README.md +0 -3
  71. package/templates/.github/copilot-instructions.md +82 -0
  72. package/templates/.junie/guidelines.md +73 -0
  73. package/templates/.vscode/launch.json +0 -27
  74. package/templates/.windsurf/rules/caws-quality-standards.md +54 -0
  75. package/templates/AGENTS.md +104 -0
  76. package/templates/CLAUDE.md +101 -0
  77. package/templates/docs/README.md +5 -5
  78. package/templates/.github/copilot/instructions.md +0 -311
  79. package/templates/agents.md +0 -1047
@@ -0,0 +1,378 @@
1
+ /**
2
+ * @fileoverview CAWS Git Worktree Manager
3
+ * Provides CRUD operations for git worktrees with scope isolation
4
+ * @author @darianrosebrook
5
+ */
6
+
7
+ const { execFileSync } = require('child_process');
8
+ const fs = require('fs-extra');
9
+ const path = require('path');
10
+ const chalk = require('chalk');
11
+
12
+ const WORKTREES_DIR = '.caws/worktrees';
13
+ const REGISTRY_FILE = '.caws/worktrees.json';
14
+ const BRANCH_PREFIX = 'caws/';
15
+
16
+ /**
17
+ * Get the git repository root
18
+ * @returns {string} Absolute path to repo root
19
+ */
20
+ function getRepoRoot() {
21
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
22
+ encoding: 'utf8',
23
+ }).trim();
24
+ }
25
+
26
+ /**
27
+ * Get current branch name
28
+ * @returns {string}
29
+ */
30
+ function getCurrentBranch() {
31
+ return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
32
+ encoding: 'utf8',
33
+ }).trim();
34
+ }
35
+
36
+ /**
37
+ * Load the worktree registry
38
+ * @param {string} root - Repository root
39
+ * @returns {Object} Registry object
40
+ */
41
+ function loadRegistry(root) {
42
+ const registryPath = path.join(root, REGISTRY_FILE);
43
+ try {
44
+ if (fs.existsSync(registryPath)) {
45
+ return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
46
+ }
47
+ } catch {
48
+ // Corrupted registry, start fresh
49
+ }
50
+ return { version: 1, worktrees: {} };
51
+ }
52
+
53
+ /**
54
+ * Save the worktree registry
55
+ * @param {string} root - Repository root
56
+ * @param {Object} registry - Registry object
57
+ */
58
+ function saveRegistry(root, registry) {
59
+ const registryPath = path.join(root, REGISTRY_FILE);
60
+ fs.ensureDirSync(path.dirname(registryPath));
61
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
62
+ }
63
+
64
+ /**
65
+ * Create a new git worktree with scope isolation
66
+ * @param {string} name - Worktree name
67
+ * @param {Object} options - Creation options
68
+ * @param {string} [options.scope] - Sparse checkout pattern (e.g., "src/auth/**")
69
+ * @param {string} [options.baseBranch] - Base branch to create from
70
+ * @param {string} [options.specId] - Associated spec ID for standard+ modes
71
+ * @returns {Object} Created worktree info
72
+ */
73
+ function createWorktree(name, options = {}) {
74
+ const root = getRepoRoot();
75
+ const { scope, baseBranch, specId } = options;
76
+
77
+ // Validate name
78
+ if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
79
+ throw new Error('Worktree name must contain only letters, numbers, hyphens, and underscores');
80
+ }
81
+
82
+ const registry = loadRegistry(root);
83
+
84
+ // Check for duplicate
85
+ if (registry.worktrees[name]) {
86
+ throw new Error(`Worktree '${name}' already exists. Use 'caws worktree destroy ${name}' first.`);
87
+ }
88
+
89
+ const worktreePath = path.join(root, WORKTREES_DIR, name);
90
+ const branchName = BRANCH_PREFIX + name;
91
+ const base = baseBranch || getCurrentBranch();
92
+
93
+ // Create the worktree directory
94
+ fs.ensureDirSync(path.dirname(worktreePath));
95
+
96
+ // Create git worktree with new branch
97
+ try {
98
+ execFileSync('git', ['worktree', 'add', '-b', branchName, worktreePath, base], {
99
+ cwd: root,
100
+ stdio: 'pipe',
101
+ });
102
+ } catch (error) {
103
+ // Branch might already exist
104
+ if (error.message.includes('already exists')) {
105
+ execFileSync('git', ['worktree', 'add', worktreePath, branchName], {
106
+ cwd: root,
107
+ stdio: 'pipe',
108
+ });
109
+ } else {
110
+ throw new Error(`Failed to create worktree: ${error.message}`);
111
+ }
112
+ }
113
+
114
+ // Set up sparse checkout if scope is provided
115
+ if (scope) {
116
+ try {
117
+ execFileSync('git', ['sparse-checkout', 'init', '--cone'], {
118
+ cwd: worktreePath,
119
+ stdio: 'pipe',
120
+ });
121
+
122
+ // Parse scope patterns (comma-separated)
123
+ const patterns = scope.split(',').map((p) => p.trim());
124
+ execFileSync('git', ['sparse-checkout', 'set', ...patterns], {
125
+ cwd: worktreePath,
126
+ stdio: 'pipe',
127
+ });
128
+ } catch (error) {
129
+ console.warn(chalk.yellow(`⚠️ Sparse checkout setup failed: ${error.message}`));
130
+ console.warn(chalk.blue('💡 Worktree created but without sparse checkout'));
131
+ }
132
+ }
133
+
134
+ // Copy .caws/ config into worktree
135
+ const cawsSource = path.join(root, '.caws');
136
+ const cawsDest = path.join(worktreePath, '.caws');
137
+ if (fs.existsSync(cawsSource)) {
138
+ try {
139
+ fs.copySync(cawsSource, cawsDest, {
140
+ filter: (src) => {
141
+ // Don't copy worktrees directory or registry into the worktree
142
+ const rel = path.relative(cawsSource, src);
143
+ return !rel.startsWith('worktrees') && rel !== 'worktrees.json';
144
+ },
145
+ });
146
+ } catch {
147
+ // Non-fatal
148
+ }
149
+ }
150
+
151
+ // Generate working spec if in standard+ mode and specId provided
152
+ if (specId) {
153
+ try {
154
+ const { generateWorkingSpec } = require('../generators/working-spec');
155
+ const specContent = generateWorkingSpec({
156
+ projectId: specId,
157
+ projectTitle: `Worktree: ${name}`,
158
+ projectDescription: `Isolated worktree for ${name}`,
159
+ riskTier: 3,
160
+ projectMode: 'feature',
161
+ scopeIn: scope || 'src/',
162
+ scopeOut: 'node_modules/, dist/, build/',
163
+ maxFiles: 25,
164
+ maxLoc: 1000,
165
+ blastModules: scope || 'src',
166
+ dataMigration: false,
167
+ rollbackSlo: '5m',
168
+ projectThreats: '',
169
+ projectInvariants: 'System maintains data consistency',
170
+ acceptanceCriteria: 'Given current state, when action occurs, then expected result',
171
+ a11yRequirements: 'keyboard',
172
+ perfBudget: 250,
173
+ securityRequirements: 'validation',
174
+ contractType: '',
175
+ contractPath: '',
176
+ observabilityLogs: '',
177
+ observabilityMetrics: '',
178
+ observabilityTraces: '',
179
+ migrationPlan: '',
180
+ rollbackPlan: '',
181
+ needsOverride: false,
182
+ isExperimental: false,
183
+ aiConfidence: 0.8,
184
+ uncertaintyAreas: '',
185
+ complexityFactors: '',
186
+ });
187
+ const specPath = path.join(cawsDest, 'working-spec.yaml');
188
+ fs.ensureDirSync(path.dirname(specPath));
189
+ fs.writeFileSync(specPath, specContent);
190
+ } catch {
191
+ // Non-fatal: spec generation is optional
192
+ }
193
+ }
194
+
195
+ // Register worktree
196
+ const entry = {
197
+ name,
198
+ path: worktreePath,
199
+ branch: branchName,
200
+ baseBranch: base,
201
+ scope: scope || null,
202
+ specId: specId || null,
203
+ createdAt: new Date().toISOString(),
204
+ status: 'active',
205
+ };
206
+
207
+ registry.worktrees[name] = entry;
208
+ saveRegistry(root, registry);
209
+
210
+ return entry;
211
+ }
212
+
213
+ /**
214
+ * List all registered worktrees with filesystem validation
215
+ * @returns {Array} Worktree entries with status
216
+ */
217
+ function listWorktrees() {
218
+ const root = getRepoRoot();
219
+ const registry = loadRegistry(root);
220
+
221
+ // Get actual git worktrees for validation
222
+ let gitWorktrees = [];
223
+ try {
224
+ const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
225
+ cwd: root,
226
+ encoding: 'utf8',
227
+ });
228
+ gitWorktrees = output
229
+ .split('\n\n')
230
+ .filter(Boolean)
231
+ .map((block) => {
232
+ const lines = block.split('\n');
233
+ const worktreeLine = lines.find((l) => l.startsWith('worktree '));
234
+ return worktreeLine ? worktreeLine.replace('worktree ', '') : null;
235
+ })
236
+ .filter(Boolean);
237
+ } catch {
238
+ // Git worktree list failed
239
+ }
240
+
241
+ const entries = Object.values(registry.worktrees).map((entry) => {
242
+ const exists = fs.existsSync(entry.path);
243
+ const inGit = gitWorktrees.some(
244
+ (wt) => path.resolve(wt) === path.resolve(entry.path)
245
+ );
246
+
247
+ return {
248
+ ...entry,
249
+ status: exists && inGit ? 'active' : exists ? 'orphaned' : 'missing',
250
+ };
251
+ });
252
+
253
+ return entries;
254
+ }
255
+
256
+ /**
257
+ * Destroy a worktree
258
+ * @param {string} name - Worktree name
259
+ * @param {Object} options - Destruction options
260
+ * @param {boolean} [options.deleteBranch] - Also delete the branch
261
+ * @param {boolean} [options.force] - Force removal even if dirty
262
+ */
263
+ function destroyWorktree(name, options = {}) {
264
+ const root = getRepoRoot();
265
+ const registry = loadRegistry(root);
266
+ const { deleteBranch = false, force = false } = options;
267
+
268
+ const entry = registry.worktrees[name];
269
+ if (!entry) {
270
+ throw new Error(`Worktree '${name}' not found in registry`);
271
+ }
272
+
273
+ // Remove git worktree
274
+ try {
275
+ const args = ['worktree', 'remove'];
276
+ if (force) args.push('--force');
277
+ args.push(entry.path);
278
+ execFileSync('git', args, { cwd: root, stdio: 'pipe' });
279
+ } catch (error) {
280
+ if (force) {
281
+ // Force cleanup: remove directory manually
282
+ if (fs.existsSync(entry.path)) {
283
+ fs.removeSync(entry.path);
284
+ }
285
+ // Prune git worktree list
286
+ try {
287
+ execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
288
+ } catch {
289
+ // Non-fatal
290
+ }
291
+ } else {
292
+ throw new Error(`Failed to remove worktree: ${error.message}. Use --force to override.`);
293
+ }
294
+ }
295
+
296
+ // Optionally delete branch
297
+ if (deleteBranch && entry.branch) {
298
+ try {
299
+ execFileSync('git', ['branch', '-d', entry.branch], { cwd: root, stdio: 'pipe' });
300
+ } catch {
301
+ if (force) {
302
+ try {
303
+ execFileSync('git', ['branch', '-D', entry.branch], { cwd: root, stdio: 'pipe' });
304
+ } catch {
305
+ // Non-fatal
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ // Update registry
312
+ registry.worktrees[name].status = 'destroyed';
313
+ registry.worktrees[name].destroyedAt = new Date().toISOString();
314
+ saveRegistry(root, registry);
315
+ }
316
+
317
+ /**
318
+ * Prune stale worktree entries
319
+ * @param {Object} options - Prune options
320
+ * @param {number} [options.maxAgeDays] - Remove entries older than this many days
321
+ * @returns {Array} Pruned entries
322
+ */
323
+ function pruneWorktrees(options = {}) {
324
+ const root = getRepoRoot();
325
+ const registry = loadRegistry(root);
326
+ const { maxAgeDays = 30 } = options;
327
+
328
+ const now = new Date();
329
+ const pruned = [];
330
+
331
+ for (const [name, entry] of Object.entries(registry.worktrees)) {
332
+ const created = new Date(entry.createdAt);
333
+ const ageDays = (now - created) / (1000 * 60 * 60 * 24);
334
+
335
+ const shouldPrune =
336
+ entry.status === 'destroyed' ||
337
+ (!fs.existsSync(entry.path) && ageDays > maxAgeDays) ||
338
+ (maxAgeDays === 0 && entry.status === 'destroyed');
339
+
340
+ if (shouldPrune) {
341
+ // Clean up filesystem if still exists
342
+ if (fs.existsSync(entry.path)) {
343
+ try {
344
+ execFileSync('git', ['worktree', 'remove', '--force', entry.path], {
345
+ cwd: root,
346
+ stdio: 'pipe',
347
+ });
348
+ } catch {
349
+ fs.removeSync(entry.path);
350
+ }
351
+ }
352
+ pruned.push(entry);
353
+ delete registry.worktrees[name];
354
+ }
355
+ }
356
+
357
+ // Prune git's worktree list
358
+ try {
359
+ execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
360
+ } catch {
361
+ // Non-fatal
362
+ }
363
+
364
+ saveRegistry(root, registry);
365
+ return pruned;
366
+ }
367
+
368
+ module.exports = {
369
+ createWorktree,
370
+ listWorktrees,
371
+ destroyWorktree,
372
+ pruneWorktrees,
373
+ loadRegistry,
374
+ getRepoRoot,
375
+ WORKTREES_DIR,
376
+ REGISTRY_FILE,
377
+ BRANCH_PREFIX,
378
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paths.design/caws-cli",
3
- "version": "8.1.0",
3
+ "version": "8.2.1",
4
4
  "description": "CAWS CLI - Coding Agent Workflow System command-line tools for spec management, quality gates, and AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -51,6 +51,10 @@
51
51
  "url": "https://github.com/Paths-Design/coding-agent-working-standard.git",
52
52
  "directory": "packages/caws-cli"
53
53
  },
54
+ "publishConfig": {
55
+ "registry": "https://registry.npmjs.org/",
56
+ "access": "public"
57
+ },
54
58
  "dependencies": {
55
59
  "chalk": "4.1.2",
56
60
  "commander": "^11.0.0",
@@ -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