@haposoft/cafekit 0.16.0-rc.2 → 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 (98) hide show
  1. package/bin/__tests__/codex-hooks.test.js +794 -21
  2. package/bin/__tests__/codex-native.test.js +228 -7
  3. package/bin/__tests__/develop-contract.test.js +1838 -112
  4. package/bin/__tests__/installer-safety.test.js +82 -29
  5. package/bin/__tests__/opencode-spec-gate.test.js +799 -0
  6. package/bin/__tests__/package-inventory.test.js +841 -10
  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 +2469 -71
  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 +153 -202
  18. package/src/claude/hooks/__tests__/completion-authority.test.js +953 -0
  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 +604 -56
  22. package/src/claude/hooks/completion-authority-check.cjs +56 -0
  23. package/src/claude/hooks/completion-authority-state.cjs +527 -0
  24. package/src/claude/hooks/completion-authority.cjs +154 -0
  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/session.cjs +33 -19
  31. package/src/claude/hooks/spec-gate.cjs +181 -139
  32. package/src/claude/hooks/spec-state.cjs +112 -47
  33. package/src/claude/migration-manifest.json +15 -2
  34. package/src/claude/rules/state-sync.md +52 -21
  35. package/src/claude/rules/workflow.md +17 -9
  36. package/src/claude/scripts/provenance.cjs +315 -0
  37. package/src/claude/scripts/spec-final-state.cjs +244 -0
  38. package/src/claude/scripts/spec-ground.cjs +535 -173
  39. package/src/claude/scripts/spec-readiness.cjs +118 -0
  40. package/src/claude/scripts/spec-receipt.cjs +162 -0
  41. package/src/claude/scripts/spec-resolver.cjs +501 -0
  42. package/src/claude/scripts/spec-scaffold.cjs +1945 -55
  43. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  44. package/src/claude/scripts/validate-spec-output.cjs +3204 -262
  45. package/src/claude/scripts/workflow-policy.cjs +1888 -203
  46. package/src/claude/settings/settings.json +12 -0
  47. package/src/claude/skills/code-review/SKILL.md +94 -66
  48. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  49. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  50. package/src/claude/skills/develop/SKILL.md +138 -283
  51. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  52. package/src/claude/skills/develop/references/quality-gate.md +123 -148
  53. package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
  54. package/src/claude/skills/specs/SKILL.md +192 -397
  55. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  56. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  57. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  58. package/src/claude/skills/specs/references/review.md +200 -260
  59. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  60. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  61. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  62. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  63. package/src/claude/skills/specs/rules/design-review.md +42 -97
  64. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  65. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  66. package/src/claude/skills/specs/rules/task-scoring-rubric.md +46 -46
  67. package/src/claude/skills/specs/rules/tasks-generation.md +189 -271
  68. package/src/claude/skills/specs/templates/design.md +49 -263
  69. package/src/claude/skills/specs/templates/requirements.md +26 -36
  70. package/src/claude/skills/specs/templates/research.md +9 -98
  71. package/src/claude/skills/specs/templates/spec-state.json +26 -67
  72. package/src/claude/skills/specs/templates/task.md +30 -82
  73. package/src/claude/skills/sync/SKILL.md +45 -15
  74. package/src/claude/skills/sync/references/sync-protocols.md +34 -16
  75. package/src/claude/skills/test/SKILL.md +151 -167
  76. package/src/codex/AGENTS.md +6 -0
  77. package/src/codex/hooks/completion-authority-check.cjs +63 -0
  78. package/src/codex/hooks/completion-authority-state.cjs +527 -0
  79. package/src/codex/hooks/completion-authority.cjs +106 -0
  80. package/src/codex/hooks/lib/hook-context.cjs +58 -4
  81. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  82. package/src/codex/hooks/lib/privacy-state.cjs +38 -16
  83. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  84. package/src/codex/hooks/lib/spec-receipt.cjs +86 -115
  85. package/src/codex/hooks/lib/spec-utils.cjs +104 -27
  86. package/src/codex/hooks/privacy-approval.cjs +1 -1
  87. package/src/codex/hooks/privacy-block.cjs +67 -11
  88. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  89. package/src/codex/hooks/session.cjs +2 -0
  90. package/src/codex/hooks/spec-gate.cjs +151 -24
  91. package/src/codex/hooks/spec-state.cjs +65 -12
  92. package/src/codex/hooks/state.cjs +2 -0
  93. package/src/codex/hooks.json +16 -1
  94. package/src/codex/rules/state-sync.md +47 -27
  95. package/src/common/AGENTS.md +1 -1
  96. package/src/opencode/AGENTS.md +4 -2
  97. package/src/opencode/plugins/spec-gate.ts +296 -122
  98. package/src/opencode/plugins/spec-state.ts +183 -116
@@ -1,22 +1,90 @@
1
1
  'use strict';
2
2
 
3
- const REVIEW_VERDICTS = Object.freeze(['PASS', 'FAIL', 'BLOCKED']);
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const PROVENANCE = require('./provenance.cjs');
7
+
8
+ const CANONICAL_VERDICTS = Object.freeze(['PASS', 'PASS_WITH_WARNINGS', 'FAIL', 'BLOCKED']);
9
+ const LEGACY_DIAGNOSTIC_VERDICTS = Object.freeze(['PARTIAL', 'NO_TESTS']);
10
+ const REVIEW_VERDICTS = CANONICAL_VERDICTS;
4
11
  const EXECUTION_TIERS = Object.freeze(['Light', 'Standard', 'Deep']);
5
12
  const LANES = Object.freeze(['Direct', 'Standard', 'Critical']);
6
- const DEEP_TASK_SEQUENCE = Object.freeze(['inspector', 'implementer', 'test-runner', 'code-auditor']);
7
- const CRITICAL_LANE_SEQUENCE = Object.freeze(['inspector', 'implementer', 'test-runner', 'code-auditor']);
8
- const LANE_DELEGATION = Object.freeze({
9
- Direct: Object.freeze([]),
10
- Standard: Object.freeze(['code-auditor']),
11
- Critical: CRITICAL_LANE_SEQUENCE,
13
+ const PLANNING_DEPTHS = Object.freeze(['None', 'Compact', 'Full']);
14
+ const ASSURANCE_LEVELS = Object.freeze(['Routine', 'Elevated', 'Strict']);
15
+ const ARTIFACT_PROFILES = Object.freeze(['targeted', 'bounded', 'strict']);
16
+ const PLANNING_OBLIGATIONS = Object.freeze([
17
+ 'needsRequirements',
18
+ 'needsDesign',
19
+ ]);
20
+ const PROOF_OBLIGATIONS = Object.freeze([
21
+ 'needsInspection',
22
+ 'needsExecutionProof',
23
+ 'needsIndependentAudit',
24
+ 'needsDurableTaskState',
25
+ ]);
26
+ const READ_COMPAT_PROOF_OBLIGATIONS = Object.freeze([
27
+ ...PROOF_OBLIGATIONS,
28
+ 'needsResearchGrounding',
29
+ ]);
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
+ ]);
39
+ const WORKFLOW_POLICY_FIELDS = Object.freeze([
40
+ 'version',
41
+ 'planning_depth',
42
+ 'automatic_planning_depth',
43
+ 'assurance_level',
44
+ 'automatic_assurance_level',
45
+ 'lane',
46
+ 'automatic_lane',
47
+ 'risks',
48
+ 'artifact_profile',
49
+ 'planning_obligations',
50
+ 'proof_obligations',
51
+ 'actor_needs',
52
+ 'override_receipt',
53
+ ]);
54
+ const RECEIPT_BINDING_FIELDS = Object.freeze(['expectedProvenance', 'requireProvenanceBinding']);
55
+ const INDEPENDENT_AUDIT_SCHEMA_VERSION = '1';
56
+ const INDEPENDENT_AUDIT_FIELDS = Object.freeze([
57
+ 'schema_version',
58
+ 'reviewer_session_id',
59
+ 'implementation_session_id',
60
+ 'expected_provenance',
61
+ 'evidence',
62
+ 'verdict',
63
+ ]);
64
+ const FLASH_PROMOTION_FIELDS = Object.freeze(['readyForSync', 'flashTransition', 'promotionReceipt']);
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
+ ]);
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' });
72
+ const ACTOR_NEEDS_BY_OBLIGATION = Object.freeze({
73
+ needsInspection: Object.freeze({ capability: 'inspection', independence: 'same-session' }),
74
+ needsExecutionProof: Object.freeze({ capability: 'execution-proof', independence: 'same-session' }),
75
+ needsIndependentAudit: Object.freeze({ capability: 'audit', independence: 'independent' }),
76
+ needsDurableTaskState: Object.freeze({ capability: 'durable-task-state', independence: 'same-session' }),
77
+ needsResearchGrounding: Object.freeze({ capability: 'research-grounding', independence: 'same-session' }),
12
78
  });
79
+ const ACTOR_CAPABILITIES = new Set(Object.values(ACTOR_NEEDS_BY_OBLIGATION).map((need) => need.capability));
80
+ const ACTOR_INDEPENDENCE = new Set(['same-session', 'independent']);
13
81
  const LANE_RISK_KEYS = Object.freeze({
14
82
  reversibility: ['reversible', 'reversibility', 'irreversible', 'non-reversible'],
15
83
  destructive: ['destructive', 'deletion', 'delete', 'destroy'],
16
84
  auth: ['auth', 'authentication', 'authorization'],
17
85
  payment: ['payment', 'billing', 'charge'],
18
86
  privacy: ['privacy', 'pii', 'personal data'],
19
- data: ['data', 'dataset', 'records'],
87
+ data: ['data loss', 'loss of data', 'data corruption', 'corrupt data', 'data integrity'],
20
88
  schema: ['schema'],
21
89
  migration: ['migration', 'migrate', 'database migration'],
22
90
  publicContract: ['publicContract', 'public_contract', 'public contract', 'api contract', 'breaking change', 'backward compatibility'],
@@ -25,8 +93,351 @@ const LANE_RISK_KEYS = Object.freeze({
25
93
  rollback: ['rollback', 'rollback difficulty', 'hard to rollback', 'cannot rollback', 'no rollback'],
26
94
  });
27
95
 
96
+ function hasOwn(value, key) {
97
+ return value !== null
98
+ && (typeof value === 'object' || typeof value === 'function')
99
+ && Object.prototype.hasOwnProperty.call(value, key);
100
+ }
101
+
102
+ const PLACEHOLDER_TOKENS = new Set(['TBD','TODO','N/A','NA','NONE','UNKNOWN','PENDING','PLACEHOLDER','REPLACE_ME','-','?']);
103
+ const TAP_METADATA_HEADING_RE = /^\s*#\s*(?:(?:tests?|suites?|pass|fail|cancel(?:l)?ed|skipped|todo)\s+\d+|duration(?:_ms)?\s+\d+(?:\.\d+)?)\s*$/i;
104
+ const ARTIFACT_DECLARATION_KEY = 'artifacts';
105
+ const TASK_ARTIFACT_KEYS = Object.freeze(['artifact', 'artifact_ref', 'artifact_path']);
106
+ const PROVENANCE_VALUE_RE = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i;
107
+ const PROSE_RECEIPT_FIELD_RE = /^\s*(?:Notes?|Description|Command(?:\(s\))?)\s*:/i;
108
+ function isPlaceholderToken(value) {
109
+ if (typeof value !== 'string') return true;
110
+ const v = value.trim();
111
+ if (v === '') return true;
112
+ if (/^\{\{.*\}\}$/.test(v)) return true;
113
+ if (/^<.*>$/.test(v)) return true;
114
+ const upper = v.toUpperCase();
115
+ if (PLACEHOLDER_TOKENS.has(upper)) return true;
116
+ return false;
117
+ }
118
+ function isTapMetadataHeading(line) {
119
+ return typeof line === 'string' && TAP_METADATA_HEADING_RE.test(line);
120
+ }
121
+
122
+ function hasConcreteArtifactDeclaration(value) {
123
+ if (typeof value === 'string') return !isPlaceholderToken(value);
124
+ if (Array.isArray(value)) return value.some(hasConcreteArtifactDeclaration);
125
+ return Boolean(value && typeof value === 'object');
126
+ }
127
+
128
+ function isSafeArtifactPath(value) {
129
+ if (typeof value !== 'string' || value !== value.trim() || isPlaceholderToken(value)) return false;
130
+ if (/^(?:[a-z]:[\\/]|[\\/]|https?:\/\/)/i.test(value)) return false;
131
+ return !value.split(/[\\/]+/).includes('..');
132
+ }
133
+
134
+ function isPathInside(root, target) {
135
+ const relative = path.relative(path.resolve(root), path.resolve(target));
136
+ return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
137
+ }
138
+
139
+ function stableStat(stat) {
140
+ return [stat.dev, stat.ino, stat.size, stat.mtimeNs?.toString(), stat.ctimeNs?.toString()].join(':');
141
+ }
142
+
143
+ function hashValidatedArtifact(root, relativePath, expectedHash) {
144
+ if (!isSafeArtifactPath(relativePath) || typeof root !== 'string' || root.trim() === '') return false;
145
+ const rootPath = path.resolve(root);
146
+ const target = path.resolve(rootPath, relativePath);
147
+ if (!isPathInside(rootPath, target)) return false;
148
+
149
+ const segments = path.relative(rootPath, target).split(path.sep).filter(Boolean);
150
+ let current = rootPath;
151
+ try {
152
+ for (let index = 0; index < segments.length; index += 1) {
153
+ current = path.join(current, segments[index]);
154
+ const stat = fs.lstatSync(current);
155
+ if (stat.isSymbolicLink()) return false;
156
+ if (index < segments.length - 1 && !stat.isDirectory()) return false;
157
+ }
158
+ const rootReal = fs.realpathSync(rootPath);
159
+ const targetReal = fs.realpathSync(target);
160
+ if (!isPathInside(rootReal, targetReal)) return false;
161
+ const before = fs.statSync(target);
162
+ if (!before.isFile()) return false;
163
+ const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
164
+ const fd = fs.openSync(target, flags);
165
+ try {
166
+ const opened = fs.fstatSync(fd);
167
+ if (!opened.isFile() || stableStat(opened) !== stableStat(before)) return false;
168
+ const bytes = fs.readFileSync(fd);
169
+ const after = fs.fstatSync(fd);
170
+ if (stableStat(after) !== stableStat(opened)) return false;
171
+ const actual = crypto.createHash('sha256').update(bytes).digest('hex');
172
+ return actual.toLowerCase() === String(expectedHash).toLowerCase();
173
+ } finally {
174
+ fs.closeSync(fd);
175
+ }
176
+ } catch (_) {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ function artifactDeclaration(task = {}) {
182
+ if (Object.prototype.hasOwnProperty.call(task, ARTIFACT_DECLARATION_KEY)) {
183
+ const artifacts = task[ARTIFACT_DECLARATION_KEY];
184
+ const unique = Array.isArray(artifacts) ? new Set(artifacts).size === artifacts.length : false;
185
+ return {
186
+ declared: true,
187
+ valid: Array.isArray(artifacts)
188
+ && artifacts.length > 0
189
+ && unique
190
+ && artifacts.every(isSafeArtifactPath),
191
+ paths: Array.isArray(artifacts) ? [...artifacts] : [],
192
+ };
193
+ }
194
+
195
+ // Read legacy aliases only for compatibility with pre-P0 task registries.
196
+ const legacyKey = TASK_ARTIFACT_KEYS.find((key) => hasConcreteArtifactDeclaration(task?.[key]));
197
+ const legacyValue = legacyKey ? task[legacyKey] : null;
198
+ const legacyPaths = typeof legacyValue === 'string'
199
+ ? [legacyValue]
200
+ : Array.isArray(legacyValue) && legacyValue.every((value) => typeof value === 'string')
201
+ ? [...legacyValue]
202
+ : [];
203
+ return {
204
+ declared: Boolean(legacyKey),
205
+ valid: !legacyKey || (legacyPaths.length > 0 && legacyPaths.every(isSafeArtifactPath)),
206
+ legacy: true,
207
+ paths: legacyPaths,
208
+ };
209
+ }
210
+
211
+ function normalizeProvenanceValue(value) {
212
+ if (typeof value !== 'string') return null;
213
+ const normalized = value.trim().replace(/^sha256:/i, '');
214
+ return normalized || null;
215
+ }
216
+
217
+ function isConcreteProvenanceValue(value) {
218
+ return PROVENANCE_VALUE_RE.test(normalizeProvenanceValue(value) || '');
219
+ }
220
+
221
+ function runtimeContextFromContext(context) {
222
+ const candidates = [
223
+ context?.runtime_context,
224
+ context?.runtimeContext,
225
+ context?.provenance_context,
226
+ context?.provenanceContext,
227
+ context?.receipt_binding?.runtimeContext,
228
+ context?.receiptBinding?.runtimeContext,
229
+ context?.provenance_binding?.runtimeContext,
230
+ context?.provenanceBinding?.runtimeContext,
231
+ ];
232
+ const candidate = candidates.find((value) => PROVENANCE.isTrustedRuntimeContext(value));
233
+ if (!candidate) return null;
234
+ try { return PROVENANCE.recomputeRuntimeContext(candidate); } catch (_) { return null; }
235
+ }
236
+
237
+ function firstDefined(source, keys) {
238
+ if (!source || typeof source !== 'object') return undefined;
239
+ for (const key of keys) {
240
+ if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
241
+ }
242
+ return undefined;
243
+ }
244
+
245
+ function expectedProvenanceFromSource(source) {
246
+ if (!source || typeof source !== 'object' || Array.isArray(source)) return null;
247
+ const nested = firstDefined(source, [
248
+ 'expectedProvenance',
249
+ 'expected_provenance',
250
+ ]);
251
+ const candidate = nested && typeof nested === 'object' && !Array.isArray(nested)
252
+ ? nested
253
+ : source;
254
+ const base = firstDefined(candidate, [
255
+ 'base', 'Base', 'base_sha', 'baseSha', 'expectedBase', 'expected_base',
256
+ 'expectedBaseSha', 'expected_base_sha',
257
+ ]);
258
+ const head = firstDefined(candidate, [
259
+ 'head', 'Head', 'head_sha', 'headSha', 'expectedHead', 'expected_head',
260
+ 'expectedHeadSha', 'expected_head_sha',
261
+ ]);
262
+ if (base === undefined && head === undefined) return null;
263
+ return { base, head };
264
+ }
265
+
266
+ function createReceiptBinding(runtimeContext) {
267
+ if (!PROVENANCE.isTrustedRuntimeContext(runtimeContext)) {
268
+ throw new TypeError('Receipt binding requires a runtime-derived provenance context');
269
+ }
270
+ try { return PROVENANCE.createReceiptBinding(runtimeContext); } catch (error) {
271
+ throw new TypeError(error.message);
272
+ }
273
+ }
274
+
275
+ function receiptValidatorOptions(task = {}, {
276
+ requireProvenanceBinding = false,
277
+ requireExplicitBinding = false,
278
+ runtimeContext,
279
+ } = {}) {
280
+ const declaration = artifactDeclaration(task);
281
+ let trusted = null;
282
+ if (runtimeContext !== undefined && PROVENANCE.isTrustedRuntimeContext(runtimeContext)) {
283
+ try { trusted = PROVENANCE.recomputeRuntimeContext(runtimeContext); } catch (_) { trusted = null; }
284
+ }
285
+ return {
286
+ requireArtifactHash: declaration.declared,
287
+ artifactDeclarationValid: declaration.valid,
288
+ artifactPaths: declaration.paths,
289
+ artifactRoot: trusted?.project_root || null,
290
+ verifyArtifactBytes: Boolean(trusted?.project_root),
291
+ expectedProvenance: trusted ? { base: trusted.base, head: trusted.head } : null,
292
+ requireProvenanceBinding: requireProvenanceBinding || (runtimeContext !== undefined && runtimeContext !== null) || requireExplicitBinding,
293
+ };
294
+ }
295
+
296
+ function sha256ValuesFromLine(line) {
297
+ const values = [];
298
+ const labels = [...line.matchAll(/\b(?:artifact[_-])?sha-?256\s*:/gi)];
299
+ for (const label of labels) {
300
+ const remainder = line.slice(label.index + label[0].length);
301
+ const token = remainder.match(/^\s*([^\s)\],;`]+)/);
302
+ values.push(token ? token[1].trim() : '');
303
+ }
304
+ return values;
305
+ }
306
+
307
+ function artifactPathText(line) {
308
+ return line
309
+ .replace(/^\s*Artifacts?\s*:\s*/i, '')
310
+ .replace(/^\s*Artifact\s+produced\s*:?\s*/i, '')
311
+ .replace(/\b(?:artifact[_-])?sha-?256\s*:\s*[^\s)\],;`]+/gi, '')
312
+ .replace(/[()[\]{}]/g, ' ')
313
+ .replace(/\s+/g, ' ')
314
+ .trim()
315
+ .replace(/(?:\s*[+,;]\s*)+$/, '')
316
+ .trim();
317
+ }
318
+
319
+ function parseArtifactDeclarations(body) {
320
+ const declarations = [];
321
+ let previous = null;
322
+ for (const raw of body.split('\n')) {
323
+ if (PROSE_RECEIPT_FIELD_RE.test(raw)) {
324
+ previous = null;
325
+ continue;
326
+ }
327
+ const isDeclaration = /^\s*Artifacts?\s*:/i.test(raw)
328
+ || /^\s*Artifact\s+produced\b/i.test(raw)
329
+ || /^\s*artifact[_-]?sha-?256\s*:/i.test(raw);
330
+ if (isDeclaration) {
331
+ previous = {
332
+ raw,
333
+ pathText: artifactPathText(raw),
334
+ hashes: sha256ValuesFromLine(raw),
335
+ };
336
+ declarations.push(previous);
337
+ continue;
338
+ }
339
+ if (previous && /^\s*sha-?256\s*:/i.test(raw)) {
340
+ previous.hashes.push(...sha256ValuesFromLine(raw));
341
+ continue;
342
+ }
343
+ if (raw.trim() !== '') previous = null;
344
+ }
345
+ return declarations;
346
+ }
347
+
348
+ function artifactSha256Values(body) {
349
+ return parseArtifactDeclarations(body).flatMap((declaration) => declaration.hashes);
350
+ }
351
+
352
+ function hasCanonicalArtifactHash(body) {
353
+ const hashes = artifactSha256Values(body);
354
+ return hashes.length > 0 && hashes.every((value) => /^[a-f0-9]{64}$/i.test(value));
355
+ }
356
+
357
+ function hasConcreteArtifactHash(body) {
358
+ const hashes = artifactSha256Values(body);
359
+ return hashes.length > 0 && hashes.every((value) => value !== '' && !isPlaceholderToken(value));
360
+ }
361
+
362
+ function hasExplicitFailure(body) {
363
+ if (typeof body !== 'string') return false;
364
+ const lines = body.split('\n');
365
+ for (const raw of lines) {
366
+ if (PROSE_RECEIPT_FIELD_RE.test(raw)) continue;
367
+ const trimmed = raw.trim();
368
+ if (trimmed === '') continue;
369
+ // 1. Zero execution (structured, anchored): zero tests, zero passing, or no collection.
370
+ if (/^\s*(?:#|ℹ)?\s*tests?\s+0\b/i.test(raw)) return true;
371
+ if (/^\s*(?:#|ℹ)?\s*0\s+tests?\b/i.test(raw)) return true;
372
+ if (/^\s*(?:#|ℹ)?\s*tests?\s+run\s*:\s*0\b/i.test(raw)) return true;
373
+ if (/^\s*0\s+(?:passing|passed)\b/i.test(raw)) return true;
374
+ if (/^\s*Tests?\s*:\s*0\s+(?:passing|passed)\b/i.test(raw)) return true;
375
+ if (/^\s*Tests?\s+passed\s*:\s*0\b/i.test(raw)) return true;
376
+ if (/^\s*Passed\s*:\s*0\b/i.test(raw)) return true;
377
+ if (/^\s*Tests:\s*0\s+total\b/i.test(raw)) return true;
378
+ if (/^\s*collected\s+0\s+items\b/i.test(raw)) return true;
379
+ if (/^\s*No tests? found\b/i.test(raw)) return true;
380
+ // 2. Cancellation (optional marker #/ℹ, cancelled/canceled, both orders, count >0; 0 passes)
381
+ if (/^\s*(?:#|ℹ)?\s*cancel(?:l)?ed\s*[:\s]*[1-9]\d*\b/i.test(raw)) return true;
382
+ if (/^\s*(?:#|ℹ)?\s*[1-9]\d*\s+cancel(?:l)?ed\b/i.test(raw)) return true;
383
+ // 3. Suite / file failures: Test Suites: N failed, Test Files N failed (N>0)
384
+ if (/^\s*Test Suites?:?\s*[1-9]\d*\s+failed\b/i.test(raw)) return true;
385
+ if (/^\s*Test Files?:?\s*[1-9]\d*\s+failed\b/i.test(raw)) return true;
386
+ if (/^\s*Test Suites?:?\s*0\s+failed\b[^\r\n]*\b0\s+passed\b/i.test(raw)) return true;
387
+ if (/^\s*Test Files?:?\s*0\s+failed\b[^\r\n]*\b0\s+passed\b/i.test(raw)) return true;
388
+ // 4. Pytest short-summary entries (anchored to a Python test path)
389
+ if (/^\s*FAILED[ \t]+\S+\.py(?:::\S+)?(?:[ \t]+-[^\r\n]*)?\s*$/i.test(raw)) return true;
390
+ // 5. Runner FAIL (anchored exact FAIL, horizontal whitespace/colon/end; --- FAIL:)
391
+ if (/^\s*FAIL(?:[ \t]+|:|$)/.test(raw)) return true;
392
+ if (/^\s*---\s*FAIL:/.test(raw)) return true;
393
+ // 6. Error/failure summaries (anchored, N>0; prose Notes/Description/Command skipped)
394
+ if (/^\s*[1-9]\d*\s+errors?\b/i.test(raw)) return true;
395
+ if (/^\s*errors?\s*[:\s]*[1-9]\d*\b/i.test(raw)) return true;
396
+ if (/^\s*failures?\s*[:\s]*[1-9]\d*\b/i.test(raw)) return true;
397
+ if (/^\s*failure\s+summary\s*[:.]?\s*$/i.test(raw)) return true;
398
+ if (/^\s*Found\s+[1-9]\d*\s+errors?\b/i.test(raw)) return true;
399
+ if (/^\s*failureCount\s*:\s*[1-9]\d*\s*[,;]?\s*$/i.test(raw)) return true;
400
+ if (/^\s*ERROR\s*$/i.test(raw)) return true;
401
+ if (/^\s*ERROR\s+collecting\b/i.test(raw)) return true;
402
+ if (/^\s*Tests run\s*:/i.test(raw) && (/\b(?:Failures?|Errors?)\s*[:=]\s*[1-9]/i.test(raw) || /\b0\s+(?:tests?|passing|passed)\b/i.test(raw))) return true;
403
+ // Maven Surefire's structured failure lines; do not treat arbitrary [ERROR] prose as failure.
404
+ if (/^\s*\[ERROR\]\s+Tests run:\s*\d+\b[^\r\n]*\b(?:Failures?|Errors?):\s*[1-9]\d*\b/i.test(raw)) return true;
405
+ if (/^\s*\[ERROR\]\s+Failed to execute goal\b/i.test(raw)) return true;
406
+ // 7. Gradle (anchored, optional)
407
+ if (/^\s*\d+\s+tests?\s+completed,\s*[1-9]\d*\s+failed\b/i.test(raw)) return true;
408
+ // Existing structured checks (preserve, anchored where needed to avoid prose false positive)
409
+ if (/^\s*Tests?\s+failed\s*$/i.test(raw)) return true;
410
+ if (/^\s*Tests?\s+failed\s*:\s*[1-9]\d*\b/i.test(raw)) return true;
411
+ if (/\b(?:Verification|Result|Status|Outcome)\s*:\s*FAIL(?:ED|URE)?\b/i.test(raw)) return true;
412
+ if (/^\s*FAIL\s*$/i.test(trimmed)) return true;
413
+ if (/^\s*FAIL\s*:/i.test(trimmed)) return true;
414
+ if (/^\s*FAILED\s*$/i.test(trimmed)) return true;
415
+ if (/^\s*FAILED\s*:/i.test(trimmed)) return true;
416
+ if (/^\s*FAILURE\s*$/i.test(trimmed)) return true;
417
+ if (/^\s*FAILURE\s*:/i.test(trimmed)) return true;
418
+ if (/^\s*one\s+failing(?:\s+(?:test|tests|suite|suites))?\s*[,:]?\s*$/i.test(raw)) return true;
419
+ if (/^\s*(?:#|ℹ)?\s*fail\s+[1-9]/i.test(raw)) return true;
420
+ if (/^\s*not ok\b/i.test(raw)) return true;
421
+ if (/^\s*Tests\s*:?\s*[1-9]/i.test(raw) && /\bfailed\b/i.test(raw)) return true;
422
+ if (/^\s*\d+\s+failed\b/i.test(raw)) {
423
+ const m = raw.match(/^\s*(\d+)\s+failed\b/i);
424
+ if (m && parseInt(m[1], 10) !== 0) return true;
425
+ }
426
+ if (/exit\s+code\s*[:=]?\s*[1-9]/i.test(raw)) return true;
427
+ }
428
+ return false;
429
+ }
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
+ }
438
+
28
439
  const APPROVAL_SCHEMA_VERSION = '2.0';
29
- 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.`;
30
441
 
31
442
  function assertLane(lane) {
32
443
  if (!LANES.includes(lane)) {
@@ -35,6 +446,35 @@ function assertLane(lane) {
35
446
  return lane;
36
447
  }
37
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
+
38
478
  function asTrue(value) {
39
479
  return value === true || value === 1 || value === 'true' || value === 'yes';
40
480
  }
@@ -53,11 +493,13 @@ function textValue(input) {
53
493
 
54
494
  function classifyRiskSignals(input = {}) {
55
495
  const nestedSignals = input.signals && typeof input.signals === 'object' ? input.signals : {};
56
- const declaredSignals = [input.riskSignals, input.risks]
496
+ const declaredRiskValues = [input.riskSignals, input.risks]
57
497
  .flatMap((value) => Array.isArray(value) ? value : [])
58
- .filter((value) => typeof value === 'string')
498
+ .filter((value) => typeof value === 'string');
499
+ const declaredSignals = declaredRiskValues
59
500
  .join(' ')
60
501
  .toLowerCase();
502
+ const explicitRisk = (terms) => declaredRiskValues.some((value) => terms.includes(value.trim().toLowerCase()));
61
503
  const source = {
62
504
  ...input,
63
505
  ...nestedSignals,
@@ -78,7 +520,9 @@ function classifyRiskSignals(input = {}) {
78
520
  auth: signalValue(source, LANE_RISK_KEYS.auth) || hasTerm(LANE_RISK_KEYS.auth),
79
521
  payment: signalValue(source, LANE_RISK_KEYS.payment) || hasTerm(LANE_RISK_KEYS.payment),
80
522
  privacy: signalValue(source, LANE_RISK_KEYS.privacy) || hasTerm(LANE_RISK_KEYS.privacy),
81
- 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),
82
526
  schema: signalValue(source, LANE_RISK_KEYS.schema) || hasTerm(LANE_RISK_KEYS.schema),
83
527
  migration: signalValue(source, LANE_RISK_KEYS.migration) || hasTerm(LANE_RISK_KEYS.migration),
84
528
  publicContract: signalValue(source, LANE_RISK_KEYS.publicContract) || hasTerm(LANE_RISK_KEYS.publicContract),
@@ -108,95 +552,603 @@ function overrideLane(input = {}) {
108
552
  return input.override ?? input.laneOverride ?? input.requestedLane ?? input.lane ?? null;
109
553
  }
110
554
 
111
- function isUserAuthorizedForDowngrade(input = {}) {
112
- // Explicit user-originated authorization is required for any downgrade from Critical.
113
- // We accept several synonyms for the authorization flag to allow different callers.
114
- return input.userAuthorized === true
115
- || input.user_authorized === true
116
- || input.explicitUserAuthorization === true
117
- || input.downgradeAuthorized === true
118
- || input.confirmDowngrade === true
119
- || input.authorization === 'user'
120
- || input.origin === 'user'
121
- || input.requestedBy === 'user'
122
- || input.user_approved === true;
555
+ function freezeClassification(obj) {
556
+ Object.freeze(obj);
557
+ if (Array.isArray(obj.warnings)) Object.freeze(obj.warnings);
558
+ if (Array.isArray(obj.risks)) Object.freeze(obj.risks);
559
+ if (obj.signals && typeof obj.signals === 'object') Object.freeze(obj.signals);
560
+ trustedClassifications.add(obj);
561
+ return obj;
562
+ }
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';
123
581
  }
124
582
 
125
583
  function classifyLane(input = {}) {
126
- if (typeof input === 'string') return { lane: assertLane(input), automaticLane: input, overridden: false, warnings: [], risks: [] };
584
+ if (typeof input === 'string') {
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
+ });
595
+ }
127
596
  if (!input || typeof input !== 'object') throw new TypeError('Lane classification input must be an object');
128
-
129
- const signals = classifyRiskSignals(input);
130
- const risks = Object.entries(signals).filter(([, active]) => active).map(([name]) => name);
131
597
  const taskCount = input.taskCount === undefined ? 1 : input.taskCount;
132
598
  if (!Number.isInteger(taskCount) || taskCount < 1) throw new RangeError('taskCount must be a positive integer');
133
599
 
134
- const automaticLane = risks.length > 0
135
- ? 'Critical'
136
- : explicitReversible(input) && input.lowRisk === true && input.isolated === true && taskCount <= 2
137
- ? 'Direct'
138
- : 'Standard';
139
- const requested = overrideLane(input);
140
- if (requested === null || requested === undefined) {
141
- return {
142
- lane: automaticLane,
143
- automaticLane,
144
- overridden: false,
145
- warnings: [],
146
- risks,
147
- signals,
148
- };
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`);
149
618
  }
150
- assertLane(requested);
151
- const isDowngrade = LANES.indexOf(requested) < LANES.indexOf(automaticLane);
152
- if (isDowngrade && !isUserAuthorizedForDowngrade(input)) {
153
- throw new Error(
154
- `Downgrade from ${automaticLane} to ${requested} requires explicit user authorization. ` +
155
- `Automatic ${automaticLane} due to: ${risks.join(', ') || 'default risk policy'}. ` +
156
- `Provide userAuthorized:true (origin=user) to confirm downgrade and acknowledge reduced review/evidence coverage.`
157
- );
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,
632
+ automaticLane,
633
+ overridden: Boolean(requestedPlanning || requestedAssurance || legacyAxes),
634
+ warnings: legacyAxes ? [`Legacy lane ${legacyRequested} adapted to Specs v2 axes.`] : [],
635
+ risks,
636
+ signals,
637
+ });
638
+ }
639
+
640
+ function uniqueStrings(values, field) {
641
+ if (!Array.isArray(values)) return { valid: false, error: `${field} must be an array` };
642
+ if (values.some((value) => typeof value !== 'string' || value.trim() === '')) {
643
+ return { valid: false, error: `${field} must contain non-empty strings` };
644
+ }
645
+ if (new Set(values).size !== values.length) return { valid: false, error: `${field} must not contain duplicates` };
646
+ return { valid: true };
647
+ }
648
+
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);
658
+ const obligations = new Set(['needsExecutionProof']);
659
+ if (level === 'Elevated') obligations.add('needsInspection');
660
+ if (level === 'Strict') {
661
+ obligations.add('needsInspection');
662
+ obligations.add('needsIndependentAudit');
663
+ }
664
+ return PROOF_OBLIGATIONS.filter((obligation) => obligations.has(obligation));
665
+ }
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
+
688
+ function actorNeedsFor(obligations) {
689
+ return obligations.map((obligation) => ({ ...ACTOR_NEEDS_BY_OBLIGATION[obligation] }));
690
+ }
691
+
692
+ function mergeProofObligations(...groups) {
693
+ const selected = new Set(groups.flat());
694
+ return READ_COMPAT_PROOF_OBLIGATIONS.filter((obligation) => selected.has(obligation));
695
+ }
696
+
697
+ function snapshotFromClassification(classification) {
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}`);
707
+ }
708
+ const risks = Array.isArray(classification.risks) ? [...new Set(classification.risks)] : [];
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(', ')}`);
712
+ }
713
+ const lane = compatibilityLane(planningDepth, assuranceLevel);
714
+ const automaticLane = compatibilityLane(automaticPlanningDepth, automaticAssuranceLevel);
715
+ const proofObligations = obligationsForAssurance(assuranceLevel, risks);
716
+ return {
717
+ version: WORKFLOW_POLICY_VERSION,
718
+ planning_depth: planningDepth,
719
+ automatic_planning_depth: automaticPlanningDepth,
720
+ assurance_level: assuranceLevel,
721
+ automatic_assurance_level: automaticAssuranceLevel,
722
+ lane,
723
+ automatic_lane: automaticLane,
724
+ risks,
725
+ artifact_profile: ARTIFACT_PROFILE_BY_PLANNING[planningDepth],
726
+ planning_obligations: planningObligationsFor(planningDepth),
727
+ proof_obligations: proofObligations,
728
+ actor_needs: actorNeedsFor(proofObligations),
729
+ override_receipt: null,
730
+ };
731
+ }
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
+
827
+ function workflowPolicySnapshot(input = {}) {
828
+ if (input && typeof input === 'object' && hasOwn(input, 'workflow_policy')) {
829
+ return readWorkflowPolicySnapshot(input);
830
+ }
831
+ if (input && typeof input === 'object' && input.design_context?.execution_tier) {
832
+ return readWorkflowPolicySnapshot(input);
833
+ }
834
+ if (input && typeof input === 'object' && isWorkflowPolicySnapshot(input)) {
835
+ assertWorkflowPolicySnapshot(input);
836
+ if (input.version === WORKFLOW_POLICY_VERSION) return clonePolicySnapshot(input);
837
+ return readWorkflowPolicySnapshot({ workflow_policy: input });
158
838
  }
159
- const warnings = [`Explicit lane override selected: ${requested}.`];
160
- if (requested !== automaticLane) {
161
- warnings.push(`Override changes automatic ${automaticLane} classification to ${requested}.`);
162
- if (LANES.indexOf(requested) < LANES.indexOf(automaticLane)) {
163
- warnings.push(`Downgrading risk lane may reduce review and evidence coverage: ${risks.join(', ') || 'default risk policy'}.`);
164
- if (isUserAuthorizedForDowngrade(input)) {
165
- warnings.push(`Downgrade authorized by user - proceeding with ${requested} despite ${automaticLane} signals.`);
839
+ const classification = trustedClassifications.has(input)
840
+ ? input
841
+ : classifyLane(input);
842
+ return snapshotFromClassification(classification);
843
+ }
844
+
845
+ function isWorkflowPolicySnapshot(value) {
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 };
877
+ }
878
+
879
+ function validateWorkflowPolicySnapshot(snapshot) {
880
+ const errors = [];
881
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
882
+ return { valid: false, errors: ['workflow_policy must be an object'] };
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');
166
913
  }
167
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 };
168
924
  }
169
- return {
170
- lane: requested,
171
- automaticLane,
172
- overridden: true,
173
- warnings,
925
+ if (snapshot.version === '1') return validateV1Snapshot(snapshot);
926
+ const keys = Object.keys(snapshot).sort();
927
+ const expectedKeys = [...WORKFLOW_POLICY_FIELDS].sort();
928
+ if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)) {
929
+ errors.push(`workflow_policy fields must be exactly ${WORKFLOW_POLICY_FIELDS.join(', ')}`);
930
+ }
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(', ')}`);
936
+ if (!LANES.includes(snapshot.lane)) errors.push(`workflow_policy.lane must be one of ${LANES.join(', ')}`);
937
+ if (!LANES.includes(snapshot.automatic_lane)) errors.push(`workflow_policy.automatic_lane must be one of ${LANES.join(', ')}`);
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');
947
+ const risks = uniqueStrings(snapshot.risks, 'workflow_policy.risks');
948
+ if (!risks.valid) errors.push(risks.error);
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`);
953
+ }
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');
959
+ const obligations = uniqueStrings(snapshot.proof_obligations, 'workflow_policy.proof_obligations');
960
+ if (!obligations.valid) {
961
+ errors.push(obligations.error);
962
+ } else {
963
+ for (const obligation of snapshot.proof_obligations) {
964
+ if (!READ_COMPAT_PROOF_OBLIGATIONS.includes(obligation)) errors.push(`workflow_policy.proof_obligations has unknown obligation ${obligation}`);
965
+ }
966
+ if (!snapshot.proof_obligations.includes('needsExecutionProof')) {
967
+ errors.push('workflow_policy.proof_obligations must include needsExecutionProof');
968
+ }
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');
976
+ }
977
+ }
978
+ }
979
+ if (!Array.isArray(snapshot.actor_needs)) {
980
+ errors.push('workflow_policy.actor_needs must be an array');
981
+ } else {
982
+ for (const need of snapshot.actor_needs) {
983
+ if (!need || typeof need !== 'object' || Array.isArray(need)
984
+ || Object.keys(need).sort().join(',') !== 'capability,independence'
985
+ || !ACTOR_CAPABILITIES.has(need.capability)
986
+ || !ACTOR_INDEPENDENCE.has(need.independence)) {
987
+ errors.push('workflow_policy.actor_needs must describe capability and independence, never an agent name');
988
+ break;
989
+ }
990
+ }
991
+ if (ASSURANCE_LEVELS.includes(snapshot.assurance_level) && obligations.valid
992
+ && JSON.stringify(snapshot.actor_needs) !== JSON.stringify(actorNeedsFor(snapshot.proof_obligations))) {
993
+ errors.push('workflow_policy.actor_needs must correspond to proof_obligations');
994
+ }
995
+ }
996
+ if (snapshot.override_receipt !== null) errors.push('legacy workflow_policy.override_receipt is inert and must be null');
997
+ return { valid: errors.length === 0, errors };
998
+ }
999
+
1000
+ function assertWorkflowPolicySnapshot(snapshot) {
1001
+ const result = validateWorkflowPolicySnapshot(snapshot);
1002
+ if (!result.valid) throw new TypeError(`Invalid workflow policy snapshot: ${result.errors.join('; ')}`);
1003
+ return snapshot;
1004
+ }
1005
+
1006
+ function readWorkflowPolicySnapshot(spec = {}) {
1007
+ if (!spec || typeof spec !== 'object') throw new TypeError('Spec state must be an object');
1008
+ if (hasOwn(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;
1039
+ }
1040
+ // Legacy state is read-compatible only. The fallback is deliberately not
1041
+ // written back and cannot authorize a downgrade or override new input.
1042
+ const rawLegacyTier = spec.design_context && spec.design_context.execution_tier;
1043
+ const legacyTier = typeof rawLegacyTier === 'string'
1044
+ ? Object.keys(LEGACY_TIER_LANES).find((tier) => tier.toLowerCase() === rawLegacyTier.toLowerCase())
1045
+ : null;
1046
+ const legacyLane = legacyTier ? LEGACY_TIER_LANES[legacyTier] : null;
1047
+ if (legacyLane) {
1048
+ const axes = v1Axes(legacyLane);
1049
+ return snapshotFromClassification({ ...axes, automaticPlanningDepth: axes.planningDepth, automaticAssuranceLevel: axes.assuranceLevel, risks: [] });
1050
+ }
1051
+ throw new TypeError('workflow_policy snapshot is missing at the spec boundary');
1052
+ }
1053
+
1054
+ function persistWorkflowPolicySnapshot(spec, input = {}) {
1055
+ if (!spec || typeof spec !== 'object' || Array.isArray(spec)) throw new TypeError('Spec state must be an object');
1056
+ const next = { ...spec };
1057
+ delete next.override_receipt;
1058
+ if (hasOwn(spec, 'workflow_policy')) {
1059
+ return { ...next, workflow_policy: canonicalWorkflowPolicySnapshot(spec) };
1060
+ }
1061
+ return { ...next, workflow_policy: canonicalWorkflowPolicySnapshot(input) };
1062
+ }
1063
+
1064
+ const persistWorkflowPolicy = persistWorkflowPolicySnapshot;
1065
+ const createWorkflowPolicySnapshot = workflowPolicySnapshot;
1066
+
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)))];
1079
+ }
1080
+
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);
1085
+ const current = policyOrSpec && typeof policyOrSpec === 'object' && hasOwn(policyOrSpec, 'workflow_policy')
1086
+ ? readWorkflowPolicySnapshot(policyOrSpec)
1087
+ : workflowPolicySnapshot(policyOrSpec);
1088
+ const discovered = classifyRiskSignals(riskInput);
1089
+ const explicitRisks = Array.isArray(riskInput?.risks)
1090
+ ? riskInput.risks.filter((risk) => typeof risk === 'string' && risk.trim() !== '')
1091
+ : [];
1092
+ const newRisks = [...new Set([
1093
+ ...Object.entries(discovered).filter(([, active]) => active).map(([name]) => name),
1094
+ ...explicitRisks,
1095
+ ])];
1096
+ const risks = [...new Set([...current.risks, ...newRisks])];
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,
174
1124
  risks,
175
- signals,
176
1125
  };
1126
+ return canonicalInput ? canonicalSnapshotFromAxes(axes) : snapshotFromClassification(axes);
1127
+ }
1128
+
1129
+ function policyForSpec(spec, riskInput = null) {
1130
+ const snapshot = riskInput ? escalateWorkflowPolicy(spec, riskInput) : readWorkflowPolicySnapshot(spec);
1131
+ return lanePolicy(snapshot);
177
1132
  }
178
1133
 
179
1134
  function lanePolicy(input = {}) {
180
- const classification = typeof input === 'string'
181
- ? classifyLane(input)
182
- : input && LANES.includes(input.lane) && Object.hasOwn(input, 'automaticLane')
183
- ? input
184
- : classifyLane(input);
185
- const lane = assertLane(classification.lane);
186
- const delegated = [...LANE_DELEGATION[lane]];
1135
+ const snapshot = input && typeof input === 'object' && hasOwn(input, 'workflow_policy')
1136
+ ? readWorkflowPolicySnapshot(input)
1137
+ : input && typeof input === 'object' && isWorkflowPolicySnapshot(input)
1138
+ ? (assertWorkflowPolicySnapshot(input), clonePolicySnapshot(input))
1139
+ : workflowPolicySnapshot(input);
1140
+ const lane = assertLane(snapshot.lane);
187
1141
  return {
188
- lane,
189
- executionTier: lane === 'Critical' ? 'Deep' : lane === 'Direct' ? 'Light' : 'Standard',
190
- automaticLane: classification.automaticLane,
191
- overridden: classification.overridden,
192
- warnings: [...classification.warnings],
193
- risks: [...classification.risks],
194
- delegated,
195
- requiresSpec: lane !== 'Direct',
196
- requiresState: lane !== 'Direct',
1142
+ ...snapshot,
1143
+ proof_obligations: [...snapshot.proof_obligations],
1144
+ actor_needs: snapshot.actor_needs.map((need) => ({ ...need })),
1145
+ planning_obligations: [...snapshot.planning_obligations],
1146
+ requiresSpec: snapshot.planning_depth !== 'None',
1147
+ requiresState: snapshot.planning_depth === 'Full'
1148
+ || snapshot.proof_obligations.includes('needsDurableTaskState'),
197
1149
  qualityGate: lane === 'Direct' ? 'main-session' : lane === 'Standard' ? 'combined-feature-review' : 'strict-evidence',
198
1150
  shipPoint: lane === 'Direct' ? 'task' : 'feature',
199
- evidence: lane === 'Critical' ? 'strict' : lane === 'Standard' ? 'bounded' : 'targeted',
1151
+ evidence: snapshot.artifact_profile,
200
1152
  };
201
1153
  }
202
1154
 
@@ -205,85 +1157,105 @@ function isLegacyApprovalObject(obj) {
205
1157
  }
206
1158
 
207
1159
  function validateApprovalSchema(spec) {
208
- if (!spec || typeof spec !== 'object') return { valid: false, legacy: false, error: 'spec must be an object' };
1160
+ if (!spec || typeof spec !== 'object' || Array.isArray(spec)) return { valid: false, absent: false, legacy: false, error: 'spec must be a non-array object' };
1161
+ if (!Object.prototype.hasOwnProperty.call(spec, 'approvals')) {
1162
+ return { valid: true, absent: true, legacy: false };
1163
+ }
209
1164
  const approvals = spec.approvals;
210
- if (!approvals || typeof approvals !== 'object') return { valid: true, legacy: false };
1165
+ if (approvals === null || typeof approvals !== 'object' || Array.isArray(approvals)) {
1166
+ return { valid: false, absent: false, legacy: false, error: 'approvals must be a non-empty object' };
1167
+ }
1168
+ if (Object.keys(approvals).length === 0) {
1169
+ return { valid: false, absent: false, legacy: false, error: 'approvals must not be empty' };
1170
+ }
211
1171
  // Check for legacy field `approved` in any stage
212
1172
  for (const [stage, val] of Object.entries(approvals)) {
213
1173
  if (isLegacyApprovalObject(val)) {
214
1174
  return {
215
1175
  valid: false,
1176
+ absent: false,
216
1177
  legacy: true,
217
1178
  error: `Legacy approval state at approvals.${stage}.approved. ${LEGACY_APPROVAL_ERROR}`,
218
1179
  };
219
1180
  }
220
- if (val && typeof val === 'object') {
221
- // also check if missing new fields but has generated/approved only
222
- if ('generated' in val && !('agent_validated' in val) && !('user_approved' in val) && !('approved' in val)) {
223
- // This is ambiguous: generated without new fields - treat as not ready but not legacy
224
- }
1181
+ if (!val || typeof val !== 'object' || Array.isArray(val)) {
1182
+ return { valid: false, absent: false, legacy: false, error: `approvals.${stage} must be an approval object` };
1183
+ }
1184
+ const keys = Object.keys(val).sort();
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` };
1187
+ }
1188
+ if (keys.some((key) => typeof val[key] !== 'boolean')) {
1189
+ return { valid: false, absent: false, legacy: false, error: `approvals.${stage} fields must be boolean` };
225
1190
  }
226
1191
  }
227
1192
  // Check schema version
228
- const version = spec.schema_version || spec.approval_schema_version;
1193
+ const version = Object.prototype.hasOwnProperty.call(spec, 'schema_version')
1194
+ ? spec.schema_version
1195
+ : spec.approval_schema_version;
1196
+ if (version !== undefined && typeof version !== 'string') {
1197
+ return { valid: false, absent: false, legacy: false, error: 'schema_version must be a string' };
1198
+ }
229
1199
  if (version && version !== APPROVAL_SCHEMA_VERSION) {
230
- return { valid: false, legacy: false, error: `Unsupported schema_version ${version}, expected ${APPROVAL_SCHEMA_VERSION}` };
1200
+ return { valid: false, absent: false, legacy: false, error: `Unsupported schema_version ${version}, expected ${APPROVAL_SCHEMA_VERSION}` };
231
1201
  }
232
1202
  if (!version && Object.keys(approvals).length > 0) {
233
1203
  // No version but uses new schema - allow for backward compat but warn that version should be present
234
1204
  // Fail closed only if legacy detected above; otherwise allow.
235
- return { valid: true, legacy: false, warning: `Missing schema_version, expected ${APPROVAL_SCHEMA_VERSION}` };
1205
+ return { valid: true, absent: false, legacy: false, warning: `Missing schema_version, expected ${APPROVAL_SCHEMA_VERSION}` };
236
1206
  }
237
- return { valid: true, legacy: false };
1207
+ return { valid: true, absent: false, legacy: false };
238
1208
  }
239
1209
 
240
1210
  function approvalState(state = {}) {
241
- // Detect legacy at top level or inside approvals map
242
- const approvalsMap = state && state.approvals && typeof state.approvals === 'object' ? state.approvals : null;
243
- if (approvalsMap) {
244
- for (const val of Object.values(approvalsMap)) {
245
- if (isLegacyApprovalObject(val)) {
246
- throw new Error(LEGACY_APPROVAL_ERROR);
247
- }
248
- }
1211
+ if (!state || typeof state !== 'object' || Array.isArray(state)) {
1212
+ throw new TypeError('approval state must be an object');
1213
+ }
1214
+ if (Object.prototype.hasOwnProperty.call(state, 'approvals')) {
1215
+ const validation = validateApprovalSchema(state);
1216
+ if (!validation.valid) throw new Error(validation.error);
1217
+ return {
1218
+ generated: false,
1219
+ agent_validated: false,
1220
+ ready: false,
1221
+ present: true,
1222
+ schema_version: APPROVAL_SCHEMA_VERSION,
1223
+ legacy: false,
1224
+ };
1225
+ }
1226
+ if (isLegacyApprovalObject(state)) throw new Error(LEGACY_APPROVAL_ERROR);
1227
+
1228
+ const version = Object.prototype.hasOwnProperty.call(state, 'schema_version')
1229
+ ? state.schema_version
1230
+ : state.approval_schema_version;
1231
+ if (version !== undefined && (typeof version !== 'string' || version !== APPROVAL_SCHEMA_VERSION)) {
1232
+ throw new Error(`Unsupported schema_version ${String(version)}, expected ${APPROVAL_SCHEMA_VERSION}`);
249
1233
  }
250
- const source = approvalsMap ? null : state;
251
- // If source is approvals map itself, we should not try to interpret it as single stage
252
- // But approvalState is intended for a single stage object; if passed whole approvals map, treat as error?
253
- // Keep original behavior: if state has approvals, use that, otherwise state itself.
254
- // For whole-spec case with legacy inside map, we already threw above.
255
- // For single-stage object:
256
- const singleSource = state && state.approvals && typeof state.approvals === 'object' && !approvalsMap ? state.approvals : state;
257
- // Actually simplify: if state has approvals, we already handled legacy; for single stage, source is state itself.
258
- // If state was {generated:true, agent_validated:true}, then approvalsMap is null, source = state
259
- const target = approvalsMap ? null : state;
260
- if (target && isLegacyApprovalObject(target)) {
261
- throw new Error(LEGACY_APPROVAL_ERROR);
262
- }
263
- // For single stage case, use target
264
- // For map case, we shouldn't be called with map; but if called with spec object containing approvals map, we return not-ready?
265
- if (approvalsMap) {
266
- // This is a spec-level object, not a stage. Return aggregate? For compatibility, treat as not ready.
267
- // Check if any stage has valid approval? But spec-level ready requires all stages.
268
- // We'll just return not ready with legacy check already done.
269
- return { generated: false, agent_validated: false, user_approved: false, ready: false, schema_version: APPROVAL_SCHEMA_VERSION, legacy: false };
270
- }
271
- const src = target || state;
272
- // Final legacy check on src
273
- if (isLegacyApprovalObject(src)) {
274
- throw new Error(LEGACY_APPROVAL_ERROR);
1234
+
1235
+ const known = ['generated', 'agent_validated', 'user_approved'];
1236
+ const present = known.some((key) => Object.prototype.hasOwnProperty.call(state, key));
1237
+ const unknown = Object.keys(state).filter((key) => !known.includes(key) && key !== 'schema_version' && key !== 'approval_schema_version');
1238
+ if (unknown.length > 0) throw new TypeError(`malformed approval state fields: ${unknown.join(', ')}`);
1239
+ for (const key of known) {
1240
+ if (Object.prototype.hasOwnProperty.call(state, key) && typeof state[key] !== 'boolean') {
1241
+ throw new TypeError(`approval state field ${key} must be boolean`);
1242
+ }
275
1243
  }
276
1244
  const result = {
277
- generated: src?.generated === true,
278
- agent_validated: src?.agent_validated === true,
279
- user_approved: src?.user_approved === true,
1245
+ generated: state.generated === true,
1246
+ agent_validated: state.agent_validated === true,
1247
+ };
1248
+ return {
1249
+ ...result,
1250
+ ready: result.generated && result.agent_validated,
1251
+ present,
1252
+ schema_version: APPROVAL_SCHEMA_VERSION,
1253
+ legacy_user_approved: state.user_approved === true,
280
1254
  };
281
- return { ...result, ready: result.generated && result.agent_validated && result.user_approved, schema_version: APPROVAL_SCHEMA_VERSION };
282
1255
  }
283
-
284
1256
  function assertVerdict(verdict) {
285
- if (!REVIEW_VERDICTS.includes(verdict)) {
286
- throw new TypeError(`Unsupported review verdict: ${String(verdict)}`);
1257
+ if (!CANONICAL_VERDICTS.includes(verdict)) {
1258
+ throw new TypeError(`Unsupported canonical verdict: ${String(verdict)}`);
287
1259
  }
288
1260
  return verdict;
289
1261
  }
@@ -310,10 +1282,9 @@ function delegationPlan({ tier, lane = null, mode = 'full-spec', taskCount = 1 }
310
1282
  const policy = lanePolicy(lane);
311
1283
  return {
312
1284
  lane: policy.lane,
313
- executionTier: policy.executionTier,
314
- tier: tier || null,
315
1285
  mode,
316
- delegated: policy.delegated,
1286
+ proof_obligations: [...policy.proof_obligations],
1287
+ actor_needs: policy.actor_needs.map((need) => ({ ...need })),
317
1288
  qualityGate: policy.qualityGate,
318
1289
  shipPoint: policy.shipPoint,
319
1290
  };
@@ -325,65 +1296,477 @@ function delegationPlan({ tier, lane = null, mode = 'full-spec', taskCount = 1 }
325
1296
  throw new RangeError('taskCount must be a positive integer');
326
1297
  }
327
1298
 
328
- if (tier === 'Light') {
329
- return { tier, mode, delegated: [], qualityGate: 'main-session', shipPoint: 'task' };
1299
+ // Legacy tier input is read-only compatibility. It is translated once to
1300
+ // the canonical lane contract and never emits a new tier authority or an
1301
+ // agent sequence.
1302
+ const legacyLane = LEGACY_TIER_LANES[tier];
1303
+ const policy = lanePolicy(legacyLane);
1304
+ return {
1305
+ tier,
1306
+ legacy: true,
1307
+ lane: policy.lane,
1308
+ mode,
1309
+ proof_obligations: [...policy.proof_obligations],
1310
+ actor_needs: policy.actor_needs.map((need) => ({ ...need })),
1311
+ qualityGate: policy.qualityGate,
1312
+ shipPoint: policy.shipPoint,
1313
+ taskCount,
1314
+ };
1315
+ }
1316
+
1317
+ function normalizeVerdict(verdict) {
1318
+ if (CANONICAL_VERDICTS.includes(verdict)) return { verdict, diagnostic: null };
1319
+ if (LEGACY_DIAGNOSTIC_VERDICTS.includes(verdict)) return { verdict: 'BLOCKED', diagnostic: verdict };
1320
+ throw new TypeError(`Unsupported canonical verdict: ${String(verdict)}`);
1321
+ }
1322
+
1323
+ function unfinishedVerdictDecision(verdict, context = {}, diagnostic = null) {
1324
+ const action = diagnostic === 'PARTIAL'
1325
+ ? 'continue-verification'
1326
+ : diagnostic === 'NO_TESTS'
1327
+ ? 'configure-or-run-tests'
1328
+ : verdict === 'FAIL'
1329
+ ? 'fix-and-rerun'
1330
+ : 'stop';
1331
+ return {
1332
+ verdict,
1333
+ ...(diagnostic ? { diagnostic } : {}),
1334
+ action,
1335
+ completion: 'unfinished',
1336
+ terminal: verdict !== 'FAIL',
1337
+ unfinished: true,
1338
+ retry: verdict === 'FAIL',
1339
+ blocker: context.blocker || (
1340
+ diagnostic === 'PARTIAL'
1341
+ ? 'verification is partial; required proof remains unfinished'
1342
+ : diagnostic === 'NO_TESTS'
1343
+ ? 'no tests were executed'
1344
+ : verdict === 'FAIL'
1345
+ ? 'verification returned FAIL'
1346
+ : 'verification is blocked; resolve the blocker before retrying'
1347
+ ),
1348
+ };
1349
+ }
1350
+
1351
+ function adaptVerdict(verdict, context = {}) {
1352
+ const normalized = normalizeVerdict(verdict);
1353
+ if (normalized.verdict === 'PASS' || normalized.verdict === 'PASS_WITH_WARNINGS') {
1354
+ return {
1355
+ verdict: normalized.verdict,
1356
+ ...(normalized.diagnostic ? { diagnostic: normalized.diagnostic } : {}),
1357
+ action: normalized.verdict === 'PASS_WITH_WARNINGS' ? 'review-passed-with-warnings' : 'review-passed',
1358
+ completion: 'unfinished',
1359
+ terminal: true,
1360
+ unfinished: true,
1361
+ retry: false,
1362
+ blocker: context.blocker || 'completion decision requires canonical execution proof and workflow obligations',
1363
+ };
1364
+ }
1365
+ return unfinishedVerdictDecision(normalized.verdict, context, normalized.diagnostic);
1366
+ }
1367
+
1368
+ function workflowPolicyObligations(context) {
1369
+ if (!context || typeof context !== 'object' || Array.isArray(context)) return null;
1370
+ const policy = hasOwn(context, 'workflow_policy')
1371
+ ? context.workflow_policy
1372
+ : hasOwn(context, 'workflowPolicy')
1373
+ ? context.workflowPolicy
1374
+ : hasOwn(context, 'policy')
1375
+ ? context.policy
1376
+ : context.task_context && typeof context.task_context === 'object'
1377
+ ? (hasOwn(context.task_context, 'workflow_policy')
1378
+ ? context.task_context.workflow_policy
1379
+ : hasOwn(context.task_context, 'workflowPolicy')
1380
+ ? context.task_context.workflowPolicy
1381
+ : null)
1382
+ : null;
1383
+ if (!validateWorkflowPolicySnapshot(policy).valid) return null;
1384
+ return [...readWorkflowPolicySnapshot({ workflow_policy: policy }).proof_obligations];
1385
+ }
1386
+
1387
+ function executionReceiptFromContext(context) {
1388
+ const candidates = [
1389
+ context?.execution_receipt,
1390
+ context?.executionReceipt,
1391
+ context?.canonical_execution_receipt,
1392
+ context?.canonicalExecutionReceipt,
1393
+ context?.execution?.receipt,
1394
+ context?.execution?.body,
1395
+ context?.receipt,
1396
+ ];
1397
+ return candidates.find((value) => typeof value === 'string' && value.trim() !== '') || null;
1398
+ }
1399
+
1400
+ function proofContainers(context) {
1401
+ return [
1402
+ context?.proofs,
1403
+ context?.workflow_proofs,
1404
+ context?.workflowProofs,
1405
+ context?.evidence,
1406
+ context?.obligation_evidence,
1407
+ context?.obligationEvidence,
1408
+ ].filter((value) => value && typeof value === 'object' && !Array.isArray(value));
1409
+ }
1410
+
1411
+ function concreteEvidence(value) {
1412
+ if (typeof value === 'string') return value.trim() !== '' && !isPlaceholderToken(value);
1413
+ if (Array.isArray(value)) return value.some(concreteEvidence);
1414
+ if (!value || typeof value !== 'object') return false;
1415
+ return ['evidence', 'proof', 'receipt', 'report', 'summary']
1416
+ .some((key) => concreteEvidence(value[key]));
1417
+ }
1418
+
1419
+ function evidenceForObligation(context, obligation) {
1420
+ for (const container of proofContainers(context)) {
1421
+ if (Object.prototype.hasOwnProperty.call(container, obligation) && concreteEvidence(container[obligation])) {
1422
+ return container[obligation];
1423
+ }
1424
+ }
1425
+ const aliases = {
1426
+ needsInspection: ['inspection', 'inspectionEvidence', 'inspection_evidence'],
1427
+ needsDurableTaskState: ['durableTaskState', 'durable_task_state', 'taskState', 'task_state'],
1428
+ needsResearchGrounding: ['research', 'researchGrounding', 'research_grounding'],
1429
+ };
1430
+ for (const key of aliases[obligation] || []) {
1431
+ if (concreteEvidence(context?.[key])) return context[key];
1432
+ }
1433
+ return null;
1434
+ }
1435
+
1436
+ function independentAuditEvidence(context) {
1437
+ const runtimeContext = runtimeContextFromContext(context);
1438
+ const expectedProvenance = runtimeContext
1439
+ ? { base: runtimeContext.base, head: runtimeContext.head }
1440
+ : null;
1441
+ const candidates = [
1442
+ ...proofContainers(context).map((container) => container.needsIndependentAudit),
1443
+ context?.independentAudit,
1444
+ context?.independent_audit,
1445
+ context?.audit,
1446
+ ];
1447
+ return candidates.find((value) => {
1448
+ if (Array.isArray(value)) return false;
1449
+ return validateIndependentAuditEvidence(value, expectedProvenance, runtimeContext).valid;
1450
+ }) || null;
1451
+ }
1452
+
1453
+ function validateIndependentAuditEvidence(value, expectedProvenance, runtimeContext) {
1454
+ const errors = [];
1455
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1456
+ return { valid: false, errors: ['audit must be an object'] };
1457
+ }
1458
+ if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...INDEPENDENT_AUDIT_FIELDS].sort())) {
1459
+ errors.push(`audit fields must be exactly ${INDEPENDENT_AUDIT_FIELDS.join(', ')}`);
1460
+ }
1461
+ if (value.schema_version !== INDEPENDENT_AUDIT_SCHEMA_VERSION) {
1462
+ errors.push(`audit.schema_version must be "${INDEPENDENT_AUDIT_SCHEMA_VERSION}"`);
1463
+ }
1464
+ for (const field of ['reviewer_session_id', 'implementation_session_id']) {
1465
+ if (typeof value[field] !== 'string' || isPlaceholderToken(value[field])) {
1466
+ errors.push(`audit.${field} must be concrete`);
1467
+ }
1468
+ }
1469
+ if (typeof value.reviewer_session_id === 'string'
1470
+ && typeof value.implementation_session_id === 'string'
1471
+ && value.reviewer_session_id.trim() === value.implementation_session_id.trim()) {
1472
+ errors.push('audit reviewer and implementation sessions must be distinct');
1473
+ }
1474
+ if (typeof value.evidence !== 'string' || isPlaceholderToken(value.evidence)) {
1475
+ errors.push('audit.evidence must be concrete');
1476
+ }
1477
+ if (value.verdict !== 'PASS') errors.push('audit.verdict must be literal PASS');
1478
+
1479
+ if (!PROVENANCE.isTrustedRuntimeContext(runtimeContext)) {
1480
+ errors.push('audit runtime provenance/session context is unavailable');
1481
+ } else if (typeof value.implementation_session_id === 'string'
1482
+ && value.implementation_session_id.trim() !== runtimeContext.runtime_session) {
1483
+ errors.push('audit implementation session does not match the runtime session');
1484
+ }
1485
+
1486
+ if (!value.expected_provenance
1487
+ || typeof value.expected_provenance !== 'object'
1488
+ || Array.isArray(value.expected_provenance)
1489
+ || JSON.stringify(Object.keys(value.expected_provenance).sort()) !== JSON.stringify(['base', 'head'])) {
1490
+ errors.push('audit.expected_provenance must contain exactly base and head');
1491
+ }
1492
+ const actualBinding = expectedProvenanceFromSource(value.expected_provenance);
1493
+ const runtimeBinding = PROVENANCE.isTrustedRuntimeContext(runtimeContext)
1494
+ ? { base: runtimeContext.base, head: runtimeContext.head }
1495
+ : null;
1496
+ if (!actualBinding
1497
+ || !isConcreteProvenanceValue(actualBinding.base)
1498
+ || !isConcreteProvenanceValue(actualBinding.head)
1499
+ || !expectedProvenance
1500
+ || !isConcreteProvenanceValue(expectedProvenance.base)
1501
+ || !isConcreteProvenanceValue(expectedProvenance.head)
1502
+ || normalizeProvenanceValue(actualBinding.base) !== normalizeProvenanceValue(expectedProvenance.base)
1503
+ || normalizeProvenanceValue(actualBinding.head) !== normalizeProvenanceValue(expectedProvenance.head)) {
1504
+ errors.push('audit expected Base and Head binding does not match the runtime binding');
1505
+ }
1506
+ if (runtimeBinding
1507
+ && (normalizeProvenanceValue(actualBinding?.base) !== runtimeBinding.base
1508
+ || normalizeProvenanceValue(actualBinding?.head) !== runtimeBinding.head)) {
1509
+ errors.push('audit expected Base and Head do not match the freshly derived runtime binding');
1510
+ }
1511
+ return { valid: errors.length === 0, errors };
1512
+ }
1513
+
1514
+ function completionReceiptOptions(context) {
1515
+ const task = context?.task_context || context?.taskContext || context?.task || context;
1516
+ const runtimeContext = runtimeContextFromContext(context);
1517
+ const derived = receiptValidatorOptions(task && typeof task === 'object' ? task : {}, {
1518
+ requireProvenanceBinding: true,
1519
+ runtimeContext,
1520
+ });
1521
+ const supplied = context?.receipt_options || context?.receiptOptions;
1522
+ if (!supplied || typeof supplied !== 'object' || Array.isArray(supplied)) return derived;
1523
+ return {
1524
+ ...derived,
1525
+ ...supplied,
1526
+ artifactDeclarationValid: derived.artifactDeclarationValid === false
1527
+ ? false
1528
+ : supplied.artifactDeclarationValid !== false,
1529
+ requireArtifactHash: derived.requireArtifactHash === true || supplied.requireArtifactHash === true,
1530
+ artifactPaths: derived.artifactPaths.length > 0 ? derived.artifactPaths : (Array.isArray(supplied.artifactPaths) ? supplied.artifactPaths : []),
1531
+ artifactRoot: derived.artifactRoot,
1532
+ verifyArtifactBytes: derived.verifyArtifactBytes === true || supplied.verifyArtifactBytes === true,
1533
+ expectedProvenance: derived.expectedProvenance,
1534
+ requireProvenanceBinding: true,
1535
+ };
1536
+ }
1537
+
1538
+ function runtimeBoundProofsFromSpec(spec, runtimeContext) {
1539
+ // Spec files are worker-writable: plain proof strings are never execution
1540
+ // evidence. Only canonical receipts bound to this fresh runtime may enter
1541
+ // the completion decision; independent audit remains a strict object schema.
1542
+ const proofSources = [
1543
+ 'completion_proofs',
1544
+ 'completionProofs',
1545
+ 'workflow_proofs',
1546
+ 'workflowProofs',
1547
+ 'obligation_evidence',
1548
+ 'obligationEvidence',
1549
+ 'proofs',
1550
+ ];
1551
+ const sourceKey = proofSources.find((key) => spec && spec[key] && typeof spec[key] === 'object' && !Array.isArray(spec[key]));
1552
+ if (!sourceKey) return null;
1553
+ const source = spec[sourceKey];
1554
+ const trusted = {};
1555
+ const receiptOptions = receiptValidatorOptions({}, {
1556
+ runtimeContext,
1557
+ requireProvenanceBinding: true,
1558
+ });
1559
+ for (const [obligation, value] of Object.entries(source)) {
1560
+ if (obligation === 'needsIndependentAudit') {
1561
+ trusted[obligation] = value;
1562
+ continue;
1563
+ }
1564
+ const candidates = value && typeof value === 'object' && !Array.isArray(value)
1565
+ ? [value.receipt, value.execution_receipt, value.executionReceipt]
1566
+ : [];
1567
+ const receipt = candidates.find((candidate) => (
1568
+ typeof candidate === 'string'
1569
+ && validateCanonicalReceipt(candidate, receiptOptions).length === 0
1570
+ ));
1571
+ if (receipt) trusted[obligation] = { receipt };
330
1572
  }
331
- if (tier === 'Standard') {
1573
+ return trusted;
1574
+ }
1575
+
1576
+ function completionDecisionForSpec(spec, {
1577
+ runtimeContext,
1578
+ executionReceipt,
1579
+ taskContext,
1580
+ } = {}) {
1581
+ const policy = spec && typeof spec === 'object' ? spec.workflow_policy : null;
1582
+ const policyView = policy ? readWorkflowPolicySnapshot({ workflow_policy: policy }) : null;
1583
+ const context = {
1584
+ workflow_policy: policy,
1585
+ runtime_context: runtimeContext,
1586
+ execution_receipt: executionReceipt,
1587
+ task_context: taskContext,
1588
+ };
1589
+ const trustedProofs = runtimeBoundProofsFromSpec(spec, runtimeContext);
1590
+ if (trustedProofs) context.proofs = trustedProofs;
1591
+ if (policyView && policyView.lane === 'Direct') {
332
1592
  return {
333
- tier,
334
- mode,
335
- delegated: ['code-auditor'],
336
- qualityGate: 'combined-ship-point',
337
- shipPoint: mode === 'full-spec' ? 'final-task' : 'requested-task',
1593
+ completion: 'not_applicable',
1594
+ unfinished: false,
1595
+ missingProof: [],
1596
+ blocker: null,
1597
+ };
1598
+ }
1599
+ return completionDecision('PASS', context);
1600
+ }
1601
+
1602
+ function completionDecision(verdict, context = {}) {
1603
+ if (!context || typeof context !== 'object' || Array.isArray(context)) context = {};
1604
+ const normalized = normalizeVerdict(verdict);
1605
+ if (normalized.verdict === 'PASS_WITH_WARNINGS') {
1606
+ return {
1607
+ verdict: normalized.verdict,
1608
+ action: 'review-passed-with-warnings',
1609
+ completion: 'unfinished',
1610
+ terminal: true,
1611
+ unfinished: true,
1612
+ retry: false,
1613
+ blocker: context.blocker || 'PASS_WITH_WARNINGS is not a completion verdict; literal PASS is required',
1614
+ };
1615
+ }
1616
+ if (normalized.verdict !== 'PASS') {
1617
+ return unfinishedVerdictDecision(normalized.verdict, context, normalized.diagnostic);
1618
+ }
1619
+
1620
+ const runtimeContext = runtimeContextFromContext(context);
1621
+ if (!runtimeContext) {
1622
+ return {
1623
+ verdict: normalized.verdict,
1624
+ action: 'blocked-runtime-provenance',
1625
+ completion: 'unfinished',
1626
+ terminal: true,
1627
+ unfinished: true,
1628
+ retry: false,
1629
+ blocker: 'completion blocked: runtime-derived provenance and session context are unavailable or stale',
1630
+ missingProof: ['runtime_provenance'],
1631
+ };
1632
+ }
1633
+ const decisionContext = { ...context, runtime_context: runtimeContext };
1634
+
1635
+ const missing = [];
1636
+ const receipt = executionReceiptFromContext(decisionContext);
1637
+ const receiptFailures = receipt
1638
+ ? validateCanonicalReceipt(receipt, completionReceiptOptions(decisionContext))
1639
+ : ['verification_state'];
1640
+ if (receiptFailures.length > 0) missing.push(`execution_receipt:${receiptFailures.join(',')}`);
1641
+
1642
+ const obligations = workflowPolicyObligations(decisionContext);
1643
+ if (!obligations) {
1644
+ missing.push('workflow_policy.proof_obligations');
1645
+ } else {
1646
+ for (const obligation of obligations) {
1647
+ if (obligation === 'needsExecutionProof') continue;
1648
+ if (obligation === 'needsIndependentAudit') {
1649
+ if (!independentAuditEvidence(decisionContext)) missing.push(obligation);
1650
+ continue;
1651
+ }
1652
+ if (!evidenceForObligation(decisionContext, obligation)) missing.push(obligation);
1653
+ }
1654
+ }
1655
+ if (missing.length > 0) {
1656
+ return {
1657
+ verdict: normalized.verdict,
1658
+ ...(normalized.diagnostic ? { diagnostic: normalized.diagnostic } : {}),
1659
+ action: 'await-required-proof',
1660
+ completion: 'unfinished',
1661
+ terminal: true,
1662
+ unfinished: true,
1663
+ retry: false,
1664
+ blocker: decisionContext.blocker || `completion blocked: missing ${missing.join(', ')}`,
1665
+ missingProof: missing,
338
1666
  };
339
1667
  }
340
1668
  return {
341
- tier,
342
- mode,
343
- delegated: Array.from({ length: taskCount }, () => [...DEEP_TASK_SEQUENCE]).flat(),
344
- qualityGate: 'per-task-stage-a-b',
345
- shipPoint: 'task',
1669
+ verdict: normalized.verdict,
1670
+ action: 'complete',
1671
+ completion: 'complete',
1672
+ terminal: true,
1673
+ unfinished: false,
1674
+ retry: false,
1675
+ blocker: null,
346
1676
  };
347
1677
  }
348
1678
 
349
- function consumeReviewVerdict(verdict) {
350
- assertVerdict(verdict);
351
- if (verdict === 'PASS') return { action: 'proceed', terminal: false };
352
- if (verdict === 'FAIL') return { action: 'fix-and-rerun', terminal: false };
353
- return { action: 'stop', terminal: true, blocker: 'review returned BLOCKED' };
1679
+ function consumeReviewVerdict(verdict, context = {}) {
1680
+ const result = adaptVerdict(verdict, context);
1681
+ return {
1682
+ ...result,
1683
+ source: 'review',
1684
+ completion: 'unfinished',
1685
+ unfinished: true,
1686
+ createsCompletion: false,
1687
+ completionDecisionRequired: true,
1688
+ action: result.verdict === 'PASS' || result.verdict === 'PASS_WITH_WARNINGS'
1689
+ ? 'review-result-only'
1690
+ : result.action,
1691
+ blocker: result.verdict === 'PASS' || result.verdict === 'PASS_WITH_WARNINGS'
1692
+ ? context.blocker || 'review verdict is not completion proof'
1693
+ : result.blocker,
1694
+ };
354
1695
  }
355
1696
 
356
1697
  function isFlashUnverified(task) {
357
1698
  return task?.status === 'in_progress' && task.receipt === 'FLASH_UNVERIFIED';
358
1699
  }
359
1700
 
1701
+ function isCanonicalFlashUnverified(task) {
1702
+ return isFlashUnverified(task)
1703
+ && task.dependencyBlocked === true
1704
+ && task.unblocks === false
1705
+ && typeof task.blocker === 'string'
1706
+ && !isPlaceholderToken(task.blocker)
1707
+ && !FLASH_PROMOTION_FIELDS.some((field) => hasOwn(task, field));
1708
+ }
1709
+
360
1710
  function isStaleFlashDone(task) {
361
1711
  return task?.status === 'done' && task.receipt === 'FLASH_UNVERIFIED';
362
1712
  }
363
1713
 
364
- function promoteFlashTask(task, verdict, proof = 'Verification: PASS') {
365
- if (verdict !== 'NO_TESTS') assertVerdict(verdict);
1714
+ const SYNC_FINALIZE_CAPABILITY = Symbol('sync-finalize-capability');
1715
+ const trustedFlashPromotions = new WeakMap();
1716
+
1717
+ function flashRuntimeContext(value) {
1718
+ if (!PROVENANCE.isTrustedRuntimeContext(value)) return null;
1719
+ try { return PROVENANCE.recomputeRuntimeContext(value); } catch (_) { return null; }
1720
+ }
1721
+
1722
+ function evaluateFlashPromotion(task, verdict, proof = 'Verification: PASS', capability = null, runtimeContext) {
1723
+ const trustedRuntime = flashRuntimeContext(runtimeContext);
1724
+ if (!trustedRuntime) {
1725
+ return blockedFlashSync(task, 'flash promotion blocked: runtime-derived provenance and session context are unavailable or stale');
1726
+ }
1727
+ const normalized = normalizeVerdict(verdict);
366
1728
  if (!isFlashUnverified(task)) return task;
367
1729
 
368
- if (verdict !== 'PASS') {
1730
+ if (normalized.verdict !== 'PASS') {
369
1731
  return {
370
1732
  ...task,
371
1733
  status: 'in_progress',
372
1734
  receipt: 'FLASH_UNVERIFIED',
373
- blocker: verdict === 'NO_TESTS'
1735
+ blocker: normalized.diagnostic === 'NO_TESTS'
374
1736
  ? 'awaiting test proof from /hapo:test <feature>'
375
- : `verification returned ${verdict}`,
1737
+ : normalized.diagnostic === 'PARTIAL'
1738
+ ? 'verification is partial; awaiting complete test proof'
1739
+ : `verification returned ${normalized.verdict}`,
376
1740
  dependencyBlocked: true,
377
1741
  unblocks: false,
378
1742
  readyForSync: false,
1743
+ flashTransition: 'unverified',
1744
+ promotionReceipt: null,
379
1745
  };
380
1746
  }
381
1747
 
382
- if (typeof proof !== 'string' || !/^Verification:\s*PASS\b/.test(proof.trim())) {
1748
+ if (typeof proof !== 'string') {
383
1749
  throw new TypeError('PASS promotion requires a concrete Verification: PASS receipt');
384
1750
  }
1751
+ const failures = validateCanonicalReceipt(proof, receiptValidatorOptions(task, {
1752
+ requireProvenanceBinding: true,
1753
+ runtimeContext: trustedRuntime,
1754
+ }));
1755
+ if (failures.length > 0) {
1756
+ return {
1757
+ ...task,
1758
+ status: 'in_progress',
1759
+ receipt: 'FLASH_UNVERIFIED',
1760
+ blocker: `canonical receipt required for promotion: missing ${failures.join(', ')}`,
1761
+ dependencyBlocked: true,
1762
+ unblocks: false,
1763
+ readyForSync: false,
1764
+ flashTransition: 'unverified',
1765
+ promotionReceipt: null,
1766
+ };
1767
+ }
385
1768
 
386
- return {
1769
+ const promoted = {
387
1770
  ...task,
388
1771
  status: 'in_progress',
389
1772
  receipt: proof.trim(),
@@ -391,24 +1774,108 @@ function promoteFlashTask(task, verdict, proof = 'Verification: PASS') {
391
1774
  dependencyBlocked: true,
392
1775
  unblocks: false,
393
1776
  readyForSync: true,
1777
+ flashTransition: 'promoted',
1778
+ promotionReceipt: proof.trim(),
394
1779
  };
1780
+ if (capability === SYNC_FINALIZE_CAPABILITY) trustedFlashPromotions.set(promoted, capability);
1781
+ return promoted;
395
1782
  }
396
1783
 
397
- function finalizeFlashTask(task, operation) {
398
- if (operation !== 'sync-finalize') return task;
1784
+ function promoteFlashTask(task, verdict, proof = 'Verification: PASS', runtimeContext) {
1785
+ const candidate = runtimeContext
1786
+ || task?.runtime_context
1787
+ || task?.runtimeContext
1788
+ || task?.receipt_binding?.runtimeContext
1789
+ || task?.receiptBinding?.runtimeContext;
1790
+ return evaluateFlashPromotion(task, verdict, proof, null, candidate);
1791
+ }
1792
+
1793
+ function finalizeFlashTaskInternal(task, operation, capability, runtimeContext) {
1794
+ if (operation !== 'sync-finalize' || capability !== SYNC_FINALIZE_CAPABILITY || trustedFlashPromotions.get(task) !== capability) {
1795
+ return blockedFlashSync(task, 'sync-finalize blocked: only trusted same-operation promotion may finalize');
1796
+ }
399
1797
  if (
400
1798
  task?.status !== 'in_progress'
401
- || !/^Verification:\s*PASS\b/.test(String(task.receipt || '').trim())
402
1799
  || task.readyForSync !== true
403
- ) return task;
1800
+ ) return blockedFlashSync(task, 'sync-finalize blocked: trusted promotion state is incomplete');
1801
+ const receipt = String(task.receipt || '');
1802
+ if (task.flashTransition !== 'promoted' || task.promotionReceipt !== receipt) {
1803
+ return blockedFlashSync(task, 'sync-finalize blocked: trusted promotion state is incomplete');
1804
+ }
1805
+ const failures = validateCanonicalReceipt(receipt, receiptValidatorOptions(task, {
1806
+ requireProvenanceBinding: true,
1807
+ runtimeContext,
1808
+ }));
1809
+ if (failures.length > 0) {
1810
+ return blockedFlashSync(task, `sync-finalize blocked: canonical receipt required (${failures.join(', ')})`);
1811
+ }
404
1812
 
1813
+ trustedFlashPromotions.delete(task);
405
1814
  return {
406
1815
  ...task,
407
1816
  status: 'done',
408
1817
  dependencyBlocked: false,
409
1818
  unblocks: true,
410
1819
  readyForSync: false,
1820
+ flashTransition: 'finalized',
1821
+ };
1822
+ }
1823
+
1824
+ function finalizeFlashTask(task, operation) {
1825
+ if (operation !== 'sync-finalize') return task;
1826
+ return blockedFlashSync(task, 'sync-finalize blocked: only syncFinalizeFlashTask owns finalization');
1827
+ }
1828
+
1829
+ function blockedFlashSync(task, blocker) {
1830
+ return {
1831
+ ...task,
1832
+ status: 'in_progress',
1833
+ receipt: 'FLASH_UNVERIFIED',
1834
+ dependencyBlocked: true,
1835
+ unblocks: false,
1836
+ readyForSync: false,
1837
+ flashTransition: 'unverified',
1838
+ promotionReceipt: null,
1839
+ blocker,
1840
+ };
1841
+ }
1842
+
1843
+ function syncFinalizeFlashTask(task, verdict, proof, runtimeContext) {
1844
+ if (!isCanonicalFlashUnverified(task)) {
1845
+ return blockedFlashSync(
1846
+ task,
1847
+ 'sync-finalize blocked: current task must be FLASH_UNVERIFIED in the exact canonical stored state; caller promotion state is not trusted',
1848
+ );
1849
+ }
1850
+ if (verdict !== 'PASS') {
1851
+ return blockedFlashSync(task, 'sync-finalize blocked: explicit PASS verdict is required');
1852
+ }
1853
+ if (typeof proof !== 'string' || proof.trim() === '') {
1854
+ return blockedFlashSync(task, 'sync-finalize blocked: explicit canonical --proof is required');
1855
+ }
1856
+
1857
+ // Treat all promotion fields from --task-json as untrusted. Rebuild the
1858
+ // unverified state, then derive promotion and finalization in this operation.
1859
+ const current = {
1860
+ ...task,
1861
+ readyForSync: false,
1862
+ flashTransition: 'unverified',
1863
+ promotionReceipt: null,
1864
+ dependencyBlocked: true,
1865
+ unblocks: false,
411
1866
  };
1867
+ const candidate = runtimeContext
1868
+ || task?.runtime_context
1869
+ || task?.runtimeContext
1870
+ || task?.receipt_binding?.runtimeContext
1871
+ || task?.receiptBinding?.runtimeContext;
1872
+ const trustedRuntime = flashRuntimeContext(candidate);
1873
+ if (!trustedRuntime) {
1874
+ return blockedFlashSync(task, 'sync-finalize blocked: runtime-derived provenance and session context are unavailable or stale');
1875
+ }
1876
+ const promoted = evaluateFlashPromotion(current, 'PASS', proof, SYNC_FINALIZE_CAPABILITY, trustedRuntime);
1877
+ if (promoted.readyForSync !== true) return promoted;
1878
+ return finalizeFlashTaskInternal(promoted, 'sync-finalize', SYNC_FINALIZE_CAPABILITY, trustedRuntime);
412
1879
  }
413
1880
 
414
1881
  function flashState(taskRegistry = {}) {
@@ -420,38 +1887,157 @@ function flashState(taskRegistry = {}) {
420
1887
  function validateCanonicalReceipt(body, options = {}) {
421
1888
  if (typeof body !== 'string') return ['verification_state'];
422
1889
  const failures = [];
1890
+ const addFailure = (failure) => {
1891
+ if (!failures.includes(failure)) failures.push(failure);
1892
+ };
1893
+ // Fail-closed on placeholders anywhere — provenance, command, or artifact must not be templated
1894
+ if (/\{\{[^}]+\}\}/.test(body)) {
1895
+ addFailure('placeholder');
1896
+ // placeholder also implies missing concrete provenance/command; keep explicit failures for mapping
1897
+ }
1898
+ // Explicit failure outcome anywhere in body must make canonical validator fail (structured only)
1899
+ if (hasExplicitFailure(body)) {
1900
+ addFailure('verification_state');
1901
+ }
423
1902
  // Unambiguous verification state: must be Verification: PASS exactly
424
1903
  if (!/^\s*Verification:\s*PASS\s*$/m.test(body)) {
425
- failures.push('verification_state');
426
- }
427
- // Command must be present
428
- if (!/^\s*Command(?:\(s\))?\s*:/m.test(body)) {
429
- failures.push('command');
430
- }
431
- // Exit or Result must be present with success
432
- if (!/^\s*Exit\s*:|exit\s+code\s*[:=]|\bResult\s*:\s*PASS\b/im.test(body)) {
433
- failures.push('exit_result');
434
- }
435
- // Provenance: must carry both endpoints with non-empty values on same line
436
- // Base: <non-whitespace> plus Head: <non-whitespace>, OR base_sha: <non-whitespace> plus head_sha: <non-whitespace>
437
- const hasBase = /^\s*Base[ \t]*:[ \t]*\S/im.test(body);
438
- const hasHead = /^\s*Head[ \t]*:[ \t]*\S/im.test(body);
439
- const hasBaseSha = /\bbase_sha[ \t]*:[ \t]*\S/im.test(body);
440
- const hasHeadSha = /\bhead_sha[ \t]*:[ \t]*\S/im.test(body);
441
- if (!((hasBase && hasHead) || (hasBaseSha && hasHeadSha))) {
442
- failures.push('provenance');
443
- }
444
- // Artifact hash: if body mentions artifact, require sha256
445
- // For strict mode, require at least one sha256 or artifact reference?
446
- // Requirement says artifact or evidence hash when applicable. We make it optional but if artifact mentioned, require hash.
447
- // For done tasks that produce artifacts, hash should be present. We enforce that if body contains Artifact|artifact, then sha256 must be present.
448
- if (/\bartifact\b/i.test(body) && !/sha256:/i.test(body)) {
449
- failures.push('artifact_hash');
450
- }
451
- // Evidence hash: if no artifact but strict evidence hash required, we could require sha256 generally for done tasks?
452
- // Make it optional for now; but we can require at least one hash when requireArtifactHash true
453
- if (options.requireArtifactHash && !/sha256:/i.test(body)) {
454
- failures.push('artifact_hash');
1904
+ addFailure('verification_state');
1905
+ }
1906
+ // Command must be present with non-empty concrete value on same line, not placeholder
1907
+ const cmdLine = body.split('\n').find((l) => /^\s*Command(?:\(s\))?\s*:/m.test(l)) || null;
1908
+ if (!cmdLine || !/^\s*Command(?:\(s\))?\s*:[ \t]*\S/m.test(cmdLine)) {
1909
+ addFailure('command');
1910
+ } else {
1911
+ const m = cmdLine.match(/^\s*Command(?:\(s\))?\s*:[ \t]*(.*)$/m);
1912
+ const val = m ? m[1].trim() : '';
1913
+ if (isPlaceholderToken(val)) {
1914
+ addFailure('command');
1915
+ } else if (/\{\{[^}]+\}\}/.test(cmdLine)) {
1916
+ addFailure('command');
1917
+ }
1918
+ }
1919
+ // Exit / Result handling: collect all Result lines; if no Exit then need at least one Result and all PASS; if has Exit then every Exit integer 0 and every Result if any all PASS
1920
+ const lines = body.split('\n');
1921
+ const exitValues = [];
1922
+ const resultValues = [];
1923
+ for (const line of lines) {
1924
+ if (/^\s*Exit\s*:/i.test(line)) {
1925
+ const m = line.match(/^\s*Exit\s*:\s*(.*)$/i);
1926
+ exitValues.push(m ? m[1].trim() : '');
1927
+ } else if (/exit\s+code\s*[:=]/i.test(line)) {
1928
+ const m = line.match(/exit\s+code\s*[:=]\s*(.*)$/i);
1929
+ if (m) exitValues.push(m[1].trim());
1930
+ }
1931
+ if (/^\s*Result\s*:/i.test(line)) {
1932
+ const m = line.match(/^\s*Result\s*:\s*(.+?)\s*$/i);
1933
+ resultValues.push(m ? m[1].trim() : '');
1934
+ }
1935
+ }
1936
+
1937
+ if (exitValues.length === 0) {
1938
+ if (resultValues.length === 0 || resultValues.some((v) => v !== 'PASS')) {
1939
+ addFailure('exit_result');
1940
+ }
1941
+ } else {
1942
+ let exitOk = true;
1943
+ for (const v of exitValues) {
1944
+ if (!/^-?\d+$/.test(v)) { exitOk = false; break; }
1945
+ const n = Number.parseInt(v, 10);
1946
+ if (n !== 0) { exitOk = false; break; }
1947
+ }
1948
+ if (!exitOk) addFailure('exit_result');
1949
+ if (resultValues.length > 0 && resultValues.some((v) => v !== 'PASS')) {
1950
+ addFailure('exit_result');
1951
+ }
1952
+ }
1953
+ // Provenance is either a concrete Base/Head pair or a concrete base_sha/head_sha pair.
1954
+ // Without expected values this validates schema only; it does not claim identity binding.
1955
+ const receiptLines = body.split('\n');
1956
+ const readField = (name) => receiptLines
1957
+ .filter((line) => new RegExp(`^\\s*${name}\\s*:`, 'i').test(line))
1958
+ .map((line) => line.replace(new RegExp(`^\\s*${name}\\s*:\\s*`, 'i'), '').trim());
1959
+ const baseValues = readField('Base');
1960
+ const headValues = readField('Head');
1961
+ const baseShaValues = readField('base_sha');
1962
+ const headShaValues = readField('head_sha');
1963
+ const labelStyle = baseValues.length > 0 || headValues.length > 0;
1964
+ const shaStyle = baseShaValues.length > 0 || headShaValues.length > 0;
1965
+ let actualProvenance = null;
1966
+ if (labelStyle === shaStyle || labelStyle && (baseShaValues.length > 0 || headShaValues.length > 0)) {
1967
+ addFailure('provenance');
1968
+ } else {
1969
+ const base = labelStyle ? baseValues : baseShaValues;
1970
+ const head = labelStyle ? headValues : headShaValues;
1971
+ if (base.length !== 1 || head.length !== 1 || !isConcreteProvenanceValue(base[0]) || !isConcreteProvenanceValue(head[0])) {
1972
+ addFailure('provenance');
1973
+ } else {
1974
+ actualProvenance = { base: normalizeProvenanceValue(base[0]), head: normalizeProvenanceValue(head[0]) };
1975
+ }
1976
+ }
1977
+ const expected = options.expectedProvenance || options.expected_provenance || options.expected || null;
1978
+ const expectedBase = expected && typeof expected === 'object'
1979
+ ? firstDefined(expected, ['base', 'Base', 'base_sha', 'baseSha', 'expectedBase', 'expected_base'])
1980
+ : options.expectedBase ?? options.expected_base ?? options.expectedBaseSha ?? options.expected_base_sha;
1981
+ const expectedHead = expected && typeof expected === 'object'
1982
+ ? firstDefined(expected, ['head', 'Head', 'head_sha', 'headSha', 'expectedHead', 'expected_head'])
1983
+ : options.expectedHead ?? options.expected_head ?? options.expectedHeadSha ?? options.expected_head_sha;
1984
+ if (options.requireProvenanceBinding === true || expectedBase !== undefined || expectedHead !== undefined) {
1985
+ if (!actualProvenance
1986
+ || !isConcreteProvenanceValue(expectedBase)
1987
+ || !isConcreteProvenanceValue(expectedHead)
1988
+ || normalizeProvenanceValue(expectedBase) !== actualProvenance.base
1989
+ || normalizeProvenanceValue(expectedHead) !== actualProvenance.head) {
1990
+ addFailure('provenance');
1991
+ }
1992
+ }
1993
+
1994
+ if (options.artifactDeclarationValid === false) addFailure('artifact_declaration');
1995
+ const artifactDeclarations = parseArtifactDeclarations(body);
1996
+ for (const declaration of artifactDeclarations) {
1997
+ if (declaration.hashes.length === 0 || declaration.hashes.some((hash) => !/^[a-f0-9]{64}$/i.test(hash))) {
1998
+ addFailure('artifact_hash');
1999
+ }
2000
+ const declaredPaths = declaration.pathText
2001
+ .split(/\s*(?:\+|,|;|\band\b)\s*/i)
2002
+ .map((value) => value.trim())
2003
+ .filter(Boolean);
2004
+ if (declaredPaths.length > 1 && declaration.hashes.length !== declaredPaths.length) {
2005
+ addFailure('artifact_hash');
2006
+ }
2007
+ }
2008
+ const artifactPaths = Array.isArray(options.artifactPaths) ? options.artifactPaths : [];
2009
+ if (options.requireArtifactHash) {
2010
+ if (artifactDeclarations.length === 0) addFailure('artifact_hash');
2011
+ for (const artifactPath of artifactPaths) {
2012
+ const matches = artifactDeclarations.filter((declaration) => {
2013
+ const candidates = declaration.pathText.split(/\s*(?:\+|,|;|\band\b)\s*/i).map((value) => value.trim()).filter(Boolean);
2014
+ return candidates.length === 1 && candidates.includes(artifactPath);
2015
+ });
2016
+ if (matches.length !== 1 || matches[0].hashes.length !== 1 || !/^[a-f0-9]{64}$/i.test(matches[0].hashes[0])) {
2017
+ addFailure('artifact_hash');
2018
+ }
2019
+ }
2020
+ }
2021
+ if (options.verifyArtifactBytes === true || typeof options.artifactRoot === 'string') {
2022
+ if (typeof options.artifactRoot !== 'string' || options.artifactRoot.trim() === '') {
2023
+ if (artifactDeclarations.length > 0 || options.requireArtifactHash) addFailure('artifact_hash');
2024
+ } else {
2025
+ for (const declaration of artifactDeclarations) {
2026
+ const declaredPaths = declaration.pathText
2027
+ .split(/\s*(?:\+|,|;|\band\b)\s*/i)
2028
+ .map((value) => value.trim())
2029
+ .filter(Boolean);
2030
+ if (declaredPaths.length === 0 || declaration.hashes.length !== declaredPaths.length) {
2031
+ addFailure('artifact_hash');
2032
+ continue;
2033
+ }
2034
+ for (let index = 0; index < declaredPaths.length; index += 1) {
2035
+ if (!hashValidatedArtifact(options.artifactRoot, declaredPaths[index], declaration.hashes[index])) {
2036
+ addFailure('artifact_hash');
2037
+ }
2038
+ }
2039
+ }
2040
+ }
455
2041
  }
456
2042
  return failures;
457
2043
  }
@@ -466,9 +2052,16 @@ function parseCliArgs(argv) {
466
2052
  action: null,
467
2053
  lane: null,
468
2054
  override: null,
469
- proof: 'Verification: PASS',
2055
+ proof: null,
470
2056
  userAuthorized: false,
471
2057
  origin: null,
2058
+ projectRoot: null,
2059
+ specsRoot: null,
2060
+ specFile: null,
2061
+ featureName: null,
2062
+ runtimeSession: null,
2063
+ provenanceMode: null,
2064
+ runtimeContextInput: undefined,
472
2065
  };
473
2066
  const args = [...argv];
474
2067
  for (let i = 0; i < args.length; i += 1) {
@@ -483,6 +2076,9 @@ function parseCliArgs(argv) {
483
2076
  else if (arg === '--sync-finalize') options.action = 'sync-finalize';
484
2077
  else if (arg === '--classify-lane') options.action = 'classify-lane';
485
2078
  else if (arg === '--lane-policy') options.action = 'lane-policy';
2079
+ else if (arg === '--policy-snapshot') options.action = 'policy-snapshot';
2080
+ else if (arg === '--validate-policy') options.action = 'validate-policy';
2081
+ else if (arg === '--completion-decision') options.action = 'completion-decision';
486
2082
  else if (arg === '--approval-state') options.action = 'approval-state';
487
2083
  else if (arg === '--validate-approval') options.action = 'validate-approval';
488
2084
  else if (arg === '--validate-receipt') options.action = 'validate-receipt';
@@ -498,6 +2094,16 @@ function parseCliArgs(argv) {
498
2094
  } else if (arg === '--proof') {
499
2095
  options.proof = args[++i];
500
2096
  if (!options.proof) throw new Error('--proof requires a value');
2097
+ } else if (arg === '--project-root') options.projectRoot = args[++i];
2098
+ else if (arg === '--specs-root') options.specsRoot = args[++i];
2099
+ else if (arg === '--spec-file') options.specFile = args[++i];
2100
+ else if (arg === '--feature-name') options.featureName = args[++i];
2101
+ else if (arg === '--runtime-session' || arg === '--session') options.runtimeSession = args[++i];
2102
+ else if (arg === '--provenance-mode') options.provenanceMode = args[++i];
2103
+ else if (arg === '--runtime-context') {
2104
+ const raw = args[++i];
2105
+ if (!raw) throw new Error('--runtime-context requires a JSON value');
2106
+ options.runtimeContextInput = JSON.parse(raw);
501
2107
  } else {
502
2108
  throw new Error(`Unknown option: ${arg}`);
503
2109
  }
@@ -505,6 +2111,23 @@ function parseCliArgs(argv) {
505
2111
  return options;
506
2112
  }
507
2113
 
2114
+ function cliRuntimeContext(options) {
2115
+ if (options.runtimeContextInput !== undefined) {
2116
+ throw new Error('--runtime-context JSON is not trusted; provide runtime identity flags');
2117
+ }
2118
+ const supplied = [options.projectRoot, options.specsRoot, options.specFile, options.featureName, options.runtimeSession]
2119
+ .some((value) => value !== null && value !== undefined);
2120
+ if (!supplied) return null;
2121
+ return PROVENANCE.deriveRuntimeContext({
2122
+ projectRoot: options.projectRoot,
2123
+ specsRoot: options.specsRoot,
2124
+ specFile: options.specFile,
2125
+ featureName: options.featureName,
2126
+ runtimeSession: options.runtimeSession,
2127
+ provenanceMode: options.provenanceMode || undefined,
2128
+ });
2129
+ }
2130
+
508
2131
  function cliResult(result, json) {
509
2132
  process.stdout.write(json ? `${JSON.stringify(result)}\n` : `${result.message}\n`);
510
2133
  return result.exitCode;
@@ -541,6 +2164,18 @@ function runCli(argv = process.argv.slice(2)) {
541
2164
  const policy = lanePolicy(input);
542
2165
  return cliResult({ ok: true, policy, exitCode: 0, message: `Lane: ${policy.lane}` }, options.json);
543
2166
  }
2167
+ if (options.action === 'policy-snapshot') {
2168
+ if (!options.task) throw new Error('--policy-snapshot requires --task-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);
2172
+ }
2173
+ if (options.action === 'validate-policy') {
2174
+ if (!options.task) throw new Error('--validate-policy requires --task-json');
2175
+ const snapshot = hasOwn(options.task, 'workflow_policy') ? options.task.workflow_policy : options.task;
2176
+ const result = validateWorkflowPolicySnapshot(snapshot);
2177
+ return cliResult({ ok: result.valid, ...result, exitCode: result.valid ? 0 : 2, message: result.valid ? 'Workflow policy snapshot valid' : result.errors.join('; ') }, options.json);
2178
+ }
544
2179
  if (options.action === 'approval-state') {
545
2180
  if (!options.task) throw new Error('--approval-state requires --task-json');
546
2181
  const state = approvalState(options.task);
@@ -554,22 +2189,30 @@ function runCli(argv = process.argv.slice(2)) {
554
2189
  if (options.action === 'validate-receipt') {
555
2190
  if (!options.task) throw new Error('--validate-receipt requires --task-json');
556
2191
  const body = options.task.body || options.task.receipt || '';
557
- const failures = validateCanonicalReceipt(body);
2192
+ const failures = validateCanonicalReceipt(body, receiptValidatorOptions(options.task, { runtimeContext: cliRuntimeContext(options) }));
558
2193
  return cliResult({ ok: failures.length === 0, failures, exitCode: failures.length === 0 ? 0 : 2, message: failures.length === 0 ? 'Receipt valid' : `Receipt missing: ${failures.join(', ')}` }, options.json);
559
2194
  }
560
2195
  if (options.action === 'consume-verdict') {
561
- const result = consumeReviewVerdict(options.verdict);
2196
+ const result = consumeReviewVerdict(options.verdict, options.task || {});
562
2197
  return cliResult({ ok: true, ...result, exitCode: 0, message: `Verdict ${options.verdict}: ${result.action}` }, options.json);
563
2198
  }
2199
+ if (options.action === 'completion-decision') {
2200
+ const runtimeContext = cliRuntimeContext(options);
2201
+ const result = completionDecision(options.verdict, runtimeContext
2202
+ ? { ...(options.task || {}), runtime_context: runtimeContext }
2203
+ : (options.task || {}));
2204
+ return cliResult({ ok: true, ...result, exitCode: 0, message: `Verdict ${options.verdict}: ${result.completion}` }, options.json);
2205
+ }
564
2206
  if (options.action === 'promote-flash') {
565
2207
  if (!options.task) throw new Error('--promote-flash requires --task-json');
566
- const result = promoteFlashTask(options.task, options.verdict, options.proof);
2208
+ const result = promoteFlashTask(options.task, options.verdict, options.proof, cliRuntimeContext(options));
567
2209
  return cliResult({ ok: true, task: result, exitCode: 0, message: 'Flash task promotion evaluated.' }, options.json);
568
2210
  }
569
2211
  if (options.action === 'sync-finalize') {
570
2212
  if (!options.task) throw new Error('--sync-finalize requires --task-json');
571
- const result = finalizeFlashTask(options.task, 'sync-finalize');
572
- return cliResult({ ok: true, task: result, exitCode: 0, message: 'Flash task sync-finalize evaluated.' }, options.json);
2213
+ const result = syncFinalizeFlashTask(options.task, options.verdict, options.proof, cliRuntimeContext(options));
2214
+ const ok = result.status === 'done';
2215
+ return cliResult({ ok, task: result, exitCode: ok ? 0 : 2, message: ok ? 'Flash task sync-finalize evaluated.' : (result.blocker || 'Flash task sync-finalize blocked.') }, options.json);
573
2216
  }
574
2217
 
575
2218
  return cliResult({ ok: true, contract: 'execution-policy', ...policy, exitCode: 0, message: `Execution mode: ${policy.mode}` }, options.json);
@@ -583,26 +2226,68 @@ if (require.main === module) process.exitCode = runCli();
583
2226
 
584
2227
  module.exports = {
585
2228
  REVIEW_VERDICTS,
2229
+ CANONICAL_VERDICTS,
2230
+ LEGACY_DIAGNOSTIC_VERDICTS,
586
2231
  EXECUTION_TIERS,
587
2232
  LANES,
2233
+ PLANNING_DEPTHS,
2234
+ ASSURANCE_LEVELS,
2235
+ ARTIFACT_PROFILES,
2236
+ PLANNING_OBLIGATIONS,
2237
+ PROOF_OBLIGATIONS,
2238
+ WORKFLOW_POLICY_VERSION,
2239
+ WORKFLOW_POLICY_FIELDS,
2240
+ CANONICAL_WORKFLOW_POLICY_VERSION,
2241
+ CANONICAL_WORKFLOW_POLICY_FIELDS,
2242
+ RECEIPT_BINDING_FIELDS,
2243
+ INDEPENDENT_AUDIT_SCHEMA_VERSION,
2244
+ INDEPENDENT_AUDIT_FIELDS,
588
2245
  APPROVAL_SCHEMA_VERSION,
589
2246
  LEGACY_APPROVAL_ERROR,
590
- DEEP_TASK_SEQUENCE,
591
- CRITICAL_LANE_SEQUENCE,
592
2247
  executionPolicy,
593
2248
  classifyLane,
2249
+ compatibilityLane,
2250
+ planningObligationsFor,
2251
+ obligationsForAssurance,
2252
+ obligationsForLane,
2253
+ actorNeedsFor,
2254
+ isWorkflowPolicySnapshot,
2255
+ validateWorkflowPolicySnapshot,
2256
+ assertWorkflowPolicySnapshot,
2257
+ workflowPolicySnapshot,
2258
+ canonicalWorkflowPolicySnapshot,
2259
+ isCanonicalTasklessTerminalSpec,
2260
+ createWorkflowPolicySnapshot,
2261
+ persistWorkflowPolicySnapshot,
2262
+ persistWorkflowPolicy,
2263
+ readWorkflowPolicySnapshot,
2264
+ escalateWorkflowPolicy,
2265
+ policyForSpec,
594
2266
  lanePolicy,
595
2267
  approvalState,
596
2268
  validateApprovalSchema,
597
2269
  isLegacyApprovalObject,
598
- isUserAuthorizedForDowngrade,
2270
+ isTapMetadataHeading,
2271
+ deriveRuntimeContext: PROVENANCE.deriveRuntimeContext,
2272
+ recomputeRuntimeContext: PROVENANCE.recomputeRuntimeContext,
2273
+ isTrustedRuntimeContext: PROVENANCE.isTrustedRuntimeContext,
2274
+ createReceiptBinding,
2275
+ receiptValidatorOptions,
599
2276
  validateCanonicalReceipt,
2277
+ validateIndependentAuditEvidence,
2278
+ completionDecisionForSpec,
600
2279
  delegationPlan,
2280
+ assertVerdict,
2281
+ normalizeVerdict,
2282
+ adaptVerdict,
2283
+ completionDecision,
601
2284
  consumeReviewVerdict,
602
2285
  isFlashUnverified,
2286
+ isCanonicalFlashUnverified,
603
2287
  isStaleFlashDone,
604
2288
  promoteFlashTask,
605
2289
  finalizeFlashTask,
2290
+ syncFinalizeFlashTask,
606
2291
  flashState,
607
2292
  parseCliArgs,
608
2293
  runCli,