@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.
- package/README.md +13 -5
- package/dist/budget-derivation.js +221 -74
- package/dist/commands/agents.js +124 -0
- package/dist/commands/evaluate.js +26 -12
- package/dist/commands/gates.js +31 -4
- package/dist/commands/init.js +7 -4
- package/dist/commands/iterate.js +7 -3
- package/dist/commands/scope.js +264 -0
- package/dist/commands/sidecar.js +6 -3
- package/dist/commands/specs.js +359 -4
- package/dist/commands/status.js +29 -4
- package/dist/commands/templates.js +0 -8
- package/dist/commands/validate.js +34 -13
- package/dist/commands/verify-acs.js +25 -10
- package/dist/commands/waivers.js +147 -5
- package/dist/commands/worktree.js +200 -4
- package/dist/gates/budget-limit.js +6 -1
- package/dist/gates/scope-boundary.js +26 -7
- package/dist/gates/spec-completeness.js +8 -1
- package/dist/index.js +56 -0
- package/dist/policy/PolicyManager.js +14 -7
- package/dist/session/session-manager.js +34 -0
- package/dist/templates/.caws/schemas/policy.schema.json +101 -34
- package/dist/templates/.caws/schemas/scope.schema.json +3 -3
- package/dist/templates/.caws/schemas/waivers.schema.json +91 -21
- package/dist/templates/.caws/schemas/working-spec.schema.json +253 -89
- package/dist/templates/.caws/templates/working-spec.template.yml +3 -1
- package/dist/templates/.caws/tools/scope-guard.js +66 -15
- package/dist/templates/.claude/README.md +1 -1
- package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +106 -27
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +96 -3
- package/dist/templates/.claude/rules/worktree-isolation.md +21 -3
- package/dist/templates/.claude/settings.json +5 -0
- package/dist/templates/CLAUDE.md +56 -0
- package/dist/templates/agents.md +47 -0
- package/dist/utils/agent-display.js +210 -0
- package/dist/utils/agent-session.js +142 -0
- package/dist/utils/event-log.js +584 -0
- package/dist/utils/event-renderer.js +521 -0
- package/dist/utils/schema-validator.js +10 -2
- package/dist/utils/working-state.js +25 -0
- package/dist/validation/spec-validation.js +102 -9
- package/dist/waivers-manager.js +84 -0
- package/dist/worktree/worktree-manager.js +593 -26
- package/package.json +5 -4
- package/templates/.caws/schemas/policy.schema.json +101 -34
- package/templates/.caws/schemas/scope.schema.json +3 -3
- package/templates/.caws/schemas/waivers.schema.json +91 -21
- package/templates/.caws/schemas/working-spec.schema.json +253 -89
- package/templates/.caws/templates/working-spec.template.yml +3 -1
- package/templates/.caws/tools/scope-guard.js +66 -15
- package/templates/.claude/README.md +1 -1
- package/templates/.claude/hooks/protected-paths.sh +39 -0
- package/templates/.claude/hooks/scope-guard.sh +106 -27
- package/templates/.claude/hooks/worktree-write-guard.sh +96 -3
- package/templates/.claude/rules/worktree-isolation.md +21 -3
- package/templates/.claude/settings.json +5 -0
- package/templates/CLAUDE.md +56 -0
- 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
|
-
|
|
208
|
-
const specs = [];
|
|
207
|
+
const projectDir = '$PROJECT_DIR';
|
|
209
208
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
//
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
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(
|
|
266
|
+
const s = yaml.load(fs.readFileSync(mainSpec, 'utf8'));
|
|
227
267
|
if (s && !TERMINAL.has(s.status)) {
|
|
228
|
-
specs.push({ source:
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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 "
|
|
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
|
-
#
|
|
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
|
-
|
|
73
|
-
|
|
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.
|
|
14
|
-
3. If
|
|
15
|
-
4.
|
|
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",
|
package/dist/templates/CLAUDE.md
CHANGED
|
@@ -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.
|
package/dist/templates/agents.md
CHANGED
|
@@ -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`
|