immune-brain 5.2.0 → 5.5.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 (77) hide show
  1. package/README.md +4 -4
  2. package/README.zh-CN.md +4 -4
  3. package/package.json +2 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
  6. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
  7. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
  8. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
  9. package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
  10. package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
  11. package/plugins/immune-brain/dist/BASELINE.md +17 -1
  12. package/plugins/immune-brain/dist/claude/mcp-server.mjs +16145 -12745
  13. package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
  14. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
  15. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  16. package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
  17. package/plugins/immune-brain/dist/imm-planner.md +63 -19
  18. package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
  19. package/plugins/immune-brain/dist/imm-run.md +89 -344
  20. package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
  21. package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
  22. package/plugins/immune-brain/hooks/hooks.json +22 -2
  23. package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
  24. package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
  25. package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
  26. package/plugins/immune-brain/runtime/assurance/phase_entry.ts +10 -26
  27. package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
  28. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
  29. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
  30. package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
  31. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
  32. package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
  33. package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
  34. package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
  35. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
  36. package/plugins/immune-brain/runtime/claude/lane_guard.ts +30 -20
  37. package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
  38. package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
  39. package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
  40. package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
  41. package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
  42. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
  43. package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
  44. package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
  45. package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
  46. package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
  47. package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
  48. package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
  49. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
  50. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
  51. package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
  52. package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
  53. package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
  54. package/plugins/immune-brain/runtime/plan_core.ts +15 -8
  55. package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
  56. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  57. package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
  58. package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
  59. package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
  60. package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
  61. package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
  62. package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
  63. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
  64. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
  65. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
  66. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
  67. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
  68. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
  69. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
  70. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
  71. package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
  72. package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
  73. package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
  74. package/plugins/immune-brain/skills/BASELINE.md +17 -1
  75. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -3
  76. package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -2
  77. package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
@@ -28,6 +28,7 @@ import { Type } from "typebox";
28
28
  import { PLUGIN_VERSION } from "../runtime/plugin_version";
29
29
  import { findResumableBatchSlugForTask } from "../runtime/unattended/batch_preflight";
30
30
  import { executePiUnattendedBatch } from "./imm-unattended-batch";
31
+ import { registerPiRunContinuation } from "./pi-run-continuation";
31
32
  import {
32
33
  } from "./pi-canary-verification";
33
34
  import {
@@ -102,10 +103,13 @@ import {
102
103
  import { inspectStorageLayout } from "../runtime/kernel/storage_paths";
103
104
  import { migrateLegacyLayout } from "../runtime/kernel/storage_layout_migration";
104
105
  import {
105
- readTaskIntent,
106
- parseTaskIntentV1,
106
+ assertScopeAdditionsAreFiles,
107
107
  canonicalIntentHash,
108
+ parseProjectTaskIntent,
109
+ parseTaskIntentV1,
110
+ readTaskIntent,
108
111
  type ReadTaskIntentResult,
112
+ withNextScope,
109
113
  } from "../runtime/kernel/intent";
110
114
  import { readBackendClaim, readTaskTombstone } from "../runtime/kernel/backend_claim";
111
115
  import {
@@ -121,8 +125,11 @@ import {
121
125
  } from "../runtime/kernel/canary_application";
122
126
  import { findingsDigestV2 } from "../runtime/kernel/reducer";
123
127
  import {
128
+ parseReworkChoice,
124
129
  projectAssurance,
130
+ reworkExitLines,
125
131
  type AssuranceProjectionResult,
132
+ type ReworkChoice,
126
133
  } from "../runtime/kernel/assurance_projection";
127
134
  import type { TaskRecord } from "../runtime/kernel/types";
128
135
  import type { RoleDelegationContext } from "../runtime/role_prompt_bridge";
@@ -131,7 +138,7 @@ import {
131
138
  runGithubTrackerOperation,
132
139
  type GithubTrackerResult,
133
140
  } from "../runtime/github_issue_tracker";
134
- import { reviewReworkFindings } from "../runtime/assurance/coordinator";
141
+ import { projectTaskStatus, reviewReworkFindings, type StatusProjectionResult } from "../runtime/assurance/coordinator";
135
142
  import { laneWriteRefusal } from "../runtime/unattended/lane_workspace";
136
143
  import { activeTaskNotice } from "../runtime/assurance/active_task_notice";
137
144
  import {
@@ -355,7 +362,7 @@ export function piRunEntryRefusal(
355
362
  ): Extract<PhaseAdmission, { ok: false }> | null {
356
363
  const sessionId = piSessionId(ctx);
357
364
  const state = sessionId ? piPhaseEntries().get(sessionId) ?? NO_PHASE_ENTRY : null;
358
- const admission = admitPhaseOperation({ kind: "tool", name: operation }, state, { activeClaim: false });
365
+ const admission = admitPhaseOperation({ kind: "tool", name: operation }, state);
359
366
  return admission.ok ? null : admission;
360
367
  }
361
368
 
@@ -421,6 +428,10 @@ export default function (
421
428
  return { action: "continue" } as const;
422
429
  });
423
430
 
431
+ // Validate dispatch before the receipt observer records an Agent call.
432
+ const continuation = registerPiRunContinuation(pi, {
433
+ entry: (text, root) => recognizePhaseEntry(text, piAliasReader(root)),
434
+ });
424
435
  pi.registerCommand("imm-tasks", {
425
436
  handler: async (_args: string, ctx?: ExtensionContext) => {
426
437
  if (!ctx || ctx.mode !== "tui") return;
@@ -658,12 +669,37 @@ export default function (
658
669
  ctx,
659
670
  taskId,
660
671
  await enrichAssuranceResult(ctx, taskId, result as unknown as Record<string, unknown>),
672
+ (slug) => continuation.selectedParent(ctx.cwd, ctx.sessionManager.getSessionId(), slug),
661
673
  );
662
674
  presentTaskRailResult(ctx, taskId, enriched);
663
675
  throwIfCanaryToolFailure(taskId, action.op, enriched);
664
676
  return toolResult(JSON.stringify(enriched, null, 2), enriched);
665
677
  }
666
- const projection = await projectAssuranceForTask(ctx.cwd, taskId);
678
+ const requested = requestedScope(action as { op: string; next_intent?: unknown });
679
+ if (requested) {
680
+ // The directory rule is the compatible route's own refusal; it runs
681
+ // before the projection so a directory's files are not reported as
682
+ // unstaged scope changes instead (ADR 0021, D4).
683
+ let prior: ReadTaskIntentResult | null = null;
684
+ try {
685
+ prior = readTaskIntentForRecord(ctx.cwd, taskId);
686
+ } catch {
687
+ // An unreadable intent is the projection's diagnostic to report.
688
+ }
689
+ if (prior) {
690
+ try {
691
+ assertScopeAdditionsAreFiles(ctx.cwd, prior.intent.scope_hint, requested);
692
+ } catch (error) {
693
+ const message = error instanceof Error ? error.message : String(error);
694
+ return failCanaryTool(taskId, action.op, "blocked", "scope_addition_is_directory", message, "name concrete files, or widen through approve_breaking_intent_revision");
695
+ }
696
+ }
697
+ }
698
+ // Status is a read: unstaged in-scope paths come back as a warning. Every
699
+ // other operation projects through the refusing delivery identity.
700
+ const projection: StatusProjectionResult = action.op === "status"
701
+ ? await projectTaskStatus(ctx.cwd, taskId)
702
+ : await projectAssuranceForTask(ctx.cwd, taskId, requested);
667
703
  if (projection.error) {
668
704
  const nextAction = recoveryActionForAssuranceFailure(projection.error) ?? "inspect authority state";
669
705
  const details = {
@@ -682,7 +718,8 @@ export default function (
682
718
  const blockers = state.blocking_finding_ids.length
683
719
  + state.unresolved_user_decision_ids.length
684
720
  + state.replan_required_ids.length;
685
- const status = { plugin_version: PLUGIN_VERSION, ...state };
721
+ const warnings = projection.warnings ?? [];
722
+ const status = { plugin_version: PLUGIN_VERSION, ...state, ...(warnings.length > 0 ? { warnings } : {}) };
686
723
  const details = {
687
724
  state: "status",
688
725
  operation: "status",
@@ -690,7 +727,8 @@ export default function (
690
727
  lifecycle: state.lifecycle,
691
728
  artifact_state: state.artifact_state,
692
729
  task_state: state,
693
- result: `${fresh}/${total} acceptance items fresh; ${blockers} blocker${blockers === 1 ? "" : "s"}`,
730
+ ...(warnings.length > 0 ? { warnings } : {}),
731
+ result: `${fresh}/${total} acceptance items fresh; ${blockers} blocker${blockers === 1 ? "" : "s"}${warnings.map((warning) => `; ${warning.message}`).join("")}`,
694
732
  next_action: state.next_obligation,
695
733
  };
696
734
  presentTaskRailResult(ctx, taskId, details);
@@ -812,6 +850,7 @@ export default function (
812
850
  type AuthorizationOutcome =
813
851
  | { state: "applied"; operation: AuthorizeOperation; lifecycle?: string; delivery_error?: string }
814
852
  | { state: "cancelled"; operation: AuthorizeOperation; reason: string }
853
+ | { state: "replan_selected"; operation: AuthorizeOperation; reason: string }
815
854
  | { state: "blocked"; reason: string };
816
855
 
817
856
  async function authorizeExactOperation(
@@ -826,7 +865,7 @@ export default function (
826
865
  let nextIntentRef: { path: string; content_hash: string } | undefined;
827
866
  if (operation === "approve-breaking-intent-revision") {
828
867
  try {
829
- nextIntent = await parseTaskIntentV1(nextIntentInput);
868
+ nextIntent = parseProjectTaskIntent(ctx.cwd, nextIntentInput);
830
869
  if (nextIntent.task_id !== taskId)
831
870
  throw new Error("next intent task_id must match the enrolled task");
832
871
  nextIntentHash = await canonicalIntentHash(nextIntent);
@@ -849,7 +888,7 @@ export default function (
849
888
  return { state: "blocked", reason };
850
889
  }
851
890
  try {
852
- const projection = await projectAssuranceForTask(ctx.cwd, taskId);
891
+ const projection = await projectAssuranceForTask(ctx.cwd, taskId, nextIntent?.scope_hint);
853
892
  if (projection.error || !projection.claim) {
854
893
  const reason = projection.error ?? "no active backend claim";
855
894
  notifyOnce(ctx, `authorization-claim:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
@@ -880,7 +919,11 @@ export default function (
880
919
  const stagedRecord = await readTaskRecordRaw(ctx.cwd, taskId);
881
920
  if (!stagedRecord.record || stagedRecord.revision !== projection.projection.record_revision)
882
921
  throw new Error("TaskRecord changed while preparing the breaking revision");
883
- stagedNextDiffHash = taskDeliveryIdentity(ctx.cwd, stagedRecord.record).diff_hash;
922
+ // The revision is bound against the scope it asks for (ADR 0021, D6).
923
+ stagedNextDiffHash = taskDeliveryIdentity(
924
+ ctx.cwd,
925
+ withNextScope(stagedRecord.record, nextIntent.scope_hint),
926
+ ).diff_hash;
884
927
  } catch (error) {
885
928
  let restoreError: unknown;
886
929
  try { restoreStagedIntent(); } catch (err) { restoreError = err; }
@@ -907,6 +950,9 @@ export default function (
907
950
  return { state: "blocked", reason };
908
951
  }
909
952
  }
953
+ // Only a replan boundary offers exits; both Hosts render the same ones.
954
+ const reworkExits = operation === "authorize-rework" ? projection.projection.authorization.exits : undefined;
955
+ let reworkChoice: ReworkChoice | undefined;
910
956
  const dialogSummary = [
911
957
  `Task: ${taskId}`,
912
958
  `Decision: ${operation}`,
@@ -932,7 +978,10 @@ export default function (
932
978
  ]
933
979
  : []),
934
980
  ...(operation === "authorize-rework"
935
- ? [`Replan boundaries: ${projection.projection.replan_required_ids.join(", ")}`]
981
+ ? [
982
+ `Replan boundaries: ${projection.projection.replan_required_ids.join(", ")}`,
983
+ ...(reworkExits ? reworkExitLines(reworkExits) : []),
984
+ ]
936
985
  : []),
937
986
  ...(nextIntent
938
987
  ? [
@@ -971,12 +1020,46 @@ export default function (
971
1020
  summary: dialogSummary,
972
1021
  details: dialogDetails,
973
1022
  signal: ctx.signal,
974
- actions: [
975
- { value: "authorize", label: uxText(UX_LANG, "Authorize", "批准"), description: uxText(UX_LANG, `Apply ${operation} after re-checking state`, `重新校验状态后应用 ${operation}`) },
976
- { value: "cancel", label: uxText(UX_LANG, "Cancel", "取消"), description: uxText(UX_LANG, "Leave managed authority unchanged", "保持托管权限状态不变") },
977
- ],
1023
+ actions: reworkExits
1024
+ ? [
1025
+ { value: "grant_rounds", label: uxText(UX_LANG, "Grant more Review rounds", "追加 Review 轮次"), description: uxText(UX_LANG, `Grant ${reworkExits.grant_rounds.min}-${reworkExits.grant_rounds.max} further rounds`, `追加 ${reworkExits.grant_rounds.min}-${reworkExits.grant_rounds.max} 轮`) },
1026
+ ...(reworkExits.accept_residual_risk
1027
+ ? [{ value: "accept_residual_risk", label: uxText(UX_LANG, "Accept residual risk", "接受残余风险"), description: uxText(UX_LANG, "Accept the open Review findings with a reason", "附理由接受未决 Review 发现") }]
1028
+ : []),
1029
+ { value: "replan", label: uxText(UX_LANG, "Replan", "重新规划"), description: uxText(UX_LANG, "Revise the Intent instead", "改为修订 Intent") },
1030
+ { value: "cancel", label: uxText(UX_LANG, "Cancel", "取消"), description: uxText(UX_LANG, "Leave managed authority unchanged", "保持托管权限状态不变") },
1031
+ ]
1032
+ : [
1033
+ { value: "authorize", label: uxText(UX_LANG, "Authorize", "批准"), description: uxText(UX_LANG, `Apply ${operation} after re-checking state`, `重新校验状态后应用 ${operation}`) },
1034
+ { value: "cancel", label: uxText(UX_LANG, "Cancel", "取消"), description: uxText(UX_LANG, "Leave managed authority unchanged", "保持托管权限状态不变") },
1035
+ ],
978
1036
  });
979
- confirmed = selected === "authorize";
1037
+ if (reworkExits) {
1038
+ if (selected === "grant_rounds" || selected === "accept_residual_risk" || selected === "replan") {
1039
+ const raw: Record<string, unknown> = { exit: selected };
1040
+ if (selected === "grant_rounds") {
1041
+ const text = await ctx.ui.input(
1042
+ uxText(UX_LANG, `Further Review rounds (${reworkExits.grant_rounds.min}-${reworkExits.grant_rounds.max})`, `追加 Review 轮次(${reworkExits.grant_rounds.min}-${reworkExits.grant_rounds.max})`),
1043
+ String(reworkExits.grant_rounds.min),
1044
+ { signal: ctx.signal },
1045
+ );
1046
+ if (text !== undefined && text.trim()) raw.rounds = Number(text.trim());
1047
+ if (text === undefined) raw.exit = "cancelled";
1048
+ } else if (selected === "accept_residual_risk") {
1049
+ const text = await ctx.ui.input(uxText(UX_LANG, "Reason for accepting residual risk", "接受残余风险的理由"), "", { signal: ctx.signal });
1050
+ if (text === undefined) raw.exit = "cancelled";
1051
+ else raw.reason = text;
1052
+ }
1053
+ // The same parser the Claude Host uses, so the accepted shapes cannot drift.
1054
+ reworkChoice = raw.exit === "cancelled" ? undefined : (parseReworkChoice(reworkExits, raw) ?? undefined);
1055
+ confirmed = reworkChoice !== undefined;
1056
+ if (raw.exit !== "cancelled" && !reworkChoice) {
1057
+ notifyOnce(ctx, `authorization-exit:${taskId}`, `authorize ${operation}: the chosen exit was not valid; nothing applied`, "warning");
1058
+ }
1059
+ }
1060
+ } else {
1061
+ confirmed = selected === "authorize";
1062
+ }
980
1063
  } catch (error) {
981
1064
  if (nextIntent) {
982
1065
  restoreStagedIntent();
@@ -1006,6 +1089,12 @@ export default function (
1006
1089
  progression.closeInvocation(invocation);
1007
1090
  return { state: "cancelled", operation, reason: "cancelled" };
1008
1091
  }
1092
+ // A replan is not a mutation of this operation: the Intent revision has
1093
+ // its own gate, so nothing is written here.
1094
+ if (reworkChoice?.exit === "replan") {
1095
+ progression.closeInvocation(invocation);
1096
+ return { state: "replan_selected", operation, reason: "revise the Intent, then authorize approve-breaking-intent-revision" };
1097
+ }
1009
1098
  if (!progression.sessionActiveValue() || progression.sessionGenerationValue() !== authorizationGeneration || progression.invocationState(invocation) !== "open") {
1010
1099
  if (nextIntent) {
1011
1100
  restoreStagedIntent();
@@ -1043,7 +1132,12 @@ export default function (
1043
1132
  next_intent_ref: nextIntentRef!,
1044
1133
  }
1045
1134
  : operation === "authorize-rework"
1046
- ? { op: "authorize_rework" as const }
1135
+ ? reworkChoice?.exit === "accept_residual_risk"
1136
+ ? { op: "accept_residual_risk" as const, reason: reworkChoice.reason }
1137
+ : {
1138
+ op: "authorize_rework" as const,
1139
+ ...(reworkChoice?.exit === "grant_rounds" ? { rounds: reworkChoice.rounds } : {}),
1140
+ }
1047
1141
  : userDecisionOperation!;
1048
1142
  // The exact host-built operation is shared by capability digest and
1049
1143
  // application payload; command arguments cannot inject authority fields.
@@ -1054,9 +1148,11 @@ export default function (
1054
1148
  const liveRecord = nextIntent ? await readTaskRecordRaw(ctx.cwd, taskId) : null;
1055
1149
  if (nextIntent && (!liveRecord?.record || liveRecord.revision !== projection.projection.record_revision || !stagedNextDiffHash))
1056
1150
  throw new Error("TaskRecord changed before the breaking revision digest");
1057
- const operationDiffHash = liveRecord?.record
1058
- ? taskDeliveryIdentity(ctx.cwd, liveRecord.record).diff_hash
1059
- : projection.projection.diff_hash;
1151
+ const operationDiffHash = liveRecord?.record && nextIntent
1152
+ ? taskDeliveryIdentity(ctx.cwd, withNextScope(liveRecord.record, nextIntent.scope_hint)).diff_hash
1153
+ : liveRecord?.record
1154
+ ? taskDeliveryIdentity(ctx.cwd, liveRecord.record).diff_hash
1155
+ : projection.projection.diff_hash;
1060
1156
  if (nextIntent && operationDiffHash !== stagedNextDiffHash)
1061
1157
  throw new Error("staged next-state diff changed after native confirmation");
1062
1158
  const capability = await mintCapability(registry, {
@@ -1075,7 +1171,8 @@ export default function (
1075
1171
  ...(exactOperation.op === "resolve_user_decision"
1076
1172
  ? { finding_id: exactOperation.finding_id, resolution: exactOperation.resolution }
1077
1173
  : {}),
1078
- ...(exactOperation.op === "stop" ? { reason: exactOperation.reason } : {}),
1174
+ ...(exactOperation.op === "stop" || exactOperation.op === "accept_residual_risk" ? { reason: exactOperation.reason } : {}),
1175
+ ...(exactOperation.op === "authorize_rework" && "rounds" in exactOperation ? { rounds: exactOperation.rounds } : {}),
1079
1176
  now,
1080
1177
  });
1081
1178
  const result = (await app.execute({
@@ -1083,13 +1180,15 @@ export default function (
1083
1180
  task_id: taskId,
1084
1181
  operation: { ...exactOperation, capability, actor_id: LITERAL_USER_ACTOR_ID } as never,
1085
1182
  prior_intent_token: priorIntent.token,
1086
- diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
1183
+ diffProvider: (root: string, record: TaskRecord) =>
1184
+ diffSnapshotOf(root, nextIntent ? withNextScope(record, nextIntent.scope_hint) : record),
1087
1185
  now,
1088
1186
  })) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
1089
1187
  if (exactOperation.op === "stop") progression.releaseStoppedReview(taskId);
1090
1188
  if (
1091
1189
  exactOperation.op === "stop" ||
1092
1190
  exactOperation.op === "authorize_rework" ||
1191
+ exactOperation.op === "accept_residual_risk" ||
1093
1192
  exactOperation.op === "approve_breaking_intent_revision"
1094
1193
  ) stagePlanningArtifactTransition(ctx.cwd, result.record);
1095
1194
  return { state: "applied", operation, lifecycle: result.record.lifecycle };
@@ -1241,8 +1340,8 @@ export function buildUserDecisionOperation(record: {
1241
1340
  * as the Claude adapter's `readTaskIntentForRecord`.
1242
1341
  */
1243
1342
  export function readTaskIntentForRecord(root: string, taskId: string): ReadTaskIntentResult {
1244
- const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref.path;
1245
- return readTaskIntent(root, taskId, currentPath);
1343
+ const record = readTaskRecordRaw(root, taskId).record;
1344
+ return readTaskIntent(root, taskId, record?.intent_ref.path, record?.intent_snapshot);
1246
1345
  }
1247
1346
 
1248
1347
  /**
@@ -1343,8 +1442,34 @@ function overviewRailState(lifecycle: string, obligation: string): TaskRailState
1343
1442
  return "Working";
1344
1443
  }
1345
1444
 
1346
- export async function projectAssuranceForTask(root: string, taskId: string): Promise<AssuranceProjectionResult> {
1347
- return projectAssurance(root, taskId);
1445
+ export async function projectAssuranceForTask(
1446
+ root: string,
1447
+ taskId: string,
1448
+ nextScope?: readonly string[],
1449
+ ): Promise<AssuranceProjectionResult> {
1450
+ // A scope revision is projected against the scope it asks for (ADR 0021, D6).
1451
+ return projectAssurance(
1452
+ root,
1453
+ taskId,
1454
+ nextScope
1455
+ ? (rootDir: string, record: TaskRecord) => diffSnapshotOf(rootDir, withNextScope(record, nextScope))
1456
+ : undefined,
1457
+ );
1458
+ }
1459
+
1460
+ /**
1461
+ * The canonical scope a `revise_intent` payload asks for. It goes through the
1462
+ * Intent parser so entries it would fold away (covered by a directory) match the
1463
+ * scope the Kernel later records; a payload the parser rejects is reported by
1464
+ * the revision path itself, not here.
1465
+ */
1466
+ function requestedScope(action: { op: string; next_intent?: unknown }): string[] | undefined {
1467
+ if (action.op !== "revise_intent") return undefined;
1468
+ try {
1469
+ return parseTaskIntentV1(action.next_intent).scope_hint;
1470
+ } catch {
1471
+ return undefined;
1472
+ }
1348
1473
  }
1349
1474
 
1350
1475
  /**
@@ -1418,6 +1543,7 @@ async function mintCapability(
1418
1543
  next_intent?: unknown;
1419
1544
  next_intent_ref?: unknown;
1420
1545
  reason?: string;
1546
+ rounds?: number;
1421
1547
  finding_id?: string;
1422
1548
  resolution?: string;
1423
1549
  now: string;
@@ -1433,6 +1559,7 @@ async function mintCapability(
1433
1559
  at: now,
1434
1560
  actor_id: input.actor_id,
1435
1561
  ...(input.reason !== undefined ? { reason: input.reason } : {}),
1562
+ ...(input.rounds !== undefined ? { rounds: input.rounds } : {}),
1436
1563
  ...(input.findings !== undefined ? { findings: input.findings } : {}),
1437
1564
  ...(input.approval !== undefined ? { approval: input.approval } : {}),
1438
1565
  ...(input.next_intent !== undefined ? { next_intent: input.next_intent } : {}),
@@ -1480,9 +1607,13 @@ async function executeOrdinaryOperation(
1480
1607
  ): Promise<unknown> {
1481
1608
  const { app } = await authorityPair();
1482
1609
  const operation = input.operation.op === "revise_intent"
1483
- ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
1610
+ ? { ...input.operation, next_intent: parseProjectTaskIntent(ctx.cwd, input.operation.next_intent) }
1484
1611
  : input.operation;
1485
1612
  const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
1613
+ const nextScope = operation.op === "revise_intent"
1614
+ ? (operation.next_intent as { scope_hint: string[] }).scope_hint
1615
+ : undefined;
1616
+ if (nextScope) assertScopeAdditionsAreFiles(ctx.cwd, priorIntent.intent.scope_hint, nextScope);
1486
1617
  const sidecar = join(ctx.cwd, priorIntent.intent_ref.path);
1487
1618
  const priorBytes = operation.op === "revise_intent" ? readFileSync(sidecar) : null;
1488
1619
  // A content-changing revision writes the sidecar before the kernel's drift
@@ -1503,7 +1634,8 @@ async function executeOrdinaryOperation(
1503
1634
  task_id: input.taskId,
1504
1635
  operation: operation as never,
1505
1636
  prior_intent_token: priorIntent.token,
1506
- diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
1637
+ diffProvider: (root: string, record: TaskRecord) =>
1638
+ diffSnapshotOf(root, nextScope ? withNextScope(record, nextScope) : record),
1507
1639
  now: new Date().toISOString(),
1508
1640
  });
1509
1641
  if (operation.op === "freeze_artifacts" || operation.op === "stop")
@@ -1548,6 +1680,7 @@ async function closeOutBatchChild(
1548
1680
  ctx: ExtensionContext,
1549
1681
  taskId: string,
1550
1682
  enriched: Record<string, unknown>,
1683
+ selectedParent: (slug: string) => boolean,
1551
1684
  ): Promise<Record<string, unknown>> {
1552
1685
  const taskState = enriched.task_state as { lifecycle?: unknown } | undefined;
1553
1686
  if (taskState?.lifecycle !== "done") return enriched;
@@ -1559,6 +1692,7 @@ async function closeOutBatchChild(
1559
1692
  }
1560
1693
  if (!slug) return enriched;
1561
1694
  try {
1695
+ if (!selectedParent(slug)) return enriched;
1562
1696
  // The batch commit takes exactly the scope envelope plus this task's own
1563
1697
  // audit evidence and refuses unstaged changes: stage only that evidence.
1564
1698
  if (existsSync(join(ctx.cwd, ".imm", "audit", taskId)))
@@ -1735,6 +1869,7 @@ function throwIfCanaryToolFailure(
1735
1869
  // Preserve only the coordinator's bounded observation metadata.
1736
1870
  {
1737
1871
  ...(details.diagnostics ? { diagnostics: details.diagnostics } : {}),
1872
+ ...(details.prepare_observation ? { prepare_observation: details.prepare_observation } : {}),
1738
1873
  ...(details.environment_failure ? { environment_failure: true } : {}),
1739
1874
  ...(details.recovery ? { recovery: details.recovery } : {}),
1740
1875
  ...(details.recovery_error ? { recovery_error: details.recovery_error } : {}),
@@ -12,12 +12,16 @@ import {
12
12
  } from "../runtime/kernel/batch_authority";
13
13
  import { createEnrollmentAuthorityRegistry } from "../runtime/kernel/enrollment_authority";
14
14
  import {
15
- startBatch,
15
+ startBatch, completedParentReport,
16
16
  type BatchRunnerKernelPort,
17
17
  type BatchRunReport,
18
18
  type BatchTrackerPort,
19
19
  } from "../runtime/unattended/batch_runner";
20
20
  import { createBatchKernelPort } from "../runtime/unattended/batch_kernel_port";
21
+ import { observeInitiative } from "../runtime/unattended/batch_plan";
22
+ import { integrateSettledCriticalSlices, advanceNativeParent } from "../runtime/unattended/batch_git";
23
+ import { parseDeliveryConsolidation, type DeliveryConsolidation } from "../runtime/unattended/parent_delivery_consolidation";
24
+
21
25
  import type { BatchRunnerGitPort } from "../runtime/unattended/batch_git";
22
26
  import type { InitiativeObservationReader } from "../runtime/unattended/types";
23
27
  import { advancePiTask, piRunEntryRefusal } from "./imm-canary-work";
@@ -27,7 +31,7 @@ import { parseLaneRevision, REVISION_DELEGATION_TEXT, type LaneRevisionRequest }
27
31
  import { createBatchTrackerPort, laneRuntimeContractRefusal, parseLaneInstruction, type LaneInstructionRequest, parseLaneOffers, parseMaxParallel, resolveLaneParallel, type LaneOffer } from "../runtime/unattended/batch_lanes";
28
32
  import {
29
33
  authorizeBatch,
30
- projectBatchPreflight,
34
+ prepareBatchPreflight,
31
35
  } from "../runtime/unattended/batch_preflight";
32
36
  import { retireStaleBatch, type StaleBatchFacts } from "../runtime/unattended/batch_disposition";
33
37
  import type { BatchLaneRunReport } from "../runtime/unattended/batch_state";
@@ -113,6 +117,7 @@ export interface PiBatchExecutionOptions {
113
117
  lane_offers?: LaneOffer[];
114
118
  /** Lane mode: the project's full verification commands, run once every child is integrated. */
115
119
  final_verification?: string[];
120
+ delivery_consolidation?: DeliveryConsolidation;
116
121
  /** Lane mode: one instruction for an idle Lane session, recorded before it is sent. */
117
122
  lane_instruction?: LaneInstructionRequest;
118
123
  /** Lane mode, ADR 0018: one breaking Intent revision the coordinator approves under the batch's grant. */
@@ -134,6 +139,7 @@ export interface PiBatchExecutionOptions {
134
139
  }
135
140
 
136
141
  export type PiBatchExecutionResult =
142
+ | NonNullable<Awaited<ReturnType<typeof advanceNativeParent>>>
137
143
  | { state: "started"; batch_id: string; report: BatchRunReport }
138
144
  | { state: "rejected"; reason: string; recovery_action: string }
139
145
  | { state: "cancelled"; reason: string; recovery_action: string }
@@ -154,13 +160,56 @@ function piConfirmationRef(input: {
154
160
 
155
161
  export async function executePiUnattendedBatch(
156
162
  options: PiBatchExecutionOptions,
163
+ ): Promise<PiBatchExecutionResult> {
164
+ let commit: string | undefined;
165
+ try {
166
+ const result = await executePreparedPiBatch(options, (value) => { commit = value; });
167
+ if (commit && "reason" in result) result.reason += `; batch precondition commit ${commit} remains`;
168
+ return result;
169
+ } catch (error) {
170
+ if (commit && error instanceof Error) error.message += `; batch precondition commit ${commit} remains`;
171
+ throw error;
172
+ }
173
+ }
174
+
175
+ async function executePreparedPiBatch(
176
+ options: PiBatchExecutionOptions, onCommit: (commit: string | undefined) => void,
157
177
  ): Promise<PiBatchExecutionResult> {
158
178
  const { root, initiativeSlug, signal } = options;
159
179
  const reuseOnly = options.reuseOnly === true;
160
180
  const interactive = options.interactive ?? true;
181
+ const consolidation = parseDeliveryConsolidation(options.delivery_consolidation);
161
182
 
162
183
  if (!reuseOnly && !interactive) return { state: "rejected", ...nonInteractiveRefusal() };
163
-
184
+ if (!reuseOnly && !options.confirmBatch) return batchReason("confirmation_port_unavailable");
185
+
186
+ await integrateSettledCriticalSlices(root, initiativeSlug, options.readInitiative);
187
+ const completed = await completedParentReport(root, initiativeSlug, options.readInitiative, options.batchTracker);
188
+ if (completed) return { state: "started", batch_id: completed.batch_id, report: completed };
189
+ const native = await advanceNativeParent(root, initiativeSlug, options.final_verification, options.readInitiative, options.batchTracker ?? createBatchTrackerPort(root, initiativeSlug),
190
+ async (effects) => {
191
+ // A continuation never opens a gate; unconfirmed commands wait for a fresh explicit entry.
192
+ if (reuseOnly || !options.confirmBatch) return batchReason("confirmation_port_unavailable");
193
+ let decision: "accept" | "accept_delegate" | "decline" | "cancel";
194
+ try {
195
+ decision = await options.confirmBatch({
196
+ title: `Authorize Parent Final Verification: ${effects.initiative_slug}`,
197
+ summary: `Initiative: ${effects.initiative_slug}\nPlan digest: ${effects.plan_digest}\nNo batch run is started.\nFinal verification commands run in this workspace outside Host permission:\n${effects.final_commands.map((command) => ` - ${command}`).join("\n")}`,
198
+ details: `Parent Tasks (${effects.children.length}):\n${effects.children.map((child) => ` - ${child.task_id} (${child.slice_id})`).join("\n")}${effects.integrate_task_ids.length > 0 ? `\n\nSettled deliveries committed first (${effects.integrate_task_ids.length}):\n${effects.integrate_task_ids.map((id) => ` - ${id}`).join("\n")}` : ""}`,
199
+ planDigest: effects.plan_digest,
200
+ ...(effects.delivery_consolidation ? { details: JSON.stringify(effects, null, 2) } : {}),
201
+ signal,
202
+ });
203
+ } catch (err) {
204
+ if (signal?.aborted) return batchReason("confirmation_cancelled");
205
+ return batchReason("confirmation_failed", err instanceof Error ? err.message : String(err));
206
+ }
207
+ if (decision === "cancel" || signal?.aborted) return batchReason("confirmation_cancelled");
208
+ if (decision === "decline") return batchReason("confirmation_declined");
209
+ if (decision !== "accept") return batchReason("confirmation_no_decision");
210
+ return null;
211
+ }, consolidation);
212
+ if (native) return native;
164
213
  // A lane_offers-only call resumes the recorded lane batch; refused before any gate otherwise.
165
214
  const resolvedParallel = resolveLaneParallel(root, initiativeSlug, options.max_parallel, options.lane_offers, options.lane_instruction ?? options.lane_revision);
166
215
  if (resolvedParallel !== undefined) options = { ...options, max_parallel: resolvedParallel };
@@ -170,12 +219,14 @@ export async function executePiUnattendedBatch(
170
219
  // plan digest, and base HEAD are one shared projection, so neither Host
171
220
  // re-implements a batch decision.
172
221
  let now = new Date().toISOString();
173
- const preflight = await projectBatchPreflight({
222
+ const preflight = await prepareBatchPreflight({
174
223
  root,
175
224
  initiative_slug: initiativeSlug,
176
225
  now,
177
226
  readInitiative: options.readInitiative,
227
+ signal, reuseOnly,
178
228
  });
229
+ onCommit(preflight.precondition_commit);
179
230
  if (!preflight.ok) {
180
231
  return { state: preflight.state, reason: preflight.reason, recovery_action: preflight.recovery_action };
181
232
  }
@@ -218,7 +269,7 @@ export async function executePiUnattendedBatch(
218
269
  }
219
270
  const confirmDetails = {
220
271
  title: `Authorize Unattended Batch: ${facts.initiative_slug}`,
221
- summary: `Initiative: ${facts.initiative_slug}\nBatch branch: ${facts.batch_branch}\nPlan digest: ${facts.plan_digest}\nBudget: max_children=${facts.budget.max_children}, qa_failure_limit=${facts.budget.qa_failure_limit}${options.max_parallel !== undefined ? `\nLane mode: max_parallel=${options.max_parallel}` : ""}${options.max_parallel !== undefined && options.final_verification ? `\nFinal verification: ${options.final_verification.join("; ")}` : ""}`,
272
+ summary: `Initiative: ${facts.initiative_slug}\nBatch branch: ${facts.batch_branch}\nPlan digest: ${facts.plan_digest}\nBudget: max_children=${facts.budget.max_children}, qa_failure_limit=${facts.budget.qa_failure_limit}${options.max_parallel !== undefined ? `\nLane mode: max_parallel=${options.max_parallel}` : ""}${options.final_verification ? `\nFinal verification: ${options.final_verification.join("; ")}` : ""}`,
222
273
  details: `Ordered children (${facts.children.length}):\n${facts.children.map((child) => ` - ${child.task_id} (${child.slice_id}) [risk: ${child.risk}] [status: ${child.status === "already_settled" ? "completed" : "pending execution"}]`).join("\n")}${facts.excluded.length > 0 ? `\n\nExcluded children:\n${facts.excluded.map((child) => ` - ${child.task_id} (${child.slice_id}): ${child.reason}`).join("\n")}` : ""}${options.max_parallel !== undefined ? `\n\nParallel groups (${facts.parallel_groups.length}):\n${facts.parallel_groups.map((group) => ` - ${group.join(", ")}`).join("\n")}${facts.scope_conflicts.length > 0 ? `\n\nSerialized by overlapping scope:\n${facts.scope_conflicts.map((c) => ` - ${c.task_id} after ${c.overlaps_with.join(", ")}`).join("\n")}` : ""}` : ""}${facts.reuse_blockers.length > 0 ? `\n\nRe-confirmation required: ${facts.reuse_blockers.join(", ")}.\nRecovery: confirm to issue a fresh authorization bound to the current plan and HEAD.` : ""}`,
223
274
  planDigest: facts.plan_digest,
224
275
  signal,
@@ -294,6 +345,7 @@ export async function executePiUnattendedBatch(
294
345
  // this Host supplies only its own advanceTask progression seam (D5).
295
346
  const kernelPort = createBatchKernelPort({
296
347
  root,
348
+ parent: await (options.readInitiative ?? observeInitiative)(root, initiativeSlug),
297
349
  enrollmentRegistry,
298
350
  registry: batchRegistry,
299
351
  capability,
@@ -303,7 +355,7 @@ export async function executePiUnattendedBatch(
303
355
  overrides: otherOverrides,
304
356
  });
305
357
 
306
- const report = await startBatch({
358
+ let report = await startBatch({
307
359
  root,
308
360
  batch_id: batchId,
309
361
  initiative_slug: initiativeSlug,
@@ -317,11 +369,11 @@ export async function executePiUnattendedBatch(
317
369
  now,
318
370
  kernel: kernelPort,
319
371
  git: options.batchGit,
372
+ ...(options.final_verification ? { final_verification: options.final_verification } : {}),
320
373
  ...(options.max_parallel !== undefined
321
374
  ? {
322
375
  max_parallel: options.max_parallel,
323
376
  tracker: options.batchTracker ?? createBatchTrackerPort(root, initiativeSlug),
324
- ...(options.final_verification ? { final_verification: options.final_verification } : {}),
325
377
  ...(options.lane_instruction ? { lane_instruction: options.lane_instruction } : {}),
326
378
  ...(options.lane_revision ? { lane_revision: options.lane_revision } : {}),
327
379
  ...(authorization.revision_delegation !== undefined ? { revision_delegation: authorization.revision_delegation } : {}),
@@ -334,6 +386,8 @@ export async function executePiUnattendedBatch(
334
386
  return batchReason("batch_run_rejected", report.reason ?? "");
335
387
  }
336
388
 
389
+ if (report.batch_state === "completed" && report.final_verification?.passed)
390
+ report = await completedParentReport(root, initiativeSlug, options.readInitiative, options.batchTracker) ?? report;
337
391
  return {
338
392
  state: "started",
339
393
  batch_id: batchId,
@@ -362,7 +416,7 @@ function retireConfirmationDetails(facts: StaleBatchFacts): {
362
416
  details: `Children (${facts.children.length}):\n${facts.children
363
417
  .map(
364
418
  (child) =>
365
- ` - ${child.task_id} (${child.slice_id}) [state: ${child.state}]${child.commit ? ` [commit: ${child.commit}]` : " [no commit]"}${child.lane_branch ? ` [lane: ${child.lane_branch}]` : ""}`,
419
+ ` - ${child.task_id} (${child.slice_id}) [state: ${child.state}]${child.commit ? ` [commit: ${child.commit}]` : " [no commit]"}${child.lane_branch ? ` [lane: ${child.lane_branch}]` : ""}${child.kernel_lifecycle ? ` [Kernel run: ${child.kernel_lifecycle}]` : ""}`,
366
420
  )
367
421
  .join("\n")}\n\nRetiring writes this record to the terminal superseded state. Child states, recorded commits and the batch branch are preserved as evidence; nothing is deleted and no batch trailer or approval is manufactured.\n\nA child without a commit was never executed by this batch: it carries no delivery evidence, and the retired authorization is never reused for a revised plan.\n\nAfter retiring, run each remaining child with imm-run under its own authorization, or start a new batch for this Initiative.`,
368
422
  };
@@ -437,6 +491,11 @@ export default function (
437
491
  lane_offers: Type.Optional(Type.Array(
438
492
  Type.Object({ task_id: Type.String(), path: Type.String() }, { additionalProperties: false }),
439
493
  )),
494
+ delivery_consolidation: Type.Optional(Type.Object({
495
+ task_id: Type.String(), run_id: Type.String(), source_runs: Type.Array(Type.Object({
496
+ task_id: Type.String(), run_id: Type.String(),
497
+ }, { additionalProperties: false }), { minItems: 1, maxItems: 16 }),
498
+ }, { additionalProperties: false })),
440
499
  final_verification: Type.Optional(Type.Array(Type.String(), { minItems: 1, maxItems: 8 })),
441
500
  lane_instruction: Type.Optional(Type.Object({
442
501
  task_id: Type.String(),
@@ -451,7 +510,7 @@ export default function (
451
510
  }, { additionalProperties: false }),
452
511
  execute: async (
453
512
  _toolCallId: string,
454
- params: { initiative_slug: string; max_parallel?: number; lane_offers?: Array<{ task_id: string; path: string }>; final_verification?: string[]; lane_instruction?: unknown; lane_revision?: unknown },
513
+ params: { initiative_slug: string; max_parallel?: number; lane_offers?: Array<{ task_id: string; path: string }>; final_verification?: string[]; delivery_consolidation?: unknown; lane_instruction?: unknown; lane_revision?: unknown },
455
514
  signal: AbortSignal | undefined,
456
515
  _onUpdate: unknown,
457
516
  ctx: ExtensionContext,
@@ -486,6 +545,7 @@ export default function (
486
545
  const maxParallel = parseMaxParallel(params.max_parallel);
487
546
  const laneOffers = parseLaneOffers(params.lane_offers);
488
547
  const finalVerification = parseFinalVerification(params.final_verification);
548
+ const deliveryConsolidation = parseDeliveryConsolidation(params.delivery_consolidation);
489
549
  const laneInstruction = parseLaneInstruction(params.lane_instruction);
490
550
  const laneRevision = parseLaneRevision(params.lane_revision);
491
551
 
@@ -495,6 +555,7 @@ export default function (
495
555
  ...(maxParallel !== undefined ? { max_parallel: maxParallel } : {}),
496
556
  ...(laneOffers !== undefined ? { lane_offers: laneOffers } : {}),
497
557
  ...(finalVerification !== undefined ? { final_verification: finalVerification } : {}),
558
+ ...(deliveryConsolidation !== undefined ? { delivery_consolidation: deliveryConsolidation } : {}),
498
559
  ...(laneInstruction !== undefined ? { lane_instruction: laneInstruction } : {}),
499
560
  ...(laneRevision !== undefined ? { lane_revision: laneRevision } : {}),
500
561
  interactive: ctx.mode === "tui",
@@ -537,7 +598,7 @@ export default function (
537
598
  },
538
599
  });
539
600
 
540
- if (result.state !== "started") {
601
+ if (result.state !== "started" && result.state !== "native_parent") {
541
602
  const failureState = result.state === "blocked" ? "blocked" : "failed";
542
603
  throwToolFailure({
543
604
  tool: "imm_canary_enrollment",
@@ -1,7 +1,7 @@
1
1
  import type { AssuranceAdvanceResult } from "../runtime/assurance/coordinator";
2
2
 
3
3
  export type AssuranceFailureDetails = Pick<AssuranceAdvanceResult,
4
- "diagnostics" | "environment_failure" | "recovery" | "recovery_error">;
4
+ "diagnostics" | "prepare_observation" | "environment_failure" | "recovery" | "recovery_error">;
5
5
 
6
6
  export interface ToolFailureV1 extends AssuranceFailureDetails {
7
7
  contract: "immune_brain/tool_failure/v1";