immune-brain 3.6.4 → 3.6.6

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 (42) hide show
  1. package/README.md +45 -0
  2. package/package.json +1 -1
  3. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  4. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +76 -20
  5. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +38 -10
  6. package/plugins/immune-brain/dist/BASELINE.md +48 -15
  7. package/plugins/immune-brain/dist/claude/mcp-server.mjs +166 -57
  8. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  9. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
  10. package/plugins/immune-brain/dist/imm-agent-doc-maintain.md +9 -1
  11. package/plugins/immune-brain/dist/imm-brainstorm.md +49 -35
  12. package/plugins/immune-brain/dist/imm-doc-prune.md +7 -1
  13. package/plugins/immune-brain/dist/imm-loop.md +31 -13
  14. package/plugins/immune-brain/dist/imm-planner.md +74 -32
  15. package/plugins/immune-brain/dist/imm-pr-fix.md +6 -2
  16. package/plugins/immune-brain/dist/role-prompts/executor.md +18 -10
  17. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +5 -2
  18. package/plugins/immune-brain/runtime/assurance/coordinator.ts +18 -0
  19. package/plugins/immune-brain/runtime/assurance/verification.ts +13 -2
  20. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +31 -9
  21. package/plugins/immune-brain/runtime/claude/mcp_server.ts +14 -1
  22. package/plugins/immune-brain/runtime/commands/kernel.ts +15 -13
  23. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1112 -20
  24. package/plugins/immune-brain/runtime/kernel/application.ts +1 -0
  25. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
  26. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +407 -0
  27. package/plugins/immune-brain/runtime/kernel/canary_application.ts +24 -9
  28. package/plugins/immune-brain/runtime/kernel/enrollment.ts +72 -13
  29. package/plugins/immune-brain/runtime/kernel/intent.ts +67 -23
  30. package/plugins/immune-brain/runtime/kernel/reducer.ts +37 -9
  31. package/plugins/immune-brain/runtime/kernel/types.ts +1 -0
  32. package/plugins/immune-brain/runtime/kernel/validation.ts +10 -6
  33. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  34. package/plugins/immune-brain/runtime/prompts/executor.md +18 -10
  35. package/plugins/immune-brain/runtime/prompts/pr-fix.md +5 -2
  36. package/plugins/immune-brain/skills/BASELINE.md +48 -15
  37. package/plugins/immune-brain/skills/imm-agent-doc-maintain/SKILL.md +20 -4
  38. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +24 -64
  39. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +18 -3
  40. package/plugins/immune-brain/skills/imm-loop/SKILL.md +20 -6
  41. package/plugins/immune-brain/skills/imm-planner/SKILL.md +35 -8
  42. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +17 -3
@@ -436,6 +436,7 @@ export class AssuranceCoordinator {
436
436
  sessionGenerationValue(): number { return this.sessionGeneration; }
437
437
 
438
438
  async advance(taskId: string, ctx: HostContext, signal?: AbortSignal, onUpdate?: (update: ForegroundToolUpdate) => void): Promise<AssuranceAdvanceResult> {
439
+ if (this.isInvocationOpen(taskId)) return { state: "blocked", reason: "an authority invocation is already open" };
439
440
  const active = this.active(taskId);
440
441
  if (active?.state === "review_ready") {
441
442
  const reservation = this.reviewReservations.get(taskId);
@@ -819,6 +820,17 @@ export class AssuranceCoordinator {
819
820
  return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` };
820
821
  }
821
822
 
823
+ isReviewVerdictValid(taskId: string, verdictInput: unknown): boolean {
824
+ const reservation = this.reviewReservations.get(taskId);
825
+ if (!reservation) return false;
826
+ try {
827
+ parseAssuranceVerdict(verdictInput, reservation.snapshot);
828
+ return true;
829
+ } catch {
830
+ return false;
831
+ }
832
+ }
833
+
822
834
  abandonReview(taskId: string, reason: string): AssuranceSubmitReviewResult {
823
835
  const reservation = this.reviewReservations.get(taskId);
824
836
  if (!reservation) return { state: "blocked", reason };
@@ -833,6 +845,12 @@ export class AssuranceCoordinator {
833
845
  return { state: "review_ready", operation: "review", operation_id: reservation.operationId, snapshot_digest: snapshotDigest(reservation.snapshot), review_bundle_digest: reservation.snapshot.review_bundle_digest ?? "", agent_params: reservation.hostReservation.dispatch };
834
846
  }
835
847
 
848
+ releaseStoppedReview(taskId: string): void {
849
+ const reservation = this.reviewReservations.get(taskId);
850
+ if (reservation) this.releaseReviewReservation(taskId, reservation);
851
+ this.rejectedReviewOperations.delete(taskId);
852
+ }
853
+
836
854
  private releaseReviewReservation(taskId: string, reservation: ReviewReservation, rejectionReason?: string): void {
837
855
  if (this.reviewReservations.get(taskId) !== reservation) return;
838
856
  this.reviewReservations.delete(taskId);
@@ -5,7 +5,7 @@
5
5
  // runner, compatibility gate, fixed execution, and findings digest.
6
6
 
7
7
  import { createHash } from "node:crypto";
8
- import { execFileSync, spawn } from "node:child_process";
8
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
9
9
  import { realpathSync, statSync } from "node:fs";
10
10
  import { isAbsolute, join, resolve, sep, relative } from "node:path";
11
11
 
@@ -43,7 +43,18 @@ export function resolveBunRunner(): FrozenRunner {
43
43
  }
44
44
  let real: string;
45
45
  try {
46
- real = realpathSync(executable);
46
+ // When which returns a version-manager shim (e.g. mise or asdf),
47
+ // realpathSync on the shim resolves to the version-manager binary rather
48
+ // than bun. Ask bun itself for its real process.execPath.
49
+ const execPath = spawnSync(executable, ["-e", "console.log(process.execPath)"], {
50
+ encoding: "utf8",
51
+ stdio: ["ignore", "pipe", "ignore"],
52
+ });
53
+ if (execPath.status === 0 && execPath.stdout.trim().length > 0) {
54
+ real = realpathSync(execPath.stdout.trim());
55
+ } else {
56
+ real = realpathSync(executable);
57
+ }
47
58
  } catch {
48
59
  throw new VerificationDescriptorError("bun runner realpath is unresolvable");
49
60
  }
@@ -119,13 +119,16 @@ export async function submitClaudeReview(
119
119
  if (observed.release) return coordinator.abandonReview(taskId, observed.reason);
120
120
  return { state: "blocked", reason: observed.reason };
121
121
  }
122
- const parentJson = extractVerdictJson(verdictInput);
123
- const receiptJson = extractVerdictJson(observed.receipt.result);
124
- if (parentJson && receiptJson && verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
125
- return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
122
+ const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
123
+ if (!parentValid) return coordinator.submitReview(taskId, ctx, verdictInput);
124
+ const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
125
+ if (!receiptValid) {
126
+ return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
126
127
  }
127
- if (parentJson && !receiptJson) {
128
- return { state: "blocked", reason: "reviewer receipt is not a valid verdict" };
128
+ const parentJson = extractVerdictJson(verdictInput)!;
129
+ const receiptJson = extractVerdictJson(observed.receipt.result)!;
130
+ if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
131
+ return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
129
132
  }
130
133
  return coordinator.submitReview(taskId, ctx, verdictInput);
131
134
  }
@@ -525,6 +528,19 @@ export class ClaudeRuntime {
525
528
  return submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput);
526
529
  }
527
530
 
531
+ /**
532
+ * Ordinary Kernel operation, not a privileged one: canary_application builds
533
+ * the action without a capability and the Pi Host lists resolve_finding in
534
+ * its ordinary KERNEL_OPERATIONS. The reducer owns every precondition, so
535
+ * this port reads no findings and tests no kind.
536
+ */
537
+ async resolveFinding(taskId: string, findingId: string) {
538
+ return this.executeOrdinary({ cwd: this.cwd }, {
539
+ taskId,
540
+ operation: { op: "resolve_finding", finding_id: findingId, actor_id: "executor" },
541
+ });
542
+ }
543
+
528
544
  async authorize(taskId: string, operation: string, meta: ToolMeta, extra: Record<string, unknown> = {}) {
529
545
  if (operation === "repair_authority_state") {
530
546
  const authority = reconcileKernelAuthority(this.cwd, taskId);
@@ -534,7 +550,7 @@ export class ClaudeRuntime {
534
550
  return repairKernelAuthority(this.cwd, taskId, authority.revision);
535
551
  }
536
552
  if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
537
- let op: PrivilegedOperation | "request_authorization" | "resolve_user_decision" = operation;
553
+ let op: PrivilegedOperation | "request_authorization" | "resolve_user_decision" | "authorize_rework" = operation;
538
554
  let decisionOp: { finding_id: string; resolution: string } | undefined;
539
555
  const projection = await this.status(taskId);
540
556
  if (projection.error || !projection.claim) throw new Error(projection.error ?? "no active backend claim");
@@ -551,6 +567,8 @@ export class ClaudeRuntime {
551
567
  if (open.length !== 1) throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
552
568
  op = "resolve_user_decision";
553
569
  decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
570
+ } else if (readiness.state === "authorize_rework") {
571
+ op = "authorize_rework";
554
572
  } else {
555
573
  throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
556
574
  }
@@ -668,7 +686,11 @@ export class ClaudeRuntime {
668
686
  diffProvider: diffSnapshotOf,
669
687
  now,
670
688
  });
671
- if (op === "stop" || op === "approve_breaking_intent_revision") stagePlanningArtifactTransition(this.cwd, result.record);
689
+ if (
690
+ op === "stop" ||
691
+ op === "authorize_rework" ||
692
+ op === "approve_breaking_intent_revision"
693
+ ) stagePlanningArtifactTransition(this.cwd, result.record);
672
694
  return result;
673
695
  } catch (error) {
674
696
  if (sidecar && priorBytes && priorIndexState) {
@@ -773,7 +795,7 @@ export class ClaudeRuntime {
773
795
  }));
774
796
  }
775
797
 
776
- private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown } }) {
798
+ private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown; finding_id?: string } }) {
777
799
  const { app } = await this.authority();
778
800
  const operation = input.operation.op === "revise_intent"
779
801
  ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
@@ -26,6 +26,7 @@ export const TOOLS = [
26
26
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
27
27
  { name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
28
28
  { name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
29
+ { name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false },
29
30
  ] as const;
30
31
 
31
32
  export function listMcpTools() {
@@ -39,8 +40,13 @@ export function listMcpTools() {
39
40
  ...(tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {}),
40
41
  ...(tool.name === "stop" ? { reason: { type: "string" } } : {}),
41
42
  ...(tool.name === "submit_review" ? { verdict: { type: "object" } } : {}),
43
+ ...(tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {}),
42
44
  },
43
- required: tool.name === "submit_review" ? ["task_id", "verdict"] : ["task_id"],
45
+ required: tool.name === "submit_review"
46
+ ? ["task_id", "verdict"]
47
+ : tool.name === "resolve_finding"
48
+ ? ["task_id", "finding_id"]
49
+ : ["task_id"],
44
50
  },
45
51
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" },
46
52
  }));
@@ -119,6 +125,13 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
119
125
  if (!Object.hasOwn(args, "verdict")) throw new Error("verdict is required");
120
126
  return runtime.submitReview(taskId, args.verdict);
121
127
  }
128
+ if (name === "resolve_finding") {
129
+ // Structural only. Which findings may be resolved, and when, stays
130
+ // the reducer's decision; duplicating it here would create a second
131
+ // authority that could drift from the Kernel.
132
+ if (typeof args.finding_id !== "string" || !args.finding_id) throw new Error("finding_id is required");
133
+ return runtime.resolveFinding(taskId, args.finding_id);
134
+ }
122
135
  if (name === "request_authorization" || name === "approve_breaking_intent_revision" || name === "stop" || name === "repair_authority_state") {
123
136
  return runtime.authorize(taskId, name, toolMeta, args);
124
137
  }
@@ -248,19 +248,6 @@ function runInspect(root: string): KernelExecution {
248
248
  ),
249
249
  };
250
250
  }
251
- let declared: TaskRisk;
252
- let intent: TaskIntentV1;
253
- try {
254
- const raw = JSON.parse(
255
- readSecureProjectFile(root, `docs/plans/${claim.task_id}.intent.json`),
256
- ) as { risk?: unknown };
257
- if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
258
- throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
259
- declared = raw.risk;
260
- intent = parseTaskIntentV1(raw);
261
- } catch (error) {
262
- return sourceFailure("inspect", error);
263
- }
264
251
  let recordRead: ReturnType<typeof readTaskRecordRaw>;
265
252
  try {
266
253
  recordRead = readTaskRecordRaw(root, claim.task_id);
@@ -285,6 +272,21 @@ function runInspect(root: string): KernelExecution {
285
272
  };
286
273
  }
287
274
  const record = recordRead.record;
275
+ let declared: TaskRisk;
276
+ let intent: TaskIntentV1;
277
+ try {
278
+ const raw = JSON.parse(
279
+ readSecureProjectFile(root, record.intent_ref.path),
280
+ ) as { risk?: unknown };
281
+ if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
282
+ throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
283
+ declared = raw.risk;
284
+ intent = parseTaskIntentV1(raw);
285
+ if (canonicalIntentHash(intent) !== record.intent_ref.content_hash)
286
+ throw new Error("TaskIntent sidecar does not match TaskRecord content hash");
287
+ } catch (error) {
288
+ return sourceFailure("inspect", error);
289
+ }
288
290
  const workspaceState = readWorkspaceStateRaw(root);
289
291
  let identity;
290
292
  try {