@paths.design/caws-cli 8.3.0 → 9.1.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/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/parallel.d.ts +7 -0
- package/dist/commands/parallel.d.ts.map +1 -0
- package/dist/commands/parallel.js +238 -0
- package/dist/commands/session.d.ts +7 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/specs.d.ts +6 -0
- package/dist/commands/specs.d.ts.map +1 -1
- package/dist/commands/specs.js +55 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +13 -3
- package/dist/commands/tutorial.js +0 -2
- package/dist/commands/waivers.d.ts.map +1 -1
- package/dist/constants/spec-types.d.ts +52 -0
- package/dist/constants/spec-types.d.ts.map +1 -1
- package/dist/constants/spec-types.js +25 -2
- package/dist/index.js +43 -2
- package/dist/parallel/parallel-manager.d.ts +67 -0
- package/dist/parallel/parallel-manager.d.ts.map +1 -0
- package/dist/parallel/parallel-manager.js +440 -0
- package/dist/scaffold/claude-hooks.d.ts.map +1 -1
- package/dist/scaffold/claude-hooks.js +78 -2
- package/dist/scaffold/git-hooks.d.ts.map +1 -1
- package/dist/scaffold/git-hooks.js +237 -73
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/session/session-manager.d.ts +94 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +14 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +67 -21
- package/dist/templates/.claude/hooks/session-caws-status.sh +117 -0
- package/dist/templates/.claude/hooks/session-log.sh +528 -0
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +46 -0
- package/dist/templates/.claude/hooks/worktree-guard.sh +207 -0
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +84 -0
- package/dist/templates/.claude/rules/git-safety.md +26 -0
- package/dist/templates/.claude/rules/worktree-isolation.md +51 -0
- package/dist/templates/.claude/settings.json +15 -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 +3 -0
- package/dist/utils/ide-detection.d.ts +89 -0
- package/dist/utils/ide-detection.d.ts.map +1 -0
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/validation/spec-validation.js +16 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -1
- package/dist/worktree/worktree-manager.js +31 -21
- package/package.json +2 -2
- package/templates/.claude/hooks/scope-guard.sh +67 -21
- package/templates/.claude/hooks/session-caws-status.sh +117 -0
- package/templates/.claude/hooks/session-log.sh +528 -0
- package/templates/.claude/hooks/stop-worktree-check.sh +46 -0
- package/templates/.claude/hooks/worktree-guard.sh +207 -0
- package/templates/.claude/hooks/worktree-write-guard.sh +84 -0
- package/templates/.claude/rules/git-safety.md +26 -0
- package/templates/.claude/rules/worktree-isolation.md +51 -0
- package/templates/.claude/settings.json +15 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# CAWS Scope Guard Hook for Claude Code
|
|
3
|
-
# Validates file edits against
|
|
3
|
+
# Validates file edits against scope boundaries from working-spec + feature specs
|
|
4
|
+
# Specs with terminal status (completed, closed, archived) are skipped
|
|
4
5
|
# @author @darianrosebrook
|
|
5
6
|
|
|
6
7
|
set -euo pipefail
|
|
@@ -25,8 +26,8 @@ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
|
25
26
|
SPEC_FILE="$PROJECT_DIR/.caws/working-spec.yaml"
|
|
26
27
|
SCOPE_FILE="$PROJECT_DIR/.caws/scope.json"
|
|
27
28
|
|
|
28
|
-
# Check if spec
|
|
29
|
-
if [[ ! -f "$SPEC_FILE" ]] && [[ ! -f "$SCOPE_FILE" ]]; then
|
|
29
|
+
# Check if any spec infrastructure exists
|
|
30
|
+
if [[ ! -f "$SPEC_FILE" ]] && [[ ! -f "$SCOPE_FILE" ]] && [[ ! -d "$PROJECT_DIR/.caws/specs" ]]; then
|
|
30
31
|
exit 0
|
|
31
32
|
fi
|
|
32
33
|
|
|
@@ -119,7 +120,9 @@ if [[ ! -f "$SPEC_FILE" ]] && [[ -f "$SCOPE_FILE" ]]; then
|
|
|
119
120
|
fi
|
|
120
121
|
fi
|
|
121
122
|
|
|
122
|
-
# Use Node.js to parse YAML and check scope
|
|
123
|
+
# Use Node.js to parse YAML and check scope across working spec + active feature specs
|
|
124
|
+
SPECS_DIR="$PROJECT_DIR/.caws/specs"
|
|
125
|
+
|
|
123
126
|
if command -v node >/dev/null 2>&1; then
|
|
124
127
|
SCOPE_CHECK=$(node -e "
|
|
125
128
|
const yaml = require('js-yaml');
|
|
@@ -127,26 +130,67 @@ if command -v node >/dev/null 2>&1; then
|
|
|
127
130
|
const path = require('path');
|
|
128
131
|
|
|
129
132
|
try {
|
|
130
|
-
const spec = yaml.load(fs.readFileSync('$SPEC_FILE', 'utf8'));
|
|
131
133
|
const filePath = '$REL_PATH';
|
|
132
134
|
|
|
133
|
-
//
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
// Terminal statuses: specs that are done — scope no longer enforced
|
|
136
|
+
const TERMINAL = new Set(['completed', 'closed', 'archived']);
|
|
137
|
+
|
|
138
|
+
// Smart allowlist: root-level files, .caws/, .claude/ always pass
|
|
139
|
+
if (!filePath.includes('/') || filePath.startsWith('.caws/') || filePath.startsWith('.claude/')) {
|
|
140
|
+
console.log('in_scope');
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Collect all active specs (working-spec + feature specs)
|
|
145
|
+
const specs = [];
|
|
146
|
+
|
|
147
|
+
// Load working-spec.yaml if present
|
|
148
|
+
const mainSpec = '$SPEC_FILE';
|
|
149
|
+
if (fs.existsSync(mainSpec)) {
|
|
150
|
+
try {
|
|
151
|
+
const s = yaml.load(fs.readFileSync(mainSpec, 'utf8'));
|
|
152
|
+
if (s && !TERMINAL.has(s.status)) {
|
|
153
|
+
specs.push({ source: 'working-spec', spec: s });
|
|
154
|
+
}
|
|
155
|
+
} catch (_) {}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Load feature specs from .caws/specs/
|
|
159
|
+
const specsDir = '$SPECS_DIR';
|
|
160
|
+
if (fs.existsSync(specsDir)) {
|
|
161
|
+
for (const f of fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
|
|
162
|
+
try {
|
|
163
|
+
const s = yaml.load(fs.readFileSync(path.join(specsDir, f), 'utf8'));
|
|
164
|
+
if (s && !TERMINAL.has(s.status)) {
|
|
165
|
+
specs.push({ source: f, spec: s });
|
|
166
|
+
}
|
|
167
|
+
} catch (_) {}
|
|
141
168
|
}
|
|
142
169
|
}
|
|
143
170
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
171
|
+
// No active specs — allow everything
|
|
172
|
+
if (specs.length === 0) {
|
|
173
|
+
console.log('in_scope');
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check scope.out across ALL active specs — any match blocks
|
|
178
|
+
for (const { source, spec } of specs) {
|
|
179
|
+
for (const pattern of (spec.scope?.out || [])) {
|
|
180
|
+
const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
|
|
181
|
+
if (regex.test(filePath)) {
|
|
182
|
+
console.log('out_of_scope:' + source + ':' + pattern);
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Union all scope.in patterns — file must match at least one
|
|
189
|
+
const allInScope = specs.flatMap(({ spec }) => spec.scope?.in || []);
|
|
190
|
+
if (allInScope.length > 0) {
|
|
147
191
|
let found = false;
|
|
148
|
-
for (const pattern of
|
|
149
|
-
const regex = new RegExp(pattern.replace(
|
|
192
|
+
for (const pattern of allInScope) {
|
|
193
|
+
const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
|
|
150
194
|
if (regex.test(filePath)) {
|
|
151
195
|
found = true;
|
|
152
196
|
break;
|
|
@@ -165,12 +209,14 @@ if command -v node >/dev/null 2>&1; then
|
|
|
165
209
|
" 2>&1)
|
|
166
210
|
|
|
167
211
|
if [[ "$SCOPE_CHECK" == out_of_scope:* ]]; then
|
|
168
|
-
|
|
212
|
+
DETAIL="${SCOPE_CHECK#out_of_scope:}"
|
|
213
|
+
SOURCE="${DETAIL%%:*}"
|
|
214
|
+
PATTERN="${DETAIL#*:}"
|
|
169
215
|
echo '{
|
|
170
216
|
"hookSpecificOutput": {
|
|
171
217
|
"hookEventName": "PreToolUse",
|
|
172
218
|
"permissionDecision": "ask",
|
|
173
|
-
"permissionDecisionReason": "This file ('"$REL_PATH"') is marked as out-of-scope in
|
|
219
|
+
"permissionDecisionReason": "This file ('"$REL_PATH"') is marked as out-of-scope in '"$SOURCE"' (pattern: '"$PATTERN"'). Editing it may cause scope creep. Please confirm this edit is intentional."
|
|
174
220
|
}
|
|
175
221
|
}'
|
|
176
222
|
exit 0
|
|
@@ -181,7 +227,7 @@ if command -v node >/dev/null 2>&1; then
|
|
|
181
227
|
"hookSpecificOutput": {
|
|
182
228
|
"hookEventName": "PreToolUse",
|
|
183
229
|
"permissionDecision": "ask",
|
|
184
|
-
"permissionDecisionReason": "This file ('"$REL_PATH"') is not in the defined scope of
|
|
230
|
+
"permissionDecisionReason": "This file ('"$REL_PATH"') is not in the defined scope of any active spec. Editing it may cause scope creep. Please confirm this edit is intentional."
|
|
185
231
|
}
|
|
186
232
|
}'
|
|
187
233
|
exit 0
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Session Status Hook for Claude Code
|
|
3
|
+
# Reports project state at session start with worktree warnings
|
|
4
|
+
# @author @darianrosebrook
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Read stdin (required by hook protocol)
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
# Only run for session-start events
|
|
12
|
+
EVENT_TYPE="${1:-}"
|
|
13
|
+
if [ "$EVENT_TYPE" != "session-start" ]; then
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Check if this is a CAWS project
|
|
18
|
+
if [ ! -d "${CLAUDE_PROJECT_DIR:-.}/.caws" ]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
cd "${CLAUDE_PROJECT_DIR:-.}"
|
|
23
|
+
|
|
24
|
+
# --- Resolve main repo root ---
|
|
25
|
+
CAWS_ROOT="."
|
|
26
|
+
if command -v git >/dev/null 2>&1; then
|
|
27
|
+
_GIT_COMMON=$(git rev-parse --git-common-dir 2>/dev/null || echo ".git")
|
|
28
|
+
if [ "$_GIT_COMMON" != ".git" ]; then
|
|
29
|
+
_CANDIDATE=$(cd "$_GIT_COMMON/.." 2>/dev/null && pwd || echo "")
|
|
30
|
+
if [ -n "$_CANDIDATE" ] && [ -d "$_CANDIDATE/.caws" ]; then
|
|
31
|
+
CAWS_ROOT="$_CANDIDATE"
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# --- Active worktree warning ---
|
|
37
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
38
|
+
|
|
39
|
+
if [ -f "$CAWS_ROOT/.caws/worktrees.json" ] && command -v node >/dev/null 2>&1; then
|
|
40
|
+
WT_INFO=$(node -e "
|
|
41
|
+
try {
|
|
42
|
+
var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
|
|
43
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
|
|
44
|
+
if (active.length > 0) {
|
|
45
|
+
var names = active.map(function(w) { return w.name + ' (' + w.branch + ')'; });
|
|
46
|
+
console.log(active.length + ':' + names.join(', '));
|
|
47
|
+
} else {
|
|
48
|
+
console.log('0:');
|
|
49
|
+
}
|
|
50
|
+
} catch(e) { console.log('0:'); }
|
|
51
|
+
" 2>/dev/null || echo "0:")
|
|
52
|
+
|
|
53
|
+
WT_COUNT=$(echo "$WT_INFO" | cut -d: -f1)
|
|
54
|
+
WT_NAMES=$(echo "$WT_INFO" | cut -d: -f2)
|
|
55
|
+
|
|
56
|
+
if [ "$WT_COUNT" -gt 0 ] 2>/dev/null; then
|
|
57
|
+
# Check if the agent is already in a worktree (not on the base branch)
|
|
58
|
+
BASE_BRANCH=$(node -e "
|
|
59
|
+
try {
|
|
60
|
+
var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
|
|
61
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
|
|
62
|
+
if (active.length > 0) console.log(active[0].baseBranch || '');
|
|
63
|
+
else console.log('');
|
|
64
|
+
} catch(e) { console.log(''); }
|
|
65
|
+
" 2>/dev/null || echo "")
|
|
66
|
+
|
|
67
|
+
echo ""
|
|
68
|
+
echo "================================================================"
|
|
69
|
+
echo " ACTIVE WORKTREES DETECTED: $WT_COUNT worktree(s)"
|
|
70
|
+
echo " $WT_NAMES"
|
|
71
|
+
echo "================================================================"
|
|
72
|
+
|
|
73
|
+
if [ -n "$BASE_BRANCH" ] && [ "$CURRENT_BRANCH" = "$BASE_BRANCH" ]; then
|
|
74
|
+
echo ""
|
|
75
|
+
echo " You MUST work in a worktree, not on $CURRENT_BRANCH."
|
|
76
|
+
echo ""
|
|
77
|
+
echo " If a worktree was created for your task:"
|
|
78
|
+
echo " cd $CAWS_ROOT/.caws/worktrees/<name>/"
|
|
79
|
+
echo ""
|
|
80
|
+
echo " If you need a new worktree:"
|
|
81
|
+
echo " caws worktree create <name>"
|
|
82
|
+
echo ""
|
|
83
|
+
echo " The only operations allowed on $CURRENT_BRANCH are:"
|
|
84
|
+
echo " - git merge --no-ff <branch> (merge completed worktree work)"
|
|
85
|
+
echo " - Commits with message: merge(worktree): <description>"
|
|
86
|
+
echo " - Commits with message: wip(checkpoint): <description>"
|
|
87
|
+
echo " (for committing prior-session dirty files)"
|
|
88
|
+
echo ""
|
|
89
|
+
echo " Writing or editing files on $CURRENT_BRANCH will be BLOCKED"
|
|
90
|
+
echo " by the PreToolUse hook while worktrees are active."
|
|
91
|
+
else
|
|
92
|
+
echo ""
|
|
93
|
+
echo " You are on branch '$CURRENT_BRANCH' (worktree). Good."
|
|
94
|
+
echo " Other active worktrees: $WT_NAMES"
|
|
95
|
+
fi
|
|
96
|
+
echo "================================================================"
|
|
97
|
+
echo ""
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Use caws session briefing for structured output
|
|
102
|
+
if command -v caws &>/dev/null; then
|
|
103
|
+
caws session briefing 2>/dev/null || {
|
|
104
|
+
echo "--- CAWS Session Briefing (fallback) ---"
|
|
105
|
+
HEAD_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
106
|
+
BRANCH=$(git branch --show-current 2>/dev/null || echo "detached")
|
|
107
|
+
DIRTY_COUNT=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
108
|
+
echo "Git: ${BRANCH} @ ${HEAD_SHA} (${DIRTY_COUNT} dirty files)"
|
|
109
|
+
if [ "$DIRTY_COUNT" -gt 0 ]; then
|
|
110
|
+
echo "WARNING: Working tree has uncommitted changes from a prior session."
|
|
111
|
+
echo "Classify and commit or stash them before starting new work."
|
|
112
|
+
fi
|
|
113
|
+
echo "--- End CAWS Briefing ---"
|
|
114
|
+
}
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
exit 0
|