immune-brain 3.6.9 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +3 -2
  2. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  3. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +18 -2
  4. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +76 -121
  5. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +106 -600
  6. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +1 -0
  7. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +3 -3
  8. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -43
  9. package/plugins/immune-brain/dist/claude/mcp-server.mjs +7587 -5049
  10. package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +11 -12
  11. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
  12. package/plugins/immune-brain/dist/imm-loop.md +27 -25
  13. package/plugins/immune-brain/dist/imm-planner.md +34 -27
  14. package/plugins/immune-brain/dist/imm-review-retro.md +2 -2
  15. package/plugins/immune-brain/dist/role-prompts/code-review.md +3 -1
  16. package/plugins/immune-brain/dist/role-prompts/executor.md +4 -4
  17. package/plugins/immune-brain/runtime/assurance/coordinator.ts +183 -40
  18. package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +240 -0
  19. package/plugins/immune-brain/runtime/assurance/qa.ts +132 -58
  20. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +15 -7
  21. package/plugins/immune-brain/runtime/assurance/verification.ts +246 -206
  22. package/plugins/immune-brain/runtime/authorization_operation.ts +20 -0
  23. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +288 -721
  24. package/plugins/immune-brain/runtime/commands/kernel.ts +158 -67
  25. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1 -1
  26. package/plugins/immune-brain/runtime/kernel/actor_identity.ts +33 -0
  27. package/plugins/immune-brain/runtime/kernel/application.ts +22 -6
  28. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +94 -5
  29. package/plugins/immune-brain/runtime/kernel/authority_port.ts +27 -6
  30. package/plugins/immune-brain/runtime/kernel/backend_claim.ts +43 -16
  31. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +10 -6
  32. package/plugins/immune-brain/runtime/kernel/canary_application.ts +50 -63
  33. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +13 -4
  34. package/plugins/immune-brain/runtime/kernel/completion.ts +5 -14
  35. package/plugins/immune-brain/runtime/kernel/enrollment.ts +124 -34
  36. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +13 -5
  37. package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
  38. package/plugins/immune-brain/runtime/kernel/intent.ts +7 -11
  39. package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +4 -1
  40. package/plugins/immune-brain/runtime/kernel/legacy_task_record.ts +323 -0
  41. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +10 -1
  42. package/plugins/immune-brain/runtime/kernel/reducer.ts +32 -31
  43. package/plugins/immune-brain/runtime/kernel/run_identity.ts +121 -0
  44. package/plugins/immune-brain/runtime/kernel/spec_binding.ts +100 -0
  45. package/plugins/immune-brain/runtime/kernel/sqlite_migration.ts +950 -0
  46. package/plugins/immune-brain/runtime/kernel/sqlite_store.ts +1193 -0
  47. package/plugins/immune-brain/runtime/kernel/storage.ts +1254 -1206
  48. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +129 -755
  49. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +419 -46
  50. package/plugins/immune-brain/runtime/kernel/types.ts +12 -43
  51. package/plugins/immune-brain/runtime/kernel/validation.ts +60 -274
  52. package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +0 -1
  53. package/plugins/immune-brain/runtime/plan_core.ts +27 -65
  54. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  55. package/plugins/immune-brain/runtime/prompts/code-review.md +3 -1
  56. package/plugins/immune-brain/runtime/prompts/executor.md +4 -4
  57. package/plugins/immune-brain/runtime/staged_intent.ts +58 -0
  58. package/plugins/immune-brain/runtime/unattended/batch_git.ts +37 -7
  59. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +42 -2
  60. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +771 -0
  61. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +189 -0
  62. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +35 -0
  63. package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +33 -0
  64. package/plugins/immune-brain/runtime/unattended/types.ts +14 -1
  65. package/plugins/immune-brain/runtime/v4_runtime.ts +19 -23
  66. package/plugins/immune-brain/runtime/verification_descriptor.ts +92 -136
  67. package/plugins/immune-brain/runtime/workspace_scope.ts +98 -13
  68. package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -3
  69. package/plugins/immune-brain/bin/imm-retire-stale-wrapper +0 -4
  70. package/plugins/immune-brain/bin/imm-retired +0 -4
  71. package/plugins/immune-brain/runtime/authority_commit_receipts.ts +0 -716
  72. package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +0 -451
  73. package/plugins/immune-brain/runtime/kernel/legacy.ts +0 -299
  74. package/plugins/immune-brain/runtime/kernel/observation.ts +0 -397
  75. package/plugins/immune-brain/runtime/kernel/readiness.ts +0 -282
  76. package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +0 -132
@@ -1,8 +1,6 @@
1
1
  import {
2
2
  TASK_ARTIFACT_STATES,
3
3
  TASK_LIFECYCLES,
4
- TASK_PHASES,
5
- TASK_RECORD_CONTRACT_V2,
6
4
  TASK_RECORD_CONTRACT_V3,
7
5
  TASK_RECORD_CONTRACT_V4,
8
6
  REVIEW_REVISION_IDENTITY_CONTRACT,
@@ -15,17 +13,15 @@ import {
15
13
  type FindingSource,
16
14
  type FindingStatus,
17
15
  type TaskAction,
16
+ type ReviewAdvisoryFindingV1,
18
17
  type TaskApprovalV2,
19
18
  type TaskAttestationV3,
20
19
  type AuthorityAuditDescriptor,
21
- type TaskEvidenceV2,
22
20
  type TaskFinding,
23
- type TaskHistoryEntryV2,
24
21
  type TaskHistoryEntryV3,
25
22
  type TaskIntentV1,
26
23
  type ReviewRevisionIdentityV1,
27
24
  type TaskRecord,
28
- type TaskRecordV2,
29
25
  type TaskRecordV3,
30
26
  type TaskRecordV4,
31
27
  } from "./types";
@@ -52,7 +48,7 @@ export class KernelInvariantError extends Error {
52
48
  }
53
49
  }
54
50
 
55
- function objectAt(
51
+ export function objectAt(
56
52
  value: unknown,
57
53
  path: string,
58
54
  violations: string[],
@@ -64,7 +60,7 @@ function objectAt(
64
60
  return value as Record<string, unknown>;
65
61
  }
66
62
 
67
- function rejectUnknown(
63
+ export function rejectUnknown(
68
64
  record: Record<string, unknown>,
69
65
  allowed: readonly string[],
70
66
  path: string,
@@ -78,7 +74,7 @@ function rejectUnknown(
78
74
  }
79
75
  }
80
76
 
81
- function stringAt(
77
+ export function stringAt(
82
78
  value: unknown,
83
79
  path: string,
84
80
  violations: string[],
@@ -90,7 +86,7 @@ function stringAt(
90
86
  return value;
91
87
  }
92
88
 
93
- function positiveInteger(
89
+ export function positiveInteger(
94
90
  value: unknown,
95
91
  path: string,
96
92
  violations: string[],
@@ -120,7 +116,7 @@ function nullablePositiveInteger(
120
116
  return positiveInteger(value, path, violations);
121
117
  }
122
118
 
123
- function enumAt<T extends string>(
119
+ export function enumAt<T extends string>(
124
120
  value: unknown,
125
121
  allowed: readonly T[],
126
122
  path: string,
@@ -133,7 +129,7 @@ function enumAt<T extends string>(
133
129
  return value as T;
134
130
  }
135
131
 
136
- function arrayAt(value: unknown, path: string, violations: string[]): unknown[] {
132
+ export function arrayAt(value: unknown, path: string, violations: string[]): unknown[] {
137
133
  if (!Array.isArray(value)) {
138
134
  violations.push(`${path} must be an array`);
139
135
  return [];
@@ -141,7 +137,7 @@ function arrayAt(value: unknown, path: string, violations: string[]): unknown[]
141
137
  return value;
142
138
  }
143
139
 
144
- function uniqueIds(
140
+ export function uniqueIds(
145
141
  items: Array<{ id: string }>,
146
142
  path: string,
147
143
  violations: string[],
@@ -153,7 +149,7 @@ function uniqueIds(
153
149
  }
154
150
  }
155
151
 
156
- const EVIDENCE_STATUSES: EvidenceStatus[] = ["passed", "failed", "blocked"];
152
+ export const EVIDENCE_STATUSES: EvidenceStatus[] = ["passed", "failed", "blocked"];
157
153
  const FINDING_KINDS: FindingKind[] = [
158
154
  "blocking",
159
155
  "advisory",
@@ -224,7 +220,7 @@ function parseFindingCounterevidence(
224
220
  };
225
221
  }
226
222
 
227
- function parseFinding(
223
+ export function parseFinding(
228
224
  value: unknown,
229
225
  index: number,
230
226
  violations: string[],
@@ -274,53 +270,6 @@ function parseFinding(
274
270
  };
275
271
  }
276
272
 
277
- function parseHistoryV2(
278
- value: unknown,
279
- index: number,
280
- violations: string[],
281
- ): TaskHistoryEntryV2 {
282
- const item = objectAt(value, `record.history[${index}]`, violations);
283
- rejectUnknown(
284
- item,
285
- ["id", "at", "type", "from_phase", "to_phase", "reason", "authority"],
286
- `record.history[${index}]`,
287
- violations,
288
- );
289
- let authority: AuthorityAuditDescriptor | undefined;
290
- if (item.authority !== undefined) {
291
- const auth = objectAt(item.authority, `record.history[${index}].authority`, violations);
292
- rejectUnknown(
293
- auth,
294
- ["authority_kind", "actor_id", "confirmation_ref", "issued_at", "expires_at"],
295
- `record.history[${index}].authority`,
296
- violations,
297
- );
298
- const kind = enumAt(
299
- auth.authority_kind,
300
- ["review", "qa", "user"],
301
- `record.history[${index}].authority.authority_kind`,
302
- violations,
303
- );
304
- authority = {
305
- authority_kind: kind as AuthorityAuditDescriptor["authority_kind"],
306
- actor_id: stringAt(auth.actor_id, `record.history[${index}].authority.actor_id`, violations),
307
- confirmation_ref: stringAt(auth.confirmation_ref, `record.history[${index}].authority.confirmation_ref`, violations),
308
- issued_at: stringAt(auth.issued_at, `record.history[${index}].authority.issued_at`, violations),
309
- expires_at: stringAt(auth.expires_at, `record.history[${index}].authority.expires_at`, violations),
310
- };
311
- }
312
- return {
313
- id: stringAt(item.id, `record.history[${index}].id`, violations),
314
- at: stringAt(item.at, `record.history[${index}].at`, violations),
315
- type: stringAt(item.type, `record.history[${index}].type`, violations),
316
- from_phase: enumAt(item.from_phase, TASK_PHASES, `record.history[${index}].from_phase`, violations),
317
- to_phase: enumAt(item.to_phase, TASK_PHASES, `record.history[${index}].to_phase`, violations),
318
- reason: stringAt(item.reason, `record.history[${index}].reason`, violations),
319
- ...(authority ? { authority } : {}),
320
- };
321
- }
322
-
323
-
324
273
  function parseHistoryV3(
325
274
  value: unknown,
326
275
  index: number,
@@ -352,57 +301,9 @@ function parseHistoryV3(
352
301
  };
353
302
  }
354
303
 
355
- const SHA256_HEX = /^sha256:[a-f0-9]{64}$/;
304
+ export const SHA256_HEX = /^sha256:[a-f0-9]{64}$/;
356
305
 
357
- function parseEvidenceV2(
358
- value: unknown,
359
- index: number,
360
- acceptanceIds: Set<string> | null,
361
- violations: string[],
362
- ): TaskEvidenceV2 {
363
- const item = objectAt(value, `record.evidence[${index}]`, violations);
364
- rejectUnknown(
365
- item,
366
- ["id", "acceptance_id", "task_revision", "intent_content_hash", "diff_hash", "status", "actor_id", "summary"],
367
- `record.evidence[${index}]`,
368
- violations,
369
- );
370
- const acceptanceId = stringAt(
371
- item.acceptance_id,
372
- `record.evidence[${index}].acceptance_id`,
373
- violations,
374
- );
375
- if (acceptanceIds && !acceptanceIds.has(acceptanceId))
376
- violations.push(
377
- `evidence ${String(item.id)} references unknown acceptance ${acceptanceId}`,
378
- );
379
- const intentContentHash = stringAt(
380
- item.intent_content_hash,
381
- `record.evidence[${index}].intent_content_hash`,
382
- violations,
383
- );
384
- if (!SHA256_HEX.test(intentContentHash))
385
- violations.push(`record.evidence[${index}].intent_content_hash must be sha256:<64 hex>`);
386
- const diffHash = stringAt(item.diff_hash, `record.evidence[${index}].diff_hash`, violations);
387
- if (!SHA256_HEX.test(diffHash))
388
- violations.push(`record.evidence[${index}].diff_hash must be sha256:<64 hex>`);
389
- return {
390
- id: stringAt(item.id, `record.evidence[${index}].id`, violations),
391
- acceptance_id: acceptanceId,
392
- task_revision: positiveInteger(
393
- item.task_revision,
394
- `record.evidence[${index}].task_revision`,
395
- violations,
396
- ),
397
- intent_content_hash: intentContentHash,
398
- diff_hash: diffHash,
399
- status: enumAt(item.status, EVIDENCE_STATUSES, `record.evidence[${index}].status`, violations),
400
- actor_id: stringAt(item.actor_id, `record.evidence[${index}].actor_id`, violations),
401
- summary: stringAt(item.summary, `record.evidence[${index}].summary`, violations),
402
- };
403
- }
404
-
405
- function parseApprovalV2(
306
+ export function parseApprovalV2(
406
307
  value: unknown,
407
308
  index: number,
408
309
  violations: string[],
@@ -411,7 +312,7 @@ function parseApprovalV2(
411
312
  const item = objectAt(value, `record.approvals[${index}]`, violations);
412
313
  rejectUnknown(
413
314
  item,
414
- ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary",
315
+ ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "advisory_findings",
415
316
  ...(allowReviewRevision ? ["review_revision"] : [])],
416
317
  `record.approvals[${index}]`,
417
318
  violations,
@@ -447,6 +348,37 @@ function parseApprovalV2(
447
348
  ...(allowReviewRevision && item.review_revision !== undefined
448
349
  ? { review_revision: parseReviewRevisionIdentity(item.review_revision, `record.approvals[${index}].review_revision`, violations) }
449
350
  : {}),
351
+ ...(item.advisory_findings !== undefined
352
+ ? {
353
+ advisory_findings: arrayAt(
354
+ item.advisory_findings,
355
+ `record.approvals[${index}].advisory_findings`,
356
+ violations,
357
+ ).map((entry, advisoryIndex) =>
358
+ parseAdvisoryFinding(
359
+ entry,
360
+ `record.approvals[${index}].advisory_findings[${advisoryIndex}]`,
361
+ violations,
362
+ ),
363
+ ),
364
+ }
365
+ : {}),
366
+ };
367
+ }
368
+
369
+ /**
370
+ * An advisory is recorded inside the review attestation, so it needs the same
371
+ * machine-checkable provenance a blocking finding carries but no lifecycle.
372
+ */
373
+ function parseAdvisoryFinding(value: unknown, path: string, violations: string[]): ReviewAdvisoryFindingV1 {
374
+ const item = objectAt(value, path, violations);
375
+ rejectUnknown(item, ["id", "acceptance_id", "summary", "anchor", "evidence"], path, violations);
376
+ return {
377
+ id: stringAt(item.id, `${path}.id`, violations),
378
+ acceptance_id: nullableString(item.acceptance_id, `${path}.acceptance_id`, violations),
379
+ summary: stringAt(item.summary, `${path}.summary`, violations),
380
+ anchor: item.anchor === undefined ? null : nullableAnchor(item.anchor, `${path}.anchor`, violations),
381
+ evidence: parseFindingEvidence(item.evidence ?? null, `${path}.evidence`, violations),
450
382
  };
451
383
  }
452
384
 
@@ -485,7 +417,7 @@ function parseAttestationV3(
485
417
  const item = objectAt(value, path, violations);
486
418
  rejectUnknown(
487
419
  item,
488
- ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "acceptance_results",
420
+ ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "acceptance_results", "advisory_findings",
489
421
  ...(allowReviewRevision ? ["review_revision"] : [])],
490
422
  path,
491
423
  violations,
@@ -521,6 +453,13 @@ function parseAttestationV3(
521
453
  violations.push(`${path}.review_revision is only valid on review attestations`);
522
454
  if (allowReviewRevision && kind === "review" && !reviewRevision)
523
455
  violations.push(`${path}.review_revision is required for v4 review attestations`);
456
+ const advisoryFindings = item.advisory_findings === undefined
457
+ ? undefined
458
+ : arrayAt(item.advisory_findings, `${path}.advisory_findings`, violations).map((entry, advisoryIndex) =>
459
+ parseAdvisoryFinding(entry, `${path}.advisory_findings[${advisoryIndex}]`, violations),
460
+ );
461
+ if (advisoryFindings !== undefined && kind !== "review")
462
+ violations.push(`${path}.advisory_findings is only valid on review attestations`);
524
463
  return {
525
464
  id: stringAt(item.id, `${path}.id`, violations),
526
465
  kind,
@@ -532,133 +471,11 @@ function parseAttestationV3(
532
471
  summary: stringAt(item.summary, `${path}.summary`, violations),
533
472
  acceptance_results: acceptanceResults,
534
473
  ...(reviewRevision ? { review_revision: reviewRevision } : {}),
474
+ ...(advisoryFindings !== undefined ? { advisory_findings: advisoryFindings } : {}),
535
475
  };
536
476
  }
537
477
 
538
- export function parseTaskRecordV2(raw: unknown): TaskRecordV2 {
539
- const violations: string[] = [];
540
- const value = objectAt(raw, "record", violations);
541
- rejectUnknown(
542
- value,
543
- ["contract", "task_id", "intent_revision", "intent_snapshot", "intent_ref", "artifact_ref", "phase", "baseline", "evidence", "findings", "approvals", "history"],
544
- "record",
545
- violations,
546
- );
547
- if (value.contract !== TASK_RECORD_CONTRACT_V2)
548
- violations.push(`contract must equal ${TASK_RECORD_CONTRACT_V2}`);
549
-
550
- let snapshot: TaskIntentV1 | null = null;
551
- try {
552
- snapshot = parseTaskIntentV1(value.intent_snapshot);
553
- } catch {
554
- violations.push("record.intent_snapshot must be a valid TaskIntent v1");
555
- }
556
-
557
- const taskId = stringAt(value.task_id, "record.task_id", violations);
558
- const intentRevision = positiveInteger(
559
- value.intent_revision,
560
- "record.intent_revision",
561
- violations,
562
- );
563
-
564
- const refRaw = objectAt(value.intent_ref, "record.intent_ref", violations);
565
- rejectUnknown(refRaw, ["path", "revision", "content_hash"], "record.intent_ref", violations);
566
- const refPath = stringAt(refRaw.path, "record.intent_ref.path", violations);
567
- const refRevision = positiveInteger(
568
- refRaw.revision,
569
- "record.intent_ref.revision",
570
- violations,
571
- );
572
- const refContentHash = stringAt(
573
- refRaw.content_hash,
574
- "record.intent_ref.content_hash",
575
- violations,
576
- );
577
- if (!SHA256_HEX.test(refContentHash))
578
- violations.push("record.intent_ref.content_hash must be sha256:<64 hex>");
579
-
580
- let artifactRef: TaskRecordV2["artifact_ref"];
581
- if (value.artifact_ref !== undefined) {
582
- const artifactRaw = objectAt(value.artifact_ref, "record.artifact_ref", violations);
583
- rejectUnknown(artifactRaw, ["state", "spec_path"], "record.artifact_ref", violations);
584
- const state = enumAt(artifactRaw.state, ["active", "frozen"], "record.artifact_ref.state", violations) as "active" | "frozen";
585
- const specPath = artifactRaw.spec_path === undefined
586
- ? undefined
587
- : stringAt(artifactRaw.spec_path, "record.artifact_ref.spec_path", violations);
588
- if (specPath !== undefined && (!/^docs\/specs\/(?!archive\/)[A-Za-z0-9._/-]+\.spec\.md$/.test(specPath) || specPath.includes("..")))
589
- violations.push("record.artifact_ref.spec_path must be one canonical active Spec path");
590
- artifactRef = { state, ...(specPath === undefined ? {} : { spec_path: specPath }) };
591
- }
592
-
593
- const activeIntentPath = `docs/plans/${taskId}.intent.json`;
594
- const frozenIntentPath = `docs/plans/archive/${taskId}.intent.json`;
595
- if (
596
- snapshot &&
597
- (snapshot.task_id !== taskId ||
598
- snapshot.revision !== intentRevision ||
599
- snapshot.revision !== refRevision ||
600
- (refPath !== activeIntentPath && refPath !== frozenIntentPath))
601
- )
602
- violations.push("intent_snapshot and intent_ref must match record identity");
603
- if (artifactRef?.state === "active" && refPath !== activeIntentPath)
604
- violations.push("active artifact_ref requires the active intent path");
605
- if (artifactRef?.state === "frozen" && refPath !== frozenIntentPath)
606
- violations.push("frozen artifact_ref requires the archived intent path");
607
- if (
608
- snapshot &&
609
- refContentHash !== "" &&
610
- canonicalIntentHash(snapshot) !== refContentHash
611
- )
612
- violations.push("intent_ref.content_hash must equal the snapshot canonical hash");
613
-
614
- const baseline = stringAt(value.baseline, "record.baseline", violations);
615
- if (!SHA256_HEX.test(baseline))
616
- violations.push("record.baseline must be sha256:<64 hex>");
617
-
618
- const acceptanceIds = new Set(
619
- snapshot ? snapshot.acceptance.map((item) => item.id) : [],
620
- );
621
- const evidence = arrayAt(value.evidence, "record.evidence", violations).map(
622
- (item, index) => parseEvidenceV2(item, index, acceptanceIds, violations),
623
- );
624
- const findings = arrayAt(value.findings, "record.findings", violations).map(
625
- (item, index) => parseFinding(item, index, violations),
626
- );
627
- const approvals = arrayAt(value.approvals, "record.approvals", violations).map(
628
- (item, index) => parseApprovalV2(item, index, violations),
629
- );
630
- const history = arrayAt(value.history, "record.history", violations).map(
631
- (item, index) => parseHistoryV2(item, index, violations),
632
- );
633
- uniqueIds(evidence, "record.evidence", violations);
634
- uniqueIds(findings, "record.findings", violations);
635
- uniqueIds(approvals, "record.approvals", violations);
636
- uniqueIds(history, "record.history", violations);
637
-
638
- const phase = enumAt(value.phase, TASK_PHASES, "phase", violations);
639
-
640
- if (violations.length > 0) throw new KernelValidationError(violations);
641
- return {
642
- contract: TASK_RECORD_CONTRACT_V2,
643
- task_id: taskId,
644
- intent_revision: intentRevision,
645
- intent_snapshot: snapshot as TaskIntentV1,
646
- intent_ref: {
647
- path: refPath,
648
- revision: refRevision,
649
- content_hash: refContentHash,
650
- },
651
- ...(artifactRef ? { artifact_ref: artifactRef } : {}),
652
- phase,
653
- baseline,
654
- evidence,
655
- findings,
656
- approvals,
657
- history,
658
- };
659
- }
660
-
661
- function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 | TaskRecordV4 {
478
+ export function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 | TaskRecordV4 {
662
479
  const violations: string[] = [];
663
480
  const value = objectAt(raw, "record", violations);
664
481
  rejectUnknown(
@@ -693,8 +510,8 @@ function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 |
693
510
  violations.push("intent_snapshot and intent_ref must match record identity");
694
511
  if (artifactState === "active" && refPath !== activeIntentPath)
695
512
  violations.push("active artifact_state requires the active intent path");
696
- if (artifactState === "frozen" && refPath !== frozenIntentPath)
697
- violations.push("frozen artifact_state requires the archived intent path");
513
+ if (artifactState === "frozen" && refPath !== activeIntentPath && refPath !== frozenIntentPath)
514
+ violations.push("frozen artifact_state requires the active or archived intent path");
698
515
  if (snapshot && refContentHash !== "" && canonicalIntentHash(snapshot) !== refContentHash)
699
516
  violations.push("intent_ref.content_hash must equal the snapshot canonical hash");
700
517
  if (lifecycle !== "active" && artifactState !== "frozen")
@@ -790,21 +607,17 @@ function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 |
790
607
  return record as TaskRecordV3 | TaskRecordV4;
791
608
  }
792
609
 
793
- /** Strict v3 drain parser: unknown fields and revision identity stay illegal. */
794
- export function parseTaskRecordV3(raw: unknown): TaskRecordV3 {
795
- return parseTaskRecordAtVersion(raw, 3) as TaskRecordV3;
796
- }
797
-
798
610
  /** Strict v4 parser: the Enrollment base commit is mandatory. */
799
611
  export function parseTaskRecordV4(raw: unknown): TaskRecordV4 {
800
612
  return parseTaskRecordAtVersion(raw, 4) as TaskRecordV4;
801
613
  }
802
614
 
803
- /** Version-dispatched parser used by every durable Kernel owner. */
615
+ /** Live TaskRecord parser. The v3 drain window is closed: a non-v4 contract is rejected, not fallen back on. */
804
616
  export function parseTaskRecord(raw: unknown): TaskRecord {
805
617
  const contract = (raw as { contract?: unknown } | null)?.contract;
806
- if (contract === TASK_RECORD_CONTRACT_V4) return parseTaskRecordV4(raw);
807
- return parseTaskRecordV3(raw);
618
+ if (contract !== TASK_RECORD_CONTRACT_V4)
619
+ throw new KernelValidationError([`contract must equal ${TASK_RECORD_CONTRACT_V4}`]);
620
+ return parseTaskRecordV4(raw);
808
621
  }
809
622
 
810
623
  export function assertKernelInvariantsV3(intentRaw: TaskIntentV1, recordRaw: TaskRecord): void {
@@ -823,33 +636,6 @@ export function assertKernelInvariantsV3(intentRaw: TaskIntentV1, recordRaw: Tas
823
636
  if (violations.length > 0) throw new KernelInvariantError(violations);
824
637
  }
825
638
 
826
- export function assertKernelInvariantsV2(
827
- intentRaw: TaskIntentV1,
828
- recordRaw: TaskRecordV2,
829
- ): void {
830
- const intent = parseTaskIntentV1(intentRaw);
831
- const record = parseTaskRecordV2(recordRaw);
832
- const violations: string[] = [];
833
- if (intent.task_id !== record.task_id)
834
- violations.push("intent and record task_id must match");
835
- if (intent.revision !== record.intent_revision)
836
- violations.push("intent revision and record intent_revision must match");
837
- if (canonicalIntentHash(record.intent_snapshot) !== record.intent_ref.content_hash)
838
- violations.push("record intent_ref.content_hash must match its snapshot");
839
- const requiredRole: Record<ApprovalKind, ApprovalAuthorityRole> = {
840
- review: "reviewer",
841
- qa: "qa",
842
- user: "user",
843
- };
844
- for (const approval of record.approvals) {
845
- if (approval.authority_role !== requiredRole[approval.kind])
846
- violations.push(
847
- `approval ${approval.id} kind ${approval.kind} requires authority_role ${requiredRole[approval.kind]}`,
848
- );
849
- }
850
- if (violations.length > 0) throw new KernelInvariantError(violations);
851
- }
852
-
853
639
  // ---------------------------------------------------------------------------
854
640
  // R2C2 TaskAction strict parser and update invariants.
855
641
  // ---------------------------------------------------------------------------
@@ -34,7 +34,6 @@ import { type Stats,
34
34
  openSync,
35
35
  readFileSync,
36
36
  realpathSync,
37
- statSync,
38
37
  } from "node:fs";
39
38
  import { join, resolve, sep } from "node:path";
40
39