immune-brain 4.5.0 → 5.0.0

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 (79) hide show
  1. package/README.md +157 -39
  2. package/README.zh-CN.md +160 -40
  3. package/package.json +1 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +42 -45
  6. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +189 -476
  7. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +77 -102
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +7 -2
  9. package/plugins/immune-brain/.pi-extension/tsconfig.json +2 -2
  10. package/plugins/immune-brain/agents/immune-brain-advisory-reviewer.md +23 -0
  11. package/plugins/immune-brain/agents/immune-brain-qa.md +42 -0
  12. package/plugins/immune-brain/agents/immune-brain-reviewer.md +90 -1
  13. package/plugins/immune-brain/agents/immune-brain-ui-review.md +21 -0
  14. package/plugins/immune-brain/dist/BASELINE.md +4 -4
  15. package/plugins/immune-brain/dist/claude/mcp-server.mjs +9924 -9654
  16. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +3 -3
  17. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -1
  18. package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
  19. package/plugins/immune-brain/dist/imm-doc-prune.md +2 -2
  20. package/plugins/immune-brain/dist/{imm-agent-doc-maintain.md → imm-doc-slim.md} +9 -9
  21. package/plugins/immune-brain/dist/imm-planner.md +60 -20
  22. package/plugins/immune-brain/dist/imm-pr-fix.md +1 -1
  23. package/plugins/immune-brain/dist/{imm-review-retro.md → imm-retro.md} +26 -13
  24. package/plugins/immune-brain/dist/{imm-loop.md → imm-run.md} +44 -11
  25. package/plugins/immune-brain/dist/registry.yaml +9 -9
  26. package/plugins/immune-brain/dist/role-prompts/code-review.md +7 -0
  27. package/plugins/immune-brain/dist/role-prompts/executor.md +35 -2
  28. package/plugins/immune-brain/runtime/assurance/coordinator.ts +138 -69
  29. package/plugins/immune-brain/runtime/assurance/host_port.ts +20 -0
  30. package/plugins/immune-brain/runtime/assurance/qa.ts +2 -2
  31. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
  32. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +347 -0
  33. package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
  34. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +135 -396
  35. package/plugins/immune-brain/runtime/claude/mcp_server.ts +5 -4
  36. package/plugins/immune-brain/runtime/claude/review_host.ts +63 -16
  37. package/plugins/immune-brain/runtime/claude/role_agents.ts +83 -0
  38. package/plugins/immune-brain/runtime/commands/kernel.ts +3 -6
  39. package/plugins/immune-brain/runtime/github_issue_tracker.ts +738 -677
  40. package/plugins/immune-brain/runtime/kernel/application.ts +7 -7
  41. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +15 -8
  42. package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
  43. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
  44. package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -5
  45. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
  46. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
  47. package/plugins/immune-brain/runtime/kernel/enrollment.ts +170 -17
  48. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
  49. package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
  50. package/plugins/immune-brain/runtime/kernel/storage.ts +170 -11
  51. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +13 -0
  52. package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
  53. package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
  54. package/plugins/immune-brain/runtime/loop_contract.ts +22 -22
  55. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  56. package/plugins/immune-brain/runtime/prompts/code-review.md +7 -0
  57. package/plugins/immune-brain/runtime/prompts/executor.md +35 -2
  58. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +27 -0
  59. package/plugins/immune-brain/runtime/unattended/batch_git.ts +66 -28
  60. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
  61. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
  62. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +132 -45
  63. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
  64. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +40 -36
  65. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +98 -209
  66. package/plugins/immune-brain/runtime/unattended/batch_state.ts +36 -14
  67. package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
  68. package/plugins/immune-brain/runtime/workspace_scope.ts +78 -8
  69. package/plugins/immune-brain/skills/BASELINE.md +4 -4
  70. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +1 -1
  71. package/plugins/immune-brain/skills/{imm-agent-doc-maintain → imm-doc-slim}/SKILL.md +9 -9
  72. package/plugins/immune-brain/skills/imm-planner/SKILL.md +4 -2
  73. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +1 -1
  74. package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/SKILL.md +7 -7
  75. package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/scripts/review_retro.ts +46 -21
  76. package/plugins/immune-brain/skills/{imm-loop → imm-run}/SKILL.md +7 -7
  77. package/plugins/immune-brain/skills/registry.yaml +9 -9
  78. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -584
  79. package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
@@ -1,3 +1,7 @@
1
+ import { buildAssuranceSnapshot, stagePlanningArtifactTransition } from "../runtime/assurance/verdict_authority";
2
+ export { buildSnapshot, ensureReviewRevision as ensureTaskReviewRevision } from "../runtime/assurance/verdict_authority";
3
+ export type { SnapshotDescriptorInput } from "../runtime/assurance/verdict_authority";
4
+ import type { ConfirmationReferenceSource } from "../runtime/assurance/host_port";
1
5
  // P3 Pi lifecycle extension: the only production route for Kernel canary
2
6
  // assurance after enrollment.
3
7
  //
@@ -21,19 +25,14 @@ import { join, resolve } from "node:path";
21
25
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
22
26
  import { Type } from "typebox";
23
27
  import { PLUGIN_VERSION } from "../runtime/plugin_version";
28
+ import { findResumableBatchSlugForTask } from "../runtime/unattended/batch_preflight";
29
+ import { executePiUnattendedBatch } from "./imm-unattended-batch";
24
30
  import {
25
- parseVerificationDescriptor,
26
- type VerificationDescriptor,
27
31
  } from "./pi-canary-verification";
28
32
  import {
29
- captureReviewBundle,
30
- captureReviewManifest,
31
33
  listReviewRefs,
32
34
  reconcileReviewRefs,
33
35
  writeNativeReviewEvidence,
34
- type ReviewBundle,
35
- type ReviewManifestV5,
36
- type ReviewRevision,
37
36
  } from "./pi-canary-review-bundle";
38
37
  import type { InvocationToken } from "./pi-canary-invocations";
39
38
  import { runDeterministicQa } from "../runtime/assurance/qa";
@@ -44,7 +43,6 @@ import {
44
43
  import {
45
44
  renderCanaryCall,
46
45
  renderCanaryResult,
47
- type AssuranceRole,
48
46
  } from "./pi-canary-assurance";
49
47
  import {
50
48
  USER_ATTENTION_EVENT,
@@ -69,8 +67,7 @@ import { resolveUxLanguage, uxText } from "./ux-language";
69
67
 
70
68
  /** Host-native UI language; see ux-language.ts. Resolved once per process. */
71
69
  const UX_LANG = resolveUxLanguage();
72
- import { taskDiffIdentity, taskRevisionIdentity, captureGitTaskSnapshot } from "../runtime/workspace_scope";
73
- import { reviewAdvisoryRecords, reviewReworkFindings } from "../runtime/assurance/coordinator";
70
+ import { taskDeliveryIdentity } from "../runtime/workspace_scope";
74
71
  import {
75
72
  AssuranceProgression,
76
73
  buildReviewPrompt,
@@ -85,44 +82,54 @@ import {
85
82
  REVIEW_TIMING_PROFILES,
86
83
  REVIEW_VERDICT_VALIDATION_TIMEOUT_MS,
87
84
  type AssuranceProgressionPorts,
88
- type AssuranceVerdict,
89
85
  type HostContext,
90
- type SnapshotDescriptor,
91
86
  } from "./pi-canary-assurance-progression";
92
87
 
93
- // The Kernel runtime graph is never type-checked from this extension: static
94
- // imports resolve to ./runtime-stub.ts (relative so the Pi extension loader
95
- // can resolve them at runtime), and the stub forwards to the real Kernel
96
- // modules via dynamic import.
88
+ // The Pi adapter imports the host-neutral runtime modules statically, exactly
89
+ // as the Claude adapter does; the retired dynamic-import isolation layer is
90
+ // gone. The adapter-owned logic that used to live behind it (the
91
+ // record-aware intent read, the tracker terminal wrapper, the shared batch
92
+ // progression accessors) is defined below, next to the machinery it uses.
93
+ import { LITERAL_USER_ACTOR_ID } from "../runtime/kernel/actor_identity";
97
94
  import {
98
- createMutationAuthorityRegistry,
99
- createCanaryApplication,
100
- buildLoopAction,
101
- buildLoopRoleDispatch,
102
- readBackendClaim,
103
- readTaskTombstone,
104
- markGithubTaskTerminal,
105
95
  reconcileKernelAuthority,
106
96
  repairKernelAuthority,
107
- readTaskRecord,
97
+ readTaskRecordRaw,
108
98
  withKernelStoreLockForTask,
109
- inspectStorageLayout,
110
- migrateLegacyLayout,
99
+ } from "../runtime/kernel/storage";
100
+ import { inspectStorageLayout } from "../runtime/kernel/storage_paths";
101
+ import { migrateLegacyLayout } from "../runtime/kernel/storage_layout_migration";
102
+ import {
111
103
  readTaskIntent,
112
104
  parseTaskIntentV1,
113
105
  canonicalIntentHash,
114
- projectAssurance,
115
- findingsDigestV2,
116
- capabilityActionFor,
106
+ type ReadTaskIntentResult,
107
+ } from "../runtime/kernel/intent";
108
+ import { readBackendClaim, readTaskTombstone } from "../runtime/kernel/backend_claim";
109
+ import {
110
+ createMutationAuthorityRegistry,
117
111
  digestOfAction,
118
- type AssuranceProjectionResult,
119
- type TaskRecordRead,
120
- type CanaryApplication,
121
- type MutationAuthorityRegistry,
122
112
  type CapabilityBindingV2,
123
- LITERAL_USER_ACTOR_ID,
124
- } from "./runtime-stub";
125
- import { invocationRegistry } from "./pi-canary-assurance-progression";
113
+ type MutationAuthorityRegistry,
114
+ } from "../runtime/kernel/authority_port";
115
+ import {
116
+ createCanaryApplication,
117
+ capabilityActionFor,
118
+ type CanaryApplication,
119
+ } from "../runtime/kernel/canary_application";
120
+ import { findingsDigestV2 } from "../runtime/kernel/reducer";
121
+ import {
122
+ projectAssurance,
123
+ type AssuranceProjectionResult,
124
+ } from "../runtime/kernel/assurance_projection";
125
+ import type { TaskRecord } from "../runtime/kernel/types";
126
+ import type { RoleDelegationContext } from "../runtime/role_prompt_bridge";
127
+ import { buildLoopAction, buildLoopRoleDispatch } from "../runtime/loop_contract";
128
+ import {
129
+ runGithubTrackerOperation,
130
+ type GithubTrackerResult,
131
+ } from "../runtime/github_issue_tracker";
132
+ import { reviewReworkFindings } from "../runtime/assurance/coordinator";
126
133
 
127
134
  const LOOP_OWNERS = ["plan", "kernel", "brainstorm", "planner", "loop"] as const;
128
135
  const LOOP_TARGETS = [
@@ -176,60 +183,6 @@ export type AuthorizeOperation =
176
183
  | "authorize-rework"
177
184
  | "stop";
178
185
 
179
- export interface SnapshotDescriptorInput {
180
- root: string;
181
- task_id: string;
182
- role: AssuranceRole;
183
- run_id?: string | null;
184
- record_revision: string;
185
- workspace_revision: string;
186
- intent_revision: number;
187
- intent_content_hash: string;
188
- diff_hash: string;
189
- lifecycle: string;
190
- artifact_state: string;
191
- risk?: "routine" | "material" | "critical";
192
- fresh_acceptance_ids: string[];
193
- missing_acceptance_ids: string[];
194
- stale_attestation_ids: string[];
195
- acceptance: Array<{ id: string; assertion: string; verification: string }>;
196
- dirty_files?: string[];
197
- review_bundle_digest?: string | null;
198
- /** Present only when Review authority binds an immutable Git revision (v4). */
199
- review_revision?: {
200
- contract: "assurance_kernel/review_revision_identity/v1";
201
- base_head: string;
202
- review_commit: string;
203
- review_tree: string;
204
- manifest_digest: string;
205
- };
206
- }
207
-
208
- export function buildSnapshot(input: SnapshotDescriptorInput): SnapshotDescriptor {
209
- return {
210
- contract: "assurance_kernel/assurance_snapshot/v2",
211
- task_id: input.task_id,
212
- run_id: input.run_id ?? null,
213
- role: input.role,
214
- record_revision: input.record_revision,
215
- workspace_revision: input.workspace_revision,
216
- intent_revision: input.intent_revision,
217
- intent_content_hash: input.intent_content_hash,
218
- diff_hash: input.diff_hash,
219
- lifecycle: input.lifecycle,
220
- artifact_state: input.artifact_state,
221
- risk: input.risk ?? "material",
222
- fresh_acceptance_ids: input.fresh_acceptance_ids,
223
- missing_acceptance_ids: input.missing_acceptance_ids,
224
- stale_attestation_ids: input.stale_attestation_ids,
225
- acceptance: input.acceptance,
226
- dirty_files: [...(input.dirty_files ?? [])].sort(),
227
- review_bundle_digest: input.review_bundle_digest ?? null,
228
- ...(input.review_revision ? { review_revision: input.review_revision } : {}),
229
- root: resolve(input.root),
230
- };
231
- }
232
-
233
186
  export interface CanaryWorkExtensionDependencies {
234
187
  buildAssurance?: typeof buildAssuranceSnapshot;
235
188
  runQa?: typeof runDeterministicQa;
@@ -275,25 +228,15 @@ export function createPiAssuranceProgressionPorts(
275
228
  dependencies: CanaryWorkExtensionDependencies = {},
276
229
  ): AssuranceProgressionPorts {
277
230
  return {
278
- projectTask: (root, taskId) => projectAssuranceState(root, taskId),
279
- readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
280
- readTaskIntent: (root, taskId) => readTaskIntent(root, taskId),
281
- buildAssurance: (root, taskId, role, projection) =>
282
- (dependencies.buildAssurance ?? buildAssuranceSnapshot)(root, taskId, role, projection),
283
- ensureReviewRevision: (root, taskId, projection) => ensureTaskReviewRevision(root, taskId, projection),
231
+ confirmationReference: piConfirmationReference,
232
+ onReworkApplied: (ctx, taskId, count) => notifyHost(ctx, `rework-parked:${taskId}`, `rework applied: review parked for replan with ${count} finding(s)`, "warning"),
233
+ projectTask: (root, taskId) => projectAssuranceForTask(root, taskId),
234
+ readTaskRecord: async (root, taskId) => readTaskRecordRaw(root, taskId),
235
+ readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
284
236
  runQa: (snapshot, descriptors, options) =>
285
237
  (dependencies.runQa ?? runDeterministicQa)(snapshot, descriptors, options),
286
238
  writeReviewEvidence: (input) =>
287
239
  (dependencies.writeReviewEvidence ?? writeNativeReviewEvidence)(input),
288
- applyVerdict: (ctx, input) =>
289
- applyAssuranceVerdict(
290
- ctx,
291
- input.snapshot,
292
- input.verdict,
293
- input.invocation,
294
- input.actorId,
295
- input.hooks,
296
- ),
297
240
  applyOrdinaryOperation: (ctx, input) => executeOrdinaryOperation(ctx, input),
298
241
  advanceBeforeProjection: dependencies.advanceBeforeProjection,
299
242
  qaBeforeProjection: dependencies.qaBeforeProjection,
@@ -301,11 +244,46 @@ export function createPiAssuranceProgressionPorts(
301
244
  qaOnAuthorityCommit: dependencies.qaOnAuthorityCommit,
302
245
  qaAfterAuthorityCommit: dependencies.qaAfterAuthorityCommit,
303
246
  qaJobTimeoutMs: dependencies.qaJobTimeoutMs,
247
+ ...(dependencies.buildAssurance ? { authorityOverrides: { buildAssurance: dependencies.buildAssurance } } : {}),
304
248
  } satisfies AssuranceProgressionPorts;
305
249
  }
306
250
 
307
251
  const GLOBAL_PI_PROGRESSION_KEY = Symbol.for("immune_brain.pi_assurance_progression");
308
252
 
253
+ /**
254
+ * The shared session progression the batch adapter advances children through.
255
+ * The work extension publishes the instance at load; this accessor is the
256
+ * lazy fallback that builds one from the production ports when no extension
257
+ * factory has run (tests, direct batch drives). Both paths publish under the
258
+ * same global key, so a batch Review reservation and the session's
259
+ * submit_review always share one progression instance.
260
+ */
261
+ export async function getSharedPiProgression(): Promise<AssuranceProgression> {
262
+ let progression = (globalThis as any)[GLOBAL_PI_PROGRESSION_KEY] as AssuranceProgression | undefined;
263
+ if (!progression) {
264
+ progression = new AssuranceProgression(createPiAssuranceProgressionPorts());
265
+ (globalThis as any)[GLOBAL_PI_PROGRESSION_KEY] = progression;
266
+ }
267
+ return progression;
268
+ }
269
+
270
+ /**
271
+ * Advance one task through the shared session progression and translate the
272
+ * progression result into the batch runner's advance report shape.
273
+ */
274
+ export async function advancePiTask(root: string, taskId: string): Promise<any> {
275
+ const progression = await getSharedPiProgression();
276
+ const result = await progression.advance(taskId, { cwd: root });
277
+ const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
278
+ if (result.state === "completed") return { state: "completed", ...facts };
279
+ if (result.state === "stopped") return { state: "stopped", ...facts };
280
+ if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
281
+ if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, agent_params: result.agent_params, ...facts };
282
+ if (result.state === "review_preparation_failed") return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
283
+ if (result.state === "blocked") return { state: "blocked", reason: result.reason, ...facts };
284
+ return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
285
+ }
286
+
309
287
  export default function (
310
288
  pi: ExtensionAPI,
311
289
  dependencies: CanaryWorkExtensionDependencies = {},
@@ -321,7 +299,7 @@ export default function (
321
299
  try {
322
300
  const claim = await readBackendClaim(ctx.cwd);
323
301
  if (!claim) return;
324
- const projection = await projectAssuranceState(ctx.cwd, claim.task_id);
302
+ const projection = await projectAssuranceForTask(ctx.cwd, claim.task_id);
325
303
  if (projection.error) {
326
304
  presentTaskRail(ctx, {
327
305
  task_id: claim.task_id,
@@ -581,12 +559,16 @@ export default function (
581
559
  : action.op === "request_stop"
582
560
  ? await authorizeExactOperation(taskId, "stop", { ...ctx, signal: signal && ctx.signal ? AbortSignal.any([signal, ctx.signal]) : signal ?? ctx.signal })
583
561
  : await requestAuthorization(taskId, ctx);
584
- const enriched = await enrichAssuranceResult(ctx, taskId, result as unknown as Record<string, unknown>);
562
+ const enriched = await closeOutBatchChild(
563
+ ctx,
564
+ taskId,
565
+ await enrichAssuranceResult(ctx, taskId, result as unknown as Record<string, unknown>),
566
+ );
585
567
  presentTaskRailResult(ctx, taskId, enriched);
586
568
  throwIfCanaryToolFailure(taskId, action.op, enriched);
587
569
  return toolResult(JSON.stringify(enriched, null, 2), enriched);
588
570
  }
589
- const projection = await projectAssuranceState(ctx.cwd, taskId);
571
+ const projection = await projectAssuranceForTask(ctx.cwd, taskId);
590
572
  if (projection.error) {
591
573
  const nextAction = recoveryActionForAssuranceFailure(projection.error) ?? "inspect authority state";
592
574
  const details = {
@@ -628,7 +610,7 @@ export default function (
628
610
  taskId,
629
611
  operation: toCanaryOperation(action, "executor") as { op: string; actor_id: string },
630
612
  })) as unknown as { revision: string; record: { lifecycle: string; artifact_state: string } };
631
- const updated = await projectAssuranceState(ctx.cwd, taskId);
613
+ const updated = await projectAssuranceForTask(ctx.cwd, taskId);
632
614
  const taskState = updated.error
633
615
  ? { lifecycle: result.record.lifecycle, artifact_state: result.record.artifact_state }
634
616
  : updated.projection;
@@ -704,15 +686,18 @@ export default function (
704
686
  _ctx: ExtensionContext,
705
687
  ) => {
706
688
  const { action } = params;
689
+ // The Tool schema is intentionally broader than loop_contract's input
690
+ // types (model-supplied JSON); loop_contract narrows and validates at
691
+ // runtime, so these boundary casts are type-level only.
707
692
  const result = action.op === "route"
708
693
  ? await buildLoopAction({
709
694
  ownership: action.ownership,
710
695
  target: action.target,
711
- context: action.context,
696
+ context: action.context as RoleDelegationContext | undefined,
712
697
  scope_expansion: action.scope_expansion,
713
- kernel_operation: action.kernel_operation,
698
+ kernel_operation: action.kernel_operation as Parameters<typeof buildLoopAction>[0]["kernel_operation"],
714
699
  })
715
- : await buildLoopRoleDispatch({ role: action.role, context: action.context });
700
+ : await buildLoopRoleDispatch({ role: action.role, context: action.context as RoleDelegationContext });
716
701
  const details = loopResultDetails(result, action.op);
717
702
  return toolResult(JSON.stringify(result, null, 2), details);
718
703
  },
@@ -769,14 +754,14 @@ export default function (
769
754
  return { state: "blocked", reason };
770
755
  }
771
756
  try {
772
- const projection = await projectAssuranceState(ctx.cwd, taskId);
757
+ const projection = await projectAssuranceForTask(ctx.cwd, taskId);
773
758
  if (projection.error || !projection.claim) {
774
759
  const reason = projection.error ?? "no active backend claim";
775
760
  notifyOnce(ctx, `authorization-claim:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
776
761
  progression.closeInvocation(invocation);
777
762
  return { state: "blocked", reason };
778
763
  }
779
- const priorIntent = await readTaskIntent(ctx.cwd, taskId);
764
+ const priorIntent = await readTaskIntentForRecord(ctx.cwd, taskId);
780
765
  const sidecar = nextIntent ? join(ctx.cwd, priorIntent.intent_ref.path) : undefined;
781
766
  const priorBytes = sidecar ? readFileSync(sidecar) : undefined;
782
767
  const stagedSnapshot = sidecar ? captureStagedIntent(ctx.cwd, priorIntent.intent_ref.path) : undefined;
@@ -797,10 +782,10 @@ export default function (
797
782
  cwd: ctx.cwd,
798
783
  stdio: ["ignore", "pipe", "pipe"],
799
784
  });
800
- const stagedRecord = await readTaskRecord(ctx.cwd, taskId);
785
+ const stagedRecord = await readTaskRecordRaw(ctx.cwd, taskId);
801
786
  if (!stagedRecord.record || stagedRecord.revision !== projection.projection.record_revision)
802
787
  throw new Error("TaskRecord changed while preparing the breaking revision");
803
- stagedNextDiffHash = diffHashOf(ctx.cwd, stagedRecord.record);
788
+ stagedNextDiffHash = taskDeliveryIdentity(ctx.cwd, stagedRecord.record).diff_hash;
804
789
  } catch (error) {
805
790
  let restoreError: unknown;
806
791
  try { restoreStagedIntent(); } catch (err) { restoreError = err; }
@@ -815,7 +800,7 @@ export default function (
815
800
  let userDecisionOperation: ReturnType<typeof buildUserDecisionOperation> | undefined;
816
801
  if (operation === "resolve-user-decision") {
817
802
  try {
818
- const current = await readTaskRecord(ctx.cwd, taskId);
803
+ const current = await readTaskRecordRaw(ctx.cwd, taskId);
819
804
  if (!current.record) throw new Error(`task ${taskId} has no TaskRecord v2`);
820
805
  if (current.revision !== projection.projection.record_revision)
821
806
  throw new Error("task record changed while preparing user decision");
@@ -971,11 +956,11 @@ export default function (
971
956
  if (nextIntent) await dependencies.authorizationAfterSidecarStage?.();
972
957
  // Re-read the owner record after confirmation and compare the staged
973
958
  // candidate digest immediately before capability issuance.
974
- const liveRecord = nextIntent ? await readTaskRecord(ctx.cwd, taskId) : null;
959
+ const liveRecord = nextIntent ? await readTaskRecordRaw(ctx.cwd, taskId) : null;
975
960
  if (nextIntent && (!liveRecord?.record || liveRecord.revision !== projection.projection.record_revision || !stagedNextDiffHash))
976
961
  throw new Error("TaskRecord changed before the breaking revision digest");
977
962
  const operationDiffHash = liveRecord?.record
978
- ? diffHashOf(ctx.cwd, liveRecord.record)
963
+ ? taskDeliveryIdentity(ctx.cwd, liveRecord.record).diff_hash
979
964
  : projection.projection.diff_hash;
980
965
  if (nextIntent && operationDiffHash !== stagedNextDiffHash)
981
966
  throw new Error("staged next-state diff changed after native confirmation");
@@ -1003,7 +988,7 @@ export default function (
1003
988
  task_id: taskId,
1004
989
  operation: { ...exactOperation, capability, actor_id: LITERAL_USER_ACTOR_ID } as never,
1005
990
  prior_intent_token: priorIntent.token,
1006
- diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
991
+ diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
1007
992
  now,
1008
993
  })) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
1009
994
  if (exactOperation.op === "stop") progression.releaseStoppedReview(taskId);
@@ -1015,7 +1000,7 @@ export default function (
1015
1000
  return { state: "applied", operation, lifecycle: result.record.lifecycle };
1016
1001
  } catch (error) {
1017
1002
  if (stagedSnapshot) {
1018
- const current = await readTaskRecord(ctx.cwd, taskId);
1003
+ const current = await readTaskRecordRaw(ctx.cwd, taskId);
1019
1004
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) restoreStagedIntent();
1020
1005
  }
1021
1006
  throw error;
@@ -1023,7 +1008,7 @@ export default function (
1023
1008
  } catch (error) {
1024
1009
  const reason = error instanceof Error ? error.message : String(error);
1025
1010
  if (operation === "stop") {
1026
- const terminal = await projectAssuranceState(ctx.cwd, taskId).catch(() => null);
1011
+ const terminal = await projectAssuranceForTask(ctx.cwd, taskId).catch(() => null);
1027
1012
  if (terminal && !terminal.error && terminal.projection.lifecycle === "stopped") {
1028
1013
  progression.releaseStoppedReview(taskId);
1029
1014
  return { state: "applied", operation, lifecycle: "stopped", delivery_error: reason };
@@ -1040,11 +1025,11 @@ export default function (
1040
1025
  if (ctx.mode !== "tui") return { state: "blocked", reason: "imm_kernel_canary mutation is TUI-only" };
1041
1026
  if (progression.isInvocationOpen(taskId))
1042
1027
  return { state: "blocked", reason: `task ${taskId} already has an open invocation; concurrent assure/authorize is rejected` };
1043
- const projection = await projectAssuranceState(ctx.cwd, taskId);
1028
+ const projection = await projectAssuranceForTask(ctx.cwd, taskId);
1044
1029
  if (projection.error || !projection.claim)
1045
1030
  return { state: "blocked", reason: projection.error ?? "no active backend claim" };
1046
1031
  await dependencies.authorizationBeforeRecordRead?.();
1047
- const read = await readTaskRecord(ctx.cwd, taskId);
1032
+ const read = await readTaskRecordRaw(ctx.cwd, taskId);
1048
1033
  if (!read.record) return { state: "blocked", reason: `task ${taskId} has no TaskRecord v3` };
1049
1034
  if (read.revision !== projection.projection.record_revision)
1050
1035
  return { state: "blocked", reason: "TaskRecord changed while deriving authorization operation" };
@@ -1108,7 +1093,7 @@ export async function recordCancelledUserDecision(
1108
1093
  snapshotDigestRef: string,
1109
1094
  ): Promise<{ recorded: boolean; finding_id: string }> {
1110
1095
  const findingId = `user-decision-${operation}`;
1111
- const current = await readTaskRecord(ctx.cwd, taskId);
1096
+ const current = await readTaskRecordRaw(ctx.cwd, taskId);
1112
1097
  const openDecision = current.record?.findings.find(
1113
1098
  (finding) =>
1114
1099
  finding.kind === "unresolved_user_decision" && finding.status === "open",
@@ -1130,8 +1115,8 @@ export async function recordCancelledUserDecision(
1130
1115
  },
1131
1116
  actor_id: LITERAL_USER_ACTOR_ID,
1132
1117
  } as never,
1133
- prior_intent_token: (await readTaskIntent(ctx.cwd, taskId)).token,
1134
- diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
1118
+ prior_intent_token: (await readTaskIntentForRecord(ctx.cwd, taskId)).token,
1119
+ diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
1135
1120
  now: new Date().toISOString(),
1136
1121
  });
1137
1122
  return { recorded: true, finding_id: findingId };
@@ -1153,24 +1138,36 @@ export function buildUserDecisionOperation(record: {
1153
1138
  }
1154
1139
 
1155
1140
  /**
1156
- * One record-aware freshness identity. v4 derives the scoped revision digest
1157
- * from the immutable Enrollment base so committed and staged task work share a
1158
- * single diff_hash with Review; v3 keeps the legacy HEAD -> index digest.
1141
+ * Read the TaskIntent through the TaskRecord's `intent_ref.path`.
1142
+ *
1143
+ * `freeze_artifacts` binds the sidecar in place, so a post-freeze read must
1144
+ * follow the record instead of guessing a default path; with no record yet
1145
+ * (pre-enrollment) the Kernel resolves the sidecar that exists. Same contract
1146
+ * as the Claude adapter's `readTaskIntentForRecord`.
1147
+ */
1148
+ export function readTaskIntentForRecord(root: string, taskId: string): ReadTaskIntentResult {
1149
+ const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref.path;
1150
+ return readTaskIntent(root, taskId, currentPath);
1151
+ }
1152
+
1153
+ async function markGithubTaskTerminal(
1154
+ root: string,
1155
+ input: { task_id: string; phase: "done" | "stopped"; terminal_event_id: string },
1156
+ ): Promise<GithubTrackerResult> {
1157
+ return runGithubTrackerOperation(root, { op: "mark-terminal", ...input });
1158
+ }
1159
+
1160
+ /**
1161
+ * The shared delivery identity selector: one function in workspace_scope.ts
1162
+ * picks the identity family per TaskRecord contract. This adapter-level alias
1163
+ * only narrows the record type for the Kernel ports that expect it; the dual-host
1164
+ * conformance seam imports it so both hosts' reportable identity is comparable.
1159
1165
  */
1160
- function diffSnapshotOf(root: string, record: NonNullable<TaskRecordRead["record"]>): {
1166
+ export function diffSnapshotOf(root: string, record: TaskRecord): {
1161
1167
  diff_hash: string;
1162
1168
  changed_paths: string[];
1163
1169
  } {
1164
- if (record.contract === "assurance_kernel/task_record/v4") {
1165
- if (!record.git_base_head)
1166
- throw new Error("TaskRecord v4 is missing git_base_head");
1167
- return taskRevisionIdentity(root, record.intent_snapshot.scope_hint, record.git_base_head, record.task_id);
1168
- }
1169
- return taskDiffIdentity(root, record.intent_snapshot.scope_hint, record.task_id);
1170
- }
1171
-
1172
- function diffHashOf(root: string, record: NonNullable<TaskRecordRead["record"]>): string {
1173
- return diffSnapshotOf(root, record).diff_hash;
1170
+ return taskDeliveryIdentity(root, record);
1174
1171
  }
1175
1172
 
1176
1173
  // Translation-only adapter for the internal Kernel assurance projection. All
@@ -1190,7 +1187,7 @@ async function buildTaskOverview(root: string): Promise<{
1190
1187
  const claim = await readBackendClaim(root);
1191
1188
  let active: TaskOverviewEntry | null = null;
1192
1189
  if (claim) {
1193
- const projection = await projectAssuranceState(root, claim.task_id);
1190
+ const projection = await projectAssuranceForTask(root, claim.task_id);
1194
1191
  if (!projection.error) {
1195
1192
  const state = projection.projection;
1196
1193
  const obligation = String(state.next_obligation);
@@ -1241,56 +1238,14 @@ function overviewRailState(lifecycle: string, obligation: string): TaskRailState
1241
1238
  return "Working";
1242
1239
  }
1243
1240
 
1244
- async function projectAssuranceState(root: string, taskId: string): Promise<AssuranceProjectionResult> {
1245
- return projectAssurance(root, taskId, diffSnapshotOf);
1241
+ export async function projectAssuranceForTask(root: string, taskId: string): Promise<AssuranceProjectionResult> {
1242
+ return projectAssurance(root, taskId);
1246
1243
  }
1247
1244
 
1248
1245
  /**
1249
1246
  * Publish and prove the task-scoped synthetic revision for a v4 record. v3
1250
1247
  * records keep the legacy full-source bundle and return null here.
1251
1248
  */
1252
- export async function ensureTaskReviewRevision(
1253
- root: string,
1254
- taskId: string,
1255
- projection: AssuranceProjectionResult,
1256
- ): Promise<ReviewRevision | null> {
1257
- const current = await readTaskRecord(root, taskId);
1258
- const record = current.record;
1259
- if (!record) throw new Error(`task ${taskId} has no TaskRecord before Review preparation`);
1260
- if (current.revision !== projection.projection.record_revision)
1261
- throw new Error("TaskRecord changed before Review preparation");
1262
- if (record.contract !== "assurance_kernel/task_record/v4") return null;
1263
- if (!record.git_base_head)
1264
- throw new Error("Review revision requires a TaskRecord v4 git_base_head");
1265
- const manifest = captureReviewManifest(root, {
1266
- taskId,
1267
- baseHead: record.git_base_head,
1268
- scopeHint: record.intent_snapshot.scope_hint,
1269
- expectedDiffHash: projection.projection.diff_hash,
1270
- intentRevision: projection.projection.intent_revision,
1271
- intentContentHash: projection.projection.intent_content_hash,
1272
- recordRevision: projection.projection.record_revision,
1273
- workspaceRevision: projection.projection.workspace_revision,
1274
- lifecycle: projection.projection.lifecycle,
1275
- artifactState: projection.projection.artifact_state,
1276
- risk: record.intent_snapshot.risk,
1277
- // The same outcomes the Review snapshot is built from. A preflight stand-in
1278
- // only matched the settled QA attestation because deterministic QA happens to
1279
- // write that exact summary, so the submit-time digest comparison held by
1280
- // coincidence rather than by construction.
1281
- outcomes: qaOutcomes(record),
1282
- });
1283
- return {
1284
- contract: "assurance_kernel/review_revision/v1",
1285
- base_head: manifest.base_head,
1286
- review_tree: manifest.review_tree,
1287
- review_commit: manifest.review_commit,
1288
- review_ref: manifest.review_ref,
1289
- diff_hash: manifest.diff_hash,
1290
- manifest_digest: manifest.manifest_digest,
1291
- };
1292
- }
1293
-
1294
1249
  /**
1295
1250
  * Review refs are reconstructible evidence transport, never workflow authority.
1296
1251
  * A ref survives only while its task owns a nonterminal TaskRecord.
@@ -1308,7 +1263,7 @@ async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: str
1308
1263
  for (const entry of listed) {
1309
1264
  if (live.has(entry.taskId)) continue;
1310
1265
  try {
1311
- const record = await readTaskRecord(root, entry.taskId);
1266
+ const record = await readTaskRecordRaw(root, entry.taskId);
1312
1267
  if (record.record && record.record.lifecycle === "active" && claim?.task_id === entry.taskId)
1313
1268
  live.add(entry.taskId);
1314
1269
  } catch {
@@ -1334,267 +1289,13 @@ function notifyHost(ctx: HostContext, key: string, message: string, level: "warn
1334
1289
  if (ui) notifyOnce({ ui }, key, message, level);
1335
1290
  }
1336
1291
 
1337
- async function applyAssuranceVerdict(
1338
- // The coordinator port hands these a `HostContext`, not the Pi
1339
- // `ExtensionContext`. Both functions only ever read `cwd`; declaring the
1340
- // wider host type made the port assignment unsound.
1341
- ctx: HostContext,
1342
- snapshot: SnapshotDescriptor,
1343
- verdict: AssuranceVerdict,
1344
- invocation: InvocationToken,
1345
- actorId: string,
1346
- hooks: { beforeCommit?: () => Promise<void>; onCommit?: () => void; afterCommit?: () => Promise<void> } = {},
1347
- authorityKind: "qa" | "review" | "user" = snapshot.role,
1348
- ): Promise<void> {
1349
- const fresh = await projectAssuranceState(ctx.cwd, snapshot.task_id);
1350
- if (
1351
- fresh.error ||
1352
- fresh.claim?.task_id !== snapshot.task_id ||
1353
- fresh.projection.record_revision !== snapshot.record_revision ||
1354
- fresh.projection.workspace_revision !== snapshot.workspace_revision ||
1355
- fresh.projection.intent_revision !== snapshot.intent_revision ||
1356
- fresh.projection.intent_content_hash !== snapshot.intent_content_hash ||
1357
- fresh.projection.diff_hash !== snapshot.diff_hash ||
1358
- fresh.projection.lifecycle !== snapshot.lifecycle ||
1359
- fresh.projection.artifact_state !== snapshot.artifact_state
1360
- ) {
1361
- throw new Error(`assurance snapshot changed before authority application: ${[
1362
- fresh.error,
1363
- fresh.claim?.task_id !== snapshot.task_id ? "claim" : null,
1364
- fresh.projection.record_revision !== snapshot.record_revision ? "record_revision" : null,
1365
- fresh.projection.workspace_revision !== snapshot.workspace_revision ? "workspace_revision" : null,
1366
- fresh.projection.intent_revision !== snapshot.intent_revision ? "intent_revision" : null,
1367
- fresh.projection.intent_content_hash !== snapshot.intent_content_hash ? "intent_content_hash" : null,
1368
- fresh.projection.diff_hash !== snapshot.diff_hash ? "diff_hash" : null,
1369
- fresh.projection.lifecycle !== snapshot.lifecycle ? `lifecycle(${snapshot.lifecycle}->${fresh.projection.lifecycle})` : null,
1370
- fresh.projection.artifact_state !== snapshot.artifact_state ? `artifact_state(${snapshot.artifact_state}->${fresh.projection.artifact_state})` : null,
1371
- ].filter(Boolean).join(", ")}`);
1372
- }
1373
- const { registry, app } = await authorityPair();
1374
- const priorIntentToken = (await readTaskIntent(ctx.cwd, snapshot.task_id)).token;
1375
- const commitAndApply = async <T>(apply: () => Promise<T>): Promise<T> => {
1376
- invocationRegistry.commit(invocation);
1377
- const settlement = apply();
1378
- let hookError: unknown;
1379
- try { hooks.onCommit?.(); } catch (error) { hookError = error; }
1380
- const result = await settlement;
1381
- try { await hooks.afterCommit?.(); } catch (error) { hookError ??= error; }
1382
- if (hookError) throw hookError;
1383
- return result;
1384
- };
1385
- if (verdict.decision === "rework") {
1386
- const findings = reviewReworkFindings(verdict);
1387
- const now = new Date().toISOString();
1388
- const capability = await mintCapability(registry, {
1389
- authority_kind: authorityKind,
1390
- task_id: snapshot.task_id,
1391
- run_id: snapshot.run_id,
1392
- action_kind: "request_rework",
1393
- expected_record_hash: snapshot.record_revision,
1394
- intent_revision: snapshot.intent_revision,
1395
- intent_content_hash: snapshot.intent_content_hash,
1396
- diff_hash: snapshot.diff_hash,
1397
- actor_id: actorId,
1398
- findings,
1399
- now,
1400
- });
1401
- await hooks.beforeCommit?.();
1402
- const result = (await commitAndApply(async () => app.execute({
1403
- root: ctx.cwd,
1404
- task_id: snapshot.task_id,
1405
- operation: {
1406
- op: "request_rework",
1407
- capability,
1408
- findings: findings as never[],
1409
- actor_id: actorId,
1410
- },
1411
- prior_intent_token: priorIntentToken,
1412
- diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
1413
- now,
1414
- }))) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] }; findings?: Array<{ kind: string; status: string }> } };
1415
- stagePlanningArtifactTransition(ctx.cwd, result.record);
1416
- const parked = (result.record as { findings?: Array<{ kind: string; status: string }> }).findings?.some(
1417
- (finding) => finding.kind === "replan_required" && finding.status === "open",
1418
- );
1419
- if (parked) notifyHost(
1420
- ctx,
1421
- `rework-parked:${snapshot.task_id}`,
1422
- `rework applied: review parked for replan with ${findings.length} finding(s)`,
1423
- "warning",
1424
- );
1425
- return;
1426
- }
1427
- const now = new Date().toISOString();
1428
- const advisories = verdict.decision === "pass" ? reviewAdvisoryRecords(verdict) : [];
1429
- const approval = {
1430
- id: `approval-${snapshot.role}-${randomUUID().slice(0, 8)}`,
1431
- kind: snapshot.role === "qa" ? "qa" : "review",
1432
- authority_role: snapshot.role === "qa" ? "qa" : "reviewer",
1433
- task_revision: snapshot.intent_revision,
1434
- intent_content_hash: snapshot.intent_content_hash,
1435
- diff_hash: snapshot.diff_hash,
1436
- actor_id: actorId,
1437
- summary: verdict.approval!.summary,
1438
- // Trusted revision identity comes from the host-verified snapshot, never
1439
- // from the reviewer payload.
1440
- ...(snapshot.role === "review" && snapshot.review_revision
1441
- ? { review_revision: snapshot.review_revision }
1442
- : {}),
1443
- ...(snapshot.role === "review" && advisories.length > 0
1444
- ? { advisory_findings: advisories }
1445
- : {}),
1446
- };
1447
- const capability = await mintCapability(registry, {
1448
- authority_kind: snapshot.role,
1449
- task_id: snapshot.task_id,
1450
- run_id: snapshot.run_id,
1451
- action_kind: "record_approval",
1452
- expected_record_hash: snapshot.record_revision,
1453
- intent_revision: snapshot.intent_revision,
1454
- intent_content_hash: snapshot.intent_content_hash,
1455
- diff_hash: snapshot.diff_hash,
1456
- actor_id: actorId,
1457
- approval,
1458
- now,
1459
- });
1460
- await hooks.beforeCommit?.();
1461
- await commitAndApply(async () => app.execute({
1462
- root: ctx.cwd,
1463
- task_id: snapshot.task_id,
1464
- operation: { op: "record_approval", capability, approval, actor_id: actorId },
1465
- prior_intent_token: priorIntentToken,
1466
- diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
1467
- now,
1468
- }));
1469
- }
1470
-
1471
- async function buildAssuranceSnapshot(
1472
- root: string,
1473
- taskId: string,
1474
- role: AssuranceRole,
1475
- projection: AssuranceProjectionResult,
1476
- ): Promise<{
1477
- snapshot: SnapshotDescriptor;
1478
- descriptors: Map<string, VerificationDescriptor>;
1479
- reviewBundle: ReviewBundle | null;
1480
- reviewManifest: ReviewManifestV5 | null;
1481
- }> {
1482
- const record = await readTaskRecord(root, taskId);
1483
- if (
1484
- !record.record ||
1485
- record.revision !== projection.projection.record_revision ||
1486
- record.record.intent_snapshot.revision !== projection.projection.intent_revision ||
1487
- record.record.intent_ref.content_hash !== projection.projection.intent_content_hash
1488
- ) {
1489
- throw new Error("TaskRecord changed before assurance snapshot capture");
1490
- }
1491
- const intent = record.record.intent_snapshot;
1492
- const acceptance = intent.acceptance;
1493
- const descriptors = new Map<string, VerificationDescriptor>();
1494
- // Review resumes from settled outcomes without resolving verification tools.
1495
- for (const item of role === "qa" ? acceptance : []) {
1496
- const descriptor = parseVerificationDescriptor(item.verification);
1497
- descriptors.set(item.id, descriptor);
1498
- }
1499
- // `git_base_head` is optional on the read shape because v3 records carry
1500
- // none, so the contract test alone does not prove it is present.
1501
- const baseHead = record.record.contract === "assurance_kernel/task_record/v4"
1502
- ? record.record.git_base_head
1503
- : undefined;
1504
- if (record.record.contract === "assurance_kernel/task_record/v4" && !baseHead)
1505
- throw new Error("TaskRecord v4 is missing its Enrollment git_base_head");
1506
- const reviewRevision = baseHead
1507
- ? {
1508
- contract: "assurance_kernel/review_revision_identity/v1" as const,
1509
- base_head: baseHead,
1510
- review_commit: "",
1511
- review_tree: "",
1512
- manifest_digest: "",
1513
- }
1514
- : null;
1515
- const reviewBundle = role === "review" && !reviewRevision
1516
- ? captureReviewBundle(
1517
- root,
1518
- intent.scope_hint,
1519
- projection.projection.diff_hash,
1520
- qaOutcomes(record.record),
1521
- )
1522
- : null;
1523
- const reviewManifest = role === "review" && reviewRevision
1524
- ? captureReviewManifest(root, {
1525
- taskId,
1526
- baseHead: reviewRevision.base_head,
1527
- scopeHint: intent.scope_hint,
1528
- expectedDiffHash: projection.projection.diff_hash,
1529
- intentRevision: projection.projection.intent_revision,
1530
- intentContentHash: projection.projection.intent_content_hash,
1531
- recordRevision: projection.projection.record_revision,
1532
- workspaceRevision: projection.projection.workspace_revision,
1533
- lifecycle: projection.projection.lifecycle,
1534
- artifactState: projection.projection.artifact_state,
1535
- risk: intent.risk,
1536
- outcomes: qaOutcomes(record.record),
1537
- })
1538
- : null;
1539
- const taskSnapshot = !reviewBundle && !reviewManifest
1540
- ? captureGitTaskSnapshot(root, intent.scope_hint, taskId)
1541
- : null;
1542
- const dirtyFiles = reviewManifest
1543
- ? Object.keys(reviewManifest.changed_paths)
1544
- : reviewBundle
1545
- ? Object.keys(reviewBundle.dirty_files)
1546
- : Object.keys(taskSnapshot!.staged_files);
1547
- return {
1548
- snapshot: buildSnapshot({
1549
- root,
1550
- task_id: taskId,
1551
- role,
1552
- run_id: projection.projection.run_id,
1553
- record_revision: projection.projection.record_revision,
1554
- workspace_revision: projection.projection.workspace_revision,
1555
- intent_revision: projection.projection.intent_revision,
1556
- intent_content_hash: projection.projection.intent_content_hash,
1557
- diff_hash: projection.projection.diff_hash,
1558
- lifecycle: projection.projection.lifecycle,
1559
- artifact_state: projection.projection.artifact_state,
1560
- risk: intent.risk,
1561
- fresh_acceptance_ids: projection.projection.fresh_acceptance_ids,
1562
- missing_acceptance_ids: projection.projection.missing_acceptance_ids,
1563
- stale_attestation_ids: projection.projection.stale_attestation_ids,
1564
- acceptance,
1565
- dirty_files: dirtyFiles,
1566
- review_bundle_digest: reviewManifest?.manifest_digest ?? reviewBundle?.bundle_digest ?? null,
1567
- review_revision: reviewManifest
1568
- ? {
1569
- contract: "assurance_kernel/review_revision_identity/v1",
1570
- base_head: reviewManifest.base_head,
1571
- review_commit: reviewManifest.review_commit,
1572
- review_tree: reviewManifest.review_tree,
1573
- manifest_digest: reviewManifest.manifest_digest,
1574
- }
1575
- : undefined,
1576
- }),
1577
- descriptors,
1578
- reviewBundle,
1579
- reviewManifest,
1580
- };
1581
- }
1582
-
1583
- function qaOutcomes(
1584
- record: NonNullable<TaskRecordRead["record"]>,
1585
- ): Record<string, { status: "passed" | "failed" | "blocked"; summary: string }> {
1586
- return Object.fromEntries(
1587
- record.attestations
1588
- .filter((item) => item.kind === "qa")
1589
- .flatMap((item) => item.acceptance_results)
1590
- .map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
1591
- );
1592
- }
1593
-
1594
1292
  // The invocation registry is shared with the progression module's
1595
1293
  // module-scoped registry (see the top-level import above); commit/cancel
1596
1294
  // semantics are identical to the previous extension implementation.
1597
1295
 
1296
+ const piConfirmationReference: ConfirmationReferenceSource = ({ snapshot, now }) =>
1297
+ `pi-confirm-${createHash("sha256").update(`${snapshot.task_id}\0${now}\0${snapshot.intent_revision}\0${snapshot.intent_content_hash}\0${snapshot.diff_hash}`).digest("hex").slice(0, 16)}`;
1298
+
1598
1299
  async function mintCapability(
1599
1300
  registry: MutationAuthorityRegistry,
1600
1301
  input: {
@@ -1648,7 +1349,6 @@ async function mintCapability(
1648
1349
  confirmation_ref: `pi-confirm-${createHash("sha256").update(
1649
1350
  `${input.task_id}\0${now}\0${input.intent_revision}\0${input.intent_content_hash}\0${input.diff_hash}`,
1650
1351
  ).digest("hex").slice(0, 16)}`,
1651
- expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
1652
1352
  findings_digest:
1653
1353
  input.action_kind === "request_rework"
1654
1354
  ? await findingsDigestV2(input.findings as never[])
@@ -1677,7 +1377,7 @@ async function executeOrdinaryOperation(
1677
1377
  const operation = input.operation.op === "revise_intent"
1678
1378
  ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
1679
1379
  : input.operation;
1680
- const priorIntent = await readTaskIntent(ctx.cwd, input.taskId);
1380
+ const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
1681
1381
  const sidecar = join(ctx.cwd, priorIntent.intent_ref.path);
1682
1382
  const priorBytes = operation.op === "revise_intent" ? readFileSync(sidecar) : null;
1683
1383
  // A content-changing revision writes the sidecar before the kernel's drift
@@ -1698,7 +1398,7 @@ async function executeOrdinaryOperation(
1698
1398
  task_id: input.taskId,
1699
1399
  operation: operation as never,
1700
1400
  prior_intent_token: priorIntent.token,
1701
- diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
1401
+ diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
1702
1402
  now: new Date().toISOString(),
1703
1403
  });
1704
1404
  if (operation.op === "freeze_artifacts" || operation.op === "stop")
@@ -1709,7 +1409,7 @@ async function executeOrdinaryOperation(
1709
1409
  return result;
1710
1410
  } catch (error) {
1711
1411
  if (priorBytes) {
1712
- const current = await readTaskRecord(ctx.cwd, input.taskId);
1412
+ const current = await readTaskRecordRaw(ctx.cwd, input.taskId);
1713
1413
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision && priorStaged) {
1714
1414
  restoreStagedIntentShared(ctx.cwd, priorStaged);
1715
1415
  }
@@ -1732,6 +1432,46 @@ async function reconcileRefsQuietly(root: string): Promise<void> {
1732
1432
  }
1733
1433
  }
1734
1434
 
1435
+ /**
1436
+ * Gate-free closeout: a batch child that reached Kernel `done` in the foreground
1437
+ * re-enters the batch in the same call, reusing its authorization. Only `done`
1438
+ * continues; stopped, parked and Review-pending children end where they do
1439
+ * today. The continuation is transport: the Kernel result is returned
1440
+ * unchanged, and any failure is reported beside it with one retry action.
1441
+ */
1442
+ async function closeOutBatchChild(
1443
+ ctx: ExtensionContext,
1444
+ taskId: string,
1445
+ enriched: Record<string, unknown>,
1446
+ ): Promise<Record<string, unknown>> {
1447
+ const taskState = enriched.task_state as { lifecycle?: unknown } | undefined;
1448
+ if (taskState?.lifecycle !== "done") return enriched;
1449
+ let slug: string | null;
1450
+ try {
1451
+ slug = findResumableBatchSlugForTask(ctx.cwd, taskId);
1452
+ } catch {
1453
+ return enriched;
1454
+ }
1455
+ if (!slug) return enriched;
1456
+ try {
1457
+ // The batch commit takes exactly the scope envelope plus this task's own
1458
+ // audit evidence and refuses unstaged changes: stage only that evidence.
1459
+ if (existsSync(join(ctx.cwd, ".imm", "audit", taskId)))
1460
+ execFileSync("git", ["-C", ctx.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
1461
+ const batch = await executePiUnattendedBatch({ root: ctx.cwd, initiativeSlug: slug, reuseOnly: true, signal: ctx.signal });
1462
+ return { ...enriched, batch };
1463
+ } catch (error) {
1464
+ return {
1465
+ ...enriched,
1466
+ batch: {
1467
+ state: "rejected",
1468
+ reason: `batch continuation failed: ${error instanceof Error ? error.message : String(error)}`,
1469
+ recovery_action: "call start_unattended_batch with the same Initiative to continue the batch",
1470
+ },
1471
+ };
1472
+ }
1473
+ }
1474
+
1735
1475
  async function enrichAssuranceResult(
1736
1476
  ctx: ExtensionContext,
1737
1477
  taskId: string,
@@ -1742,7 +1482,7 @@ async function enrichAssuranceResult(
1742
1482
  const taskState = { error: typeof result.reason === "string" ? result.reason : "authority projection unavailable" };
1743
1483
  return { ...result, task_state: taskState, next_action: nextActionForAssuranceResult(result, taskState) };
1744
1484
  }
1745
- const projection = await projectAssuranceState(ctx.cwd, taskId);
1485
+ const projection = await projectAssuranceForTask(ctx.cwd, taskId);
1746
1486
  const taskState: AssuranceTaskState = projection.error
1747
1487
  ? { error: projection.error }
1748
1488
  : projection.projection;
@@ -1843,33 +1583,6 @@ function prepareActionArgs<Params>(args: unknown): Params {
1843
1583
  return input as Params;
1844
1584
  }
1845
1585
 
1846
- function stagePlanningArtifactTransition(root: string, record: {
1847
- intent_ref: { path: string };
1848
- intent_snapshot: { scope_hint: string[] };
1849
- }): void {
1850
- const intentActive = record.intent_ref.path.replace("docs/plans/archive/", "docs/plans/");
1851
- const intentArchive = intentActive.replace("docs/plans/", "docs/plans/archive/");
1852
- const specActive = record.intent_snapshot.scope_hint.find((path) =>
1853
- /^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path)
1854
- && record.intent_snapshot.scope_hint.includes(path.replace("docs/specs/", "docs/specs/archive/")),
1855
- );
1856
- const candidates = [
1857
- intentActive,
1858
- intentArchive,
1859
- ...(specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []),
1860
- ];
1861
- const paths = candidates.filter((path) => existsSync(join(root, path)) || execFileSync(
1862
- "git",
1863
- ["ls-files", "--cached", "--", path],
1864
- { cwd: root, encoding: "utf8" },
1865
- ).trim().length > 0);
1866
- if (paths.length === 0) return;
1867
- execFileSync("git", ["add", "--", ...paths], {
1868
- cwd: root,
1869
- stdio: ["ignore", "pipe", "pipe"],
1870
- });
1871
- }
1872
-
1873
1586
  function failCanaryTool(
1874
1587
  taskId: string,
1875
1588
  operation: string,