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
@@ -15,6 +15,8 @@ import {
15
15
  KernelStoreSecurityError,
16
16
  appendJournalEntry,
17
17
  readSecureProjectFile,
18
+ readTaskRecordRaw,
19
+ readWorkspaceStateRaw,
18
20
  type JournalEntry,
19
21
  type JournalReasonCode,
20
22
  type TaskPhase,
@@ -23,7 +25,12 @@ import {
23
25
  INTENT_MAX_BYTES,
24
26
  canonicalIntentHash,
25
27
  parseTaskIntentV1,
28
+ RISK_FLOOR_SCOPE_PREFIXES,
26
29
  } from "../kernel/intent";
30
+ import { projectTask, resolveProjectedRisk } from "../kernel/completion";
31
+ import { deriveAssuranceAuthorization } from "../kernel/assurance_projection";
32
+ import { taskDiffIdentity, taskRevisionIdentity } from "../workspace_scope";
33
+ import type { TaskIntentV1, TaskRisk } from "../kernel/types";
27
34
  import {
28
35
  canonicalDescriptorBytes,
29
36
  parseVerificationDescriptor,
@@ -152,6 +159,202 @@ function runStatus(root: string): KernelExecution {
152
159
  }
153
160
  }
154
161
 
162
+ const UNOBSERVABLE = {
163
+ capability: "unobservable",
164
+ rehearsal: "unobservable",
165
+ cas_holder: "unobservable",
166
+ } as const;
167
+
168
+ const RISK_ORDER: Record<TaskRisk, number> = {
169
+ routine: 0,
170
+ material: 1,
171
+ critical: 2,
172
+ };
173
+
174
+ function matchingFloorEntries(intent: TaskIntentV1): string[] {
175
+ return intent.scope_hint.filter((entry) => {
176
+ try {
177
+ const trial = parseTaskIntentV1({
178
+ contract: intent.contract,
179
+ task_id: intent.task_id,
180
+ goal: intent.goal,
181
+ acceptance: intent.acceptance,
182
+ scope_hint: [entry],
183
+ risk: "routine",
184
+ revision: intent.revision,
185
+ owner: "user",
186
+ });
187
+ return trial.risk !== "routine";
188
+ } catch {
189
+ return false;
190
+ }
191
+ });
192
+ }
193
+
194
+ function readWorkspaceOwner(root: string): string | null {
195
+ try {
196
+ const raw = JSON.parse(readSecureProjectFile(root, ".imm/state/workspace.json")) as {
197
+ current_working?: unknown;
198
+ };
199
+ return typeof raw.current_working === "string" ? raw.current_working : null;
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * `inspect --json` is a read-only Inspect Projection. It never journals,
207
+ * never takes a Kernel store lock, and never becomes authority.
208
+ */
209
+ function runInspect(root: string): KernelExecution {
210
+ try {
211
+ const layout = inspectStorageLayout(root);
212
+ const claim = readBackendClaim(root);
213
+ const workspace = readWorkspaceOwner(root);
214
+ const kernel = {
215
+ claim: claim
216
+ ? {
217
+ task_id: claim.task_id,
218
+ lifecycle_status: claim.lifecycle_status,
219
+ }
220
+ : null,
221
+ workspace: { current_working: workspace },
222
+ };
223
+ const idleRisk = {
224
+ declared: null as TaskRisk | null,
225
+ resolved: null as TaskRisk | null,
226
+ floor_prefixes: [...RISK_FLOOR_SCOPE_PREFIXES],
227
+ floor_applied: false,
228
+ matching_scope_entries: [] as string[],
229
+ };
230
+ const ok = layout.layout === "ready" || layout.layout === "migration_uncommitted";
231
+ if (!claim || !ok || layout.layout !== "ready") {
232
+ return {
233
+ result: jsonResult({
234
+ contract: "assurance_kernel/inspect/v1",
235
+ layout,
236
+ kernel,
237
+ assurance: null,
238
+ risk: idleRisk,
239
+ unobservable: UNOBSERVABLE,
240
+ }),
241
+ journal: journalFor(
242
+ "inspect",
243
+ null,
244
+ ok ? "ok" : "escalated",
245
+ layout.layout === "ready" ? "command_ok" : "source_invalid",
246
+ null,
247
+ layout.reason,
248
+ ),
249
+ };
250
+ }
251
+ let declared: TaskRisk;
252
+ let intent: TaskIntentV1;
253
+ try {
254
+ const raw = JSON.parse(
255
+ readSecureProjectFile(root, `docs/plans/${claim.task_id}.intent.json`),
256
+ ) as { risk?: unknown };
257
+ if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
258
+ throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
259
+ declared = raw.risk;
260
+ intent = parseTaskIntentV1(raw);
261
+ } catch (error) {
262
+ return sourceFailure("inspect", error);
263
+ }
264
+ let recordRead: ReturnType<typeof readTaskRecordRaw>;
265
+ try {
266
+ recordRead = readTaskRecordRaw(root, claim.task_id);
267
+ } catch (error) {
268
+ return sourceFailure("inspect", error);
269
+ }
270
+ if (!recordRead.record) {
271
+ return {
272
+ result: errorResult(
273
+ "source_read_failed",
274
+ `task ${claim.task_id} has no TaskRecord`,
275
+ 1,
276
+ ),
277
+ journal: journalFor(
278
+ "inspect",
279
+ null,
280
+ "rejected",
281
+ "source_read_failed",
282
+ claim.task_id,
283
+ "Resolve the claimed TaskRecord before retrying inspect.",
284
+ ),
285
+ };
286
+ }
287
+ const record = recordRead.record;
288
+ const workspaceState = readWorkspaceStateRaw(root);
289
+ let identity;
290
+ try {
291
+ identity =
292
+ record.contract === "assurance_kernel/task_record/v4"
293
+ ? taskRevisionIdentity(root, intent.scope_hint, record.git_base_head)
294
+ : taskDiffIdentity(root, intent.scope_hint);
295
+ } catch (error) {
296
+ return sourceFailure("inspect", error);
297
+ }
298
+ const diffHash = identity.diff_hash;
299
+ const changedPaths = identity.changed_paths;
300
+ const decision = projectTask(intent, record, diffHash, record.intent_ref.content_hash, changedPaths);
301
+ const openUserDecisionCount = record.findings.filter(
302
+ (finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
303
+ ).length;
304
+ const resolved = resolveProjectedRisk(intent, changedPaths);
305
+ return {
306
+ result: jsonResult({
307
+ contract: "assurance_kernel/inspect/v1",
308
+ layout,
309
+ kernel,
310
+ assurance: {
311
+ contract: "assurance_kernel/assurance_projection/v1",
312
+ task_id: claim.task_id,
313
+ error: null,
314
+ claim: kernel.claim,
315
+ projection: {
316
+ record_revision: recordRead.revision,
317
+ workspace_revision: workspaceState.revision,
318
+ intent_revision: intent.revision,
319
+ intent_content_hash: record.intent_ref.content_hash,
320
+ diff_hash: diffHash,
321
+ lifecycle: record.lifecycle,
322
+ artifact_state: record.artifact_state,
323
+ risk: resolveProjectedRisk(intent, changedPaths),
324
+ next_obligation: decision.next_obligation,
325
+ fresh_acceptance_ids: decision.fresh_acceptance_ids,
326
+ missing_acceptance_ids: decision.missing_acceptance_ids,
327
+ stale_attestation_ids: decision.stale_attestation_ids,
328
+ fresh_approval_kinds: [],
329
+ missing_approval_kinds: decision.missing_approval_kinds,
330
+ blocking_finding_ids: decision.blocking_finding_ids,
331
+ unresolved_user_decision_ids: decision.unresolved_user_decision_ids,
332
+ replan_required_ids: decision.replan_required_ids,
333
+ independence_violations: decision.independence_violations,
334
+ open_user_decision_count: openUserDecisionCount,
335
+ completion_ready: decision.complete,
336
+ authorization: deriveAssuranceAuthorization({
337
+ next_obligation: decision.next_obligation,
338
+ open_user_decision_count: openUserDecisionCount,
339
+ }),
340
+ },
341
+ },
342
+ risk: {
343
+ declared,
344
+ resolved,
345
+ floor_prefixes: [...RISK_FLOOR_SCOPE_PREFIXES],
346
+ floor_applied: RISK_ORDER[resolved] > RISK_ORDER[declared],
347
+ matching_scope_entries: matchingFloorEntries(intent),
348
+ },
349
+ unobservable: UNOBSERVABLE,
350
+ }),
351
+ journal: journalFor("inspect", null, "ok", "command_ok", claim.task_id, null),
352
+ };
353
+ } catch (error) {
354
+ return sourceFailure("inspect", error);
355
+ }
356
+ }
357
+
155
358
  const INTENT_SIDECAR_PREFIX = "docs/plans/";
156
359
  const TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
157
360
 
@@ -1024,6 +1227,21 @@ function executeKernelCommand(args: string[], root: string): KernelExecution {
1024
1227
  };
1025
1228
  return runStatus(root);
1026
1229
  }
1230
+ if (command === "inspect") {
1231
+ if (flags.length !== 1 || flags[0] !== "--json")
1232
+ return {
1233
+ result: errorResult("invalid_command", "inspect accepts only --json", 2),
1234
+ journal: journalFor(
1235
+ command,
1236
+ null,
1237
+ "rejected",
1238
+ "invalid_command",
1239
+ null,
1240
+ "Run imm-kernel inspect --json.",
1241
+ ),
1242
+ };
1243
+ return runInspect(root);
1244
+ }
1027
1245
  if (command === "audit") {
1028
1246
  if (args.length !== 2 || args[1] !== "--legacy")
1029
1247
  return {
@@ -1097,7 +1315,7 @@ function executeKernelCommand(args: string[], root: string): KernelExecution {
1097
1315
  return {
1098
1316
  result: {
1099
1317
  stdout:
1100
- "usage: imm-kernel status --json\n imm-kernel audit --legacy\n imm-kernel intent author <path> --stdin --json\n imm-kernel intent validate <path> --json\n",
1318
+ "usage: imm-kernel status --json\n imm-kernel inspect --json\n imm-kernel audit --legacy\n imm-kernel intent author <path> --stdin --json\n imm-kernel intent validate <path> --json\n",
1101
1319
  stderr: "",
1102
1320
  returncode: 0,
1103
1321
  },
@@ -1106,7 +1324,7 @@ function executeKernelCommand(args: string[], root: string): KernelExecution {
1106
1324
  return {
1107
1325
  result: errorResult(
1108
1326
  "invalid_command",
1109
- "usage: imm-kernel status --json | audit --legacy | intent author <path> --stdin --json | intent validate <path> --json",
1327
+ "usage: imm-kernel status --json | inspect --json | audit --legacy | intent author <path> --stdin --json | intent validate <path> --json",
1110
1328
 
1111
1329
  2,
1112
1330
  ),
@@ -1130,7 +1348,7 @@ export function runKernelCommand(
1130
1348
  // subcommands (migrate/readiness/journal) are no-journal: they never
1131
1349
  // append the friction journal. Only the fallback unknown-command path
1132
1350
  // keeps journaling.
1133
- if (["intent", "status", "audit", "migrate", "readiness", "journal"].includes(args[0] ?? ""))
1351
+ if (["intent", "status", "inspect", "audit", "migrate", "readiness", "journal"].includes(args[0] ?? ""))
1134
1352
  return execution.result;
1135
1353
  let warning = "";
1136
1354
  try {
@@ -313,7 +313,7 @@ function parseIssues(raw: string): GithubIssue[] {
313
313
  || !Number.isSafeInteger(item.number)
314
314
  || typeof item.html_url !== "string"
315
315
  || typeof item.title !== "string"
316
- || typeof item.body !== "string"
316
+ || (typeof item.body !== "string" && item.body !== null)
317
317
  || (item.state !== "open" && item.state !== "closed")
318
318
  ) throw new Error("gh returned a malformed Issue");
319
319
  return {
@@ -321,7 +321,7 @@ function parseIssues(raw: string): GithubIssue[] {
321
321
  number: item.number as number,
322
322
  url: item.html_url,
323
323
  title: item.title,
324
- body: item.body,
324
+ body: typeof item.body === "string" ? item.body : "",
325
325
  state: item.state,
326
326
  state_reason: typeof item.state_reason === "string" ? item.state_reason.toLowerCase() : null,
327
327
  };
@@ -8,6 +8,7 @@ import {
8
8
  type MutationAuthorityRegistry,
9
9
  type ValidatedAuthorityV2,
10
10
  } from "./authority_port";
11
+ import { asTaskDiffSnapshot } from "./completion";
11
12
  import { canonicalIntentHash, readTaskIntent } from "./intent";
12
13
  import {
13
14
  inspectIntentTokenPair,
@@ -38,6 +39,7 @@ import type {
38
39
  StoredTaskMutationV3,
39
40
  TaskAction,
40
41
  TaskIntentV1,
42
+ TaskRecord,
41
43
  } from "./types";
42
44
  import { TASK_TOMBSTONE_CONTRACT, type TaskTombstone } from "./backend_claim";
43
45
  import { KernelInvariantError, parseTaskAction } from "./validation";
@@ -51,7 +53,7 @@ export interface ApplyTaskActionInput {
51
53
  registry: MutationAuthorityRegistry;
52
54
  capability?: MutationAuthorityCapabilityV2;
53
55
  /** Trusted injected diff provider; the action's diff_hash is only an expectation. */
54
- diffProvider: (root: string, intent: TaskIntentV1) => string;
56
+ diffProvider: (root: string, record: TaskRecord) => string | { diff_hash: string; changed_paths?: readonly string[] };
55
57
  now?: number;
56
58
  /**
57
59
  * Terminal commit mode: record + workspace + active-claim removal + task
@@ -89,10 +91,10 @@ export function applyTaskAction(
89
91
  // closed before any token or capability is consumed.
90
92
 
91
93
  // Trusted diff provider is the only diff authority.
92
- const diffHash = diffProvider(root, current.record.intent_snapshot);
93
- if (diffHash !== action.diff_hash)
94
+ const trustedDiff = asTaskDiffSnapshot(diffProvider(root, current.record));
95
+ if (trustedDiff.diff_hash !== action.diff_hash)
94
96
  throw new KernelInvariantError([
95
- `action diff_hash ${action.diff_hash} does not match the trusted diff ${diffHash}`,
97
+ `action diff_hash ${action.diff_hash} does not match the trusted diff ${trustedDiff.diff_hash}`,
96
98
  ]);
97
99
 
98
100
  // Fresh secure reread of the sidecar inside the same lock.
@@ -173,7 +175,7 @@ export function applyTaskAction(
173
175
  intent_content_hash: isRevisionAction
174
176
  ? freshRead.content_hash
175
177
  : current.record.intent_ref.content_hash,
176
- diff_hash: diffHash,
178
+ diff_hash: trustedDiff.diff_hash,
177
179
  ...(action.type === "request_rework"
178
180
  ? { findings_digest: findingsDigestV2(action.findings) }
179
181
  : {}),
@@ -191,6 +193,7 @@ export function applyTaskAction(
191
193
  current.record,
192
194
  action,
193
195
  inspectedAudit ? (inspectedAudit.audit as AuthorityAuditDescriptor) : null,
196
+ trustedDiff.changed_paths,
194
197
  );
195
198
  if (!isReducedMutation(mutation))
196
199
  throw new KernelInvariantError(["reducer returned an invalid mutation"]);
@@ -13,8 +13,13 @@
13
13
 
14
14
  import { readBackendClaim, readTaskTombstone } from "./backend_claim";
15
15
  import { readTaskRecord, readAuditTaskPair, readWorkspaceStateRaw, reconcileKernelAuthority } from "./storage";
16
- import { projectTask } from "./completion";
17
- import type { AssuranceObligation, TaskIntentV1, TaskRecordV2, TaskRecordV3 } from "./types";
16
+ import { projectTask, resolveProjectedRisk } from "./completion";
17
+ import type { AssuranceObligation, TaskIntentV1, TaskRecord, TaskRecordV2 } from "./types";
18
+
19
+ export interface TaskDiffSnapshot {
20
+ diff_hash: string;
21
+ changed_paths: readonly string[];
22
+ }
18
23
 
19
24
  export interface AssuranceAuthorizationReadiness {
20
25
  /**
@@ -127,7 +132,7 @@ function projectHistoricalTerminal(
127
132
  }
128
133
 
129
134
  function freshApprovalKinds(
130
- record: TaskRecordV3,
135
+ record: TaskRecord,
131
136
  currentIntentContentHash: string,
132
137
  diffHash: string,
133
138
  ): string[] {
@@ -148,14 +153,20 @@ function freshApprovalKinds(
148
153
  }
149
154
 
150
155
  function projectFromRecord(
151
- record: TaskRecordV3,
156
+ record: TaskRecord,
152
157
  recordRevision: string,
153
158
  workspaceRevision: string,
154
- diffHash: string,
159
+ snapshot: TaskDiffSnapshot,
155
160
  ): AssuranceProjection {
156
161
  const intent: TaskIntentV1 = record.intent_snapshot;
157
- const decision = projectTask(intent, record, diffHash, record.intent_ref.content_hash);
158
- const approvalKinds = freshApprovalKinds(record, record.intent_ref.content_hash, diffHash);
162
+ const decision = projectTask(
163
+ intent,
164
+ record,
165
+ snapshot.diff_hash,
166
+ record.intent_ref.content_hash,
167
+ snapshot.changed_paths,
168
+ );
169
+ const approvalKinds = freshApprovalKinds(record, record.intent_ref.content_hash, snapshot.diff_hash);
159
170
  const openUserDecisionCount = record.findings.filter(
160
171
  (finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
161
172
  ).length;
@@ -164,10 +175,10 @@ function projectFromRecord(
164
175
  workspace_revision: workspaceRevision,
165
176
  intent_revision: record.intent_snapshot.revision,
166
177
  intent_content_hash: record.intent_ref.content_hash,
167
- diff_hash: diffHash,
178
+ diff_hash: snapshot.diff_hash,
168
179
  lifecycle: record.lifecycle,
169
180
  artifact_state: record.artifact_state,
170
- risk: intent.risk,
181
+ risk: resolveProjectedRisk(intent, snapshot.changed_paths),
171
182
  next_obligation: decision.next_obligation,
172
183
  fresh_acceptance_ids: decision.fresh_acceptance_ids,
173
184
  missing_acceptance_ids: decision.missing_acceptance_ids,
@@ -203,7 +214,7 @@ function projectFromRecord(
203
214
  export async function projectAssurance(
204
215
  root: string,
205
216
  taskId: string,
206
- diffProvider: (root: string, intent: { scope_hint?: unknown }) => string,
217
+ diffProvider: (root: string, record: TaskRecord) => TaskDiffSnapshot,
207
218
  ): Promise<AssuranceProjectionResult> {
208
219
  const fail = (
209
220
  error: string,
@@ -262,7 +273,7 @@ export async function projectAssurance(
262
273
  task_id: taskId,
263
274
  error: null,
264
275
  claim: null,
265
- projection: projectFromRecord(auditPair.record, auditPair.recordRevision, workspace.revision, diffProvider(root, auditPair.record.intent_snapshot)),
276
+ projection: projectFromRecord(auditPair.record, auditPair.recordRevision, workspace.revision, diffProvider(root, auditPair.record)),
266
277
  };
267
278
  }
268
279
  if (read.record.task_id !== taskId)
@@ -270,13 +281,13 @@ export async function projectAssurance(
270
281
  if (claim && read.record.lifecycle !== "active")
271
282
  return fail(`terminal task ${taskId} has no matching tombstone proof`, claim);
272
283
  const workspace = await readWorkspaceStateRaw(root);
273
- const diffHash = diffProvider(root, read.record.intent_snapshot);
284
+ const snapshot = diffProvider(root, read.record);
274
285
  return {
275
286
  contract: "assurance_kernel/assurance_projection/v1",
276
287
  task_id: taskId,
277
288
  error: null,
278
289
  claim,
279
- projection: projectFromRecord(read.record, read.revision, workspace.revision, diffHash),
290
+ projection: projectFromRecord(read.record, read.revision, workspace.revision, snapshot),
280
291
  };
281
292
  } catch (error) {
282
293
  return fail(error instanceof Error ? error.message : String(error));
@@ -1,10 +1,7 @@
1
1
  // P2B2 mutation authority consumption port. NOT exported from kernel/index.ts.
2
- // Registry/application pairing: each registry owns its capability state in a
3
- // closure-private WeakMap, and capabilities are only recognized by the
4
- // registry that issued them. No module-level singleton and no test issuer
5
- // exist here; the production registry/application pair is created inside the
6
- // Pi lifecycle extension activation closure, and tests issue capabilities
7
- // through tests/fixtures/mutation-authority-test-seam.ts.
2
+ // Uses the shared capability registry factory; mutation-specific binding
3
+ // validation, action digest, findings digest enforcement, and projection
4
+ // remain here.
8
5
 
9
6
  import { createHash } from "node:crypto";
10
7
 
@@ -15,6 +12,7 @@ import {
15
12
  type MutationAuthorityKind,
16
13
  type TaskAction,
17
14
  } from "./types";
15
+ import { createCapabilityRegistry } from "./capability_registry";
18
16
 
19
17
  export interface CapabilityBindingV2 {
20
18
  authority_kind: MutationAuthorityKind;
@@ -31,11 +29,6 @@ export interface CapabilityBindingV2 {
31
29
  findings_digest: string | null;
32
30
  }
33
31
 
34
- interface CapabilityState extends CapabilityBindingV2 {
35
- issued_at: string;
36
- consumed: boolean;
37
- }
38
-
39
32
  export interface ValidatedAuthorityV2 {
40
33
  audit: AuthorityAuditDescriptor;
41
34
  action_digest: string;
@@ -84,125 +77,95 @@ export function digestOfAction(action: TaskAction): string {
84
77
  }
85
78
 
86
79
  export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
87
- const states = new WeakMap<object, CapabilityState>();
88
- const brand = Symbol("assurance-kernel-mutation-authority-registry");
89
-
90
- function isCapability(value: unknown): value is MutationAuthorityCapabilityV2 {
91
- return (
92
- !!value &&
93
- typeof value === "object" &&
94
- (value as Record<symbol, unknown>)[MUTATION_AUTHORITY_CAPABILITY_BRAND] === true &&
95
- (value as Record<symbol, unknown>)[brand] === true
96
- );
97
- }
98
-
99
- function stateOf(capability: MutationAuthorityCapabilityV2): CapabilityState {
100
- const state = states.get(capability);
101
- if (!state) throw new Error("authority capability is not recognized by this registry");
102
- return state;
103
- }
104
-
105
- function validateBinding(binding: CapabilityBindingV2, issuedAt: string): void {
106
- const missing: string[] = [];
107
- for (const [key, value] of Object.entries(binding)) {
108
- if (key === "findings_digest") continue;
109
- if (value === undefined || value === null || value === "") missing.push(key);
110
- }
111
- if (missing.length > 0)
112
- throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
113
- if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
114
- throw new Error("authority capability findings_digest must be a canonical sha256 hash");
115
- if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
116
- throw new Error("authority capability must have a future expiry");
117
- }
118
-
119
- return {
120
- brand,
121
- issue(binding: CapabilityBindingV2, issuedAt = new Date().toISOString()): MutationAuthorityCapabilityV2 {
122
- validateBinding(binding, issuedAt);
123
- const capability = Object.freeze(
124
- Object.defineProperties(
125
- {},
126
- {
127
- [MUTATION_AUTHORITY_CAPABILITY_BRAND]: {
128
- value: true,
129
- enumerable: false,
130
- writable: false,
131
- configurable: false,
132
- },
133
- [brand]: {
134
- value: true,
135
- enumerable: false,
136
- writable: false,
137
- configurable: false,
138
- },
80
+ const inner = createCapabilityRegistry<CapabilityBindingV2, MutationAuthorityInspection, ValidatedAuthorityV2, MutationAuthorityCapabilityV2>(
81
+ MUTATION_AUTHORITY_CAPABILITY_BRAND,
82
+ {
83
+ validateBinding(binding, issuedAt) {
84
+ const missing: string[] = [];
85
+ for (const [key, value] of Object.entries(binding)) {
86
+ if (key === "findings_digest") continue;
87
+ if (value === undefined || value === null || value === "") missing.push(key);
88
+ }
89
+ if (missing.length > 0)
90
+ throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
91
+ if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
92
+ throw new Error("authority capability findings_digest must be a canonical sha256 hash");
93
+ if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
94
+ throw new Error("authority capability must have a future expiry");
95
+ },
96
+ validateAndProject(state, expected, now) {
97
+ if (Date.parse(state.expires_at) <= now)
98
+ throw new Error("authority capability has expired");
99
+ const actionDigest = digestOfAction(expected.action);
100
+ if (state.action_digest !== actionDigest)
101
+ throw new Error("authority capability action digest mismatch");
102
+ if (state.task_id !== expected.task_id)
103
+ throw new Error("authority capability task mismatch");
104
+ if (state.expected_record_hash !== expected.expected_record_hash)
105
+ throw new Error("authority capability record hash mismatch");
106
+ if (state.intent_revision !== expected.intent_revision)
107
+ throw new Error("authority capability intent revision mismatch");
108
+ if (state.intent_content_hash !== expected.intent_content_hash)
109
+ throw new Error("authority capability intent hash mismatch");
110
+ if (state.diff_hash !== expected.diff_hash)
111
+ throw new Error("authority capability diff hash mismatch");
112
+ if (expected.findings_digest !== undefined) {
113
+ if (state.findings_digest === null)
114
+ throw new Error("authority capability is not bound to findings");
115
+ if (state.findings_digest !== expected.findings_digest)
116
+ throw new Error("authority capability findings digest mismatch");
117
+ }
118
+ return {
119
+ audit: {
120
+ authority_kind: state.authority_kind,
121
+ actor_id: state.actor_id,
122
+ confirmation_ref: state.confirmation_ref,
123
+ issued_at: state.issued_at,
124
+ expires_at: state.expires_at,
139
125
  },
140
- ),
141
- ) as MutationAuthorityCapabilityV2;
142
- states.set(capability, {
143
- ...binding,
144
- issued_at: issuedAt,
145
- consumed: false,
146
- });
147
- return capability;
126
+ action_digest: actionDigest,
127
+ expected_record_hash: state.expected_record_hash,
128
+ intent_revision: state.intent_revision,
129
+ intent_content_hash: state.intent_content_hash,
130
+ diff_hash: state.diff_hash,
131
+ task_id: state.task_id,
132
+ };
133
+ },
148
134
  },
135
+ "authority",
136
+ );
137
+
138
+ return {
139
+ brand: inner.brand,
140
+ issue: inner.issue.bind(inner),
149
141
  inspect(
150
142
  capability: MutationAuthorityCapabilityV2 | undefined,
151
143
  expected: MutationAuthorityInspection,
152
144
  now = Date.now(),
153
145
  ): ValidatedAuthorityV2 {
154
- if (!capability || !isCapability(capability))
146
+ if (!capability)
155
147
  throw new Error("privileged action requires an opaque authority capability");
156
- const state = stateOf(capability);
157
- if (state.consumed) throw new Error("authority capability is already consumed");
158
- if (Date.parse(state.expires_at) <= now)
159
- throw new Error("authority capability has expired");
160
- const actionDigest = digestOfAction(expected.action);
161
- if (state.action_digest !== actionDigest)
162
- throw new Error("authority capability action digest mismatch");
163
- if (state.task_id !== expected.task_id)
164
- throw new Error("authority capability task mismatch");
165
- if (state.expected_record_hash !== expected.expected_record_hash)
166
- throw new Error("authority capability record hash mismatch");
167
- if (state.intent_revision !== expected.intent_revision)
168
- throw new Error("authority capability intent revision mismatch");
169
- if (state.intent_content_hash !== expected.intent_content_hash)
170
- throw new Error("authority capability intent hash mismatch");
171
- if (state.diff_hash !== expected.diff_hash)
172
- throw new Error("authority capability diff hash mismatch");
173
- if (expected.findings_digest !== undefined) {
174
- if (state.findings_digest === null)
175
- throw new Error("authority capability is not bound to findings");
176
- if (state.findings_digest !== expected.findings_digest)
177
- throw new Error("authority capability findings digest mismatch");
148
+ try {
149
+ return inner.inspect(capability, expected, now);
150
+ } catch (err) {
151
+ if (err instanceof Error && err.message.includes("not recognized by this registry"))
152
+ throw new Error("privileged action requires an opaque authority capability");
153
+ throw err;
178
154
  }
179
- return {
180
- audit: {
181
- authority_kind: state.authority_kind,
182
- actor_id: state.actor_id,
183
- confirmation_ref: state.confirmation_ref,
184
- issued_at: state.issued_at,
185
- expires_at: state.expires_at,
186
- },
187
- action_digest: actionDigest,
188
- expected_record_hash: state.expected_record_hash,
189
- intent_revision: state.intent_revision,
190
- intent_content_hash: state.intent_content_hash,
191
- diff_hash: state.diff_hash,
192
- task_id: state.task_id,
193
- };
194
155
  },
195
156
  consume(
196
157
  capability: MutationAuthorityCapabilityV2,
197
158
  expected: MutationAuthorityInspection,
198
159
  now = Date.now(),
199
160
  ): ValidatedAuthorityV2 {
200
- const validated = this.inspect(capability, expected, now);
201
- stateOf(capability).consumed = true;
202
- return validated;
203
- },
204
- isConsumed(capability: MutationAuthorityCapabilityV2): boolean {
205
- return stateOf(capability).consumed;
161
+ try {
162
+ return inner.consume(capability, expected, now);
163
+ } catch (err) {
164
+ if (err instanceof Error && err.message.includes("not recognized by this registry"))
165
+ throw new Error("privileged action requires an opaque authority capability");
166
+ throw err;
167
+ }
206
168
  },
169
+ isConsumed: inner.isConsumed.bind(inner),
207
170
  };
208
171
  }