@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.4

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 (91) hide show
  1. package/bin/__tests__/codex-hooks.test.js +444 -14
  2. package/bin/__tests__/codex-native.test.js +228 -7
  3. package/bin/__tests__/develop-contract.test.js +615 -185
  4. package/bin/__tests__/installer-safety.test.js +39 -0
  5. package/bin/__tests__/opencode-spec-gate.test.js +24 -16
  6. package/bin/__tests__/package-inventory.test.js +687 -13
  7. package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
  8. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
  9. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
  10. package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
  11. package/bin/__tests__/validator-grounding.test.js +1914 -221
  12. package/bin/lib/managed-writer.js +12 -3
  13. package/bin/phases/codex-runtime.js +17 -1
  14. package/package.json +1 -1
  15. package/src/claude/CLAUDE.md +6 -0
  16. package/src/claude/agents/code-auditor.md +20 -0
  17. package/src/claude/agents/spec-maker.md +143 -99
  18. package/src/claude/hooks/__tests__/completion-authority.test.js +291 -58
  19. package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
  20. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
  21. package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
  22. package/src/claude/hooks/completion-authority-check.cjs +37 -288
  23. package/src/claude/hooks/completion-authority-state.cjs +10 -8
  24. package/src/claude/hooks/completion-authority.cjs +13 -7
  25. package/src/claude/hooks/docs-sync.cjs +5 -2
  26. package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
  27. package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
  28. package/src/claude/hooks/privacy-block.cjs +77 -30
  29. package/src/claude/hooks/semantic-review-authority.cjs +152 -0
  30. package/src/claude/hooks/spec-gate.cjs +47 -117
  31. package/src/claude/hooks/spec-state.cjs +8 -2
  32. package/src/claude/migration-manifest.json +9 -1
  33. package/src/claude/rules/state-sync.md +52 -21
  34. package/src/claude/rules/workflow.md +17 -9
  35. package/src/claude/scripts/spec-final-state.cjs +244 -0
  36. package/src/claude/scripts/spec-ground.cjs +535 -173
  37. package/src/claude/scripts/spec-readiness.cjs +118 -0
  38. package/src/claude/scripts/spec-receipt.cjs +162 -0
  39. package/src/claude/scripts/spec-resolver.cjs +67 -16
  40. package/src/claude/scripts/spec-scaffold.cjs +1354 -139
  41. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  42. package/src/claude/scripts/validate-spec-output.cjs +2782 -142
  43. package/src/claude/scripts/workflow-policy.cjs +501 -183
  44. package/src/claude/settings/settings.json +4 -0
  45. package/src/claude/skills/code-review/SKILL.md +12 -3
  46. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  47. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  48. package/src/claude/skills/develop/SKILL.md +47 -60
  49. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  50. package/src/claude/skills/develop/references/quality-gate.md +19 -12
  51. package/src/claude/skills/specs/SKILL.md +183 -177
  52. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  53. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  54. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  55. package/src/claude/skills/specs/references/review.md +200 -260
  56. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  57. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  58. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  59. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  60. package/src/claude/skills/specs/rules/design-review.md +42 -97
  61. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  62. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  63. package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
  64. package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
  65. package/src/claude/skills/specs/templates/design.md +49 -263
  66. package/src/claude/skills/specs/templates/requirements.md +26 -36
  67. package/src/claude/skills/specs/templates/research.md +9 -98
  68. package/src/claude/skills/specs/templates/spec-state.json +23 -85
  69. package/src/claude/skills/specs/templates/task.md +30 -102
  70. package/src/claude/skills/sync/SKILL.md +33 -10
  71. package/src/claude/skills/sync/references/sync-protocols.md +31 -13
  72. package/src/claude/skills/test/SKILL.md +17 -7
  73. package/src/codex/AGENTS.md +6 -0
  74. package/src/codex/hooks/completion-authority-check.cjs +43 -240
  75. package/src/codex/hooks/completion-authority-state.cjs +10 -8
  76. package/src/codex/hooks/completion-authority.cjs +13 -7
  77. package/src/codex/hooks/lib/hook-context.cjs +16 -2
  78. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  79. package/src/codex/hooks/lib/privacy-state.cjs +38 -16
  80. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  81. package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
  82. package/src/codex/hooks/lib/spec-utils.cjs +56 -5
  83. package/src/codex/hooks/privacy-approval.cjs +1 -1
  84. package/src/codex/hooks/privacy-block.cjs +67 -11
  85. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  86. package/src/codex/hooks/spec-gate.cjs +49 -34
  87. package/src/codex/hooks/spec-state.cjs +10 -3
  88. package/src/codex/hooks/state.cjs +2 -0
  89. package/src/codex/hooks.json +6 -1
  90. package/src/codex/rules/state-sync.md +47 -27
  91. package/src/common/AGENTS.md +1 -1
@@ -10,21 +10,43 @@ const LEGACY_DIAGNOSTIC_VERDICTS = Object.freeze(['PARTIAL', 'NO_TESTS']);
10
10
  const REVIEW_VERDICTS = CANONICAL_VERDICTS;
11
11
  const EXECUTION_TIERS = Object.freeze(['Light', 'Standard', 'Deep']);
12
12
  const LANES = Object.freeze(['Direct', 'Standard', 'Critical']);
13
+ const PLANNING_DEPTHS = Object.freeze(['None', 'Compact', 'Full']);
14
+ const ASSURANCE_LEVELS = Object.freeze(['Routine', 'Elevated', 'Strict']);
13
15
  const ARTIFACT_PROFILES = Object.freeze(['targeted', 'bounded', 'strict']);
16
+ const PLANNING_OBLIGATIONS = Object.freeze([
17
+ 'needsRequirements',
18
+ 'needsDesign',
19
+ ]);
14
20
  const PROOF_OBLIGATIONS = Object.freeze([
15
21
  'needsInspection',
16
22
  'needsExecutionProof',
17
23
  'needsIndependentAudit',
18
24
  'needsDurableTaskState',
25
+ ]);
26
+ const READ_COMPAT_PROOF_OBLIGATIONS = Object.freeze([
27
+ ...PROOF_OBLIGATIONS,
19
28
  'needsResearchGrounding',
20
29
  ]);
21
- const WORKFLOW_POLICY_VERSION = '1';
30
+ const WORKFLOW_POLICY_VERSION = '2';
31
+ const CANONICAL_WORKFLOW_POLICY_VERSION = '2.1';
32
+ const CANONICAL_WORKFLOW_POLICY_FIELDS = Object.freeze([
33
+ 'version',
34
+ 'planning_depth',
35
+ 'assurance_level',
36
+ 'classified_minimum',
37
+ 'risks',
38
+ ]);
22
39
  const WORKFLOW_POLICY_FIELDS = Object.freeze([
23
40
  'version',
41
+ 'planning_depth',
42
+ 'automatic_planning_depth',
43
+ 'assurance_level',
44
+ 'automatic_assurance_level',
24
45
  'lane',
25
46
  'automatic_lane',
26
47
  'risks',
27
48
  'artifact_profile',
49
+ 'planning_obligations',
28
50
  'proof_obligations',
29
51
  'actor_needs',
30
52
  'override_receipt',
@@ -41,7 +63,12 @@ const INDEPENDENT_AUDIT_FIELDS = Object.freeze([
41
63
  ]);
42
64
  const FLASH_PROMOTION_FIELDS = Object.freeze(['readyForSync', 'flashTransition', 'promotionReceipt']);
43
65
  const LEGACY_TIER_LANES = Object.freeze({ Light: 'Direct', Standard: 'Standard', Deep: 'Critical' });
66
+ const V1_POLICY_FIELDS = Object.freeze([
67
+ 'version', 'lane', 'automatic_lane', 'risks', 'artifact_profile',
68
+ 'proof_obligations', 'actor_needs', 'override_receipt',
69
+ ]);
44
70
  const ARTIFACT_PROFILE_BY_LANE = Object.freeze({ Direct: 'targeted', Standard: 'bounded', Critical: 'strict' });
71
+ const ARTIFACT_PROFILE_BY_PLANNING = Object.freeze({ None: 'targeted', Compact: 'bounded', Full: 'strict' });
45
72
  const ACTOR_NEEDS_BY_OBLIGATION = Object.freeze({
46
73
  needsInspection: Object.freeze({ capability: 'inspection', independence: 'same-session' }),
47
74
  needsExecutionProof: Object.freeze({ capability: 'execution-proof', independence: 'same-session' }),
@@ -51,27 +78,13 @@ const ACTOR_NEEDS_BY_OBLIGATION = Object.freeze({
51
78
  });
52
79
  const ACTOR_CAPABILITIES = new Set(Object.values(ACTOR_NEEDS_BY_OBLIGATION).map((need) => need.capability));
53
80
  const ACTOR_INDEPENDENCE = new Set(['same-session', 'independent']);
54
- const CRITICAL_RISKS = new Set([
55
- 'reversibility',
56
- 'destructive',
57
- 'auth',
58
- 'payment',
59
- 'privacy',
60
- 'data',
61
- 'schema',
62
- 'migration',
63
- 'publicContract',
64
- 'crossRuntime',
65
- 'ambiguity',
66
- 'rollback',
67
- ]);
68
81
  const LANE_RISK_KEYS = Object.freeze({
69
82
  reversibility: ['reversible', 'reversibility', 'irreversible', 'non-reversible'],
70
83
  destructive: ['destructive', 'deletion', 'delete', 'destroy'],
71
84
  auth: ['auth', 'authentication', 'authorization'],
72
85
  payment: ['payment', 'billing', 'charge'],
73
86
  privacy: ['privacy', 'pii', 'personal data'],
74
- data: ['data', 'dataset', 'records'],
87
+ data: ['data loss', 'loss of data', 'data corruption', 'corrupt data', 'data integrity'],
75
88
  schema: ['schema'],
76
89
  migration: ['migration', 'migrate', 'database migration'],
77
90
  publicContract: ['publicContract', 'public_contract', 'public contract', 'api contract', 'breaking change', 'backward compatibility'],
@@ -415,9 +428,16 @@ function hasExplicitFailure(body) {
415
428
  return false;
416
429
  }
417
430
  const trustedClassifications = new WeakSet();
431
+ const trustedV1Adapters = new WeakSet();
432
+
433
+ function clonePolicySnapshot(snapshot) {
434
+ const clone = JSON.parse(JSON.stringify(snapshot));
435
+ if (trustedV1Adapters.has(snapshot)) trustedV1Adapters.add(clone);
436
+ return clone;
437
+ }
418
438
 
419
439
  const APPROVAL_SCHEMA_VERSION = '2.0';
420
- const LEGACY_APPROVAL_ERROR = `Legacy approval state detected (field \`approved\`). Migration required: replace \`approved\` with \`agent_validated\` and \`user_approved\` per schema v${APPROVAL_SCHEMA_VERSION} (schema_version: "${APPROVAL_SCHEMA_VERSION}"). See spec-state.json template. Refusing to infer user approval.`;
440
+ const LEGACY_APPROVAL_ERROR = `Legacy approval state detected (field \`approved\`). Migration required: replace \`approved\` with \`agent_validated\` per schema v${APPROVAL_SCHEMA_VERSION} (schema_version: "${APPROVAL_SCHEMA_VERSION}"). See spec-state.json template.`;
421
441
 
422
442
  function assertLane(lane) {
423
443
  if (!LANES.includes(lane)) {
@@ -426,6 +446,35 @@ function assertLane(lane) {
426
446
  return lane;
427
447
  }
428
448
 
449
+ function assertPlanningDepth(depth) {
450
+ if (!PLANNING_DEPTHS.includes(depth)) {
451
+ throw new TypeError(`Unsupported planning depth: ${String(depth)}`);
452
+ }
453
+ return depth;
454
+ }
455
+
456
+ function assertAssuranceLevel(level) {
457
+ if (!ASSURANCE_LEVELS.includes(level)) {
458
+ throw new TypeError(`Unsupported assurance level: ${String(level)}`);
459
+ }
460
+ return level;
461
+ }
462
+
463
+ function compatibilityLane(planningDepth, assuranceLevel) {
464
+ assertPlanningDepth(planningDepth);
465
+ assertAssuranceLevel(assuranceLevel);
466
+ if (assuranceLevel === 'Strict') return 'Critical';
467
+ if (planningDepth === 'None' && assuranceLevel === 'Routine') return 'Direct';
468
+ return 'Standard';
469
+ }
470
+
471
+ function v1Axes(lane) {
472
+ assertLane(lane);
473
+ if (lane === 'Direct') return { planningDepth: 'None', assuranceLevel: 'Routine' };
474
+ if (lane === 'Critical') return { planningDepth: 'Full', assuranceLevel: 'Strict' };
475
+ return { planningDepth: 'Compact', assuranceLevel: 'Routine' };
476
+ }
477
+
429
478
  function asTrue(value) {
430
479
  return value === true || value === 1 || value === 'true' || value === 'yes';
431
480
  }
@@ -444,11 +493,13 @@ function textValue(input) {
444
493
 
445
494
  function classifyRiskSignals(input = {}) {
446
495
  const nestedSignals = input.signals && typeof input.signals === 'object' ? input.signals : {};
447
- const declaredSignals = [input.riskSignals, input.risks]
496
+ const declaredRiskValues = [input.riskSignals, input.risks]
448
497
  .flatMap((value) => Array.isArray(value) ? value : [])
449
- .filter((value) => typeof value === 'string')
498
+ .filter((value) => typeof value === 'string');
499
+ const declaredSignals = declaredRiskValues
450
500
  .join(' ')
451
501
  .toLowerCase();
502
+ const explicitRisk = (terms) => declaredRiskValues.some((value) => terms.includes(value.trim().toLowerCase()));
452
503
  const source = {
453
504
  ...input,
454
505
  ...nestedSignals,
@@ -469,7 +520,9 @@ function classifyRiskSignals(input = {}) {
469
520
  auth: signalValue(source, LANE_RISK_KEYS.auth) || hasTerm(LANE_RISK_KEYS.auth),
470
521
  payment: signalValue(source, LANE_RISK_KEYS.payment) || hasTerm(LANE_RISK_KEYS.payment),
471
522
  privacy: signalValue(source, LANE_RISK_KEYS.privacy) || hasTerm(LANE_RISK_KEYS.privacy),
472
- data: signalValue(source, LANE_RISK_KEYS.data) || hasTerm(LANE_RISK_KEYS.data) || /\bdata\b/.test(declaredSignals),
523
+ data: signalValue(source, ['data', 'dataset', 'records'])
524
+ || explicitRisk(['data', 'dataset', 'records'])
525
+ || hasTerm(LANE_RISK_KEYS.data),
473
526
  schema: signalValue(source, LANE_RISK_KEYS.schema) || hasTerm(LANE_RISK_KEYS.schema),
474
527
  migration: signalValue(source, LANE_RISK_KEYS.migration) || hasTerm(LANE_RISK_KEYS.migration),
475
528
  publicContract: signalValue(source, LANE_RISK_KEYS.publicContract) || hasTerm(LANE_RISK_KEYS.publicContract),
@@ -499,23 +552,6 @@ function overrideLane(input = {}) {
499
552
  return input.override ?? input.laneOverride ?? input.requestedLane ?? input.lane ?? null;
500
553
  }
501
554
 
502
- function isValidOverrideReceipt(receipt, automaticLane, requestedLane, risks = []) {
503
- // P0.4 fail-closed: no trusted runtime issuer exists yet.
504
- // Even a correctly shaped object with source:runtime must NOT be considered valid provenance.
505
- // Downgrade is blocked until a verifiable, runtime-issued override receipt is implemented
506
- // (bound to automaticLane/requestedLane/risks/session and verifiable by that issuer).
507
- // Keep this function fail-closed; do not accept plain objects — always return false for P0.
508
- return false;
509
- }
510
-
511
- function isUserAuthorizedForDowngrade(input = {}, automaticLane, requestedLane, risks) {
512
- // P0.4: downgrade requires structured override receipt with runtime provenance.
513
- // Boolean flags (userAuthorized, user_approved, etc.) are never trusted.
514
- // Fail closed — no trusted issuer exists yet, so always false.
515
- // Explicitly ignore all boolean/string downgrade flags to prevent forged approvals.
516
- return false;
517
- }
518
-
519
555
  function freezeClassification(obj) {
520
556
  Object.freeze(obj);
521
557
  if (Array.isArray(obj.warnings)) Object.freeze(obj.warnings);
@@ -525,63 +561,80 @@ function freezeClassification(obj) {
525
561
  return obj;
526
562
  }
527
563
 
564
+ function requestedAxis(input, snake, camel) {
565
+ return input[snake] ?? input[camel] ?? null;
566
+ }
567
+
568
+ function inferredPlanningDepth(input, taskCount) {
569
+ const complexity = String(input.systemComplexity ?? input.system_complexity ?? input.complexity ?? '').toLowerCase();
570
+ const scope = String(input.scope ?? input.scopeSize ?? input.scope_size ?? '').toLowerCase();
571
+ if (['high', 'complex', 'system', 'cross-system'].includes(complexity)
572
+ || ['large', 'broad', 'multi-package', 'system'].includes(scope)
573
+ || taskCount >= 5) return 'Full';
574
+ if (explicitReversible(input) && input.lowRisk === true && input.isolated === true && taskCount <= 2) return 'None';
575
+ return 'Compact';
576
+ }
577
+
578
+ function minimumAssuranceForRisks(risks = []) {
579
+ const listed = Array.isArray(risks) ? risks : [];
580
+ return listed.length > 0 ? 'Elevated' : 'Routine';
581
+ }
582
+
528
583
  function classifyLane(input = {}) {
529
584
  if (typeof input === 'string') {
530
- const c = { lane: assertLane(input), automaticLane: input, overridden: false, warnings: [], risks: [], signals: {} };
531
- return freezeClassification(c);
585
+ const axes = v1Axes(input);
586
+ return freezeClassification({
587
+ ...axes,
588
+ automaticPlanningDepth: axes.planningDepth,
589
+ automaticAssuranceLevel: axes.assuranceLevel,
590
+ lane: input,
591
+ automaticLane: input,
592
+ overridden: false,
593
+ warnings: [], risks: [], signals: {},
594
+ });
532
595
  }
533
596
  if (!input || typeof input !== 'object') throw new TypeError('Lane classification input must be an object');
534
-
535
- const signals = classifyRiskSignals(input);
536
- const risks = Object.entries(signals).filter(([, active]) => active).map(([name]) => name);
537
597
  const taskCount = input.taskCount === undefined ? 1 : input.taskCount;
538
598
  if (!Number.isInteger(taskCount) || taskCount < 1) throw new RangeError('taskCount must be a positive integer');
539
599
 
540
- const automaticLane = risks.length > 0
541
- ? 'Critical'
542
- : explicitReversible(input) && input.lowRisk === true && input.isolated === true && taskCount <= 2
543
- ? 'Direct'
544
- : 'Standard';
545
- const requested = overrideLane(input);
546
- if (requested === null || requested === undefined) {
547
- const c = {
548
- lane: automaticLane,
549
- automaticLane,
550
- overridden: false,
551
- warnings: [],
552
- risks,
553
- signals,
554
- };
555
- return freezeClassification(c);
556
- }
557
- assertLane(requested);
558
- const isDowngrade = LANES.indexOf(requested) < LANES.indexOf(automaticLane);
559
- if (isDowngrade && !isUserAuthorizedForDowngrade(input, automaticLane, requested, risks)) {
560
- throw new Error(
561
- `Downgrade from ${automaticLane} to ${requested} is blocked: no trusted runtime issuer is available to authorize Critical downgrades. ` +
562
- `Automatic ${automaticLane} due to: ${risks.join(', ') || 'default risk policy'}. ` +
563
- `Keep ${automaticLane} or escalate; downgrade is unavailable until a verifiable runtime override receipt is implemented.`
564
- );
565
- }
566
- const warnings = [`Explicit lane override selected: ${requested}.`];
567
- if (requested !== automaticLane) {
568
- warnings.push(`Override changes automatic ${automaticLane} classification to ${requested}.`);
569
- if (LANES.indexOf(requested) < LANES.indexOf(automaticLane)) {
570
- warnings.push(`Downgrading risk lane may reduce review and evidence coverage: ${risks.join(', ') || 'default risk policy'}.`);
571
- if (isUserAuthorizedForDowngrade(input, automaticLane, requested, risks)) {
572
- warnings.push(`Downgrade authorized by user - proceeding with ${requested} despite ${automaticLane} signals.`);
573
- }
574
- }
575
- }
576
- const c2 = {
577
- lane: requested,
600
+ const signals = classifyRiskSignals(input);
601
+ const explicitRisks = Array.isArray(input.risks)
602
+ ? input.risks.filter((risk) => typeof risk === 'string' && risk.trim() !== '')
603
+ : [];
604
+ const risks = [...new Set([
605
+ ...Object.entries(signals).filter(([, active]) => active).map(([name]) => name),
606
+ ...explicitRisks,
607
+ ])];
608
+ const inferredPlanning = inferredPlanningDepth(input, taskCount);
609
+ const automaticAssurance = minimumAssuranceForRisks(risks);
610
+ const legacyRequested = overrideLane(input);
611
+ const legacyAxes = legacyRequested === null || legacyRequested === undefined ? null : v1Axes(assertLane(legacyRequested));
612
+ const requestedPlanning = requestedAxis(input, 'planning_depth', 'planningDepth') ?? legacyAxes?.planningDepth;
613
+ const requestedAssurance = requestedAxis(input, 'assurance_level', 'assuranceLevel') ?? legacyAxes?.assuranceLevel;
614
+ if (requestedPlanning !== null && requestedPlanning !== undefined) assertPlanningDepth(requestedPlanning);
615
+ if (requestedAssurance !== null && requestedAssurance !== undefined) assertAssuranceLevel(requestedAssurance);
616
+ if (requestedPlanning && PLANNING_DEPTHS.indexOf(requestedPlanning) < PLANNING_DEPTHS.indexOf(inferredPlanning)) {
617
+ throw new Error(`planning_depth downgrade from ${inferredPlanning} to ${requestedPlanning} is not permitted`);
618
+ }
619
+ if (requestedAssurance && ASSURANCE_LEVELS.indexOf(requestedAssurance) < ASSURANCE_LEVELS.indexOf(automaticAssurance)) {
620
+ throw new Error(`assurance_level downgrade from ${automaticAssurance} to ${requestedAssurance} is blocked for risks: ${risks.join(', ')}`);
621
+ }
622
+ const planningDepth = requestedPlanning || inferredPlanning;
623
+ const assuranceLevel = requestedAssurance || automaticAssurance;
624
+ const lane = compatibilityLane(planningDepth, assuranceLevel);
625
+ const automaticLane = compatibilityLane(inferredPlanning, automaticAssurance);
626
+ return freezeClassification({
627
+ planningDepth,
628
+ automaticPlanningDepth: inferredPlanning,
629
+ assuranceLevel,
630
+ automaticAssuranceLevel: automaticAssurance,
631
+ lane,
578
632
  automaticLane,
579
- overridden: true,
580
- warnings,
633
+ overridden: Boolean(requestedPlanning || requestedAssurance || legacyAxes),
634
+ warnings: legacyAxes ? [`Legacy lane ${legacyRequested} adapted to Specs v2 axes.`] : [],
581
635
  risks,
582
636
  signals,
583
- };
584
- return freezeClassification(c2);
637
+ });
585
638
  }
586
639
 
587
640
  function uniqueStrings(values, field) {
@@ -593,67 +646,184 @@ function uniqueStrings(values, field) {
593
646
  return { valid: true };
594
647
  }
595
648
 
596
- function obligationsForLane(lane, risks = []) {
597
- assertLane(lane);
598
- const riskSet = new Set(risks);
649
+ function planningObligationsFor(depth) {
650
+ assertPlanningDepth(depth);
651
+ if (depth === 'None') return [];
652
+ if (depth === 'Compact') return ['needsRequirements', 'needsDesign'];
653
+ return [...PLANNING_OBLIGATIONS];
654
+ }
655
+
656
+ function obligationsForAssurance(level, risks = []) {
657
+ assertAssuranceLevel(level);
599
658
  const obligations = new Set(['needsExecutionProof']);
600
- if (lane === 'Standard') obligations.add('needsInspection');
601
- if (lane === 'Critical') {
659
+ if (level === 'Elevated') obligations.add('needsInspection');
660
+ if (level === 'Strict') {
602
661
  obligations.add('needsInspection');
603
662
  obligations.add('needsIndependentAudit');
604
- if ([
605
- 'destructive', 'reversibility', 'rollback', 'data', 'schema', 'migration',
606
- 'privacy', 'payment', 'crossRuntime',
607
- ].some((risk) => riskSet.has(risk))) {
608
- obligations.add('needsDurableTaskState');
609
- }
610
- if ([
611
- 'auth', 'payment', 'privacy', 'schema', 'migration', 'publicContract',
612
- 'crossRuntime', 'ambiguity',
613
- ].some((risk) => riskSet.has(risk))) {
614
- obligations.add('needsResearchGrounding');
615
- }
616
663
  }
617
664
  return PROOF_OBLIGATIONS.filter((obligation) => obligations.has(obligation));
618
665
  }
619
666
 
667
+ function obligationsForLane(lane, risks = []) {
668
+ return obligationsForAssurance(v1Axes(assertLane(lane)).assuranceLevel, risks);
669
+ }
670
+
671
+ function legacyObligationsForLane(lane, risks = []) {
672
+ assertLane(lane);
673
+ const riskSet = new Set(risks);
674
+ const selected = new Set(['needsExecutionProof']);
675
+ if (lane !== 'Direct') selected.add('needsInspection');
676
+ if (lane === 'Critical') selected.add('needsIndependentAudit');
677
+ if (lane === 'Critical' && [
678
+ 'destructive', 'reversibility', 'rollback', 'data', 'schema', 'migration',
679
+ 'privacy', 'payment', 'crossRuntime',
680
+ ].some((risk) => riskSet.has(risk))) selected.add('needsDurableTaskState');
681
+ if (lane === 'Critical' && [
682
+ 'auth', 'payment', 'privacy', 'schema', 'migration', 'publicContract',
683
+ 'crossRuntime', 'ambiguity',
684
+ ].some((risk) => riskSet.has(risk))) selected.add('needsResearchGrounding');
685
+ return [...PROOF_OBLIGATIONS, 'needsResearchGrounding'].filter((obligation) => selected.has(obligation));
686
+ }
687
+
620
688
  function actorNeedsFor(obligations) {
621
689
  return obligations.map((obligation) => ({ ...ACTOR_NEEDS_BY_OBLIGATION[obligation] }));
622
690
  }
623
691
 
624
- function minimumLaneForRisks(risks = []) {
625
- const listedRisks = Array.isArray(risks) ? risks : [];
626
- const classifiedRisks = classifyRiskSignals({ risks: listedRisks });
627
- if (Object.entries(classifiedRisks).some(([risk, active]) => active && CRITICAL_RISKS.has(risk))) {
628
- return 'Critical';
629
- }
630
- if (listedRisks.length > 0) return 'Standard';
631
- return 'Direct';
692
+ function mergeProofObligations(...groups) {
693
+ const selected = new Set(groups.flat());
694
+ return READ_COMPAT_PROOF_OBLIGATIONS.filter((obligation) => selected.has(obligation));
632
695
  }
633
696
 
634
697
  function snapshotFromClassification(classification) {
635
- const lane = assertLane(classification.lane);
636
- const automaticLane = assertLane(classification.automaticLane || lane);
637
- if (LANES.indexOf(lane) < LANES.indexOf(automaticLane)) {
638
- throw new Error(`Workflow policy cannot downgrade ${automaticLane} to ${lane} without a trusted override receipt`);
698
+ const planningDepth = assertPlanningDepth(classification.planningDepth);
699
+ const automaticPlanningDepth = assertPlanningDepth(classification.automaticPlanningDepth || planningDepth);
700
+ const assuranceLevel = assertAssuranceLevel(classification.assuranceLevel);
701
+ const automaticAssuranceLevel = assertAssuranceLevel(classification.automaticAssuranceLevel || assuranceLevel);
702
+ if (PLANNING_DEPTHS.indexOf(planningDepth) < PLANNING_DEPTHS.indexOf(automaticPlanningDepth)) {
703
+ throw new Error(`Workflow policy cannot downgrade planning_depth ${automaticPlanningDepth} to ${planningDepth}`);
704
+ }
705
+ if (ASSURANCE_LEVELS.indexOf(assuranceLevel) < ASSURANCE_LEVELS.indexOf(automaticAssuranceLevel)) {
706
+ throw new Error(`Workflow policy cannot downgrade assurance_level ${automaticAssuranceLevel} to ${assuranceLevel}`);
639
707
  }
640
708
  const risks = Array.isArray(classification.risks) ? [...new Set(classification.risks)] : [];
641
- const minimumLane = minimumLaneForRisks(risks);
642
- if (LANES.indexOf(lane) < LANES.indexOf(minimumLane)) {
643
- throw new Error(`Workflow policy lane ${lane} is below ${minimumLane} for risks: ${risks.join(', ')}`);
709
+ const minimumAssurance = minimumAssuranceForRisks(risks);
710
+ if (ASSURANCE_LEVELS.indexOf(assuranceLevel) < ASSURANCE_LEVELS.indexOf(minimumAssurance)) {
711
+ throw new Error(`Workflow policy assurance_level ${assuranceLevel} is below ${minimumAssurance} for risks: ${risks.join(', ')}`);
644
712
  }
713
+ const lane = compatibilityLane(planningDepth, assuranceLevel);
714
+ const automaticLane = compatibilityLane(automaticPlanningDepth, automaticAssuranceLevel);
715
+ const proofObligations = obligationsForAssurance(assuranceLevel, risks);
645
716
  return {
646
717
  version: WORKFLOW_POLICY_VERSION,
718
+ planning_depth: planningDepth,
719
+ automatic_planning_depth: automaticPlanningDepth,
720
+ assurance_level: assuranceLevel,
721
+ automatic_assurance_level: automaticAssuranceLevel,
647
722
  lane,
648
723
  automatic_lane: automaticLane,
649
724
  risks,
650
- artifact_profile: ARTIFACT_PROFILE_BY_LANE[lane],
651
- proof_obligations: obligationsForLane(lane, risks),
652
- actor_needs: actorNeedsFor(obligationsForLane(lane, risks)),
725
+ artifact_profile: ARTIFACT_PROFILE_BY_PLANNING[planningDepth],
726
+ planning_obligations: planningObligationsFor(planningDepth),
727
+ proof_obligations: proofObligations,
728
+ actor_needs: actorNeedsFor(proofObligations),
653
729
  override_receipt: null,
654
730
  };
655
731
  }
656
732
 
733
+ function canonicalSnapshotFromAxes({
734
+ planningDepth,
735
+ assuranceLevel,
736
+ automaticPlanningDepth,
737
+ automaticAssuranceLevel,
738
+ risks = [],
739
+ }) {
740
+ assertPlanningDepth(planningDepth);
741
+ assertAssuranceLevel(assuranceLevel);
742
+ assertPlanningDepth(automaticPlanningDepth);
743
+ assertAssuranceLevel(automaticAssuranceLevel);
744
+ if (PLANNING_DEPTHS.indexOf(planningDepth) < PLANNING_DEPTHS.indexOf(automaticPlanningDepth)) {
745
+ throw new Error(`Workflow policy cannot downgrade planning_depth ${automaticPlanningDepth} to ${planningDepth}`);
746
+ }
747
+ if (ASSURANCE_LEVELS.indexOf(assuranceLevel) < ASSURANCE_LEVELS.indexOf(automaticAssuranceLevel)) {
748
+ throw new Error(`Workflow policy cannot downgrade assurance_level ${automaticAssuranceLevel} to ${assuranceLevel}`);
749
+ }
750
+ const uniqueRisks = [...new Set(risks)];
751
+ const minimumForRisks = minimumAssuranceForRisks(uniqueRisks);
752
+ if (ASSURANCE_LEVELS.indexOf(assuranceLevel) < ASSURANCE_LEVELS.indexOf(minimumForRisks)) {
753
+ throw new Error(`Workflow policy assurance_level ${assuranceLevel} is below ${minimumForRisks} for risks: ${uniqueRisks.join(', ')}`);
754
+ }
755
+ return {
756
+ version: CANONICAL_WORKFLOW_POLICY_VERSION,
757
+ planning_depth: planningDepth,
758
+ assurance_level: assuranceLevel,
759
+ classified_minimum: {
760
+ planning_depth: automaticPlanningDepth,
761
+ assurance_level: automaticAssuranceLevel,
762
+ },
763
+ risks: uniqueRisks,
764
+ };
765
+ }
766
+
767
+ function canonicalWorkflowPolicySnapshot(input = {}) {
768
+ if (input && typeof input === 'object' && hasOwn(input, 'workflow_policy')) {
769
+ const view = readWorkflowPolicySnapshot(input);
770
+ return canonicalSnapshotFromAxes({
771
+ planningDepth: view.planning_depth,
772
+ assuranceLevel: view.assurance_level,
773
+ automaticPlanningDepth: view.automatic_planning_depth,
774
+ automaticAssuranceLevel: view.automatic_assurance_level,
775
+ risks: view.risks,
776
+ });
777
+ }
778
+ const classification = trustedClassifications.has(input) ? input : classifyLane(input);
779
+ return canonicalSnapshotFromAxes({
780
+ planningDepth: classification.planningDepth,
781
+ assuranceLevel: classification.assuranceLevel,
782
+ automaticPlanningDepth: classification.automaticPlanningDepth,
783
+ automaticAssuranceLevel: classification.automaticAssuranceLevel,
784
+ risks: classification.risks,
785
+ });
786
+ }
787
+
788
+ function isCanonicalTasklessTerminalSpec(spec, specDir) {
789
+ if (!spec || typeof spec !== 'object' || Array.isArray(spec) || spec.schema_version !== '2.1') return false;
790
+ const policy = spec.workflow_policy;
791
+ const validation = validateWorkflowPolicySnapshot(policy);
792
+ if (!validation.valid || policy.version !== CANONICAL_WORKFLOW_POLICY_VERSION
793
+ || !['Compact', 'Full'].includes(policy.planning_depth)) return false;
794
+ if (Object.prototype.hasOwnProperty.call(spec, 'task_files')
795
+ && (!Array.isArray(spec.task_files) || spec.task_files.length !== 0)) return false;
796
+ if (Object.prototype.hasOwnProperty.call(spec, 'task_registry')
797
+ && (!spec.task_registry || typeof spec.task_registry !== 'object'
798
+ || Array.isArray(spec.task_registry) || Object.keys(spec.task_registry).length !== 0)) return false;
799
+ if (typeof specDir !== 'string' || specDir.trim() === '') return false;
800
+ try {
801
+ const entries = fs.readdirSync(path.join(specDir, 'tasks'));
802
+ return entries.length === 0;
803
+ } catch (error) {
804
+ return error && error.code === 'ENOENT';
805
+ }
806
+ }
807
+
808
+ function canonicalPolicyView(snapshot) {
809
+ const planningDepth = snapshot.planning_depth;
810
+ const assuranceLevel = snapshot.assurance_level;
811
+ const automaticPlanningDepth = snapshot.classified_minimum.planning_depth;
812
+ const automaticAssuranceLevel = snapshot.classified_minimum.assurance_level;
813
+ const proofObligations = obligationsForAssurance(assuranceLevel, snapshot.risks);
814
+ return {
815
+ ...JSON.parse(JSON.stringify(snapshot)),
816
+ automatic_planning_depth: automaticPlanningDepth,
817
+ automatic_assurance_level: automaticAssuranceLevel,
818
+ lane: compatibilityLane(planningDepth, assuranceLevel),
819
+ automatic_lane: compatibilityLane(automaticPlanningDepth, automaticAssuranceLevel),
820
+ artifact_profile: ARTIFACT_PROFILE_BY_PLANNING[planningDepth],
821
+ planning_obligations: planningObligationsFor(planningDepth),
822
+ proof_obligations: proofObligations,
823
+ actor_needs: actorNeedsFor(proofObligations),
824
+ };
825
+ }
826
+
657
827
  function workflowPolicySnapshot(input = {}) {
658
828
  if (input && typeof input === 'object' && hasOwn(input, 'workflow_policy')) {
659
829
  return readWorkflowPolicySnapshot(input);
@@ -663,7 +833,8 @@ function workflowPolicySnapshot(input = {}) {
663
833
  }
664
834
  if (input && typeof input === 'object' && isWorkflowPolicySnapshot(input)) {
665
835
  assertWorkflowPolicySnapshot(input);
666
- return JSON.parse(JSON.stringify(input));
836
+ if (input.version === WORKFLOW_POLICY_VERSION) return clonePolicySnapshot(input);
837
+ return readWorkflowPolicySnapshot({ workflow_policy: input });
667
838
  }
668
839
  const classification = trustedClassifications.has(input)
669
840
  ? input
@@ -672,9 +843,37 @@ function workflowPolicySnapshot(input = {}) {
672
843
  }
673
844
 
674
845
  function isWorkflowPolicySnapshot(value) {
675
- return Boolean(value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'version')
676
- && Object.prototype.hasOwnProperty.call(value, 'automatic_lane')
677
- && Object.prototype.hasOwnProperty.call(value, 'artifact_profile'));
846
+ return Boolean(value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'version'));
847
+ }
848
+
849
+ function validateV1Snapshot(snapshot) {
850
+ const errors = [];
851
+ if (JSON.stringify(Object.keys(snapshot).sort()) !== JSON.stringify([...V1_POLICY_FIELDS].sort())) {
852
+ errors.push(`workflow_policy v1 fields must be exactly ${V1_POLICY_FIELDS.join(', ')}`);
853
+ }
854
+ if (!LANES.includes(snapshot.lane)) errors.push(`workflow_policy.lane must be one of ${LANES.join(', ')}`);
855
+ if (!LANES.includes(snapshot.automatic_lane)) errors.push(`workflow_policy.automatic_lane must be one of ${LANES.join(', ')}`);
856
+ if (LANES.includes(snapshot.lane) && LANES.includes(snapshot.automatic_lane)
857
+ && LANES.indexOf(snapshot.lane) < LANES.indexOf(snapshot.automatic_lane)) errors.push('workflow_policy.lane cannot be lower than automatic_lane');
858
+ const risks = uniqueStrings(snapshot.risks, 'workflow_policy.risks');
859
+ if (!risks.valid) errors.push(risks.error);
860
+ if (risks.valid && LANES.includes(snapshot.lane)) {
861
+ const minimum = compatibilityLane('None', minimumAssuranceForRisks(snapshot.risks));
862
+ if (LANES.indexOf(snapshot.lane) < LANES.indexOf(minimum)) errors.push(`workflow_policy.lane must be at least ${minimum} for its risks`);
863
+ }
864
+ if (LANES.includes(snapshot.lane) && snapshot.artifact_profile !== ARTIFACT_PROFILE_BY_LANE[snapshot.lane]) {
865
+ errors.push(`workflow_policy.artifact_profile must be ${ARTIFACT_PROFILE_BY_LANE[snapshot.lane]} for ${snapshot.lane}`);
866
+ }
867
+ const obligations = uniqueStrings(snapshot.proof_obligations, 'workflow_policy.proof_obligations');
868
+ if (!obligations.valid) errors.push(obligations.error);
869
+ else if (LANES.includes(snapshot.lane) && risks.valid) {
870
+ const expected = legacyObligationsForLane(snapshot.lane, snapshot.risks);
871
+ if (JSON.stringify(snapshot.proof_obligations) !== JSON.stringify(expected)) errors.push('workflow_policy.proof_obligations do not match v1 lane and risks');
872
+ }
873
+ if (!Array.isArray(snapshot.actor_needs)) errors.push('workflow_policy.actor_needs must be an array');
874
+ else if (obligations.valid && JSON.stringify(snapshot.actor_needs) !== JSON.stringify(actorNeedsFor(snapshot.proof_obligations))) errors.push('workflow_policy.actor_needs must correspond to proof_obligations');
875
+ if (snapshot.override_receipt !== null) errors.push('workflow_policy.override_receipt must be null');
876
+ return { valid: errors.length === 0, errors };
678
877
  }
679
878
 
680
879
  function validateWorkflowPolicySnapshot(snapshot) {
@@ -682,43 +881,98 @@ function validateWorkflowPolicySnapshot(snapshot) {
682
881
  if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
683
882
  return { valid: false, errors: ['workflow_policy must be an object'] };
684
883
  }
884
+ if (snapshot.version === CANONICAL_WORKFLOW_POLICY_VERSION) {
885
+ const keys = Object.keys(snapshot).sort();
886
+ if (JSON.stringify(keys) !== JSON.stringify([...CANONICAL_WORKFLOW_POLICY_FIELDS].sort())) {
887
+ errors.push(`workflow_policy v2.1 fields must be exactly ${CANONICAL_WORKFLOW_POLICY_FIELDS.join(', ')}`);
888
+ }
889
+ if (!PLANNING_DEPTHS.includes(snapshot.planning_depth)) {
890
+ errors.push(`workflow_policy.planning_depth must be one of ${PLANNING_DEPTHS.join(', ')}`);
891
+ }
892
+ if (!ASSURANCE_LEVELS.includes(snapshot.assurance_level)) {
893
+ errors.push(`workflow_policy.assurance_level must be one of ${ASSURANCE_LEVELS.join(', ')}`);
894
+ }
895
+ const minimum = snapshot.classified_minimum;
896
+ if (!minimum || typeof minimum !== 'object' || Array.isArray(minimum)
897
+ || Object.keys(minimum).sort().join(',') !== 'assurance_level,planning_depth') {
898
+ errors.push('workflow_policy.classified_minimum must contain exactly planning_depth and assurance_level');
899
+ } else {
900
+ if (!PLANNING_DEPTHS.includes(minimum.planning_depth)) {
901
+ errors.push(`workflow_policy.classified_minimum.planning_depth must be one of ${PLANNING_DEPTHS.join(', ')}`);
902
+ }
903
+ if (!ASSURANCE_LEVELS.includes(minimum.assurance_level)) {
904
+ errors.push(`workflow_policy.classified_minimum.assurance_level must be one of ${ASSURANCE_LEVELS.join(', ')}`);
905
+ }
906
+ if (PLANNING_DEPTHS.includes(snapshot.planning_depth) && PLANNING_DEPTHS.includes(minimum.planning_depth)
907
+ && PLANNING_DEPTHS.indexOf(snapshot.planning_depth) < PLANNING_DEPTHS.indexOf(minimum.planning_depth)) {
908
+ errors.push('workflow_policy.planning_depth cannot be lower than classified_minimum.planning_depth');
909
+ }
910
+ if (ASSURANCE_LEVELS.includes(snapshot.assurance_level) && ASSURANCE_LEVELS.includes(minimum.assurance_level)
911
+ && ASSURANCE_LEVELS.indexOf(snapshot.assurance_level) < ASSURANCE_LEVELS.indexOf(minimum.assurance_level)) {
912
+ errors.push('workflow_policy.assurance_level cannot be lower than classified_minimum.assurance_level');
913
+ }
914
+ }
915
+ const risks = uniqueStrings(snapshot.risks, 'workflow_policy.risks');
916
+ if (!risks.valid) errors.push(risks.error);
917
+ if (risks.valid && ASSURANCE_LEVELS.includes(snapshot.assurance_level)) {
918
+ const riskMinimum = minimumAssuranceForRisks(snapshot.risks);
919
+ if (ASSURANCE_LEVELS.indexOf(snapshot.assurance_level) < ASSURANCE_LEVELS.indexOf(riskMinimum)) {
920
+ errors.push(`workflow_policy.assurance_level must be at least ${riskMinimum} for its risks`);
921
+ }
922
+ }
923
+ return { valid: errors.length === 0, errors };
924
+ }
925
+ if (snapshot.version === '1') return validateV1Snapshot(snapshot);
685
926
  const keys = Object.keys(snapshot).sort();
686
927
  const expectedKeys = [...WORKFLOW_POLICY_FIELDS].sort();
687
928
  if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)) {
688
929
  errors.push(`workflow_policy fields must be exactly ${WORKFLOW_POLICY_FIELDS.join(', ')}`);
689
930
  }
690
- if (snapshot.version !== WORKFLOW_POLICY_VERSION) errors.push('workflow_policy.version must be "1"');
931
+ if (snapshot.version !== WORKFLOW_POLICY_VERSION) errors.push('workflow_policy.version must be "2"');
932
+ if (!PLANNING_DEPTHS.includes(snapshot.planning_depth)) errors.push(`workflow_policy.planning_depth must be one of ${PLANNING_DEPTHS.join(', ')}`);
933
+ if (!PLANNING_DEPTHS.includes(snapshot.automatic_planning_depth)) errors.push(`workflow_policy.automatic_planning_depth must be one of ${PLANNING_DEPTHS.join(', ')}`);
934
+ if (!ASSURANCE_LEVELS.includes(snapshot.assurance_level)) errors.push(`workflow_policy.assurance_level must be one of ${ASSURANCE_LEVELS.join(', ')}`);
935
+ if (!ASSURANCE_LEVELS.includes(snapshot.automatic_assurance_level)) errors.push(`workflow_policy.automatic_assurance_level must be one of ${ASSURANCE_LEVELS.join(', ')}`);
691
936
  if (!LANES.includes(snapshot.lane)) errors.push(`workflow_policy.lane must be one of ${LANES.join(', ')}`);
692
937
  if (!LANES.includes(snapshot.automatic_lane)) errors.push(`workflow_policy.automatic_lane must be one of ${LANES.join(', ')}`);
693
- if (LANES.includes(snapshot.lane) && LANES.includes(snapshot.automatic_lane)
694
- && LANES.indexOf(snapshot.lane) < LANES.indexOf(snapshot.automatic_lane)) {
695
- errors.push('workflow_policy.lane cannot be lower than automatic_lane without a trusted override receipt');
696
- }
697
- if (LANES.includes(snapshot.lane) && snapshot.artifact_profile !== ARTIFACT_PROFILE_BY_LANE[snapshot.lane]) {
698
- errors.push(`workflow_policy.artifact_profile must be ${ARTIFACT_PROFILE_BY_LANE[snapshot.lane]} for ${snapshot.lane}`);
699
- }
938
+ if (PLANNING_DEPTHS.includes(snapshot.planning_depth) && PLANNING_DEPTHS.includes(snapshot.automatic_planning_depth)
939
+ && PLANNING_DEPTHS.indexOf(snapshot.planning_depth) < PLANNING_DEPTHS.indexOf(snapshot.automatic_planning_depth)) errors.push('workflow_policy.planning_depth cannot be lower than automatic_planning_depth');
940
+ if (ASSURANCE_LEVELS.includes(snapshot.assurance_level) && ASSURANCE_LEVELS.includes(snapshot.automatic_assurance_level)
941
+ && ASSURANCE_LEVELS.indexOf(snapshot.assurance_level) < ASSURANCE_LEVELS.indexOf(snapshot.automatic_assurance_level)) errors.push('workflow_policy.assurance_level cannot be lower than automatic_assurance_level');
942
+ if (PLANNING_DEPTHS.includes(snapshot.planning_depth) && ASSURANCE_LEVELS.includes(snapshot.assurance_level)
943
+ && snapshot.lane !== compatibilityLane(snapshot.planning_depth, snapshot.assurance_level)) errors.push('workflow_policy.lane is not the derived compatibility lane');
944
+ if (PLANNING_DEPTHS.includes(snapshot.automatic_planning_depth) && ASSURANCE_LEVELS.includes(snapshot.automatic_assurance_level)
945
+ && snapshot.automatic_lane !== compatibilityLane(snapshot.automatic_planning_depth, snapshot.automatic_assurance_level)) errors.push('workflow_policy.automatic_lane is not the derived compatibility lane');
946
+ if (PLANNING_DEPTHS.includes(snapshot.planning_depth) && snapshot.artifact_profile !== ARTIFACT_PROFILE_BY_PLANNING[snapshot.planning_depth]) errors.push('workflow_policy.artifact_profile does not match planning_depth');
700
947
  const risks = uniqueStrings(snapshot.risks, 'workflow_policy.risks');
701
948
  if (!risks.valid) errors.push(risks.error);
702
- if (risks.valid && LANES.includes(snapshot.lane)) {
703
- const minimumLane = minimumLaneForRisks(snapshot.risks);
704
- if (LANES.indexOf(snapshot.lane) < LANES.indexOf(minimumLane)) {
705
- errors.push(`workflow_policy.lane must be at least ${minimumLane} for its risks`);
949
+ if (risks.valid && ASSURANCE_LEVELS.includes(snapshot.assurance_level)) {
950
+ const minimumAssurance = minimumAssuranceForRisks(snapshot.risks);
951
+ if (ASSURANCE_LEVELS.indexOf(snapshot.assurance_level) < ASSURANCE_LEVELS.indexOf(minimumAssurance)) {
952
+ errors.push(`workflow_policy.assurance_level must be at least ${minimumAssurance} for its risks`);
706
953
  }
707
954
  }
955
+ const planning = uniqueStrings(snapshot.planning_obligations, 'workflow_policy.planning_obligations');
956
+ if (!planning.valid) errors.push(planning.error);
957
+ else if (PLANNING_DEPTHS.includes(snapshot.planning_depth)
958
+ && JSON.stringify(snapshot.planning_obligations) !== JSON.stringify(planningObligationsFor(snapshot.planning_depth))) errors.push('workflow_policy.planning_obligations do not match planning_depth');
708
959
  const obligations = uniqueStrings(snapshot.proof_obligations, 'workflow_policy.proof_obligations');
709
960
  if (!obligations.valid) {
710
961
  errors.push(obligations.error);
711
962
  } else {
712
963
  for (const obligation of snapshot.proof_obligations) {
713
- if (!PROOF_OBLIGATIONS.includes(obligation)) errors.push(`workflow_policy.proof_obligations has unknown obligation ${obligation}`);
964
+ if (!READ_COMPAT_PROOF_OBLIGATIONS.includes(obligation)) errors.push(`workflow_policy.proof_obligations has unknown obligation ${obligation}`);
714
965
  }
715
966
  if (!snapshot.proof_obligations.includes('needsExecutionProof')) {
716
967
  errors.push('workflow_policy.proof_obligations must include needsExecutionProof');
717
968
  }
718
- if (LANES.includes(snapshot.lane) && risks.valid) {
719
- const expected = obligationsForLane(snapshot.lane, snapshot.risks);
720
- if (JSON.stringify(snapshot.proof_obligations) !== JSON.stringify(expected)) {
721
- errors.push('workflow_policy.proof_obligations do not match lane and risks');
969
+ if (ASSURANCE_LEVELS.includes(snapshot.assurance_level) && risks.valid) {
970
+ const expected = obligationsForAssurance(snapshot.assurance_level, snapshot.risks);
971
+ const exact = JSON.stringify(snapshot.proof_obligations) === JSON.stringify(expected);
972
+ const trustedCompat = trustedV1Adapters.has(snapshot)
973
+ && JSON.stringify(snapshot.proof_obligations) === JSON.stringify(mergeProofObligations(expected, snapshot.proof_obligations));
974
+ if (!exact && !trustedCompat) {
975
+ errors.push('workflow_policy.proof_obligations do not match assurance_level and risks');
722
976
  }
723
977
  }
724
978
  }
@@ -734,14 +988,12 @@ function validateWorkflowPolicySnapshot(snapshot) {
734
988
  break;
735
989
  }
736
990
  }
737
- if (LANES.includes(snapshot.lane) && obligations.valid
991
+ if (ASSURANCE_LEVELS.includes(snapshot.assurance_level) && obligations.valid
738
992
  && JSON.stringify(snapshot.actor_needs) !== JSON.stringify(actorNeedsFor(snapshot.proof_obligations))) {
739
993
  errors.push('workflow_policy.actor_needs must correspond to proof_obligations');
740
994
  }
741
995
  }
742
- if (snapshot.override_receipt !== null) {
743
- errors.push('workflow_policy.override_receipt must be null until a trusted downgrade issuer exists');
744
- }
996
+ if (snapshot.override_receipt !== null) errors.push('legacy workflow_policy.override_receipt is inert and must be null');
745
997
  return { valid: errors.length === 0, errors };
746
998
  }
747
999
 
@@ -754,8 +1006,36 @@ function assertWorkflowPolicySnapshot(snapshot) {
754
1006
  function readWorkflowPolicySnapshot(spec = {}) {
755
1007
  if (!spec || typeof spec !== 'object') throw new TypeError('Spec state must be an object');
756
1008
  if (hasOwn(spec, 'workflow_policy')) {
757
- assertWorkflowPolicySnapshot(spec.workflow_policy);
758
- return JSON.parse(JSON.stringify(spec.workflow_policy));
1009
+ let persisted = spec.workflow_policy;
1010
+ if (persisted?.version === CANONICAL_WORKFLOW_POLICY_VERSION
1011
+ && hasOwn(persisted, 'override_receipt')) {
1012
+ if (persisted.override_receipt !== null) {
1013
+ throw new TypeError('Invalid workflow policy snapshot: legacy workflow_policy.override_receipt is inert and must be null');
1014
+ }
1015
+ persisted = { ...persisted };
1016
+ delete persisted.override_receipt;
1017
+ }
1018
+ assertWorkflowPolicySnapshot(persisted);
1019
+ if (persisted.version === CANONICAL_WORKFLOW_POLICY_VERSION) {
1020
+ return canonicalPolicyView(persisted);
1021
+ }
1022
+ if (persisted.version === WORKFLOW_POLICY_VERSION) return clonePolicySnapshot(persisted);
1023
+ const selected = v1Axes(persisted.lane);
1024
+ const automatic = v1Axes(persisted.automatic_lane);
1025
+ const adapted = snapshotFromClassification({
1026
+ ...selected,
1027
+ automaticPlanningDepth: automatic.planningDepth,
1028
+ automaticAssuranceLevel: automatic.assuranceLevel,
1029
+ risks: persisted.risks,
1030
+ });
1031
+ const proofObligations = mergeProofObligations(adapted.proof_obligations, persisted.proof_obligations);
1032
+ const result = {
1033
+ ...adapted,
1034
+ proof_obligations: proofObligations,
1035
+ actor_needs: actorNeedsFor(proofObligations),
1036
+ };
1037
+ trustedV1Adapters.add(result);
1038
+ return result;
759
1039
  }
760
1040
  // Legacy state is read-compatible only. The fallback is deliberately not
761
1041
  // written back and cannot authorize a downgrade or override new input.
@@ -764,7 +1044,10 @@ function readWorkflowPolicySnapshot(spec = {}) {
764
1044
  ? Object.keys(LEGACY_TIER_LANES).find((tier) => tier.toLowerCase() === rawLegacyTier.toLowerCase())
765
1045
  : null;
766
1046
  const legacyLane = legacyTier ? LEGACY_TIER_LANES[legacyTier] : null;
767
- if (legacyLane) return snapshotFromClassification({ lane: legacyLane, automaticLane: legacyLane, risks: [] });
1047
+ if (legacyLane) {
1048
+ const axes = v1Axes(legacyLane);
1049
+ return snapshotFromClassification({ ...axes, automaticPlanningDepth: axes.planningDepth, automaticAssuranceLevel: axes.assuranceLevel, risks: [] });
1050
+ }
768
1051
  throw new TypeError('workflow_policy snapshot is missing at the spec boundary');
769
1052
  }
770
1053
 
@@ -773,29 +1056,32 @@ function persistWorkflowPolicySnapshot(spec, input = {}) {
773
1056
  const next = { ...spec };
774
1057
  delete next.override_receipt;
775
1058
  if (hasOwn(spec, 'workflow_policy')) {
776
- assertWorkflowPolicySnapshot(spec.workflow_policy);
777
- return { ...next, workflow_policy: JSON.parse(JSON.stringify(spec.workflow_policy)) };
1059
+ return { ...next, workflow_policy: canonicalWorkflowPolicySnapshot(spec) };
778
1060
  }
779
- return { ...next, workflow_policy: workflowPolicySnapshot(input) };
1061
+ return { ...next, workflow_policy: canonicalWorkflowPolicySnapshot(input) };
780
1062
  }
781
1063
 
782
1064
  const persistWorkflowPolicy = persistWorkflowPolicySnapshot;
783
1065
  const createWorkflowPolicySnapshot = workflowPolicySnapshot;
784
1066
 
785
- function candidateLaneForRisks(input, risks) {
786
- const explicitLevel = String(input?.riskLevel || input?.risk_level || input?.level || '').toLowerCase();
787
- const explicitLane = ['critical', 'high', 'deep'].includes(explicitLevel)
788
- ? 'Critical'
789
- : ['standard', 'medium'].includes(explicitLevel)
790
- ? 'Standard'
791
- : null;
792
- const riskLane = minimumLaneForRisks(risks);
793
- if (explicitLane === null && riskLane === 'Direct') return null;
794
- if (explicitLane === null) return riskLane;
795
- return LANES[Math.max(LANES.indexOf(explicitLane), LANES.indexOf(riskLane))];
1067
+ function candidateAssuranceForRisks(input, risks) {
1068
+ // A risk severity label is evidence for inspection, not independent-audit
1069
+ // authority. Strict is selected only through the explicit assurance axis.
1070
+ const explicitRiskLevel = String(input?.riskLevel || input?.risk_level || input?.level || '').toLowerCase();
1071
+ const riskLevelAssurance = ['standard', 'medium', 'high', 'critical', 'deep'].includes(explicitRiskLevel)
1072
+ ? 'Elevated'
1073
+ : null;
1074
+ const riskLevel = minimumAssuranceForRisks(risks);
1075
+ const candidates = [riskLevelAssurance, riskLevel]
1076
+ .filter((level) => level !== null && level !== undefined);
1077
+ if (candidates.length === 0 || candidates.every((level) => level === 'Routine')) return null;
1078
+ return ASSURANCE_LEVELS[Math.max(...candidates.map((level) => ASSURANCE_LEVELS.indexOf(level)))];
796
1079
  }
797
1080
 
798
1081
  function escalateWorkflowPolicy(policyOrSpec, riskInput = {}) {
1082
+ const canonicalInput = policyOrSpec && typeof policyOrSpec === 'object'
1083
+ && (policyOrSpec.version === CANONICAL_WORKFLOW_POLICY_VERSION
1084
+ || policyOrSpec.workflow_policy?.version === CANONICAL_WORKFLOW_POLICY_VERSION);
799
1085
  const current = policyOrSpec && typeof policyOrSpec === 'object' && hasOwn(policyOrSpec, 'workflow_policy')
800
1086
  ? readWorkflowPolicySnapshot(policyOrSpec)
801
1087
  : workflowPolicySnapshot(policyOrSpec);
@@ -808,15 +1094,36 @@ function escalateWorkflowPolicy(policyOrSpec, riskInput = {}) {
808
1094
  ...explicitRisks,
809
1095
  ])];
810
1096
  const risks = [...new Set([...current.risks, ...newRisks])];
811
- if (newRisks.every((risk) => current.risks.includes(risk))) return current;
812
- const candidate = candidateLaneForRisks(riskInput, newRisks);
813
- if (!candidate) return current;
814
- const lane = LANES[Math.max(LANES.indexOf(current.lane), LANES.indexOf(candidate))];
815
- return snapshotFromClassification({
816
- lane,
817
- automaticLane: current.automatic_lane,
1097
+ const requestedAssurance = riskInput?.assurance_level ?? riskInput?.assuranceLevel;
1098
+ if (requestedAssurance !== undefined && requestedAssurance !== null) {
1099
+ assertAssuranceLevel(requestedAssurance);
1100
+ }
1101
+ const hasNewRisk = newRisks.some((risk) => !current.risks.includes(risk));
1102
+ const hasRiskLevel = ['standard', 'medium', 'high', 'critical', 'deep'].includes(
1103
+ String(riskInput?.riskLevel || riskInput?.risk_level || riskInput?.level || '').toLowerCase(),
1104
+ );
1105
+ const raisesRequestedAssurance = requestedAssurance
1106
+ && ASSURANCE_LEVELS.indexOf(requestedAssurance) > ASSURANCE_LEVELS.indexOf(current.assurance_level);
1107
+ if (!hasNewRisk && !hasRiskLevel && !raisesRequestedAssurance) return current;
1108
+ const candidate = candidateAssuranceForRisks(riskInput, risks);
1109
+ if (!candidate && !raisesRequestedAssurance) return current;
1110
+ const automaticAssuranceLevel = ASSURANCE_LEVELS[Math.max(
1111
+ ASSURANCE_LEVELS.indexOf(current.automatic_assurance_level),
1112
+ ASSURANCE_LEVELS.indexOf(candidate || 'Routine'),
1113
+ )];
1114
+ const assuranceLevel = ASSURANCE_LEVELS[Math.max(
1115
+ ASSURANCE_LEVELS.indexOf(current.assurance_level),
1116
+ ASSURANCE_LEVELS.indexOf(automaticAssuranceLevel),
1117
+ ASSURANCE_LEVELS.indexOf(requestedAssurance || 'Routine'),
1118
+ )];
1119
+ const axes = {
1120
+ planningDepth: current.planning_depth,
1121
+ automaticPlanningDepth: current.automatic_planning_depth,
1122
+ assuranceLevel,
1123
+ automaticAssuranceLevel,
818
1124
  risks,
819
- });
1125
+ };
1126
+ return canonicalInput ? canonicalSnapshotFromAxes(axes) : snapshotFromClassification(axes);
820
1127
  }
821
1128
 
822
1129
  function policyForSpec(spec, riskInput = null) {
@@ -828,15 +1135,17 @@ function lanePolicy(input = {}) {
828
1135
  const snapshot = input && typeof input === 'object' && hasOwn(input, 'workflow_policy')
829
1136
  ? readWorkflowPolicySnapshot(input)
830
1137
  : input && typeof input === 'object' && isWorkflowPolicySnapshot(input)
831
- ? (assertWorkflowPolicySnapshot(input), JSON.parse(JSON.stringify(input)))
1138
+ ? (assertWorkflowPolicySnapshot(input), clonePolicySnapshot(input))
832
1139
  : workflowPolicySnapshot(input);
833
1140
  const lane = assertLane(snapshot.lane);
834
1141
  return {
835
1142
  ...snapshot,
836
1143
  proof_obligations: [...snapshot.proof_obligations],
837
1144
  actor_needs: snapshot.actor_needs.map((need) => ({ ...need })),
838
- requiresSpec: lane !== 'Direct',
839
- requiresState: lane === 'Critical',
1145
+ planning_obligations: [...snapshot.planning_obligations],
1146
+ requiresSpec: snapshot.planning_depth !== 'None',
1147
+ requiresState: snapshot.planning_depth === 'Full'
1148
+ || snapshot.proof_obligations.includes('needsDurableTaskState'),
840
1149
  qualityGate: lane === 'Direct' ? 'main-session' : lane === 'Standard' ? 'combined-feature-review' : 'strict-evidence',
841
1150
  shipPoint: lane === 'Direct' ? 'task' : 'feature',
842
1151
  evidence: snapshot.artifact_profile,
@@ -873,8 +1182,8 @@ function validateApprovalSchema(spec) {
873
1182
  return { valid: false, absent: false, legacy: false, error: `approvals.${stage} must be an approval object` };
874
1183
  }
875
1184
  const keys = Object.keys(val).sort();
876
- if (keys.join(',') !== 'agent_validated,generated,user_approved') {
877
- return { valid: false, absent: false, legacy: false, error: `approvals.${stage} must contain exactly generated, agent_validated, and user_approved` };
1185
+ if (!['agent_validated,generated', 'agent_validated,generated,user_approved'].includes(keys.join(','))) {
1186
+ return { valid: false, absent: false, legacy: false, error: `approvals.${stage} must contain generated and agent_validated; legacy user_approved is readable but has zero authority` };
878
1187
  }
879
1188
  if (keys.some((key) => typeof val[key] !== 'boolean')) {
880
1189
  return { valid: false, absent: false, legacy: false, error: `approvals.${stage} fields must be boolean` };
@@ -908,7 +1217,6 @@ function approvalState(state = {}) {
908
1217
  return {
909
1218
  generated: false,
910
1219
  agent_validated: false,
911
- user_approved: false,
912
1220
  ready: false,
913
1221
  present: true,
914
1222
  schema_version: APPROVAL_SCHEMA_VERSION,
@@ -936,13 +1244,13 @@ function approvalState(state = {}) {
936
1244
  const result = {
937
1245
  generated: state.generated === true,
938
1246
  agent_validated: state.agent_validated === true,
939
- user_approved: state.user_approved === true,
940
1247
  };
941
1248
  return {
942
1249
  ...result,
943
- ready: result.generated && result.agent_validated && result.user_approved,
1250
+ ready: result.generated && result.agent_validated,
944
1251
  present,
945
1252
  schema_version: APPROVAL_SCHEMA_VERSION,
1253
+ legacy_user_approved: state.user_approved === true,
946
1254
  };
947
1255
  }
948
1256
  function assertVerdict(verdict) {
@@ -1073,7 +1381,7 @@ function workflowPolicyObligations(context) {
1073
1381
  : null)
1074
1382
  : null;
1075
1383
  if (!validateWorkflowPolicySnapshot(policy).valid) return null;
1076
- return [...policy.proof_obligations];
1384
+ return [...readWorkflowPolicySnapshot({ workflow_policy: policy }).proof_obligations];
1077
1385
  }
1078
1386
 
1079
1387
  function executionReceiptFromContext(context) {
@@ -1271,6 +1579,7 @@ function completionDecisionForSpec(spec, {
1271
1579
  taskContext,
1272
1580
  } = {}) {
1273
1581
  const policy = spec && typeof spec === 'object' ? spec.workflow_policy : null;
1582
+ const policyView = policy ? readWorkflowPolicySnapshot({ workflow_policy: policy }) : null;
1274
1583
  const context = {
1275
1584
  workflow_policy: policy,
1276
1585
  runtime_context: runtimeContext,
@@ -1279,7 +1588,7 @@ function completionDecisionForSpec(spec, {
1279
1588
  };
1280
1589
  const trustedProofs = runtimeBoundProofsFromSpec(spec, runtimeContext);
1281
1590
  if (trustedProofs) context.proofs = trustedProofs;
1282
- if (policy && policy.lane === 'Direct') {
1591
+ if (policyView && policyView.lane === 'Direct') {
1283
1592
  return {
1284
1593
  completion: 'not_applicable',
1285
1594
  unfinished: false,
@@ -1857,8 +2166,9 @@ function runCli(argv = process.argv.slice(2)) {
1857
2166
  }
1858
2167
  if (options.action === 'policy-snapshot') {
1859
2168
  if (!options.task) throw new Error('--policy-snapshot requires --task-json');
1860
- const snapshot = workflowPolicySnapshot(options.task);
1861
- return cliResult({ ok: true, snapshot, exitCode: 0, message: `Policy snapshot: ${snapshot.lane}` }, options.json);
2169
+ const snapshot = canonicalWorkflowPolicySnapshot(options.task);
2170
+ const view = readWorkflowPolicySnapshot({ workflow_policy: snapshot });
2171
+ return cliResult({ ok: true, snapshot, exitCode: 0, message: `Policy snapshot: ${view.lane}` }, options.json);
1862
2172
  }
1863
2173
  if (options.action === 'validate-policy') {
1864
2174
  if (!options.task) throw new Error('--validate-policy requires --task-json');
@@ -1920,10 +2230,15 @@ module.exports = {
1920
2230
  LEGACY_DIAGNOSTIC_VERDICTS,
1921
2231
  EXECUTION_TIERS,
1922
2232
  LANES,
2233
+ PLANNING_DEPTHS,
2234
+ ASSURANCE_LEVELS,
1923
2235
  ARTIFACT_PROFILES,
2236
+ PLANNING_OBLIGATIONS,
1924
2237
  PROOF_OBLIGATIONS,
1925
2238
  WORKFLOW_POLICY_VERSION,
1926
2239
  WORKFLOW_POLICY_FIELDS,
2240
+ CANONICAL_WORKFLOW_POLICY_VERSION,
2241
+ CANONICAL_WORKFLOW_POLICY_FIELDS,
1927
2242
  RECEIPT_BINDING_FIELDS,
1928
2243
  INDEPENDENT_AUDIT_SCHEMA_VERSION,
1929
2244
  INDEPENDENT_AUDIT_FIELDS,
@@ -1931,12 +2246,17 @@ module.exports = {
1931
2246
  LEGACY_APPROVAL_ERROR,
1932
2247
  executionPolicy,
1933
2248
  classifyLane,
2249
+ compatibilityLane,
2250
+ planningObligationsFor,
2251
+ obligationsForAssurance,
1934
2252
  obligationsForLane,
1935
2253
  actorNeedsFor,
1936
2254
  isWorkflowPolicySnapshot,
1937
2255
  validateWorkflowPolicySnapshot,
1938
2256
  assertWorkflowPolicySnapshot,
1939
2257
  workflowPolicySnapshot,
2258
+ canonicalWorkflowPolicySnapshot,
2259
+ isCanonicalTasklessTerminalSpec,
1940
2260
  createWorkflowPolicySnapshot,
1941
2261
  persistWorkflowPolicySnapshot,
1942
2262
  persistWorkflowPolicy,
@@ -1947,8 +2267,6 @@ module.exports = {
1947
2267
  approvalState,
1948
2268
  validateApprovalSchema,
1949
2269
  isLegacyApprovalObject,
1950
- isUserAuthorizedForDowngrade,
1951
- isValidOverrideReceipt,
1952
2270
  isTapMetadataHeading,
1953
2271
  deriveRuntimeContext: PROVENANCE.deriveRuntimeContext,
1954
2272
  recomputeRuntimeContext: PROVENANCE.recomputeRuntimeContext,