@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.
@@ -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 ".caws" ]; then
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 \\(minimum 10 characters\\)"
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)(.+)? ]]; then
689
- echo "Conventional commit format detected"
866
+ if [[ $COMMIT_MSG =~ ^(feat|fix|docs|style|refactor|test|chore|merge|perf|wip)(\\(.*\\))?: ]]; then
867
+ : # valid format
690
868
  else
691
- echo "Consider using conventional commit format:"
692
- echo " feat: add new feature"
693
- echo " fix: bug fix"
694
- echo " docs: documentation"
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 the working spec's scope boundaries
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 file or scope.json exists
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
- // 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);
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
- // Check if file is in scope (if scope is explicitly defined)
145
- const inScope = spec.scope?.in || [];
146
- if (inScope.length > 0) {
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 inScope) {
149
- const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
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
- PATTERN="${SCOPE_CHECK#out_of_scope:}"
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 the working spec (pattern: '"$PATTERN"'). Editing it may cause scope creep. Please confirm this edit is intentional."
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 the working spec. Editing it may cause scope creep. Please confirm this edit is intentional."
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