immune-brain 2.8.3 → 3.0.2

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 +2 -2
  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
@@ -1,9 +1,8 @@
1
1
  // P2B1 enrollment authority. NOT exported from kernel/index.ts.
2
- // Explicit registry factory: each registry owns its WeakMap; capabilities are
3
- // only recognized by the registry that issued them. No module-level singleton.
4
- // The production registry is created only inside the Pi extension activation
5
- // closure; tests create isolated registries via the fixture seam.
2
+ // Uses the shared capability registry factory; enrollment-specific binding
3
+ // validation and projection remain here.
6
4
 
5
+ import { createCapabilityRegistry } from "./capability_registry";
7
6
  import { MUTATION_AUTHORITY_CAPABILITY_BRAND } from "./types";
8
7
 
9
8
  export const ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
@@ -20,11 +19,6 @@ export interface EnrollmentCapabilityBinding {
20
19
  nonce: string;
21
20
  }
22
21
 
23
- interface EnrollmentCapabilityState extends EnrollmentCapabilityBinding {
24
- issued_at: string;
25
- consumed: boolean;
26
- }
27
-
28
22
  export interface ValidatedEnrollment {
29
23
  task_id: string;
30
24
  intent_path: string;
@@ -48,76 +42,39 @@ export interface EnrollmentAuthorityRegistry {
48
42
  }
49
43
 
50
44
  export function createEnrollmentAuthorityRegistry(): EnrollmentAuthorityRegistry {
51
- const states = new WeakMap<object, EnrollmentCapabilityState>();
52
- const brand = Symbol.for("assurance-kernel.enrollment-capability-registry");
53
-
54
- function isEnrollmentCapability(value: unknown): value is object {
55
- return (
56
- !!value &&
57
- typeof value === "object" &&
58
- (value as Record<symbol, unknown>)[ENROLLMENT_CAPABILITY_BRAND] === true &&
59
- (value as Record<symbol, unknown>)[brand] === true
60
- );
61
- }
62
-
63
- function stateOf(capability: object): EnrollmentCapabilityState {
64
- const state = states.get(capability);
65
- if (!state) throw new Error("enrollment capability is not recognized by this registry");
66
- return state;
67
- }
68
-
69
- return {
70
- brand,
71
- issue(binding: EnrollmentCapabilityBinding, issuedAt = new Date().toISOString()): object {
72
- const missing: string[] = [];
73
- for (const [key, value] of Object.entries(binding)) {
74
- if (value === undefined || value === null || value === "") missing.push(key);
75
- }
76
- if (missing.length > 0)
77
- throw new Error(`enrollment capability binding is incomplete: ${missing.join(", ")}`);
78
- if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
79
- throw new Error("enrollment capability must have a future expiry");
80
- const capability = Object.freeze(
81
- Object.defineProperties(
82
- {},
83
- {
84
- [ENROLLMENT_CAPABILITY_BRAND]: { value: true, enumerable: false, writable: false, configurable: false },
85
- [brand]: { value: true, enumerable: false, writable: false, configurable: false },
86
- },
87
- ),
88
- );
89
- states.set(capability, { ...binding, issued_at: issuedAt, consumed: false });
90
- return capability;
91
- },
92
- inspect(capability: object, expected: EnrollmentCapabilityBinding, now = Date.now()): ValidatedEnrollment {
93
- if (!isEnrollmentCapability(capability)) throw new Error("enrollment capability required");
94
- const state = stateOf(capability);
95
- if (state.consumed) throw new Error("enrollment capability already consumed");
96
- if (Date.parse(state.expires_at) <= now) throw new Error("enrollment capability has expired");
97
- for (const key of Object.keys(expected) as Array<keyof EnrollmentCapabilityBinding>) {
98
- if (state[key] !== expected[key])
99
- throw new Error(`enrollment capability ${key} mismatch`);
100
- }
101
- return {
102
- task_id: state.task_id,
103
- intent_path: state.intent_path,
104
- intent_revision: state.intent_revision,
105
- intent_content_hash: state.intent_content_hash,
106
- preparation_digest: state.preparation_digest,
107
- actor_id: state.actor_id,
108
- confirmation_ref: state.confirmation_ref,
109
- issued_at: state.issued_at,
110
- expires_at: state.expires_at,
111
- nonce: state.nonce,
112
- };
113
- },
114
- consume(capability: object, expected: EnrollmentCapabilityBinding, now = Date.now()): ValidatedEnrollment {
115
- const validated = this.inspect(capability, expected, now);
116
- stateOf(capability).consumed = true;
117
- return validated;
118
- },
119
- isConsumed(capability: object): boolean {
120
- return stateOf(capability).consumed;
45
+ return createCapabilityRegistry<EnrollmentCapabilityBinding, EnrollmentCapabilityBinding, ValidatedEnrollment>(
46
+ ENROLLMENT_CAPABILITY_BRAND,
47
+ {
48
+ validateBinding(binding, issuedAt) {
49
+ const missing: string[] = [];
50
+ for (const [key, value] of Object.entries(binding)) {
51
+ if (value === undefined || value === null || value === "") missing.push(key);
52
+ }
53
+ if (missing.length > 0)
54
+ throw new Error(`enrollment capability binding is incomplete: ${missing.join(", ")}`);
55
+ if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
56
+ throw new Error("enrollment capability must have a future expiry");
57
+ },
58
+ validateAndProject(state, expected, now) {
59
+ if (Date.parse(state.expires_at) <= now) throw new Error("enrollment capability has expired");
60
+ for (const key of Object.keys(expected) as Array<keyof EnrollmentCapabilityBinding>) {
61
+ if (state[key] !== expected[key])
62
+ throw new Error(`enrollment capability ${key} mismatch`);
63
+ }
64
+ return {
65
+ task_id: state.task_id,
66
+ intent_path: state.intent_path,
67
+ intent_revision: state.intent_revision,
68
+ intent_content_hash: state.intent_content_hash,
69
+ preparation_digest: state.preparation_digest,
70
+ actor_id: state.actor_id,
71
+ confirmation_ref: state.confirmation_ref,
72
+ issued_at: state.issued_at,
73
+ expires_at: state.expires_at,
74
+ nonce: state.nonce,
75
+ };
76
+ },
121
77
  },
122
- };
78
+ "enrollment",
79
+ );
123
80
  }
@@ -46,6 +46,16 @@ export const RISK_FLOOR_SCOPE_PREFIXES = [
46
46
  "plugins/immune-brain/.pi-extension",
47
47
  ] as const;
48
48
 
49
+ // Concrete changed paths use a narrower, segment-safe prefix policy. Keep this
50
+ // separate from the glob-aware scope_hint policy above: the two inputs have
51
+ // different contracts and docs-only scope remains routine today.
52
+ export const CHANGED_PATH_RISK_FLOOR_PREFIXES = [
53
+ "plugins/immune-brain/runtime/kernel",
54
+ "plugins/immune-brain/.pi-extension",
55
+ "docs/specs",
56
+ "docs/plans",
57
+ ] as const;
58
+
49
59
  function segmentMatches(e: string, s: string): boolean {
50
60
  if (e === "*") return true;
51
61
  if (!e.includes("*") && !e.includes("?")) return e === s;
@@ -330,6 +340,20 @@ export function canonicalIntentHash(intent: unknown): string {
330
340
 
331
341
  const RISK_RANK: Record<TaskRisk, number> = { routine: 0, material: 1, critical: 2 };
332
342
 
343
+ export function classifyTaskRisk(
344
+ changedPaths: readonly string[],
345
+ declaredRisk: TaskRisk,
346
+ ): TaskRisk {
347
+ const pathFloor = changedPaths.some((path) =>
348
+ CHANGED_PATH_RISK_FLOOR_PREFIXES.some(
349
+ (prefix) => path === prefix || path.startsWith(`${prefix}/`),
350
+ ),
351
+ )
352
+ ? "material"
353
+ : "routine";
354
+ return RISK_RANK[declaredRisk] >= RISK_RANK[pathFloor] ? declaredRisk : pathFloor;
355
+ }
356
+
333
357
  function contentHashWithoutRevision(intent: TaskIntentV1): string {
334
358
  const { revision: _revision, ...rest } = intent;
335
359
  return canonicalIntentHash(rest);
@@ -4,6 +4,7 @@
4
4
  // enrollment call, or writer. Direct import cannot authorize or mutate.
5
5
 
6
6
  import { createHash } from "node:crypto";
7
+ import { spawnSync } from "node:child_process";
7
8
  import { resolve } from "node:path";
8
9
  import { readBackendClaim, readTaskTombstone } from "./backend_claim";
9
10
  import { readTaskIntent } from "./intent";
@@ -13,6 +14,21 @@ import {
13
14
  } from "./storage";
14
15
 
15
16
  const SOURCE_PATH = ".imm/state/workspace.json";
17
+ const GIT_OBJECT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
18
+
19
+ /**
20
+ * The Enrollment base commit. v4 TaskRecords bind this exact commit so QA,
21
+ * Review, authorization, and completion can all derive one scoped revision.
22
+ */
23
+ export function readGitHead(root: string): string {
24
+ const result = spawnSync("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
25
+ encoding: "utf8",
26
+ });
27
+ const head = typeof result.stdout === "string" ? result.stdout.trim() : "";
28
+ if (result.status !== 0 || !GIT_OBJECT_ID.test(head))
29
+ throw new Error("enrollment requires a committed Git HEAD in the project root");
30
+ return head.toLowerCase();
31
+ }
16
32
 
17
33
  export interface PiCanaryPrepareInput {
18
34
  task_id: string;
@@ -43,6 +59,9 @@ export interface PiCanaryPreparation {
43
59
  lifecycle: string | null;
44
60
  artifact_state: string | null;
45
61
  } | null;
62
+ /** Commit the confirmation is bound to; becomes TaskRecord v4 `git_base_head`. */
63
+ git_base_head: string | null;
64
+ git_error: string | null;
46
65
  workspace: {
47
66
  current_working: string | null;
48
67
  };
@@ -139,6 +158,16 @@ export function preparePiCanary(root: string, input: PiCanaryPrepareInput): PiCa
139
158
  `workspace owner ${state.state.current_working} contradicts backend claim task ${claim.task_id}`,
140
159
  );
141
160
 
161
+ // A missing HEAD is a readiness blocker, not a crash: the rehearsal and the
162
+ // native gate must be able to report it while still binding the digest.
163
+ let gitBaseHead: string | null = null;
164
+ let gitError: string | null = null;
165
+ try {
166
+ gitBaseHead = readGitHead(canonicalRoot);
167
+ } catch (error) {
168
+ gitError = error instanceof Error ? error.message : String(error);
169
+ }
170
+
142
171
  const preparation: PiCanaryPreparation = {
143
172
  contract: "assurance_kernel/pi_canary_preparation/v1",
144
173
  task_id: input.task_id,
@@ -148,6 +177,8 @@ export function preparePiCanary(root: string, input: PiCanaryPrepareInput): PiCa
148
177
  backend_claim: backend,
149
178
  task_tombstone: taskTombstone,
150
179
  task_record_v3: record,
180
+ git_base_head: gitBaseHead,
181
+ git_error: gitError,
151
182
  workspace,
152
183
  digest: "",
153
184
  };
@@ -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;