immune-brain 3.2.2 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +3 -3
  3. package/package.json +5 -3
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +19 -73
  6. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +1 -2
  7. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +2 -2
  8. package/plugins/immune-brain/dist/BASELINE.md +1 -1
  9. package/plugins/immune-brain/dist/claude/mcp-server.mjs +317 -316
  10. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
  11. package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
  12. package/plugins/immune-brain/dist/imm-loop.md +15 -10
  13. package/plugins/immune-brain/dist/imm-planner.md +22 -17
  14. package/plugins/immune-brain/hooks/hooks.json +0 -10
  15. package/plugins/immune-brain/runtime/assurance/coordinator.ts +0 -8
  16. package/plugins/immune-brain/runtime/claude/capability.ts +4 -10
  17. package/plugins/immune-brain/runtime/claude/interaction.ts +55 -22
  18. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +63 -73
  19. package/plugins/immune-brain/runtime/claude/mcp_server.ts +233 -55
  20. package/plugins/immune-brain/runtime/claude/review_host.ts +2 -138
  21. package/plugins/immune-brain/runtime/kernel/application.ts +0 -1
  22. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +1 -7
  23. package/plugins/immune-brain/runtime/kernel/canary_application.ts +0 -7
  24. package/plugins/immune-brain/runtime/kernel/completion.ts +1 -3
  25. package/plugins/immune-brain/runtime/kernel/reducer.ts +0 -31
  26. package/plugins/immune-brain/runtime/kernel/types.ts +0 -2
  27. package/plugins/immune-brain/runtime/kernel/validation.ts +1 -3
  28. package/plugins/immune-brain/runtime/plugin_version.ts +2 -0
  29. package/plugins/immune-brain/skills/BASELINE.md +1 -1
  30. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -0
  31. package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -0
  32. package/plugins/immune-brain/skills/imm-planner/SKILL.md +6 -0
@@ -45,23 +45,17 @@ export type ClaudeHookEvent =
45
45
  taskId?: string;
46
46
  operationId?: string;
47
47
  }
48
- | { type: "SessionEnd"; sessionId: string }
49
- | { type: "ElicitationResult"; sessionId: string; toolCallId: string; decision: "accept" | "deny" | "cancel" }
50
- | { type: "ElicitationConsumed"; sessionId: string; toolCallId: string };
48
+ | { type: "SessionEnd"; sessionId: string };
51
49
 
52
50
  export interface HookEventLog {
53
51
  append(event: ClaudeHookEvent): boolean;
54
52
  list(sessionId?: string): ClaudeHookEvent[];
55
53
  sessions(): string[];
56
54
  clear(sessionId?: string): void;
57
- /** Persists consumed identity durably; true only after the record is verified. */
58
- consumeElicitation(sessionId: string, toolCallId: string): boolean;
59
- consumedKeys(): string[];
60
55
  }
61
56
 
62
57
  export class MemoryHookEventLog implements HookEventLog {
63
58
  private readonly events: ClaudeHookEvent[] = [];
64
- private readonly consumed: string[] = [];
65
59
  append(event: ClaudeHookEvent): boolean { this.events.push(event); return true; }
66
60
  list(sessionId?: string): ClaudeHookEvent[] {
67
61
  return sessionId ? this.events.filter((event) => event.sessionId === sessionId) : [...this.events];
@@ -78,17 +72,9 @@ export class MemoryHookEventLog implements HookEventLog {
78
72
  if (this.events[i].sessionId === sessionId) this.events.splice(i, 1);
79
73
  }
80
74
  }
81
- consumeElicitation(sessionId: string, toolCallId: string): boolean {
82
- this.consumed.push(`${sessionId}\0${toolCallId}`);
83
- this.events.push({ type: "ElicitationConsumed", sessionId, toolCallId });
84
- return true;
85
- }
86
- consumedKeys(): string[] { return [...this.consumed]; }
87
75
  }
88
76
 
89
77
  const CACHE_DIR = "immune-brain-claude";
90
- const CONSUMED_FILE = "consumed.jsonl";
91
- const CONSUMED_DIR = "consumed";
92
78
 
93
79
  function sessionHash(sessionId: string): string {
94
80
  return createHash("sha256").update(sessionId).digest("hex");
@@ -120,25 +106,6 @@ function ensurePrivateDir(dir: string): boolean {
120
106
  }
121
107
  }
122
108
 
123
- function claimAtomicKey(dir: string, keyName: string): boolean {
124
- const claimsDir = join(dir, CONSUMED_DIR);
125
- if (!ensurePrivateDir(claimsDir)) return false;
126
- const claimPath = join(claimsDir, `${keyName}.claim`);
127
- const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_NONBLOCK;
128
- let fd: number;
129
- try {
130
- fd = openSync(claimPath, flags, 0o600);
131
- } catch (error) {
132
- return false;
133
- }
134
- try {
135
- const stat = fstatSync(fd);
136
- if (!stat.isFile() || !ownedByUs(stat) || (stat.mode & 0o777) !== 0o600) return false;
137
- return true;
138
- } finally {
139
- closeSync(fd);
140
- }
141
- }
142
109
  function appendPrivate(path: string, dir: string, line: string): boolean {
143
110
  if (!ensurePrivateDir(dir)) return false;
144
111
  const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK;
@@ -185,7 +152,7 @@ export class FileHookEventLog implements HookEventLog {
185
152
  if (!ensurePrivateDir(dir)) return [];
186
153
  try {
187
154
  return readdirSync(dir)
188
- .filter((name) => name.endsWith(".jsonl") && name !== CONSUMED_FILE)
155
+ .filter((name) => name.endsWith(".jsonl"))
189
156
  .flatMap((name) => this.readFile(join(dir, name)));
190
157
  } catch {
191
158
  return [];
@@ -218,34 +185,6 @@ export class FileHookEventLog implements HookEventLog {
218
185
  rmSync(path, { force: true });
219
186
  } catch { /* cleanup cannot create authority */ }
220
187
  }
221
- consumeElicitation(sessionId: string, toolCallId: string): boolean {
222
- // The durable consumed identity is claimed atomically (O_EXCL per-key)
223
- // and committed to consumed.jsonl BEFORE any session evidence is touched:
224
- // concurrent consumers cannot double-claim the same identity.
225
- const dir = cacheDir(this.root);
226
- const claimKey = createHash("sha256").update(`${sessionId}\0${toolCallId}`).digest("hex");
227
- const keyClaimed = claimAtomicKey(dir, claimKey);
228
- if (!keyClaimed) return false;
229
- if (!appendPrivate(join(dir, CONSUMED_FILE), dir, `${JSON.stringify({ sessionId, toolCallId })}\n`)) {
230
- // Roll back the claim file if the durable record append failed
231
- try { rmSync(join(dir, CONSUMED_DIR, `${claimKey}.claim`), { force: true }); } catch { /* ignore */ }
232
- return false;
233
- }
234
- if (!this.consumedKeys().includes(`${sessionId}\0${toolCallId}`)) return false;
235
- return this.append({ type: "ElicitationConsumed", sessionId, toolCallId });
236
- }
237
- consumedKeys(): string[] {
238
- if (!ensurePrivateDir(cacheDir(this.root))) return [];
239
- const text = readPrivate(join(cacheDir(this.root), CONSUMED_FILE));
240
- if (!text) return [];
241
- try {
242
- return text.split("\n").filter(Boolean)
243
- .map((line) => JSON.parse(line) as { sessionId: string; toolCallId: string })
244
- .map((item) => `${item.sessionId}\0${item.toolCallId}`);
245
- } catch {
246
- return [];
247
- }
248
- }
249
188
  }
250
189
 
251
190
  interface PendingReview {
@@ -287,11 +226,8 @@ export class ClaudeReviewHost implements AssuranceHostPort {
287
226
  readonly host = "claude-code" as const;
288
227
  private readonly pending = new Map<string, PendingReview>();
289
228
  private readonly appliedBySession = new Map<string, number>();
290
- private readonly consumedElicitations = new Set<string>();
291
229
  constructor(private readonly log: HookEventLog = new MemoryHookEventLog()) {}
292
230
 
293
- private readonly confirmations = new Map<string, "accept" | "deny" | "cancel">();
294
-
295
231
  prepareReview(request: ReviewRequest): HostReviewReservation {
296
232
  const initialCursors = new Map<string, number>();
297
233
  const sessionCursors = new Map<string, number>();
@@ -322,7 +258,6 @@ export class ClaudeReviewHost implements AssuranceHostPort {
322
258
  }
323
259
 
324
260
  private drain(): void {
325
- for (const key of this.log.consumedKeys()) this.consumedElicitations.add(key);
326
261
  for (const sessionId of this.log.sessions()) {
327
262
  const events = this.log.list(sessionId);
328
263
  let start = this.appliedBySession.get(sessionId) ?? 0;
@@ -330,34 +265,10 @@ export class ClaudeReviewHost implements AssuranceHostPort {
330
265
  let ended = false;
331
266
  for (let i = start; i < events.length; i++) {
332
267
  const event = events[i];
333
- if (event.type === "ElicitationResult") {
334
- const key = `${event.sessionId}\0${event.toolCallId}`;
335
- if (this.consumedElicitations.has(key)) continue;
336
- if (this.confirmations.has(key)) {
337
- if (this.log.consumeElicitation(event.sessionId, event.toolCallId)) {
338
- this.confirmations.delete(key);
339
- this.consumedElicitations.add(key);
340
- }
341
- continue;
342
- }
343
- this.confirmations.set(key, event.decision);
344
- continue;
345
- }
346
- if (event.type === "ElicitationConsumed") {
347
- const key = `${event.sessionId}\0${event.toolCallId}`;
348
- this.consumedElicitations.add(key);
349
- this.confirmations.delete(key);
350
- continue;
351
- }
352
268
  if (event.type === "SessionEnd") {
353
269
  this.log.clear(event.sessionId);
354
270
  ended = true;
355
271
  this.appliedBySession.delete(event.sessionId);
356
- // A confirmation cached before the session ended must never
357
- // authorize a later privileged call.
358
- for (const key of this.confirmations.keys()) {
359
- if (key.startsWith(`${event.sessionId}\0`)) this.confirmations.delete(key);
360
- }
361
272
  for (const [id, state] of this.pending) {
362
273
  if (state.startEvent?.sessionId === event.sessionId || state.postEvent?.sessionId === event.sessionId || state.stopEvent?.sessionId === event.sessionId) {
363
274
  this.pending.delete(id);
@@ -394,38 +305,6 @@ export class ClaudeReviewHost implements AssuranceHostPort {
394
305
  }
395
306
  }
396
307
 
397
- peekConfirmation(sessionId: string, toolCallId: string): boolean {
398
- this.drain();
399
- return this.confirmations.has(`${sessionId}\0${toolCallId}`);
400
- }
401
-
402
- takeConfirmation(sessionId: string, toolCallId: string): "accept" | "deny" | "cancel" | undefined {
403
- this.drain();
404
- const key = `${sessionId}\0${toolCallId}`;
405
- const decision = this.confirmations.get(key);
406
- if (!decision) return undefined;
407
- if (!this.log.consumeElicitation(sessionId, toolCallId)) {
408
- this.confirmations.delete(key);
409
- return undefined;
410
- }
411
- this.confirmations.delete(key);
412
- this.consumedElicitations.add(key);
413
- this.drain();
414
- return decision;
415
- }
416
-
417
- sessionOfElicitation(toolCallId: string): string | undefined {
418
- this.drain();
419
- const sessions: string[] = [];
420
- for (const key of this.confirmations.keys()) {
421
- const sep = key.lastIndexOf("\0");
422
- if (sep >= 0 && key.slice(sep + 1) === toolCallId) sessions.push(key.slice(0, sep));
423
- }
424
- // Ambiguous correlation across sessions fails closed.
425
- if (sessions.length !== 1) return undefined;
426
- return sessions[0];
427
- }
428
-
429
308
  private applyReviewEvent(event: ClaudeHookEvent, state: PendingReview): void {
430
309
  if (state.error) return;
431
310
  if (event.type === "SubagentStart") {
@@ -543,10 +422,6 @@ export function parseHookStdin(raw: string): ClaudeHookEvent | null {
543
422
  try { payload = JSON.parse(raw) as Record<string, unknown>; }
544
423
  catch { return null; }
545
424
  const hookType = String(payload.hook_event_name ?? payload.type ?? "");
546
- // ElicitationResult is native authority evidence: it must carry the Host's
547
- // own session_id verbatim. Alias or environment fallbacks are never
548
- // accepted for this event; anything else is malformed and fails closed.
549
- if (hookType === "ElicitationResult" && (typeof payload.session_id !== "string" || !payload.session_id)) return null;
550
425
  const sessionId = String(payload.session_id ?? payload.sessionId ?? process.env.CLAUDE_SESSION_ID ?? "");
551
426
  if (!sessionId) return null;
552
427
  const agent = String(payload.agent_type ?? payload.agent ?? payload.subagent_type ?? "");
@@ -630,16 +505,5 @@ export function parseHookStdin(raw: string): ClaudeHookEvent | null {
630
505
  };
631
506
  }
632
507
  if (hookType === "SessionEnd") return { type: "SessionEnd", sessionId };
633
- if (hookType === "ElicitationResult") {
634
- const rawToolCallId = payload.tool_use_id ?? payload.toolCallId;
635
- if (typeof rawToolCallId !== "string" || !rawToolCallId) return null;
636
- const toolCallId = rawToolCallId;
637
- const raw = payload.decision ?? payload.result ?? payload.action;
638
- // Only the exact MCP elicitation decision strings are native evidence;
639
- // booleans and aliases like "yes" are malformed and fail closed.
640
- const decision = raw === "accept" ? "accept" : raw === "deny" ? "deny" : raw === "cancel" ? "cancel" : null;
641
- if (!toolCallId || !decision) return null;
642
- return { type: "ElicitationResult", sessionId, toolCallId, decision };
643
- }
644
508
  return null;
645
509
  }
@@ -158,7 +158,6 @@ export function applyTaskAction(
158
158
 
159
159
  const privileged =
160
160
  action.type === "record_approval" ||
161
- action.type === "record_user_approval" ||
162
161
  action.type === "approve_breaking_intent_revision" ||
163
162
  action.type === "request_rework" ||
164
163
  action.type === "stop" ||
@@ -25,13 +25,9 @@ export interface AssuranceAuthorizationReadiness {
25
25
  /**
26
26
  * Kernel-decidable authorization readiness:
27
27
  * - "resolve_user_decision": exactly one open unresolved-user-decision finding;
28
- * - "record_user_approval": Kernel projects authorize_user after fresh QA
29
- * and Review attestations for a critical task;
30
28
  * - "none": nothing uniquely decidable from Kernel facts.
31
- * A pending Pi native Review verdict is a session fact and is NOT visible
32
- * here; the host composes it before this readiness.
33
29
  */
34
- state: "resolve_user_decision" | "record_user_approval" | "none";
30
+ state: "resolve_user_decision" | "none";
35
31
  /** Non-null only when Kernel facts prove the authorization is blocked. */
36
32
  blocked: string | null;
37
33
  }
@@ -79,8 +75,6 @@ export function deriveAssuranceAuthorization(input: {
79
75
  state: "none",
80
76
  blocked: `resolve-user-decision requires exactly one open user decision; found ${input.open_user_decision_count}`,
81
77
  };
82
- if (input.next_obligation === "authorize_user")
83
- return { state: "record_user_approval", blocked: null };
84
78
  return { state: "none", blocked: null };
85
79
  }
86
80
 
@@ -51,7 +51,6 @@ export type CanaryOperation =
51
51
  | { op: "resolve_finding"; finding_id: string; actor_id: string }
52
52
  | { op: "request_rework"; capability: object; findings: TaskFinding[]; actor_id: string }
53
53
  | { op: "record_approval"; capability: object; approval: TaskApprovalV2; actor_id: string }
54
- | { op: "record_user_approval"; capability: object; approval: TaskApprovalV2; actor_id: string }
55
54
  | { op: "revise_intent"; next_intent: TaskIntentV1; actor_id: string }
56
55
  | { op: "approve_breaking_intent_revision"; capability: object; next_intent: TaskIntentV1; actor_id: string }
57
56
  | { op: "complete"; actor_id: string }
@@ -141,8 +140,6 @@ export function capabilityActionFor(input: {
141
140
  switch (input.op) {
142
141
  case "record_approval":
143
142
  return { ...base, approval: input.approval } as TaskAction;
144
- case "record_user_approval":
145
- return { ...base, approval: input.approval } as TaskAction;
146
143
  case "request_rework":
147
144
  return { ...base, findings: input.findings } as TaskAction;
148
145
  case "stop":
@@ -373,10 +370,6 @@ export function createCanaryApplication(
373
370
  capability = operation.capability;
374
371
  action = { ...base, type: "record_approval", approval: operation.approval };
375
372
  break;
376
- case "record_user_approval":
377
- capability = operation.capability;
378
- action = { ...base, type: "record_user_approval", approval: operation.approval };
379
- break;
380
373
  case "revise_intent":
381
374
  action = {
382
375
  ...base,
@@ -12,7 +12,7 @@ import { assertKernelInvariantsV3, KernelInvariantError } from "./validation";
12
12
  const REQUIRED_ATTESTATIONS: Record<TaskIntentV1["risk"], ApprovalKind[]> = {
13
13
  routine: ["qa"],
14
14
  material: ["qa", "review"],
15
- critical: ["qa", "review", "user"],
15
+ critical: ["qa", "review"],
16
16
  };
17
17
 
18
18
  function archiveActivePlanningPath(path: string): string | null {
@@ -199,8 +199,6 @@ export function projectTask(
199
199
  nextObligation = "run_qa";
200
200
  } else if (decision.missing_approval_kinds.includes("review")) {
201
201
  nextObligation = "run_review";
202
- } else if (decision.missing_approval_kinds.includes("user")) {
203
- nextObligation = "authorize_user";
204
202
  } else if (decision.complete) {
205
203
  nextObligation = "complete";
206
204
  }
@@ -176,7 +176,6 @@ function intentRefMatches(intent: TaskIntentV1, ref: TaskIntentRefV3): boolean {
176
176
  function hasPrivilegedKind(action: TaskAction): boolean {
177
177
  return (
178
178
  action.type === "record_approval" ||
179
- action.type === "record_user_approval" ||
180
179
  action.type === "approve_breaking_intent_revision" ||
181
180
  action.type === "request_rework" ||
182
181
  action.type === "stop" ||
@@ -374,36 +373,6 @@ export function reduceTask(
374
373
  appendHistory(record, action, from, approval.id, authorityAudit);
375
374
  break;
376
375
  }
377
- case "record_user_approval": {
378
- if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
379
- throw new KernelInvariantError([
380
- `cannot record user approval while state is ${stateOf(record)}`,
381
- ]);
382
- const approval = action.approval;
383
- if (approval.kind !== "user")
384
- throw new KernelInvariantError([
385
- "record_user_approval requires kind user",
386
- ]);
387
- if (!authorityAudit || authorityAudit.authority_kind !== "user")
388
- throw new KernelInvariantError([
389
- "record_user_approval requires user authority",
390
- ]);
391
- if (approval.task_revision !== record.intent_snapshot.revision)
392
- throw new KernelInvariantError(["approval task_revision must equal the current intent revision"]);
393
- if (approval.intent_content_hash !== record.intent_ref.content_hash)
394
- throw new KernelInvariantError(["approval intent_content_hash must equal the current intent hash"]);
395
- if (approval.diff_hash !== diffHash)
396
- throw new KernelInvariantError(["approval diff_hash must equal the action diff hash"]);
397
- if (record.attestations.some((item) => item.id === approval.id))
398
- throw new KernelInvariantError([
399
- `attestations contains duplicate id ${approval.id}`,
400
- ]);
401
- if (approval.review_revision)
402
- throw new KernelInvariantError(["review_revision is only valid on review approvals"]);
403
- record.attestations.push({ ...approval, acceptance_results: [] });
404
- appendHistory(record, action, from, approval.id, authorityAudit);
405
- break;
406
- }
407
376
  case "revise_intent":
408
377
  case "approve_breaking_intent_revision": {
409
378
  if (record.lifecycle !== "active")
@@ -57,7 +57,6 @@ export type AssuranceObligation =
57
57
  | "submit_assurance"
58
58
  | "run_qa"
59
59
  | "run_review"
60
- | "authorize_user"
61
60
  | "complete"
62
61
  | "none";
63
62
 
@@ -276,7 +275,6 @@ export type TaskAction =
276
275
  | (TaskActionBase & { type: "record_finding"; finding: TaskFinding })
277
276
  | (TaskActionBase & { type: "resolve_finding"; finding_id: string })
278
277
  | (TaskActionBase & { type: "record_approval"; approval: TaskApprovalV2 })
279
- | (TaskActionBase & { type: "record_user_approval"; approval: TaskApprovalV2 })
280
278
  | (TaskActionBase & {
281
279
  type: "revise_intent";
282
280
  next_intent: TaskIntentV1;
@@ -730,7 +730,6 @@ const ACTION_V2_TYPES = [
730
730
  "record_finding",
731
731
  "resolve_finding",
732
732
  "record_approval",
733
- "record_user_approval",
734
733
  "revise_intent",
735
734
  "approve_breaking_intent_revision",
736
735
  "request_rework",
@@ -831,8 +830,7 @@ export function parseTaskAction(raw: unknown): TaskAction {
831
830
  };
832
831
  break;
833
832
  }
834
- case "record_approval":
835
- case "record_user_approval": {
833
+ case "record_approval": {
836
834
  rejectUnknown(
837
835
  value,
838
836
  [...ACTION_BASE_FIELDS, "approval"],
@@ -0,0 +1,2 @@
1
+ // Generated by scripts/plugin_versioning.ts from the root package.json.
2
+ export const PLUGIN_VERSION = "3.4.0" as const;
@@ -59,7 +59,7 @@ Require exact host confirmation only for privileged effects:
59
59
  override; and
60
60
  - external writes whose target or impact cannot be safely reversed locally.
61
61
 
62
- Routine Managed enrollment uses one host confirmation bound to the TaskIntent content hash at the Planner's final `ctx.ui.custom` gate. Enrollment validates intent, Git ownership, scope, workspace claim, and final authority preconditions without executing acceptance descriptors; deterministic QA executes them after implementation. The routine task proceeds from that single confirmation through enrollment, execution, and QA without a second human stop. Do not request confirmation for local in-scope edits, local verification, ordinary Direct rework, scoped diff review, or completion reporting. Managed evidence, QA, Review, and completion authority remain governed by their Managed contracts; R2 does not weaken them.
62
+ Routine Managed enrollment uses one current-Host native confirmation bound to the TaskIntent content hash after Planner validation. Explicit Plan-only requests stop with candidate artifacts and do not invoke Enrollment; execution-bearing requests open the native gate directly without chat pre-confirmation. Enrollment validates intent, Git ownership, scope, workspace claim, and final authority preconditions without executing acceptance descriptors; deterministic QA executes them after implementation. The routine task proceeds from that single confirmation through enrollment, execution, and QA without a second human stop. Do not request confirmation for local in-scope edits, local verification, ordinary Direct rework, scoped diff review, or completion reporting. Managed evidence, QA, Review, and completion authority remain governed by their Managed contracts; R2 does not weaken them. Managed native-authority failures fail closed with one stable reason and exactly one same-Host recovery action; never offer a Pi, Direct Path, cross-Host/worktree, unmanaged, or automatic-retry fallback.
63
63
 
64
64
  ## Parallel Read-Only Dispatch
65
65
 
@@ -64,3 +64,7 @@ collect completed child outputs, and feed them to
64
64
  background work, mutate state, or own final Spec/Plan authority.
65
65
  Agreement becomes framing evidence, Disagreement becomes decision criteria or
66
66
  `BR-Q-*`, and strong-model blockers become risks or verification requirements.
67
+
68
+ When framing discusses later execution, describe Enrollment only as the current
69
+ Host's native gate. Never recommend another Host, worktree, or unmanaged
70
+ implementation as a fallback for a failed Managed authority interaction.
@@ -7,3 +7,7 @@ description: Use to run an enrolled TaskIntent to completion through Kernel-gove
7
7
 
8
8
  Load [`../../dist/imm-loop.md`](../../dist/imm-loop.md), then follow that
9
9
  canonical contract in the current host conversation.
10
+
11
+ All Managed authority gates use the current Host's native interaction. A failed
12
+ gate stays fail-closed and reports one same-Host recovery action; never suggest
13
+ another Host, worktree, or unmanaged implementation as a fallback.
@@ -7,3 +7,9 @@ description: Use to create or revise a spec and TaskIntent from requirements; ow
7
7
 
8
8
  Load [`../../dist/imm-planner.md`](../../dist/imm-planner.md), then follow that
9
9
  canonical contract. `mode: page_design` selects its page-design branch.
10
+
11
+ Plan-only requests stop after candidate Spec/TaskIntent validation. Requests that
12
+ include execution invoke the current Host's native Enrollment gate directly,
13
+ without chat pre-confirmation. Native-gate failure stays fail-closed in that Host:
14
+ report its reason and one retry action only; never suggest another Host, worktree,
15
+ or unmanaged implementation as a fallback.