@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.
- package/README.md +5 -6
- package/dist/commands/archive.d.ts +1 -1
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +185 -39
- package/dist/commands/mode.d.ts +2 -1
- package/dist/commands/mode.d.ts.map +1 -1
- package/dist/commands/provenance.d.ts.map +1 -1
- package/dist/commands/specs.d.ts.map +1 -1
- package/dist/commands/worktree.d.ts +7 -0
- package/dist/commands/worktree.d.ts.map +1 -0
- package/dist/commands/worktree.js +136 -0
- package/dist/config/lite-scope.d.ts +33 -0
- package/dist/config/lite-scope.d.ts.map +1 -0
- package/dist/config/lite-scope.js +158 -0
- package/dist/config/modes.d.ts +90 -51
- package/dist/config/modes.d.ts.map +1 -1
- package/dist/config/modes.js +26 -0
- package/dist/error-handler.d.ts +3 -16
- package/dist/error-handler.d.ts.map +1 -1
- package/dist/generators/jest-config-generator.d.ts +32 -0
- package/dist/generators/jest-config-generator.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/scaffold/claude-hooks.d.ts +28 -0
- package/dist/scaffold/claude-hooks.d.ts.map +1 -0
- package/dist/scaffold/claude-hooks.js +28 -0
- package/dist/scaffold/index.d.ts +2 -0
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/scaffold/index.js +90 -88
- package/dist/templates/.caws/schemas/scope.schema.json +52 -0
- package/dist/templates/.caws/schemas/working-spec.schema.json +1 -1
- package/dist/templates/.caws/schemas/worktrees.schema.json +36 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +33 -0
- package/dist/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +93 -6
- package/dist/templates/.claude/hooks/simplification-guard.sh +92 -0
- package/dist/templates/.cursor/README.md +0 -3
- package/dist/templates/.github/copilot-instructions.md +82 -0
- package/dist/templates/.junie/guidelines.md +73 -0
- package/dist/templates/.vscode/launch.json +0 -27
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +54 -0
- package/dist/templates/CLAUDE.md +101 -0
- package/dist/templates/agents.md +73 -1016
- package/dist/templates/docs/README.md +5 -5
- package/dist/test-analysis.d.ts +50 -1
- package/dist/test-analysis.d.ts.map +1 -1
- package/dist/utils/error-categories.d.ts +52 -0
- package/dist/utils/error-categories.d.ts.map +1 -0
- package/dist/utils/gitignore-updater.d.ts +1 -1
- package/dist/utils/gitignore-updater.d.ts.map +1 -1
- package/dist/utils/gitignore-updater.js +4 -0
- package/dist/utils/ide-detection.js +133 -0
- package/dist/utils/quality-gates-utils.d.ts +49 -0
- package/dist/utils/quality-gates-utils.d.ts.map +1 -0
- package/dist/utils/typescript-detector.d.ts +8 -5
- package/dist/utils/typescript-detector.d.ts.map +1 -1
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/worktree/worktree-manager.d.ts +54 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -0
- package/dist/worktree/worktree-manager.js +378 -0
- package/package.json +5 -1
- package/templates/.caws/schemas/scope.schema.json +52 -0
- package/templates/.caws/schemas/working-spec.schema.json +1 -1
- package/templates/.caws/schemas/worktrees.schema.json +36 -0
- package/templates/.claude/hooks/block-dangerous.sh +33 -0
- package/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
- package/templates/.claude/hooks/scope-guard.sh +93 -6
- package/templates/.claude/hooks/simplification-guard.sh +92 -0
- package/templates/.cursor/README.md +0 -3
- package/templates/.github/copilot-instructions.md +82 -0
- package/templates/.junie/guidelines.md +73 -0
- package/templates/.vscode/launch.json +0 -27
- package/templates/.windsurf/rules/caws-quality-standards.md +54 -0
- package/templates/AGENTS.md +104 -0
- package/templates/CLAUDE.md +101 -0
- package/templates/docs/README.md +5 -5
- package/templates/.github/copilot/instructions.md +0 -311
- package/templates/agents.md +0 -1047
package/dist/scaffold/index.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
86
|
+
console.log(chalk.yellow(`Warning: Git hooks setup failed: ${error.message}`));
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
//
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
171
|
+
} catch (_) {
|
|
175
172
|
// Ignore chmod errors on some systems
|
|
176
173
|
}
|
|
177
174
|
}
|
|
178
175
|
|
|
179
|
-
console.log(chalk.green(
|
|
176
|
+
console.log(chalk.green(`Added ${template.desc}`));
|
|
180
177
|
addedCount++;
|
|
181
178
|
} catch (error) {
|
|
182
|
-
console.log(chalk.red(
|
|
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(`\
|
|
188
|
-
console.log(chalk.
|
|
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
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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: '
|
|
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": "^
|
|
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
|
-
|
|
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?.
|
|
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?.
|
|
145
|
+
const inScope = spec.scope?.in || [];
|
|
59
146
|
if (inScope.length > 0) {
|
|
60
147
|
let found = false;
|
|
61
148
|
for (const pattern of inScope) {
|