@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"title": "CAWS Working Spec",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"required": [
|
|
@@ -15,79 +15,178 @@
|
|
|
15
15
|
"non_functional",
|
|
16
16
|
"contracts"
|
|
17
17
|
],
|
|
18
|
+
"not": {
|
|
19
|
+
"required": [
|
|
20
|
+
"change_budget"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
18
23
|
"properties": {
|
|
19
|
-
"id": {
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
"id": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"pattern": "^[A-Z][A-Z0-9]*(-[A-Z0-9]+)*-\\d+[a-z]*$",
|
|
27
|
+
"description": "Unique identifier for the change. Format: uppercase/digit PREFIX segments separated by dashes, final segment is one+ digits with an optional lowercase suffix (e.g. CAWSFIX-16, P03-TRUTH-001, ALG-001A-HARDEN-01, APC-01a). Matches SPEC_ID_PATTERN in spec-validation.js (CAWSFIX-25 alignment)."
|
|
28
|
+
},
|
|
29
|
+
"title": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"minLength": 10,
|
|
32
|
+
"maxLength": 200,
|
|
33
|
+
"description": "Clear, descriptive title"
|
|
34
|
+
},
|
|
35
|
+
"type": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"enum": [
|
|
38
|
+
"feature",
|
|
39
|
+
"fix",
|
|
40
|
+
"refactor",
|
|
41
|
+
"chore",
|
|
42
|
+
"doc",
|
|
43
|
+
"docs"
|
|
44
|
+
],
|
|
45
|
+
"description": "Type of work (informational; `mode` is the enforced gate input)"
|
|
46
|
+
},
|
|
22
47
|
"status": {
|
|
23
48
|
"type": "string",
|
|
24
|
-
"enum": [
|
|
49
|
+
"enum": [
|
|
50
|
+
"draft",
|
|
51
|
+
"active",
|
|
52
|
+
"in_progress",
|
|
53
|
+
"completed",
|
|
54
|
+
"closed",
|
|
55
|
+
"archived"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"created_at": {
|
|
59
|
+
"type": "string"
|
|
60
|
+
},
|
|
61
|
+
"updated_at": {
|
|
62
|
+
"type": "string"
|
|
63
|
+
},
|
|
64
|
+
"worktree": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "CAWS worktree name assigned to this spec"
|
|
67
|
+
},
|
|
68
|
+
"risk_tier": {
|
|
69
|
+
"type": [
|
|
70
|
+
"integer",
|
|
71
|
+
"string"
|
|
72
|
+
],
|
|
73
|
+
"enum": [
|
|
74
|
+
1,
|
|
75
|
+
2,
|
|
76
|
+
3,
|
|
77
|
+
"1",
|
|
78
|
+
"2",
|
|
79
|
+
"3"
|
|
80
|
+
],
|
|
81
|
+
"description": "Risk level (1=high, 2=medium, 3=low)"
|
|
82
|
+
},
|
|
83
|
+
"mode": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"enum": [
|
|
86
|
+
"feature",
|
|
87
|
+
"refactor",
|
|
88
|
+
"fix",
|
|
89
|
+
"doc",
|
|
90
|
+
"docs",
|
|
91
|
+
"chore",
|
|
92
|
+
"development"
|
|
93
|
+
],
|
|
94
|
+
"description": "Mode of change. `development` is the default used by `caws specs create`; feature/refactor/fix/doc/chore are the classic modes the legacy validator accepted."
|
|
25
95
|
},
|
|
26
|
-
"created_at": { "type": "string" },
|
|
27
|
-
"updated_at": { "type": "string" },
|
|
28
|
-
"risk_tier": { "type": ["integer", "string"], "enum": [1, 2, 3, "1", "2", "3"] },
|
|
29
|
-
"mode": { "type": "string", "enum": ["feature", "refactor", "fix", "doc", "docs", "chore", "development"] },
|
|
30
96
|
"waiver_ids": {
|
|
31
97
|
"type": "array",
|
|
32
|
-
"items": {
|
|
98
|
+
"items": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"pattern": "^WV-\\d{4}$"
|
|
101
|
+
},
|
|
33
102
|
"description": "IDs of active waivers applying to this spec"
|
|
34
103
|
},
|
|
35
104
|
"blast_radius": {
|
|
36
105
|
"type": "object",
|
|
37
|
-
"required": [
|
|
106
|
+
"required": [
|
|
107
|
+
"modules",
|
|
108
|
+
"data_migration"
|
|
109
|
+
],
|
|
38
110
|
"properties": {
|
|
39
|
-
"modules": {
|
|
40
|
-
|
|
41
|
-
|
|
111
|
+
"modules": {
|
|
112
|
+
"type": "array",
|
|
113
|
+
"items": {
|
|
114
|
+
"type": "string"
|
|
115
|
+
},
|
|
116
|
+
"description": "List of modules/paths the change touches"
|
|
117
|
+
},
|
|
118
|
+
"data_migration": {
|
|
119
|
+
"type": "boolean",
|
|
120
|
+
"description": "Whether the change requires a data migration"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"additionalProperties": true
|
|
124
|
+
},
|
|
125
|
+
"operational_rollback_slo": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"minLength": 1,
|
|
128
|
+
"description": "Time target for operational rollback (e.g. '5m', '30m', '1h')"
|
|
42
129
|
},
|
|
43
|
-
"operational_rollback_slo": { "type": "string" },
|
|
44
130
|
"scope": {
|
|
45
131
|
"type": "object",
|
|
46
|
-
"required": [
|
|
132
|
+
"required": [
|
|
133
|
+
"in"
|
|
134
|
+
],
|
|
47
135
|
"properties": {
|
|
48
|
-
"in": {
|
|
49
|
-
|
|
50
|
-
|
|
136
|
+
"in": {
|
|
137
|
+
"type": "array",
|
|
138
|
+
"items": {
|
|
139
|
+
"type": "string"
|
|
140
|
+
},
|
|
141
|
+
"minItems": 1,
|
|
142
|
+
"description": "Files/paths allowed to be edited (non-empty)"
|
|
143
|
+
},
|
|
144
|
+
"out": {
|
|
145
|
+
"type": "array",
|
|
146
|
+
"items": {
|
|
147
|
+
"type": "string"
|
|
148
|
+
},
|
|
149
|
+
"description": "Files/paths explicitly excluded from edits"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"additionalProperties": true
|
|
51
153
|
},
|
|
52
|
-
"invariants": {
|
|
53
|
-
"acceptance": {
|
|
154
|
+
"invariants": {
|
|
54
155
|
"type": "array",
|
|
55
|
-
"minItems": 0,
|
|
56
156
|
"items": {
|
|
57
|
-
"type": "
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"given": { "type": "string" },
|
|
62
|
-
"when": { "type": "string" },
|
|
63
|
-
"then": { "type": "string" }
|
|
64
|
-
}
|
|
65
|
-
}
|
|
157
|
+
"type": "string"
|
|
158
|
+
},
|
|
159
|
+
"minItems": 1,
|
|
160
|
+
"description": "Properties that must hold across the change (non-empty)"
|
|
66
161
|
},
|
|
67
|
-
"
|
|
162
|
+
"acceptance": {
|
|
68
163
|
"type": "array",
|
|
69
|
-
"minItems":
|
|
164
|
+
"minItems": 1,
|
|
165
|
+
"description": "Given/When/Then acceptance criteria (non-empty)",
|
|
70
166
|
"items": {
|
|
71
167
|
"type": "object",
|
|
72
|
-
"required": [
|
|
168
|
+
"required": [
|
|
169
|
+
"id",
|
|
170
|
+
"given",
|
|
171
|
+
"when",
|
|
172
|
+
"then"
|
|
173
|
+
],
|
|
73
174
|
"properties": {
|
|
74
|
-
"id": {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"test": { "type": "string" },
|
|
78
|
-
"test_command": { "type": "string" },
|
|
79
|
-
"test_nodeids": {
|
|
80
|
-
"type": "array",
|
|
81
|
-
"items": { "type": "string" }
|
|
175
|
+
"id": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"minLength": 1
|
|
82
178
|
},
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
179
|
+
"given": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"minLength": 1
|
|
182
|
+
},
|
|
183
|
+
"when": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"minLength": 1
|
|
186
|
+
},
|
|
187
|
+
"then": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"minLength": 1
|
|
91
190
|
}
|
|
92
191
|
},
|
|
93
192
|
"additionalProperties": true
|
|
@@ -95,82 +194,147 @@
|
|
|
95
194
|
},
|
|
96
195
|
"non_functional": {
|
|
97
196
|
"type": "object",
|
|
197
|
+
"required": [
|
|
198
|
+
"a11y",
|
|
199
|
+
"perf",
|
|
200
|
+
"security"
|
|
201
|
+
],
|
|
98
202
|
"properties": {
|
|
99
|
-
"a11y": {
|
|
203
|
+
"a11y": {
|
|
204
|
+
"type": "array",
|
|
205
|
+
"items": {
|
|
206
|
+
"type": "string"
|
|
207
|
+
},
|
|
208
|
+
"description": "Accessibility requirements (may be empty array if not applicable)"
|
|
209
|
+
},
|
|
100
210
|
"perf": {
|
|
101
211
|
"type": "object",
|
|
102
|
-
"
|
|
103
|
-
"api_p95_ms": { "type": "integer", "minimum": 1 },
|
|
104
|
-
"lcp_ms": { "type": "integer", "minimum": 1 }
|
|
105
|
-
},
|
|
106
|
-
"additionalProperties": false
|
|
212
|
+
"description": "Performance requirements (e.g. api_p95_ms, lcp_ms)"
|
|
107
213
|
},
|
|
108
|
-
"security": {
|
|
214
|
+
"security": {
|
|
215
|
+
"type": "array",
|
|
216
|
+
"items": {
|
|
217
|
+
"type": "string"
|
|
218
|
+
},
|
|
219
|
+
"description": "Security requirements (may be empty array if not applicable)"
|
|
220
|
+
}
|
|
109
221
|
},
|
|
110
|
-
"additionalProperties":
|
|
222
|
+
"additionalProperties": true
|
|
111
223
|
},
|
|
112
224
|
"contracts": {
|
|
113
225
|
"type": "array",
|
|
226
|
+
"description": "API contracts. Empty array is permitted; CAWSFIX-06 will warn (not fail) when mode=feature with empty contracts.",
|
|
114
227
|
"items": {
|
|
115
228
|
"type": "object",
|
|
116
|
-
"required": [
|
|
229
|
+
"required": [
|
|
230
|
+
"type",
|
|
231
|
+
"path"
|
|
232
|
+
],
|
|
117
233
|
"properties": {
|
|
118
|
-
"type": {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
234
|
+
"type": {
|
|
235
|
+
"type": "string",
|
|
236
|
+
"enum": [
|
|
237
|
+
"openapi",
|
|
238
|
+
"graphql",
|
|
239
|
+
"proto",
|
|
240
|
+
"pact",
|
|
241
|
+
"project_setup"
|
|
242
|
+
]
|
|
243
|
+
},
|
|
244
|
+
"path": {
|
|
245
|
+
"type": "string"
|
|
246
|
+
},
|
|
247
|
+
"description": {
|
|
248
|
+
"type": "string"
|
|
249
|
+
},
|
|
250
|
+
"version": {
|
|
251
|
+
"type": "string"
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
"additionalProperties": true
|
|
123
255
|
}
|
|
124
256
|
},
|
|
125
257
|
"observability": {
|
|
126
|
-
"type": "object"
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
258
|
+
"type": "object"
|
|
259
|
+
},
|
|
260
|
+
"migrations": {
|
|
261
|
+
"type": "array",
|
|
262
|
+
"items": {
|
|
263
|
+
"type": "string"
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
"rollback": {
|
|
267
|
+
"type": "array",
|
|
268
|
+
"items": {
|
|
269
|
+
"type": "string"
|
|
131
270
|
}
|
|
132
271
|
},
|
|
133
|
-
"migrations": { "type": "array", "items": { "type": "string" } },
|
|
134
|
-
"rollback": { "type": "array", "items": { "type": "string" } },
|
|
135
272
|
"experimental_mode": {
|
|
136
273
|
"type": "object",
|
|
137
|
-
"
|
|
138
|
-
|
|
274
|
+
"required": [
|
|
275
|
+
"enabled",
|
|
276
|
+
"rationale",
|
|
277
|
+
"expires_at"
|
|
278
|
+
],
|
|
139
279
|
"properties": {
|
|
140
|
-
"enabled": {
|
|
141
|
-
|
|
142
|
-
|
|
280
|
+
"enabled": {
|
|
281
|
+
"type": "boolean"
|
|
282
|
+
},
|
|
283
|
+
"rationale": {
|
|
284
|
+
"type": "string"
|
|
285
|
+
},
|
|
286
|
+
"expires_at": {
|
|
287
|
+
"type": "string"
|
|
288
|
+
}
|
|
143
289
|
}
|
|
144
290
|
},
|
|
145
291
|
"timeboxed_hours": {
|
|
146
292
|
"type": "integer",
|
|
147
|
-
"minimum": 1
|
|
148
|
-
"description": "Time limit for experimental features in hours"
|
|
293
|
+
"minimum": 1
|
|
149
294
|
},
|
|
150
295
|
"human_override": {
|
|
151
296
|
"type": "object",
|
|
297
|
+
"required": [
|
|
298
|
+
"approved_by",
|
|
299
|
+
"reason"
|
|
300
|
+
],
|
|
152
301
|
"properties": {
|
|
153
|
-
"approved_by": {
|
|
154
|
-
|
|
302
|
+
"approved_by": {
|
|
303
|
+
"type": "string"
|
|
304
|
+
},
|
|
305
|
+
"reason": {
|
|
306
|
+
"type": "string"
|
|
307
|
+
},
|
|
155
308
|
"waived_requirements": {
|
|
156
309
|
"type": "array",
|
|
157
310
|
"items": {
|
|
158
|
-
"type": "string"
|
|
159
|
-
"enum": ["mutation_testing", "contract_tests", "coverage", "manual_review"]
|
|
311
|
+
"type": "string"
|
|
160
312
|
}
|
|
161
313
|
},
|
|
162
|
-
"expiry_date": {
|
|
163
|
-
|
|
164
|
-
|
|
314
|
+
"expiry_date": {
|
|
315
|
+
"type": "string"
|
|
316
|
+
}
|
|
317
|
+
}
|
|
165
318
|
},
|
|
166
319
|
"ai_assessment": {
|
|
167
320
|
"type": "object",
|
|
168
321
|
"properties": {
|
|
169
|
-
"confidence_level": {
|
|
170
|
-
|
|
171
|
-
|
|
322
|
+
"confidence_level": {
|
|
323
|
+
"type": "integer",
|
|
324
|
+
"minimum": 1,
|
|
325
|
+
"maximum": 10
|
|
326
|
+
},
|
|
327
|
+
"uncertainty_areas": {
|
|
328
|
+
"type": "array",
|
|
329
|
+
"items": {
|
|
330
|
+
"type": "string"
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
"recommended_pairing": {
|
|
334
|
+
"type": "boolean"
|
|
335
|
+
}
|
|
172
336
|
}
|
|
173
337
|
}
|
|
174
338
|
},
|
|
175
|
-
"additionalProperties":
|
|
339
|
+
"additionalProperties": true
|
|
176
340
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
id: '{{FEATURE_ID}}'
|
|
2
2
|
title: '{{FEATURE_TITLE}}'
|
|
3
|
+
# Recommended when the spec belongs to a CAWS worktree:
|
|
4
|
+
# worktree: '{{WORKTREE_NAME}}'
|
|
3
5
|
risk_tier: {{TIER}}
|
|
4
6
|
mode: feature
|
|
5
7
|
blast_radius:
|
|
6
8
|
modules: []
|
|
9
|
+
data_migration: false
|
|
7
10
|
operational_rollback_slo: "30m"
|
|
8
11
|
scope:
|
|
9
12
|
in:
|
|
@@ -75,4 +78,3 @@ rollback:
|
|
|
75
78
|
# reason: "Urgent production fix - bypassing mutation tests for immediate deployment"
|
|
76
79
|
# waived_requirements: ["mutation_testing", "manual_review"]
|
|
77
80
|
# expiry_date: "2025-10-01T00:00:00Z"
|
|
78
|
-
|
|
@@ -70,26 +70,71 @@ function checkFileScope(filePath, projectDir) {
|
|
|
70
70
|
return { inScope: true, reason: 'js-yaml not available' };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
// --- Authoritative spec detection ---
|
|
74
|
+
// If inside a worktree with a mutual spec binding, only check that spec.
|
|
75
|
+
let authoritativeSpec = null;
|
|
76
|
+
let mode = 'union';
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
const registryPath = path.join(projectDir, '.caws', 'worktrees.json');
|
|
79
|
+
const worktreesBase = path.join(projectDir, '.caws', 'worktrees');
|
|
80
|
+
const cwd = process.cwd();
|
|
81
|
+
|
|
82
|
+
if (cwd.startsWith(worktreesBase + path.sep)) {
|
|
83
|
+
const relative = path.relative(worktreesBase, cwd);
|
|
84
|
+
const worktreeName = relative.split(path.sep)[0];
|
|
85
|
+
|
|
86
|
+
if (worktreeName && fs.existsSync(registryPath)) {
|
|
87
|
+
try {
|
|
88
|
+
const reg = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
89
|
+
const entry = reg.worktrees && reg.worktrees[worktreeName];
|
|
90
|
+
|
|
91
|
+
if (entry && entry.specId) {
|
|
92
|
+
const specCandidates = [
|
|
93
|
+
path.join(specsDir, entry.specId + '.yaml'),
|
|
94
|
+
path.join(specsDir, entry.specId + '.yml'),
|
|
95
|
+
];
|
|
96
|
+
for (const candidate of specCandidates) {
|
|
97
|
+
if (fs.existsSync(candidate)) {
|
|
98
|
+
try {
|
|
99
|
+
const s = yaml.load(fs.readFileSync(candidate, 'utf8'));
|
|
100
|
+
if (s && !TERMINAL.has(s.status) && s.worktree === worktreeName) {
|
|
101
|
+
authoritativeSpec = { source: path.basename(candidate), spec: s };
|
|
102
|
+
mode = 'authoritative';
|
|
103
|
+
}
|
|
104
|
+
} catch (_) {}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (_) {}
|
|
110
|
+
}
|
|
82
111
|
}
|
|
83
112
|
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
// --- Collect specs based on mode ---
|
|
114
|
+
const specs = [];
|
|
115
|
+
|
|
116
|
+
if (authoritativeSpec) {
|
|
117
|
+
specs.push(authoritativeSpec);
|
|
118
|
+
} else {
|
|
119
|
+
if (fs.existsSync(specFile)) {
|
|
86
120
|
try {
|
|
87
|
-
const s = yaml.load(fs.readFileSync(
|
|
121
|
+
const s = yaml.load(fs.readFileSync(specFile, 'utf8'));
|
|
88
122
|
if (s && !TERMINAL.has(s.status)) {
|
|
89
|
-
specs.push({ source:
|
|
123
|
+
specs.push({ source: 'working-spec', spec: s });
|
|
90
124
|
}
|
|
91
125
|
} catch (_) {}
|
|
92
126
|
}
|
|
127
|
+
|
|
128
|
+
if (fs.existsSync(specsDir)) {
|
|
129
|
+
for (const f of fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
|
|
130
|
+
try {
|
|
131
|
+
const s = yaml.load(fs.readFileSync(path.join(specsDir, f), 'utf8'));
|
|
132
|
+
if (s && !TERMINAL.has(s.status)) {
|
|
133
|
+
specs.push({ source: f, spec: s });
|
|
134
|
+
}
|
|
135
|
+
} catch (_) {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
93
138
|
}
|
|
94
139
|
|
|
95
140
|
if (specs.length === 0) {
|
|
@@ -100,17 +145,23 @@ function checkFileScope(filePath, projectDir) {
|
|
|
100
145
|
for (const { source, spec } of specs) {
|
|
101
146
|
for (const pattern of (spec.scope?.out || [])) {
|
|
102
147
|
if (globToRegex(pattern).test(filePath)) {
|
|
103
|
-
|
|
148
|
+
const modeHint = mode === 'union'
|
|
149
|
+
? '. No authoritative spec bound — checking all active specs. Fix: caws worktree bind <spec-id>'
|
|
150
|
+
: '';
|
|
151
|
+
return { inScope: false, reason: `out-of-scope in ${source} (pattern: ${pattern})${modeHint}` };
|
|
104
152
|
}
|
|
105
153
|
}
|
|
106
154
|
}
|
|
107
155
|
|
|
108
|
-
//
|
|
156
|
+
// scope.in — must match at least one
|
|
109
157
|
const allIn = specs.flatMap(({ spec }) => spec.scope?.in || []);
|
|
110
158
|
if (allIn.length > 0) {
|
|
111
159
|
const found = allIn.some(pattern => globToRegex(pattern).test(filePath));
|
|
112
160
|
if (!found) {
|
|
113
|
-
|
|
161
|
+
const modeHint = mode === 'union'
|
|
162
|
+
? '. No authoritative spec bound — checking all active specs. Fix: caws worktree bind <spec-id>'
|
|
163
|
+
: '';
|
|
164
|
+
return { inScope: false, reason: `not in any active spec scope.in${modeHint}` };
|
|
114
165
|
}
|
|
115
166
|
}
|
|
116
167
|
|
|
@@ -38,7 +38,7 @@ Run before Claude executes a tool:
|
|
|
38
38
|
|------|---------|---------|
|
|
39
39
|
| `block-dangerous.sh` | `Bash` | Block destructive shell commands |
|
|
40
40
|
| `scan-secrets.sh` | `Read` | Warn when reading sensitive files |
|
|
41
|
-
| `scope-guard.sh` | `Write\|Edit` | Check scope boundaries before edits |
|
|
41
|
+
| `scope-guard.sh` | `Write\|Edit` | Check scope boundaries before edits (use `caws scope show` to diagnose blocks) |
|
|
42
42
|
|
|
43
43
|
### PostToolUse Hooks
|
|
44
44
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Protected Paths Guard for Claude Code
|
|
3
|
+
# Blocks direct Write/Edit access to guard code and guard state.
|
|
4
|
+
# @author @darianrosebrook
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
|
|
10
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
11
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
12
|
+
|
|
13
|
+
case "$TOOL_NAME" in
|
|
14
|
+
Write|Edit) ;;
|
|
15
|
+
*) exit 0 ;;
|
|
16
|
+
esac
|
|
17
|
+
|
|
18
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# If you are reading this because a write was blocked, do not edit hook files or
|
|
23
|
+
# strike-state files to bypass a guard. Switch into the correct worktree, fix the
|
|
24
|
+
# active spec scope, or ask the user if the guard itself is wrong.
|
|
25
|
+
case "$FILE_PATH" in
|
|
26
|
+
*/.claude/hooks/*)
|
|
27
|
+
echo "BLOCKED: $FILE_PATH is protected." >&2
|
|
28
|
+
echo "Ask the user for permission before editing Claude hook scripts." >&2
|
|
29
|
+
exit 1
|
|
30
|
+
;;
|
|
31
|
+
*/.claude/logs/guard-strikes-*.json)
|
|
32
|
+
echo "BLOCKED: $FILE_PATH is protected guard state." >&2
|
|
33
|
+
echo "Do not reset or edit strike counters to bypass enforcement." >&2
|
|
34
|
+
echo "Switch into the correct worktree, update the active CAWS spec scope, or ask the user for direction instead." >&2
|
|
35
|
+
exit 2
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
exit 0
|