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
@@ -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,
@@ -493,21 +491,15 @@ export class ClaudeRuntime {
493
491
  }
494
492
 
495
493
  async authorize(taskId: string, operation: string, meta: ToolMeta, extra: Record<string, unknown> = {}) {
496
- if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
497
- this.rejectBeforePreparation(operation, { ...meta, taskId });
498
494
  if (operation === "repair_authority_state") {
499
495
  const authority = reconcileKernelAuthority(this.cwd, taskId);
500
496
  if (authority.state !== "repairable_stale_claim" || authority.owner_task_id !== taskId) {
501
497
  throw new Error(authority.diagnostic ?? "authority repair requires a repairable stale claim");
502
498
  }
503
- const gate = this.gate(operation, { ...meta, taskId }, { bindingDigest: `repair:${authority.revision}` });
504
- const current = reconcileKernelAuthority(this.cwd, taskId);
505
- if (current.state !== authority.state || current.owner_task_id !== authority.owner_task_id || current.revision !== authority.revision) {
506
- throw new Error("authority changed after native confirmation; repair aborted before capability issuance");
507
- }
508
- return repairKernelAuthority(this.cwd, taskId, current.revision);
499
+ return repairKernelAuthority(this.cwd, taskId, authority.revision);
509
500
  }
510
- let op = operation === "request_authorization" ? "record_user_approval" : operation;
501
+ if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
502
+ let op = operation;
511
503
  let decisionOp: { finding_id: string; resolution: string } | undefined;
512
504
  const projection = await this.status(taskId);
513
505
  if (projection.error || !projection.claim) throw new Error(projection.error ?? "no active backend claim");
@@ -524,7 +516,7 @@ export class ClaudeRuntime {
524
516
  if (open.length !== 1) throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
525
517
  op = "resolve_user_decision";
526
518
  decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
527
- } else if (readiness.state !== "record_user_approval") {
519
+ } else {
528
520
  throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
529
521
  }
530
522
  }
@@ -567,15 +559,22 @@ export class ClaudeRuntime {
567
559
  writeFileSync(sidecar, `${JSON.stringify(nextIntent, null, 2)}\n`);
568
560
  execFileSync("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
569
561
  const preparedRecord = await readTaskRecord(this.cwd, taskId);
570
- 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
+ }
571
565
  preparedDiffHash = diffHashOf(this.cwd, preparedRecord.record);
572
566
  }
573
567
  const preparedProjection = await this.status(taskId);
574
- assertProjectionBinding(projection, preparedProjection, Boolean(nextIntent));
575
- if (preparedProjection.projection.diff_hash !== preparedDiffHash) {
576
- 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));
577
575
  }
578
- gate = this.gate(operation, { ...meta, taskId }, {
576
+ gate = await this.gate(operation, { ...meta, taskId }, {
577
+ risk: projection.projection.risk,
579
578
  intentRevision: nextIntent?.revision ?? projection.projection.intent_revision,
580
579
  intentContentHash: nextIntentHash ?? projection.projection.intent_content_hash,
581
580
  bindingDigest: `${preparedDiffHash}:${nextIntentHash ?? ""}`,
@@ -591,25 +590,18 @@ export class ClaudeRuntime {
591
590
  }
592
591
  const { registry, app } = this.authority();
593
592
  const confirmation = gate.confirmation_ref;
594
- const approval = op === "record_user_approval"
595
- ? {
596
- id: `approval-user-${randomUUID().slice(0, 8)}`,
597
- kind: "user" as const,
598
- authority_role: "user" as const,
599
- task_revision: projection.projection.intent_revision,
600
- intent_content_hash: projection.projection.intent_content_hash,
601
- diff_hash: projection.projection.diff_hash,
602
- actor_id: actorId,
603
- summary: "literal user approval",
604
- }
605
- : undefined;
606
593
  try {
607
594
  const capabilityProjection = await this.status(taskId);
608
- 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
+ }
609
600
  const operationDiffHash = capabilityProjection.projection.diff_hash;
610
601
  if (nextIntent && operationDiffHash !== preparedDiffHash) {
611
- throw new Error("Workspace changed after native confirmation; authority aborted before capability issuance");
602
+ throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
612
603
  }
604
+ throwIfCancelled(meta.signal);
613
605
  const capability = await mintCapability(registry, {
614
606
  authority_kind: "user",
615
607
  task_id: taskId,
@@ -621,11 +613,11 @@ export class ClaudeRuntime {
621
613
  actor_id: actorId,
622
614
  now,
623
615
  confirmation_ref: confirmation,
624
- ...(approval ? { approval } : {}),
625
616
  ...(op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {}),
626
617
  ...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
627
618
  ...(op === "stop" ? { reason: extra.reason ?? "user stop" } : {}),
628
619
  });
620
+ throwIfCancelled(meta.signal);
629
621
  const result = app.execute({
630
622
  root: this.cwd,
631
623
  task_id: taskId,
@@ -633,7 +625,6 @@ export class ClaudeRuntime {
633
625
  op,
634
626
  capability,
635
627
  actor_id: actorId,
636
- ...(approval ? { approval } : {}),
637
628
  ...(op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {}),
638
629
  ...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
639
630
  ...(op === "stop" ? { reason: extra.reason ?? "user stop" } : {}),
@@ -784,6 +775,5 @@ export interface ToolMeta {
784
775
  requiresUserInteraction?: boolean;
785
776
  permissionMode?: PermissionMode;
786
777
  interactive?: boolean;
787
- decision?: NativeDecision;
788
778
  signal?: AbortSignal;
789
779
  }