immune-brain 4.6.0 → 5.1.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 (75) hide show
  1. package/README.md +374 -46
  2. package/README.zh-CN.md +384 -47
  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 +42 -44
  6. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +143 -474
  7. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +78 -87
  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 +8601 -6975
  16. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
  17. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +3 -3
  18. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -1
  19. package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
  20. package/plugins/immune-brain/dist/imm-doc-prune.md +2 -2
  21. package/plugins/immune-brain/dist/{imm-agent-doc-maintain.md → imm-doc-slim.md} +9 -9
  22. package/plugins/immune-brain/dist/imm-planner.md +23 -8
  23. package/plugins/immune-brain/dist/imm-pr-fix.md +1 -1
  24. package/plugins/immune-brain/dist/{imm-review-retro.md → imm-retro.md} +26 -13
  25. package/plugins/immune-brain/dist/{imm-loop.md → imm-run.md} +231 -10
  26. package/plugins/immune-brain/dist/registry.yaml +9 -9
  27. package/plugins/immune-brain/dist/role-prompts/code-review.md +7 -0
  28. package/plugins/immune-brain/dist/role-prompts/executor.md +35 -2
  29. package/plugins/immune-brain/dist/role-prompts/lane-steward.md +44 -0
  30. package/plugins/immune-brain/runtime/assurance/coordinator.ts +83 -67
  31. package/plugins/immune-brain/runtime/assurance/host_port.ts +20 -0
  32. package/plugins/immune-brain/runtime/assurance/qa.ts +2 -2
  33. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +347 -0
  34. package/plugins/immune-brain/runtime/claude/interaction.ts +6 -0
  35. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +78 -373
  36. package/plugins/immune-brain/runtime/claude/mcp_server.ts +26 -2
  37. package/plugins/immune-brain/runtime/claude/review_host.ts +48 -16
  38. package/plugins/immune-brain/runtime/claude/role_agents.ts +84 -0
  39. package/plugins/immune-brain/runtime/commands/kernel.ts +3 -6
  40. package/plugins/immune-brain/runtime/kernel/application.ts +6 -3
  41. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +15 -8
  42. package/plugins/immune-brain/runtime/kernel/canary_application.ts +5 -3
  43. package/plugins/immune-brain/runtime/kernel/enrollment.ts +149 -5
  44. package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
  45. package/plugins/immune-brain/runtime/kernel/storage.ts +170 -11
  46. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +13 -0
  47. package/plugins/immune-brain/runtime/local_initiative.ts +94 -0
  48. package/plugins/immune-brain/runtime/loop_contract.ts +77 -24
  49. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  50. package/plugins/immune-brain/runtime/prompts/code-review.md +7 -0
  51. package/plugins/immune-brain/runtime/prompts/executor.md +35 -2
  52. package/plugins/immune-brain/runtime/prompts/lane-steward.md +44 -0
  53. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +42 -2
  54. package/plugins/immune-brain/runtime/unattended/batch_git.ts +57 -27
  55. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
  56. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
  57. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +909 -0
  58. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
  59. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +53 -36
  60. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +34 -29
  61. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +46 -91
  62. package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
  63. package/plugins/immune-brain/runtime/unattended/batch_state.ts +356 -8
  64. package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
  65. package/plugins/immune-brain/runtime/workspace_scope.ts +34 -0
  66. package/plugins/immune-brain/skills/BASELINE.md +4 -4
  67. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +1 -1
  68. package/plugins/immune-brain/skills/{imm-agent-doc-maintain → imm-doc-slim}/SKILL.md +9 -9
  69. package/plugins/immune-brain/skills/imm-planner/SKILL.md +4 -2
  70. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +1 -1
  71. package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/SKILL.md +7 -7
  72. package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/scripts/review_retro.ts +46 -21
  73. package/plugins/immune-brain/skills/{imm-loop → imm-run}/SKILL.md +11 -7
  74. package/plugins/immune-brain/skills/registry.yaml +9 -9
  75. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -581
@@ -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
  //
@@ -24,18 +28,11 @@ import { PLUGIN_VERSION } from "../runtime/plugin_version";
24
28
  import { findResumableBatchSlugForTask } from "../runtime/unattended/batch_preflight";
25
29
  import { executePiUnattendedBatch } from "./imm-unattended-batch";
26
30
  import {
27
- parseVerificationDescriptor,
28
- type VerificationDescriptor,
29
31
  } from "./pi-canary-verification";
30
32
  import {
31
- captureReviewBundle,
32
- captureReviewManifest,
33
33
  listReviewRefs,
34
34
  reconcileReviewRefs,
35
35
  writeNativeReviewEvidence,
36
- type ReviewBundle,
37
- type ReviewManifestV5,
38
- type ReviewRevision,
39
36
  } from "./pi-canary-review-bundle";
40
37
  import type { InvocationToken } from "./pi-canary-invocations";
41
38
  import { runDeterministicQa } from "../runtime/assurance/qa";
@@ -46,7 +43,6 @@ import {
46
43
  import {
47
44
  renderCanaryCall,
48
45
  renderCanaryResult,
49
- type AssuranceRole,
50
46
  } from "./pi-canary-assurance";
51
47
  import {
52
48
  USER_ATTENTION_EVENT,
@@ -71,8 +67,7 @@ import { resolveUxLanguage, uxText } from "./ux-language";
71
67
 
72
68
  /** Host-native UI language; see ux-language.ts. Resolved once per process. */
73
69
  const UX_LANG = resolveUxLanguage();
74
- import { taskDiffIdentity, taskRevisionIdentity, captureGitTaskSnapshot } from "../runtime/workspace_scope";
75
- import { reviewAdvisoryRecords, reviewReworkFindings } from "../runtime/assurance/coordinator";
70
+ import { taskDeliveryIdentity } from "../runtime/workspace_scope";
76
71
  import {
77
72
  AssuranceProgression,
78
73
  buildReviewPrompt,
@@ -87,44 +82,54 @@ import {
87
82
  REVIEW_TIMING_PROFILES,
88
83
  REVIEW_VERDICT_VALIDATION_TIMEOUT_MS,
89
84
  type AssuranceProgressionPorts,
90
- type AssuranceVerdict,
91
85
  type HostContext,
92
- type SnapshotDescriptor,
93
86
  } from "./pi-canary-assurance-progression";
94
87
 
95
- // The Kernel runtime graph is never type-checked from this extension: static
96
- // imports resolve to ./runtime-stub.ts (relative so the Pi extension loader
97
- // can resolve them at runtime), and the stub forwards to the real Kernel
98
- // 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";
99
94
  import {
100
- createMutationAuthorityRegistry,
101
- createCanaryApplication,
102
- buildLoopAction,
103
- buildLoopRoleDispatch,
104
- readBackendClaim,
105
- readTaskTombstone,
106
- markGithubTaskTerminal,
107
95
  reconcileKernelAuthority,
108
96
  repairKernelAuthority,
109
- readTaskRecord,
97
+ readTaskRecordRaw,
110
98
  withKernelStoreLockForTask,
111
- inspectStorageLayout,
112
- 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 {
113
103
  readTaskIntent,
114
104
  parseTaskIntentV1,
115
105
  canonicalIntentHash,
116
- projectAssurance,
117
- findingsDigestV2,
118
- capabilityActionFor,
106
+ type ReadTaskIntentResult,
107
+ } from "../runtime/kernel/intent";
108
+ import { readBackendClaim, readTaskTombstone } from "../runtime/kernel/backend_claim";
109
+ import {
110
+ createMutationAuthorityRegistry,
119
111
  digestOfAction,
120
- type AssuranceProjectionResult,
121
- type TaskRecordRead,
122
- type CanaryApplication,
123
- type MutationAuthorityRegistry,
124
112
  type CapabilityBindingV2,
125
- LITERAL_USER_ACTOR_ID,
126
- } from "./runtime-stub";
127
- 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";
128
133
 
129
134
  const LOOP_OWNERS = ["plan", "kernel", "brainstorm", "planner", "loop"] as const;
130
135
  const LOOP_TARGETS = [
@@ -134,6 +139,7 @@ const LOOP_TARGETS = [
134
139
  "architecture-exploration",
135
140
  "advisory-review",
136
141
  "compounder",
142
+ "lane-supply",
137
143
  ] as const;
138
144
  const LOOP_DIRECT_ROLES = ["qa", "code-review", "ui-review"] as const;
139
145
  const KERNEL_OPERATIONS = [
@@ -178,60 +184,6 @@ export type AuthorizeOperation =
178
184
  | "authorize-rework"
179
185
  | "stop";
180
186
 
181
- export interface SnapshotDescriptorInput {
182
- root: string;
183
- task_id: string;
184
- role: AssuranceRole;
185
- run_id?: string | null;
186
- record_revision: string;
187
- workspace_revision: string;
188
- intent_revision: number;
189
- intent_content_hash: string;
190
- diff_hash: string;
191
- lifecycle: string;
192
- artifact_state: string;
193
- risk?: "routine" | "material" | "critical";
194
- fresh_acceptance_ids: string[];
195
- missing_acceptance_ids: string[];
196
- stale_attestation_ids: string[];
197
- acceptance: Array<{ id: string; assertion: string; verification: string }>;
198
- dirty_files?: string[];
199
- review_bundle_digest?: string | null;
200
- /** Present only when Review authority binds an immutable Git revision (v4). */
201
- review_revision?: {
202
- contract: "assurance_kernel/review_revision_identity/v1";
203
- base_head: string;
204
- review_commit: string;
205
- review_tree: string;
206
- manifest_digest: string;
207
- };
208
- }
209
-
210
- export function buildSnapshot(input: SnapshotDescriptorInput): SnapshotDescriptor {
211
- return {
212
- contract: "assurance_kernel/assurance_snapshot/v2",
213
- task_id: input.task_id,
214
- run_id: input.run_id ?? null,
215
- role: input.role,
216
- record_revision: input.record_revision,
217
- workspace_revision: input.workspace_revision,
218
- intent_revision: input.intent_revision,
219
- intent_content_hash: input.intent_content_hash,
220
- diff_hash: input.diff_hash,
221
- lifecycle: input.lifecycle,
222
- artifact_state: input.artifact_state,
223
- risk: input.risk ?? "material",
224
- fresh_acceptance_ids: input.fresh_acceptance_ids,
225
- missing_acceptance_ids: input.missing_acceptance_ids,
226
- stale_attestation_ids: input.stale_attestation_ids,
227
- acceptance: input.acceptance,
228
- dirty_files: [...(input.dirty_files ?? [])].sort(),
229
- review_bundle_digest: input.review_bundle_digest ?? null,
230
- ...(input.review_revision ? { review_revision: input.review_revision } : {}),
231
- root: resolve(input.root),
232
- };
233
- }
234
-
235
187
  export interface CanaryWorkExtensionDependencies {
236
188
  buildAssurance?: typeof buildAssuranceSnapshot;
237
189
  runQa?: typeof runDeterministicQa;
@@ -277,25 +229,15 @@ export function createPiAssuranceProgressionPorts(
277
229
  dependencies: CanaryWorkExtensionDependencies = {},
278
230
  ): AssuranceProgressionPorts {
279
231
  return {
280
- projectTask: (root, taskId) => projectAssuranceState(root, taskId),
281
- readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
282
- readTaskIntent: (root, taskId) => readTaskIntent(root, taskId),
283
- buildAssurance: (root, taskId, role, projection) =>
284
- (dependencies.buildAssurance ?? buildAssuranceSnapshot)(root, taskId, role, projection),
285
- ensureReviewRevision: (root, taskId, projection) => ensureTaskReviewRevision(root, taskId, projection),
232
+ confirmationReference: piConfirmationReference,
233
+ onReworkApplied: (ctx, taskId, count) => notifyHost(ctx, `rework-parked:${taskId}`, `rework applied: review parked for replan with ${count} finding(s)`, "warning"),
234
+ projectTask: (root, taskId) => projectAssuranceForTask(root, taskId),
235
+ readTaskRecord: async (root, taskId) => readTaskRecordRaw(root, taskId),
236
+ readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
286
237
  runQa: (snapshot, descriptors, options) =>
287
238
  (dependencies.runQa ?? runDeterministicQa)(snapshot, descriptors, options),
288
239
  writeReviewEvidence: (input) =>
289
240
  (dependencies.writeReviewEvidence ?? writeNativeReviewEvidence)(input),
290
- applyVerdict: (ctx, input) =>
291
- applyAssuranceVerdict(
292
- ctx,
293
- input.snapshot,
294
- input.verdict,
295
- input.invocation,
296
- input.actorId,
297
- input.hooks,
298
- ),
299
241
  applyOrdinaryOperation: (ctx, input) => executeOrdinaryOperation(ctx, input),
300
242
  advanceBeforeProjection: dependencies.advanceBeforeProjection,
301
243
  qaBeforeProjection: dependencies.qaBeforeProjection,
@@ -303,11 +245,46 @@ export function createPiAssuranceProgressionPorts(
303
245
  qaOnAuthorityCommit: dependencies.qaOnAuthorityCommit,
304
246
  qaAfterAuthorityCommit: dependencies.qaAfterAuthorityCommit,
305
247
  qaJobTimeoutMs: dependencies.qaJobTimeoutMs,
248
+ ...(dependencies.buildAssurance ? { authorityOverrides: { buildAssurance: dependencies.buildAssurance } } : {}),
306
249
  } satisfies AssuranceProgressionPorts;
307
250
  }
308
251
 
309
252
  const GLOBAL_PI_PROGRESSION_KEY = Symbol.for("immune_brain.pi_assurance_progression");
310
253
 
254
+ /**
255
+ * The shared session progression the batch adapter advances children through.
256
+ * The work extension publishes the instance at load; this accessor is the
257
+ * lazy fallback that builds one from the production ports when no extension
258
+ * factory has run (tests, direct batch drives). Both paths publish under the
259
+ * same global key, so a batch Review reservation and the session's
260
+ * submit_review always share one progression instance.
261
+ */
262
+ export async function getSharedPiProgression(): Promise<AssuranceProgression> {
263
+ let progression = (globalThis as any)[GLOBAL_PI_PROGRESSION_KEY] as AssuranceProgression | undefined;
264
+ if (!progression) {
265
+ progression = new AssuranceProgression(createPiAssuranceProgressionPorts());
266
+ (globalThis as any)[GLOBAL_PI_PROGRESSION_KEY] = progression;
267
+ }
268
+ return progression;
269
+ }
270
+
271
+ /**
272
+ * Advance one task through the shared session progression and translate the
273
+ * progression result into the batch runner's advance report shape.
274
+ */
275
+ export async function advancePiTask(root: string, taskId: string): Promise<any> {
276
+ const progression = await getSharedPiProgression();
277
+ const result = await progression.advance(taskId, { cwd: root });
278
+ const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
279
+ if (result.state === "completed") return { state: "completed", ...facts };
280
+ if (result.state === "stopped") return { state: "stopped", ...facts };
281
+ if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
282
+ if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, agent_params: result.agent_params, ...facts };
283
+ if (result.state === "review_preparation_failed") return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
284
+ if (result.state === "blocked") return { state: "blocked", reason: result.reason, ...facts };
285
+ return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
286
+ }
287
+
311
288
  export default function (
312
289
  pi: ExtensionAPI,
313
290
  dependencies: CanaryWorkExtensionDependencies = {},
@@ -323,7 +300,7 @@ export default function (
323
300
  try {
324
301
  const claim = await readBackendClaim(ctx.cwd);
325
302
  if (!claim) return;
326
- const projection = await projectAssuranceState(ctx.cwd, claim.task_id);
303
+ const projection = await projectAssuranceForTask(ctx.cwd, claim.task_id);
327
304
  if (projection.error) {
328
305
  presentTaskRail(ctx, {
329
306
  task_id: claim.task_id,
@@ -592,7 +569,7 @@ export default function (
592
569
  throwIfCanaryToolFailure(taskId, action.op, enriched);
593
570
  return toolResult(JSON.stringify(enriched, null, 2), enriched);
594
571
  }
595
- const projection = await projectAssuranceState(ctx.cwd, taskId);
572
+ const projection = await projectAssuranceForTask(ctx.cwd, taskId);
596
573
  if (projection.error) {
597
574
  const nextAction = recoveryActionForAssuranceFailure(projection.error) ?? "inspect authority state";
598
575
  const details = {
@@ -634,7 +611,7 @@ export default function (
634
611
  taskId,
635
612
  operation: toCanaryOperation(action, "executor") as { op: string; actor_id: string },
636
613
  })) as unknown as { revision: string; record: { lifecycle: string; artifact_state: string } };
637
- const updated = await projectAssuranceState(ctx.cwd, taskId);
614
+ const updated = await projectAssuranceForTask(ctx.cwd, taskId);
638
615
  const taskState = updated.error
639
616
  ? { lifecycle: result.record.lifecycle, artifact_state: result.record.artifact_state }
640
617
  : updated.projection;
@@ -710,15 +687,18 @@ export default function (
710
687
  _ctx: ExtensionContext,
711
688
  ) => {
712
689
  const { action } = params;
690
+ // The Tool schema is intentionally broader than loop_contract's input
691
+ // types (model-supplied JSON); loop_contract narrows and validates at
692
+ // runtime, so these boundary casts are type-level only.
713
693
  const result = action.op === "route"
714
694
  ? await buildLoopAction({
715
695
  ownership: action.ownership,
716
696
  target: action.target,
717
- context: action.context,
697
+ context: action.context as RoleDelegationContext | undefined,
718
698
  scope_expansion: action.scope_expansion,
719
- kernel_operation: action.kernel_operation,
699
+ kernel_operation: action.kernel_operation as Parameters<typeof buildLoopAction>[0]["kernel_operation"],
720
700
  })
721
- : await buildLoopRoleDispatch({ role: action.role, context: action.context });
701
+ : await buildLoopRoleDispatch({ role: action.role, context: action.context as RoleDelegationContext });
722
702
  const details = loopResultDetails(result, action.op);
723
703
  return toolResult(JSON.stringify(result, null, 2), details);
724
704
  },
@@ -775,14 +755,14 @@ export default function (
775
755
  return { state: "blocked", reason };
776
756
  }
777
757
  try {
778
- const projection = await projectAssuranceState(ctx.cwd, taskId);
758
+ const projection = await projectAssuranceForTask(ctx.cwd, taskId);
779
759
  if (projection.error || !projection.claim) {
780
760
  const reason = projection.error ?? "no active backend claim";
781
761
  notifyOnce(ctx, `authorization-claim:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
782
762
  progression.closeInvocation(invocation);
783
763
  return { state: "blocked", reason };
784
764
  }
785
- const priorIntent = await readTaskIntent(ctx.cwd, taskId);
765
+ const priorIntent = await readTaskIntentForRecord(ctx.cwd, taskId);
786
766
  const sidecar = nextIntent ? join(ctx.cwd, priorIntent.intent_ref.path) : undefined;
787
767
  const priorBytes = sidecar ? readFileSync(sidecar) : undefined;
788
768
  const stagedSnapshot = sidecar ? captureStagedIntent(ctx.cwd, priorIntent.intent_ref.path) : undefined;
@@ -803,10 +783,10 @@ export default function (
803
783
  cwd: ctx.cwd,
804
784
  stdio: ["ignore", "pipe", "pipe"],
805
785
  });
806
- const stagedRecord = await readTaskRecord(ctx.cwd, taskId);
786
+ const stagedRecord = await readTaskRecordRaw(ctx.cwd, taskId);
807
787
  if (!stagedRecord.record || stagedRecord.revision !== projection.projection.record_revision)
808
788
  throw new Error("TaskRecord changed while preparing the breaking revision");
809
- stagedNextDiffHash = diffHashOf(ctx.cwd, stagedRecord.record);
789
+ stagedNextDiffHash = taskDeliveryIdentity(ctx.cwd, stagedRecord.record).diff_hash;
810
790
  } catch (error) {
811
791
  let restoreError: unknown;
812
792
  try { restoreStagedIntent(); } catch (err) { restoreError = err; }
@@ -821,7 +801,7 @@ export default function (
821
801
  let userDecisionOperation: ReturnType<typeof buildUserDecisionOperation> | undefined;
822
802
  if (operation === "resolve-user-decision") {
823
803
  try {
824
- const current = await readTaskRecord(ctx.cwd, taskId);
804
+ const current = await readTaskRecordRaw(ctx.cwd, taskId);
825
805
  if (!current.record) throw new Error(`task ${taskId} has no TaskRecord v2`);
826
806
  if (current.revision !== projection.projection.record_revision)
827
807
  throw new Error("task record changed while preparing user decision");
@@ -977,11 +957,11 @@ export default function (
977
957
  if (nextIntent) await dependencies.authorizationAfterSidecarStage?.();
978
958
  // Re-read the owner record after confirmation and compare the staged
979
959
  // candidate digest immediately before capability issuance.
980
- const liveRecord = nextIntent ? await readTaskRecord(ctx.cwd, taskId) : null;
960
+ const liveRecord = nextIntent ? await readTaskRecordRaw(ctx.cwd, taskId) : null;
981
961
  if (nextIntent && (!liveRecord?.record || liveRecord.revision !== projection.projection.record_revision || !stagedNextDiffHash))
982
962
  throw new Error("TaskRecord changed before the breaking revision digest");
983
963
  const operationDiffHash = liveRecord?.record
984
- ? diffHashOf(ctx.cwd, liveRecord.record)
964
+ ? taskDeliveryIdentity(ctx.cwd, liveRecord.record).diff_hash
985
965
  : projection.projection.diff_hash;
986
966
  if (nextIntent && operationDiffHash !== stagedNextDiffHash)
987
967
  throw new Error("staged next-state diff changed after native confirmation");
@@ -1009,7 +989,7 @@ export default function (
1009
989
  task_id: taskId,
1010
990
  operation: { ...exactOperation, capability, actor_id: LITERAL_USER_ACTOR_ID } as never,
1011
991
  prior_intent_token: priorIntent.token,
1012
- diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
992
+ diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
1013
993
  now,
1014
994
  })) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
1015
995
  if (exactOperation.op === "stop") progression.releaseStoppedReview(taskId);
@@ -1021,7 +1001,7 @@ export default function (
1021
1001
  return { state: "applied", operation, lifecycle: result.record.lifecycle };
1022
1002
  } catch (error) {
1023
1003
  if (stagedSnapshot) {
1024
- const current = await readTaskRecord(ctx.cwd, taskId);
1004
+ const current = await readTaskRecordRaw(ctx.cwd, taskId);
1025
1005
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) restoreStagedIntent();
1026
1006
  }
1027
1007
  throw error;
@@ -1029,7 +1009,7 @@ export default function (
1029
1009
  } catch (error) {
1030
1010
  const reason = error instanceof Error ? error.message : String(error);
1031
1011
  if (operation === "stop") {
1032
- const terminal = await projectAssuranceState(ctx.cwd, taskId).catch(() => null);
1012
+ const terminal = await projectAssuranceForTask(ctx.cwd, taskId).catch(() => null);
1033
1013
  if (terminal && !terminal.error && terminal.projection.lifecycle === "stopped") {
1034
1014
  progression.releaseStoppedReview(taskId);
1035
1015
  return { state: "applied", operation, lifecycle: "stopped", delivery_error: reason };
@@ -1046,11 +1026,11 @@ export default function (
1046
1026
  if (ctx.mode !== "tui") return { state: "blocked", reason: "imm_kernel_canary mutation is TUI-only" };
1047
1027
  if (progression.isInvocationOpen(taskId))
1048
1028
  return { state: "blocked", reason: `task ${taskId} already has an open invocation; concurrent assure/authorize is rejected` };
1049
- const projection = await projectAssuranceState(ctx.cwd, taskId);
1029
+ const projection = await projectAssuranceForTask(ctx.cwd, taskId);
1050
1030
  if (projection.error || !projection.claim)
1051
1031
  return { state: "blocked", reason: projection.error ?? "no active backend claim" };
1052
1032
  await dependencies.authorizationBeforeRecordRead?.();
1053
- const read = await readTaskRecord(ctx.cwd, taskId);
1033
+ const read = await readTaskRecordRaw(ctx.cwd, taskId);
1054
1034
  if (!read.record) return { state: "blocked", reason: `task ${taskId} has no TaskRecord v3` };
1055
1035
  if (read.revision !== projection.projection.record_revision)
1056
1036
  return { state: "blocked", reason: "TaskRecord changed while deriving authorization operation" };
@@ -1114,7 +1094,7 @@ export async function recordCancelledUserDecision(
1114
1094
  snapshotDigestRef: string,
1115
1095
  ): Promise<{ recorded: boolean; finding_id: string }> {
1116
1096
  const findingId = `user-decision-${operation}`;
1117
- const current = await readTaskRecord(ctx.cwd, taskId);
1097
+ const current = await readTaskRecordRaw(ctx.cwd, taskId);
1118
1098
  const openDecision = current.record?.findings.find(
1119
1099
  (finding) =>
1120
1100
  finding.kind === "unresolved_user_decision" && finding.status === "open",
@@ -1136,8 +1116,8 @@ export async function recordCancelledUserDecision(
1136
1116
  },
1137
1117
  actor_id: LITERAL_USER_ACTOR_ID,
1138
1118
  } as never,
1139
- prior_intent_token: (await readTaskIntent(ctx.cwd, taskId)).token,
1140
- diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
1119
+ prior_intent_token: (await readTaskIntentForRecord(ctx.cwd, taskId)).token,
1120
+ diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
1141
1121
  now: new Date().toISOString(),
1142
1122
  });
1143
1123
  return { recorded: true, finding_id: findingId };
@@ -1159,24 +1139,36 @@ export function buildUserDecisionOperation(record: {
1159
1139
  }
1160
1140
 
1161
1141
  /**
1162
- * One record-aware freshness identity. v4 derives the scoped revision digest
1163
- * from the immutable Enrollment base so committed and staged task work share a
1164
- * single diff_hash with Review; v3 keeps the legacy HEAD -> index digest.
1142
+ * Read the TaskIntent through the TaskRecord's `intent_ref.path`.
1143
+ *
1144
+ * `freeze_artifacts` binds the sidecar in place, so a post-freeze read must
1145
+ * follow the record instead of guessing a default path; with no record yet
1146
+ * (pre-enrollment) the Kernel resolves the sidecar that exists. Same contract
1147
+ * as the Claude adapter's `readTaskIntentForRecord`.
1148
+ */
1149
+ export function readTaskIntentForRecord(root: string, taskId: string): ReadTaskIntentResult {
1150
+ const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref.path;
1151
+ return readTaskIntent(root, taskId, currentPath);
1152
+ }
1153
+
1154
+ async function markGithubTaskTerminal(
1155
+ root: string,
1156
+ input: { task_id: string; phase: "done" | "stopped"; terminal_event_id: string },
1157
+ ): Promise<GithubTrackerResult> {
1158
+ return runGithubTrackerOperation(root, { op: "mark-terminal", ...input });
1159
+ }
1160
+
1161
+ /**
1162
+ * The shared delivery identity selector: one function in workspace_scope.ts
1163
+ * picks the identity family per TaskRecord contract. This adapter-level alias
1164
+ * only narrows the record type for the Kernel ports that expect it; the dual-host
1165
+ * conformance seam imports it so both hosts' reportable identity is comparable.
1165
1166
  */
1166
- function diffSnapshotOf(root: string, record: NonNullable<TaskRecordRead["record"]>): {
1167
+ export function diffSnapshotOf(root: string, record: TaskRecord): {
1167
1168
  diff_hash: string;
1168
1169
  changed_paths: string[];
1169
1170
  } {
1170
- if (record.contract === "assurance_kernel/task_record/v4") {
1171
- if (!record.git_base_head)
1172
- throw new Error("TaskRecord v4 is missing git_base_head");
1173
- return taskRevisionIdentity(root, record.intent_snapshot.scope_hint, record.git_base_head, record.task_id);
1174
- }
1175
- return taskDiffIdentity(root, record.intent_snapshot.scope_hint, record.task_id);
1176
- }
1177
-
1178
- function diffHashOf(root: string, record: NonNullable<TaskRecordRead["record"]>): string {
1179
- return diffSnapshotOf(root, record).diff_hash;
1171
+ return taskDeliveryIdentity(root, record);
1180
1172
  }
1181
1173
 
1182
1174
  // Translation-only adapter for the internal Kernel assurance projection. All
@@ -1196,7 +1188,7 @@ async function buildTaskOverview(root: string): Promise<{
1196
1188
  const claim = await readBackendClaim(root);
1197
1189
  let active: TaskOverviewEntry | null = null;
1198
1190
  if (claim) {
1199
- const projection = await projectAssuranceState(root, claim.task_id);
1191
+ const projection = await projectAssuranceForTask(root, claim.task_id);
1200
1192
  if (!projection.error) {
1201
1193
  const state = projection.projection;
1202
1194
  const obligation = String(state.next_obligation);
@@ -1247,56 +1239,14 @@ function overviewRailState(lifecycle: string, obligation: string): TaskRailState
1247
1239
  return "Working";
1248
1240
  }
1249
1241
 
1250
- async function projectAssuranceState(root: string, taskId: string): Promise<AssuranceProjectionResult> {
1251
- return projectAssurance(root, taskId, diffSnapshotOf);
1242
+ export async function projectAssuranceForTask(root: string, taskId: string): Promise<AssuranceProjectionResult> {
1243
+ return projectAssurance(root, taskId);
1252
1244
  }
1253
1245
 
1254
1246
  /**
1255
1247
  * Publish and prove the task-scoped synthetic revision for a v4 record. v3
1256
1248
  * records keep the legacy full-source bundle and return null here.
1257
1249
  */
1258
- export async function ensureTaskReviewRevision(
1259
- root: string,
1260
- taskId: string,
1261
- projection: AssuranceProjectionResult,
1262
- ): Promise<ReviewRevision | null> {
1263
- const current = await readTaskRecord(root, taskId);
1264
- const record = current.record;
1265
- if (!record) throw new Error(`task ${taskId} has no TaskRecord before Review preparation`);
1266
- if (current.revision !== projection.projection.record_revision)
1267
- throw new Error("TaskRecord changed before Review preparation");
1268
- if (record.contract !== "assurance_kernel/task_record/v4") return null;
1269
- if (!record.git_base_head)
1270
- throw new Error("Review revision requires a TaskRecord v4 git_base_head");
1271
- const manifest = captureReviewManifest(root, {
1272
- taskId,
1273
- baseHead: record.git_base_head,
1274
- scopeHint: record.intent_snapshot.scope_hint,
1275
- expectedDiffHash: projection.projection.diff_hash,
1276
- intentRevision: projection.projection.intent_revision,
1277
- intentContentHash: projection.projection.intent_content_hash,
1278
- recordRevision: projection.projection.record_revision,
1279
- workspaceRevision: projection.projection.workspace_revision,
1280
- lifecycle: projection.projection.lifecycle,
1281
- artifactState: projection.projection.artifact_state,
1282
- risk: record.intent_snapshot.risk,
1283
- // The same outcomes the Review snapshot is built from. A preflight stand-in
1284
- // only matched the settled QA attestation because deterministic QA happens to
1285
- // write that exact summary, so the submit-time digest comparison held by
1286
- // coincidence rather than by construction.
1287
- outcomes: qaOutcomes(record),
1288
- });
1289
- return {
1290
- contract: "assurance_kernel/review_revision/v1",
1291
- base_head: manifest.base_head,
1292
- review_tree: manifest.review_tree,
1293
- review_commit: manifest.review_commit,
1294
- review_ref: manifest.review_ref,
1295
- diff_hash: manifest.diff_hash,
1296
- manifest_digest: manifest.manifest_digest,
1297
- };
1298
- }
1299
-
1300
1250
  /**
1301
1251
  * Review refs are reconstructible evidence transport, never workflow authority.
1302
1252
  * A ref survives only while its task owns a nonterminal TaskRecord.
@@ -1314,7 +1264,7 @@ async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: str
1314
1264
  for (const entry of listed) {
1315
1265
  if (live.has(entry.taskId)) continue;
1316
1266
  try {
1317
- const record = await readTaskRecord(root, entry.taskId);
1267
+ const record = await readTaskRecordRaw(root, entry.taskId);
1318
1268
  if (record.record && record.record.lifecycle === "active" && claim?.task_id === entry.taskId)
1319
1269
  live.add(entry.taskId);
1320
1270
  } catch {
@@ -1340,267 +1290,13 @@ function notifyHost(ctx: HostContext, key: string, message: string, level: "warn
1340
1290
  if (ui) notifyOnce({ ui }, key, message, level);
1341
1291
  }
1342
1292
 
1343
- async function applyAssuranceVerdict(
1344
- // The coordinator port hands these a `HostContext`, not the Pi
1345
- // `ExtensionContext`. Both functions only ever read `cwd`; declaring the
1346
- // wider host type made the port assignment unsound.
1347
- ctx: HostContext,
1348
- snapshot: SnapshotDescriptor,
1349
- verdict: AssuranceVerdict,
1350
- invocation: InvocationToken,
1351
- actorId: string,
1352
- hooks: { beforeCommit?: () => Promise<void>; onCommit?: () => void; afterCommit?: () => Promise<void> } = {},
1353
- authorityKind: "qa" | "review" | "user" = snapshot.role,
1354
- ): Promise<void> {
1355
- const fresh = await projectAssuranceState(ctx.cwd, snapshot.task_id);
1356
- if (
1357
- fresh.error ||
1358
- fresh.claim?.task_id !== snapshot.task_id ||
1359
- fresh.projection.record_revision !== snapshot.record_revision ||
1360
- fresh.projection.workspace_revision !== snapshot.workspace_revision ||
1361
- fresh.projection.intent_revision !== snapshot.intent_revision ||
1362
- fresh.projection.intent_content_hash !== snapshot.intent_content_hash ||
1363
- fresh.projection.diff_hash !== snapshot.diff_hash ||
1364
- fresh.projection.lifecycle !== snapshot.lifecycle ||
1365
- fresh.projection.artifact_state !== snapshot.artifact_state
1366
- ) {
1367
- throw new Error(`assurance snapshot changed before authority application: ${[
1368
- fresh.error,
1369
- fresh.claim?.task_id !== snapshot.task_id ? "claim" : null,
1370
- fresh.projection.record_revision !== snapshot.record_revision ? "record_revision" : null,
1371
- fresh.projection.workspace_revision !== snapshot.workspace_revision ? "workspace_revision" : null,
1372
- fresh.projection.intent_revision !== snapshot.intent_revision ? "intent_revision" : null,
1373
- fresh.projection.intent_content_hash !== snapshot.intent_content_hash ? "intent_content_hash" : null,
1374
- fresh.projection.diff_hash !== snapshot.diff_hash ? "diff_hash" : null,
1375
- fresh.projection.lifecycle !== snapshot.lifecycle ? `lifecycle(${snapshot.lifecycle}->${fresh.projection.lifecycle})` : null,
1376
- fresh.projection.artifact_state !== snapshot.artifact_state ? `artifact_state(${snapshot.artifact_state}->${fresh.projection.artifact_state})` : null,
1377
- ].filter(Boolean).join(", ")}`);
1378
- }
1379
- const { registry, app } = await authorityPair();
1380
- const priorIntentToken = (await readTaskIntent(ctx.cwd, snapshot.task_id)).token;
1381
- const commitAndApply = async <T>(apply: () => Promise<T>): Promise<T> => {
1382
- invocationRegistry.commit(invocation);
1383
- const settlement = apply();
1384
- let hookError: unknown;
1385
- try { hooks.onCommit?.(); } catch (error) { hookError = error; }
1386
- const result = await settlement;
1387
- try { await hooks.afterCommit?.(); } catch (error) { hookError ??= error; }
1388
- if (hookError) throw hookError;
1389
- return result;
1390
- };
1391
- if (verdict.decision === "rework") {
1392
- const findings = reviewReworkFindings(verdict);
1393
- const now = new Date().toISOString();
1394
- const capability = await mintCapability(registry, {
1395
- authority_kind: authorityKind,
1396
- task_id: snapshot.task_id,
1397
- run_id: snapshot.run_id,
1398
- action_kind: "request_rework",
1399
- expected_record_hash: snapshot.record_revision,
1400
- intent_revision: snapshot.intent_revision,
1401
- intent_content_hash: snapshot.intent_content_hash,
1402
- diff_hash: snapshot.diff_hash,
1403
- actor_id: actorId,
1404
- findings,
1405
- now,
1406
- });
1407
- await hooks.beforeCommit?.();
1408
- const result = (await commitAndApply(async () => app.execute({
1409
- root: ctx.cwd,
1410
- task_id: snapshot.task_id,
1411
- operation: {
1412
- op: "request_rework",
1413
- capability,
1414
- findings: findings as never[],
1415
- actor_id: actorId,
1416
- },
1417
- prior_intent_token: priorIntentToken,
1418
- diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
1419
- now,
1420
- }))) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] }; findings?: Array<{ kind: string; status: string }> } };
1421
- stagePlanningArtifactTransition(ctx.cwd, result.record);
1422
- const parked = (result.record as { findings?: Array<{ kind: string; status: string }> }).findings?.some(
1423
- (finding) => finding.kind === "replan_required" && finding.status === "open",
1424
- );
1425
- if (parked) notifyHost(
1426
- ctx,
1427
- `rework-parked:${snapshot.task_id}`,
1428
- `rework applied: review parked for replan with ${findings.length} finding(s)`,
1429
- "warning",
1430
- );
1431
- return;
1432
- }
1433
- const now = new Date().toISOString();
1434
- const advisories = verdict.decision === "pass" ? reviewAdvisoryRecords(verdict) : [];
1435
- const approval = {
1436
- id: `approval-${snapshot.role}-${randomUUID().slice(0, 8)}`,
1437
- kind: snapshot.role === "qa" ? "qa" : "review",
1438
- authority_role: snapshot.role === "qa" ? "qa" : "reviewer",
1439
- task_revision: snapshot.intent_revision,
1440
- intent_content_hash: snapshot.intent_content_hash,
1441
- diff_hash: snapshot.diff_hash,
1442
- actor_id: actorId,
1443
- summary: verdict.approval!.summary,
1444
- // Trusted revision identity comes from the host-verified snapshot, never
1445
- // from the reviewer payload.
1446
- ...(snapshot.role === "review" && snapshot.review_revision
1447
- ? { review_revision: snapshot.review_revision }
1448
- : {}),
1449
- ...(snapshot.role === "review" && advisories.length > 0
1450
- ? { advisory_findings: advisories }
1451
- : {}),
1452
- };
1453
- const capability = await mintCapability(registry, {
1454
- authority_kind: snapshot.role,
1455
- task_id: snapshot.task_id,
1456
- run_id: snapshot.run_id,
1457
- action_kind: "record_approval",
1458
- expected_record_hash: snapshot.record_revision,
1459
- intent_revision: snapshot.intent_revision,
1460
- intent_content_hash: snapshot.intent_content_hash,
1461
- diff_hash: snapshot.diff_hash,
1462
- actor_id: actorId,
1463
- approval,
1464
- now,
1465
- });
1466
- await hooks.beforeCommit?.();
1467
- await commitAndApply(async () => app.execute({
1468
- root: ctx.cwd,
1469
- task_id: snapshot.task_id,
1470
- operation: { op: "record_approval", capability, approval, actor_id: actorId },
1471
- prior_intent_token: priorIntentToken,
1472
- diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
1473
- now,
1474
- }));
1475
- }
1476
-
1477
- async function buildAssuranceSnapshot(
1478
- root: string,
1479
- taskId: string,
1480
- role: AssuranceRole,
1481
- projection: AssuranceProjectionResult,
1482
- ): Promise<{
1483
- snapshot: SnapshotDescriptor;
1484
- descriptors: Map<string, VerificationDescriptor>;
1485
- reviewBundle: ReviewBundle | null;
1486
- reviewManifest: ReviewManifestV5 | null;
1487
- }> {
1488
- const record = await readTaskRecord(root, taskId);
1489
- if (
1490
- !record.record ||
1491
- record.revision !== projection.projection.record_revision ||
1492
- record.record.intent_snapshot.revision !== projection.projection.intent_revision ||
1493
- record.record.intent_ref.content_hash !== projection.projection.intent_content_hash
1494
- ) {
1495
- throw new Error("TaskRecord changed before assurance snapshot capture");
1496
- }
1497
- const intent = record.record.intent_snapshot;
1498
- const acceptance = intent.acceptance;
1499
- const descriptors = new Map<string, VerificationDescriptor>();
1500
- // Review resumes from settled outcomes without resolving verification tools.
1501
- for (const item of role === "qa" ? acceptance : []) {
1502
- const descriptor = parseVerificationDescriptor(item.verification);
1503
- descriptors.set(item.id, descriptor);
1504
- }
1505
- // `git_base_head` is optional on the read shape because v3 records carry
1506
- // none, so the contract test alone does not prove it is present.
1507
- const baseHead = record.record.contract === "assurance_kernel/task_record/v4"
1508
- ? record.record.git_base_head
1509
- : undefined;
1510
- if (record.record.contract === "assurance_kernel/task_record/v4" && !baseHead)
1511
- throw new Error("TaskRecord v4 is missing its Enrollment git_base_head");
1512
- const reviewRevision = baseHead
1513
- ? {
1514
- contract: "assurance_kernel/review_revision_identity/v1" as const,
1515
- base_head: baseHead,
1516
- review_commit: "",
1517
- review_tree: "",
1518
- manifest_digest: "",
1519
- }
1520
- : null;
1521
- const reviewBundle = role === "review" && !reviewRevision
1522
- ? captureReviewBundle(
1523
- root,
1524
- intent.scope_hint,
1525
- projection.projection.diff_hash,
1526
- qaOutcomes(record.record),
1527
- )
1528
- : null;
1529
- const reviewManifest = role === "review" && reviewRevision
1530
- ? captureReviewManifest(root, {
1531
- taskId,
1532
- baseHead: reviewRevision.base_head,
1533
- scopeHint: intent.scope_hint,
1534
- expectedDiffHash: projection.projection.diff_hash,
1535
- intentRevision: projection.projection.intent_revision,
1536
- intentContentHash: projection.projection.intent_content_hash,
1537
- recordRevision: projection.projection.record_revision,
1538
- workspaceRevision: projection.projection.workspace_revision,
1539
- lifecycle: projection.projection.lifecycle,
1540
- artifactState: projection.projection.artifact_state,
1541
- risk: intent.risk,
1542
- outcomes: qaOutcomes(record.record),
1543
- })
1544
- : null;
1545
- const taskSnapshot = !reviewBundle && !reviewManifest
1546
- ? captureGitTaskSnapshot(root, intent.scope_hint, taskId)
1547
- : null;
1548
- const dirtyFiles = reviewManifest
1549
- ? Object.keys(reviewManifest.changed_paths)
1550
- : reviewBundle
1551
- ? Object.keys(reviewBundle.dirty_files)
1552
- : Object.keys(taskSnapshot!.staged_files);
1553
- return {
1554
- snapshot: buildSnapshot({
1555
- root,
1556
- task_id: taskId,
1557
- role,
1558
- run_id: projection.projection.run_id,
1559
- record_revision: projection.projection.record_revision,
1560
- workspace_revision: projection.projection.workspace_revision,
1561
- intent_revision: projection.projection.intent_revision,
1562
- intent_content_hash: projection.projection.intent_content_hash,
1563
- diff_hash: projection.projection.diff_hash,
1564
- lifecycle: projection.projection.lifecycle,
1565
- artifact_state: projection.projection.artifact_state,
1566
- risk: intent.risk,
1567
- fresh_acceptance_ids: projection.projection.fresh_acceptance_ids,
1568
- missing_acceptance_ids: projection.projection.missing_acceptance_ids,
1569
- stale_attestation_ids: projection.projection.stale_attestation_ids,
1570
- acceptance,
1571
- dirty_files: dirtyFiles,
1572
- review_bundle_digest: reviewManifest?.manifest_digest ?? reviewBundle?.bundle_digest ?? null,
1573
- review_revision: reviewManifest
1574
- ? {
1575
- contract: "assurance_kernel/review_revision_identity/v1",
1576
- base_head: reviewManifest.base_head,
1577
- review_commit: reviewManifest.review_commit,
1578
- review_tree: reviewManifest.review_tree,
1579
- manifest_digest: reviewManifest.manifest_digest,
1580
- }
1581
- : undefined,
1582
- }),
1583
- descriptors,
1584
- reviewBundle,
1585
- reviewManifest,
1586
- };
1587
- }
1588
-
1589
- function qaOutcomes(
1590
- record: NonNullable<TaskRecordRead["record"]>,
1591
- ): Record<string, { status: "passed" | "failed" | "blocked"; summary: string }> {
1592
- return Object.fromEntries(
1593
- record.attestations
1594
- .filter((item) => item.kind === "qa")
1595
- .flatMap((item) => item.acceptance_results)
1596
- .map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
1597
- );
1598
- }
1599
-
1600
1293
  // The invocation registry is shared with the progression module's
1601
1294
  // module-scoped registry (see the top-level import above); commit/cancel
1602
1295
  // semantics are identical to the previous extension implementation.
1603
1296
 
1297
+ const piConfirmationReference: ConfirmationReferenceSource = ({ snapshot, now }) =>
1298
+ `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)}`;
1299
+
1604
1300
  async function mintCapability(
1605
1301
  registry: MutationAuthorityRegistry,
1606
1302
  input: {
@@ -1682,7 +1378,7 @@ async function executeOrdinaryOperation(
1682
1378
  const operation = input.operation.op === "revise_intent"
1683
1379
  ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
1684
1380
  : input.operation;
1685
- const priorIntent = await readTaskIntent(ctx.cwd, input.taskId);
1381
+ const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
1686
1382
  const sidecar = join(ctx.cwd, priorIntent.intent_ref.path);
1687
1383
  const priorBytes = operation.op === "revise_intent" ? readFileSync(sidecar) : null;
1688
1384
  // A content-changing revision writes the sidecar before the kernel's drift
@@ -1703,7 +1399,7 @@ async function executeOrdinaryOperation(
1703
1399
  task_id: input.taskId,
1704
1400
  operation: operation as never,
1705
1401
  prior_intent_token: priorIntent.token,
1706
- diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
1402
+ diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
1707
1403
  now: new Date().toISOString(),
1708
1404
  });
1709
1405
  if (operation.op === "freeze_artifacts" || operation.op === "stop")
@@ -1714,7 +1410,7 @@ async function executeOrdinaryOperation(
1714
1410
  return result;
1715
1411
  } catch (error) {
1716
1412
  if (priorBytes) {
1717
- const current = await readTaskRecord(ctx.cwd, input.taskId);
1413
+ const current = await readTaskRecordRaw(ctx.cwd, input.taskId);
1718
1414
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision && priorStaged) {
1719
1415
  restoreStagedIntentShared(ctx.cwd, priorStaged);
1720
1416
  }
@@ -1787,7 +1483,7 @@ async function enrichAssuranceResult(
1787
1483
  const taskState = { error: typeof result.reason === "string" ? result.reason : "authority projection unavailable" };
1788
1484
  return { ...result, task_state: taskState, next_action: nextActionForAssuranceResult(result, taskState) };
1789
1485
  }
1790
- const projection = await projectAssuranceState(ctx.cwd, taskId);
1486
+ const projection = await projectAssuranceForTask(ctx.cwd, taskId);
1791
1487
  const taskState: AssuranceTaskState = projection.error
1792
1488
  ? { error: projection.error }
1793
1489
  : projection.projection;
@@ -1888,33 +1584,6 @@ function prepareActionArgs<Params>(args: unknown): Params {
1888
1584
  return input as Params;
1889
1585
  }
1890
1586
 
1891
- function stagePlanningArtifactTransition(root: string, record: {
1892
- intent_ref: { path: string };
1893
- intent_snapshot: { scope_hint: string[] };
1894
- }): void {
1895
- const intentActive = record.intent_ref.path.replace("docs/plans/archive/", "docs/plans/");
1896
- const intentArchive = intentActive.replace("docs/plans/", "docs/plans/archive/");
1897
- const specActive = record.intent_snapshot.scope_hint.find((path) =>
1898
- /^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path)
1899
- && record.intent_snapshot.scope_hint.includes(path.replace("docs/specs/", "docs/specs/archive/")),
1900
- );
1901
- const candidates = [
1902
- intentActive,
1903
- intentArchive,
1904
- ...(specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []),
1905
- ];
1906
- const paths = candidates.filter((path) => existsSync(join(root, path)) || execFileSync(
1907
- "git",
1908
- ["ls-files", "--cached", "--", path],
1909
- { cwd: root, encoding: "utf8" },
1910
- ).trim().length > 0);
1911
- if (paths.length === 0) return;
1912
- execFileSync("git", ["add", "--", ...paths], {
1913
- cwd: root,
1914
- stdio: ["ignore", "pipe", "pipe"],
1915
- });
1916
- }
1917
-
1918
1587
  function failCanaryTool(
1919
1588
  taskId: string,
1920
1589
  operation: string,