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
@@ -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 {
@@ -8,9 +8,10 @@ const PROTOCOL_MARKER = "<!-- immune-brain-tracker:v1 -->";
8
8
  const KIND_INITIATIVE_MARKER = "<!-- immune-brain:kind=initiative -->";
9
9
  const KIND_TASK_MARKER = "<!-- immune-brain:kind=task -->";
10
10
  const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
11
- const MAX_GH_OUTPUT = 1024 * 1024;
11
+ const MAX_GH_OUTPUT = 8 * 1024 * 1024;
12
12
  const MAX_DIAGNOSTIC = 512;
13
13
  const GH_TIMEOUT_MS = 20_000;
14
+ const MAX_SNAPSHOT_PAGES = 100;
14
15
  const GITHUB_ISSUE_BODY_LIMIT = 65_536;
15
16
  const MAX_TERMINAL_EVENT_ID = 500;
16
17
 
@@ -348,19 +349,33 @@ async function snapshot(root: string, gh: GhTransport, operation: TrackerOperati
348
349
  } catch (error) {
349
350
  return result(operation, "permanent_failure", error instanceof Error ? error.message : String(error));
350
351
  }
351
- const issuesExecution = await gh.run([
352
- "api",
353
- "--paginate",
354
- "--slurp",
355
- `repos/${repository.name_with_owner}/issues?state=all&per_page=100`,
356
- ], { cwd: root });
357
- if (issuesExecution.exit_code !== 0 || issuesExecution.output_exceeded)
358
- return ghFailure(operation, issuesExecution, "cannot query GitHub Issues");
359
- try {
360
- return { repository, issues: parseIssues(issuesExecution.stdout) };
361
- } catch (error) {
362
- return result(operation, "permanent_failure", error instanceof Error ? error.message : String(error));
352
+ // ponytail: paginated fetch avoids single 1MiB slurp blob; 100 issues/page * 8MiB handles 65KB bodies without hitting limit
353
+ const issues: GithubIssue[] = [];
354
+ for (let page = 1; page <= MAX_SNAPSHOT_PAGES; page += 1) {
355
+ const issuesExecution = await gh.run(
356
+ ["api", `repos/${repository.name_with_owner}/issues?state=all&per_page=100&page=${page}`],
357
+ { cwd: root },
358
+ );
359
+ if (issuesExecution.exit_code !== 0 || issuesExecution.output_exceeded)
360
+ return ghFailure(operation, issuesExecution, "cannot query GitHub Issues");
361
+ let raw: unknown;
362
+ try {
363
+ raw = JSON.parse(issuesExecution.stdout);
364
+ } catch (error) {
365
+ return result(operation, "permanent_failure", error instanceof Error ? error.message : String(error));
366
+ }
367
+ if (!Array.isArray(raw)) return result(operation, "permanent_failure", "gh returned malformed Issue list");
368
+ const pageCount = raw.length;
369
+ try {
370
+ issues.push(...parseIssues(issuesExecution.stdout));
371
+ } catch (error) {
372
+ return result(operation, "permanent_failure", error instanceof Error ? error.message : String(error));
373
+ }
374
+ if (pageCount < 100) break;
375
+ if (page === MAX_SNAPSHOT_PAGES)
376
+ return result(operation, "permanent_failure", "too many GitHub Issues to snapshot");
363
377
  }
378
+ return { repository, issues };
364
379
  }
365
380
 
366
381
  function findIssue(issues: GithubIssue[], primary: string[], required: string[]): IssueLookup {
@@ -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));