immune-brain 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +239 -0
  2. package/README.zh-CN.md +239 -0
  3. package/package.json +84 -0
  4. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +666 -0
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1622 -0
  6. package/plugins/immune-brain/.pi-extension/package.json +11 -0
  7. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +711 -0
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance.ts +154 -0
  9. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +349 -0
  10. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +90 -0
  11. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +214 -0
  12. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +50 -0
  13. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +262 -0
  14. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +28 -0
  15. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +237 -0
  16. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +414 -0
  17. package/plugins/immune-brain/.pi-extension/tsconfig.json +20 -0
  18. package/plugins/immune-brain/bin/imm-kernel +4 -0
  19. package/plugins/immune-brain/bin/imm-plan +4 -0
  20. package/plugins/immune-brain/bin/imm-pr-diag +230 -0
  21. package/plugins/immune-brain/bin/imm-retire-stale-wrapper +4 -0
  22. package/plugins/immune-brain/bin/imm-retired +4 -0
  23. package/plugins/immune-brain/bin/imm-tracker +4 -0
  24. package/plugins/immune-brain/dist/BASELINE.md +138 -0
  25. package/plugins/immune-brain/dist/docs/reference/HANDOFF-template.md +122 -0
  26. package/plugins/immune-brain/dist/docs/reference/design-contract-audit-rubric.md +149 -0
  27. package/plugins/immune-brain/dist/docs/reference/design-contract-review-checklist.md +55 -0
  28. package/plugins/immune-brain/dist/docs/reference/i18n-review-checklist.md +110 -0
  29. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +52 -0
  30. package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +94 -0
  31. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +44 -0
  32. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +105 -0
  33. package/plugins/immune-brain/dist/docs/reference/ux-heuristic-checklist.md +131 -0
  34. package/plugins/immune-brain/dist/imm-brainstorm.md +140 -0
  35. package/plugins/immune-brain/dist/imm-doc-prune.md +137 -0
  36. package/plugins/immune-brain/dist/imm-loop.md +158 -0
  37. package/plugins/immune-brain/dist/imm-planner.md +387 -0
  38. package/plugins/immune-brain/dist/imm-pr-fix.md +71 -0
  39. package/plugins/immune-brain/dist/registry.yaml +49 -0
  40. package/plugins/immune-brain/dist/role-prompts/advisory-reviewer.md +16 -0
  41. package/plugins/immune-brain/dist/role-prompts/arch-explorer.md +14 -0
  42. package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
  43. package/plugins/immune-brain/dist/role-prompts/compounder.md +20 -0
  44. package/plugins/immune-brain/dist/role-prompts/executor.md +13 -0
  45. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +81 -0
  46. package/plugins/immune-brain/dist/role-prompts/qa.md +23 -0
  47. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +3 -0
  48. package/plugins/immune-brain/dist/role-prompts/ui-review.md +14 -0
  49. package/plugins/immune-brain/runtime/authority_commit_receipts.ts +716 -0
  50. package/plugins/immune-brain/runtime/canonical_json.ts +19 -0
  51. package/plugins/immune-brain/runtime/commands/kernel.ts +1160 -0
  52. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1009 -0
  53. package/plugins/immune-brain/runtime/kernel/application.ts +300 -0
  54. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +284 -0
  55. package/plugins/immune-brain/runtime/kernel/authority_port.ts +208 -0
  56. package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +451 -0
  57. package/plugins/immune-brain/runtime/kernel/backend_claim.ts +197 -0
  58. package/plugins/immune-brain/runtime/kernel/canary_application.ts +507 -0
  59. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +73 -0
  60. package/plugins/immune-brain/runtime/kernel/completion.ts +160 -0
  61. package/plugins/immune-brain/runtime/kernel/enrollment.ts +194 -0
  62. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +123 -0
  63. package/plugins/immune-brain/runtime/kernel/index.ts +29 -0
  64. package/plugins/immune-brain/runtime/kernel/intent.ts +563 -0
  65. package/plugins/immune-brain/runtime/kernel/intent_token_registry.ts +80 -0
  66. package/plugins/immune-brain/runtime/kernel/legacy.ts +299 -0
  67. package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +153 -0
  68. package/plugins/immune-brain/runtime/kernel/observation.ts +395 -0
  69. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +169 -0
  70. package/plugins/immune-brain/runtime/kernel/readiness.ts +282 -0
  71. package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +132 -0
  72. package/plugins/immune-brain/runtime/kernel/reducer.ts +624 -0
  73. package/plugins/immune-brain/runtime/kernel/storage.ts +1780 -0
  74. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +791 -0
  75. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +492 -0
  76. package/plugins/immune-brain/runtime/kernel/types.ts +295 -0
  77. package/plugins/immune-brain/runtime/kernel/validation.ts +963 -0
  78. package/plugins/immune-brain/runtime/loop_contract.ts +362 -0
  79. package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +462 -0
  80. package/plugins/immune-brain/runtime/plan_core.ts +1053 -0
  81. package/plugins/immune-brain/runtime/prompts/advisory-reviewer.md +16 -0
  82. package/plugins/immune-brain/runtime/prompts/arch-explorer.md +14 -0
  83. package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
  84. package/plugins/immune-brain/runtime/prompts/compounder.md +20 -0
  85. package/plugins/immune-brain/runtime/prompts/executor.md +13 -0
  86. package/plugins/immune-brain/runtime/prompts/pr-fix.md +81 -0
  87. package/plugins/immune-brain/runtime/prompts/qa.md +23 -0
  88. package/plugins/immune-brain/runtime/prompts/test-fixer.md +3 -0
  89. package/plugins/immune-brain/runtime/prompts/ui-review.md +14 -0
  90. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +160 -0
  91. package/plugins/immune-brain/runtime/v4_runtime.ts +295 -0
  92. package/plugins/immune-brain/runtime/verification_descriptor.ts +162 -0
  93. package/plugins/immune-brain/runtime/workspace_scope.ts +623 -0
  94. package/plugins/immune-brain/skills/.ignore +1 -0
  95. package/plugins/immune-brain/skills/BASELINE.md +138 -0
  96. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +66 -0
  97. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +11 -0
  98. package/plugins/immune-brain/skills/imm-loop/SKILL.md +52 -0
  99. package/plugins/immune-brain/skills/imm-planner/SKILL.md +221 -0
  100. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +10 -0
  101. package/plugins/immune-brain/skills/registry.yaml +49 -0
@@ -0,0 +1,1622 @@
1
+ // P3 Pi lifecycle extension: the only production route for Kernel canary
2
+ // assurance after enrollment.
3
+ //
4
+ // Surface:
5
+ // 1. `imm_kernel_canary` — foreground assurance and Review authority.
6
+ // 2. `imm_loop_action` — read-only projection of internal Loop actions.
7
+ // 3. `input` — Task Rail refresh; ordinary input stays host-native.
8
+ //
9
+ // Deterministic QA and native Review sequencing lives in
10
+ // `pi-canary-assurance-progression.ts`. The adapter owns Tool schemas, TUI
11
+ // authorization, Kernel capability creation, and translation of direct
12
+ // progression results. No detached assurance job, completion follow-up,
13
+ // progression path, Footer content, polling, or secondary authority state is
14
+ // created here. A bounded task-level Task Rail mirrors existing projections at
15
+ // host input and Tool lifecycle boundaries.
16
+
17
+ import { execFileSync } from "node:child_process";
18
+ import { createHash, randomUUID } from "node:crypto";
19
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { join, resolve } from "node:path";
21
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
22
+ import { Type } from "typebox";
23
+ import {
24
+ parseVerificationDescriptor,
25
+ canonicalDescriptorBytes,
26
+ resolveBunRunner,
27
+ assertRunnerCompatible,
28
+ runFixedVerification,
29
+ VerificationAbortedError,
30
+ findingsDigest,
31
+ type FrozenRunner,
32
+ type VerificationDescriptor,
33
+ } from "./pi-canary-verification";
34
+ import { captureReviewBundle, writeNativeReviewEvidence, type ReviewBundle } from "./pi-canary-review-bundle";
35
+ import type { InvocationToken } from "./pi-canary-invocations";
36
+ import { qaFindingId } from "./pi-canary-qa-findings";
37
+ import {
38
+ reservedAgentParams,
39
+ type ReservedAgentParams,
40
+ } from "./pi-canary-native-review";
41
+ import {
42
+ renderCanaryCall,
43
+ renderCanaryResult,
44
+ type AssuranceRole,
45
+ } from "./pi-canary-assurance";
46
+ import {
47
+ USER_ATTENTION_EVENT,
48
+ clearTerminalTaskRailOnInput,
49
+ loopResultDetails,
50
+ notifyOnce,
51
+ presentTaskRail,
52
+ presentTaskRailResult,
53
+ renderStructuredCall,
54
+ renderStructuredResult,
55
+ requestAuthorityDialog,
56
+ resetInteractionPresentation,
57
+ type UserAttentionEventV1,
58
+ type UserAttentionReason,
59
+ } from "./pi-canary-interaction";
60
+ import { isToolFailureState, throwToolFailure } from "./pi-canary-tool-failure";
61
+ import { taskDiffHash, captureGitTaskSnapshot } from "../runtime/workspace_scope";
62
+ import {
63
+ AssuranceProgression,
64
+ buildReviewPrompt,
65
+ classifyReviewWorkload,
66
+ deriveQaJobTimeoutMs,
67
+ deriveGithubTerminalProjectionInput,
68
+ parseAssuranceVerdict,
69
+ snapshotDigest,
70
+ QA_JOB_TIMEOUT_SECONDS,
71
+ REVIEW_DISPATCH_TIMEOUT_MS,
72
+ REVIEW_PREPARATION_TIMEOUT_MS,
73
+ REVIEW_TIMING_PROFILES,
74
+ REVIEW_VERDICT_VALIDATION_TIMEOUT_MS,
75
+ type AssuranceAdvanceResult,
76
+ type AssuranceProgressionPorts,
77
+ type AssuranceSubmitReviewResult,
78
+ type AssuranceVerdict,
79
+ type QaVerificationProgress,
80
+ type SnapshotDescriptor,
81
+ } from "./pi-canary-assurance-progression";
82
+
83
+ // The Kernel runtime graph is never type-checked from this extension: static
84
+ // imports resolve to ./runtime-stub.ts (relative so the Pi extension loader
85
+ // can resolve them at runtime), and the stub forwards to the real Kernel
86
+ // modules via dynamic import.
87
+ import {
88
+ createMutationAuthorityRegistry,
89
+ createCanaryApplication,
90
+ buildLoopAction,
91
+ buildLoopRoleDispatch,
92
+ readBackendClaim,
93
+ readTaskTombstone,
94
+ markGithubTaskTerminal,
95
+ reconcileKernelAuthority,
96
+ repairKernelAuthority,
97
+ readTaskRecord,
98
+ withKernelStoreLock,
99
+ inspectStorageLayout,
100
+ migrateLegacyLayout,
101
+ readTaskIntent,
102
+ parseTaskIntentV1,
103
+ canonicalIntentHash,
104
+ projectAssurance,
105
+ deriveAssuranceAuthorization,
106
+ findingsDigestV2,
107
+ capabilityActionFor,
108
+ digestOfAction,
109
+ type AssuranceAuthorizationReadiness,
110
+ type AssuranceProjectionResult,
111
+ type CanaryApplication,
112
+ type MutationAuthorityRegistry,
113
+ type CapabilityBindingV2,
114
+ } from "./runtime-stub";
115
+ import { invocationRegistry } from "./pi-canary-assurance-progression";
116
+
117
+ const TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
118
+ const LOOP_OWNERS = ["plan", "kernel", "brainstorm", "planner", "loop"] as const;
119
+ const LOOP_TARGETS = [
120
+ "step",
121
+ "test-repair",
122
+ "pr-repair",
123
+ "architecture-exploration",
124
+ "advisory-review",
125
+ "compounder",
126
+ ] as const;
127
+ const LOOP_DIRECT_ROLES = ["qa", "code-review", "ui-review"] as const;
128
+ const KERNEL_OPERATIONS = [
129
+ "status",
130
+ "freeze_artifacts",
131
+ "record_finding",
132
+ "resolve_finding",
133
+ "revise_intent",
134
+ "complete",
135
+ ] as const;
136
+
137
+ function literalUnion(values: readonly string[]) {
138
+ return Type.Union(values.map((value) => Type.Literal(value)));
139
+ }
140
+
141
+ const TASK_INTENT_SCHEMA = Type.Object({
142
+ contract: Type.Literal("assurance_kernel/task_intent/v1"),
143
+ task_id: Type.String(),
144
+ goal: Type.String(),
145
+ acceptance: Type.Array(
146
+ Type.Object({
147
+ id: Type.String(),
148
+ assertion: Type.String(),
149
+ verification: Type.String(),
150
+ }),
151
+ ),
152
+ scope_hint: Type.Array(Type.String()),
153
+ risk: Type.Union([
154
+ Type.Literal("routine"),
155
+ Type.Literal("material"),
156
+ Type.Literal("critical"),
157
+ ]),
158
+ revision: Type.Number(),
159
+ owner: Type.Literal("user"),
160
+ });
161
+
162
+ export type { AssuranceRole } from "./pi-canary-assurance";
163
+ export type AuthorizeOperation =
164
+ | "record-user-approval"
165
+ | "approve-breaking-intent-revision"
166
+ | "resolve-user-decision"
167
+ | "stop";
168
+
169
+ export interface SnapshotDescriptorInput {
170
+ root: string;
171
+ task_id: string;
172
+ role: AssuranceRole;
173
+ record_revision: string;
174
+ workspace_revision: string;
175
+ intent_revision: number;
176
+ intent_content_hash: string;
177
+ diff_hash: string;
178
+ lifecycle: string;
179
+ artifact_state: string;
180
+ risk?: "routine" | "material" | "critical";
181
+ fresh_acceptance_ids: string[];
182
+ missing_acceptance_ids: string[];
183
+ stale_attestation_ids: string[];
184
+ acceptance: Array<{ id: string; assertion: string; verification: string }>;
185
+ dirty_files?: string[];
186
+ review_bundle_digest?: string | null;
187
+ }
188
+
189
+ export function buildSnapshot(input: SnapshotDescriptorInput): SnapshotDescriptor {
190
+ return {
191
+ contract: "assurance_kernel/assurance_snapshot/v2",
192
+ task_id: input.task_id,
193
+ role: input.role,
194
+ record_revision: input.record_revision,
195
+ workspace_revision: input.workspace_revision,
196
+ intent_revision: input.intent_revision,
197
+ intent_content_hash: input.intent_content_hash,
198
+ diff_hash: input.diff_hash,
199
+ lifecycle: input.lifecycle,
200
+ artifact_state: input.artifact_state,
201
+ risk: input.risk ?? "material",
202
+ fresh_acceptance_ids: input.fresh_acceptance_ids,
203
+ missing_acceptance_ids: input.missing_acceptance_ids,
204
+ stale_attestation_ids: input.stale_attestation_ids,
205
+ acceptance: input.acceptance,
206
+ dirty_files: [...(input.dirty_files ?? [])].sort(),
207
+ review_bundle_digest: input.review_bundle_digest ?? null,
208
+ root: resolve(input.root),
209
+ };
210
+ }
211
+
212
+ export interface CanaryWorkExtensionDependencies {
213
+ buildAssurance?: typeof buildAssuranceSnapshot;
214
+ runQa?: typeof runDeterministicQa;
215
+ writeReviewEvidence?: typeof writeNativeReviewEvidence;
216
+ advanceBeforeProjection?: () => Promise<void>;
217
+ qaBeforeProjection?: () => Promise<void>;
218
+ qaBeforeAuthorityCommit?: () => Promise<void>;
219
+ qaOnAuthorityCommit?: () => void;
220
+ qaAfterAuthorityCommit?: () => Promise<void>;
221
+ authorizationBeforeRecordRead?: () => Promise<void>;
222
+ authorizationAfterSidecarStage?: () => Promise<void>;
223
+ qaJobTimeoutMs?: number;
224
+ reviewJobTimeoutMs?: number;
225
+ reviewSoftDeadlineMs?: number;
226
+ reviewPreparationTimeoutMs?: number;
227
+ reviewSpawnTimeoutMs?: number;
228
+ }
229
+
230
+ type LoopToolAction =
231
+ | {
232
+ op: "route";
233
+ ownership: (typeof LOOP_OWNERS)[number];
234
+ target: (typeof LOOP_TARGETS)[number];
235
+ context?: Record<string, unknown>;
236
+ scope_expansion?: boolean;
237
+ kernel_operation?: (typeof KERNEL_OPERATIONS)[number];
238
+ }
239
+ | {
240
+ op: "dispatch_role";
241
+ role: (typeof LOOP_DIRECT_ROLES)[number];
242
+ context: Record<string, unknown>;
243
+ };
244
+
245
+ export default function (
246
+ pi: ExtensionAPI,
247
+ dependencies: CanaryWorkExtensionDependencies = {},
248
+ ) {
249
+ const progression = new AssuranceProgression({
250
+ projectTask: (root, taskId) => projectAssuranceState(root, taskId),
251
+ readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
252
+ readTaskIntent: (root, taskId) => readTaskIntent(root, taskId),
253
+ frozenRunner: () => frozenRunner(),
254
+ buildAssurance: (root, taskId, role, projection, runner) =>
255
+ (dependencies.buildAssurance ?? buildAssuranceSnapshot)(root, taskId, role, projection, runner),
256
+ runQa: (snapshot, descriptors, runner, options) =>
257
+ (dependencies.runQa ?? runDeterministicQa)(snapshot, descriptors, runner, options),
258
+ writeReviewEvidence: (input) =>
259
+ (dependencies.writeReviewEvidence ?? writeNativeReviewEvidence)(input),
260
+ applyVerdict: (ctx, input) =>
261
+ applyAssuranceVerdict(
262
+ ctx,
263
+ input.snapshot,
264
+ input.verdict,
265
+ input.invocation,
266
+ input.actorId,
267
+ input.hooks,
268
+ ),
269
+ applyOrdinaryOperation: (ctx, input) => executeOrdinaryOperation(ctx, input),
270
+ advanceBeforeProjection: dependencies.advanceBeforeProjection,
271
+ qaBeforeProjection: dependencies.qaBeforeProjection,
272
+ qaBeforeAuthorityCommit: dependencies.qaBeforeAuthorityCommit,
273
+ qaOnAuthorityCommit: dependencies.qaOnAuthorityCommit,
274
+ qaAfterAuthorityCommit: dependencies.qaAfterAuthorityCommit,
275
+ qaJobTimeoutMs: dependencies.qaJobTimeoutMs,
276
+ } satisfies AssuranceProgressionPorts);
277
+
278
+ let railContext: ExtensionContext | undefined;
279
+ const refreshTaskRail = async (ctx: ExtensionContext) => {
280
+ try {
281
+ const claim = await readBackendClaim(ctx.cwd);
282
+ if (!claim) return;
283
+ const projection = await projectAssuranceState(ctx.cwd, claim.task_id);
284
+ if (projection.error) {
285
+ presentTaskRail(ctx, {
286
+ task_id: claim.task_id,
287
+ state: "Blocked",
288
+ result: projection.error,
289
+ next: "Inspect authority state",
290
+ });
291
+ return;
292
+ }
293
+ presentTaskRailResult(ctx, claim.task_id, {
294
+ state: "status",
295
+ operation: "status",
296
+ task_state: projection.projection,
297
+ result: "Authoritative Assurance projection loaded",
298
+ next_action: projection.projection.next_obligation,
299
+ });
300
+ } catch (error) {
301
+ notifyOnce(
302
+ ctx,
303
+ "task-rail:projection",
304
+ `Task Rail projection failed: ${error instanceof Error ? error.message : String(error)}`,
305
+ "warning",
306
+ );
307
+ }
308
+ };
309
+ const attentionEvents = pi.events as unknown as {
310
+ on?: (name: string, listener: (event: UserAttentionEventV1) => void) => void;
311
+ } | undefined;
312
+ attentionEvents?.on?.(USER_ATTENTION_EVENT, (event) => {
313
+ if (!event.active || !railContext) return;
314
+ presentTaskRail(railContext, {
315
+ task_id: event.task_id,
316
+ state: "Approval required",
317
+ result: event.label ?? "Literal-user decision required",
318
+ next: "Complete or cancel the native authorization dialog",
319
+ });
320
+ });
321
+ pi.on("input", async (event, ctx) => {
322
+ if (event.source === "extension") return { action: "continue" } as const;
323
+ railContext = ctx;
324
+ clearTerminalTaskRailOnInput(ctx);
325
+ await refreshTaskRail(ctx);
326
+ return { action: "continue" } as const;
327
+ });
328
+
329
+ pi.on("session_start", async (_event: unknown, ctx?: ExtensionContext) => {
330
+ progression.onSessionStart();
331
+ if (!ctx || ctx.mode !== "tui") return;
332
+ railContext = ctx;
333
+ await refreshTaskRail(ctx);
334
+ });
335
+ pi.on("tool_call", (event: { toolName?: string; input?: unknown; toolCallId?: string }, ctx?: ExtensionContext) => {
336
+ if (ctx) railContext = ctx;
337
+ if (event.toolName === "imm_canary_enrollment" && ctx) {
338
+ const input = event.input as { task_id?: string } | undefined;
339
+ if (input?.task_id) presentTaskRail(ctx, {
340
+ task_id: input.task_id,
341
+ state: "Planning",
342
+ result: "Preparing enrollment",
343
+ next: "Review the native enrollment decision",
344
+ });
345
+ }
346
+ return progression.observeToolCall(event);
347
+ });
348
+ pi.on("tool_result", (event: unknown, ctx?: ExtensionContext) => {
349
+ if (ctx) railContext = ctx;
350
+ const result = event as { toolName?: string; details?: Record<string, unknown> };
351
+ if (result.toolName === "imm_canary_enrollment" && ctx) {
352
+ const taskId = typeof result.details?.task_id === "string" ? result.details.task_id : undefined;
353
+ if (taskId) presentTaskRailResult(ctx, taskId, result.details);
354
+ }
355
+ progression.observeToolResult(event as Parameters<AssuranceProgression["observeToolResult"]>[0]);
356
+ });
357
+ pi.on("tool_execution_end", (event: unknown) => {
358
+ progression.observeToolEnd(event as Parameters<AssuranceProgression["observeToolEnd"]>[0]);
359
+ });
360
+ pi.on("session_shutdown", async (_event: unknown, ctx?: ExtensionContext) => {
361
+ resetInteractionPresentation(ctx ?? railContext);
362
+ railContext = undefined;
363
+ await progression.onSessionShutdown();
364
+ });
365
+
366
+ pi.registerTool({
367
+ name: "imm_kernel_canary",
368
+ label: "Kernel canary assurance and executor operations",
369
+ description:
370
+ "Advance observable QA/Review orchestration, record executor facts, or request host confirmation for one enrolled Kernel canary task.",
371
+ promptSnippet: "Kernel canary: record facts, run foreground QA, then submit one native Review receipt without polling.",
372
+ promptGuidelines: [
373
+ "Only the exact enrolled canary task is routable; verify the active backend claim first via status.",
374
+ "After implementation and focused verification, freeze the artifacts, call advance_assurance, and consume its direct terminal result; do not poll or create a detached job.",
375
+ "When advance_assurance returns review_ready, invoke the exact foreground Agent parameters from agent_params once, then call submit_review.",
376
+ "For a complete breaking revision, call approve_breaking_intent_revision with the complete next_intent directly; do not ask for chat pre-confirmation because the host opens the single native confirmation before applying it.",
377
+ "For a proven stale authority claim, call repair_authority_state directly; do not ask for chat pre-confirmation because the host opens the single native confirmation.",
378
+ "After awaiting_user, call request_authorization directly so the host opens the single native confirmation; do not ask for chat pre-confirmation or ask the user to copy or report a command.",
379
+ ],
380
+ parameters: Type.Object({
381
+ task_id: Type.String({ pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" }),
382
+ action: Type.Union([
383
+ Type.Object({ op: Type.Literal("status") }),
384
+ Type.Object({ op: Type.Literal("advance_assurance") }),
385
+ Type.Object({ op: Type.Literal("submit_review") }),
386
+ Type.Object({ op: Type.Literal("request_authorization") }),
387
+ Type.Object({ op: Type.Literal("repair_authority_state") }),
388
+ Type.Object({ op: Type.Literal("freeze_artifacts") }),
389
+ Type.Object({
390
+ op: Type.Literal("record_finding"),
391
+ finding: Type.Object({
392
+ id: Type.String(),
393
+ kind: Type.Union([Type.Literal("blocking"), Type.Literal("advisory")]),
394
+ acceptance_id: Type.Union([Type.String(), Type.Null()]),
395
+ summary: Type.String(),
396
+ }),
397
+ }),
398
+ Type.Object({ op: Type.Literal("resolve_finding"), finding_id: Type.String() }),
399
+ Type.Object({
400
+ op: Type.Literal("revise_intent"),
401
+ next_intent: TASK_INTENT_SCHEMA,
402
+ }),
403
+ Type.Object({
404
+ op: Type.Literal("approve_breaking_intent_revision"),
405
+ next_intent: TASK_INTENT_SCHEMA,
406
+ }),
407
+ Type.Object({ op: Type.Literal("complete") }),
408
+ ]),
409
+ }),
410
+ execute: async (toolCallId: string, params: { task_id: string; action: { op: string } }, signal: AbortSignal | undefined, onUpdate: ((update: ReturnType<typeof toolResult>) => void) | undefined, ctx: ExtensionContext) => {
411
+ const { task_id: taskId, action } = params;
412
+ railContext = ctx;
413
+ presentTaskRailResult(ctx, taskId, {
414
+ state: "running",
415
+ operation: action.op,
416
+ result: `${action.op} started`,
417
+ next_action: "Wait for the foreground Tool result",
418
+ });
419
+ // Storage-layout gate (BR-REQ-005/006): only `status` is read-only
420
+ // and may inspect a non-ready layout. Every mutation recovers
421
+ // Kernel transaction markers first, then runs the one-release
422
+ // migration and STOPS until the affected diff is committed.
423
+ if (action.op !== "status") {
424
+ if (ctx.mode !== "tui")
425
+ return failCanaryTool(taskId, action.op, "blocked", "tui_required", "Kernel mutation is TUI-only", "invoke the TUI Tool");
426
+ try {
427
+ await withKernelStoreLock(ctx.cwd, () => undefined);
428
+ } catch (error) {
429
+ return failCanaryTool(taskId, action.op, "blocked", "layout_recovery_failed", `Kernel transaction recovery failed: ${error instanceof Error ? error.message : String(error)}`, "resolve the pending marker and retry");
430
+ }
431
+ const inspection = await inspectStorageLayout(ctx.cwd);
432
+ if (inspection.layout === "migration_required" || inspection.layout === "recovery_required") {
433
+ const migration = await migrateLegacyLayout(ctx.cwd);
434
+ const summary = migration.outcome === "migrated"
435
+ ? `Legacy storage migrated (${migration.affected_paths.length} paths); commit the affected migration diff and retry ${action.op}`
436
+ : `Mutation blocked by storage layout (${migration.outcome}): ${migration.reason ?? inspection.reason ?? ""}`;
437
+ return failCanaryTool(taskId, action.op, "blocked", "layout_migration_required", summary, "commit the migration diff and retry");
438
+ }
439
+ if (inspection.layout !== "ready") {
440
+ return failCanaryTool(taskId, action.op, "blocked", "layout_not_ready", `Mutation blocked by storage layout (${inspection.layout}): ${inspection.reason ?? ""}`, "resolve the layout condition and retry");
441
+ }
442
+ }
443
+ if (action.op === "repair_authority_state") {
444
+ if (ctx.mode !== "tui") return failCanaryTool(taskId, action.op, "blocked", "tui_required", "imm_kernel_canary mutation is TUI-only", "invoke the TUI Tool");
445
+ const authority = await reconcileKernelAuthority(ctx.cwd, taskId);
446
+ if (
447
+ authority.state !== "repairable_stale_claim" ||
448
+ authority.owner_task_id !== taskId
449
+ ) {
450
+ const blocked = {
451
+ state: "authority_conflict",
452
+ operation: action.op,
453
+ result: authority.diagnostic ?? `Authority state is ${authority.state}`,
454
+ next_action: "inspect authority state",
455
+ };
456
+ presentTaskRailResult(ctx, taskId, blocked);
457
+ return failCanaryTool(taskId, action.op, "authority_conflict", "authority_conflict", blocked.result, blocked.next_action);
458
+ }
459
+ presentTaskRail(ctx, {
460
+ task_id: taskId,
461
+ state: "Approval required",
462
+ result: "Kernel authority repair requires literal-user approval",
463
+ next: "Decide whether to repair the stale claim",
464
+ });
465
+ const repairSelection = await requestAuthorityDialog(pi, ctx, {
466
+ attention_id: randomUUID(),
467
+ task_id: taskId,
468
+ reason: "authority_repair",
469
+ label: "Kernel authority repair required",
470
+ }, {
471
+ title: "Repair Kernel authority state?",
472
+ summary: [
473
+ `Owner: ${taskId}`,
474
+ `Terminal lifecycle: ${authority.owner_lifecycle}`,
475
+ `Claim lifecycle: ${authority.claim_lifecycle_status}`,
476
+ ].join("\n"),
477
+ details: [
478
+ `Projection revision: ${authority.revision}`,
479
+ "Action: remove only the stale global claim; preserve TaskRecord and tombstone.",
480
+ ].join("\n"),
481
+ signal: ctx.signal ?? signal,
482
+ actions: [
483
+ { value: "repair", label: "Repair stale claim", description: "Remove only the stale global claim" },
484
+ { value: "cancel", label: "Cancel", description: "Preserve all authority state unchanged" },
485
+ ],
486
+ });
487
+ if (repairSelection !== "repair") {
488
+ const cancelled = {
489
+ state: "cancelled",
490
+ operation: action.op,
491
+ result: "Authority repair cancelled with zero writes",
492
+ next_action: "request authorization again if repair is still intended",
493
+ };
494
+ return toolResult(JSON.stringify(cancelled, null, 2), cancelled);
495
+ }
496
+ try {
497
+ const repaired = await repairKernelAuthority(ctx.cwd, taskId, authority.revision);
498
+ const result = {
499
+ state: "recovered_retry",
500
+ operation: action.op,
501
+ authority: repaired,
502
+ result: `Stale authority claim repaired for ${taskId}`,
503
+ next_action: "retry the blocked managed request once",
504
+ };
505
+ return toolResult(JSON.stringify(result, null, 2), result);
506
+ } catch (error) {
507
+ const message = error instanceof Error ? error.message : String(error);
508
+ const blocked = {
509
+ state: "authority_conflict",
510
+ operation: action.op,
511
+ result: message,
512
+ next_action: "inspect authority state",
513
+ };
514
+ presentTaskRailResult(ctx, taskId, blocked);
515
+ return failCanaryTool(taskId, action.op, "authority_conflict", "authority_repair_failed", message, blocked.next_action);
516
+ }
517
+ }
518
+ if (action.op === "advance_assurance" || action.op === "request_authorization" || action.op === "submit_review" || action.op === "approve_breaking_intent_revision") {
519
+ if ((action.op === "request_authorization" || action.op === "approve_breaking_intent_revision") && ctx.mode !== "tui")
520
+ return failCanaryTool(taskId, action.op, "blocked", "tui_required", "literal-user authorization is TUI-only", "invoke the TUI Tool");
521
+ const result = action.op === "advance_assurance"
522
+ ? await progression.advance(taskId, ctx, signal, (update) => {
523
+ onUpdate?.(update);
524
+ presentTaskRailResult(ctx, taskId, update.details as Record<string, unknown> | undefined);
525
+ })
526
+ : action.op === "submit_review"
527
+ ? await progression.submitReview(taskId, ctx)
528
+ : action.op === "approve_breaking_intent_revision"
529
+ ? await authorizeExactOperation(
530
+ taskId,
531
+ "approve-breaking-intent-revision",
532
+ ctx,
533
+ (action as { next_intent?: unknown }).next_intent,
534
+ )
535
+ : await requestAuthorization(taskId, ctx);
536
+ const enriched = await enrichAssuranceResult(ctx, taskId, result as unknown as Record<string, unknown>);
537
+ presentTaskRailResult(ctx, taskId, enriched);
538
+ throwIfCanaryToolFailure(taskId, action.op, enriched);
539
+ return toolResult(JSON.stringify(enriched, null, 2), enriched);
540
+ }
541
+ const projection = await projectAssuranceState(ctx.cwd, taskId);
542
+ if (projection.error) {
543
+ const details = {
544
+ state: "blocked",
545
+ operation: action.op,
546
+ result: projection.error,
547
+ next_action: "inspect authority state",
548
+ };
549
+ presentTaskRailResult(ctx, taskId, details);
550
+ return failCanaryTool(taskId, action.op, "blocked", "projection_unavailable", projection.error, details.next_action);
551
+ }
552
+ if (action.op === "status") {
553
+ const state = projection.projection;
554
+ const fresh = state.fresh_acceptance_ids.length;
555
+ const total = fresh + state.missing_acceptance_ids.length;
556
+ const blockers = state.blocking_finding_ids.length
557
+ + state.unresolved_user_decision_ids.length
558
+ + state.replan_required_ids.length;
559
+ const details = {
560
+ state: "status",
561
+ operation: "status",
562
+ lifecycle: state.lifecycle,
563
+ artifact_state: state.artifact_state,
564
+ task_state: state,
565
+ result: `${fresh}/${total} acceptance items fresh; ${blockers} blocker${blockers === 1 ? "" : "s"}`,
566
+ next_action: state.next_obligation,
567
+ };
568
+ presentTaskRailResult(ctx, taskId, details);
569
+ return toolResult(JSON.stringify(state, null, 2), details);
570
+ }
571
+ const claim = projection.claim;
572
+ if (!claim || claim.task_id !== taskId) {
573
+ return failCanaryTool(taskId, action.op, "blocked", "claim_missing", `no active backend claim for ${taskId}`, "inspect authority state");
574
+ }
575
+ try {
576
+ const result = (await executeOrdinaryOperation(ctx, {
577
+ taskId,
578
+ operation: toCanaryOperation(action, "executor") as { op: string; actor_id: string },
579
+ })) as unknown as { revision: string; record: { lifecycle: string; artifact_state: string } };
580
+ const updated = await projectAssuranceState(ctx.cwd, taskId);
581
+ const taskState = updated.error
582
+ ? { lifecycle: result.record.lifecycle, artifact_state: result.record.artifact_state }
583
+ : updated.projection;
584
+ const nextAction = updated.error ? "inspect authority state" : updated.projection.next_obligation;
585
+ const details = {
586
+ state: "recorded",
587
+ operation: action.op,
588
+ lifecycle: result.record.lifecycle,
589
+ artifact_state: result.record.artifact_state,
590
+ task_state: taskState,
591
+ result: "Kernel executor fact recorded",
592
+ next_action: nextAction,
593
+ };
594
+ presentTaskRailResult(ctx, taskId, details);
595
+ return toolResult(
596
+ JSON.stringify(
597
+ { revision: result.revision, lifecycle: result.record.lifecycle, artifact_state: result.record.artifact_state, task_state: taskState, next_action: nextAction },
598
+ null,
599
+ 2,
600
+ ),
601
+ details,
602
+ );
603
+ } catch (error) {
604
+ return failCanaryTool(taskId, action.op, "failed", "mutation_failed", error instanceof Error ? error.message : String(error), "correct the reported failure and retry");
605
+ }
606
+ },
607
+ renderCall(args, theme) {
608
+ return renderCanaryCall(args, theme);
609
+ },
610
+ renderResult(result, _options, theme) {
611
+ return renderCanaryResult(
612
+ result as Parameters<typeof renderCanaryResult>[0],
613
+ theme,
614
+ );
615
+ },
616
+ });
617
+
618
+ pi.registerTool({
619
+ name: "imm_loop_action",
620
+ label: "Project internal Loop action",
621
+ description:
622
+ "Build one deterministic, read-only Loop action or internal role dispatch envelope. This Tool never mutates repository or workflow state.",
623
+ promptSnippet:
624
+ "Use imm_loop_action at every internal Loop role boundary before invoking an Agent or performing current-context Executor work.",
625
+ promptGuidelines: [
626
+ "Use route for Step, repair, architecture, advisory, Compounder, Kernel, or scope-expansion authority projection.",
627
+ "Use dispatch_role for QA and Review roles, then invoke the returned foreground Agent call exactly.",
628
+ ],
629
+ parameters: Type.Object({
630
+ action: Type.Union([
631
+ Type.Object({
632
+ op: Type.Literal("route"),
633
+ ownership: literalUnion(LOOP_OWNERS),
634
+ target: literalUnion(LOOP_TARGETS),
635
+ context: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
636
+ scope_expansion: Type.Optional(Type.Boolean()),
637
+ kernel_operation: Type.Optional(literalUnion(KERNEL_OPERATIONS)),
638
+ }),
639
+ Type.Object({
640
+ op: Type.Literal("dispatch_role"),
641
+ role: literalUnion(LOOP_DIRECT_ROLES),
642
+ context: Type.Record(Type.String(), Type.Unknown()),
643
+ }),
644
+ ]),
645
+ }),
646
+ execute: async (
647
+ _toolCallId: string,
648
+ params: { action: LoopToolAction },
649
+ _signal: AbortSignal | undefined,
650
+ _onUpdate: unknown,
651
+ _ctx: ExtensionContext,
652
+ ) => {
653
+ const { action } = params;
654
+ const result = action.op === "route"
655
+ ? await buildLoopAction({
656
+ ownership: action.ownership,
657
+ target: action.target,
658
+ context: action.context,
659
+ scope_expansion: action.scope_expansion,
660
+ kernel_operation: action.kernel_operation,
661
+ })
662
+ : await buildLoopRoleDispatch({ role: action.role, context: action.context });
663
+ const details = loopResultDetails(result, action.op);
664
+ return toolResult(JSON.stringify(result, null, 2), details);
665
+ },
666
+ renderCall(args, theme) {
667
+ const action = (args as { action?: LoopToolAction }).action;
668
+ const subject = action?.op === "route" ? action.target : action?.role;
669
+ return renderStructuredCall("imm_loop_action", action?.op ?? "unknown", subject, theme);
670
+ },
671
+ renderResult(result, _options, theme) {
672
+ return renderStructuredResult(
673
+ result as Parameters<typeof renderStructuredResult>[0],
674
+ theme,
675
+ );
676
+ },
677
+ });
678
+
679
+ type AuthorizationOutcome =
680
+ | { state: "applied"; operation: AuthorizeOperation; lifecycle?: string }
681
+ | { state: "cancelled"; operation: AuthorizeOperation; reason: string }
682
+ | { state: "blocked"; reason: string };
683
+
684
+ async function authorizeExactOperation(
685
+ taskId: string,
686
+ operation: AuthorizeOperation,
687
+ ctx: ExtensionContext,
688
+ nextIntentInput?: unknown,
689
+ ): Promise<AuthorizationOutcome> {
690
+ if (ctx.mode !== "tui") return { state: "blocked", reason: "imm_kernel_canary mutation is TUI-only" };
691
+ let nextIntent: Awaited<ReturnType<typeof parseTaskIntentV1>> | undefined;
692
+ let nextIntentHash: string | undefined;
693
+ let nextIntentRef: { path: string; content_hash: string } | undefined;
694
+ if (operation === "approve-breaking-intent-revision") {
695
+ try {
696
+ nextIntent = await parseTaskIntentV1(nextIntentInput);
697
+ if (nextIntent.task_id !== taskId)
698
+ throw new Error("next intent task_id must match the enrolled task");
699
+ nextIntentHash = await canonicalIntentHash(nextIntent);
700
+ } catch (error) {
701
+ return {
702
+ state: "blocked",
703
+ reason: error instanceof Error ? error.message : String(error),
704
+ };
705
+ }
706
+ }
707
+ let invocation: InvocationToken;
708
+ const authorizationGeneration = progression.sessionGenerationValue();
709
+ try {
710
+ invocation = progression.openInvocation(taskId);
711
+ } catch (error) {
712
+ const reason = error instanceof Error ? error.message : String(error);
713
+ notifyOnce(ctx, `authorization-open:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
714
+ return { state: "blocked", reason };
715
+ }
716
+ const projection = await projectAssuranceState(ctx.cwd, taskId);
717
+ if (projection.error || !projection.claim) {
718
+ const reason = projection.error ?? "no active backend claim";
719
+ notifyOnce(ctx, `authorization-claim:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
720
+ progression.closeInvocation(invocation);
721
+ return { state: "blocked", reason };
722
+ }
723
+ let userDecisionOperation: ReturnType<typeof buildUserDecisionOperation> | undefined;
724
+ if (operation === "resolve-user-decision") {
725
+ try {
726
+ const current = await readTaskRecord(ctx.cwd, taskId);
727
+ if (!current.record) throw new Error(`task ${taskId} has no TaskRecord v2`);
728
+ if (current.revision !== projection.projection.record_revision)
729
+ throw new Error("task record changed while preparing user decision");
730
+ userDecisionOperation = buildUserDecisionOperation(current.record);
731
+ } catch (error) {
732
+ const reason = error instanceof Error ? error.message : String(error);
733
+ notifyOnce(ctx, `authorization-decision:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
734
+ progression.closeInvocation(invocation);
735
+ return { state: "blocked", reason };
736
+ }
737
+ }
738
+ const dialogSummary = [
739
+ `Task: ${taskId}`,
740
+ `Decision: ${operation}`,
741
+ `State: ${projection.projection.lifecycle}:${projection.projection.artifact_state} | Claim: ${projection.claim.lifecycle_status}`,
742
+ ].join("\n");
743
+ const dialogDetails = [
744
+ `Operation: ${operation}`,
745
+ ...(userDecisionOperation
746
+ ? [
747
+ `Finding: ${userDecisionOperation.finding_id}`,
748
+ `Resolution: ${userDecisionOperation.resolution}`,
749
+ ]
750
+ : []),
751
+ ...(nextIntent
752
+ ? [
753
+ `Next Intent: rev ${nextIntent.revision} (${nextIntentHash})`,
754
+ `Next Goal: ${nextIntent.goal}`,
755
+ `Next Scope: ${nextIntent.scope_hint.join(", ")}`,
756
+ `Next Acceptance Items: ${nextIntent.acceptance.length}`,
757
+ ]
758
+ : []),
759
+ `Claim: ${projection.claim.lifecycle_status}`,
760
+ `Record revision: ${projection.projection.record_revision}`,
761
+ `State: ${projection.projection.lifecycle}:${projection.projection.artifact_state}`,
762
+ `Intent: rev ${projection.projection.intent_revision} (${projection.projection.intent_content_hash})`,
763
+ `Diff: ${projection.projection.diff_hash}`,
764
+ ].join("\n");
765
+ const snapshotDigestRef = projection.projection.record_revision;
766
+ let confirmed = false;
767
+ const attentionReason: UserAttentionReason = operation === "approve-breaking-intent-revision"
768
+ ? "breaking_intent_revision"
769
+ : "review_authorization";
770
+ presentTaskRail(ctx, {
771
+ task_id: taskId,
772
+ state: "Approval required",
773
+ result: `${operation} requires literal-user approval`,
774
+ next: `Decide ${operation}`,
775
+ });
776
+ const attention = {
777
+ attention_id: randomUUID(),
778
+ task_id: taskId,
779
+ reason: attentionReason,
780
+ label: `${operation} approval required`,
781
+ };
782
+ try {
783
+ const selected = await requestAuthorityDialog(pi, ctx, attention, {
784
+ title: `Authorize ${operation}?`,
785
+ summary: dialogSummary,
786
+ details: dialogDetails,
787
+ signal: ctx.signal,
788
+ actions: [
789
+ { value: "authorize", label: "Authorize", description: `Apply ${operation} after freshness revalidation` },
790
+ { value: "cancel", label: "Cancel", description: "Leave managed authority unchanged" },
791
+ ],
792
+ });
793
+ confirmed = selected === "authorize";
794
+ } catch {
795
+ if (operation !== "stop" && operation !== "approve-breaking-intent-revision")
796
+ await recordCancelledUserDecision(ctx, taskId, operation, snapshotDigestRef).catch(() => undefined);
797
+ progression.closeInvocation(invocation);
798
+ return { state: "cancelled", operation, reason: "confirmation aborted" };
799
+ }
800
+ if (!confirmed) {
801
+ if (operation !== "stop" && operation !== "approve-breaking-intent-revision")
802
+ await recordCancelledUserDecision(ctx, taskId, operation, snapshotDigestRef).catch(() => undefined);
803
+ progression.closeInvocation(invocation);
804
+ return { state: "cancelled", operation, reason: "cancelled" };
805
+ }
806
+ if (!progression.sessionActiveValue() || progression.sessionGenerationValue() !== authorizationGeneration || progression.invocationState(invocation) !== "open") {
807
+ notifyOnce(ctx, `authorization-session:${taskId}:${operation}`, `authorize ${operation}: session changed; confirmation discarded`, "warning");
808
+ progression.closeInvocation(invocation);
809
+ return { state: "blocked", reason: "session changed; confirmation discarded" };
810
+ }
811
+ try {
812
+ // Linearization point: only this fresh affirmative continuation
813
+ // may mint/apply; timeout/cancel already won open -> cancelled.
814
+ try {
815
+ progression.commitInvocation(invocation);
816
+ } catch (error) {
817
+ const reason = error instanceof Error ? error.message : String(error);
818
+ notifyOnce(ctx, `authorization-commit:${taskId}:${operation}:${reason}`, `authorize ${operation} aborted: ${reason}`, "error");
819
+ return { state: "blocked", reason };
820
+ }
821
+ const { registry, app } = await authorityPair();
822
+ const now = new Date().toISOString();
823
+ const priorIntent = await readTaskIntent(ctx.cwd, taskId);
824
+ if (nextIntent) {
825
+ nextIntentRef = {
826
+ path: `docs/plans/${nextIntent.task_id}.intent.json`,
827
+ content_hash: nextIntentHash!,
828
+ };
829
+ }
830
+ // record-user-approval: literal-user approval for critical-task-completion. The approval payload is bound to the fresh
831
+ // projection (task revision, intent content hash, diff hash) and
832
+ // applied through the same exact-action capability path; the
833
+ // The reducer requires kind user, user authority, and active:frozen state.
834
+ const isUserApproval = operation === "record-user-approval";
835
+ const approval = isUserApproval
836
+ ? {
837
+ id: `approval-user-${randomUUID().slice(0, 8)}`,
838
+ kind: "user" as const,
839
+ authority_role: "user" as const,
840
+ task_revision: projection.projection.intent_revision,
841
+ intent_content_hash: projection.projection.intent_content_hash,
842
+ diff_hash: projection.projection.diff_hash,
843
+ actor_id: "literal-user",
844
+ summary: "literal user approval",
845
+ }
846
+ : undefined;
847
+ const exactOperation = operation === "stop"
848
+ ? { op: "stop" as const, reason: "literal user stopped task parked for replan" }
849
+ : operation === "approve-breaking-intent-revision"
850
+ ? {
851
+ op: "approve_breaking_intent_revision" as const,
852
+ next_intent: nextIntent!,
853
+ next_intent_ref: nextIntentRef!,
854
+ }
855
+ : userDecisionOperation ?? userOperationFor(operation, approval);
856
+ // The exact host-built operation is shared by capability digest and
857
+ // application payload; command arguments cannot inject authority fields.
858
+ const sidecar = nextIntent ? join(ctx.cwd, priorIntent.intent_ref.path) : undefined;
859
+ const priorBytes = sidecar ? readFileSync(sidecar) : undefined;
860
+ try {
861
+ if (sidecar) {
862
+ writeFileSync(sidecar, `${JSON.stringify(nextIntent, null, 2)}\n`);
863
+ execFileSync("git", ["add", "--", priorIntent.intent_ref.path], {
864
+ cwd: ctx.cwd,
865
+ stdio: ["ignore", "pipe", "pipe"],
866
+ });
867
+ await dependencies.authorizationAfterSidecarStage?.();
868
+ }
869
+ const operationDiffHash = nextIntent
870
+ ? diffHashOf(ctx.cwd, priorIntent.intent)
871
+ : projection.projection.diff_hash;
872
+ const capability = await mintCapability(registry, {
873
+ authority_kind: "user",
874
+ task_id: taskId,
875
+ action_kind: exactOperation.op,
876
+ expected_record_hash: projection.projection.record_revision,
877
+ intent_revision: nextIntent?.revision ?? projection.projection.intent_revision,
878
+ intent_content_hash: nextIntentHash ?? projection.projection.intent_content_hash,
879
+ diff_hash: operationDiffHash,
880
+ actor_id: "literal-user",
881
+ ...(exactOperation.op === "record_user_approval" ? { approval: exactOperation.approval } : {}),
882
+ ...(exactOperation.op === "approve_breaking_intent_revision"
883
+ ? { next_intent: exactOperation.next_intent, next_intent_ref: exactOperation.next_intent_ref }
884
+ : {}),
885
+ ...(exactOperation.op === "resolve_user_decision"
886
+ ? { finding_id: exactOperation.finding_id, resolution: exactOperation.resolution }
887
+ : {}),
888
+ ...(exactOperation.op === "stop" ? { reason: exactOperation.reason } : {}),
889
+ now,
890
+ });
891
+ const result = (await app.execute({
892
+ root: ctx.cwd,
893
+ task_id: taskId,
894
+ operation: { ...exactOperation, capability, actor_id: "literal-user" } as never,
895
+ prior_intent_token: priorIntent.token,
896
+ diffProvider: (root: string, intent: { scope_hint: unknown }) => diffHashOf(root, intent),
897
+ now,
898
+ })) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
899
+ if (
900
+ exactOperation.op === "stop"
901
+ || exactOperation.op === "approve_breaking_intent_revision"
902
+ ) stagePlanningArtifactTransition(ctx.cwd, result.record);
903
+ return { state: "applied", operation, lifecycle: result.record.lifecycle };
904
+ } catch (error) {
905
+ if (sidecar && priorBytes) {
906
+ const current = await readTaskRecord(ctx.cwd, taskId);
907
+ if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) {
908
+ writeFileSync(sidecar, priorBytes);
909
+ execFileSync("git", ["add", "--", priorIntent.intent_ref.path], {
910
+ cwd: ctx.cwd,
911
+ stdio: ["ignore", "pipe", "pipe"],
912
+ });
913
+ }
914
+ }
915
+ throw error;
916
+ }
917
+ } catch (error) {
918
+ const reason = error instanceof Error ? error.message : String(error);
919
+ notifyOnce(ctx, `authorization-apply:${taskId}:${operation}:${reason}`, `authorize failed: ${reason}`, "error");
920
+ return { state: "blocked", reason };
921
+ } finally {
922
+ progression.closeInvocation(invocation);
923
+ }
924
+ }
925
+
926
+ async function requestAuthorization(taskId: string, ctx: ExtensionContext): Promise<AuthorizationOutcome> {
927
+ if (ctx.mode !== "tui") return { state: "blocked", reason: "imm_kernel_canary mutation is TUI-only" };
928
+ if (progression.isInvocationOpen(taskId))
929
+ return { state: "blocked", reason: `task ${taskId} already has an open invocation; concurrent assure/authorize is rejected` };
930
+ const projection = await projectAssuranceState(ctx.cwd, taskId);
931
+ if (projection.error || !projection.claim)
932
+ return { state: "blocked", reason: projection.error ?? "no active backend claim" };
933
+ await dependencies.authorizationBeforeRecordRead?.();
934
+ const read = await readTaskRecord(ctx.cwd, taskId);
935
+ if (!read.record) return { state: "blocked", reason: `task ${taskId} has no TaskRecord v3` };
936
+ if (read.revision !== projection.projection.record_revision)
937
+ return { state: "blocked", reason: "TaskRecord changed while deriving authorization operation" };
938
+ const derived = deriveAuthorizationOperation({
939
+ readiness: projection.projection.authorization,
940
+ hasOpenReplanRequired: read.record.findings.some(
941
+ (finding) => finding.kind === "replan_required" && finding.status === "open",
942
+ ),
943
+ });
944
+ if ("blocked" in derived) return { state: "blocked", reason: derived.blocked };
945
+ return authorizeExactOperation(taskId, derived.operation, ctx);
946
+ }
947
+ }
948
+
949
+ // ---------------------------------------------------------------------------
950
+ // Helpers (module scope; no workflow state)
951
+ // ---------------------------------------------------------------------------
952
+
953
+ export type DerivedAuthorizationOperation =
954
+ | "resolve-user-decision"
955
+ | "record-user-approval"
956
+ | "stop";
957
+
958
+ // Kernel projection is the sole source of authorization readiness.
959
+ export function deriveAuthorizationOperation(input: {
960
+ readiness: AssuranceAuthorizationReadiness;
961
+ hasOpenReplanRequired?: boolean;
962
+ }): { operation: DerivedAuthorizationOperation } | { blocked: string } {
963
+ if (input.hasOpenReplanRequired) return { operation: "stop" };
964
+ if (input.readiness.state === "resolve_user_decision") return { operation: "resolve-user-decision" };
965
+ if (input.readiness.state === "record_user_approval") return { operation: "record-user-approval" };
966
+ if (input.readiness.blocked) return { blocked: input.readiness.blocked };
967
+ return { blocked: "no unique host-derived authorization operation" };
968
+ }
969
+
970
+ function toCanaryOperation(action: { op: string }, actorId: string) {
971
+ switch (action.op) {
972
+ case "freeze_artifacts":
973
+ return { op: "freeze_artifacts", actor_id: actorId };
974
+ case "record_finding":
975
+ return {
976
+ op: "record_finding",
977
+ finding: (action as unknown as { finding: unknown }).finding,
978
+ actor_id: actorId,
979
+ };
980
+ case "resolve_finding":
981
+ return { op: "resolve_finding", finding_id: (action as unknown as { finding_id: string }).finding_id, actor_id: actorId };
982
+ case "revise_intent":
983
+ return { op: "revise_intent", next_intent: (action as unknown as { next_intent: unknown }).next_intent, actor_id: actorId };
984
+ case "complete":
985
+ return { op: "complete", actor_id: actorId };
986
+ default:
987
+ throw new Error(`unsupported ordinary operation: ${action.op}`);
988
+ }
989
+ }
990
+
991
+ export async function recordCancelledUserDecision(
992
+ ctx: ExtensionContext,
993
+ taskId: string,
994
+ operation: string,
995
+ snapshotDigestRef: string,
996
+ ): Promise<{ recorded: boolean; finding_id: string }> {
997
+ const findingId = `user-decision-${operation}`;
998
+ const current = await readTaskRecord(ctx.cwd, taskId);
999
+ const openDecision = current.record?.findings.find(
1000
+ (finding) =>
1001
+ finding.kind === "unresolved_user_decision" && finding.status === "open",
1002
+ );
1003
+ // Deduplicate onto the existing open decision trail regardless of its id:
1004
+ // a pending decision must never be shadowed by a second trail entry.
1005
+ if (openDecision) return { recorded: false, finding_id: openDecision.id };
1006
+ const { app } = await authorityPair();
1007
+ await app.execute({
1008
+ root: ctx.cwd,
1009
+ task_id: taskId,
1010
+ operation: {
1011
+ op: "record_finding",
1012
+ finding: {
1013
+ id: findingId,
1014
+ kind: "unresolved_user_decision",
1015
+ acceptance_id: null,
1016
+ summary: `${operation} confirmation cancelled by literal user; snapshot ${snapshotDigestRef}`,
1017
+ },
1018
+ actor_id: "literal-user",
1019
+ } as never,
1020
+ prior_intent_token: (await readTaskIntent(ctx.cwd, taskId)).token,
1021
+ diffProvider: (root: string, intent: { scope_hint: unknown }) => diffHashOf(root, intent),
1022
+ now: new Date().toISOString(),
1023
+ });
1024
+ return { recorded: true, finding_id: findingId };
1025
+ }
1026
+
1027
+ export function buildUserDecisionOperation(record: {
1028
+ findings: Array<{ id: string; kind: string; status: string; summary?: string }>;
1029
+ }) {
1030
+ const open = record.findings.filter(
1031
+ (finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
1032
+ );
1033
+ if (open.length !== 1)
1034
+ throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
1035
+ return {
1036
+ op: "resolve_user_decision" as const,
1037
+ finding_id: open[0].id,
1038
+ resolution: `resume after literal-user decision: ${open[0].summary}`,
1039
+ };
1040
+ }
1041
+
1042
+ export function userOperationFor(operation: AuthorizeOperation, approval?: unknown) {
1043
+ if (operation !== "record-user-approval")
1044
+ throw new Error(`unsupported authorize operation: ${operation}`);
1045
+ // The approval payload is constructed by the authorize handler from
1046
+ // the fresh projection; it is never derived from untrusted input.
1047
+ if (approval === undefined) throw new Error("record-user-approval requires an approval payload");
1048
+ return { op: "record_user_approval" as const, approval };
1049
+ }
1050
+
1051
+ function diffHashOf(root: string, intent: { scope_hint?: unknown }): string {
1052
+ return taskDiffHash(root, intent.scope_hint);
1053
+ }
1054
+
1055
+ // Translation-only adapter for the internal Kernel assurance projection. All
1056
+ // freshness, approval, finding, claim, and authorization facts come from the
1057
+ // Kernel module; this wrapper only binds the host diff provider. The retired
1058
+ // active-v2 migrator is gone: a v2 TaskRecord in the state layout is a
1059
+ // fail-closed projection error, never an automatic migration trigger.
1060
+ async function projectAssuranceState(root: string, taskId: string): Promise<AssuranceProjectionResult> {
1061
+ return projectAssurance(root, taskId, diffHashOf);
1062
+ }
1063
+
1064
+ export interface QaVerificationProgressInput {
1065
+ index: number;
1066
+ total: number;
1067
+ acceptance_id: string;
1068
+ phase: "running" | "passed" | "failed";
1069
+ elapsed_ms: number;
1070
+ }
1071
+
1072
+ export function boundedVerificationFailureDetail(stdout: string, stderr: string): string {
1073
+ const output = (stderr || stdout).trim();
1074
+ const limit = 500;
1075
+ if (output.length <= limit) return output;
1076
+ const marker = "\n... output omitted ...\n";
1077
+ const available = limit - marker.length;
1078
+ const headLength = Math.floor(available / 3);
1079
+ const tailLength = available - headLength;
1080
+ return `${output.slice(0, headLength)}${marker}${output.slice(-tailLength)}`;
1081
+ }
1082
+
1083
+ export async function runDeterministicQa(
1084
+ snapshot: SnapshotDescriptor,
1085
+ descriptors: Map<string, VerificationDescriptor>,
1086
+ runner: FrozenRunner,
1087
+ options: {
1088
+ signal?: AbortSignal;
1089
+ onProgress?: (progress: QaVerificationProgressInput) => void;
1090
+ runVerification?: typeof runFixedVerification;
1091
+ } = {},
1092
+ ): Promise<AssuranceVerdict> {
1093
+ if (snapshot.role !== "qa") throw new Error("deterministic QA requires qa role");
1094
+ if (options.signal?.aborted) throw new VerificationAbortedError();
1095
+ const findings: NonNullable<AssuranceVerdict["findings"]> = [];
1096
+ const runVerification = options.runVerification ?? runFixedVerification;
1097
+ for (const [offset, item] of snapshot.acceptance.entries()) {
1098
+ if (options.signal?.aborted) throw new VerificationAbortedError();
1099
+ const descriptor = descriptors.get(item.id);
1100
+ if (!descriptor) throw new Error(`verification descriptor missing for ${item.id}`);
1101
+ const startedAt = Date.now();
1102
+ options.onProgress?.({
1103
+ index: offset + 1,
1104
+ total: snapshot.acceptance.length,
1105
+ acceptance_id: item.id,
1106
+ phase: "running",
1107
+ elapsed_ms: 0,
1108
+ });
1109
+ const result = await runVerification(snapshot.root, descriptor, runner, {
1110
+ signal: options.signal,
1111
+ });
1112
+ const failed = result.exit_code !== 0 || result.timed_out;
1113
+ options.onProgress?.({
1114
+ index: offset + 1,
1115
+ total: snapshot.acceptance.length,
1116
+ acceptance_id: item.id,
1117
+ phase: failed ? "failed" : "passed",
1118
+ elapsed_ms: Date.now() - startedAt,
1119
+ });
1120
+ if (failed) {
1121
+ const detail = boundedVerificationFailureDetail(result.stdout, result.stderr);
1122
+ findings.push({
1123
+ id: qaFindingId(item.id, snapshotDigest(snapshot)),
1124
+ kind: "blocking",
1125
+ acceptance_id: item.id,
1126
+ summary: `verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""}) stdout=${result.stdout.length}B stderr=${result.stderr.length}B${detail ? `: ${detail}` : ""}`,
1127
+ findings_digest: "",
1128
+ });
1129
+ }
1130
+ }
1131
+ if (findings.length > 0) {
1132
+ return {
1133
+ contract: "assurance_kernel/assurance_verdict/v2",
1134
+ role: "qa",
1135
+ task_id: snapshot.task_id,
1136
+ snapshot_digest: snapshotDigest(snapshot),
1137
+ decision: "rework",
1138
+ findings,
1139
+ };
1140
+ }
1141
+ return {
1142
+ contract: "assurance_kernel/assurance_verdict/v2",
1143
+ role: "qa",
1144
+ task_id: snapshot.task_id,
1145
+ snapshot_digest: snapshotDigest(snapshot),
1146
+ decision: "pass",
1147
+ approval: {
1148
+ kind: "qa",
1149
+ authority_role: "qa",
1150
+ summary: `all ${snapshot.acceptance.length} fixed verification descriptor(s) passed`,
1151
+ },
1152
+ };
1153
+ }
1154
+
1155
+ async function applyAssuranceVerdict(
1156
+ ctx: ExtensionContext,
1157
+ snapshot: SnapshotDescriptor,
1158
+ verdict: AssuranceVerdict,
1159
+ invocation: InvocationToken,
1160
+ actorId: string,
1161
+ hooks: { beforeCommit?: () => Promise<void>; onCommit?: () => void; afterCommit?: () => Promise<void> } = {},
1162
+ authorityKind: "qa" | "review" | "user" = snapshot.role,
1163
+ ): Promise<void> {
1164
+ const fresh = await projectAssuranceState(ctx.cwd, snapshot.task_id);
1165
+ if (
1166
+ fresh.error ||
1167
+ fresh.claim?.task_id !== snapshot.task_id ||
1168
+ fresh.projection.record_revision !== snapshot.record_revision ||
1169
+ fresh.projection.workspace_revision !== snapshot.workspace_revision ||
1170
+ fresh.projection.intent_revision !== snapshot.intent_revision ||
1171
+ fresh.projection.intent_content_hash !== snapshot.intent_content_hash ||
1172
+ fresh.projection.diff_hash !== snapshot.diff_hash ||
1173
+ fresh.projection.lifecycle !== snapshot.lifecycle ||
1174
+ fresh.projection.artifact_state !== snapshot.artifact_state
1175
+ ) {
1176
+ throw new Error(`assurance snapshot changed before authority application: ${[
1177
+ fresh.error,
1178
+ fresh.claim?.task_id !== snapshot.task_id ? "claim" : null,
1179
+ fresh.projection.record_revision !== snapshot.record_revision ? "record_revision" : null,
1180
+ fresh.projection.workspace_revision !== snapshot.workspace_revision ? "workspace_revision" : null,
1181
+ fresh.projection.intent_revision !== snapshot.intent_revision ? "intent_revision" : null,
1182
+ fresh.projection.intent_content_hash !== snapshot.intent_content_hash ? "intent_content_hash" : null,
1183
+ fresh.projection.diff_hash !== snapshot.diff_hash ? "diff_hash" : null,
1184
+ fresh.projection.lifecycle !== snapshot.lifecycle ? `lifecycle(${snapshot.lifecycle}->${fresh.projection.lifecycle})` : null,
1185
+ fresh.projection.artifact_state !== snapshot.artifact_state ? `artifact_state(${snapshot.artifact_state}->${fresh.projection.artifact_state})` : null,
1186
+ ].filter(Boolean).join(", ")}`);
1187
+ }
1188
+ const { registry, app } = await authorityPair();
1189
+ const priorIntentToken = (await readTaskIntent(ctx.cwd, snapshot.task_id)).token;
1190
+ const commitAndApply = async <T>(apply: () => Promise<T>): Promise<T> => {
1191
+ invocationRegistry.commit(invocation);
1192
+ const settlement = apply();
1193
+ let hookError: unknown;
1194
+ try { hooks.onCommit?.(); } catch (error) { hookError = error; }
1195
+ const result = await settlement;
1196
+ try { await hooks.afterCommit?.(); } catch (error) { hookError ??= error; }
1197
+ if (hookError) throw hookError;
1198
+ return result;
1199
+ };
1200
+ if (verdict.decision === "rework") {
1201
+ const findings = verdict.findings!.map((finding) => ({
1202
+ id: finding.id,
1203
+ kind: finding.kind,
1204
+ status: "open",
1205
+ acceptance_id: finding.acceptance_id,
1206
+ source: "review",
1207
+ review_round: null,
1208
+ summary: finding.summary,
1209
+ }));
1210
+ const now = new Date().toISOString();
1211
+ const capability = await mintCapability(registry, {
1212
+ authority_kind: authorityKind,
1213
+ task_id: snapshot.task_id,
1214
+ action_kind: "request_rework",
1215
+ expected_record_hash: snapshot.record_revision,
1216
+ intent_revision: snapshot.intent_revision,
1217
+ intent_content_hash: snapshot.intent_content_hash,
1218
+ diff_hash: snapshot.diff_hash,
1219
+ actor_id: actorId,
1220
+ findings,
1221
+ now,
1222
+ });
1223
+ await hooks.beforeCommit?.();
1224
+ const result = (await commitAndApply(async () => app.execute({
1225
+ root: ctx.cwd,
1226
+ task_id: snapshot.task_id,
1227
+ operation: {
1228
+ op: "request_rework",
1229
+ capability,
1230
+ findings: findings as never[],
1231
+ actor_id: actorId,
1232
+ },
1233
+ prior_intent_token: priorIntentToken,
1234
+ diffProvider: (root: string, intent: { scope_hint: unknown }) => diffHashOf(root, intent),
1235
+ now,
1236
+ }))) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] }; findings?: Array<{ kind: string; status: string }> } };
1237
+ stagePlanningArtifactTransition(ctx.cwd, result.record);
1238
+ const parked = (result.record as { findings?: Array<{ kind: string; status: string }> }).findings?.some(
1239
+ (finding) => finding.kind === "replan_required" && finding.status === "open",
1240
+ );
1241
+ if (parked) notifyOnce(
1242
+ ctx,
1243
+ `rework-parked:${snapshot.task_id}`,
1244
+ `rework applied: review parked for replan with ${findings.length} finding(s)`,
1245
+ "warning",
1246
+ );
1247
+ return;
1248
+ }
1249
+ const now = new Date().toISOString();
1250
+ const approval = {
1251
+ id: `approval-${snapshot.role}-${randomUUID().slice(0, 8)}`,
1252
+ kind: snapshot.role === "qa" ? "qa" : "review",
1253
+ authority_role: snapshot.role === "qa" ? "qa" : "reviewer",
1254
+ task_revision: snapshot.intent_revision,
1255
+ intent_content_hash: snapshot.intent_content_hash,
1256
+ diff_hash: snapshot.diff_hash,
1257
+ actor_id: actorId,
1258
+ summary: verdict.approval!.summary,
1259
+ };
1260
+ const capability = await mintCapability(registry, {
1261
+ authority_kind: snapshot.role,
1262
+ task_id: snapshot.task_id,
1263
+ action_kind: "record_approval",
1264
+ expected_record_hash: snapshot.record_revision,
1265
+ intent_revision: snapshot.intent_revision,
1266
+ intent_content_hash: snapshot.intent_content_hash,
1267
+ diff_hash: snapshot.diff_hash,
1268
+ actor_id: actorId,
1269
+ approval,
1270
+ now,
1271
+ });
1272
+ await hooks.beforeCommit?.();
1273
+ await commitAndApply(async () => app.execute({
1274
+ root: ctx.cwd,
1275
+ task_id: snapshot.task_id,
1276
+ operation: { op: "record_approval", capability, approval, actor_id: actorId },
1277
+ prior_intent_token: priorIntentToken,
1278
+ diffProvider: (root: string, intent: { scope_hint: unknown }) => diffHashOf(root, intent),
1279
+ now,
1280
+ }));
1281
+ }
1282
+
1283
+ async function buildAssuranceSnapshot(
1284
+ root: string,
1285
+ taskId: string,
1286
+ role: AssuranceRole,
1287
+ projection: AssuranceProjectionResult,
1288
+ runner: FrozenRunner,
1289
+ ): Promise<{ snapshot: SnapshotDescriptor; descriptors: Map<string, VerificationDescriptor>; reviewBundle: ReviewBundle | null }> {
1290
+ const record = await readTaskRecord(root, taskId);
1291
+ if (
1292
+ !record.record ||
1293
+ record.revision !== projection.projection.record_revision ||
1294
+ record.record.intent_snapshot.revision !== projection.projection.intent_revision ||
1295
+ record.record.intent_ref.content_hash !== projection.projection.intent_content_hash
1296
+ ) {
1297
+ throw new Error("TaskRecord changed before assurance snapshot capture");
1298
+ }
1299
+ const intent = record.record.intent_snapshot;
1300
+ const acceptance = intent.acceptance;
1301
+ const descriptors = new Map<string, VerificationDescriptor>();
1302
+ // Every verification string must parse as strict canonical JSON
1303
+ // verification_descriptor/v1 and claim the frozen runner version;
1304
+ // a free-form or version-mismatched string is ineligible.
1305
+ for (const item of acceptance) {
1306
+ const descriptor = parseVerificationDescriptor(item.verification);
1307
+ assertRunnerCompatible(descriptor, runner);
1308
+ descriptors.set(item.id, descriptor);
1309
+ }
1310
+ const reviewBundle = role === "review"
1311
+ ? captureReviewBundle(
1312
+ root,
1313
+ intent.scope_hint,
1314
+ projection.projection.diff_hash,
1315
+ Object.fromEntries(
1316
+ record.record.attestations
1317
+ .filter((item) => item.kind === "qa")
1318
+ .flatMap((item) => item.acceptance_results)
1319
+ .map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
1320
+ ),
1321
+ )
1322
+ : null;
1323
+ const taskSnapshot = reviewBundle ? null : captureGitTaskSnapshot(root, intent.scope_hint);
1324
+ const dirtyFiles = reviewBundle
1325
+ ? Object.keys(reviewBundle.dirty_files)
1326
+ : Object.keys(taskSnapshot!.staged_files);
1327
+ return {
1328
+ snapshot: buildSnapshot({
1329
+ root,
1330
+ task_id: taskId,
1331
+ role,
1332
+ record_revision: projection.projection.record_revision,
1333
+ workspace_revision: projection.projection.workspace_revision,
1334
+ intent_revision: projection.projection.intent_revision,
1335
+ intent_content_hash: projection.projection.intent_content_hash,
1336
+ diff_hash: projection.projection.diff_hash,
1337
+ lifecycle: projection.projection.lifecycle,
1338
+ artifact_state: projection.projection.artifact_state,
1339
+ risk: intent.risk,
1340
+ fresh_acceptance_ids: projection.projection.fresh_acceptance_ids,
1341
+ missing_acceptance_ids: projection.projection.missing_acceptance_ids,
1342
+ stale_attestation_ids: projection.projection.stale_attestation_ids,
1343
+ acceptance,
1344
+ dirty_files: dirtyFiles,
1345
+ review_bundle_digest: reviewBundle?.bundle_digest ?? null,
1346
+ }),
1347
+ descriptors,
1348
+ reviewBundle,
1349
+ };
1350
+ }
1351
+
1352
+ let frozenRunnerValue: FrozenRunner | undefined;
1353
+ async function frozenRunner(): Promise<FrozenRunner> {
1354
+ frozenRunnerValue ??= resolveBunRunner();
1355
+ return frozenRunnerValue;
1356
+ }
1357
+
1358
+ // The invocation registry is shared with the progression module's
1359
+ // module-scoped registry (see the top-level import above); commit/cancel
1360
+ // semantics are identical to the previous extension implementation.
1361
+
1362
+ async function mintCapability(
1363
+ registry: MutationAuthorityRegistry,
1364
+ input: {
1365
+ authority_kind: "review" | "qa" | "user";
1366
+ task_id: string;
1367
+ action_kind: string;
1368
+ expected_record_hash: string;
1369
+ intent_revision: number;
1370
+ intent_content_hash: string;
1371
+ diff_hash: string;
1372
+ actor_id: string;
1373
+ findings?: unknown[];
1374
+ approval?: unknown;
1375
+ next_intent?: unknown;
1376
+ next_intent_ref?: unknown;
1377
+ reason?: string;
1378
+ finding_id?: string;
1379
+ resolution?: string;
1380
+ now: string;
1381
+ },
1382
+ ) {
1383
+ const now = input.now;
1384
+ // The action digest is computed by the Kernel from the canonical action
1385
+ // builder (same field order and payload the consuming application will
1386
+ // inspect), so the minted capability always matches the applied action.
1387
+ const action = (await capabilityActionFor({
1388
+ op: input.action_kind,
1389
+ task_id: input.task_id,
1390
+ at: now,
1391
+ actor_id: input.actor_id,
1392
+ ...(input.reason !== undefined ? { reason: input.reason } : {}),
1393
+ ...(input.findings !== undefined ? { findings: input.findings } : {}),
1394
+ ...(input.approval !== undefined ? { approval: input.approval } : {}),
1395
+ ...(input.next_intent !== undefined ? { next_intent: input.next_intent } : {}),
1396
+ ...(input.next_intent_ref !== undefined ? { next_intent_ref: input.next_intent_ref } : {}),
1397
+ ...(input.finding_id !== undefined ? { finding_id: input.finding_id } : {}),
1398
+ ...(input.resolution !== undefined ? { resolution: input.resolution } : {}),
1399
+ })) as unknown as Record<string, unknown>;
1400
+ const digest = await digestOfAction(action as never);
1401
+ const binding: CapabilityBindingV2 = {
1402
+ authority_kind: input.authority_kind,
1403
+ task_id: input.task_id,
1404
+ action_digest: digest,
1405
+ expected_record_hash: input.expected_record_hash,
1406
+ intent_revision: input.intent_revision,
1407
+ intent_content_hash: input.intent_content_hash,
1408
+ diff_hash: input.diff_hash,
1409
+ actor_id: input.actor_id,
1410
+ confirmation_ref: `pi-confirm-${createHash("sha256").update(`${input.task_id}\0${now}`).digest("hex").slice(0, 16)}`,
1411
+ expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
1412
+ findings_digest:
1413
+ input.action_kind === "request_rework"
1414
+ ? await findingsDigestV2(
1415
+ (input.findings as Array<{ id: string; kind: string; acceptance_id: string | null; summary: string }>).map((f) => ({
1416
+ id: f.id,
1417
+ kind: f.kind,
1418
+ acceptance_id: f.acceptance_id,
1419
+ summary: f.summary,
1420
+ })),
1421
+ )
1422
+ : null,
1423
+ };
1424
+ return registry.issue(binding);
1425
+ }
1426
+
1427
+ let authorityPairPromise: Promise<{ registry: MutationAuthorityRegistry; app: CanaryApplication }> | null = null;
1428
+ function authorityPair(): Promise<{ registry: MutationAuthorityRegistry; app: CanaryApplication }> {
1429
+ if (!authorityPairPromise) {
1430
+ authorityPairPromise = (async () => {
1431
+ const registry = await createMutationAuthorityRegistry();
1432
+ const app = await createCanaryApplication(registry);
1433
+ return { registry, app };
1434
+ })();
1435
+ }
1436
+ return authorityPairPromise;
1437
+ }
1438
+
1439
+ async function executeOrdinaryOperation(
1440
+ ctx: ExtensionContext,
1441
+ input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown } },
1442
+ ): Promise<unknown> {
1443
+ const { app } = await authorityPair();
1444
+ const operation = input.operation.op === "revise_intent"
1445
+ ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
1446
+ : input.operation;
1447
+ const priorIntent = await readTaskIntent(ctx.cwd, input.taskId);
1448
+ const sidecar = join(ctx.cwd, priorIntent.intent_ref.path);
1449
+ const priorBytes = operation.op === "revise_intent" ? readFileSync(sidecar) : null;
1450
+ try {
1451
+ if (priorBytes) writeFileSync(sidecar, `${JSON.stringify(operation.next_intent, null, 2)}\n`);
1452
+ const result = await app.execute({
1453
+ root: ctx.cwd,
1454
+ task_id: input.taskId,
1455
+ operation: operation as never,
1456
+ prior_intent_token: priorIntent.token,
1457
+ diffProvider: (root: string, intent: { scope_hint: unknown }) => diffHashOf(root, intent),
1458
+ now: new Date().toISOString(),
1459
+ });
1460
+ if (operation.op === "freeze_artifacts" || operation.op === "stop")
1461
+ stagePlanningArtifactTransition(ctx.cwd, result.record);
1462
+ return result;
1463
+ } catch (error) {
1464
+ if (priorBytes) {
1465
+ const current = await readTaskRecord(ctx.cwd, input.taskId);
1466
+ if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) writeFileSync(sidecar, priorBytes);
1467
+ }
1468
+ throw error;
1469
+ }
1470
+ }
1471
+
1472
+ type AssuranceTaskState = AssuranceProjectionResult["projection"] | { error: string };
1473
+
1474
+ async function enrichAssuranceResult(
1475
+ ctx: ExtensionContext,
1476
+ taskId: string,
1477
+ result: Record<string, unknown>,
1478
+ ): Promise<Record<string, unknown>> {
1479
+ const projection = await projectAssuranceState(ctx.cwd, taskId);
1480
+ const taskState: AssuranceTaskState = projection.error
1481
+ ? { error: projection.error }
1482
+ : projection.projection;
1483
+ let tracker: Awaited<ReturnType<typeof markGithubTaskTerminal>> | undefined;
1484
+ if (!projection.error) {
1485
+ try {
1486
+ const terminalInput = deriveGithubTerminalProjectionInput(
1487
+ taskId,
1488
+ projection,
1489
+ await readTaskTombstone(ctx.cwd, taskId),
1490
+ );
1491
+ if (terminalInput) tracker = await markGithubTaskTerminal(ctx.cwd, terminalInput);
1492
+ } catch {
1493
+ tracker = {
1494
+ contract: "immune_brain/github_issue_tracker_result/v1",
1495
+ operation: "mark-terminal",
1496
+ status: "retryable_failure",
1497
+ association_found: false,
1498
+ message: "tracker observation failed after authoritative settlement",
1499
+ };
1500
+ }
1501
+ }
1502
+ return {
1503
+ ...result,
1504
+ task_state: taskState,
1505
+ ...(tracker ? { tracker } : {}),
1506
+ next_action: nextActionForAssuranceResult(result, taskState),
1507
+ };
1508
+ }
1509
+
1510
+ function nextActionForAssuranceResult(result: Record<string, unknown>, taskState: AssuranceTaskState): string {
1511
+ if ("error" in taskState) return "inspect authority state";
1512
+ if (taskState.lifecycle === "done" || taskState.lifecycle === "stopped") return "none";
1513
+ switch (result.state) {
1514
+ case "review_ready": return "invoke the reserved foreground Agent";
1515
+ case "awaiting_user": return "request_authorization";
1516
+ case "applied": return taskState.completion_ready ? "complete task" : taskState.next_obligation;
1517
+ case "completed":
1518
+ case "stopped": return "none";
1519
+ case "rework": return "repair findings, then advance assurance";
1520
+ case "cancelled": return "retry the interrupted foreground operation";
1521
+ case "settlement_unknown": return "inspect authority state";
1522
+ case "blocked":
1523
+ case "failed":
1524
+ default: return taskState.next_obligation;
1525
+ }
1526
+ }
1527
+
1528
+ function toolResult(text: string, details?: Record<string, unknown>) {
1529
+ return { content: [{ type: "text" as const, text }], details };
1530
+ }
1531
+
1532
+ function stagePlanningArtifactTransition(root: string, record: {
1533
+ intent_ref: { path: string };
1534
+ intent_snapshot: { scope_hint: string[] };
1535
+ }): void {
1536
+ const intentActive = record.intent_ref.path.replace("docs/plans/archive/", "docs/plans/");
1537
+ const intentArchive = intentActive.replace("docs/plans/", "docs/plans/archive/");
1538
+ const specActive = record.intent_snapshot.scope_hint.find((path) =>
1539
+ /^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path)
1540
+ && record.intent_snapshot.scope_hint.includes(path.replace("docs/specs/", "docs/specs/archive/")),
1541
+ );
1542
+ const candidates = [
1543
+ intentActive,
1544
+ intentArchive,
1545
+ ...(specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []),
1546
+ ];
1547
+ const paths = candidates.filter((path) => existsSync(join(root, path)) || execFileSync(
1548
+ "git",
1549
+ ["ls-files", "--cached", "--", path],
1550
+ { cwd: root, encoding: "utf8" },
1551
+ ).trim().length > 0);
1552
+ if (paths.length === 0) return;
1553
+ execFileSync("git", ["add", "--", ...paths], {
1554
+ cwd: root,
1555
+ stdio: ["ignore", "pipe", "pipe"],
1556
+ });
1557
+ }
1558
+
1559
+ function failCanaryTool(
1560
+ taskId: string,
1561
+ operation: string,
1562
+ state: "blocked" | "failed" | "authority_conflict" | "settlement_unknown",
1563
+ code: string,
1564
+ message: string,
1565
+ nextAction: string,
1566
+ ): never {
1567
+ return throwToolFailure({
1568
+ tool: "imm_kernel_canary",
1569
+ task_id: taskId,
1570
+ operation,
1571
+ state,
1572
+ code,
1573
+ message,
1574
+ next_action: nextAction,
1575
+ });
1576
+ }
1577
+
1578
+ function throwIfCanaryToolFailure(
1579
+ taskId: string,
1580
+ operation: string,
1581
+ result: Record<string, unknown>,
1582
+ ): void {
1583
+ if (!isToolFailureState(result.state)) return;
1584
+ failCanaryTool(
1585
+ taskId,
1586
+ operation,
1587
+ result.state,
1588
+ `assurance_${result.state}`,
1589
+ typeof result.reason === "string"
1590
+ ? result.reason
1591
+ : typeof result.result === "string"
1592
+ ? result.result
1593
+ : "Kernel assurance operation failed",
1594
+ typeof result.next_action === "string"
1595
+ ? result.next_action
1596
+ : "inspect authority state",
1597
+ );
1598
+ }
1599
+
1600
+ // Re-exported pure lifecycle helpers and types (single source of truth in the
1601
+ // progression module; the extension keeps its historical export surface).
1602
+ export {
1603
+ AssuranceProgression,
1604
+ buildReviewPrompt,
1605
+ classifyReviewWorkload,
1606
+ deriveQaJobTimeoutMs,
1607
+ parseAssuranceVerdict,
1608
+ snapshotDigest,
1609
+ QA_JOB_TIMEOUT_SECONDS,
1610
+ REVIEW_DISPATCH_TIMEOUT_MS,
1611
+ REVIEW_PREPARATION_TIMEOUT_MS,
1612
+ REVIEW_TIMING_PROFILES,
1613
+ REVIEW_VERDICT_VALIDATION_TIMEOUT_MS,
1614
+ };
1615
+ export type {
1616
+ AssuranceAdvanceResult,
1617
+ AssuranceProgressionPorts,
1618
+ AssuranceSubmitReviewResult,
1619
+ AssuranceVerdict,
1620
+ QaVerificationProgress,
1621
+ SnapshotDescriptor,
1622
+ } from "./pi-canary-assurance-progression";