@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.
Files changed (54) hide show
  1. package/README.md +13 -5
  2. package/dist/budget-derivation.js +221 -74
  3. package/dist/commands/evaluate.js +26 -12
  4. package/dist/commands/gates.js +31 -4
  5. package/dist/commands/init.js +7 -4
  6. package/dist/commands/iterate.js +7 -3
  7. package/dist/commands/scope.js +264 -0
  8. package/dist/commands/sidecar.js +6 -3
  9. package/dist/commands/specs.js +148 -1
  10. package/dist/commands/status.js +8 -4
  11. package/dist/commands/templates.js +0 -8
  12. package/dist/commands/validate.js +34 -13
  13. package/dist/commands/verify-acs.js +25 -10
  14. package/dist/commands/waivers.js +147 -5
  15. package/dist/commands/worktree.js +81 -1
  16. package/dist/gates/budget-limit.js +6 -1
  17. package/dist/gates/spec-completeness.js +8 -1
  18. package/dist/index.js +27 -0
  19. package/dist/policy/PolicyManager.js +9 -7
  20. package/dist/session/session-manager.js +34 -0
  21. package/dist/templates/.caws/schemas/policy.schema.json +96 -34
  22. package/dist/templates/.caws/schemas/scope.schema.json +3 -3
  23. package/dist/templates/.caws/schemas/waivers.schema.json +91 -21
  24. package/dist/templates/.caws/schemas/working-spec.schema.json +253 -89
  25. package/dist/templates/.caws/templates/working-spec.template.yml +3 -1
  26. package/dist/templates/.caws/tools/scope-guard.js +66 -15
  27. package/dist/templates/.claude/README.md +1 -1
  28. package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
  29. package/dist/templates/.claude/hooks/scope-guard.sh +106 -27
  30. package/dist/templates/.claude/hooks/worktree-write-guard.sh +96 -3
  31. package/dist/templates/.claude/settings.json +5 -0
  32. package/dist/templates/CLAUDE.md +34 -0
  33. package/dist/templates/agents.md +21 -0
  34. package/dist/utils/event-log.js +584 -0
  35. package/dist/utils/event-renderer.js +521 -0
  36. package/dist/utils/schema-validator.js +10 -2
  37. package/dist/utils/working-state.js +25 -0
  38. package/dist/validation/spec-validation.js +99 -9
  39. package/dist/waivers-manager.js +84 -0
  40. package/dist/worktree/worktree-manager.js +214 -8
  41. package/package.json +5 -4
  42. package/templates/.caws/schemas/policy.schema.json +96 -34
  43. package/templates/.caws/schemas/scope.schema.json +3 -3
  44. package/templates/.caws/schemas/waivers.schema.json +91 -21
  45. package/templates/.caws/schemas/working-spec.schema.json +253 -89
  46. package/templates/.caws/templates/working-spec.template.yml +3 -1
  47. package/templates/.caws/tools/scope-guard.js +66 -15
  48. package/templates/.claude/README.md +1 -1
  49. package/templates/.claude/hooks/protected-paths.sh +39 -0
  50. package/templates/.claude/hooks/scope-guard.sh +106 -27
  51. package/templates/.claude/hooks/worktree-write-guard.sh +96 -3
  52. package/templates/.claude/settings.json +5 -0
  53. package/templates/CLAUDE.md +34 -0
  54. package/templates/agents.md +21 -0
@@ -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 uses patternProperties keyed by waiver ID
182
- const waiverDoc = { [waiverId]: waiver };
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({ [waiver.id]: waiver });
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 { status: 'pass', messages: ['Budget check skipped in CLI context (budget applies to changes, not full repo)'] };
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
- // Validate all tiers have required fields
274
- for (const tier of [1, 2, 3]) {
275
- const budget = policy.risk_tiers[tier];
276
- if (!budget) {
277
- throw new Error(`Policy missing risk tier ${tier} configuration`);
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 ${tier} missing or invalid budget limits`);
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": "https://json-schema.org/draft/2020-12/schema",
3
- "title": "CAWS Policy",
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
3
  "type": "object",
5
- "required": ["version", "risk_tiers"],
4
+ "required": [
5
+ "version",
6
+ "risk_tiers",
7
+ "edit_rules"
8
+ ],
6
9
  "properties": {
7
- "version": { "type": "integer", "const": 1 },
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
- "properties": {
11
- "1": { "$ref": "#/$defs/tier" },
12
- "2": { "$ref": "#/$defs/tier" },
13
- "3": { "$ref": "#/$defs/tier" }
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
- "required": ["1", "2", "3"]
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": { "type": "boolean" },
21
- "min_approvers_for_budget_raise": { "type": "integer", "minimum": 1 },
22
- "require_signed_commits": { "type": "boolean" }
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
- "additionalProperties": {
28
- "type": "object",
29
- "properties": {
30
- "enabled": { "type": "boolean" },
31
- "description": { "type": "string" },
32
- "mode": { "type": "string", "enum": ["block", "warn", "skip"] },
33
- "thresholds": { "type": "object" }
34
- },
35
- "required": ["enabled"]
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
- "$defs": {
40
- "tier": {
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": ["version", "allowedDirectories"],
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",