@paths.design/caws-cli 11.0.0 → 11.1.1

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.
Files changed (119) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +2 -2
  3. package/dist/init/harness-detect.d.ts +18 -0
  4. package/dist/init/harness-detect.d.ts.map +1 -0
  5. package/dist/init/harness-detect.js +90 -0
  6. package/dist/init/harness-detect.js.map +1 -0
  7. package/dist/init/hook-install.d.ts +53 -0
  8. package/dist/init/hook-install.d.ts.map +1 -0
  9. package/dist/init/hook-install.js +421 -0
  10. package/dist/init/hook-install.js.map +1 -0
  11. package/dist/init/hook-packs/manifest-claude-code.d.ts +4 -0
  12. package/dist/init/hook-packs/manifest-claude-code.d.ts.map +1 -0
  13. package/dist/init/hook-packs/manifest-claude-code.js +190 -0
  14. package/dist/init/hook-packs/manifest-claude-code.js.map +1 -0
  15. package/dist/init/hook-packs/register.d.ts +19 -0
  16. package/dist/init/hook-packs/register.d.ts.map +1 -0
  17. package/dist/init/hook-packs/register.js +37 -0
  18. package/dist/init/hook-packs/register.js.map +1 -0
  19. package/dist/init/hook-packs/types.d.ts +123 -0
  20. package/dist/init/hook-packs/types.d.ts.map +1 -0
  21. package/dist/init/hook-packs/types.js +29 -0
  22. package/dist/init/hook-packs/types.js.map +1 -0
  23. package/dist/shell/commands/gates.d.ts.map +1 -1
  24. package/dist/shell/commands/gates.js +28 -1
  25. package/dist/shell/commands/gates.js.map +1 -1
  26. package/dist/shell/commands/init.d.ts +9 -0
  27. package/dist/shell/commands/init.d.ts.map +1 -1
  28. package/dist/shell/commands/init.js +131 -27
  29. package/dist/shell/commands/init.js.map +1 -1
  30. package/dist/shell/commands/specs.d.ts +41 -0
  31. package/dist/shell/commands/specs.d.ts.map +1 -0
  32. package/dist/shell/commands/specs.js +264 -0
  33. package/dist/shell/commands/specs.js.map +1 -0
  34. package/dist/shell/commands/worktree.d.ts +38 -0
  35. package/dist/shell/commands/worktree.d.ts.map +1 -0
  36. package/dist/shell/commands/worktree.js +286 -0
  37. package/dist/shell/commands/worktree.js.map +1 -0
  38. package/dist/shell/gates/disposition.d.ts.map +1 -1
  39. package/dist/shell/gates/disposition.js +33 -3
  40. package/dist/shell/gates/disposition.js.map +1 -1
  41. package/dist/shell/gates/local-evaluators/budget-limit.d.ts +24 -0
  42. package/dist/shell/gates/local-evaluators/budget-limit.d.ts.map +1 -0
  43. package/dist/shell/gates/local-evaluators/budget-limit.js +67 -0
  44. package/dist/shell/gates/local-evaluators/budget-limit.js.map +1 -0
  45. package/dist/shell/gates/local-evaluators/diff-helpers.d.ts +25 -0
  46. package/dist/shell/gates/local-evaluators/diff-helpers.d.ts.map +1 -0
  47. package/dist/shell/gates/local-evaluators/diff-helpers.js +74 -0
  48. package/dist/shell/gates/local-evaluators/diff-helpers.js.map +1 -0
  49. package/dist/shell/gates/local-evaluators/index.d.ts +28 -0
  50. package/dist/shell/gates/local-evaluators/index.d.ts.map +1 -0
  51. package/dist/shell/gates/local-evaluators/index.js +67 -0
  52. package/dist/shell/gates/local-evaluators/index.js.map +1 -0
  53. package/dist/shell/gates/local-evaluators/scope-boundary.d.ts +23 -0
  54. package/dist/shell/gates/local-evaluators/scope-boundary.d.ts.map +1 -0
  55. package/dist/shell/gates/local-evaluators/scope-boundary.js +67 -0
  56. package/dist/shell/gates/local-evaluators/scope-boundary.js.map +1 -0
  57. package/dist/shell/gates/local-evaluators/spec-completeness.d.ts +12 -0
  58. package/dist/shell/gates/local-evaluators/spec-completeness.d.ts.map +1 -0
  59. package/dist/shell/gates/local-evaluators/spec-completeness.js +73 -0
  60. package/dist/shell/gates/local-evaluators/spec-completeness.js.map +1 -0
  61. package/dist/shell/index.d.ts +4 -0
  62. package/dist/shell/index.d.ts.map +1 -1
  63. package/dist/shell/index.js +13 -1
  64. package/dist/shell/index.js.map +1 -1
  65. package/dist/shell/register.d.ts.map +1 -1
  66. package/dist/shell/register.js +192 -2
  67. package/dist/shell/register.js.map +1 -1
  68. package/dist/shell/render/init-hook-pack.d.ts +16 -0
  69. package/dist/shell/render/init-hook-pack.d.ts.map +1 -0
  70. package/dist/shell/render/init-hook-pack.js +206 -0
  71. package/dist/shell/render/init-hook-pack.js.map +1 -0
  72. package/dist/store/atomic-write.d.ts +20 -2
  73. package/dist/store/atomic-write.d.ts.map +1 -1
  74. package/dist/store/atomic-write.js +44 -2
  75. package/dist/store/atomic-write.js.map +1 -1
  76. package/dist/store/lifecycle-lock.d.ts +34 -0
  77. package/dist/store/lifecycle-lock.d.ts.map +1 -0
  78. package/dist/store/lifecycle-lock.js +168 -0
  79. package/dist/store/lifecycle-lock.js.map +1 -0
  80. package/dist/store/lifecycle-transaction.d.ts +79 -0
  81. package/dist/store/lifecycle-transaction.d.ts.map +1 -0
  82. package/dist/store/lifecycle-transaction.js +319 -0
  83. package/dist/store/lifecycle-transaction.js.map +1 -0
  84. package/dist/store/rules.d.ts +16 -0
  85. package/dist/store/rules.d.ts.map +1 -1
  86. package/dist/store/rules.js +17 -0
  87. package/dist/store/rules.js.map +1 -1
  88. package/dist/store/specs-writer.d.ts +61 -0
  89. package/dist/store/specs-writer.d.ts.map +1 -0
  90. package/dist/store/specs-writer.js +506 -0
  91. package/dist/store/specs-writer.js.map +1 -0
  92. package/dist/store/worktrees-writer.d.ts +77 -0
  93. package/dist/store/worktrees-writer.d.ts.map +1 -0
  94. package/dist/store/worktrees-writer.js +674 -0
  95. package/dist/store/worktrees-writer.js.map +1 -0
  96. package/dist/store/yaml-patch.d.ts +7 -0
  97. package/dist/store/yaml-patch.d.ts.map +1 -0
  98. package/dist/store/yaml-patch.js +250 -0
  99. package/dist/store/yaml-patch.js.map +1 -0
  100. package/package.json +7 -4
  101. package/templates/hook-packs/claude-code/CLAUDE.md +172 -0
  102. package/templates/hook-packs/claude-code/audit.sh +121 -0
  103. package/templates/hook-packs/claude-code/block-dangerous.sh +158 -0
  104. package/templates/hook-packs/claude-code/classify_command.py +1064 -0
  105. package/templates/hook-packs/claude-code/dispatch/post_tool_use.sh +63 -0
  106. package/templates/hook-packs/claude-code/dispatch/pre_tool_use.sh +50 -0
  107. package/templates/hook-packs/claude-code/dispatch/session_start.sh +41 -0
  108. package/templates/hook-packs/claude-code/dispatch/stop.sh +37 -0
  109. package/templates/hook-packs/claude-code/guard-strikes.sh +140 -0
  110. package/templates/hook-packs/claude-code/lib/parse-input.sh +127 -0
  111. package/templates/hook-packs/claude-code/lib/run-handlers.sh +212 -0
  112. package/templates/hook-packs/claude-code/reset-danger-latch.sh +21 -0
  113. package/templates/hook-packs/claude-code/reset-strikes.sh +243 -0
  114. package/templates/hook-packs/claude-code/runtime-paths.sh +80 -0
  115. package/templates/hook-packs/claude-code/scope-guard.sh +392 -0
  116. package/templates/hook-packs/claude-code/session-caws-status.sh +171 -0
  117. package/templates/hook-packs/claude-code/session-log.sh +180 -0
  118. package/templates/hook-packs/claude-code/worktree-guard.sh +240 -0
  119. package/templates/hook-packs/claude-code/worktree-write-guard.sh +77 -0
@@ -0,0 +1,392 @@
1
+ #!/bin/bash
2
+ # CAWS-MANAGED-HOOK
3
+ # hook_pack: claude-code
4
+ # hook_pack_version: 2
5
+ # caws_min_major: 11
6
+ # lineage_refs: 8,11,12,16
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ #
9
+ # CAWS Scope Guard Hook for Claude Code (v11-shape).
10
+ # Validates file edits against scope boundaries from per-feature specs under .caws/specs/.
11
+ #
12
+ # Lifecycle resolution (v11-shape, with v10 fallback):
13
+ # lifecycle_state first, status second.
14
+ # Terminal (not enforced): closed, archived, completed.
15
+ # active: participates in union enforcement.
16
+ # draft: does NOT participate in union-wide blocking unless authoritative/bound.
17
+ # Both fields missing: treat as active (legacy compatibility).
18
+ #
19
+ # Worktree registry shape compatibility:
20
+ # v11 direct-key: { "<name>": { ... } }
21
+ # v10 nested: { "worktrees": { "<name>": { ... } } }
22
+ # Bound id key: specId (v10) OR spec_id (v11) — both accepted.
23
+
24
+ set -euo pipefail
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ # shellcheck source=lib/parse-input.sh
28
+ source "$SCRIPT_DIR/lib/parse-input.sh"
29
+ # shellcheck source=guard-strikes.sh
30
+ source "$SCRIPT_DIR/guard-strikes.sh"
31
+ parse_hook_input
32
+
33
+ # Back-compat aliases kept to minimize diff in the scope-resolution logic below.
34
+ FILE_PATH="$HOOK_FILE_PATH"
35
+ TOOL_NAME="$HOOK_TOOL_NAME"
36
+ SESSION_ID="$HOOK_SESSION_ID"
37
+
38
+ # Only check Write/Edit operations
39
+ if [[ "$TOOL_NAME" != "Write" ]] && [[ "$TOOL_NAME" != "Edit" ]]; then
40
+ exit 0
41
+ fi
42
+
43
+ if [[ -z "$FILE_PATH" ]]; then
44
+ exit 0
45
+ fi
46
+
47
+ emit_scope_progression() {
48
+ local detail="$1"
49
+ # Strike-level diagnostic triage: strike 1 fires often (any agent
50
+ # touching the edge of its lane) and the edit proceeds — keep the
51
+ # message short so it informs without burying. Strike 2 escalates to
52
+ # user-approval and adds the spec/binding-fix options. Strike 3 is the
53
+ # hard block and surfaces the full reset-strikes + binding guidance.
54
+ local fix_options="Fix options: (1) edit a file already in scope, (2) update the bound spec's scope.in if this path should be in scope, (3) ask the user."
55
+ local hard_block_guidance="If prior strikes from earlier edits are cornering this session and the scope is now correct, ask the user to run: bash .claude/hooks/reset-strikes.sh --current (or --session <uuid>) to clear stale strike state. Verify the worktree binding: the spec must declare 'worktree: <name>' and .caws/worktrees.json must map that same worktree name to the correct 'specId' (v10) or 'spec_id' (v11). On CAWS v11.0 the worktree lifecycle CLI is not yet restored; on v11.1+ use 'caws worktree bind'. Do not edit .claude/hooks/, .claude/logs/guard-strikes-*.json, or other guard state to bypass this check."
56
+
57
+ guard_enforce_progressive_strikes \
58
+ "$SESSION_ID" \
59
+ "scope_guard" \
60
+ "$WORK_DIR" \
61
+ "Scope guard strike 1 of 3 for '$REL_PATH'. This edit proceeds, but a second out-of-scope edit will require user approval. $detail" \
62
+ "Scope guard strike 2 of 3 for '$REL_PATH'. Blocked — asking the user for approval. $detail $fix_options" \
63
+ "Scope guard strike 3 of 3 for '$REL_PATH'. Hard-blocked until scope is corrected. $detail $fix_options $hard_block_guidance"
64
+ }
65
+
66
+ resolve_worktree_root() {
67
+ local candidate="${1:-}"
68
+
69
+ if [[ -n "$candidate" ]] && [[ "$candidate" =~ ^(.*\/\.caws\/worktrees\/[^/]+)($|/) ]]; then
70
+ printf '%s\n' "${BASH_REMATCH[1]}"
71
+ return 0
72
+ fi
73
+
74
+ return 1
75
+ }
76
+
77
+ # Always-allowed paths bypass scope checks entirely.
78
+ ALLOW_PREFIXES=(
79
+ "$HOME/.claude/"
80
+ ".caws/"
81
+ ".claude/"
82
+ "docs/"
83
+ "tests/"
84
+ "scripts/"
85
+ "tmp/"
86
+ ".archive/"
87
+ )
88
+
89
+ # Policy-declared non-governed zones (CAWSFIX-26 / ledger D9).
90
+ POLICY_FILE="${CLAUDE_PROJECT_DIR:-.}/.caws/policy.yaml"
91
+ if [[ -f "$POLICY_FILE" ]]; then
92
+ while IFS= read -r raw_zone; do
93
+ [[ -z "$raw_zone" ]] && continue
94
+ raw_zone="${raw_zone%\"}"; raw_zone="${raw_zone#\"}"
95
+ raw_zone="${raw_zone%\'}"; raw_zone="${raw_zone#\'}"
96
+ raw_zone="${raw_zone%/\*\*}"
97
+ raw_zone="${raw_zone%/\*}"
98
+ [[ "$raw_zone" != */ ]] && raw_zone="${raw_zone}/"
99
+ ALLOW_PREFIXES+=("$raw_zone")
100
+ done < <(awk '
101
+ /^non_governed_zones:[[:space:]]*$/ { in_zones = 1; next }
102
+ /^[^[:space:]#-]/ && in_zones { in_zones = 0 }
103
+ in_zones && /^[[:space:]]+-[[:space:]]+/ {
104
+ sub(/^[[:space:]]+-[[:space:]]+/, "")
105
+ sub(/[[:space:]]+#.*$/, "")
106
+ print
107
+ }
108
+ ' "$POLICY_FILE" 2>/dev/null)
109
+ fi
110
+
111
+ WORK_DIR="${HOOK_CWD:-${CLAUDE_PROJECT_DIR:-.}}"
112
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
113
+
114
+ FILE_WORKTREE_ROOT="$(resolve_worktree_root "$FILE_PATH" || true)"
115
+ CWD_WORKTREE_ROOT="$(resolve_worktree_root "$HOOK_CWD" || true)"
116
+ PROJECT_WORKTREE_ROOT="$(resolve_worktree_root "$PROJECT_DIR" || true)"
117
+
118
+ if [[ -n "$FILE_WORKTREE_ROOT" ]]; then
119
+ WORK_DIR="$FILE_WORKTREE_ROOT"
120
+ elif [[ -n "$CWD_WORKTREE_ROOT" ]]; then
121
+ WORK_DIR="$CWD_WORKTREE_ROOT"
122
+ elif [[ -n "$PROJECT_WORKTREE_ROOT" ]]; then
123
+ WORK_DIR="$PROJECT_WORKTREE_ROOT"
124
+ fi
125
+
126
+ PROJECT_DIR="$(cd "$PROJECT_DIR" 2>/dev/null && pwd || printf '%s\n' "$PROJECT_DIR")"
127
+ WORK_DIR="$(cd "$WORK_DIR" 2>/dev/null && pwd || printf '%s\n' "$WORK_DIR")"
128
+ WORKTREE_NAME=""
129
+ if [[ "$WORK_DIR" =~ \/\.caws\/worktrees\/([^/]+)$ ]]; then
130
+ WORKTREE_NAME="${BASH_REMATCH[1]}"
131
+ fi
132
+
133
+ if [[ -d "$WORK_DIR/.caws/specs" ]]; then
134
+ SCOPE_FILE="$WORK_DIR/.caws/scope.json"
135
+ SPECS_BASE="$WORK_DIR"
136
+ else
137
+ SCOPE_FILE="$PROJECT_DIR/.caws/scope.json"
138
+ SPECS_BASE="$PROJECT_DIR"
139
+ fi
140
+
141
+ if [[ ! -f "$SCOPE_FILE" ]] && [[ ! -d "$SPECS_BASE/.caws/specs" ]]; then
142
+ exit 0
143
+ fi
144
+
145
+ if [[ "$FILE_PATH" == "$WORK_DIR"/* ]]; then
146
+ REL_PATH="${FILE_PATH#$WORK_DIR/}"
147
+ elif [[ "$FILE_PATH" == "$PROJECT_DIR"/* ]]; then
148
+ REL_PATH="${FILE_PATH#$PROJECT_DIR/}"
149
+ else
150
+ REL_PATH="$FILE_PATH"
151
+ fi
152
+
153
+ if [[ "$REL_PATH" != */* ]]; then
154
+ exit 0
155
+ fi
156
+ for prefix in "${ALLOW_PREFIXES[@]}"; do
157
+ if [[ "$FILE_PATH" == "${prefix}"* ]] || [[ "$REL_PATH" == "${prefix}"* ]]; then
158
+ exit 0
159
+ fi
160
+ done
161
+
162
+ # Lite mode: scope.json (no .caws/specs/)
163
+ if [[ ! -d "$SPECS_BASE/.caws/specs" ]] && [[ -f "$SCOPE_FILE" ]]; then
164
+ if command -v node >/dev/null 2>&1; then
165
+ LITE_CHECK=$(node -e "
166
+ var fs = require('fs');
167
+ var path = require('path');
168
+ try {
169
+ var scope = JSON.parse(fs.readFileSync('$SCOPE_FILE', 'utf8'));
170
+ var filePath = '$REL_PATH';
171
+ var dirs = scope.allowedDirectories || [];
172
+ var banned = scope.bannedPatterns || {};
173
+
174
+ var basename = path.basename(filePath);
175
+ var bannedFiles = banned.files || [];
176
+ for (var i = 0; i < bannedFiles.length; i++) {
177
+ var regex = new RegExp(bannedFiles[i].replace(/\\*/g, '.*').replace(/\\?/g, '.'));
178
+ if (regex.test(basename)) {
179
+ console.log('banned:' + bannedFiles[i]);
180
+ process.exit(0);
181
+ }
182
+ }
183
+
184
+ var bannedDocs = banned.docs || [];
185
+ for (var i = 0; i < bannedDocs.length; i++) {
186
+ var regex = new RegExp(bannedDocs[i].replace(/\\*/g, '.*').replace(/\\?/g, '.'));
187
+ if (regex.test(basename)) {
188
+ console.log('banned:' + bannedDocs[i]);
189
+ process.exit(0);
190
+ }
191
+ }
192
+
193
+ if (dirs.length > 0) {
194
+ var normalized = filePath.replace(/\\\\\\\\/g, '/');
195
+ var found = false;
196
+ for (var i = 0; i < dirs.length; i++) {
197
+ var d = dirs[i].replace(/\\/$/, '');
198
+ if (normalized.startsWith(d + '/') || normalized === d) { found = true; break; }
199
+ }
200
+ if (!found) {
201
+ console.log('not_allowed');
202
+ process.exit(0);
203
+ }
204
+ }
205
+ console.log('allowed');
206
+ } catch (error) {
207
+ console.log('error:' + error.message);
208
+ }
209
+ " 2>&1)
210
+
211
+ if [[ "$LITE_CHECK" == banned:* ]]; then
212
+ PATTERN="${LITE_CHECK#banned:}"
213
+ emit_scope_progression "This file matches banned pattern '$PATTERN' in .caws/scope.json."
214
+ exit 0
215
+ fi
216
+
217
+ if [[ "$LITE_CHECK" == "not_allowed" ]]; then
218
+ emit_scope_progression "This file is outside the allowed directories in .caws/scope.json."
219
+ exit 0
220
+ fi
221
+
222
+ exit 0
223
+ fi
224
+ fi
225
+
226
+ # Full mode: per-feature specs under .caws/specs/ (v11-shape aware)
227
+ SPECS_DIR="$SPECS_BASE/.caws/specs"
228
+
229
+ if command -v node >/dev/null 2>&1; then
230
+ SCOPE_CHECK=$(node -e "
231
+ var yaml = require('js-yaml');
232
+ var fs = require('fs');
233
+ var path = require('path');
234
+
235
+ try {
236
+ var filePath = '$REL_PATH';
237
+ var projectDir = '$PROJECT_DIR';
238
+ var worktreeName = '$WORKTREE_NAME';
239
+
240
+ // v11-shape lifecycle resolution.
241
+ // Read lifecycle_state first, fall back to status, then 'active'.
242
+ function lifecycleOf(s) {
243
+ return (s && (s.lifecycle_state || s.status)) || 'active';
244
+ }
245
+ // Terminal: not enforced at all.
246
+ var TERMINAL = { closed: 1, archived: 1, completed: 1 };
247
+ // Draft: does not participate in union-wide blocking. Only enforces
248
+ // scope when it is the authoritative/bound spec.
249
+ function isDraft(state) { return state === 'draft'; }
250
+
251
+ // Collect all non-terminal per-feature specs under .caws/specs/.
252
+ // Draft specs are collected but separately tagged.
253
+ var specs = [];
254
+
255
+ var specsDir = '$SPECS_DIR';
256
+ if (fs.existsSync(specsDir)) {
257
+ var files = fs.readdirSync(specsDir).filter(function(f) { return f.endsWith('.yaml') || f.endsWith('.yml'); });
258
+ for (var fi = 0; fi < files.length; fi++) {
259
+ try {
260
+ var s = yaml.load(fs.readFileSync(path.join(specsDir, files[fi]), 'utf8'));
261
+ if (!s) continue;
262
+ var state = lifecycleOf(s);
263
+ if (TERMINAL[state]) continue;
264
+ specs.push({ source: files[fi], spec: s, state: state });
265
+ } catch (_) {}
266
+ }
267
+ }
268
+
269
+ if (specs.length === 0) {
270
+ console.log('in_scope');
271
+ process.exit(0);
272
+ }
273
+
274
+ // Authoritative binding lookup (v10 + v11 registry shape compat).
275
+ function worktreeEntry(registry, name) {
276
+ if (!registry) return null;
277
+ if (registry.worktrees && registry.worktrees[name]) return registry.worktrees[name];
278
+ if (registry[name] && typeof registry[name] === 'object') return registry[name];
279
+ return null;
280
+ }
281
+ function boundSpecIdOf(entry) {
282
+ if (!entry) return null;
283
+ return entry.specId || entry.spec_id || null;
284
+ }
285
+
286
+ var authoritativeSpec = null;
287
+ if (worktreeName) {
288
+ try {
289
+ var registryPath = path.join(projectDir, '.caws', 'worktrees.json');
290
+ if (fs.existsSync(registryPath)) {
291
+ var registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
292
+ var entry = worktreeEntry(registry, worktreeName);
293
+ var boundId = boundSpecIdOf(entry);
294
+ if (boundId) {
295
+ for (var si = 0; si < specs.length; si++) {
296
+ var candidate = specs[si].spec || {};
297
+ if (candidate.id === boundId && candidate.worktree === worktreeName) {
298
+ authoritativeSpec = specs[si];
299
+ break;
300
+ }
301
+ }
302
+ }
303
+ }
304
+ } catch (_) {}
305
+ }
306
+
307
+ var mode = authoritativeSpec ? 'authoritative' : 'union';
308
+ var specsToCheck;
309
+ if (authoritativeSpec) {
310
+ specsToCheck = [authoritativeSpec];
311
+ } else {
312
+ // Union mode: drafts do NOT participate. Only active specs.
313
+ specsToCheck = specs.filter(function(s) { return !isDraft(s.state); });
314
+ if (specsToCheck.length === 0) {
315
+ // Only drafts present, none authoritative — allow.
316
+ console.log('in_scope');
317
+ process.exit(0);
318
+ }
319
+ }
320
+
321
+ // Check scope.out across applicable specs — any match blocks
322
+ for (var si = 0; si < specsToCheck.length; si++) {
323
+ var outPatterns = (specsToCheck[si].spec.scope && specsToCheck[si].spec.scope.out) || [];
324
+ for (var pi = 0; pi < outPatterns.length; pi++) {
325
+ var regex = new RegExp(outPatterns[pi].replace(/\\*/g, '.*').replace(/\\?/g, '.'));
326
+ if (regex.test(filePath)) {
327
+ console.log('out_of_scope:' + mode + ':' + specsToCheck[si].source + ':' + outPatterns[pi]);
328
+ process.exit(0);
329
+ }
330
+ }
331
+ }
332
+
333
+ // Union all scope.in patterns — file must match at least one
334
+ var allInScope = [];
335
+ for (var si = 0; si < specsToCheck.length; si++) {
336
+ var inPatterns = (specsToCheck[si].spec.scope && specsToCheck[si].spec.scope.in) || [];
337
+ for (var pi = 0; pi < inPatterns.length; pi++) {
338
+ allInScope.push(inPatterns[pi]);
339
+ }
340
+ }
341
+ if (allInScope.length > 0) {
342
+ var found = false;
343
+ for (var pi = 0; pi < allInScope.length; pi++) {
344
+ var regex = new RegExp(allInScope[pi].replace(/\\*/g, '.*').replace(/\\?/g, '.'));
345
+ if (regex.test(filePath)) {
346
+ found = true;
347
+ break;
348
+ }
349
+ }
350
+ if (!found) {
351
+ console.log('not_in_scope:' + mode);
352
+ process.exit(0);
353
+ }
354
+ }
355
+
356
+ console.log('in_scope');
357
+ } catch (error) {
358
+ console.log('error:' + error.message);
359
+ }
360
+ " 2>&1)
361
+
362
+ if [[ "$SCOPE_CHECK" == out_of_scope:* ]]; then
363
+ DETAIL="${SCOPE_CHECK#out_of_scope:}"
364
+ MODE="${DETAIL%%:*}"
365
+ REST="${DETAIL#*:}"
366
+ SOURCE="${REST%%:*}"
367
+ PATTERN="${REST#*:}"
368
+ if [[ "$MODE" == "union" ]]; then
369
+ emit_scope_progression "This file is marked out-of-scope in '$SOURCE' by pattern '$PATTERN'. Mode: union (no authoritative spec bound). An unrelated spec may be blocking this edit. Diagnose: caws scope show."
370
+ else
371
+ emit_scope_progression "This file is marked out-of-scope in '$SOURCE' by pattern '$PATTERN'. Mode: authoritative (checking only your bound spec)."
372
+ fi
373
+ exit 0
374
+ fi
375
+
376
+ if [[ "$SCOPE_CHECK" == not_in_scope:* ]]; then
377
+ MODE="${SCOPE_CHECK#not_in_scope:}"
378
+ if [[ "$MODE" == "union" ]]; then
379
+ emit_scope_progression "This file is not in the defined scope of any active spec. Mode: union (no authoritative spec bound). Diagnose: caws scope show."
380
+ else
381
+ emit_scope_progression "This file is not in the defined scope of your bound spec. Mode: authoritative. Update your spec's scope.in if this file should be in scope."
382
+ fi
383
+ exit 0
384
+ fi
385
+
386
+ if [[ "$SCOPE_CHECK" == "not_in_scope" ]]; then
387
+ emit_scope_progression "This file is not in the defined scope of any active spec. Diagnose: caws scope show."
388
+ exit 0
389
+ fi
390
+ fi
391
+
392
+ exit 0
@@ -0,0 +1,171 @@
1
+ #!/bin/bash
2
+ # CAWS-MANAGED-HOOK
3
+ # hook_pack: claude-code
4
+ # hook_pack_version: 2
5
+ # caws_min_major: 11
6
+ # lineage_refs: 4,11
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ #
9
+ # CAWS Session Status Hook for Claude Code (v11-shape).
10
+ # Fires on session-start. Surfaces:
11
+ # - active-worktree warning (dual-shape registry compatible)
12
+ # - global vs repo CAWS version skew warning
13
+ # - caws status briefing (v11-shape)
14
+ # Never blocks; emits to stdout for the agent's session start.
15
+
16
+ set -euo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ # shellcheck source=lib/parse-input.sh
20
+ source "$SCRIPT_DIR/lib/parse-input.sh"
21
+ # Hook does not read stdin fields -- dispatches on a positional arg.
22
+ # Sourcing parse-input.sh still wires up PATH (nvm/homebrew) for CAWS CLI.
23
+
24
+ EVENT_TYPE="${1:-}"
25
+ if [ "$EVENT_TYPE" != "session-start" ]; then
26
+ exit 0
27
+ fi
28
+
29
+ if ! command -v caws &>/dev/null; then
30
+ echo "CAWS CLI not found. Install with: npm install -g @paths.design/caws-cli"
31
+ exit 0
32
+ fi
33
+
34
+ if [ ! -d "${CLAUDE_PROJECT_DIR:-.}/.caws" ]; then
35
+ exit 0
36
+ fi
37
+
38
+ cd "${CLAUDE_PROJECT_DIR:-.}"
39
+
40
+ CAWS_ROOT="."
41
+ if command -v git >/dev/null 2>&1; then
42
+ _GIT_COMMON=$(git rev-parse --git-common-dir 2>/dev/null || echo ".git")
43
+ if [ "$_GIT_COMMON" != ".git" ]; then
44
+ _CANDIDATE=$(cd "$_GIT_COMMON/.." 2>/dev/null && pwd || echo "")
45
+ if [ -n "$_CANDIDATE" ] && [ -d "$_CANDIDATE/.caws" ]; then
46
+ CAWS_ROOT="$_CANDIDATE"
47
+ fi
48
+ fi
49
+ fi
50
+
51
+ # --- Active-worktree warning (dual-shape registry compatible) ---
52
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
53
+
54
+ if [ -f "$CAWS_ROOT/.caws/worktrees.json" ] && command -v node >/dev/null 2>&1; then
55
+ WT_INFO=$(node -e "
56
+ try {
57
+ var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
58
+ function entriesOf(r) {
59
+ if (!r || typeof r !== 'object') return [];
60
+ if (r.worktrees && typeof r.worktrees === 'object') return Object.values(r.worktrees);
61
+ // v11 direct-key: filter to objects with a 'status' field.
62
+ var out = [];
63
+ for (var k in r) {
64
+ if (Object.prototype.hasOwnProperty.call(r, k)) {
65
+ var v = r[k];
66
+ if (v && typeof v === 'object' && typeof v.status === 'string') {
67
+ // For v11 direct-key, the worktree name is the outer key,
68
+ // not entry.name. Synthesize name from key when absent.
69
+ if (!v.name) v = Object.assign({}, v, { name: k });
70
+ out.push(v);
71
+ }
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+ var entries = entriesOf(reg);
77
+ var active = entries.filter(function(w) { return w.status === 'active'; });
78
+ if (active.length > 0) {
79
+ var names = active.map(function(w) { return (w.name || '<unknown>') + ' (' + (w.branch || '?') + ')'; });
80
+ var bases = active.map(function(w) { return w.baseBranch || ''; }).filter(function(v,i,a) { return v && a.indexOf(v) === i; });
81
+ console.log(active.length + ':' + names.join(', ') + ':' + bases.join(','));
82
+ } else {
83
+ console.log('0::');
84
+ }
85
+ } catch(e) { console.log('0::'); }
86
+ " 2>/dev/null || echo "0::")
87
+
88
+ WT_COUNT=$(echo "$WT_INFO" | cut -d: -f1)
89
+ WT_NAMES=$(echo "$WT_INFO" | cut -d: -f2)
90
+ WT_BASES=$(echo "$WT_INFO" | cut -d: -f3)
91
+
92
+ if [ "$WT_COUNT" -gt 0 ] 2>/dev/null; then
93
+ BASE_BRANCH=$(echo "$WT_BASES" | cut -d',' -f1)
94
+
95
+ echo ""
96
+ echo "================================================================"
97
+ echo " ACTIVE WORKTREES DETECTED: $WT_COUNT worktree(s)"
98
+ echo " $WT_NAMES"
99
+ echo "================================================================"
100
+
101
+ if [ -n "$BASE_BRANCH" ] && [ "$CURRENT_BRANCH" = "$BASE_BRANCH" ]; then
102
+ echo ""
103
+ echo " Worktrees are preferred for isolated feature work, but direct"
104
+ echo " checkpoint edits on $CURRENT_BRANCH are allowed."
105
+ echo ""
106
+ echo " If a worktree was created for your task:"
107
+ echo " cd $CAWS_ROOT/.caws/worktrees/<name>/"
108
+ echo ""
109
+ echo " Worktree lifecycle commands (create/destroy/merge) return in"
110
+ echo " CAWS v11.1+; if you are on v11.0 they are not yet available."
111
+ echo ""
112
+ else
113
+ echo ""
114
+ echo " You are on branch '$CURRENT_BRANCH' (worktree). Good."
115
+ echo " Other active worktrees: $WT_NAMES"
116
+ fi
117
+ echo "================================================================"
118
+ echo ""
119
+ fi
120
+ fi
121
+
122
+ # --- Version-skew warning (advisory, never blocks) ---
123
+ #
124
+ # Hooks parse local CAWS state directly. The global `caws` binary may be a
125
+ # different major version than the repo's caws-cli — for example, an
126
+ # operator has the v10 binary globally installed while editing a v11 repo,
127
+ # or vice versa during transitions. Diagnostics from a mismatched binary
128
+ # can recommend commands that do not exist in the target version.
129
+ if command -v caws >/dev/null 2>&1 && command -v node >/dev/null 2>&1; then
130
+ GLOBAL_VER="$(caws --version 2>/dev/null | head -1 | tr -d '[:space:]' || echo '')"
131
+ GLOBAL_MAJOR="${GLOBAL_VER%%.*}"
132
+ REPO_PKG_JSON=""
133
+ for cand in \
134
+ "$CAWS_ROOT/packages/caws-cli/package.json" \
135
+ "$CAWS_ROOT/node_modules/@paths.design/caws-cli/package.json"; do
136
+ if [ -f "$cand" ]; then REPO_PKG_JSON="$cand"; break; fi
137
+ done
138
+ if [ -n "$REPO_PKG_JSON" ] && [ -n "$GLOBAL_MAJOR" ]; then
139
+ REPO_VER="$(node -e "
140
+ try { console.log((require('$REPO_PKG_JSON').version || '').trim()); }
141
+ catch(e) { console.log(''); }
142
+ " 2>/dev/null || echo '')"
143
+ REPO_MAJOR="${REPO_VER%%.*}"
144
+ if [ -n "$REPO_MAJOR" ] && [ "$REPO_MAJOR" != "$GLOBAL_MAJOR" ]; then
145
+ echo ""
146
+ echo "WARNING: global caws major version ($GLOBAL_MAJOR) differs from repo caws-cli major version ($REPO_MAJOR)."
147
+ echo "Hooks parse local state directly, but any CLI advice in diagnostics may be invalid."
148
+ echo "Consider: npm install -g @paths.design/caws-cli@^$REPO_MAJOR"
149
+ echo ""
150
+ fi
151
+ fi
152
+ fi
153
+
154
+ # --- CAWS status briefing (v11-shape) ---
155
+ # v11 replaces `caws session briefing` with `caws status`. Fall back if unavailable.
156
+ if caws status >/dev/null 2>&1; then
157
+ caws status 2>/dev/null || true
158
+ else
159
+ echo "--- CAWS Session Briefing (fallback) ---"
160
+ HEAD_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
161
+ BRANCH=$(git branch --show-current 2>/dev/null || echo "detached")
162
+ DIRTY_COUNT=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
163
+ echo "Git: ${BRANCH} @ ${HEAD_SHA} (${DIRTY_COUNT} dirty files)"
164
+ if [ "$DIRTY_COUNT" -gt 0 ]; then
165
+ echo "WARNING: Working tree has uncommitted changes from a prior session."
166
+ echo "Classify and commit or stash them before starting new work."
167
+ fi
168
+ echo "--- End CAWS Briefing ---"
169
+ fi
170
+
171
+ exit 0