@paths.design/caws-cli 10.0.1 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +13 -5
  2. package/dist/budget-derivation.js +221 -74
  3. package/dist/commands/agents.js +124 -0
  4. package/dist/commands/evaluate.js +26 -12
  5. package/dist/commands/gates.js +31 -4
  6. package/dist/commands/init.js +7 -4
  7. package/dist/commands/iterate.js +7 -3
  8. package/dist/commands/scope.js +264 -0
  9. package/dist/commands/sidecar.js +6 -3
  10. package/dist/commands/specs.js +359 -4
  11. package/dist/commands/status.js +29 -4
  12. package/dist/commands/templates.js +0 -8
  13. package/dist/commands/validate.js +34 -13
  14. package/dist/commands/verify-acs.js +25 -10
  15. package/dist/commands/waivers.js +147 -5
  16. package/dist/commands/worktree.js +200 -4
  17. package/dist/gates/budget-limit.js +6 -1
  18. package/dist/gates/scope-boundary.js +26 -7
  19. package/dist/gates/spec-completeness.js +8 -1
  20. package/dist/index.js +56 -0
  21. package/dist/policy/PolicyManager.js +14 -7
  22. package/dist/session/session-manager.js +34 -0
  23. package/dist/templates/.caws/schemas/policy.schema.json +101 -34
  24. package/dist/templates/.caws/schemas/scope.schema.json +3 -3
  25. package/dist/templates/.caws/schemas/waivers.schema.json +91 -21
  26. package/dist/templates/.caws/schemas/working-spec.schema.json +253 -89
  27. package/dist/templates/.caws/templates/working-spec.template.yml +3 -1
  28. package/dist/templates/.caws/tools/scope-guard.js +66 -15
  29. package/dist/templates/.claude/README.md +1 -1
  30. package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
  31. package/dist/templates/.claude/hooks/scope-guard.sh +106 -27
  32. package/dist/templates/.claude/hooks/worktree-write-guard.sh +96 -3
  33. package/dist/templates/.claude/rules/worktree-isolation.md +21 -3
  34. package/dist/templates/.claude/settings.json +5 -0
  35. package/dist/templates/CLAUDE.md +56 -0
  36. package/dist/templates/agents.md +47 -0
  37. package/dist/utils/agent-display.js +210 -0
  38. package/dist/utils/agent-session.js +142 -0
  39. package/dist/utils/event-log.js +584 -0
  40. package/dist/utils/event-renderer.js +521 -0
  41. package/dist/utils/schema-validator.js +10 -2
  42. package/dist/utils/working-state.js +25 -0
  43. package/dist/validation/spec-validation.js +102 -9
  44. package/dist/waivers-manager.js +84 -0
  45. package/dist/worktree/worktree-manager.js +593 -26
  46. package/package.json +5 -4
  47. package/templates/.caws/schemas/policy.schema.json +101 -34
  48. package/templates/.caws/schemas/scope.schema.json +3 -3
  49. package/templates/.caws/schemas/waivers.schema.json +91 -21
  50. package/templates/.caws/schemas/working-spec.schema.json +253 -89
  51. package/templates/.caws/templates/working-spec.template.yml +3 -1
  52. package/templates/.caws/tools/scope-guard.js +66 -15
  53. package/templates/.claude/README.md +1 -1
  54. package/templates/.claude/hooks/protected-paths.sh +39 -0
  55. package/templates/.claude/hooks/scope-guard.sh +106 -27
  56. package/templates/.claude/hooks/worktree-write-guard.sh +96 -3
  57. package/templates/.claude/rules/worktree-isolation.md +21 -3
  58. package/templates/.claude/settings.json +5 -0
  59. package/templates/CLAUDE.md +56 -0
  60. package/templates/agents.md +47 -0
@@ -204,31 +204,83 @@ if command -v node >/dev/null 2>&1; then
204
204
  process.exit(0);
205
205
  }
206
206
 
207
- // Collect all active specs (working-spec + feature specs)
208
- const specs = [];
207
+ const projectDir = '$PROJECT_DIR';
209
208
 
210
- // Load working-spec.yaml if present
211
- const mainSpec = '$SPEC_FILE';
212
- if (fs.existsSync(mainSpec)) {
213
- try {
214
- const s = yaml.load(fs.readFileSync(mainSpec, 'utf8'));
215
- if (s && !TERMINAL.has(s.status)) {
216
- specs.push({ source: 'working-spec', spec: s });
217
- }
218
- } catch (_) {}
209
+ // --- Authoritative spec detection ---
210
+ // If we are inside a worktree with a bound specId, ONLY check that spec.
211
+ // This prevents unrelated specs from blocking writes via broad scope.out.
212
+ let authoritativeSpec = null;
213
+ let mode = 'union';
214
+
215
+ const registryPath = path.join(projectDir, '.caws', 'worktrees.json');
216
+ const cwd = process.cwd();
217
+ const worktreesBase = path.join(projectDir, '.caws', 'worktrees');
218
+
219
+ if (cwd.startsWith(worktreesBase + '/')) {
220
+ const relative = cwd.slice(worktreesBase.length + 1);
221
+ const worktreeName = relative.split('/')[0];
222
+
223
+ if (worktreeName && fs.existsSync(registryPath)) {
224
+ try {
225
+ const reg = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
226
+ const entry = reg.worktrees && reg.worktrees[worktreeName];
227
+
228
+ if (entry && entry.specId) {
229
+ // Try to load the bound spec
230
+ const specsDir = '$SPECS_DIR';
231
+ const specCandidates = [
232
+ path.join(specsDir, entry.specId + '.yaml'),
233
+ path.join(specsDir, entry.specId + '.yml'),
234
+ ];
235
+ for (const candidate of specCandidates) {
236
+ if (fs.existsSync(candidate)) {
237
+ try {
238
+ const s = yaml.load(fs.readFileSync(candidate, 'utf8'));
239
+ if (s && !TERMINAL.has(s.status)) {
240
+ // Verify mutual binding: spec must also reference this worktree
241
+ if (s.worktree === worktreeName) {
242
+ authoritativeSpec = { source: path.basename(candidate), spec: s };
243
+ mode = 'authoritative';
244
+ }
245
+ }
246
+ } catch (_) {}
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ } catch (_) {}
252
+ }
219
253
  }
220
254
 
221
- // Load feature specs from .caws/specs/
222
- const specsDir = '$SPECS_DIR';
223
- if (fs.existsSync(specsDir)) {
224
- for (const f of fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
255
+ // --- Collect specs based on mode ---
256
+ const specs = [];
257
+
258
+ if (authoritativeSpec) {
259
+ // Authoritative: only the bound spec matters
260
+ specs.push(authoritativeSpec);
261
+ } else {
262
+ // Union: load all active specs
263
+ const mainSpec = '$SPEC_FILE';
264
+ if (fs.existsSync(mainSpec)) {
225
265
  try {
226
- const s = yaml.load(fs.readFileSync(path.join(specsDir, f), 'utf8'));
266
+ const s = yaml.load(fs.readFileSync(mainSpec, 'utf8'));
227
267
  if (s && !TERMINAL.has(s.status)) {
228
- specs.push({ source: f, spec: s });
268
+ specs.push({ source: 'working-spec', spec: s });
229
269
  }
230
270
  } catch (_) {}
231
271
  }
272
+
273
+ const specsDir = '$SPECS_DIR';
274
+ if (fs.existsSync(specsDir)) {
275
+ for (const f of fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
276
+ try {
277
+ const s = yaml.load(fs.readFileSync(path.join(specsDir, f), 'utf8'));
278
+ if (s && !TERMINAL.has(s.status)) {
279
+ specs.push({ source: f, spec: s });
280
+ }
281
+ } catch (_) {}
282
+ }
283
+ }
232
284
  }
233
285
 
234
286
  // No active specs — allow everything
@@ -237,18 +289,18 @@ if command -v node >/dev/null 2>&1; then
237
289
  process.exit(0);
238
290
  }
239
291
 
240
- // Check scope.out across ALL active specs — any match blocks
292
+ // Check scope.out — any match blocks
241
293
  for (const { source, spec } of specs) {
242
294
  for (const pattern of (spec.scope?.out || [])) {
243
295
  const regex = globToRegex(pattern);
244
296
  if (regex.test(filePath)) {
245
- console.log('out_of_scope:' + source + ':' + pattern);
297
+ console.log('out_of_scope:' + mode + ':' + source + ':' + pattern);
246
298
  process.exit(0);
247
299
  }
248
300
  }
249
301
  }
250
302
 
251
- // Union all scope.in patterns — file must match at least one
303
+ // scope.in — file must match at least one pattern
252
304
  const allInScope = specs.flatMap(({ spec }) => spec.scope?.in || []);
253
305
  if (allInScope.length > 0) {
254
306
  let found = false;
@@ -260,7 +312,7 @@ if command -v node >/dev/null 2>&1; then
260
312
  }
261
313
  }
262
314
  if (!found) {
263
- console.log('not_in_scope');
315
+ console.log('not_in_scope:' + mode);
264
316
  process.exit(0);
265
317
  }
266
318
  }
@@ -282,18 +334,45 @@ if command -v node >/dev/null 2>&1; then
282
334
 
283
335
  if [[ "$SCOPE_CHECK" == out_of_scope:* ]]; then
284
336
  DETAIL="${SCOPE_CHECK#out_of_scope:}"
285
- SOURCE="${DETAIL%%:*}"
286
- PATTERN="${DETAIL#*:}"
337
+ # Format: mode:source:pattern
338
+ MODE="${DETAIL%%:*}"
339
+ REST="${DETAIL#*:}"
340
+ SOURCE="${REST%%:*}"
341
+ PATTERN="${REST#*:}"
287
342
  echo "BLOCKED: $REL_PATH is excluded by scope.out in $SOURCE (pattern: $PATTERN)"
288
- echo " Scope allows: files not matching scope.out patterns"
289
- echo " To modify scope, update the spec's scope.out field"
343
+ if [[ "$MODE" == "union" ]]; then
344
+ echo " Mode: union (no authoritative spec bound to this worktree)"
345
+ echo " The scope guard is checking ALL active specs because the worktree<->spec"
346
+ echo " binding is missing. An unrelated spec may be blocking this edit."
347
+ echo " Fix: caws worktree bind <your-spec-id>"
348
+ echo " Diagnose: caws scope show"
349
+ else
350
+ echo " Mode: authoritative (checking only your bound spec)"
351
+ echo " To modify scope, update the spec's scope.out field"
352
+ fi
353
+ exit 2
354
+ fi
355
+
356
+ if [[ "$SCOPE_CHECK" == not_in_scope:* ]]; then
357
+ MODE="${SCOPE_CHECK#not_in_scope:}"
358
+ echo "BLOCKED: $REL_PATH is not in the defined scope.in of any active spec"
359
+ if [[ "$MODE" == "union" ]]; then
360
+ echo " Mode: union (no authoritative spec bound to this worktree)"
361
+ echo " The scope guard is checking ALL active specs because the worktree<->spec"
362
+ echo " binding is missing. Your file may be in a scope that no spec covers."
363
+ echo " Fix: caws worktree bind <your-spec-id>"
364
+ echo " Diagnose: caws scope show"
365
+ else
366
+ echo " Mode: authoritative (checking only your bound spec)"
367
+ echo " To modify scope, update the spec's scope.in field"
368
+ fi
290
369
  exit 2
291
370
  fi
292
371
 
372
+ # Legacy fallback for unqualified not_in_scope (shouldn't happen with updated logic)
293
373
  if [[ "$SCOPE_CHECK" == "not_in_scope" ]]; then
294
374
  echo "BLOCKED: $REL_PATH is not in the defined scope.in of any active spec"
295
- echo " Scope allows: files matching scope.in patterns in active specs"
296
- echo " To modify scope, update the spec's scope.in field"
375
+ echo " Diagnose: caws scope show"
297
376
  exit 2
298
377
  fi
299
378
  fi
@@ -66,14 +66,107 @@ if [[ "$WT_COUNT" -le 0 ]] 2>/dev/null; then
66
66
  exit 0
67
67
  fi
68
68
 
69
- # Allow edits to configuration and documentation (benign, no merge conflict risk)
69
+ # Main is blocked during active worktree work because shared unstaged state makes
70
+ # agents stash, checkpoint, or explain each other's edits. Keep direct main edits
71
+ # limited to coordination/docs/scratch paths, then use active spec scope below to
72
+ # permit only files no worktree claims.
70
73
  if [[ -n "$FILE_PATH" ]]; then
71
74
  case "$FILE_PATH" in
72
- */.claude/*|*/.caws/*) exit 0 ;;
73
- */docs/*) exit 0 ;;
75
+ .caws/*|*/.caws/*) exit 0 ;;
76
+ .claude/*|*/.claude/*) exit 0 ;;
77
+ .gitignore|*/.gitignore) exit 0 ;;
78
+ .tmp/*|*/.tmp/*) exit 0 ;;
79
+ tmp/*|*/tmp/*) exit 0 ;;
80
+ .archive/*|*/.archive/*) exit 0 ;;
81
+ .githooks/*|*/.githooks/*) exit 0 ;;
82
+ .github/*|*/.github/*) exit 0 ;;
83
+ docs/*|*/docs/*) exit 0 ;;
74
84
  esac
75
85
  fi
76
86
 
87
+ if [[ -n "$FILE_PATH" ]]; then
88
+ REL_PATH="$FILE_PATH"
89
+ if [[ "$FILE_PATH" == "$PROJECT_DIR"/* ]]; then
90
+ REL_PATH="${FILE_PATH#$PROJECT_DIR/}"
91
+ fi
92
+
93
+ SPEC_CONTENTION_CHECK=$(PROJECT_DIR="$PROJECT_DIR" CURRENT_BRANCH="$CURRENT_BRANCH" REL_PATH="$REL_PATH" node -e "
94
+ var fs = require('fs');
95
+ var path = require('path');
96
+ var yaml;
97
+
98
+ try {
99
+ yaml = require('js-yaml');
100
+ } catch (_) {
101
+ console.log('unknown:no-js-yaml');
102
+ process.exit(0);
103
+ }
104
+
105
+ function globToRegExp(pattern) {
106
+ return new RegExp(String(pattern).replace(/\\*/g, '.*').replace(/\\?/g, '.'));
107
+ }
108
+
109
+ try {
110
+ var projectDir = process.env.PROJECT_DIR;
111
+ var currentBranch = process.env.CURRENT_BRANCH;
112
+ var relPath = process.env.REL_PATH;
113
+ var registryPath = path.join(projectDir, '.caws', 'worktrees.json');
114
+ var registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
115
+ var worktrees = Object.values(registry.worktrees || {}).filter(function(w) {
116
+ return (w.status === 'active' || w.status === 'fresh' || w.status === 'merged') && w.baseBranch === currentBranch;
117
+ });
118
+
119
+ if (worktrees.length === 0) {
120
+ console.log('unknown:no-registry-worktrees');
121
+ process.exit(0);
122
+ }
123
+
124
+ for (var wi = 0; wi < worktrees.length; wi++) {
125
+ var wt = worktrees[wi];
126
+ if (!wt.specId) {
127
+ console.log('unknown:missing-specId:' + (wt.name || 'unnamed'));
128
+ process.exit(0);
129
+ }
130
+
131
+ var specPath = path.join(projectDir, '.caws', 'specs', wt.specId + '.yaml');
132
+ if (!fs.existsSync(specPath)) {
133
+ specPath = path.join(projectDir, '.caws', 'specs', wt.specId + '.yml');
134
+ }
135
+ if (!fs.existsSync(specPath)) {
136
+ console.log('unknown:missing-spec:' + wt.specId);
137
+ process.exit(0);
138
+ }
139
+
140
+ var spec = yaml.load(fs.readFileSync(specPath, 'utf8')) || {};
141
+ var scope = spec.scope || {};
142
+ var patterns = []
143
+ .concat(Array.isArray(scope.in) ? scope.in : [])
144
+ .concat(Array.isArray(scope.out) ? scope.out : []);
145
+
146
+ if (patterns.length === 0) {
147
+ console.log('unknown:missing-scope:' + wt.specId);
148
+ process.exit(0);
149
+ }
150
+
151
+ for (var pi = 0; pi < patterns.length; pi++) {
152
+ if (globToRegExp(patterns[pi]).test(relPath)) {
153
+ console.log('claimed:' + (wt.name || wt.specId) + ':' + patterns[pi]);
154
+ process.exit(0);
155
+ }
156
+ }
157
+ }
158
+
159
+ console.log('clear');
160
+ } catch (error) {
161
+ console.log('unknown:' + error.message);
162
+ }
163
+ " 2>/dev/null || echo "unknown:node-error")
164
+
165
+ if [[ "$SPEC_CONTENTION_CHECK" == "clear" ]]; then
166
+ exit 0
167
+ fi
168
+ fi
169
+
77
170
  # Allow edits during an active merge (conflict resolution).
78
171
  # The worktree-isolation rules explicitly permit merge commits on the base branch.
79
172
  # Conflict resolution requires Write/Edit on the conflicted files.
@@ -10,9 +10,27 @@ When multiple agents are working on this project, each agent MUST work in its ow
10
10
  ## Before starting work
11
11
 
12
12
  1. Check if worktrees exist: `caws worktree list` shows all active worktrees with last commit time and owner
13
- 2. If worktrees are active and you are on the base branch, switch to your assigned worktree
14
- 3. If no worktree exists for you, create one with `caws worktree create <name>` or `caws parallel setup <plan-file>`
15
- 4. **Never touch a worktree you did not create.** Do not destroy, prune, stash, or "clean up" another agent's worktree — even if it looks stale. Another agent may be actively working in it. If you think a worktree is abandoned, leave it alone and let the user decide.
13
+ 2. Check who's actually working: `caws agents list` shows registered sessions and their bound worktree/spec, formatted as `<sessionId>:<platform>`
14
+ 3. If you're inside a worktree, run `caws status` the Claim panel shows the current owner, last heartbeat, and any session-log path under `tmp/<sessionId>/`
15
+ 4. If worktrees are active and you are on the base branch, switch to your assigned worktree
16
+ 5. If no worktree exists for you, create one with `caws worktree create <name>` or `caws parallel setup <plan-file>`
17
+ 6. **Never touch a worktree you did not create.** Do not destroy, prune, stash, or "clean up" another agent's worktree — even if it looks stale. Another agent may be actively working in it. If you think a worktree is abandoned, leave it alone and let the user decide.
18
+
19
+ ## Foreign-claim soft-block
20
+
21
+ `caws worktree bind`, `merge`, and `claim` refuse to mutate a worktree whose `worktrees.json:owner` is a session id different from the current session — unless `--takeover` is supplied. The refusal prints a structured warning naming the claimer, the heartbeat age, any session-log pointer under `tmp/<sessionId>/`, and the exact `--takeover` command:
22
+
23
+ ```
24
+ Worktree 'wt-foo' is claimed by 8be65780-...:claude-code
25
+ Last heartbeat: 2026-04-27T17:04:00Z (23 min ago)
26
+ Session log: tmp/8be65780-72e0-4fc7-a989-4ebac148c18d
27
+ 15 turns, last turn 2026-04-27T17:26:49Z
28
+ To proceed: caws worktree claim wt-foo --takeover
29
+ ```
30
+
31
+ **Decision-gating uses session-id equality only.** A stale heartbeat is NOT authorization to take over — paused sessions are not ended sessions. Read the session log under `tmp/<sessionId>/` for context first. Take over only when you have explicit user authorization.
32
+
33
+ `--takeover` writes a durable `prior_owners` audit on the worktree entry (sessionId, platform, lastSeen-at-takeover, takenOver_at) so the handoff is traceable in `worktrees.json`, not just in agent memory.
16
34
 
17
35
  ## Forbidden operations when worktrees are active
18
36
 
@@ -29,6 +29,11 @@
29
29
  {
30
30
  "matcher": "Write|Edit",
31
31
  "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protected-paths.sh",
35
+ "timeout": 5
36
+ },
32
37
  {
33
38
  "type": "command",
34
39
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/worktree-write-guard.sh",
@@ -29,6 +29,9 @@ Before writing code, check the canonical spec for the current feature:
29
29
  # Create a feature spec for isolated work
30
30
  caws specs create FEAT-001 --type feature --title "description"
31
31
 
32
+ # If you're in a CAWS worktree, the created spec should record it:
33
+ # worktree: <worktree-name>
34
+
32
35
  # Validate the feature spec
33
36
  caws validate --spec-id FEAT-001
34
37
 
@@ -68,6 +71,7 @@ Canonical feature specs live at `.caws/specs/<ID>.yaml` (create with `caws specs
68
71
 
69
72
  - **Risk tier**: Quality requirements (T1: critical, T2: standard, T3: low risk)
70
73
  - **Mode**: The type of change (`feature`, `refactor`, `fix`, `doc`, `chore`) -- required
74
+ - **Worktree**: The owning CAWS worktree name for this spec (`worktree`) -- recommended for all isolated work
71
75
  - **Blast radius**: Which modules are affected (`blast_radius.modules`) -- required
72
76
  - **Operational rollback SLO**: Time target for rollback (e.g. `"30m"`) -- required
73
77
  - **Scope**: Which files you can edit (`scope.in`) and which are off-limits (`scope.out`)
@@ -76,6 +80,58 @@ Canonical feature specs live at `.caws/specs/<ID>.yaml` (create with `caws specs
76
80
 
77
81
  Always stay within scope boundaries and change budgets.
78
82
 
83
+ Recommended operating rule: one active feature spec, one active worktree. If a task has a worktree, record that ownership in the spec YAML with `worktree: <name>`.
84
+
85
+ ### Scope and Worktree Binding
86
+
87
+ The scope guard enforces file edit boundaries based on your spec's `scope.in` and `scope.out` patterns. **How it enforces depends on whether your worktree is bound to a spec:**
88
+
89
+ - **Authoritative mode** (worktree bound to a spec): Only your spec's scope patterns are checked. Other agents' specs cannot block your edits. This is the correct state.
90
+ - **Union mode** (no binding): The guard checks ALL active specs. Any `scope.out` from any spec can block you, even unrelated ones. This is the common source of "why is spec X blocking me?" confusion.
91
+
92
+ **The mutual binding** requires both sides:
93
+ 1. The worktree registry (`.caws/worktrees.json`) must have `specId` pointing to your spec
94
+ 2. Your spec (`.caws/specs/<id>.yaml`) must have `worktree: <name>` pointing to your worktree
95
+
96
+ If either side is missing, the guard falls back to union mode.
97
+
98
+ **Quick commands:**
99
+ ```bash
100
+ # See your effective scope and binding health
101
+ caws scope show
102
+
103
+ # Fix a broken binding
104
+ caws worktree bind <spec-id>
105
+
106
+ # Inspect the agent registry — who is currently working what
107
+ caws agents list
108
+
109
+ # Inspect a specific worktree's claim (read-only by default)
110
+ caws worktree claim <name>
111
+ ```
112
+
113
+ **Recovery checklist** (when the scope guard blocks you unexpectedly):
114
+ 1. Run `caws scope show` — check if you're in authoritative or union mode
115
+ 2. If union mode: bind your spec with `caws worktree bind <spec-id>`
116
+ 3. If authoritative but still blocked: the file is genuinely outside your spec's scope. Update your spec's `scope.in` if the file should be in scope, or request a waiver
117
+ 4. Do NOT modify another spec's `scope.out` to unblock yourself — that defeats the isolation
118
+
119
+ ### Agent Claims & Multi-Agent Coordination
120
+
121
+ Each session gets registered in `.caws/agents.json` automatically (via the session-log hook and on every CAWS lifecycle CLI invocation). Worktree session ownership is tracked in `.caws/worktrees.json:owner` as a session id.
122
+
123
+ `caws worktree bind`, `merge`, and `claim` will refuse to mutate a worktree owned by a different session id without explicit `--takeover`. The refusal prints a structured warning naming the claimer as `<sessionId>:<platform>`, the heartbeat age, and any matching `tmp/<sessionId>/` session-log path so you can read context before deciding.
124
+
125
+ **Decision-gating uses session-id equality only.** TTL pruning of `agents.json` is registry hygiene; it does NOT authorize takeover. A stale heartbeat doesn't mean the prior session is dead — it may be paused.
126
+
127
+ `--takeover` writes a durable `prior_owners` audit on the worktree entry (sessionId, platform, lastSeen-at-takeover, takenOver_at) so handoffs are traceable in `worktrees.json`, not just in agent memory.
128
+
129
+ ### Spec lifecycle: archive
130
+
131
+ Use `caws specs archive <id>` to move a closed spec to the canonical `.caws/specs/.archive/` directory. The directory is filesystem-authoritative — `caws specs list` reports any file under `.archive/` as `status: archived` regardless of the YAML literal. This means manually-moved legacy specs (no registry entry) are correctly classified.
132
+
133
+ If you try to `caws specs create <id>` for an id that already exists in `.archive/`, the command refuses without `--force`. With `--force`, the archived YAML is removed and a fresh draft is created — useful for resurrecting an old id with new intent.
134
+
79
135
  > **Budget note**: `change_budget:` in a spec is informational documentation only. CAWS
80
136
  > derives the enforced budget from `policy.yaml` keyed on `risk_tier`. The field in the
81
137
  > spec is not used by `caws validate` for enforcement.
@@ -38,6 +38,53 @@ caws specs create my-feature --type feature --title "My Feature"
38
38
  caws validate --spec-id my-feature
39
39
  ```
40
40
 
41
+ ## Scope and Worktree Binding
42
+
43
+ The scope guard enforces `scope.in` and `scope.out` from your spec. How it enforces depends on binding:
44
+
45
+ - **Authoritative mode** (worktree bound to a spec): Only your spec's scope is checked. Other agents' specs cannot block you.
46
+ - **Union mode** (no binding): ALL active specs are checked. Any `scope.out` from any spec can block you.
47
+
48
+ ```bash
49
+ # See your effective scope and binding health
50
+ caws scope show
51
+
52
+ # Fix a broken binding
53
+ caws worktree bind <spec-id>
54
+ ```
55
+
56
+ **Recovery** (when blocked unexpectedly):
57
+ 1. Run `caws scope show` to check mode and binding health
58
+ 2. If union mode: `caws worktree bind <spec-id>`
59
+ 3. If authoritative but blocked: update your spec's `scope.in`
60
+ 4. Do NOT edit another spec's `scope.out` to unblock yourself
61
+
62
+ ## Multi-Agent Claims
63
+
64
+ Each session is registered in `.caws/agents.json` automatically. Worktree session ownership is recorded in `.caws/worktrees.json:owner` as a session id. `caws worktree bind`, `merge`, and `claim` will refuse to mutate a worktree owned by a different session id without `--takeover`.
65
+
66
+ ```bash
67
+ # See registered agents (composite <sessionId>:<platform> format)
68
+ caws agents list
69
+
70
+ # Inspect a worktree's claim — read-only by default
71
+ caws worktree claim <name>
72
+
73
+ # Take over a foreign claim (writes prior_owners audit)
74
+ caws worktree claim <name> --takeover
75
+ ```
76
+
77
+ When a refusal fires, the warning includes the claimer's session id, heartbeat age, and a pointer to any `tmp/<sessionId>/` session-log directory — read that log for context before deciding to take over. A stale heartbeat does NOT mean the prior session is dead; it may be paused.
78
+
79
+ ## Spec Lifecycle: Archive
80
+
81
+ ```bash
82
+ # Move a closed spec to the canonical archive
83
+ caws specs archive <spec-id>
84
+ ```
85
+
86
+ The `.caws/specs/.archive/` directory is filesystem-authoritative — `caws specs list` reports any file under it as `archived` regardless of YAML status. `caws specs create` refuses ids that collide with archived files unless `--force` is supplied (which removes the archived copy and writes a fresh draft).
87
+
41
88
  ## Key Rules
42
89
 
43
90
  1. **Stay in scope** -- only edit files listed in `scope.in`, never touch `scope.out`