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
@@ -9,14 +9,14 @@ import {
9
9
  type EnrollmentCapabilityBinding,
10
10
  } from "./enrollment_authority";
11
11
  import { readTaskTombstone, type BackendClaim } from "./backend_claim";
12
- import { preparePiCanary } from "./pi_canary_prepare";
12
+ import { preparePiCanary, readGitHead } from "./pi_canary_prepare";
13
13
  import {
14
14
  commitEnrollmentLocked,
15
15
  readTaskRecordRaw,
16
16
  readWorkspaceStateRaw,
17
17
  withKernelStoreLock,
18
18
  } from "./storage";
19
- import type { TaskRecordV3, WorkspaceStateLike } from "./types";
19
+ import type { TaskRecord, TaskRecordV4, WorkspaceStateLike } from "./types";
20
20
 
21
21
  export interface EnrollCanaryInput {
22
22
  task_id: string;
@@ -29,17 +29,121 @@ export interface EnrollCanaryInput {
29
29
  }
30
30
 
31
31
  export interface EnrollCanaryResult {
32
- record: TaskRecordV3;
32
+ record: TaskRecord;
33
33
  backend_claim: BackendClaim;
34
34
  workspace: { revision: string; state: WorkspaceStateLike };
35
35
  }
36
36
 
37
- function buildTaskRecordV3(
37
+ type EnrollmentPreconditionState = {
38
+ validated: ReturnType<EnrollmentAuthorityRegistry["inspect"]> | null;
39
+ current: ReturnType<typeof readTaskRecordRaw> | null;
40
+ workspace: ReturnType<typeof readWorkspaceStateRaw> | null;
41
+ intent: Awaited<ReturnType<typeof readTaskIntent>> | null;
42
+ gitBaseHead: string | null;
43
+ };
44
+
45
+ type EnrollmentPreconditionResult = EnrollmentPreconditionState & {
46
+ blockers: string[];
47
+ };
48
+
49
+ function runEnrollmentPreconditionChecks<T>(
50
+ root: string,
51
+ input: EnrollCanaryInput,
52
+ capability: object,
53
+ registry: EnrollmentAuthorityRegistry,
54
+ mode: "report" | "fail_fast",
55
+ beforeLock: (validated: EnrollmentPreconditionState["validated"]) => void,
56
+ onReady: (state: EnrollmentPreconditionState) => T,
57
+ ): T;
58
+ function runEnrollmentPreconditionChecks(
59
+ root: string,
60
+ input: EnrollCanaryInput,
61
+ capability: object,
62
+ registry: EnrollmentAuthorityRegistry,
63
+ mode: "report" | "fail_fast",
64
+ beforeLock: (validated: EnrollmentPreconditionState["validated"]) => void,
65
+ ): EnrollmentPreconditionResult;
66
+ function runEnrollmentPreconditionChecks<T>(
67
+ root: string,
68
+ input: EnrollCanaryInput,
69
+ capability: object,
70
+ registry: EnrollmentAuthorityRegistry,
71
+ mode: "report" | "fail_fast",
72
+ beforeLock: (validated: EnrollmentPreconditionState["validated"]) => void,
73
+ onReady?: (state: EnrollmentPreconditionState) => T,
74
+ ): T | EnrollmentPreconditionResult {
75
+ const blockers: string[] = [];
76
+ let validated: EnrollmentPreconditionState["validated"] = null;
77
+ try {
78
+ validated = registry.inspect(capability, input.capability_binding);
79
+ if (mode === "fail_fast" && validated.task_id !== input.task_id)
80
+ throw new Error("enrollment capability task mismatch");
81
+ } catch (error) {
82
+ if (mode === "fail_fast") throw error;
83
+ blockers.push(`capability: ${error instanceof Error ? error.message : String(error)}`);
84
+ }
85
+ beforeLock(validated);
86
+
87
+ return withKernelStoreLock(root, () => {
88
+ let current: EnrollmentPreconditionState["current"] = null;
89
+ let workspace: EnrollmentPreconditionState["workspace"] = null;
90
+ let intent: EnrollmentPreconditionState["intent"] = null;
91
+ let gitBaseHead: string | null = null;
92
+ const fail = (report: string, error: unknown) => {
93
+ if (mode === "fail_fast")
94
+ throw error instanceof Error ? error : new Error(String(error));
95
+ blockers.push(report);
96
+ };
97
+
98
+ const tombstone = readTaskTombstone(root, input.task_id);
99
+ if (tombstone) {
100
+ fail(
101
+ "task tombstone exists; same-task re-enrollment is forbidden",
102
+ new Error(`task ${input.task_id} is terminal; same-task re-enrollment is forbidden`),
103
+ );
104
+ } else {
105
+ current = readTaskRecordRaw(root, input.task_id);
106
+ if (current.record)
107
+ fail(
108
+ "task record already exists",
109
+ new Error(`task ${input.task_id} already has a TaskRecord`),
110
+ );
111
+ }
112
+ workspace = readWorkspaceStateRaw(root);
113
+ if (workspace.state.current_working !== null)
114
+ fail(
115
+ `workspace already owned by ${workspace.state.current_working}`,
116
+ new Error(`workspace is already owned by ${workspace.state.current_working}`),
117
+ );
118
+ try {
119
+ intent = readTaskIntent(root, input.task_id);
120
+ } catch (error) {
121
+ fail(`intent: ${error instanceof Error ? error.message : String(error)}`, error);
122
+ }
123
+ try {
124
+ gitBaseHead = readGitHead(root);
125
+ } catch (error) {
126
+ fail(`git base: ${error instanceof Error ? error.message : String(error)}`, error);
127
+ }
128
+
129
+ const state: EnrollmentPreconditionState = {
130
+ validated,
131
+ current,
132
+ workspace,
133
+ intent,
134
+ gitBaseHead,
135
+ };
136
+ return onReady ? onReady(state) : { ...state, blockers };
137
+ });
138
+ }
139
+
140
+ function buildTaskRecordV4(
38
141
  input: EnrollCanaryInput,
39
142
  intent: Awaited<ReturnType<typeof readTaskIntent>>,
40
- ): TaskRecordV3 {
143
+ gitBaseHead: string,
144
+ ): TaskRecordV4 {
41
145
  return {
42
- contract: "assurance_kernel/task_record/v3",
146
+ contract: "assurance_kernel/task_record/v4",
43
147
  task_id: input.task_id,
44
148
  intent_snapshot: intent.intent,
45
149
  intent_ref: {
@@ -49,6 +153,7 @@ function buildTaskRecordV3(
49
153
  lifecycle: "active",
50
154
  artifact_state: "active",
51
155
  baseline: intent.content_hash,
156
+ git_base_head: gitBaseHead,
52
157
  attestations: [],
53
158
  findings: [],
54
159
  history: [],
@@ -75,43 +180,25 @@ export function runEnrollmentRehearsal(
75
180
  generated_at: string;
76
181
  };
77
182
  } {
78
- const blockers: string[] = [];
79
- const result = {
183
+ const preconditions = runEnrollmentPreconditionChecks(
184
+ root,
185
+ input,
186
+ capability,
187
+ registry,
188
+ "report",
189
+ () => undefined,
190
+ );
191
+ return {
80
192
  rehearsed: true,
81
193
  writes_performed: false,
82
194
  evidence: {
83
- contract: "assurance_kernel/enrollment_rehearsal/v1" as const,
195
+ contract: "assurance_kernel/enrollment_rehearsal/v1",
84
196
  task_id: input.task_id,
85
- outcome: "ready" as "ready" | "not_ready",
86
- blockers,
197
+ outcome: preconditions.blockers.length === 0 ? "ready" : "not_ready",
198
+ blockers: preconditions.blockers,
87
199
  generated_at: input.now,
88
200
  },
89
201
  };
90
- try {
91
- // capability inspect (does not consume)
92
- registry.inspect(capability, input.capability_binding);
93
- } catch (error) {
94
- blockers.push(`capability: ${error instanceof Error ? error.message : String(error)}`);
95
- }
96
- withKernelStoreLock(root, () => {
97
- const tombstone = readTaskTombstone(root, input.task_id);
98
- if (tombstone) {
99
- blockers.push("task tombstone exists; same-task re-enrollment is forbidden");
100
- } else {
101
- const current = readTaskRecordRaw(root, input.task_id);
102
- if (current.record) blockers.push("task record already exists");
103
- }
104
- const workspace = readWorkspaceStateRaw(root);
105
- if (workspace.state.current_working !== null)
106
- blockers.push(`workspace already owned by ${workspace.state.current_working}`);
107
- });
108
- try {
109
- readTaskIntent(root, input.task_id);
110
- } catch (error) {
111
- blockers.push(`intent: ${error instanceof Error ? error.message : String(error)}`);
112
- }
113
- result.evidence.outcome = blockers.length === 0 ? "ready" : "not_ready";
114
- return result;
115
202
  }
116
203
 
117
204
  /**
@@ -124,71 +211,73 @@ export function enrollCanaryTask(
124
211
  input: EnrollCanaryInput,
125
212
  registry: EnrollmentAuthorityRegistry,
126
213
  ): EnrollCanaryResult {
127
- const validated = registry.inspect(input.capability, input.capability_binding);
128
- if (validated.task_id !== input.task_id)
129
- throw new Error("enrollment capability task mismatch");
130
-
131
- // v4 storage retirement: the capability is bound to the preparation
132
- // digest computed from Kernel owners only. Recompute it inside the store
133
- // lock and reject if the owner set changed since the confirmation.
134
- const recomputed = preparePiCanary(root, { task_id: input.task_id, now: input.now });
135
- if (recomputed.digest !== input.preparation_digest)
136
- throw new Error("enrollment preparation digest mismatch");
137
-
138
- return withKernelStoreLock(root, () => {
139
- const tombstone = readTaskTombstone(root, input.task_id);
140
- if (tombstone)
141
- throw new Error(
142
- `task ${input.task_id} is terminal; same-task re-enrollment is forbidden`,
143
- );
144
- const current = readTaskRecordRaw(root, input.task_id);
145
- if (current.record)
146
- throw new Error(`task ${input.task_id} already has a TaskRecord`);
147
- const workspace = readWorkspaceStateRaw(root);
148
- if (workspace.state.current_working !== null)
149
- throw new Error(`workspace is already owned by ${workspace.state.current_working}`);
150
-
151
- // fresh secure intent reread inside the lock
152
- const intent = readTaskIntent(root, input.task_id);
153
- if (intent.intent.revision !== input.intent_revision)
154
- throw new Error("intent revision mismatch");
214
+ let gitBaseHead: string | null = null;
215
+ return runEnrollmentPreconditionChecks(
216
+ root,
217
+ input,
218
+ input.capability,
219
+ registry,
220
+ "fail_fast",
221
+ () => {
222
+ // v4 storage retirement: the capability is bound to the preparation
223
+ // digest computed from Kernel owners only. Recompute it before the
224
+ // locked owner reads and reject if the owner set changed.
225
+ const recomputed = preparePiCanary(root, { task_id: input.task_id, now: input.now });
226
+ if (recomputed.digest !== input.preparation_digest)
227
+ throw new Error("enrollment preparation digest mismatch");
228
+ gitBaseHead = recomputed.git_base_head;
229
+ if (!gitBaseHead)
230
+ throw new Error(recomputed.git_error ?? "enrollment requires a committed Git HEAD");
231
+ },
232
+ (checks) => {
233
+ if (!checks.validated || !checks.intent || !checks.workspace || !checks.current)
234
+ throw new Error("enrollment precondition state incomplete");
235
+ if (checks.intent.intent.revision !== input.intent_revision)
236
+ throw new Error("intent revision mismatch");
237
+ if (checks.intent.content_hash !== checks.validated.intent_content_hash)
238
+ throw new Error("intent content hash mismatch");
239
+ // The confirmation is bound to this exact commit; a moved HEAD means the
240
+ // operator approved a different base than the one being recorded.
241
+ if (checks.gitBaseHead !== gitBaseHead)
242
+ throw new Error("Git HEAD moved after the enrollment confirmation");
155
243
 
156
- // consume immediately before the marker write
157
- registry.consume(input.capability, input.capability_binding);
244
+ // consume immediately before the marker write
245
+ registry.consume(input.capability, input.capability_binding);
158
246
 
159
- const record = buildTaskRecordV3(input, intent);
160
- const nextWorkspace: WorkspaceStateLike = {
161
- ...workspace.state,
162
- current_working: input.task_id,
163
- };
164
- const claim: BackendClaim = {
165
- contract: "assurance_kernel/backend_claim/v2",
166
- backend: "kernel",
167
- task_id: input.task_id,
168
- intent_revision: input.intent_revision,
169
- intent_content_hash: intent.content_hash,
170
- enrollment_event_id: `enroll-${input.task_id}-${input.now}`,
171
- lifecycle_status: "active",
172
- created_at: input.now,
173
- updated_at: input.now,
174
- };
175
- const mutation = commitEnrollmentLocked(
176
- root,
177
- input.task_id,
178
- {
179
- contract: "assurance_kernel/workspace_transaction/v2",
247
+ const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
248
+ const nextWorkspace: WorkspaceStateLike = {
249
+ ...checks.workspace.state,
250
+ current_working: input.task_id,
251
+ };
252
+ const claim: BackendClaim = {
253
+ contract: "assurance_kernel/backend_claim/v2",
254
+ backend: "kernel",
180
255
  task_id: input.task_id,
181
- expected_record_hash: current.revision,
182
- next_record_content: `${JSON.stringify(record, null, 2)}\n`,
183
- expected_workspace_hash: workspace.revision,
184
- next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}\n`,
185
- },
186
- claim as unknown as Record<string, unknown>,
187
- );
188
- return {
189
- record: mutation.record,
190
- backend_claim: claim,
191
- workspace: { revision: "", state: mutation.workspace },
192
- };
193
- });
256
+ intent_revision: input.intent_revision,
257
+ intent_content_hash: checks.intent.content_hash,
258
+ enrollment_event_id: `enroll-${input.task_id}-${input.now}`,
259
+ lifecycle_status: "active",
260
+ created_at: input.now,
261
+ updated_at: input.now,
262
+ };
263
+ const mutation = commitEnrollmentLocked(
264
+ root,
265
+ input.task_id,
266
+ {
267
+ contract: "assurance_kernel/workspace_transaction/v2",
268
+ task_id: input.task_id,
269
+ expected_record_hash: checks.current.revision,
270
+ next_record_content: `${JSON.stringify(record, null, 2)}\n`,
271
+ expected_workspace_hash: checks.workspace.revision,
272
+ next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}\n`,
273
+ },
274
+ claim as unknown as Record<string, unknown>,
275
+ );
276
+ return {
277
+ record: mutation.record,
278
+ backend_claim: claim,
279
+ workspace: { revision: "", state: mutation.workspace },
280
+ };
281
+ },
282
+ );
194
283
  }
@@ -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
  };