@paths.design/caws-cli 8.0.1 → 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 -0
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/archive.js +114 -6
- package/dist/commands/burnup.d.ts.map +1 -1
- package/dist/commands/burnup.js +109 -10
- package/dist/commands/diagnose.js +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/mode.js +24 -14
- package/dist/commands/provenance.d.ts.map +1 -1
- package/dist/commands/provenance.js +216 -93
- package/dist/commands/quality-gates.d.ts.map +1 -1
- package/dist/commands/quality-gates.js +3 -1
- package/dist/commands/specs.d.ts.map +1 -1
- package/dist/commands/specs.js +184 -6
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +134 -10
- package/dist/commands/templates.js +2 -2
- 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/error-handler.js +6 -98
- package/dist/generators/jest-config-generator.d.ts +32 -0
- package/dist/generators/jest-config-generator.d.ts.map +1 -0
- package/dist/generators/jest-config-generator.js +242 -0
- package/dist/index.js +40 -7
- package/dist/minimal-cli.js +3 -1
- 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 +344 -0
- package/dist/scaffold/index.d.ts +2 -0
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/scaffold/index.js +96 -76
- 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/README.md +190 -0
- package/dist/templates/.claude/hooks/audit.sh +96 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +123 -0
- package/dist/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
- package/dist/templates/.claude/hooks/naming-check.sh +97 -0
- package/dist/templates/.claude/hooks/quality-check.sh +68 -0
- package/dist/templates/.claude/hooks/scan-secrets.sh +85 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +192 -0
- package/dist/templates/.claude/hooks/simplification-guard.sh +92 -0
- package/dist/templates/.claude/hooks/validate-spec.sh +76 -0
- package/dist/templates/.claude/settings.json +95 -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/test-analysis.js +203 -10
- package/dist/utils/error-categories.d.ts +52 -0
- package/dist/utils/error-categories.d.ts.map +1 -0
- package/dist/utils/error-categories.js +210 -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/quality-gates-utils.js +402 -0
- package/dist/utils/typescript-detector.d.ts +8 -5
- package/dist/utils/typescript-detector.d.ts.map +1 -1
- package/dist/utils/typescript-detector.js +36 -90
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/validation/spec-validation.js +59 -6
- 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 +9 -3
- 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/README.md +190 -0
- package/templates/.claude/hooks/audit.sh +96 -0
- package/templates/.claude/hooks/block-dangerous.sh +123 -0
- package/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
- package/templates/.claude/hooks/naming-check.sh +97 -0
- package/templates/.claude/hooks/quality-check.sh +68 -0
- package/templates/.claude/hooks/scan-secrets.sh +85 -0
- package/templates/.claude/hooks/scope-guard.sh +192 -0
- package/templates/.claude/hooks/simplification-guard.sh +92 -0
- package/templates/.claude/hooks/validate-spec.sh +76 -0
- package/templates/.claude/settings.json +95 -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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Quality Check Hook for Claude Code
|
|
3
|
+
# Runs CAWS quality validation after file edits
|
|
4
|
+
# @author @darianrosebrook
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Read JSON input from Claude Code
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
# Extract file info from PostToolUse input
|
|
12
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
13
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
14
|
+
|
|
15
|
+
# Only run on Write/Edit of source files
|
|
16
|
+
if [[ "$TOOL_NAME" != "Write" ]] && [[ "$TOOL_NAME" != "Edit" ]]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Skip non-source files and node_modules/dist
|
|
21
|
+
if [[ ! "$FILE_PATH" =~ \.(js|ts|jsx|tsx|py|go|rs|java|mjs|cjs)$ ]] || \
|
|
22
|
+
[[ "$FILE_PATH" =~ node_modules ]] || \
|
|
23
|
+
[[ "$FILE_PATH" =~ dist/ ]] || \
|
|
24
|
+
[[ "$FILE_PATH" =~ build/ ]]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Determine project directory
|
|
29
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
30
|
+
|
|
31
|
+
# Check if we're in a CAWS project
|
|
32
|
+
if [[ ! -f "$PROJECT_DIR/.caws/working-spec.yaml" ]]; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Check if CAWS CLI is available
|
|
37
|
+
if ! command -v caws &> /dev/null; then
|
|
38
|
+
# Suggest installing CAWS
|
|
39
|
+
echo '{
|
|
40
|
+
"hookSpecificOutput": {
|
|
41
|
+
"hookEventName": "PostToolUse",
|
|
42
|
+
"additionalContext": "CAWS CLI not available. Consider installing with: npm install -g @caws/cli"
|
|
43
|
+
}
|
|
44
|
+
}'
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Run CAWS quality gates in quiet mode for quick feedback
|
|
49
|
+
if caws quality-gates --context=commit --quiet 2>/dev/null; then
|
|
50
|
+
# Quality check passed - provide positive feedback
|
|
51
|
+
echo '{
|
|
52
|
+
"hookSpecificOutput": {
|
|
53
|
+
"hookEventName": "PostToolUse",
|
|
54
|
+
"additionalContext": "Quality gates passed for this change."
|
|
55
|
+
}
|
|
56
|
+
}'
|
|
57
|
+
else
|
|
58
|
+
# Quality check failed - provide feedback to Claude
|
|
59
|
+
# Run again to get violations summary
|
|
60
|
+
VIOLATIONS=$(caws quality-gates --context=commit --json 2>/dev/null | jq -r '.violations[:3] | .[] | "- \(.gate): \(.message)"' 2>/dev/null || echo "Run 'caws quality-gates' for details")
|
|
61
|
+
|
|
62
|
+
echo '{
|
|
63
|
+
"decision": "block",
|
|
64
|
+
"reason": "Quality gate violations detected. Please address the following issues before continuing:\n'"$VIOLATIONS"'\n\nRun '\''caws quality-gates'\'' for full details."
|
|
65
|
+
}'
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
exit 0
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Secret Scanner for Claude Code
|
|
3
|
+
# Warns when reading files that may contain secrets
|
|
4
|
+
# @author @darianrosebrook
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Read JSON input from Claude Code
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
# Extract file path
|
|
12
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
13
|
+
|
|
14
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Get just the filename for pattern matching
|
|
19
|
+
FILENAME=$(basename "$FILE_PATH")
|
|
20
|
+
|
|
21
|
+
# Files that commonly contain secrets
|
|
22
|
+
SECRET_FILE_PATTERNS=(
|
|
23
|
+
'.env'
|
|
24
|
+
'.env.local'
|
|
25
|
+
'.env.production'
|
|
26
|
+
'.env.development'
|
|
27
|
+
'.env.*'
|
|
28
|
+
'credentials.json'
|
|
29
|
+
'service-account.json'
|
|
30
|
+
'secrets.yaml'
|
|
31
|
+
'secrets.yml'
|
|
32
|
+
'secrets.json'
|
|
33
|
+
'.netrc'
|
|
34
|
+
'.npmrc'
|
|
35
|
+
'.pypirc'
|
|
36
|
+
'id_rsa'
|
|
37
|
+
'id_ed25519'
|
|
38
|
+
'id_ecdsa'
|
|
39
|
+
'*.pem'
|
|
40
|
+
'*.key'
|
|
41
|
+
'*.p12'
|
|
42
|
+
'*.pfx'
|
|
43
|
+
'htpasswd'
|
|
44
|
+
'shadow'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Directories that commonly contain secrets
|
|
48
|
+
SECRET_DIRS=(
|
|
49
|
+
'.ssh'
|
|
50
|
+
'.aws'
|
|
51
|
+
'.azure'
|
|
52
|
+
'.gcloud'
|
|
53
|
+
'.kube'
|
|
54
|
+
'.gnupg'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Check if file matches secret patterns
|
|
58
|
+
for pattern in "${SECRET_FILE_PATTERNS[@]}"; do
|
|
59
|
+
if [[ "$FILENAME" == $pattern ]]; then
|
|
60
|
+
# Output JSON with warning for Claude
|
|
61
|
+
echo '{
|
|
62
|
+
"hookSpecificOutput": {
|
|
63
|
+
"hookEventName": "PreToolUse",
|
|
64
|
+
"additionalContext": "WARNING: This file may contain secrets. Do not include sensitive values in your response. If you need to reference credentials, use placeholders like <API_KEY> instead of actual values."
|
|
65
|
+
}
|
|
66
|
+
}'
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
# Check if file is in a sensitive directory
|
|
72
|
+
for dir in "${SECRET_DIRS[@]}"; do
|
|
73
|
+
if [[ "$FILE_PATH" == *"/$dir/"* ]] || [[ "$FILE_PATH" == *"/$dir" ]]; then
|
|
74
|
+
echo '{
|
|
75
|
+
"hookSpecificOutput": {
|
|
76
|
+
"hookEventName": "PreToolUse",
|
|
77
|
+
"additionalContext": "WARNING: This file is in a sensitive directory that may contain secrets. Do not include any sensitive values in your response."
|
|
78
|
+
}
|
|
79
|
+
}'
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
done
|
|
83
|
+
|
|
84
|
+
# Allow the read
|
|
85
|
+
exit 0
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Scope Guard Hook for Claude Code
|
|
3
|
+
# Validates file edits against the working spec's scope boundaries
|
|
4
|
+
# @author @darianrosebrook
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Read JSON input from Claude Code
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
# Extract file path from PreToolUse input
|
|
12
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
13
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
14
|
+
|
|
15
|
+
# Only check Write/Edit operations
|
|
16
|
+
if [[ "$TOOL_NAME" != "Write" ]] && [[ "$TOOL_NAME" != "Edit" ]]; 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
|
+
SPEC_FILE="$PROJECT_DIR/.caws/working-spec.yaml"
|
|
26
|
+
SCOPE_FILE="$PROJECT_DIR/.caws/scope.json"
|
|
27
|
+
|
|
28
|
+
# Check if spec file or scope.json exists
|
|
29
|
+
if [[ ! -f "$SPEC_FILE" ]] && [[ ! -f "$SCOPE_FILE" ]]; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
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
|
|
121
|
+
|
|
122
|
+
# Use Node.js to parse YAML and check scope
|
|
123
|
+
if command -v node >/dev/null 2>&1; then
|
|
124
|
+
SCOPE_CHECK=$(node -e "
|
|
125
|
+
const yaml = require('js-yaml');
|
|
126
|
+
const fs = require('fs');
|
|
127
|
+
const path = require('path');
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const spec = yaml.load(fs.readFileSync('$SPEC_FILE', 'utf8'));
|
|
131
|
+
const filePath = '$REL_PATH';
|
|
132
|
+
|
|
133
|
+
// Check if file is explicitly out of scope
|
|
134
|
+
const outOfScope = spec.scope?.out || [];
|
|
135
|
+
for (const pattern of outOfScope) {
|
|
136
|
+
// Simple glob-like matching
|
|
137
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
|
|
138
|
+
if (regex.test(filePath)) {
|
|
139
|
+
console.log('out_of_scope:' + pattern);
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check if file is in scope (if scope is explicitly defined)
|
|
145
|
+
const inScope = spec.scope?.in || [];
|
|
146
|
+
if (inScope.length > 0) {
|
|
147
|
+
let found = false;
|
|
148
|
+
for (const pattern of inScope) {
|
|
149
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
|
|
150
|
+
if (regex.test(filePath)) {
|
|
151
|
+
found = true;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!found) {
|
|
156
|
+
console.log('not_in_scope');
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log('in_scope');
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.log('error:' + error.message);
|
|
164
|
+
}
|
|
165
|
+
" 2>&1)
|
|
166
|
+
|
|
167
|
+
if [[ "$SCOPE_CHECK" == out_of_scope:* ]]; then
|
|
168
|
+
PATTERN="${SCOPE_CHECK#out_of_scope:}"
|
|
169
|
+
echo '{
|
|
170
|
+
"hookSpecificOutput": {
|
|
171
|
+
"hookEventName": "PreToolUse",
|
|
172
|
+
"permissionDecision": "ask",
|
|
173
|
+
"permissionDecisionReason": "This file ('"$REL_PATH"') is marked as out-of-scope in the working spec (pattern: '"$PATTERN"'). Editing it may cause scope creep. Please confirm this edit is intentional."
|
|
174
|
+
}
|
|
175
|
+
}'
|
|
176
|
+
exit 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
if [[ "$SCOPE_CHECK" == "not_in_scope" ]]; then
|
|
180
|
+
echo '{
|
|
181
|
+
"hookSpecificOutput": {
|
|
182
|
+
"hookEventName": "PreToolUse",
|
|
183
|
+
"permissionDecision": "ask",
|
|
184
|
+
"permissionDecisionReason": "This file ('"$REL_PATH"') is not in the defined scope of the working spec. Editing it may cause scope creep. Please confirm this edit is intentional."
|
|
185
|
+
}
|
|
186
|
+
}'
|
|
187
|
+
exit 0
|
|
188
|
+
fi
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# File is in scope or scope couldn't be checked - allow
|
|
192
|
+
exit 0
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Simplification Guard Hook
|
|
3
|
+
# Detects when files are being stubbed out (large deletions + stub content)
|
|
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 Edit operations (modifications to existing files)
|
|
16
|
+
if [[ "$TOOL_NAME" != "Edit" ]]; 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
|
+
|
|
26
|
+
# Get relative path (portable — macOS realpath lacks --relative-to)
|
|
27
|
+
if [[ "$FILE_PATH" == "$PROJECT_DIR"/* ]]; then
|
|
28
|
+
REL_PATH="${FILE_PATH#$PROJECT_DIR/}"
|
|
29
|
+
else
|
|
30
|
+
REL_PATH="$FILE_PATH"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Only check code files
|
|
34
|
+
case "$REL_PATH" in
|
|
35
|
+
*.js|*.jsx|*.ts|*.tsx|*.mjs|*.cjs|*.py|*.rs|*.go|*.java|*.kt|*.swift|*.rb|*.php|*.c|*.cpp|*.h)
|
|
36
|
+
;;
|
|
37
|
+
*)
|
|
38
|
+
exit 0
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
|
|
42
|
+
# Check if the file exists in git (skip new files)
|
|
43
|
+
if ! git show "HEAD:$REL_PATH" >/dev/null 2>&1; then
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Compare staged version with HEAD
|
|
48
|
+
if command -v node >/dev/null 2>&1; then
|
|
49
|
+
SIMP_CHECK=$(node -e "
|
|
50
|
+
const { execFileSync } = require('child_process');
|
|
51
|
+
try {
|
|
52
|
+
const headContent = execFileSync('git', ['show', 'HEAD:$REL_PATH'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
53
|
+
const currentContent = require('fs').readFileSync('$FILE_PATH', 'utf8');
|
|
54
|
+
|
|
55
|
+
const countLOC = (c) => c.split('\\n').filter(l => l.trim() && !l.trim().startsWith('//') && !l.trim().startsWith('#')).length;
|
|
56
|
+
const headLOC = countLOC(headContent);
|
|
57
|
+
const currentLOC = countLOC(currentContent);
|
|
58
|
+
|
|
59
|
+
if (headLOC < 10) { console.log('ok'); process.exit(0); }
|
|
60
|
+
|
|
61
|
+
const decrease = (headLOC - currentLOC) / headLOC;
|
|
62
|
+
|
|
63
|
+
// Check for stub patterns in new content
|
|
64
|
+
const stubPatterns = [/^\\s*pass\\s*$/m, /^\\s*\\.\\.\\.\\s*$/m, /raise\\s+NotImplementedError/,
|
|
65
|
+
/throw\\s+new\\s+Error.*not implemented/i, /\\/\\/\\s*TODO\\s*$/m, /#\\s*TODO\\s*$/m];
|
|
66
|
+
const hasStubs = stubPatterns.some(p => p.test(currentContent));
|
|
67
|
+
|
|
68
|
+
if (decrease >= 0.3 && hasStubs) {
|
|
69
|
+
console.log('simplified:' + Math.round(decrease * 100) + ':' + headLOC + ':' + currentLOC);
|
|
70
|
+
} else {
|
|
71
|
+
console.log('ok');
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.log('ok');
|
|
75
|
+
}
|
|
76
|
+
" 2>&1)
|
|
77
|
+
|
|
78
|
+
if [[ "$SIMP_CHECK" == simplified:* ]]; then
|
|
79
|
+
IFS=':' read -r _ PERCENT OLD_LOC NEW_LOC <<< "$SIMP_CHECK"
|
|
80
|
+
echo '{
|
|
81
|
+
"hookSpecificOutput": {
|
|
82
|
+
"hookEventName": "PreToolUse",
|
|
83
|
+
"permissionDecision": "ask",
|
|
84
|
+
"permissionDecisionReason": "WARNING: This edit would reduce '"$REL_PATH"' by '"$PERCENT"'% ('"$OLD_LOC"' → '"$NEW_LOC"' LOC) and introduces stub patterns (pass, TODO, NotImplementedError). This looks like a simplification — implementations should be modified, not replaced with stubs. Please confirm this is intentional."
|
|
85
|
+
}
|
|
86
|
+
}'
|
|
87
|
+
exit 0
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Allow the edit
|
|
92
|
+
exit 0
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Spec Validation Hook for Claude Code
|
|
3
|
+
# Validates working-spec.yaml when it's edited
|
|
4
|
+
# @author @darianrosebrook
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Read JSON input from Claude Code
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
# Extract file path from PostToolUse input
|
|
12
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
13
|
+
|
|
14
|
+
# Only validate CAWS YAML files
|
|
15
|
+
if [[ "$FILE_PATH" != *".caws/"* ]] || ([[ "$FILE_PATH" != *.yaml ]] && [[ "$FILE_PATH" != *.yml ]]); then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
20
|
+
|
|
21
|
+
# First, validate YAML syntax using Node.js if available
|
|
22
|
+
if command -v node >/dev/null 2>&1; then
|
|
23
|
+
YAML_CHECK=$(node -e "
|
|
24
|
+
try {
|
|
25
|
+
const yaml = require('js-yaml');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const content = fs.readFileSync('$FILE_PATH', 'utf8');
|
|
28
|
+
yaml.load(content);
|
|
29
|
+
console.log('valid');
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(error.message);
|
|
32
|
+
if (error.mark) {
|
|
33
|
+
console.error('Line: ' + (error.mark.line + 1) + ', Column: ' + (error.mark.column + 1));
|
|
34
|
+
}
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
" 2>&1)
|
|
38
|
+
|
|
39
|
+
if [ $? -ne 0 ]; then
|
|
40
|
+
echo '{
|
|
41
|
+
"decision": "block",
|
|
42
|
+
"reason": "YAML syntax error in spec file:\n'"$YAML_CHECK"'\n\nPlease fix the syntax before continuing. Common issues:\n- Check indentation (YAML uses 2 spaces)\n- Ensure arrays use consistent format\n- Remove duplicate keys"
|
|
43
|
+
}'
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Run CAWS CLI validation if available
|
|
49
|
+
if command -v caws &> /dev/null; then
|
|
50
|
+
if VALIDATION=$(caws validate "$FILE_PATH" --quiet 2>&1); then
|
|
51
|
+
echo '{
|
|
52
|
+
"hookSpecificOutput": {
|
|
53
|
+
"hookEventName": "PostToolUse",
|
|
54
|
+
"additionalContext": "Spec validation passed. The specification is valid and complete."
|
|
55
|
+
}
|
|
56
|
+
}'
|
|
57
|
+
else
|
|
58
|
+
# Get suggestions
|
|
59
|
+
SUGGESTIONS=$(caws validate "$FILE_PATH" --suggestions 2>/dev/null | head -5 | tr '\n' ' ' || echo "Run 'caws validate --suggestions' for details")
|
|
60
|
+
|
|
61
|
+
echo '{
|
|
62
|
+
"decision": "block",
|
|
63
|
+
"reason": "Spec validation failed:\n'"$VALIDATION"'\n\nSuggestions:\n'"$SUGGESTIONS"'"
|
|
64
|
+
}'
|
|
65
|
+
fi
|
|
66
|
+
else
|
|
67
|
+
# Basic validation without CAWS CLI
|
|
68
|
+
echo '{
|
|
69
|
+
"hookSpecificOutput": {
|
|
70
|
+
"hookEventName": "PostToolUse",
|
|
71
|
+
"additionalContext": "CAWS CLI not available for full spec validation. Install with: npm install -g @caws/cli"
|
|
72
|
+
}
|
|
73
|
+
}'
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
exit 0
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh",
|
|
10
|
+
"timeout": 10
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Read",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scan-secrets.sh",
|
|
20
|
+
"timeout": 10
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"matcher": "Write|Edit",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scope-guard.sh",
|
|
30
|
+
"timeout": 10
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"PostToolUse": [
|
|
36
|
+
{
|
|
37
|
+
"matcher": "Write|Edit",
|
|
38
|
+
"hooks": [
|
|
39
|
+
{
|
|
40
|
+
"type": "command",
|
|
41
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/quality-check.sh",
|
|
42
|
+
"timeout": 30
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-spec.sh",
|
|
47
|
+
"timeout": 15
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "command",
|
|
51
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/naming-check.sh",
|
|
52
|
+
"timeout": 10
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit.sh tool-use",
|
|
57
|
+
"timeout": 5
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"matcher": "Bash",
|
|
63
|
+
"hooks": [
|
|
64
|
+
{
|
|
65
|
+
"type": "command",
|
|
66
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit.sh tool-use",
|
|
67
|
+
"timeout": 5
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"SessionStart": [
|
|
73
|
+
{
|
|
74
|
+
"hooks": [
|
|
75
|
+
{
|
|
76
|
+
"type": "command",
|
|
77
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit.sh session-start",
|
|
78
|
+
"timeout": 5
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"Stop": [
|
|
84
|
+
{
|
|
85
|
+
"hooks": [
|
|
86
|
+
{
|
|
87
|
+
"type": "command",
|
|
88
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit.sh stop",
|
|
89
|
+
"timeout": 5
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
}
|