@paths.design/caws-cli 8.3.0 → 9.0.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/parallel.js +238 -0
- package/dist/commands/specs.js +55 -2
- package/dist/commands/status.js +13 -3
- package/dist/constants/spec-types.js +25 -2
- package/dist/index.js +43 -2
- package/dist/parallel/parallel-manager.js +443 -0
- package/dist/scaffold/claude-hooks.js +54 -1
- package/dist/scaffold/git-hooks.js +188 -14
- 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/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/validation/spec-validation.js +16 -0
- package/dist/worktree/worktree-manager.js +31 -21
- package/package.json +1 -1
- package/templates/.claude/hooks/scope-guard.sh +67 -21
- package/templates/.claude/hooks/session-caws-status.sh +117 -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
|
@@ -236,6 +236,128 @@ if [ "$YAML_VALIDATION_FAILED" = true ]; then
|
|
|
236
236
|
exit 1
|
|
237
237
|
fi
|
|
238
238
|
|
|
239
|
+
# ===== CAWS Multi-Agent Safety Guard =====
|
|
240
|
+
# Prevents unsafe concurrent operations on shared branches
|
|
241
|
+
|
|
242
|
+
if [ -d ".caws" ]; then
|
|
243
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
244
|
+
|
|
245
|
+
# Guard 1a: Block commits on base branch when parallel worktrees are active (caws parallel)
|
|
246
|
+
if [ -f ".caws/parallel.json" ] && command -v node >/dev/null 2>&1; then
|
|
247
|
+
PARALLEL_BASE=$(node -e "
|
|
248
|
+
try {
|
|
249
|
+
var reg = JSON.parse(require('fs').readFileSync('.caws/parallel.json', 'utf8'));
|
|
250
|
+
console.log(reg.baseBranch || '');
|
|
251
|
+
} catch(e) { console.log(''); }
|
|
252
|
+
" 2>/dev/null)
|
|
253
|
+
|
|
254
|
+
if [ -n "$PARALLEL_BASE" ] && [ "$CURRENT_BRANCH" = "$PARALLEL_BASE" ]; then
|
|
255
|
+
AGENT_COUNT=$(node -e "
|
|
256
|
+
try {
|
|
257
|
+
var reg = JSON.parse(require('fs').readFileSync('.caws/parallel.json', 'utf8'));
|
|
258
|
+
console.log((reg.agents || []).length);
|
|
259
|
+
} catch(e) { console.log('0'); }
|
|
260
|
+
" 2>/dev/null)
|
|
261
|
+
|
|
262
|
+
if [ "$AGENT_COUNT" -gt 0 ] 2>/dev/null; then
|
|
263
|
+
echo "BLOCKED: Committing to '$CURRENT_BRANCH' while $AGENT_COUNT parallel agent worktree(s) are active."
|
|
264
|
+
echo " Active agents are working in isolated worktrees."
|
|
265
|
+
echo " Committing to the base branch risks interleaved history and merge conflicts."
|
|
266
|
+
echo ""
|
|
267
|
+
echo " To see parallel status: caws parallel status"
|
|
268
|
+
echo " To merge agent work: caws parallel merge"
|
|
269
|
+
echo " To override (unsafe): git commit --no-verify"
|
|
270
|
+
exit 1
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
# Guard 1b: Block commits on base branch when ANY active worktrees exist (caws worktree create)
|
|
276
|
+
if [ -f ".caws/worktrees.json" ] && command -v node >/dev/null 2>&1; then
|
|
277
|
+
ACTIVE_WORKTREES=$(node -e "
|
|
278
|
+
try {
|
|
279
|
+
var reg = JSON.parse(require('fs').readFileSync('.caws/worktrees.json', 'utf8'));
|
|
280
|
+
var wts = Object.values(reg.worktrees || {});
|
|
281
|
+
var active = wts.filter(function(w) {
|
|
282
|
+
return w.status === 'active' && w.baseBranch === '$CURRENT_BRANCH';
|
|
283
|
+
});
|
|
284
|
+
console.log(active.length + ':' + active.map(function(w) { return w.name; }).join(','));
|
|
285
|
+
} catch(e) { console.log('0:'); }
|
|
286
|
+
" 2>/dev/null)
|
|
287
|
+
|
|
288
|
+
WT_COUNT=$(echo "$ACTIVE_WORKTREES" | cut -d: -f1)
|
|
289
|
+
WT_NAMES=$(echo "$ACTIVE_WORKTREES" | cut -d: -f2)
|
|
290
|
+
|
|
291
|
+
if [ "$WT_COUNT" -gt 0 ] 2>/dev/null; then
|
|
292
|
+
echo "BLOCKED: Committing to '$CURRENT_BRANCH' while $WT_COUNT active worktree(s) exist: $WT_NAMES"
|
|
293
|
+
echo " You should be working in your worktree, not on the base branch."
|
|
294
|
+
echo " Committing here risks interleaved history with agents in worktrees."
|
|
295
|
+
echo ""
|
|
296
|
+
echo " To work in your worktree: cd .caws/worktrees/<name>/"
|
|
297
|
+
echo " To see worktrees: caws worktree list"
|
|
298
|
+
echo " To override (unsafe): git commit --no-verify"
|
|
299
|
+
exit 1
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
# Guard 2: Warn if multiple active sessions exist on same branch
|
|
304
|
+
if [ -f ".caws/sessions.json" ] && command -v node >/dev/null 2>&1; then
|
|
305
|
+
ACTIVE_ON_BRANCH=$(node -e "
|
|
306
|
+
try {
|
|
307
|
+
var reg = JSON.parse(require('fs').readFileSync('.caws/sessions.json', 'utf8'));
|
|
308
|
+
var count = Object.values(reg.sessions || {}).filter(
|
|
309
|
+
function(s) { return s.status === 'active' && s.branch === '$CURRENT_BRANCH'; }
|
|
310
|
+
).length;
|
|
311
|
+
console.log(count);
|
|
312
|
+
} catch(e) { console.log('0'); }
|
|
313
|
+
" 2>/dev/null)
|
|
314
|
+
|
|
315
|
+
if [ "$ACTIVE_ON_BRANCH" -gt 1 ] 2>/dev/null; then
|
|
316
|
+
echo "WARNING: $ACTIVE_ON_BRANCH active sessions detected on branch '$CURRENT_BRANCH'."
|
|
317
|
+
echo " Multiple agents committing to the same branch risks interleaved history."
|
|
318
|
+
echo " Consider using worktrees: caws parallel setup <plan-file>"
|
|
319
|
+
echo ""
|
|
320
|
+
fi
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
# Guard 3: Block --amend when HEAD commit may not belong to current session
|
|
324
|
+
# Detect --amend by inspecting the parent git process arguments
|
|
325
|
+
AMEND_FLAG=false
|
|
326
|
+
if command -v ps >/dev/null 2>&1; then
|
|
327
|
+
PARENT_ARGS=$(ps -o args= -p $PPID 2>/dev/null || echo "")
|
|
328
|
+
case "$PARENT_ARGS" in
|
|
329
|
+
*--amend*) AMEND_FLAG=true ;;
|
|
330
|
+
esac
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
if [ "$AMEND_FLAG" = true ]; then
|
|
334
|
+
BLOCK_AMEND=false
|
|
335
|
+
if [ -f ".caws/parallel.json" ]; then
|
|
336
|
+
BLOCK_AMEND=true
|
|
337
|
+
elif [ -f ".caws/worktrees.json" ] && command -v node >/dev/null 2>&1; then
|
|
338
|
+
HAS_ACTIVE_WT=$(node -e "
|
|
339
|
+
try {
|
|
340
|
+
var reg = JSON.parse(require('fs').readFileSync('.caws/worktrees.json', 'utf8'));
|
|
341
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
|
|
342
|
+
console.log(active.length > 0 ? 'yes' : 'no');
|
|
343
|
+
} catch(e) { console.log('no'); }
|
|
344
|
+
" 2>/dev/null)
|
|
345
|
+
if [ "$HAS_ACTIVE_WT" = "yes" ]; then
|
|
346
|
+
BLOCK_AMEND=true
|
|
347
|
+
fi
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
if [ "$BLOCK_AMEND" = true ]; then
|
|
351
|
+
echo "BLOCKED: --amend is not allowed while worktrees are active."
|
|
352
|
+
echo " Amending commits risks rewriting another agent's work."
|
|
353
|
+
echo " Create a new commit instead."
|
|
354
|
+
echo " To override (dangerous): git commit --amend --no-verify"
|
|
355
|
+
exit 1
|
|
356
|
+
fi
|
|
357
|
+
fi
|
|
358
|
+
fi
|
|
359
|
+
# ===== End Multi-Agent Safety Guard =====
|
|
360
|
+
|
|
239
361
|
# Fallback chain for quality gates:
|
|
240
362
|
# 1. Try Node.js script (if exists)
|
|
241
363
|
# 2. Try CAWS CLI
|
|
@@ -665,37 +787,89 @@ echo "All quality gates passed - ready to push"
|
|
|
665
787
|
function generateCommitMsgHook() {
|
|
666
788
|
return `#!/bin/bash
|
|
667
789
|
# CAWS Commit Message Hook
|
|
668
|
-
# Validates commit message format
|
|
790
|
+
# Validates commit message format and enforces merge(worktree): convention
|
|
669
791
|
|
|
670
792
|
COMMIT_MSG_FILE=$1
|
|
671
793
|
|
|
672
794
|
# Read the commit message
|
|
673
795
|
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
|
674
796
|
|
|
797
|
+
# Resolve CAWS root (works from worktrees too)
|
|
798
|
+
CAWS_ROOT="."
|
|
799
|
+
if command -v git >/dev/null 2>&1; then
|
|
800
|
+
_GIT_COMMON=$(git rev-parse --git-common-dir 2>/dev/null || echo ".git")
|
|
801
|
+
if [ "$_GIT_COMMON" != ".git" ]; then
|
|
802
|
+
_CANDIDATE=$(cd "$_GIT_COMMON/.." 2>/dev/null && pwd || echo "")
|
|
803
|
+
if [ -n "$_CANDIDATE" ] && [ -d "$_CANDIDATE/.caws" ]; then
|
|
804
|
+
CAWS_ROOT="$_CANDIDATE"
|
|
805
|
+
fi
|
|
806
|
+
fi
|
|
807
|
+
fi
|
|
808
|
+
|
|
675
809
|
# Check if CAWS is initialized
|
|
676
|
-
if [ ! -d "
|
|
810
|
+
if [ ! -d "$CAWS_ROOT/.caws" ]; then
|
|
677
811
|
exit 0
|
|
678
812
|
fi
|
|
679
813
|
|
|
814
|
+
# ===== Worktree merge message guard =====
|
|
815
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
816
|
+
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo ".git")
|
|
817
|
+
HAS_ACTIVE_WORKTREES=false
|
|
818
|
+
|
|
819
|
+
if [ -f "$CAWS_ROOT/.caws/worktrees.json" ] && command -v node >/dev/null 2>&1; then
|
|
820
|
+
WT_COUNT=$(node -e "
|
|
821
|
+
try {
|
|
822
|
+
var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
|
|
823
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) {
|
|
824
|
+
return w.status === 'active' && w.baseBranch === '$CURRENT_BRANCH';
|
|
825
|
+
});
|
|
826
|
+
console.log(active.length);
|
|
827
|
+
} catch(e) { console.log('0'); }
|
|
828
|
+
" 2>/dev/null)
|
|
829
|
+
if [ "$WT_COUNT" -gt 0 ] 2>/dev/null; then
|
|
830
|
+
HAS_ACTIVE_WORKTREES=true
|
|
831
|
+
fi
|
|
832
|
+
fi
|
|
833
|
+
|
|
834
|
+
if [ "$HAS_ACTIVE_WORKTREES" = true ]; then
|
|
835
|
+
IS_GIT_MERGE=false
|
|
836
|
+
if [ -f "$GIT_DIR/MERGE_HEAD" ]; then
|
|
837
|
+
IS_GIT_MERGE=true
|
|
838
|
+
fi
|
|
839
|
+
|
|
840
|
+
if [[ "$COMMIT_MSG" =~ ^merge\\(worktree\\): ]] || [ "$IS_GIT_MERGE" = true ]; then
|
|
841
|
+
echo "Merge commit to base branch allowed (worktrees active)"
|
|
842
|
+
elif [[ "$COMMIT_MSG" =~ ^wip\\(checkpoint\\): ]]; then
|
|
843
|
+
echo "Checkpoint commit allowed (prior-session cleanup)"
|
|
844
|
+
else
|
|
845
|
+
echo "BLOCKED: Direct commit to '$CURRENT_BRANCH' while worktrees are active."
|
|
846
|
+
echo " Only these commit types are allowed on the base branch during parallel work:"
|
|
847
|
+
echo ""
|
|
848
|
+
echo " merge(worktree): <description> — merge a completed worktree branch"
|
|
849
|
+
echo " wip(checkpoint): <description> — commit prior-session dirty files"
|
|
850
|
+
echo " git merge --no-ff <branch> — git merge commit"
|
|
851
|
+
echo ""
|
|
852
|
+
echo " To override (unsafe): git commit --no-verify"
|
|
853
|
+
exit 1
|
|
854
|
+
fi
|
|
855
|
+
fi
|
|
856
|
+
# ===== End worktree merge message guard =====
|
|
857
|
+
|
|
680
858
|
# Basic commit message validation
|
|
681
859
|
if [ \${#COMMIT_MSG} -lt 10 ]; then
|
|
682
|
-
echo "Commit message too short
|
|
683
|
-
echo "Write descriptive commit messages"
|
|
860
|
+
echo "Commit message too short (minimum 10 characters)"
|
|
861
|
+
echo " Write descriptive commit messages"
|
|
684
862
|
exit 1
|
|
685
863
|
fi
|
|
686
864
|
|
|
687
865
|
# Check for conventional commit format (optional but encouraged)
|
|
688
|
-
if [[ $COMMIT_MSG =~ ^(feat|fix|docs|style|refactor|test|chore)(
|
|
689
|
-
|
|
866
|
+
if [[ $COMMIT_MSG =~ ^(feat|fix|docs|style|refactor|test|chore|merge|perf|wip)(\\(.*\\))?: ]]; then
|
|
867
|
+
: # valid format
|
|
690
868
|
else
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
echo " style: formatting"
|
|
696
|
-
echo " refactor: code restructuring"
|
|
697
|
-
echo " test: testing"
|
|
698
|
-
echo " chore: maintenance"
|
|
869
|
+
if [[ ! $COMMIT_MSG =~ ^Merge\\ (branch|remote) ]]; then
|
|
870
|
+
echo "Consider using conventional commit format:"
|
|
871
|
+
echo " feat: / fix: / docs: / refactor: / chore: / merge(worktree):"
|
|
872
|
+
fi
|
|
699
873
|
fi
|
|
700
874
|
|
|
701
875
|
echo "Commit message validation passed"
|
|
@@ -533,6 +533,19 @@ function findActiveSession(registry) {
|
|
|
533
533
|
return active.length > 0 ? active[0][0] : null;
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Find all active sessions on a specific branch
|
|
538
|
+
* @param {string} branch - Branch name to search
|
|
539
|
+
* @returns {Object[]} Active sessions on that branch with id and metadata
|
|
540
|
+
*/
|
|
541
|
+
function findActiveSessionsOnBranch(branch) {
|
|
542
|
+
const root = getRepoRoot();
|
|
543
|
+
const registry = loadRegistry(root);
|
|
544
|
+
return Object.entries(registry.sessions)
|
|
545
|
+
.filter(([, meta]) => meta.status === 'active' && meta.branch === branch)
|
|
546
|
+
.map(([id, meta]) => ({ id, ...meta }));
|
|
547
|
+
}
|
|
548
|
+
|
|
536
549
|
module.exports = {
|
|
537
550
|
startSession,
|
|
538
551
|
checkpointSession,
|
|
@@ -545,4 +558,5 @@ module.exports = {
|
|
|
545
558
|
SESSIONS_DIR,
|
|
546
559
|
REGISTRY_FILE,
|
|
547
560
|
CAPSULE_SCHEMA_VERSION,
|
|
561
|
+
findActiveSessionsOnBranch,
|
|
548
562
|
};
|
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Worktree Cleanup Reminder for Claude Code
|
|
3
|
+
# Warns at session end if active worktrees remain
|
|
4
|
+
# @author @darianrosebrook
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Read JSON input from Claude Code (required by hook protocol)
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
# Resolve main repo root
|
|
12
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
13
|
+
if command -v git >/dev/null 2>&1; then
|
|
14
|
+
GIT_COMMON_DIR=$(cd "$PROJECT_DIR" && git rev-parse --git-common-dir 2>/dev/null || echo "")
|
|
15
|
+
if [[ -n "$GIT_COMMON_DIR" ]] && [[ "$GIT_COMMON_DIR" != ".git" ]]; then
|
|
16
|
+
CANDIDATE=$(cd "$PROJECT_DIR" && cd "$GIT_COMMON_DIR/.." 2>/dev/null && pwd || echo "")
|
|
17
|
+
if [[ -n "$CANDIDATE" ]] && [[ -d "$CANDIDATE/.caws" ]]; then
|
|
18
|
+
PROJECT_DIR="$CANDIDATE"
|
|
19
|
+
fi
|
|
20
|
+
fi
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Check for active worktrees
|
|
24
|
+
if [[ -f "$PROJECT_DIR/.caws/worktrees.json" ]] && command -v node >/dev/null 2>&1; then
|
|
25
|
+
ACTIVE_INFO=$(node -e "
|
|
26
|
+
try {
|
|
27
|
+
var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/worktrees.json', 'utf8'));
|
|
28
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
|
|
29
|
+
if (active.length > 0) {
|
|
30
|
+
console.log(active.length + ':' + active.map(function(w) { return w.name; }).join(', '));
|
|
31
|
+
} else {
|
|
32
|
+
console.log('0:');
|
|
33
|
+
}
|
|
34
|
+
} catch(e) { console.log('0:'); }
|
|
35
|
+
" 2>/dev/null || echo "0:")
|
|
36
|
+
|
|
37
|
+
COUNT=$(echo "$ACTIVE_INFO" | cut -d: -f1)
|
|
38
|
+
NAMES=$(echo "$ACTIVE_INFO" | cut -d: -f2)
|
|
39
|
+
|
|
40
|
+
if [[ "$COUNT" -gt 0 ]] 2>/dev/null; then
|
|
41
|
+
echo "REMINDER: $COUNT active worktree(s) remain: $NAMES. Other agents cannot commit to the base branch until all worktrees are destroyed. If your work is complete, run: caws worktree destroy <name> --delete-branch" >&2
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
exit 0
|