@paths.design/caws-cli 10.0.1 → 10.1.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/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 +148 -1
- package/dist/commands/status.js +8 -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 +81 -1
- package/dist/gates/budget-limit.js +6 -1
- package/dist/gates/spec-completeness.js +8 -1
- package/dist/index.js +27 -0
- package/dist/policy/PolicyManager.js +9 -7
- package/dist/session/session-manager.js +34 -0
- package/dist/templates/.caws/schemas/policy.schema.json +96 -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/settings.json +5 -0
- package/dist/templates/CLAUDE.md +34 -0
- package/dist/templates/agents.md +21 -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 +99 -9
- package/dist/waivers-manager.js +84 -0
- package/dist/worktree/worktree-manager.js +214 -8
- package/package.json +5 -4
- package/templates/.caws/schemas/policy.schema.json +96 -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/settings.json +5 -0
- package/templates/CLAUDE.md +34 -0
- package/templates/agents.md +21 -0
package/README.md
CHANGED
|
@@ -110,6 +110,19 @@ caws worktree merge <name>
|
|
|
110
110
|
|
|
111
111
|
# Destroy a worktree
|
|
112
112
|
caws worktree destroy <name>
|
|
113
|
+
|
|
114
|
+
# Bind a spec to a worktree (fixes authoritative scope mode)
|
|
115
|
+
caws worktree bind <spec-id>
|
|
116
|
+
|
|
117
|
+
# Repair registry inconsistencies
|
|
118
|
+
caws worktree repair
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Scope Management
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Inspect effective scope boundaries, mode, and binding health
|
|
125
|
+
caws scope show
|
|
113
126
|
```
|
|
114
127
|
|
|
115
128
|
### Session Management
|
|
@@ -186,14 +199,9 @@ caws-cli/
|
|
|
186
199
|
│ │
|
|
187
200
|
└───────────────────────┘
|
|
188
201
|
│
|
|
189
|
-
┌─────────────────┐
|
|
190
|
-
│ caws-mcp-server │
|
|
191
|
-
│ (Agent Bridge) │
|
|
192
|
-
└─────────────────┘
|
|
193
202
|
```
|
|
194
203
|
|
|
195
204
|
- **caws-template**: Provides the tools and configurations that CLI manages
|
|
196
|
-
- **caws-mcp-server**: Exposes CLI functionality to AI agents via MCP protocol
|
|
197
205
|
|
|
198
206
|
### Quality Gates Integration
|
|
199
207
|
|
|
@@ -156,6 +156,168 @@ function getDefaultPolicy() {
|
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Load policy.yaml synchronously for contexts that can't go async.
|
|
161
|
+
* Falls back to the bundled default policy when the file is absent or invalid.
|
|
162
|
+
* NOTE: bypasses PolicyManager's TTL cache by design — callers that need
|
|
163
|
+
* caching should use the async `deriveBudget` path.
|
|
164
|
+
* @param {string} projectRoot - Project root directory
|
|
165
|
+
* @returns {Object} Policy object (validated, with _isDefault flag if fallback used)
|
|
166
|
+
*/
|
|
167
|
+
function loadPolicySync(projectRoot) {
|
|
168
|
+
const policyPath = path.join(projectRoot, '.caws', 'policy.yaml');
|
|
169
|
+
if (!fs.existsSync(policyPath)) {
|
|
170
|
+
return { ...getDefaultPolicy(), _isDefault: true };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let policyContent;
|
|
174
|
+
try {
|
|
175
|
+
policyContent = yaml.load(fs.readFileSync(policyPath, 'utf-8'));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.warn(`Could not parse policy.yaml (${error.message}); using defaults`);
|
|
178
|
+
return { ...getDefaultPolicy(), _isDefault: true };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!policyContent || typeof policyContent !== 'object') {
|
|
182
|
+
return { ...getDefaultPolicy(), _isDefault: true };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
validatePolicy(policyContent);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
// Policy file exists but is structurally invalid — surface as warning and
|
|
189
|
+
// fall back to defaults so validation can continue. The PolicyManager
|
|
190
|
+
// async path uses console.warn for the same shape.
|
|
191
|
+
console.warn(`Policy has structure violations (${error.message}); using defaults`);
|
|
192
|
+
return { ...getDefaultPolicy(), _isDefault: true };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return policyContent;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Normalize spec.risk_tier to a canonical lookup key.
|
|
200
|
+
* Accepts numeric tier (2), numeric-string ("2"), or "T2"/"t2" forms.
|
|
201
|
+
* Returns the numeric tier (2) when the input is recognizable, otherwise
|
|
202
|
+
* returns the original value so downstream "missing tier" logic can report it.
|
|
203
|
+
* @param {*} riskTier - spec.risk_tier
|
|
204
|
+
* @returns {number|*} numeric tier or original value
|
|
205
|
+
*/
|
|
206
|
+
function normalizeRiskTier(riskTier) {
|
|
207
|
+
if (typeof riskTier === 'number') {
|
|
208
|
+
return riskTier;
|
|
209
|
+
}
|
|
210
|
+
if (typeof riskTier === 'string') {
|
|
211
|
+
const match = riskTier.match(/^T?(\d)$/i);
|
|
212
|
+
if (match) {
|
|
213
|
+
return parseInt(match[1], 10);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return riskTier;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Look up a tier in policy.risk_tiers, tolerant of numeric vs string keys.
|
|
221
|
+
* policy.yaml serializes tier keys as strings ("1", "2", "3") while specs
|
|
222
|
+
* may use numeric risk_tier. Check both representations.
|
|
223
|
+
* @param {Object} policy - Policy object with risk_tiers map
|
|
224
|
+
* @param {number|string} tier - Normalized tier key
|
|
225
|
+
* @returns {Object|undefined} Tier budget config or undefined if missing
|
|
226
|
+
*/
|
|
227
|
+
function lookupTierBudget(policy, tier) {
|
|
228
|
+
if (!policy || !policy.risk_tiers) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return policy.risk_tiers[tier] ?? policy.risk_tiers[String(tier)];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Build a derived-budget result from a spec, policy, and optional project root.
|
|
236
|
+
* Shared by both `deriveBudget` (async) and `deriveBudgetSync`. Pure function
|
|
237
|
+
* over already-loaded policy — no I/O.
|
|
238
|
+
*
|
|
239
|
+
* Baseline resolution order (per CAWSFIX-07 A1/A2):
|
|
240
|
+
* 1. If spec.change_budget has numeric max_files and max_loc, use it.
|
|
241
|
+
* Legacy specs still in the tree may carry change_budget; CAWSFIX-03
|
|
242
|
+
* forbade it in the schema but not the runtime, so we honor it when
|
|
243
|
+
* present.
|
|
244
|
+
* 2. Otherwise, fall back to policy.risk_tiers[spec.risk_tier].
|
|
245
|
+
*
|
|
246
|
+
* Throws a named-tier error (A3) if the tier isn't present in policy and no
|
|
247
|
+
* spec-level change_budget is available.
|
|
248
|
+
*
|
|
249
|
+
* @param {Object} spec - Working spec
|
|
250
|
+
* @param {Object} policy - Loaded policy object
|
|
251
|
+
* @param {string} projectRoot - Project root (for waiver loading)
|
|
252
|
+
* @returns {Object} { baseline, effective, waivers_applied, derived_at }
|
|
253
|
+
*/
|
|
254
|
+
function applyBudgetDerivation(spec, policy, projectRoot) {
|
|
255
|
+
const riskTier = normalizeRiskTier(spec.risk_tier);
|
|
256
|
+
const tierBudget = lookupTierBudget(policy, riskTier);
|
|
257
|
+
const specBudget = spec && spec.change_budget;
|
|
258
|
+
const hasSpecBudget =
|
|
259
|
+
specBudget &&
|
|
260
|
+
typeof specBudget.max_files === 'number' &&
|
|
261
|
+
typeof specBudget.max_loc === 'number';
|
|
262
|
+
|
|
263
|
+
let baseline;
|
|
264
|
+
if (hasSpecBudget) {
|
|
265
|
+
baseline = {
|
|
266
|
+
max_files: specBudget.max_files,
|
|
267
|
+
max_loc: specBudget.max_loc,
|
|
268
|
+
};
|
|
269
|
+
} else if (tierBudget) {
|
|
270
|
+
baseline = {
|
|
271
|
+
max_files: tierBudget.max_files,
|
|
272
|
+
max_loc: tierBudget.max_loc,
|
|
273
|
+
};
|
|
274
|
+
} else {
|
|
275
|
+
const available = policy && policy.risk_tiers ? Object.keys(policy.risk_tiers).join(', ') : 'none';
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Risk tier ${spec.risk_tier} not defined in policy.yaml\n` +
|
|
278
|
+
`Policy only defines tiers: ${available}\n` +
|
|
279
|
+
`Valid tiers are: 1 (critical), 2 (standard), 3 (low-risk)` +
|
|
280
|
+
(typeof spec.risk_tier === 'string'
|
|
281
|
+
? `\nHint: use numeric risk_tier (e.g., 2) instead of "${spec.risk_tier}"`
|
|
282
|
+
: '')
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let effectiveBudget = { ...baseline };
|
|
287
|
+
|
|
288
|
+
if (spec.waiver_ids && Array.isArray(spec.waiver_ids)) {
|
|
289
|
+
for (const waiverId of spec.waiver_ids) {
|
|
290
|
+
const waiver = loadWaiver(waiverId, projectRoot);
|
|
291
|
+
if (waiver && waiver.status === 'active' && isWaiverValid(waiver)) {
|
|
292
|
+
if (!waiver.gates || !waiver.gates.includes('budget_limit')) {
|
|
293
|
+
console.warn(
|
|
294
|
+
`\nWaiver ${waiverId} does not cover 'budget_limit' gate\n` +
|
|
295
|
+
` Current gates: [${waiver.gates ? waiver.gates.join(', ') : 'none'}]\n` +
|
|
296
|
+
` Add 'budget_limit' to gates array to apply to budget violations\n`
|
|
297
|
+
);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (waiver.delta) {
|
|
302
|
+
if (waiver.delta.max_files) {
|
|
303
|
+
effectiveBudget.max_files += waiver.delta.max_files;
|
|
304
|
+
}
|
|
305
|
+
if (waiver.delta.max_loc) {
|
|
306
|
+
effectiveBudget.max_loc += waiver.delta.max_loc;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
baseline,
|
|
315
|
+
effective: effectiveBudget,
|
|
316
|
+
waivers_applied: spec.waiver_ids || [],
|
|
317
|
+
derived_at: new Date().toISOString(),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
159
321
|
/**
|
|
160
322
|
* Derive budget for a working spec based on policy and waivers
|
|
161
323
|
* Enhanced to use PolicyManager for caching
|
|
@@ -204,70 +366,32 @@ async function deriveBudget(spec, projectRoot = process.cwd(), options = {}) {
|
|
|
204
366
|
}
|
|
205
367
|
}
|
|
206
368
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
riskTier = parseInt(match[1], 10);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Check if risk tier exists in policy
|
|
217
|
-
if (!policy.risk_tiers[riskTier]) {
|
|
218
|
-
throw new Error(
|
|
219
|
-
`Risk tier ${spec.risk_tier} not defined in policy.yaml\n` +
|
|
220
|
-
`Policy only defines tiers: ${Object.keys(policy.risk_tiers).join(', ')}\n` +
|
|
221
|
-
`Valid tiers are: 1 (critical), 2 (standard), 3 (low-risk)` +
|
|
222
|
-
(typeof spec.risk_tier === 'string'
|
|
223
|
-
? `\nHint: use numeric risk_tier (e.g., 2) instead of "${spec.risk_tier}"`
|
|
224
|
-
: '')
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const tierBudget = policy.risk_tiers[riskTier];
|
|
229
|
-
const baseline = {
|
|
230
|
-
max_files: tierBudget.max_files,
|
|
231
|
-
max_loc: tierBudget.max_loc,
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
// Start with baseline budget
|
|
235
|
-
let effectiveBudget = { ...baseline };
|
|
236
|
-
|
|
237
|
-
// Apply waivers if any
|
|
238
|
-
if (spec.waiver_ids && Array.isArray(spec.waiver_ids)) {
|
|
239
|
-
for (const waiverId of spec.waiver_ids) {
|
|
240
|
-
const waiver = loadWaiver(waiverId, projectRoot);
|
|
241
|
-
if (waiver && waiver.status === 'active' && isWaiverValid(waiver)) {
|
|
242
|
-
// Validate waiver covers budget_limit gate
|
|
243
|
-
if (!waiver.gates || !waiver.gates.includes('budget_limit')) {
|
|
244
|
-
console.warn(
|
|
245
|
-
`\nWaiver ${waiverId} does not cover 'budget_limit' gate\n` +
|
|
246
|
-
` Current gates: [${waiver.gates ? waiver.gates.join(', ') : 'none'}]\n` +
|
|
247
|
-
` Add 'budget_limit' to gates array to apply to budget violations\n`
|
|
248
|
-
);
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Apply additive deltas
|
|
253
|
-
if (waiver.delta) {
|
|
254
|
-
if (waiver.delta.max_files) {
|
|
255
|
-
effectiveBudget.max_files += waiver.delta.max_files;
|
|
256
|
-
}
|
|
257
|
-
if (waiver.delta.max_loc) {
|
|
258
|
-
effectiveBudget.max_loc += waiver.delta.max_loc;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
369
|
+
return applyBudgetDerivation(spec, policy, projectRoot);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
throw new Error(`Budget derivation failed: ${error.message}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
264
374
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Synchronous version of deriveBudget for callers that cannot go async.
|
|
377
|
+
* Uses `loadPolicySync` (no PolicyManager caching) and otherwise shares
|
|
378
|
+
* the same derivation semantics as the async variant.
|
|
379
|
+
*
|
|
380
|
+
* Added for CAWSFIX-07: the legacy synchronous call site in
|
|
381
|
+
* `validation/spec-validation.js` was passing the un-awaited Promise from
|
|
382
|
+
* `deriveBudget` into `checkBudgetCompliance`, which then read
|
|
383
|
+
* `derivedBudget.effective.max_files` on an undefined `.effective` —
|
|
384
|
+
* producing the "Cannot read properties of undefined (reading 'max_files')"
|
|
385
|
+
* warning on every schema-compliant spec.
|
|
386
|
+
*
|
|
387
|
+
* @param {Object} spec - Working spec object
|
|
388
|
+
* @param {string} projectRoot - Project root directory
|
|
389
|
+
* @returns {Object} Derived budget with baseline and effective limits
|
|
390
|
+
*/
|
|
391
|
+
function deriveBudgetSync(spec, projectRoot = process.cwd()) {
|
|
392
|
+
try {
|
|
393
|
+
const policy = loadPolicySync(projectRoot);
|
|
394
|
+
return applyBudgetDerivation(spec, policy, projectRoot);
|
|
271
395
|
} catch (error) {
|
|
272
396
|
throw new Error(`Budget derivation failed: ${error.message}`);
|
|
273
397
|
}
|
|
@@ -278,15 +402,25 @@ async function deriveBudget(spec, projectRoot = process.cwd(), options = {}) {
|
|
|
278
402
|
* @param {Object} waiver - Waiver document to validate
|
|
279
403
|
* @throws {Error} If waiver structure is invalid
|
|
280
404
|
*/
|
|
281
|
-
|
|
282
|
-
|
|
405
|
+
const WAIVER_REQUIRED_FIELDS = [
|
|
406
|
+
'id',
|
|
407
|
+
'applies_to',
|
|
408
|
+
'gates',
|
|
409
|
+
'delta',
|
|
410
|
+
'reason_code',
|
|
411
|
+
'expires_at',
|
|
412
|
+
'risk_owner',
|
|
413
|
+
'approvers',
|
|
414
|
+
'status',
|
|
415
|
+
];
|
|
283
416
|
|
|
417
|
+
function validateWaiverStructure(waiver) {
|
|
284
418
|
// Check all required fields present
|
|
285
|
-
for (const field of
|
|
419
|
+
for (const field of WAIVER_REQUIRED_FIELDS) {
|
|
286
420
|
if (!(field in waiver)) {
|
|
287
421
|
throw new Error(
|
|
288
422
|
`Waiver missing required field: ${field}\n` +
|
|
289
|
-
`Required fields: ${
|
|
423
|
+
`Required fields: ${WAIVER_REQUIRED_FIELDS.join(', ')}\n` +
|
|
290
424
|
`Fix the waiver file at .caws/waivers/${waiver.id || 'unknown'}.yaml`
|
|
291
425
|
);
|
|
292
426
|
}
|
|
@@ -302,8 +436,8 @@ function validateWaiverStructure(waiver) {
|
|
|
302
436
|
);
|
|
303
437
|
}
|
|
304
438
|
|
|
305
|
-
// Validate status
|
|
306
|
-
const validStatuses = ['active', 'expired', 'revoked'];
|
|
439
|
+
// Validate status (proposed is valid per schema but not applied by derivation)
|
|
440
|
+
const validStatuses = ['proposed', 'active', 'expired', 'revoked'];
|
|
307
441
|
if (!validStatuses.includes(waiver.status)) {
|
|
308
442
|
throw new Error(
|
|
309
443
|
`Invalid waiver status: ${waiver.status}\n` +
|
|
@@ -322,15 +456,25 @@ function validateWaiverStructure(waiver) {
|
|
|
322
456
|
);
|
|
323
457
|
}
|
|
324
458
|
|
|
325
|
-
// Validate approvers is array
|
|
459
|
+
// Validate approvers is array of {handle, approved_at?} objects
|
|
326
460
|
if (!Array.isArray(waiver.approvers) || waiver.approvers.length === 0) {
|
|
327
461
|
throw new Error(
|
|
328
462
|
`Invalid waiver approvers: ${JSON.stringify(waiver.approvers)}\n` +
|
|
329
|
-
'approvers must be a non-empty array of
|
|
330
|
-
'Example: approvers: ["tech-lead
|
|
463
|
+
'approvers must be a non-empty array of objects with a `handle` field\n' +
|
|
464
|
+
'Example: approvers: [{ handle: "tech-lead", approved_at: "2025-01-01T00:00:00Z" }]\n' +
|
|
331
465
|
`Fix the approvers field in .caws/waivers/${waiver.id}.yaml`
|
|
332
466
|
);
|
|
333
467
|
}
|
|
468
|
+
for (const approver of waiver.approvers) {
|
|
469
|
+
if (typeof approver !== 'object' || approver === null || typeof approver.handle !== 'string') {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`Invalid waiver approver entry: ${JSON.stringify(approver)}\n` +
|
|
472
|
+
'Each approver must be an object with a required `handle` field (string).\n' +
|
|
473
|
+
'Expected shape: { handle: "github-or-email", approved_at: "ISO-8601" }\n' +
|
|
474
|
+
`Fix the approvers field in .caws/waivers/${waiver.id}.yaml`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
334
478
|
|
|
335
479
|
// Validate expires_at is valid date string
|
|
336
480
|
const expiryDate = new Date(waiver.expires_at);
|
|
@@ -451,8 +595,9 @@ function isWaiverValid(waiver, policy = null) {
|
|
|
451
595
|
}
|
|
452
596
|
}
|
|
453
597
|
|
|
454
|
-
//
|
|
455
|
-
|
|
598
|
+
// Shallow sanity check. Full schema conformance is enforced by
|
|
599
|
+
// validateWaiverStructure at load time (see loadWaiver).
|
|
600
|
+
if (!waiver.id || !waiver.gates) {
|
|
456
601
|
console.warn(`Waiver ${waiver.id || 'unknown'} missing required fields`);
|
|
457
602
|
return false;
|
|
458
603
|
}
|
|
@@ -592,6 +737,7 @@ function generateBurnupReport(derivedBudget, currentStats) {
|
|
|
592
737
|
|
|
593
738
|
module.exports = {
|
|
594
739
|
deriveBudget,
|
|
740
|
+
deriveBudgetSync,
|
|
595
741
|
loadWaiver,
|
|
596
742
|
isWaiverValid,
|
|
597
743
|
checkBudgetCompliance,
|
|
@@ -601,4 +747,5 @@ module.exports = {
|
|
|
601
747
|
validatePolicy,
|
|
602
748
|
getDefaultPolicy,
|
|
603
749
|
validateWaiverStructure,
|
|
750
|
+
WAIVER_REQUIRED_FIELDS,
|
|
604
751
|
};
|
|
@@ -12,6 +12,7 @@ const chalk = require('chalk');
|
|
|
12
12
|
const { initializeGlobalSetup } = require('../config');
|
|
13
13
|
const { resolveSpec } = require('../utils/spec-resolver');
|
|
14
14
|
const { recordEvaluation } = require('../utils/working-state');
|
|
15
|
+
const { appendEvent } = require('../utils/event-log');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Evaluate command handler
|
|
@@ -202,18 +203,31 @@ async function evaluateCommand(specFile, options = {}) {
|
|
|
202
203
|
? 'D'
|
|
203
204
|
: 'F';
|
|
204
205
|
|
|
205
|
-
// Record to working state
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
206
|
+
// Record to working state (Phase 1 dual-write: state layer + event log)
|
|
207
|
+
const checksPassed = results.checks.filter(c => c.status === 'pass').length;
|
|
208
|
+
const evaluationPayload = {
|
|
209
|
+
score: results.score,
|
|
210
|
+
max_score: results.maxScore,
|
|
211
|
+
percentage,
|
|
212
|
+
grade,
|
|
213
|
+
checks_passed: checksPassed,
|
|
214
|
+
checks_total: results.checks.length,
|
|
215
|
+
};
|
|
216
|
+
// CAWSFIX-02: guard recordEvaluation with `spec && spec.id` check to
|
|
217
|
+
// prevent the .caws/state/undefined.json bug class. Matches the pattern
|
|
218
|
+
// gates.js already uses and the appendEvent call below.
|
|
219
|
+
if (spec && spec.id) {
|
|
220
|
+
try {
|
|
221
|
+
recordEvaluation(spec.id, evaluationPayload);
|
|
222
|
+
} catch { /* non-fatal */ }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// EVLOG-001: emit evaluation_completed event alongside state write.
|
|
226
|
+
if (spec && spec.id) {
|
|
227
|
+
await appendEvent(
|
|
228
|
+
{ actor: 'cli', event: 'evaluation_completed', spec_id: spec.id, data: evaluationPayload }
|
|
229
|
+
);
|
|
230
|
+
}
|
|
217
231
|
|
|
218
232
|
console.log('\n' + '-'.repeat(60));
|
|
219
233
|
console.log(
|
package/dist/commands/gates.js
CHANGED
|
@@ -10,7 +10,13 @@ const { formatText, formatJson, formatEnrichedText } = require('../gates/format'
|
|
|
10
10
|
const { enrichGateResults } = require('../gates/feedback');
|
|
11
11
|
const { resolveSpec } = require('../utils/spec-resolver');
|
|
12
12
|
const { commandWrapper } = require('../utils/command-wrapper');
|
|
13
|
-
|
|
13
|
+
// EVLOG-002 Phase 2 read flip: gates still writes via recordGates (state layer)
|
|
14
|
+
// AND emits a gates_evaluated event (from Phase 1), but the feedback-enrichment
|
|
15
|
+
// READ at line ~116 now goes through the event log renderer instead of loadState.
|
|
16
|
+
// The write path is deliberately unchanged in Phase 2 — only reads flip.
|
|
17
|
+
const { recordGates } = require('../utils/working-state');
|
|
18
|
+
const { loadStateFromEvents } = require('../utils/event-renderer');
|
|
19
|
+
const { appendEvent } = require('../utils/event-log');
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* Run quality gates via the v2 pipeline
|
|
@@ -80,18 +86,39 @@ async function gatesCommand(options = {}) {
|
|
|
80
86
|
|
|
81
87
|
const report = await evaluateGates({ projectRoot, stagedFiles, spec, context });
|
|
82
88
|
|
|
83
|
-
// Record to working state
|
|
89
|
+
// Record to working state (Phase 1 dual-write: state layer + event log)
|
|
84
90
|
if (spec && spec.id) {
|
|
85
91
|
try { recordGates(spec.id, report, context, projectRoot); } catch { /* non-fatal */ }
|
|
92
|
+
|
|
93
|
+
// EVLOG-001: emit gates_evaluated event alongside state write.
|
|
94
|
+
// Payload shape mirrors the `gates` field produced by recordGates.
|
|
95
|
+
await appendEvent(
|
|
96
|
+
{
|
|
97
|
+
actor: 'cli',
|
|
98
|
+
event: 'gates_evaluated',
|
|
99
|
+
spec_id: spec.id,
|
|
100
|
+
data: {
|
|
101
|
+
context,
|
|
102
|
+
passed: report.passed,
|
|
103
|
+
summary: report.summary || {},
|
|
104
|
+
gates: (report.gates || []).map((g) => ({
|
|
105
|
+
name: g.name,
|
|
106
|
+
status: g.status,
|
|
107
|
+
mode: g.mode,
|
|
108
|
+
})),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{ projectRoot }
|
|
112
|
+
);
|
|
86
113
|
}
|
|
87
114
|
|
|
88
115
|
if (options.json || options.format === 'json') {
|
|
89
116
|
console.log(formatJson(report));
|
|
90
117
|
} else if (!options.quiet) {
|
|
91
|
-
// Enrich feedback on failure or --verbose
|
|
118
|
+
// Enrich feedback on failure or --verbose (EVLOG-002: from event log)
|
|
92
119
|
if (!report.passed || options.verbose) {
|
|
93
120
|
try {
|
|
94
|
-
const state = spec?.id ?
|
|
121
|
+
const state = spec?.id ? loadStateFromEvents(spec.id, { projectRoot }) : null;
|
|
95
122
|
const enrichments = enrichGateResults(report, { spec, state, projectRoot });
|
|
96
123
|
if (enrichments.size > 0) {
|
|
97
124
|
console.log(formatEnrichedText(report, enrichments));
|
package/dist/commands/init.js
CHANGED
|
@@ -28,6 +28,8 @@ const {
|
|
|
28
28
|
getRecommendedIDEs,
|
|
29
29
|
parseIDESelection,
|
|
30
30
|
} = require('../utils/ide-detection');
|
|
31
|
+
// CAWSFIX-10: share the canonical spec-ID regex with the validator
|
|
32
|
+
const { SPEC_ID_PATTERN } = require('../validation/spec-validation');
|
|
31
33
|
|
|
32
34
|
function buildInitialFeatureSpec(specContent, fallbackId) {
|
|
33
35
|
const parsed = yaml.load(specContent);
|
|
@@ -519,11 +521,12 @@ async function initProject(projectName, options) {
|
|
|
519
521
|
return `PROJ-${randomNum.toString().padStart(3, '0')}`;
|
|
520
522
|
},
|
|
521
523
|
validate: (input) => {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
+
// CAWSFIX-10: accept multi-segment IDs like P03-IMPL-01
|
|
525
|
+
if (!SPEC_ID_PATTERN.test(input)) {
|
|
526
|
+
return 'Project ID must be in format PREFIX-NUMBER or PREFIX-SEGMENT-NUMBER (e.g., PROJ-001, P03-IMPL-01)';
|
|
524
527
|
}
|
|
525
|
-
if (input.length >
|
|
526
|
-
return 'Project ID must be less than
|
|
528
|
+
if (input.length > 40) {
|
|
529
|
+
return 'Project ID must be less than 40 characters';
|
|
527
530
|
}
|
|
528
531
|
return true;
|
|
529
532
|
},
|
package/dist/commands/iterate.js
CHANGED
|
@@ -14,7 +14,11 @@ const { initializeGlobalSetup } = require('../config');
|
|
|
14
14
|
|
|
15
15
|
// Import spec resolution system
|
|
16
16
|
const { resolveSpec } = require('../utils/spec-resolver');
|
|
17
|
-
|
|
17
|
+
// EVLOG-002 Phase 2 read flip: iterate reads from the event log instead of
|
|
18
|
+
// the state layer. loadStateFromEvents matches loadState's contract exactly
|
|
19
|
+
// (returns null for specs with no events), so the `workingState &&` guard
|
|
20
|
+
// below stays correct without code changes.
|
|
21
|
+
const { loadStateFromEvents } = require('../utils/event-renderer');
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Iterate command handler
|
|
@@ -58,9 +62,9 @@ async function iterateCommand(specFile, options = {}) {
|
|
|
58
62
|
console.log(`ID: ${spec.id} | Tier: ${spec.risk_tier} | Mode: ${spec.mode}`);
|
|
59
63
|
console.log(`Current State: ${stateDescription}\n`);
|
|
60
64
|
|
|
61
|
-
// Load working state for evidence-based guidance
|
|
65
|
+
// Load working state for evidence-based guidance (EVLOG-002: from event log)
|
|
62
66
|
let workingState = null;
|
|
63
|
-
try { workingState =
|
|
67
|
+
try { workingState = loadStateFromEvents(spec.id); } catch { /* non-fatal */ }
|
|
64
68
|
|
|
65
69
|
// Analyze progress based on mode
|
|
66
70
|
const guidance = generateGuidance(spec, currentState, options);
|