immune-brain 2.8.2 → 3.0.1

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 (66) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/package.json +9 -2
  5. package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
  6. package/plugins/immune-brain/.mcp.json +8 -0
  7. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
  9. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
  10. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
  11. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
  12. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
  13. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
  14. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
  15. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
  16. package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
  17. package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
  18. package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
  19. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
  20. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  21. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
  22. package/plugins/immune-brain/dist/imm-loop.md +6 -7
  23. package/plugins/immune-brain/dist/imm-planner.md +1 -1
  24. package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
  25. package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
  26. package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
  27. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
  28. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
  29. package/plugins/immune-brain/hooks/hooks.json +55 -0
  30. package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
  31. package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
  32. package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
  33. package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
  34. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
  35. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
  36. package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
  37. package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
  38. package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
  39. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
  40. package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
  41. package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
  42. package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
  43. package/plugins/immune-brain/runtime/github_issue_tracker.ts +28 -13
  44. package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
  45. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
  46. package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
  47. package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
  48. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
  49. package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
  50. package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
  51. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
  52. package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
  53. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
  54. package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
  55. package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
  56. package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
  57. package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
  58. package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
  59. package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
  60. package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
  61. package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
  62. package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
  63. package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
  64. package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
  65. package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
  66. package/plugins/immune-brain/skills/imm-planner/SKILL.md +2 -0
@@ -6,20 +6,21 @@ import { createHash } from "node:crypto";
6
6
  import { completionDecision } from "./completion";
7
7
  import {
8
8
  REDUCED_MUTATION_BRAND,
9
+ TASK_RECORD_CONTRACT_V4,
9
10
  type AuthorityAuditDescriptor,
10
11
  type ReducedTaskMutation,
11
12
  type TaskAction,
12
13
  type TaskFinding,
13
14
  type TaskIntentRefV3,
14
15
  type TaskIntentV1,
15
- type TaskRecordV3,
16
+ type TaskRecord,
16
17
  } from "./types";
17
18
  import {
18
19
  KernelInvariantError,
19
20
  assertKernelInvariantsV3,
20
21
  assertTaskRecordUpdateV3,
21
22
  parseTaskAction,
22
- parseTaskRecordV3,
23
+ parseTaskRecord,
23
24
  } from "./validation";
24
25
  import { classifyIntentRevision, canonicalIntentHash } from "./intent";
25
26
 
@@ -29,7 +30,7 @@ const RISK_RANK: Record<TaskIntentV1["risk"], number> = {
29
30
  critical: 2,
30
31
  };
31
32
 
32
- function transitionLifecycle(record: TaskRecordV3, to: "done" | "stopped"): void {
33
+ function transitionLifecycle(record: TaskRecord, to: "done" | "stopped"): void {
33
34
  if (record.lifecycle !== "active")
34
35
  throw new KernelInvariantError([
35
36
  `illegal lifecycle transition: ${record.lifecycle} -> ${to}`,
@@ -37,7 +38,7 @@ function transitionLifecycle(record: TaskRecordV3, to: "done" | "stopped"): void
37
38
  record.lifecycle = to;
38
39
  }
39
40
 
40
- function stateOf(record: TaskRecordV3): string {
41
+ function stateOf(record: TaskRecord): string {
41
42
  return `${record.lifecycle}:${record.artifact_state}`;
42
43
  }
43
44
 
@@ -54,7 +55,7 @@ function stableJson(value: unknown): string {
54
55
  return primitive === undefined ? "null" : primitive;
55
56
  }
56
57
 
57
- export function canonicalRecordHash(record: TaskRecordV3): string {
58
+ export function canonicalRecordHash(record: TaskRecord): string {
58
59
  // Must equal the storage CAS revision: the exact serialized file bytes.
59
60
  return `sha256:${createHash("sha256")
60
61
  .update(`${JSON.stringify(record, null, 2)}\n`)
@@ -99,7 +100,7 @@ export function recordedActionFingerprint(reason: string | null): string | null
99
100
  return matched?.[1] ?? null;
100
101
  }
101
102
 
102
- function copyRecord(record: TaskRecordV3): TaskRecordV3 {
103
+ function copyRecord(record: TaskRecord): TaskRecord {
103
104
  return {
104
105
  ...record,
105
106
  intent_snapshot: { ...record.intent_snapshot },
@@ -113,7 +114,7 @@ function copyRecord(record: TaskRecordV3): TaskRecordV3 {
113
114
  };
114
115
  }
115
116
 
116
- function reviewRound(record: TaskRecordV3): number {
117
+ function reviewRound(record: TaskRecord): number {
117
118
  return (
118
119
  Math.max(
119
120
  0,
@@ -125,7 +126,7 @@ function reviewRound(record: TaskRecordV3): number {
125
126
  }
126
127
 
127
128
  function appendHistory(
128
- record: TaskRecordV3,
129
+ record: TaskRecord,
129
130
  action: TaskAction,
130
131
  from: string,
131
132
  detail: string | null,
@@ -200,11 +201,12 @@ export function findingsDigestV2(findings: TaskFinding[]): string {
200
201
  }
201
202
 
202
203
  export function reduceTask(
203
- recordRaw: TaskRecordV3,
204
+ recordRaw: TaskRecord,
204
205
  actionRaw: TaskAction,
205
206
  authorityAudit: AuthorityAuditDescriptor | null = null,
207
+ changedPaths?: readonly string[],
206
208
  ): ReducedTaskMutation {
207
- const previous = parseTaskRecordV3(recordRaw);
209
+ const previous = parseTaskRecord(recordRaw);
208
210
  assertKernelInvariantsV3(previous.intent_snapshot, previous);
209
211
  const action = parseTaskAction(actionRaw);
210
212
  const record = copyRecord(previous);
@@ -343,8 +345,23 @@ export function reduceTask(
343
345
  throw new KernelInvariantError([
344
346
  `attestations contains duplicate id ${approval.id}`,
345
347
  ]);
348
+ // A v4 review attestation must pin the exact immutable revision the
349
+ // reviewer analyzed; nothing else may carry that identity.
350
+ const reviewRevision = approval.review_revision;
351
+ if (reviewRevision && approval.kind !== "review")
352
+ throw new KernelInvariantError(["review_revision is only valid on review approvals"]);
353
+ if (record.contract !== TASK_RECORD_CONTRACT_V4) {
354
+ if (reviewRevision)
355
+ throw new KernelInvariantError(["review_revision requires a TaskRecord v4"]);
356
+ } else if (approval.kind === "review") {
357
+ if (!reviewRevision)
358
+ throw new KernelInvariantError(["v4 review approval requires review_revision"]);
359
+ if (reviewRevision.base_head !== record.git_base_head)
360
+ throw new KernelInvariantError(["review_revision.base_head must equal the Enrollment git_base_head"]);
361
+ }
362
+ const { review_revision: storedReviewRevision, ...approvalWithoutRevision } = approval;
346
363
  record.attestations.push({
347
- ...approval,
364
+ ...approvalWithoutRevision,
348
365
  acceptance_results: approval.kind === "qa"
349
366
  ? record.intent_snapshot.acceptance.map((item) => ({
350
367
  acceptance_id: item.id,
@@ -352,6 +369,7 @@ export function reduceTask(
352
369
  summary: `host-attested QA: ${approval.summary}`,
353
370
  }))
354
371
  : [],
372
+ ...(storedReviewRevision ? { review_revision: storedReviewRevision } : {}),
355
373
  });
356
374
  appendHistory(record, action, from, approval.id, authorityAudit);
357
375
  break;
@@ -380,6 +398,8 @@ export function reduceTask(
380
398
  throw new KernelInvariantError([
381
399
  `attestations contains duplicate id ${approval.id}`,
382
400
  ]);
401
+ if (approval.review_revision)
402
+ throw new KernelInvariantError(["review_revision is only valid on review approvals"]);
383
403
  record.attestations.push({ ...approval, acceptance_results: [] });
384
404
  appendHistory(record, action, from, approval.id, authorityAudit);
385
405
  break;
@@ -518,11 +538,14 @@ export function reduceTask(
518
538
  throw new KernelInvariantError([
519
539
  `cannot complete while state is ${stateOf(record)}`,
520
540
  ]);
541
+ if (!Array.isArray(changedPaths))
542
+ throw new KernelInvariantError(["complete requires trusted changed paths"]);
521
543
  const decision = completionDecision(
522
544
  record.intent_snapshot,
523
545
  record,
524
546
  diffHash,
525
547
  record.intent_ref.content_hash,
548
+ changedPaths,
526
549
  );
527
550
  if (!decision.complete)
528
551
  throw new KernelInvariantError([
@@ -587,12 +610,12 @@ export function reduceTask(
587
610
  return brandResult(record, nextWorkingFor(record));
588
611
  }
589
612
 
590
- function nextWorkingFor(record: TaskRecordV3): string | null {
613
+ function nextWorkingFor(record: TaskRecord): string | null {
591
614
  return record.lifecycle === "active" ? record.task_id : null;
592
615
  }
593
616
 
594
617
  function brandResult(
595
- record: TaskRecordV3,
618
+ record: TaskRecord,
596
619
  nextWorking: string | null,
597
620
  ): ReducedTaskMutation {
598
621
  const target = {} as ReducedTaskMutation;
@@ -33,12 +33,12 @@ import {
33
33
  stateTransactionPath,
34
34
  stateWorkspacePath,
35
35
  } from "./storage_paths";
36
- import { parseTaskRecordV2, parseTaskRecordV3 } from "./validation";
36
+ import { parseTaskRecord, parseTaskRecordV2 } from "./validation";
37
37
  import type {
38
38
  TaskLifecycle,
39
39
  TaskPhase,
40
40
  TaskRecordV2,
41
- TaskRecordV3,
41
+ TaskRecord,
42
42
  StoredTaskMutationV3,
43
43
  V3AuthorityObservation,
44
44
  } from "./types";
@@ -649,7 +649,7 @@ function parseWorkspaceTransactionV2(raw: Record<string, unknown>): WorkspaceTra
649
649
  ...(artifactRelocations.length > 0 ? { artifact_relocations: artifactRelocations } : {}),
650
650
  };
651
651
  validateTaskId(transaction.task_id);
652
- const record = parseTaskRecordV3(
652
+ const record = parseTaskRecord(
653
653
  JSON.parse(transaction.next_record_content),
654
654
  );
655
655
  if (record.task_id !== transaction.task_id)
@@ -736,7 +736,7 @@ function completeTransactionV2Locked(
736
736
  transaction.expected_workspace_hash,
737
737
  transaction.next_workspace_content,
738
738
  );
739
- const record = parseTaskRecordV3(
739
+ const record = parseTaskRecord(
740
740
  JSON.parse(transaction.next_record_content),
741
741
  );
742
742
  const workspace = parseWorkspaceContent(transaction.next_workspace_content);
@@ -791,7 +791,7 @@ function recoverAnyPendingTransactionLocked(root: string): void {
791
791
  export function readTaskRecordRaw(
792
792
  root: string,
793
793
  taskId: string,
794
- ): { revision: string; record: TaskRecordV3 | null } {
794
+ ): { revision: string; record: TaskRecord | null } {
795
795
  validateTaskId(taskId);
796
796
  const relativePath = stateTaskRecordPath(taskId);
797
797
  if (currentRevision(root, relativePath) === MISSING_REVISION)
@@ -802,7 +802,7 @@ export function readTaskRecordRaw(
802
802
  throw new KernelStoreSecurityError(
803
803
  "TaskRecord v2 is not supported in the state layout; v2 records belong to the historical audit layout",
804
804
  );
805
- const record = parseTaskRecordV3(raw);
805
+ const record = parseTaskRecord(raw);
806
806
  if (record.task_id !== taskId)
807
807
  throw new KernelStoreSecurityError("task record v3 identity is inconsistent");
808
808
  return { revision: revisionFor(content), record };
@@ -821,7 +821,7 @@ export function readAuditTaskPair(
821
821
  taskId: string,
822
822
  ): {
823
823
  recordRevision: string;
824
- record: TaskRecordV3 | TaskRecordV2;
824
+ record: TaskRecord | TaskRecordV2;
825
825
  proof: TaskTombstone;
826
826
  } | null {
827
827
  validateTaskId(taskId);
@@ -846,7 +846,7 @@ export function readAuditTaskPair(
846
846
  "terminal audit proof does not match its task record",
847
847
  );
848
848
  const raw = JSON.parse(recordContent) as { contract?: unknown };
849
- let record: TaskRecordV3 | TaskRecordV2;
849
+ let record: TaskRecord | TaskRecordV2;
850
850
  if (raw.contract === "assurance_kernel/task_record/v2") {
851
851
  const legacy = parseTaskRecordV2(raw);
852
852
  if (
@@ -858,7 +858,7 @@ export function readAuditTaskPair(
858
858
  );
859
859
  record = legacy;
860
860
  } else {
861
- const current = parseTaskRecordV3(raw);
861
+ const current = parseTaskRecord(raw);
862
862
  if (
863
863
  current.task_id !== taskId ||
864
864
  (current.lifecycle !== "done" && current.lifecycle !== "stopped")
@@ -874,7 +874,7 @@ export function readAuditTaskPair(
874
874
  export function readTaskRecord(
875
875
  root: string,
876
876
  taskId: string,
877
- ): { revision: string; record: TaskRecordV3 | null } {
877
+ ): { revision: string; record: TaskRecord | null } {
878
878
  return withKernelStoreLock(root, () => readTaskRecordRaw(root, taskId));
879
879
  }
880
880
 
@@ -883,7 +883,7 @@ export function commitTaskRecordLocked(
883
883
  root: string,
884
884
  taskId: string,
885
885
  expectedRecordHash: string,
886
- nextRecord: TaskRecordV3,
886
+ nextRecord: TaskRecord,
887
887
  expectedWorkspaceHash: string,
888
888
  nextWorkspace: WorkspaceState,
889
889
  artifactRelocations: ArtifactRelocationV1[] = [],
@@ -1358,7 +1358,7 @@ export function commitEnrollmentLocked(
1358
1358
  );
1359
1359
  }
1360
1360
  return {
1361
- record: parseTaskRecordV3(JSON.parse(transaction.next_record_content)),
1361
+ record: parseTaskRecord(JSON.parse(transaction.next_record_content)),
1362
1362
  workspace: parseWorkspaceContent(transaction.next_workspace_content),
1363
1363
  };
1364
1364
  }
@@ -1572,7 +1572,7 @@ function parseTerminalMarker(raw: Record<string, unknown>): TerminalMarker {
1572
1572
  expected_claim_sha256: expectedClaim,
1573
1573
  at: raw.at as string,
1574
1574
  };
1575
- const record = parseTaskRecordV3(JSON.parse(marker.audit_record_content));
1575
+ const record = parseTaskRecord(JSON.parse(marker.audit_record_content));
1576
1576
  if (record.task_id !== marker.task_id)
1577
1577
  throw new KernelStoreSecurityError("terminal marker task identity is inconsistent");
1578
1578
  if (record.lifecycle !== "done" && record.lifecycle !== "stopped")
@@ -1709,7 +1709,7 @@ export function commitTerminalLocked(
1709
1709
  taskId: string,
1710
1710
  transaction: WorkspaceTransactionV2,
1711
1711
  tombstone: TaskTombstone,
1712
- ): { record: TaskRecordV3; workspace: WorkspaceState } {
1712
+ ): { record: TaskRecord; workspace: WorkspaceState } {
1713
1713
  validateTaskId(taskId);
1714
1714
  if (transaction.task_id !== taskId)
1715
1715
  throw new KernelStoreSecurityError(
@@ -1728,7 +1728,7 @@ export function commitTerminalLocked(
1728
1728
  throw new KernelStoreSecurityError(
1729
1729
  "terminal proof must match the terminal record bytes",
1730
1730
  );
1731
- const terminalRecord = parseTaskRecordV3(
1731
+ const terminalRecord = parseTaskRecord(
1732
1732
  JSON.parse(transaction.next_record_content) as Record<string, unknown>,
1733
1733
  );
1734
1734
  if (tombstone.terminal_lifecycle !== terminalRecord.lifecycle)
@@ -1774,7 +1774,7 @@ export function commitTerminalLocked(
1774
1774
  );
1775
1775
  }
1776
1776
  return {
1777
- record: parseTaskRecordV3(JSON.parse(transaction.next_record_content)),
1777
+ record: parseTaskRecord(JSON.parse(transaction.next_record_content)),
1778
1778
  workspace: parseWorkspaceContent(transaction.next_workspace_content),
1779
1779
  };
1780
1780
  }
@@ -99,6 +99,9 @@ export interface V3AuthorityObservation {
99
99
  export const TASK_INTENT_CONTRACT_V1 = "assurance_kernel/task_intent/v1" as const;
100
100
  export const TASK_RECORD_CONTRACT_V2 = "assurance_kernel/task_record/v2" as const;
101
101
  export const TASK_RECORD_CONTRACT_V3 = "assurance_kernel/task_record/v3" as const;
102
+ export const TASK_RECORD_CONTRACT_V4 = "assurance_kernel/task_record/v4" as const;
103
+ export const REVIEW_REVISION_IDENTITY_CONTRACT =
104
+ "assurance_kernel/review_revision_identity/v1" as const;
102
105
 
103
106
  export interface TaskIntentAcceptanceItemV1 {
104
107
  id: string;
@@ -148,6 +151,20 @@ export interface TaskApprovalV2 {
148
151
  diff_hash: string;
149
152
  actor_id: string;
150
153
  summary: string;
154
+ /**
155
+ * Immutable Git revision identity the reviewer actually analyzed. Legal only
156
+ * on a review attestation of a TaskRecord v4; the strict parser rejects it
157
+ * anywhere else, so QA and user attestations never carry revision bytes.
158
+ */
159
+ review_revision?: ReviewRevisionIdentityV1;
160
+ }
161
+
162
+ export interface ReviewRevisionIdentityV1 {
163
+ contract: typeof REVIEW_REVISION_IDENTITY_CONTRACT;
164
+ base_head: string;
165
+ review_commit: string;
166
+ review_tree: string;
167
+ manifest_digest: string;
151
168
  }
152
169
 
153
170
  export interface TaskHistoryEntryV2 {
@@ -206,6 +223,19 @@ export interface TaskRecordV3 {
206
223
  history: TaskHistoryEntryV3[];
207
224
  }
208
225
 
226
+ /**
227
+ * TaskRecord v4 binds the Enrollment commit so Review, QA, authorization, and
228
+ * completion can all derive one scoped revision from base to final index tree.
229
+ * `git_base_head` is immutable from Enrollment through terminal settlement.
230
+ */
231
+ export interface TaskRecordV4 extends Omit<TaskRecordV3, "contract"> {
232
+ contract: typeof TASK_RECORD_CONTRACT_V4;
233
+ git_base_head: string;
234
+ }
235
+
236
+ /** The record shape every Kernel owner passes around during the v3 drain window. */
237
+ export type TaskRecord = TaskRecordV3 | TaskRecordV4;
238
+
209
239
  export interface TaskProjectionV3 extends CompletionDecision {
210
240
  contract: "assurance_kernel/projection/v3";
211
241
  task_id: string;
@@ -268,13 +298,13 @@ export const REDUCED_MUTATION_BRAND = Symbol("assurance-kernel-reduced-mutation-
268
298
 
269
299
  export interface ReducedTaskMutation {
270
300
  readonly [REDUCED_MUTATION_BRAND]: true;
271
- readonly record: TaskRecordV3;
301
+ readonly record: TaskRecord;
272
302
  readonly next_workspace_working: string | null;
273
303
  }
274
304
 
275
305
  export interface StoredTaskMutationV3 {
276
306
  revision: string;
277
- record: TaskRecordV3;
307
+ record: TaskRecord;
278
308
  workspace: {
279
309
  revision: string;
280
310
  state: WorkspaceStateLike;
@@ -4,6 +4,8 @@ import {
4
4
  TASK_PHASES,
5
5
  TASK_RECORD_CONTRACT_V2,
6
6
  TASK_RECORD_CONTRACT_V3,
7
+ TASK_RECORD_CONTRACT_V4,
8
+ REVIEW_REVISION_IDENTITY_CONTRACT,
7
9
  type ApprovalAuthorityRole,
8
10
  type ApprovalKind,
9
11
  type EvidenceStatus,
@@ -19,11 +21,16 @@ import {
19
21
  type TaskHistoryEntryV2,
20
22
  type TaskHistoryEntryV3,
21
23
  type TaskIntentV1,
24
+ type ReviewRevisionIdentityV1,
25
+ type TaskRecord,
22
26
  type TaskRecordV2,
23
27
  type TaskRecordV3,
28
+ type TaskRecordV4,
24
29
  } from "./types";
25
30
  import { canonicalIntentHash, parseTaskIntentV1 } from "./intent";
26
31
 
32
+ const GIT_OBJECT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
33
+
27
34
  export class KernelValidationError extends Error {
28
35
  readonly code = "kernel_schema_invalid";
29
36
 
@@ -320,11 +327,13 @@ function parseApprovalV2(
320
327
  value: unknown,
321
328
  index: number,
322
329
  violations: string[],
330
+ allowReviewRevision = false,
323
331
  ): TaskApprovalV2 {
324
332
  const item = objectAt(value, `record.approvals[${index}]`, violations);
325
333
  rejectUnknown(
326
334
  item,
327
- ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary"],
335
+ ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary",
336
+ ...(allowReviewRevision ? ["review_revision"] : [])],
328
337
  `record.approvals[${index}]`,
329
338
  violations,
330
339
  );
@@ -356,18 +365,52 @@ function parseApprovalV2(
356
365
  diff_hash: diffHash,
357
366
  actor_id: stringAt(item.actor_id, `record.approvals[${index}].actor_id`, violations),
358
367
  summary: stringAt(item.summary, `record.approvals[${index}].summary`, violations),
368
+ ...(allowReviewRevision && item.review_revision !== undefined
369
+ ? { review_revision: parseReviewRevisionIdentity(item.review_revision, `record.approvals[${index}].review_revision`, violations) }
370
+ : {}),
359
371
  };
360
372
  }
361
373
 
374
+ function parseReviewRevisionIdentity(
375
+ value: unknown,
376
+ path: string,
377
+ violations: string[],
378
+ ): ReviewRevisionIdentityV1 {
379
+ const item = objectAt(value, path, violations);
380
+ rejectUnknown(item, ["contract", "base_head", "review_commit", "review_tree", "manifest_digest"], path, violations);
381
+ if (item.contract !== REVIEW_REVISION_IDENTITY_CONTRACT)
382
+ violations.push(`${path}.contract must equal ${REVIEW_REVISION_IDENTITY_CONTRACT}`);
383
+ const identity = {
384
+ contract: REVIEW_REVISION_IDENTITY_CONTRACT,
385
+ base_head: stringAt(item.base_head, `${path}.base_head`, violations),
386
+ review_commit: stringAt(item.review_commit, `${path}.review_commit`, violations),
387
+ review_tree: stringAt(item.review_tree, `${path}.review_tree`, violations),
388
+ manifest_digest: stringAt(item.manifest_digest, `${path}.manifest_digest`, violations),
389
+ };
390
+ for (const field of ["base_head", "review_commit", "review_tree"] as const)
391
+ if (!GIT_OBJECT_ID.test(identity[field]))
392
+ violations.push(`${path}.${field} must be a lowercase Git object id`);
393
+ if (!SHA256_HEX.test(identity.manifest_digest))
394
+ violations.push(`${path}.manifest_digest must be sha256:<64 hex>`);
395
+ return identity;
396
+ }
397
+
362
398
  function parseAttestationV3(
363
399
  value: unknown,
364
400
  index: number,
365
401
  acceptanceIds: Set<string>,
366
402
  violations: string[],
403
+ allowReviewRevision = false,
367
404
  ): TaskAttestationV3 {
368
405
  const path = `record.attestations[${index}]`;
369
406
  const item = objectAt(value, path, violations);
370
- rejectUnknown(item, ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "acceptance_results"], path, violations);
407
+ rejectUnknown(
408
+ item,
409
+ ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "acceptance_results",
410
+ ...(allowReviewRevision ? ["review_revision"] : [])],
411
+ path,
412
+ violations,
413
+ );
371
414
  const kind = enumAt(item.kind, APPROVAL_KINDS, `${path}.kind`, violations);
372
415
  const intentContentHash = stringAt(item.intent_content_hash, `${path}.intent_content_hash`, violations);
373
416
  const diffHash = stringAt(item.diff_hash, `${path}.diff_hash`, violations);
@@ -392,6 +435,13 @@ function parseAttestationV3(
392
435
  if (resultIds.length !== acceptanceIds.size || new Set(resultIds).size !== acceptanceIds.size)
393
436
  violations.push(`${path}.acceptance_results must cover every acceptance exactly once`);
394
437
  }
438
+ const reviewRevision = item.review_revision === undefined
439
+ ? undefined
440
+ : parseReviewRevisionIdentity(item.review_revision, `${path}.review_revision`, violations);
441
+ if (reviewRevision && kind !== "review")
442
+ violations.push(`${path}.review_revision is only valid on review attestations`);
443
+ if (allowReviewRevision && kind === "review" && !reviewRevision)
444
+ violations.push(`${path}.review_revision is required for v4 review attestations`);
395
445
  return {
396
446
  id: stringAt(item.id, `${path}.id`, violations),
397
447
  kind,
@@ -402,6 +452,7 @@ function parseAttestationV3(
402
452
  actor_id: stringAt(item.actor_id, `${path}.actor_id`, violations),
403
453
  summary: stringAt(item.summary, `${path}.summary`, violations),
404
454
  acceptance_results: acceptanceResults,
455
+ ...(reviewRevision ? { review_revision: reviewRevision } : {}),
405
456
  };
406
457
  }
407
458
 
@@ -528,17 +579,19 @@ export function parseTaskRecordV2(raw: unknown): TaskRecordV2 {
528
579
  };
529
580
  }
530
581
 
531
- export function parseTaskRecordV3(raw: unknown): TaskRecordV3 {
582
+ function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 | TaskRecordV4 {
532
583
  const violations: string[] = [];
533
584
  const value = objectAt(raw, "record", violations);
534
585
  rejectUnknown(
535
586
  value,
536
- ["contract", "task_id", "intent_snapshot", "intent_ref", "lifecycle", "artifact_state", "baseline", "attestations", "findings", "history"],
587
+ ["contract", "task_id", "intent_snapshot", "intent_ref", "lifecycle", "artifact_state", "baseline", "attestations", "findings", "history",
588
+ ...(version === 4 ? ["git_base_head"] : [])],
537
589
  "record",
538
590
  violations,
539
591
  );
540
- if (value.contract !== TASK_RECORD_CONTRACT_V3)
541
- violations.push(`contract must equal ${TASK_RECORD_CONTRACT_V3}`);
592
+ const expectedContract = version === 4 ? TASK_RECORD_CONTRACT_V4 : TASK_RECORD_CONTRACT_V3;
593
+ if (value.contract !== expectedContract)
594
+ violations.push(`contract must equal ${expectedContract}`);
542
595
 
543
596
  let snapshot: TaskIntentV1 | null = null;
544
597
  try {
@@ -570,31 +623,65 @@ export function parseTaskRecordV3(raw: unknown): TaskRecordV3 {
570
623
 
571
624
  const baseline = stringAt(value.baseline, "record.baseline", violations);
572
625
  if (!SHA256_HEX.test(baseline)) violations.push("record.baseline must be sha256:<64 hex>");
626
+ let gitBaseHead = "";
627
+ if (version === 4) {
628
+ const rawGitBaseHead = stringAt(value.git_base_head, "record.git_base_head", violations);
629
+ if (rawGitBaseHead !== rawGitBaseHead.toLowerCase())
630
+ violations.push("record.git_base_head must be lowercase");
631
+ gitBaseHead = rawGitBaseHead;
632
+ if (!GIT_OBJECT_ID.test(gitBaseHead))
633
+ violations.push("record.git_base_head must be a lowercase Git commit id");
634
+ }
573
635
  const acceptanceIds = new Set(snapshot ? snapshot.acceptance.map((item) => item.id) : []);
574
- const attestations = arrayAt(value.attestations, "record.attestations", violations).map((item, index) => parseAttestationV3(item, index, acceptanceIds, violations));
636
+ const attestations = arrayAt(value.attestations, "record.attestations", violations).map((item, index) => parseAttestationV3(item, index, acceptanceIds, violations, version === 4));
637
+ if (version === 4) {
638
+ for (const [index, attestation] of attestations.entries()) {
639
+ if (attestation.kind === "review" && attestation.review_revision && attestation.review_revision.base_head !== gitBaseHead)
640
+ violations.push(`record.attestations[${index}].review_revision.base_head must equal record.git_base_head`);
641
+ }
642
+ }
575
643
  const findings = arrayAt(value.findings, "record.findings", violations).map((item, index) => parseFinding(item, index, violations));
576
644
  const history = arrayAt(value.history, "record.history", violations).map((item, index) => parseHistoryV3(item, index, violations));
577
645
  uniqueIds(attestations, "record.attestations", violations);
578
646
  uniqueIds(findings, "record.findings", violations);
579
647
  uniqueIds(history, "record.history", violations);
580
648
  if (violations.length > 0) throw new KernelValidationError(violations);
581
- return {
582
- contract: TASK_RECORD_CONTRACT_V3,
649
+ const record = {
650
+ contract: expectedContract,
583
651
  task_id: taskId,
584
652
  intent_snapshot: snapshot as TaskIntentV1,
585
653
  intent_ref: { path: refPath, content_hash: refContentHash },
586
654
  lifecycle: lifecycle as TaskRecordV3["lifecycle"],
587
655
  artifact_state: artifactState as TaskRecordV3["artifact_state"],
588
656
  baseline,
657
+ ...(version === 4 ? { git_base_head: gitBaseHead } : {}),
589
658
  attestations,
590
659
  findings,
591
660
  history,
592
661
  };
662
+ return record as TaskRecordV3 | TaskRecordV4;
663
+ }
664
+
665
+ /** Strict v3 drain parser: unknown fields and revision identity stay illegal. */
666
+ export function parseTaskRecordV3(raw: unknown): TaskRecordV3 {
667
+ return parseTaskRecordAtVersion(raw, 3) as TaskRecordV3;
668
+ }
669
+
670
+ /** Strict v4 parser: the Enrollment base commit is mandatory. */
671
+ export function parseTaskRecordV4(raw: unknown): TaskRecordV4 {
672
+ return parseTaskRecordAtVersion(raw, 4) as TaskRecordV4;
673
+ }
674
+
675
+ /** Version-dispatched parser used by every durable Kernel owner. */
676
+ export function parseTaskRecord(raw: unknown): TaskRecord {
677
+ const contract = (raw as { contract?: unknown } | null)?.contract;
678
+ if (contract === TASK_RECORD_CONTRACT_V4) return parseTaskRecordV4(raw);
679
+ return parseTaskRecordV3(raw);
593
680
  }
594
681
 
595
- export function assertKernelInvariantsV3(intentRaw: TaskIntentV1, recordRaw: TaskRecordV3): void {
682
+ export function assertKernelInvariantsV3(intentRaw: TaskIntentV1, recordRaw: TaskRecord): void {
596
683
  const intent = parseTaskIntentV1(intentRaw);
597
- const record = parseTaskRecordV3(recordRaw);
684
+ const record = parseTaskRecord(recordRaw);
598
685
  const violations: string[] = [];
599
686
  if (intent.task_id !== record.task_id) violations.push("intent and record task_id must match");
600
687
  if (intent.revision !== record.intent_snapshot.revision) violations.push("intent revision must match record snapshot");
@@ -752,7 +839,9 @@ export function parseTaskAction(raw: unknown): TaskAction {
752
839
  "action",
753
840
  violations,
754
841
  );
755
- const approval = parseApprovalV2(value.approval, 0, violations);
842
+ const approval = parseApprovalV2(value.approval, 0, violations, true);
843
+ if (approval.review_revision && approval.kind !== "review")
844
+ violations.push("action.approval.review_revision is only valid on review approvals");
756
845
  action = {
757
846
  ...base,
758
847
  type: base.type,
@@ -871,12 +960,12 @@ export function parseTaskAction(raw: unknown): TaskAction {
871
960
  }
872
961
 
873
962
  export function assertTaskRecordUpdateV3(
874
- previousRaw: TaskRecordV3,
875
- nextRaw: TaskRecordV3,
963
+ previousRaw: TaskRecord,
964
+ nextRaw: TaskRecord,
876
965
  action: TaskAction,
877
966
  ): void {
878
- const previous = parseTaskRecordV3(previousRaw);
879
- const next = parseTaskRecordV3(nextRaw);
967
+ const previous = parseTaskRecord(previousRaw);
968
+ const next = parseTaskRecord(nextRaw);
880
969
  const violations: string[] = [];
881
970
 
882
971
  if (next.contract !== previous.contract)
@@ -885,6 +974,8 @@ export function assertTaskRecordUpdateV3(
885
974
  violations.push("record task_id must remain immutable");
886
975
  if (next.baseline !== previous.baseline)
887
976
  violations.push("record baseline must remain immutable");
977
+ if (previous.contract === TASK_RECORD_CONTRACT_V4 && next.contract === TASK_RECORD_CONTRACT_V4 && next.git_base_head !== previous.git_base_head)
978
+ violations.push("record git_base_head must remain immutable");
888
979
 
889
980
  const isIntentAction =
890
981
  action.type === "revise_intent" ||
@@ -217,7 +217,7 @@ export function buildLoopAction(input: {
217
217
  export interface LoopRoleDispatch {
218
218
  packet: RoleDelegationPacket;
219
219
  call: {
220
- subagent_type: "general-purpose";
220
+ subagent_type: LoopRoleSubagent;
221
221
  description: string;
222
222
  prompt: string;
223
223
  inherit_context: false;
@@ -226,6 +226,21 @@ export interface LoopRoleDispatch {
226
226
  };
227
227
  }
228
228
 
229
+ /**
230
+ * Authoritative code review runs on the host's configured `Review` agent so the
231
+ * user's own model/provider selection applies. Architecture exploration uses
232
+ * the host `Explore` agent. Every other internal role, and especially the
233
+ * tool-less planning `advisory-reviewer`, stays on the generic agent and never
234
+ * inherits review authority.
235
+ */
236
+ export type LoopRoleSubagent = "Review" | "Explore" | "general-purpose";
237
+
238
+ export function loopRoleSubagentFor(role: LoopRole): LoopRoleSubagent {
239
+ if (role === "code-review") return "Review";
240
+ if (role === "arch-explorer") return "Explore";
241
+ return "general-purpose";
242
+ }
243
+
229
244
  export function buildLoopRoleDispatch(input: {
230
245
  role: LoopRole;
231
246
  context: RoleDelegationContext;
@@ -235,7 +250,7 @@ export function buildLoopRoleDispatch(input: {
235
250
  return {
236
251
  packet,
237
252
  call: {
238
- subagent_type: "general-purpose",
253
+ subagent_type: loopRoleSubagentFor(input.role),
239
254
  description: input.description ?? `${input.role} internal role`,
240
255
  prompt: packet.prompt,
241
256
  inherit_context: false,