immune-brain 3.3.0 → 3.5.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.
@@ -103,7 +103,7 @@ Runtime helpers: `imm_core.buildBrainstormEnsembleRequest`, `imm_core.buildBrain
103
103
 
104
104
  A Brainstorm ensemble is optional advisory-only framing input, not a vote and not a child-owned decision. The default roles are clarify scope, divergent options, minimal solution, and risk review.
105
105
 
106
- All Brainstorm ensemble children are advisory-only with `tool_policy: no tools`; they do not edit code, write Specs, write Plans, mutate workflow state, or close QA. The parent `imm-brainstorm` owns final framing synthesis, Brainstorm manifest IDs, and decision-delta confirmation. Final Spec and Plan authority stays with `imm-planner`. Routine Managed enrollment uses the Planner's final `ctx.ui.custom` confirmation bound to the TaskIntent content hash as the single authority gate; Enrollment validates descriptor structure without executing acceptance descriptors, deterministic QA executes them after implementation, and the routine task proceeds without a second human stop.
106
+ All Brainstorm ensemble children are advisory-only with `tool_policy: no tools`; they do not edit code, write Specs, write Plans, mutate workflow state, or close QA. The parent `imm-brainstorm` owns final framing synthesis, Brainstorm manifest IDs, and decision-delta confirmation. Final Spec and Plan authority stays with `imm-planner`. Routine Managed enrollment uses the current Host's native confirmation bound to the TaskIntent revision, content hash, and preparation digest as the single authority gate; Enrollment validates descriptor structure without executing acceptance descriptors, deterministic QA executes them after implementation, and the routine task proceeds without a second human stop. A later authority-gate failure stays fail-closed in the current Host and reports exactly one same-Host recovery action; Brainstorm must never recommend another Host, worktree, or unmanaged implementation as a fallback.
107
107
 
108
108
  Pi's adapter may consume `brainstorm_ensemble` dispatch JSON to prepare advisory Pi subagent envelopes, but envelope construction is not child execution and does not transfer framing authority. Pi launches one foreground Agent at a time, consumes its direct result, and re-evaluates the remaining dispatch budget before launching another candidate. Runtime does not call any agent, poll or recover background work, mutate state, or own final Spec/Plan authority. Pi subagent children remain no-tools advisory candidates; the parent `imm-brainstorm` collects outputs before synthesis.
109
109
 
@@ -36,7 +36,7 @@ before any workflow mutation.
36
36
 
37
37
  ## Core Responsibilities
38
38
 
39
- - **Main-context completion loop**: Drive the enrolled Kernel task in the current Pi conversation until completion or a safe stop.
39
+ - **Main-context completion loop**: Drive the enrolled Kernel task in the current Host conversation until completion or a safe stop.
40
40
  - **Context-preserving execution**: Call `imm_loop_action` with `op: route`, then follow the returned `executor` context in the current Parent conversation. Implement only the active Step or pending same-boundary `follow_up`, then record structured execution evidence through the Loop runtime action. A bounded test failure uses the returned internal `test-fixer` dispatch with its explicit delegated test-file list; PR feedback or CI repair uses the returned internal `pr-fix` dispatch inside the current Plan boundary.
41
41
  - **Independent authority isolation**: Use the host `Agent` subagent primitive for `awaiting_qa_decision` and for the exact runtime-reported review gate. Standard Plan Steps close from accepted passing evidence before an internal QA boundary exists; Strict Steps and all follow-ups retain isolated QA. The parent records accepted child decisions through the Loop runtime action.
42
42
  - **Observable progress**: Update only at major phase changes: Step start, execution evidence recorded, QA/review result, or terminal stop. Always emit a terminal summary.
@@ -67,6 +67,7 @@ Use Pi native `Agent` subagents. Do not spawn Pi child processes or invoke a sep
67
67
  - The parent may implement but must not issue its own QA or review pass.
68
68
  - QA and reviewer children must not edit files, write Plans, mutate Kernel state, or close decisions directly.
69
69
  - Missing `Agent` support, failed or malformed child output, stale child target, runtime write failure, invalid projection, missing credentials, unclear verification, repeated unchanged failure, or user cancellation stops fail-closed with an explicit reason and no decision write.
70
+ - A Managed native authority failure reports its stable reason and exactly one same-Host recovery action. Never recommend another Host, worktree, Direct Path, unmanaged implementation, or automatic retry as a fallback.
70
71
  - `replan_needed` stops at `imm-planner`; do not widen scope or rewrite the active Plan. A replacement must use a new sequential Plan path after the current Plan reaches `completed`, or after a literal user explicitly marks it `cancelled` or `superseded`.
71
72
  - Plans never suspend, resume, queue, or execute in parallel. Do not insert a repair Plan ahead of the current Plan.
72
73
  - Same-boundary review `follow_up` repeats execution, independent QA, and the originating review gate.
@@ -139,12 +140,12 @@ Default user-facing shape: checkpoint progress lines, then `Conclusion -> Eviden
139
140
  ## Kernel Canary Routing
140
141
 
141
142
  When the Kernel projection reports an active/draining backend claim, keep
142
- `imm-loop` as the user-facing entry and call the `imm_kernel_canary` Tool for
143
- that owned task. Enrollment uses the `imm_canary_enrollment` Tool and Review
144
- authorization remains a native TUI gate. When the projection calls for
145
- `request_authorization` or `approve_breaking_intent_revision`, invoke the exact
146
- Tool operation directly without asking the user for chat pre-confirmation; the
147
- native host interaction is the single authority decision. Invoke
143
+ `imm-loop` as the user-facing entry and call the current Host's Kernel integration
144
+ for that owned task. Enrollment and Review authorization use the current Host's
145
+ native gates. When the projection calls for `request_authorization` or
146
+ `approve_breaking_intent_revision`, invoke the exact Tool operation directly
147
+ without asking the user for chat pre-confirmation; the native Host interaction is
148
+ the single authority decision. Invoke
148
149
  `repair_authority_state` directly for a proven stale claim; Kernel revalidation
149
150
  removes only the redundant claim without user interaction. Do not invoke the
150
151
  removed `imm-canary-work` Skill as
@@ -22,9 +22,12 @@ planning artifacts. A later literal-user request to start Enrollment is a
22
22
  non-authoritative execution trigger: invoke the native Enrollment gate directly,
23
23
  without asking for chat pre-confirmation. For a clear mutation request that
24
24
  already includes execution, invoke that gate as soon as the candidate is
25
- validated and Git-tracked. Literal-user confirmation in the native gate remains
26
- the authority boundary. Fast-Track may compress the same phases but cannot
27
- bypass that boundary, QA, Review, authorization, or completion.
25
+ validated and Git-tracked. Literal-user confirmation in the current Host's
26
+ native gate remains the authority boundary. A gate failure preserves candidate
27
+ artifacts and reports its reason plus exactly one same-Host recovery action; it
28
+ must never recommend another Host, worktree, Direct Path, unmanaged
29
+ implementation, or automatic retry. Fast-Track may compress the same phases but
30
+ cannot bypass that boundary, QA, Review, authorization, or completion.
28
31
 
29
32
  ## Clarification supplement
30
33
 
@@ -73,7 +76,7 @@ Then route deterministically:
73
76
  - an active or otherwise nonterminal v3 Plan remains on its existing v3 route;
74
77
  - no routing policy preserves the legacy v3 Planner behavior;
75
78
  - a valid `kernel_task_intent` retirement policy produces one TaskIntent draft
76
- through Pi `imm-planner`;
79
+ through the current Host's explicit `imm-planner`;
77
80
  - an invalid, unreadable, untracked, or tracked-deleted policy rejects new
78
81
  planning authority with `routing_policy_invalid`;
79
82
  - no Planner path enrolls a task or falls back to v3 after retirement.
@@ -90,17 +93,18 @@ The routing projection selects the planning route; explicit
90
93
  of that Plan artifact. A valid Plan never proves Managed authority. Under an
91
94
  active `kernel_task_intent` policy, authority still requires a Git-tracked
92
95
  TaskIntent whose `imm-kernel intent validate <path> --json` projection is
93
- `valid: true` and `enrollment_ready: true`, followed by Pi TUI enrollment.
94
-
95
- Pi host identity is implicit and never a planning input. The production boundary
96
- that turns a Git-tracked TaskIntent draft into managed execution authority is the
97
- native host TUI: the Planner's final `ctx.ui.custom` gate (via the
98
- `imm_canary_enrollment` foreground Tool) provides one literal-user confirmation
99
- bound to the TaskIntent content hash. Invoke the Tool directly when the route is
100
- ready; do not ask for a chat pre-confirmation. Enrollment validates the intent,
101
- Git ownership, scope, workspace claim, and final authority preconditions without
102
- executing acceptance descriptors. A routine task proceeds from that single
103
- confirmation through enrollment, execution and QA without a second human stop.
96
+ `valid: true` and `enrollment_ready: true`, followed by current-Host native
97
+ Enrollment.
98
+
99
+ Host identity is implicit and never a planning input. The production boundary
100
+ that turns a Git-tracked TaskIntent draft into managed execution authority is
101
+ the current Host's native Enrollment gate. Invoke that Host integration
102
+ directly when the route is ready; do not ask for chat pre-confirmation. The
103
+ single literal-user decision is bound to the TaskIntent revision, content hash,
104
+ and preparation digest. Enrollment validates the intent, Git ownership, scope,
105
+ workspace claim, and final authority preconditions without executing acceptance
106
+ descriptors. A routine task proceeds from that single confirmation through
107
+ enrollment, execution and QA without a second human stop.
104
108
 
105
109
  Before authoring a TaskIntent, trace each expected behavior from its public or
106
110
  runtime entry point through existing imports and callers to the highest focused
@@ -387,10 +391,11 @@ Iteration plan under `docs/plans/` and spec under `docs/specs/`. Includes: `Summ
387
391
  - When a project explicitly expects Chinese document prose, `imm-plan <plan-path> --json` includes an `output_language` warning if target Plan or referenced Spec prose appears mostly English.
388
392
  - Spec references align with steps: each step’s `Verification` is copy-paste-checkable against repo commands or files.
389
393
  - For brainstorm-origin Plans with a manifest, `imm-plan <plan-path> --json` reports `origin_coverage` totals with no `unmapped_items` and no reason-required trace rows without reasons.
390
- - Managed execution handoff is Git-tracked TaskIntent author/validate plus Pi TUI enrollment. Do not sync a v3 State Ledger or invoke a missing dispatcher.
394
+ - Managed execution handoff is Git-tracked TaskIntent author/validate plus current-Host native Enrollment. Do not sync a v3 State Ledger or invoke a missing dispatcher.
391
395
 
392
396
  ## Next Action
393
397
 
394
398
  - Gate: Reference closure and the clarification supplement are complete; every upstream `BR-*` item is represented; no unresolved user-owned decision remains; any Planner-introduced decision delta is confirmed; the Plan passes `imm-plan --json` validation; and no step has a hypothetical-only verification path.
395
- - If gates pass: for Kernel-managed work, invoke the `imm_canary_enrollment` Tool directly without chat pre-confirmation. Its native `ctx.ui.custom` gate provides the single literal-user confirmation bound to the TaskIntent content hash, validates Enrollment preconditions without executing acceptance descriptors, and enrolls the task to continue through `imm-loop`.
399
+ - If gates pass: for Kernel-managed work, invoke the current Host's native Enrollment Tool directly without chat pre-confirmation. Its single literal-user gate binds the TaskIntent revision, content hash, and preparation digest, validates Enrollment preconditions without executing acceptance descriptors, and enrolls the task to continue through `imm-loop`.
400
+ - If the native gate fails: preserve candidate artifacts and report the stable reason plus exactly one same-Host recovery action. Do not suggest another Host, worktree, Direct Path, unmanaged implementation, or automatic retry.
396
401
  - If gates are not met: state which validation failures, unresolved verification paths, or material decision deltas remain; do not name a next skill.
@@ -31,16 +31,6 @@
31
31
  ]
32
32
  }
33
33
  ],
34
- "ElicitationResult": [
35
- {
36
- "hooks": [
37
- {
38
- "type": "command",
39
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/claude/mcp-server.mjs\" --hook"
40
- }
41
- ]
42
- }
43
- ],
44
34
  "SessionEnd": [
45
35
  {
46
36
  "hooks": [
@@ -559,7 +559,13 @@ export class AssuranceCoordinator {
559
559
  ensureOperationLive();
560
560
  qaVerdict = await this.ports.runQa(assurance.snapshot, assurance.descriptors, runner, {
561
561
  signal: operationController.signal,
562
- onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, { current: item.index, total: item.total, acceptance_id: item.acceptance_id }),
562
+ onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
563
+ current: item.index,
564
+ total: item.total,
565
+ acceptance_id: item.acceptance_id,
566
+ acceptance_phase: item.phase,
567
+ elapsed_ms: item.elapsed_ms,
568
+ }),
563
569
  });
564
570
  ensureOperationLive();
565
571
  const invocation = this.openInvocation(taskId);
@@ -1,4 +1,4 @@
1
- export const MIN_CLAUDE_CODE_VERSION = "2.1.199";
1
+ export const MIN_CLAUDE_CODE_VERSION = "2.1.236";
2
2
  export const HOST_ID = "claude-code" as const;
3
3
  export const CORE_CONTRACT = "assurance_kernel/host_adapter/claude-code/v1";
4
4
  export const SUPPORTED_PLATFORMS = ["darwin", "linux"] as const;
@@ -6,12 +6,12 @@ export const SUPPORTED_PLATFORMS = ["darwin", "linux"] as const;
6
6
  export type PermissionMode = "manual" | "acceptEdits" | "auto" | "bypassPermissions" | "dontAsk";
7
7
 
8
8
  export type HostProbe =
9
- | { ok: true; version: string; permissionMode: PermissionMode; platform: string }
9
+ | { ok: true; version: string; platform: string }
10
10
  | { ok: false; reason: string };
11
11
 
12
12
  function parseSemver(value: string): [number, number, number] | null {
13
13
  // Prerelease and build suffixes are rejected: an unstable build such as
14
- // 2.1.199-alpha never satisfies the stable minimum.
14
+ // Prerelease builds never satisfy the stable minimum.
15
15
  const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(value.trim());
16
16
  if (!match) return null;
17
17
  return [Number(match[1]), Number(match[2]), Number(match[3])];
@@ -57,11 +57,5 @@ export function probeHost(
57
57
  if (platform !== "darwin" && platform !== "linux") {
58
58
  return { ok: false, reason: `unsupported platform ${platform}; native Windows is out of scope` };
59
59
  }
60
- const rawPermissionMode = env.CLAUDE_CODE_PERMISSION_MODE;
61
- if (rawPermissionMode !== undefined && rawPermissionMode !== "") {
62
- const permissionMode = parsePermissionMode(rawPermissionMode);
63
- if (!permissionMode) return { ok: false, reason: `unsupported permission mode ${rawPermissionMode}` };
64
- return { ok: true, version, permissionMode, platform };
65
- }
66
- return { ok: true, version, permissionMode: "manual", platform };
60
+ return { ok: true, version, platform };
67
61
  }
@@ -1,5 +1,4 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { parsePermissionMode, type PermissionMode } from "./capability";
3
2
 
4
3
  export const PRIVILEGED_OPERATIONS = [
5
4
  "enroll",
@@ -9,49 +8,84 @@ export const PRIVILEGED_OPERATIONS = [
9
8
  ] as const;
10
9
 
11
10
  export type PrivilegedOperation = (typeof PRIVILEGED_OPERATIONS)[number];
12
- export type NativeDecision = "accept" | "deny" | "cancel";
11
+ export type NativeDecision = "accept" | "decline" | "cancel";
12
+ export type NativeFailureCode =
13
+ | "interaction_not_opened"
14
+ | "user_denied"
15
+ | "user_cancelled"
16
+ | "correlation_missing"
17
+ | "unsupported_host"
18
+ | "workspace_changed";
19
+
20
+ const RECOVERY_ACTIONS: Record<NativeFailureCode, string> = {
21
+ interaction_not_opened: "retry through a fresh native gate in the current Host",
22
+ user_denied: "wait for a fresh literal-user request",
23
+ user_cancelled: "wait for a fresh literal-user request",
24
+ correlation_missing: "retry through a fresh native gate in the current Host",
25
+ unsupported_host: "upgrade to a supported Claude Code version and retry in the current Host",
26
+ workspace_changed: "review the current workspace and retry through a fresh native gate",
27
+ };
28
+
29
+ export class NativeAuthorityError extends Error {
30
+ constructor(
31
+ readonly reasonCode: NativeFailureCode,
32
+ detail: string,
33
+ readonly recoveryAction = RECOVERY_ACTIONS[reasonCode],
34
+ ) {
35
+ super(`${reasonCode}: ${detail}; recovery: ${recoveryAction}`);
36
+ this.name = "NativeAuthorityError";
37
+ }
38
+ }
13
39
 
14
40
  export function isPrivilegedOperation(operation: string): operation is PrivilegedOperation {
15
41
  return (PRIVILEGED_OPERATIONS as readonly string[]).includes(operation);
16
42
  }
17
43
 
18
44
  export function privilegedAnnotations(): Record<string, unknown> {
19
- return {
20
- destructiveHint: true,
21
- "anthropic/requiresUserInteraction": true,
22
- };
45
+ return { destructiveHint: true };
23
46
  }
24
47
 
25
48
  export interface NativeGateInput {
26
49
  operation: string;
27
- permissionMode: PermissionMode | string;
28
- requiresUserInteraction: boolean;
29
50
  interactive: boolean;
30
51
  decision?: NativeDecision;
31
52
  }
32
53
 
33
54
  export type NativeGateResult =
34
55
  | { ok: true }
35
- | { ok: false; reason: string };
56
+ | { ok: false; error: NativeAuthorityError };
36
57
 
37
58
  export function evaluateNativeGate(input: NativeGateInput): NativeGateResult {
38
59
  if (!isPrivilegedOperation(input.operation)) return { ok: true };
39
- if (!input.interactive) return { ok: false, reason: "non-interactive execution cannot mint authority" };
40
- const mode = parsePermissionMode(input.permissionMode);
41
- if (!mode) return { ok: false, reason: `unsupported permission mode ${String(input.permissionMode)}` };
42
- if (mode === "dontAsk") return { ok: false, reason: "dontAsk cannot mint authority" };
43
- if (!input.requiresUserInteraction) {
44
- return { ok: false, reason: "privileged operation requires anthropic/requiresUserInteraction" };
45
- }
46
- if (input.decision === "deny") return { ok: false, reason: "native interaction denied" };
47
- if (input.decision === "cancel") return { ok: false, reason: "native interaction cancelled" };
48
- if (input.decision !== "accept") return { ok: false, reason: "native interaction missing" };
60
+ if (!input.interactive) return { ok: false, error: new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable") };
61
+ if (input.decision === "decline") return { ok: false, error: new NativeAuthorityError("user_denied", "native interaction declined") };
62
+ if (input.decision === "cancel") return { ok: false, error: new NativeAuthorityError("user_cancelled", "native interaction cancelled") };
63
+ if (input.decision !== "accept") return { ok: false, error: new NativeAuthorityError("interaction_not_opened", "native interaction returned no decision") };
49
64
  return { ok: true };
50
65
  }
51
66
 
67
+ export interface NativeConfirmationInput {
68
+ operation: PrivilegedOperation;
69
+ taskId: string;
70
+ toolCallId: string;
71
+ risk?: string;
72
+ intentRevision?: number;
73
+ intentContentHash?: string;
74
+ bindingDigest?: string;
75
+ signal?: AbortSignal;
76
+ }
77
+
78
+ export interface NativeConfirmationResult {
79
+ decision: NativeDecision;
80
+ requestId: string;
81
+ }
82
+
83
+ export type NativeConfirmationPort = (input: NativeConfirmationInput) => Promise<NativeConfirmationResult>;
84
+
52
85
  export function confirmationRef(input: {
53
- sessionId: string;
86
+ connectionId: string;
54
87
  toolCallId: string;
88
+ requestId: string;
55
89
  operation: string;
56
90
  taskId: string;
57
91
  intentRevision?: number;
@@ -59,7 +93,7 @@ export function confirmationRef(input: {
59
93
  bindingDigest?: string;
60
94
  }): string {
61
95
  return `claude-confirm-${createHash("sha256")
62
- .update(`${input.sessionId}\0${input.toolCallId}\0${input.operation}\0${input.taskId}\0${input.intentRevision ?? ""}\0${input.intentContentHash ?? ""}\0${input.bindingDigest ?? ""}`)
96
+ .update(`${input.connectionId}\0${input.toolCallId}\0${input.requestId}\0${input.operation}\0${input.taskId}\0${input.intentRevision ?? ""}\0${input.intentContentHash ?? ""}\0${input.bindingDigest ?? ""}`)
63
97
  .digest("hex")
64
98
  .slice(0, 16)}`;
65
99
  }
@@ -47,7 +47,14 @@ import { reconcileKernelAuthority, repairKernelAuthority } from "../kernel/stora
47
47
  import { preparePiCanary, revalidatePiCanary } from "../kernel/pi_canary_prepare";
48
48
  import { qaFindingId } from "../assurance/qa_findings";
49
49
  import { taskDiffIdentity, taskRevisionIdentity } from "../workspace_scope";
50
- import { confirmationRef, enrollmentNonce, evaluateNativeGate, isPrivilegedOperation, type NativeDecision } from "./interaction";
50
+ import {
51
+ confirmationRef,
52
+ enrollmentNonce,
53
+ evaluateNativeGate,
54
+ isPrivilegedOperation,
55
+ NativeAuthorityError,
56
+ type NativeConfirmationPort,
57
+ } from "./interaction";
51
58
  import { ClaudeReviewHost, FileHookEventLog, type ClaudeHookEvent } from "./review_host";
52
59
  import { probeHost, type PermissionMode } from "./capability";
53
60
 
@@ -311,6 +318,10 @@ async function mintCapability(
311
318
  return registry.issue(binding);
312
319
  }
313
320
 
321
+ function throwIfCancelled(signal?: AbortSignal): void {
322
+ if (signal?.aborted) throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
323
+ }
324
+
314
325
  export interface ClaudeRuntimeOptions {
315
326
  cwd: string;
316
327
  env?: Record<string, string | undefined>;
@@ -318,7 +329,7 @@ export interface ClaudeRuntimeOptions {
318
329
  ports?: AssuranceCoordinatorPorts;
319
330
  interactive?: boolean;
320
331
  permissionMode?: PermissionMode;
321
- decisions?: Map<string, NativeDecision>;
332
+ requestConfirmation?: NativeConfirmationPort;
322
333
  }
323
334
 
324
335
  export class ClaudeRuntime {
@@ -327,8 +338,7 @@ export class ClaudeRuntime {
327
338
  private readonly cwd: string;
328
339
  private readonly env: Record<string, string | undefined>;
329
340
  private readonly interactive: boolean;
330
- private readonly permissionMode: PermissionMode;
331
- private readonly decisions: Map<string, NativeDecision>;
341
+ private requestConfirmation?: NativeConfirmationPort;
332
342
  private hostVersion: string | undefined;
333
343
  private mutationRegistry: MutationAuthorityRegistry | null = null;
334
344
  private enrollmentRegistry = createEnrollmentAuthorityRegistry();
@@ -338,8 +348,7 @@ export class ClaudeRuntime {
338
348
  this.cwd = options.cwd;
339
349
  this.env = options.env ?? process.env;
340
350
  this.interactive = options.interactive ?? true;
341
- this.permissionMode = options.permissionMode ?? "manual";
342
- this.decisions = options.decisions ?? new Map();
351
+ this.requestConfirmation = options.requestConfirmation;
343
352
  this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog());
344
353
  if (options.ports) {
345
354
  this.coordinator = new AssuranceCoordinator({ ...options.ports, host: this.host });
@@ -357,6 +366,10 @@ export class ClaudeRuntime {
357
366
  this.hostVersion = version || undefined;
358
367
  }
359
368
 
369
+ bindNativeConfirmation(port: NativeConfirmationPort): void {
370
+ this.requestConfirmation = port;
371
+ }
372
+
360
373
  async shutdown(): Promise<void> {
361
374
  await this.coordinator.onSessionShutdown();
362
375
  }
@@ -393,45 +406,28 @@ export class ClaudeRuntime {
393
406
  return { registry: this.mutationRegistry, app: this.app };
394
407
  }
395
408
 
396
- private rejectBeforePreparation(operation: string, meta: ToolMeta): void {
397
- // Zero-mutation fast path only: a configured deny/cancel rejects before
398
- // any workspace preparation. The live accept elicitation is requested
399
- // and consumed only after preparation computes the binding digests, so
400
- // native confirmation is always bound to hashes computed before it.
401
- const configured = this.decisions.get(operation) ?? meta.decision;
402
- if (configured === "deny" || configured === "cancel") this.gate(operation, meta);
403
- }
404
-
405
- private gate(operation: string, meta: ToolMeta, binding: {
409
+ private async gate(operation: string, meta: ToolMeta, binding: {
410
+ risk?: string;
406
411
  intentRevision?: number;
407
412
  intentContentHash?: string;
408
413
  bindingDigest?: string;
409
- } = {}): { confirmation_ref: string } {
414
+ } = {}): Promise<{ confirmation_ref: string }> {
415
+ throwIfCancelled(meta.signal);
410
416
  const probe = probeHost(this.env, process.platform, this.hostVersion);
411
- if (!probe.ok) throw new Error(probe.reason);
412
- const requiresUserInteraction = Boolean(meta.requiresUserInteraction);
413
- const permissionMode = meta.permissionMode ?? this.permissionMode;
414
- const configuredDecision = this.decisions.get(operation) ?? meta.decision;
415
- const decision = configuredDecision ?? (
416
- isPrivilegedOperation(operation)
417
- && Boolean(meta.interactive ?? this.interactive)
418
- && requiresUserInteraction
419
- && permissionMode !== "dontAsk"
420
- ? this.host.takeConfirmation(meta.sessionId, meta.toolCallId)
421
- : undefined
422
- );
423
- const gate = evaluateNativeGate({
424
- operation,
425
- permissionMode,
426
- requiresUserInteraction,
427
- interactive: meta.interactive ?? this.interactive,
428
- decision,
429
- });
430
- if (!gate.ok) throw new Error(gate.reason);
417
+ if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
418
+ const interactive = meta.interactive ?? this.interactive;
419
+ if (!interactive) throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
420
+ if (!isPrivilegedOperation(operation)) throw new Error(`unsupported native operation ${operation}`);
421
+ if (!this.requestConfirmation) throw new NativeAuthorityError("interaction_not_opened", "native confirmation port is unavailable");
422
+ const result = await this.requestConfirmation({ operation, taskId: meta.taskId, toolCallId: meta.toolCallId, signal: meta.signal, ...binding });
423
+ throwIfCancelled(meta.signal);
424
+ const gate = evaluateNativeGate({ operation, interactive, decision: result.decision });
425
+ if (!gate.ok) throw gate.error;
431
426
  return {
432
427
  confirmation_ref: confirmationRef({
433
- sessionId: meta.sessionId,
428
+ connectionId: meta.sessionId,
434
429
  toolCallId: meta.toolCallId,
430
+ requestId: result.requestId,
435
431
  operation,
436
432
  taskId: meta.taskId,
437
433
  ...binding,
@@ -444,17 +440,19 @@ export class ClaudeRuntime {
444
440
  }
445
441
 
446
442
  async enroll(taskId: string, meta: ToolMeta) {
447
- this.rejectBeforePreparation("enroll", { ...meta, taskId });
448
443
  const now = new Date().toISOString();
449
444
  const preparation = await preparePiCanary(this.cwd, { task_id: taskId, now });
450
- const gate = this.gate("enroll", { ...meta, taskId }, {
445
+ const intent = await readTaskIntent(this.cwd, taskId);
446
+ const gate = await this.gate("enroll", { ...meta, taskId }, {
447
+ risk: intent.intent.risk,
451
448
  intentRevision: preparation.intent?.revision,
452
449
  intentContentHash: preparation.intent?.content_hash,
453
450
  bindingDigest: preparation.digest,
454
451
  });
455
452
  const { unchanged } = await revalidatePiCanary(this.cwd, { task_id: taskId, now }, preparation);
456
- if (!unchanged) throw new Error("Workspace changed after confirmation; enrollment aborted before authority");
453
+ if (!unchanged) throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
457
454
  if (!preparation.intent) throw new Error("enrollment requires a readable TaskIntent");
455
+ throwIfCancelled(meta.signal);
458
456
  const nonce = enrollmentNonce();
459
457
  const binding: EnrollmentCapabilityBinding = {
460
458
  task_id: taskId,
@@ -501,7 +499,6 @@ export class ClaudeRuntime {
501
499
  return repairKernelAuthority(this.cwd, taskId, authority.revision);
502
500
  }
503
501
  if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
504
- this.rejectBeforePreparation(operation, { ...meta, taskId });
505
502
  let op = operation;
506
503
  let decisionOp: { finding_id: string; resolution: string } | undefined;
507
504
  const projection = await this.status(taskId);
@@ -562,15 +559,22 @@ export class ClaudeRuntime {
562
559
  writeFileSync(sidecar, `${JSON.stringify(nextIntent, null, 2)}\n`);
563
560
  execFileSync("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
564
561
  const preparedRecord = await readTaskRecord(this.cwd, taskId);
565
- if (!preparedRecord.record) throw new Error("TaskRecord changed before the breaking revision digest");
562
+ if (!preparedRecord.record) {
563
+ throw new NativeAuthorityError("workspace_changed", "TaskRecord changed before the breaking revision digest");
564
+ }
566
565
  preparedDiffHash = diffHashOf(this.cwd, preparedRecord.record);
567
566
  }
568
567
  const preparedProjection = await this.status(taskId);
569
- assertProjectionBinding(projection, preparedProjection, Boolean(nextIntent));
570
- if (preparedProjection.projection.diff_hash !== preparedDiffHash) {
571
- throw new Error("Workspace changed while preparing the authority digest");
568
+ try {
569
+ assertProjectionBinding(projection, preparedProjection, Boolean(nextIntent));
570
+ if (preparedProjection.projection.diff_hash !== preparedDiffHash) {
571
+ throw new Error("workspace changed while preparing the authority digest");
572
+ }
573
+ } catch (error) {
574
+ throw new NativeAuthorityError("workspace_changed", error instanceof Error ? error.message : String(error));
572
575
  }
573
- gate = this.gate(operation, { ...meta, taskId }, {
576
+ gate = await this.gate(operation, { ...meta, taskId }, {
577
+ risk: projection.projection.risk,
574
578
  intentRevision: nextIntent?.revision ?? projection.projection.intent_revision,
575
579
  intentContentHash: nextIntentHash ?? projection.projection.intent_content_hash,
576
580
  bindingDigest: `${preparedDiffHash}:${nextIntentHash ?? ""}`,
@@ -588,11 +592,16 @@ export class ClaudeRuntime {
588
592
  const confirmation = gate.confirmation_ref;
589
593
  try {
590
594
  const capabilityProjection = await this.status(taskId);
591
- assertProjectionBinding(projection, capabilityProjection, Boolean(nextIntent));
595
+ try {
596
+ assertProjectionBinding(projection, capabilityProjection, Boolean(nextIntent));
597
+ } catch (error) {
598
+ throw new NativeAuthorityError("workspace_changed", error instanceof Error ? error.message : String(error));
599
+ }
592
600
  const operationDiffHash = capabilityProjection.projection.diff_hash;
593
601
  if (nextIntent && operationDiffHash !== preparedDiffHash) {
594
- throw new Error("Workspace changed after native confirmation; authority aborted before capability issuance");
602
+ throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
595
603
  }
604
+ throwIfCancelled(meta.signal);
596
605
  const capability = await mintCapability(registry, {
597
606
  authority_kind: "user",
598
607
  task_id: taskId,
@@ -608,6 +617,7 @@ export class ClaudeRuntime {
608
617
  ...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
609
618
  ...(op === "stop" ? { reason: extra.reason ?? "user stop" } : {}),
610
619
  });
620
+ throwIfCancelled(meta.signal);
611
621
  const result = app.execute({
612
622
  root: this.cwd,
613
623
  task_id: taskId,
@@ -765,6 +775,5 @@ export interface ToolMeta {
765
775
  requiresUserInteraction?: boolean;
766
776
  permissionMode?: PermissionMode;
767
777
  interactive?: boolean;
768
- decision?: NativeDecision;
769
778
  signal?: AbortSignal;
770
779
  }