@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/dist/commands/waivers.js
CHANGED
|
@@ -66,10 +66,12 @@ async function waiversCommand(subcommand = 'list', options = {}) {
|
|
|
66
66
|
return await showWaiver(options.id, options);
|
|
67
67
|
case 'revoke':
|
|
68
68
|
return await revokeWaiver(options.id, options);
|
|
69
|
+
case 'prune':
|
|
70
|
+
return await pruneWaivers(options);
|
|
69
71
|
default:
|
|
70
72
|
throw new Error(
|
|
71
73
|
`Unknown waiver subcommand: ${subcommand}.\n` +
|
|
72
|
-
'Available subcommands: create, list, show, revoke'
|
|
74
|
+
'Available subcommands: create, list, show, revoke, prune'
|
|
73
75
|
);
|
|
74
76
|
}
|
|
75
77
|
},
|
|
@@ -178,9 +180,8 @@ async function createWaiver(options) {
|
|
|
178
180
|
const { createValidator, getSchemaPath } = require('../utils/schema-validator');
|
|
179
181
|
const schemaPath = getSchemaPath('waivers.schema.json', process.cwd());
|
|
180
182
|
const validate = createValidator(schemaPath);
|
|
181
|
-
// waivers.schema.json
|
|
182
|
-
const
|
|
183
|
-
const result = validate(waiverDoc);
|
|
183
|
+
// waivers.schema.json validates a single waiver document directly (CAWSFIX-17)
|
|
184
|
+
const result = validate(waiver);
|
|
184
185
|
if (!result.valid) {
|
|
185
186
|
console.warn(chalk.yellow('Waiver has schema violations:'));
|
|
186
187
|
result.errors.forEach((err) => {
|
|
@@ -248,7 +249,7 @@ async function listWaivers(_options) {
|
|
|
248
249
|
|
|
249
250
|
// Validate each loaded waiver against schema
|
|
250
251
|
if (waiverValidate && waiver && waiver.id) {
|
|
251
|
-
const result = waiverValidate(
|
|
252
|
+
const result = waiverValidate(waiver);
|
|
252
253
|
if (!result.valid) {
|
|
253
254
|
console.warn(chalk.yellow(`Schema warning for ${file}:`));
|
|
254
255
|
result.errors.forEach((err) => {
|
|
@@ -405,6 +406,147 @@ async function revokeWaiver(waiverId, options) {
|
|
|
405
406
|
console.log(` Reason: ${waiver.revocation_reason}\n`);
|
|
406
407
|
}
|
|
407
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Prune expired waivers.
|
|
411
|
+
*
|
|
412
|
+
* Behavior per CAWSFIX-04 AC3-A6:
|
|
413
|
+
* --expired : dry run — list prunable waivers, no disk changes,
|
|
414
|
+
* no events emitted. Exit 0.
|
|
415
|
+
* --expired --apply : transition each prunable waiver from
|
|
416
|
+
* `status: active` to `status: expired` in place
|
|
417
|
+
* (file is updated, NOT deleted, to preserve the
|
|
418
|
+
* audit trail) and append a `waiver_pruned` event
|
|
419
|
+
* to the event log for each.
|
|
420
|
+
*
|
|
421
|
+
* A waiver is "prunable" iff `status === 'active'` AND `expires_at < now`.
|
|
422
|
+
* Waivers already `expired` or `revoked` are untouched (A4). Non-expired
|
|
423
|
+
* active waivers are untouched (A5). Empty registries exit 0 with a
|
|
424
|
+
* friendly message (A6).
|
|
425
|
+
*
|
|
426
|
+
* @param {object} options
|
|
427
|
+
* @param {boolean} [options.expired] — currently the only prune criterion
|
|
428
|
+
* @param {boolean} [options.apply] — if false, dry-run (default)
|
|
429
|
+
* @param {boolean} [options.json] — machine-readable output
|
|
430
|
+
*/
|
|
431
|
+
async function pruneWaivers(options = {}) {
|
|
432
|
+
if (!options.expired) {
|
|
433
|
+
console.error(chalk.red('\n`caws waivers prune` requires --expired\n'));
|
|
434
|
+
console.log(chalk.yellow('Usage: caws waivers prune --expired [--apply]\n'));
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const waiversManager = new WaiversManager();
|
|
439
|
+
|
|
440
|
+
// Fast path: no waivers directory or no waiver files at all.
|
|
441
|
+
const allWaivers = waiversManager.enumerateWaiverFiles();
|
|
442
|
+
if (allWaivers.length === 0) {
|
|
443
|
+
const msg = 'No active waivers to check.';
|
|
444
|
+
if (options.json) {
|
|
445
|
+
console.log(JSON.stringify({ status: 'ok', pruned: [], message: msg }));
|
|
446
|
+
} else {
|
|
447
|
+
console.log(chalk.yellow(`\n${msg}\n`));
|
|
448
|
+
}
|
|
449
|
+
return { pruned: [], applied: false };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const candidates = waiversManager.findExpiredWaivers();
|
|
453
|
+
|
|
454
|
+
// No prunable waivers — report and return.
|
|
455
|
+
if (candidates.length === 0) {
|
|
456
|
+
const msg = 'No expired waivers to prune.';
|
|
457
|
+
if (options.json) {
|
|
458
|
+
console.log(JSON.stringify({ status: 'ok', pruned: [], message: msg }));
|
|
459
|
+
} else {
|
|
460
|
+
console.log(chalk.green(`\n${msg}\n`));
|
|
461
|
+
}
|
|
462
|
+
return { pruned: [], applied: false };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const apply = Boolean(options.apply);
|
|
466
|
+
|
|
467
|
+
if (!apply) {
|
|
468
|
+
// Dry run — report only, no disk changes, no events.
|
|
469
|
+
if (options.json) {
|
|
470
|
+
console.log(
|
|
471
|
+
JSON.stringify({
|
|
472
|
+
status: 'dry_run',
|
|
473
|
+
applied: false,
|
|
474
|
+
pruned: candidates.map((c) => ({ id: c.id, expires_at: c.expires_at })),
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
} else {
|
|
478
|
+
console.log(chalk.yellow(`\nDry run — ${candidates.length} waiver(s) would be pruned:\n`));
|
|
479
|
+
candidates.forEach((c) => {
|
|
480
|
+
console.log(` ${chalk.bold(c.id)} expired at ${c.expires_at}`);
|
|
481
|
+
});
|
|
482
|
+
console.log(chalk.dim('\nRe-run with --apply to transition status to expired.\n'));
|
|
483
|
+
}
|
|
484
|
+
return { pruned: candidates, applied: false };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Apply path — transition each file and emit events.
|
|
488
|
+
const { appendEvent } = require('../utils/event-log');
|
|
489
|
+
const pruned = [];
|
|
490
|
+
const failures = [];
|
|
491
|
+
|
|
492
|
+
for (const c of candidates) {
|
|
493
|
+
try {
|
|
494
|
+
const updated = waiversManager.markWaiverExpired(c.path);
|
|
495
|
+
|
|
496
|
+
// Emit waiver_pruned event. spec_id is optional for this event
|
|
497
|
+
// (waivers may or may not be tied to a spec). Using the waiver's
|
|
498
|
+
// applies_to field when available, otherwise omitting.
|
|
499
|
+
const spec_id =
|
|
500
|
+
updated && typeof updated.applies_to === 'string' && updated.applies_to.trim() !== ''
|
|
501
|
+
? updated.applies_to
|
|
502
|
+
: undefined;
|
|
503
|
+
|
|
504
|
+
await appendEvent({
|
|
505
|
+
actor: 'cli',
|
|
506
|
+
event: 'waiver_pruned',
|
|
507
|
+
spec_id,
|
|
508
|
+
data: {
|
|
509
|
+
waiver_id: c.id,
|
|
510
|
+
expires_at: c.expires_at,
|
|
511
|
+
previous_status: 'active',
|
|
512
|
+
new_status: 'expired',
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
pruned.push({ id: c.id, expires_at: c.expires_at });
|
|
517
|
+
} catch (err) {
|
|
518
|
+
failures.push({ id: c.id, error: err.message });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (options.json) {
|
|
523
|
+
console.log(
|
|
524
|
+
JSON.stringify({
|
|
525
|
+
status: failures.length === 0 ? 'ok' : 'partial',
|
|
526
|
+
applied: true,
|
|
527
|
+
pruned,
|
|
528
|
+
failures,
|
|
529
|
+
})
|
|
530
|
+
);
|
|
531
|
+
} else {
|
|
532
|
+
console.log(
|
|
533
|
+
chalk.green(`\nPruned ${pruned.length} expired waiver(s):\n`)
|
|
534
|
+
);
|
|
535
|
+
pruned.forEach((p) => {
|
|
536
|
+
console.log(` ${chalk.bold(p.id)} (was expired at ${p.expires_at})`);
|
|
537
|
+
});
|
|
538
|
+
if (failures.length > 0) {
|
|
539
|
+
console.log(chalk.red(`\n${failures.length} failure(s):\n`));
|
|
540
|
+
failures.forEach((f) => {
|
|
541
|
+
console.log(` ${chalk.bold(f.id)}: ${f.error}`);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
console.log();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return { pruned, applied: true, failures };
|
|
548
|
+
}
|
|
549
|
+
|
|
408
550
|
/**
|
|
409
551
|
* Add waiver to active waivers file for quality gates integration
|
|
410
552
|
*/
|
|
@@ -12,6 +12,10 @@ const {
|
|
|
12
12
|
mergeWorktree,
|
|
13
13
|
pruneWorktrees,
|
|
14
14
|
repairWorktrees,
|
|
15
|
+
loadRegistry,
|
|
16
|
+
saveRegistry,
|
|
17
|
+
getRepoRoot,
|
|
18
|
+
findFeatureSpecPath,
|
|
15
19
|
} = require('../worktree/worktree-manager');
|
|
16
20
|
const { getAgentSessionId } = require('../utils/agent-session');
|
|
17
21
|
|
|
@@ -35,9 +39,11 @@ async function worktreeCommand(subcommand, options = {}) {
|
|
|
35
39
|
return handlePrune(options);
|
|
36
40
|
case 'repair':
|
|
37
41
|
return handleRepair(options);
|
|
42
|
+
case 'bind':
|
|
43
|
+
return handleBind(options);
|
|
38
44
|
default:
|
|
39
45
|
console.error(chalk.red(`Unknown worktree subcommand: ${subcommand}`));
|
|
40
|
-
console.log(chalk.blue('Available: create, list, destroy, merge, prune, repair'));
|
|
46
|
+
console.log(chalk.blue('Available: create, list, destroy, merge, prune, repair, bind'));
|
|
41
47
|
process.exit(1);
|
|
42
48
|
}
|
|
43
49
|
} catch (error) {
|
|
@@ -303,4 +309,78 @@ function handleRepair(options) {
|
|
|
303
309
|
}
|
|
304
310
|
}
|
|
305
311
|
|
|
312
|
+
function handleBind(options) {
|
|
313
|
+
const path = require('path');
|
|
314
|
+
const fs = require('fs-extra');
|
|
315
|
+
const yaml = require('js-yaml');
|
|
316
|
+
const { specId, name } = options;
|
|
317
|
+
|
|
318
|
+
if (!specId) {
|
|
319
|
+
console.error(chalk.red('Spec ID is required'));
|
|
320
|
+
console.log(chalk.blue('Usage: caws worktree bind <spec-id> [--name <worktree-name>]'));
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Determine worktree name: from option, or detect from cwd
|
|
325
|
+
let worktreeName = name;
|
|
326
|
+
if (!worktreeName) {
|
|
327
|
+
const root = getRepoRoot();
|
|
328
|
+
const cwd = process.cwd();
|
|
329
|
+
const worktreesBase = path.join(root, '.caws', 'worktrees');
|
|
330
|
+
|
|
331
|
+
if (cwd.startsWith(worktreesBase + path.sep)) {
|
|
332
|
+
const relative = path.relative(worktreesBase, cwd);
|
|
333
|
+
worktreeName = relative.split(path.sep)[0];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!worktreeName) {
|
|
338
|
+
console.error(chalk.red('Could not determine worktree name.'));
|
|
339
|
+
console.log(chalk.blue('Either run this from inside a worktree, or pass --name <worktree-name>'));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const root = getRepoRoot();
|
|
344
|
+
const registry = loadRegistry(root);
|
|
345
|
+
|
|
346
|
+
// Find the worktree entry in the registry
|
|
347
|
+
if (!registry.worktrees || !registry.worktrees[worktreeName]) {
|
|
348
|
+
console.error(chalk.red(`Worktree '${worktreeName}' not found in registry.`));
|
|
349
|
+
console.log(chalk.blue('Run: caws worktree list to see available worktrees'));
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Load the spec file
|
|
354
|
+
const specPath = findFeatureSpecPath(root, specId);
|
|
355
|
+
if (!specPath) {
|
|
356
|
+
console.error(chalk.red(`Spec '${specId}' not found in .caws/specs/`));
|
|
357
|
+
console.log(chalk.blue('Run: caws specs list to see available specs'));
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const specContent = fs.readFileSync(specPath, 'utf8');
|
|
362
|
+
const specData = yaml.load(specContent);
|
|
363
|
+
|
|
364
|
+
// Warn if spec already bound to a different worktree
|
|
365
|
+
if (specData.worktree && specData.worktree !== worktreeName) {
|
|
366
|
+
console.log(chalk.yellow(`Warning: Spec '${specId}' is currently bound to worktree '${specData.worktree}'.`));
|
|
367
|
+
console.log(chalk.yellow(`Rebinding to '${worktreeName}'.`));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Update registry side: set specId on the worktree entry
|
|
371
|
+
registry.worktrees[worktreeName].specId = specId;
|
|
372
|
+
saveRegistry(root, registry);
|
|
373
|
+
|
|
374
|
+
// Update spec side: set worktree field
|
|
375
|
+
specData.worktree = worktreeName;
|
|
376
|
+
const updatedYaml = yaml.dump(specData, { lineWidth: 120, noRefs: true });
|
|
377
|
+
fs.writeFileSync(specPath, updatedYaml, 'utf8');
|
|
378
|
+
|
|
379
|
+
console.log(chalk.green(`Binding established`));
|
|
380
|
+
console.log(chalk.gray(` Worktree: ${worktreeName} -> spec: ${specId}`));
|
|
381
|
+
console.log(chalk.gray(` Spec: ${specId} -> worktree: ${worktreeName}`));
|
|
382
|
+
console.log(chalk.gray(` Registry: ${path.join(root, '.caws', 'worktrees.json')}`));
|
|
383
|
+
console.log(chalk.gray(` Spec file: ${specPath}`));
|
|
384
|
+
}
|
|
385
|
+
|
|
306
386
|
module.exports = { worktreeCommand };
|
|
@@ -60,8 +60,13 @@ async function run({ stagedFiles, spec, _policy, projectRoot, riskTier, context
|
|
|
60
60
|
|
|
61
61
|
// Budget limits apply to changes, not to the entire repo.
|
|
62
62
|
// In cli context with all tracked files, budget check is meaningless.
|
|
63
|
+
// Return 'skipped' (not 'pass') so the pipeline summary counts this gate under
|
|
64
|
+
// `skipped`, matching pipeline.js semantics for gates that did not actually run.
|
|
63
65
|
if (context === 'cli') {
|
|
64
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
status: 'skipped',
|
|
68
|
+
messages: ['Budget check skipped in CLI context (budget applies to changes, not full repo)'],
|
|
69
|
+
};
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
// Build a minimal spec for deriveBudget
|
|
@@ -42,10 +42,17 @@ async function run({ projectRoot, spec }) {
|
|
|
42
42
|
return { status: 'fail', messages: ['Resolved spec is empty'] };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Try to find and load the schema
|
|
45
|
+
// Try to find and load the schema.
|
|
46
|
+
// CAWSFIX-03: The canonical project-level schema lives at
|
|
47
|
+
// `.caws/working-spec.schema.json` (flat layout, matches the pattern used for
|
|
48
|
+
// policy.schema.json and waiver.schema.json). Older projects may still have
|
|
49
|
+
// a `.caws/schemas/` directory from scaffolded templates, so we check both.
|
|
50
|
+
// Bundled fallbacks cover packaged installs.
|
|
46
51
|
const schemaPaths = [
|
|
52
|
+
path.join(projectRoot, '.caws', 'working-spec.schema.json'),
|
|
47
53
|
path.join(projectRoot, '.caws', 'schemas', 'working-spec.schema.json'),
|
|
48
54
|
path.join(projectRoot, 'node_modules', '@caws', 'cli', 'templates', '.caws', 'schemas', 'working-spec.schema.json'),
|
|
55
|
+
path.join(projectRoot, 'node_modules', '@paths.design', 'caws-cli', 'templates', '.caws', 'schemas', 'working-spec.schema.json'),
|
|
49
56
|
];
|
|
50
57
|
|
|
51
58
|
let schema = null;
|
package/dist/index.js
CHANGED
|
@@ -55,6 +55,7 @@ const { sessionCommand } = require('./commands/session');
|
|
|
55
55
|
const { parallelCommand } = require('./commands/parallel');
|
|
56
56
|
const { verifyAcsCommand } = require('./commands/verify-acs');
|
|
57
57
|
const { sidecarCommand } = require('./commands/sidecar');
|
|
58
|
+
const { scopeCommand } = require('./commands/scope');
|
|
58
59
|
|
|
59
60
|
// Import scaffold functionality
|
|
60
61
|
const { scaffoldProject, setScaffoldDependencies } = require('./scaffold');
|
|
@@ -380,6 +381,22 @@ worktreeCmd
|
|
|
380
381
|
.option('--force', 'Allow pruning entries owned by other sessions', false)
|
|
381
382
|
.action((options) => worktreeCommand('repair', options));
|
|
382
383
|
|
|
384
|
+
worktreeCmd
|
|
385
|
+
.command('bind <spec-id>')
|
|
386
|
+
.description('Bind a spec to this worktree (fixes mutual reference)')
|
|
387
|
+
.option('--name <name>', 'Worktree name (auto-detected from cwd if omitted)')
|
|
388
|
+
.action((specId, options) => worktreeCommand('bind', { specId, ...options }));
|
|
389
|
+
|
|
390
|
+
// Scope command group
|
|
391
|
+
const scopeCmd = program
|
|
392
|
+
.command('scope')
|
|
393
|
+
.description('Inspect and manage scope boundaries');
|
|
394
|
+
|
|
395
|
+
scopeCmd
|
|
396
|
+
.command('show')
|
|
397
|
+
.description('Show effective scope for the current context')
|
|
398
|
+
.action(() => scopeCommand('show'));
|
|
399
|
+
|
|
383
400
|
// Session command group
|
|
384
401
|
const sessionCmd = program
|
|
385
402
|
.command('session')
|
|
@@ -552,6 +569,15 @@ waiversCmd
|
|
|
552
569
|
.option('-v, --verbose', 'Show detailed error information', false)
|
|
553
570
|
.action((id, options) => waiversCommand('revoke', { ...options, id }));
|
|
554
571
|
|
|
572
|
+
waiversCmd
|
|
573
|
+
.command('prune')
|
|
574
|
+
.description('Prune expired waivers (dry-run by default; use --apply to persist)')
|
|
575
|
+
.option('--expired', 'Prune active waivers whose expires_at is in the past')
|
|
576
|
+
.option('--apply', 'Actually transition status (default: dry run)')
|
|
577
|
+
.option('--json', 'Emit machine-readable JSON output')
|
|
578
|
+
.option('-v, --verbose', 'Show detailed error information', false)
|
|
579
|
+
.action((options) => waiversCommand('prune', options));
|
|
580
|
+
|
|
555
581
|
// Workflow command group
|
|
556
582
|
program
|
|
557
583
|
.command('workflow <type>')
|
|
@@ -745,6 +771,7 @@ const VALID_COMMANDS = [
|
|
|
745
771
|
'session',
|
|
746
772
|
'parallel',
|
|
747
773
|
'verify-acs',
|
|
774
|
+
'scope',
|
|
748
775
|
];
|
|
749
776
|
|
|
750
777
|
program.exitOverride((err) => {
|
|
@@ -270,15 +270,17 @@ class PolicyManager {
|
|
|
270
270
|
throw new Error('Policy missing risk_tiers configuration');
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
273
|
+
const tierKeys = Object.keys(policy.risk_tiers);
|
|
274
|
+
if (tierKeys.length === 0) {
|
|
275
|
+
throw new Error('Policy risk_tiers must define at least one risk tier (1, 2, or 3)');
|
|
276
|
+
}
|
|
277
|
+
for (const key of tierKeys) {
|
|
278
|
+
if (!/^[1-3]$/.test(key)) {
|
|
279
|
+
throw new Error(`Policy risk_tiers has unknown tier '${key}' (expected 1, 2, or 3)`);
|
|
278
280
|
}
|
|
279
|
-
|
|
281
|
+
const budget = policy.risk_tiers[key];
|
|
280
282
|
if (typeof budget.max_files !== 'number' || typeof budget.max_loc !== 'number') {
|
|
281
|
-
throw new Error(`Risk tier ${
|
|
283
|
+
throw new Error(`Risk tier ${key} missing or invalid budget limits`);
|
|
282
284
|
}
|
|
283
285
|
}
|
|
284
286
|
|
|
@@ -12,6 +12,7 @@ const path = require('path');
|
|
|
12
12
|
const crypto = require('crypto');
|
|
13
13
|
|
|
14
14
|
const { mergeFilesTouched } = require('../utils/working-state');
|
|
15
|
+
const { appendEventSync } = require('../utils/event-log');
|
|
15
16
|
|
|
16
17
|
const SESSIONS_DIR = '.caws/sessions';
|
|
17
18
|
const REGISTRY_FILE = '.caws/sessions.json';
|
|
@@ -297,6 +298,22 @@ function startSession(options = {}) {
|
|
|
297
298
|
};
|
|
298
299
|
saveRegistry(root, registry);
|
|
299
300
|
|
|
301
|
+
// EVLOG-001: emit session_started event via the sync append path.
|
|
302
|
+
// spec_id is optional for this event type; we include it only when
|
|
303
|
+
// the session is bound to a spec.
|
|
304
|
+
const sessionStartedEvent = {
|
|
305
|
+
actor: 'session',
|
|
306
|
+
event: 'session_started',
|
|
307
|
+
data: {
|
|
308
|
+
session_id: sessionId,
|
|
309
|
+
role,
|
|
310
|
+
branch: capsule.base_state.branch,
|
|
311
|
+
head_rev: capsule.base_state.head_rev,
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
if (specId) sessionStartedEvent.spec_id = specId;
|
|
315
|
+
appendEventSync(sessionStartedEvent, { projectRoot: root, session_id: sessionId });
|
|
316
|
+
|
|
300
317
|
return capsule;
|
|
301
318
|
}
|
|
302
319
|
|
|
@@ -447,6 +464,23 @@ function endSession(data = {}) {
|
|
|
447
464
|
registry.sessions[sessionId].ended_at = capsule.ended_at;
|
|
448
465
|
saveRegistry(root, registry);
|
|
449
466
|
|
|
467
|
+
// EVLOG-001: emit session_ended event with final files_touched list.
|
|
468
|
+
// The renderer uses this to merge file touches into the spec view.
|
|
469
|
+
const sessionEndedEvent = {
|
|
470
|
+
actor: 'session',
|
|
471
|
+
event: 'session_ended',
|
|
472
|
+
data: {
|
|
473
|
+
session_id: sessionId,
|
|
474
|
+
files_touched: capsule.work_summary.paths_touched || [],
|
|
475
|
+
outcome: capsule.known_issues.some((i) => i.type === 'error') ? 'error' : 'success',
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
if (capsule.spec_id) {
|
|
479
|
+
sessionEndedEvent.spec_id = capsule.spec_id;
|
|
480
|
+
sessionEndedEvent.data.spec_id = capsule.spec_id;
|
|
481
|
+
}
|
|
482
|
+
appendEventSync(sessionEndedEvent, { projectRoot: root, session_id: sessionId });
|
|
483
|
+
|
|
450
484
|
return capsule;
|
|
451
485
|
}
|
|
452
486
|
|
|
@@ -1,50 +1,112 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "
|
|
3
|
-
"title": "CAWS Policy",
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
3
|
"type": "object",
|
|
5
|
-
"required": [
|
|
4
|
+
"required": [
|
|
5
|
+
"version",
|
|
6
|
+
"risk_tiers",
|
|
7
|
+
"edit_rules"
|
|
8
|
+
],
|
|
6
9
|
"properties": {
|
|
7
|
-
"version": {
|
|
10
|
+
"version": {
|
|
11
|
+
"type": "integer",
|
|
12
|
+
"enum": [
|
|
13
|
+
1
|
|
14
|
+
],
|
|
15
|
+
"description": "Policy schema version"
|
|
16
|
+
},
|
|
8
17
|
"risk_tiers": {
|
|
9
18
|
"type": "object",
|
|
10
|
-
"
|
|
11
|
-
"1": {
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
"patternProperties": {
|
|
20
|
+
"^[1-3]$": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"required": [
|
|
23
|
+
"max_files",
|
|
24
|
+
"max_loc"
|
|
25
|
+
],
|
|
26
|
+
"properties": {
|
|
27
|
+
"max_files": {
|
|
28
|
+
"type": "integer",
|
|
29
|
+
"minimum": 1,
|
|
30
|
+
"description": "Maximum files allowed for this risk tier"
|
|
31
|
+
},
|
|
32
|
+
"max_loc": {
|
|
33
|
+
"type": "integer",
|
|
34
|
+
"minimum": 1,
|
|
35
|
+
"description": "Maximum lines of code allowed for this risk tier"
|
|
36
|
+
},
|
|
37
|
+
"description": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Human-readable description of the tier"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"additionalProperties": false
|
|
43
|
+
}
|
|
14
44
|
},
|
|
15
|
-
"
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"description": "Risk tier definitions with budget limits"
|
|
16
47
|
},
|
|
17
48
|
"edit_rules": {
|
|
18
49
|
"type": "object",
|
|
50
|
+
"required": [
|
|
51
|
+
"policy_and_code_same_pr",
|
|
52
|
+
"min_approvers_for_budget_raise"
|
|
53
|
+
],
|
|
19
54
|
"properties": {
|
|
20
|
-
"policy_and_code_same_pr": {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
55
|
+
"policy_and_code_same_pr": {
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"description": "Whether policy and code changes can be in the same PR"
|
|
58
|
+
},
|
|
59
|
+
"min_approvers_for_budget_raise": {
|
|
60
|
+
"type": "integer",
|
|
61
|
+
"minimum": 1,
|
|
62
|
+
"description": "Minimum approvers required for budget increases"
|
|
63
|
+
},
|
|
64
|
+
"require_signed_commits": {
|
|
65
|
+
"type": "boolean",
|
|
66
|
+
"description": "Whether signed commits are required for policy changes"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"description": "Rules governing policy file edits"
|
|
24
71
|
},
|
|
25
72
|
"gates": {
|
|
26
73
|
"type": "object",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
74
|
+
"patternProperties": {
|
|
75
|
+
"^.*$": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"required": [
|
|
78
|
+
"enabled"
|
|
79
|
+
],
|
|
80
|
+
"properties": {
|
|
81
|
+
"enabled": {
|
|
82
|
+
"type": "boolean",
|
|
83
|
+
"description": "Whether this gate is active"
|
|
84
|
+
},
|
|
85
|
+
"mode": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"enum": [
|
|
88
|
+
"warn",
|
|
89
|
+
"block",
|
|
90
|
+
"skip"
|
|
91
|
+
],
|
|
92
|
+
"description": "How the gate reports failures: warn, block, or skip entirely"
|
|
93
|
+
},
|
|
94
|
+
"description": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "Human-readable description of the gate"
|
|
97
|
+
},
|
|
98
|
+
"thresholds": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"description": "Gate-specific thresholds (e.g. warning/critical limits)"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"additionalProperties": false
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"additionalProperties": false,
|
|
107
|
+
"description": "Quality gate configurations"
|
|
37
108
|
}
|
|
38
109
|
},
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"type": "object",
|
|
42
|
-
"required": ["max_files", "max_loc"],
|
|
43
|
-
"properties": {
|
|
44
|
-
"max_files": { "type": "integer", "minimum": 1 },
|
|
45
|
-
"max_loc": { "type": "integer", "minimum": 1 },
|
|
46
|
-
"description": { "type": "string" }
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
110
|
+
"additionalProperties": false,
|
|
111
|
+
"title": "CAWS Policy"
|
|
50
112
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"title": "CAWS Lite Scope Configuration",
|
|
4
|
-
"description": "Scope configuration for CAWS lite mode — guardrails without YAML specs",
|
|
4
|
+
"description": "Scope configuration for CAWS lite mode — guardrails without YAML specs. This schema governs the standalone .caws/scope.json file ONLY; inline scope: blocks inside working-spec.yaml or feature specs are governed by the working-spec schema's scope sub-schema and do NOT invoke this schema. See CAWSFIX-11.",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"required": ["
|
|
6
|
+
"required": ["allowedDirectories"],
|
|
7
7
|
"properties": {
|
|
8
8
|
"version": {
|
|
9
9
|
"type": "integer",
|
|
10
10
|
"const": 1,
|
|
11
|
-
"description": "Schema version"
|
|
11
|
+
"description": "Schema version. Optional for back-compat with scope.json files that predate versioning; the runtime (src/config/lite-scope.js) defaults to 1 when missing. If present, must be exactly 1. CAWSFIX-11 lifted `version` from the required list because no code path enforces a version mismatch — only the schema did, producing spurious warnings for pre-versioning scope.json files."
|
|
12
12
|
},
|
|
13
13
|
"allowedDirectories": {
|
|
14
14
|
"type": "array",
|