immune-brain 3.6.5 → 3.6.7

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 (49) hide show
  1. package/README.md +69 -3
  2. package/README.zh-CN.md +23 -3
  3. package/package.json +2 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +60 -21
  6. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +932 -0
  7. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +38 -10
  8. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +200 -2
  9. package/plugins/immune-brain/dist/BASELINE.md +48 -15
  10. package/plugins/immune-brain/dist/claude/mcp-server.mjs +3415 -228
  11. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  12. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
  13. package/plugins/immune-brain/dist/imm-agent-doc-maintain.md +9 -1
  14. package/plugins/immune-brain/dist/imm-brainstorm.md +49 -35
  15. package/plugins/immune-brain/dist/imm-doc-prune.md +7 -1
  16. package/plugins/immune-brain/dist/imm-loop.md +45 -11
  17. package/plugins/immune-brain/dist/imm-planner.md +65 -28
  18. package/plugins/immune-brain/dist/imm-pr-fix.md +6 -2
  19. package/plugins/immune-brain/dist/role-prompts/code-review.md +9 -1
  20. package/plugins/immune-brain/dist/role-prompts/executor.md +18 -10
  21. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +5 -2
  22. package/plugins/immune-brain/runtime/assurance/coordinator.ts +101 -3
  23. package/plugins/immune-brain/runtime/assurance/verification.ts +13 -2
  24. package/plugins/immune-brain/runtime/claude/interaction.ts +16 -3
  25. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +840 -17
  26. package/plugins/immune-brain/runtime/claude/mcp_server.ts +72 -10
  27. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1018 -17
  28. package/plugins/immune-brain/runtime/kernel/canary_application.ts +16 -1
  29. package/plugins/immune-brain/runtime/kernel/completion.ts +19 -1
  30. package/plugins/immune-brain/runtime/kernel/reducer.ts +165 -12
  31. package/plugins/immune-brain/runtime/kernel/refutation.ts +82 -0
  32. package/plugins/immune-brain/runtime/kernel/types.ts +34 -1
  33. package/plugins/immune-brain/runtime/kernel/validation.ts +202 -9
  34. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  35. package/plugins/immune-brain/runtime/prompts/code-review.md +9 -1
  36. package/plugins/immune-brain/runtime/prompts/executor.md +18 -10
  37. package/plugins/immune-brain/runtime/prompts/pr-fix.md +5 -2
  38. package/plugins/immune-brain/runtime/unattended/batch_git.ts +775 -0
  39. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +203 -0
  40. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +1224 -0
  41. package/plugins/immune-brain/runtime/unattended/batch_state.ts +360 -0
  42. package/plugins/immune-brain/runtime/unattended/types.ts +60 -0
  43. package/plugins/immune-brain/skills/BASELINE.md +48 -15
  44. package/plugins/immune-brain/skills/imm-agent-doc-maintain/SKILL.md +20 -4
  45. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +24 -64
  46. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +18 -3
  47. package/plugins/immune-brain/skills/imm-loop/SKILL.md +21 -6
  48. package/plugins/immune-brain/skills/imm-planner/SKILL.md +35 -8
  49. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +17 -3
@@ -1,13 +1,16 @@
1
1
  # Internal role: executor
2
2
 
3
- You are the Immune-Brain Executor role inside Loop. Implement exactly one
4
- active Step, or one accepted same-boundary follow-up, in the current Parent
5
- conversation. Use workspace tools only for the supplied target and keep every
6
- edit inside the authoritative Scope. Do not discover or load a Pi Skill.
3
+ You are the Immune-Brain Executor role inside Loop. Implement exactly the
4
+ enrolled TaskIntent acceptance and `scope_hint` (or one accepted
5
+ same-boundary follow-up) in the current Parent conversation. Use workspace
6
+ tools only for the supplied target and keep every edit inside the
7
+ authoritative Scope. Do not discover or load a Pi Skill.
7
8
 
8
- Before handoff, verify the active Result with the supplied Verification
9
- commands and record structured execution evidence through the Loop runtime
10
- action. Preserve failed and blocked attempts. Do not perform QA,
9
+ Before handoff, run the permitted diagnostic checks and return commands and
10
+ outcomes to the Parent as structured diagnostic evidence. The read-only Loop
11
+ runtime action only constructs the dispatch envelope; it does not store
12
+ evidence or change task state. Preserve failed and blocked attempts. Do not
13
+ perform QA,
11
14
  review, plan mutation, successor approval, Compounder work, or authority
12
15
  writes. If the requested change needs scope expansion, stop and return an
13
16
  `imm-planner` route with the concrete missing scope and verification reason.
@@ -22,6 +25,11 @@ Do not weaken tests or hide an incomplete result to make Verification pass.
22
25
  Treat naming, function length, parameter count, nesting, and abstraction taste
23
26
  as contextual signals, never as automatic failure thresholds.
24
27
 
25
- Fix in-scope integrity defects before Verification. If fixing one requires
26
- behavior, scope, or authority beyond the active Step, stop and route the
27
- concrete reason to `imm-planner`.
28
+ Fix in-scope integrity defects before Verification. Autonomously diagnose,
29
+ repair, and rerun failing ordinary local checks within the authorized scope;
30
+ do not stop for a repair round that stays inside the TaskIntent boundary. If
31
+ fixing requires
32
+ behavior, scope, or authority beyond the enrolled TaskIntent, stop and route the
33
+ concrete reason to `imm-planner`. An unavailable or still-failing required
34
+ check is an explicit blocker: report it to the Parent; never present failed
35
+ verification as completion.
@@ -71,8 +71,11 @@ decision, stop and report it to the Parent.
71
71
 
72
72
  ## Boundary
73
73
 
74
- Work only inside the supplied Plan, `plan_id`, changed-file boundary, review
75
- feedback, and verification commands. Do not create a second Plan, silently
74
+ Work only inside the supplied repair boundary: the current TaskIntent
75
+ acceptance and `scope_hint` when operating under one, otherwise the legacy
76
+ supplied Plan, `plan_id`, changed-file boundary, review feedback, and
77
+ verification commands from the delegation context. Do not create a second
78
+ Plan or TaskIntent, silently
76
79
  widen scope, push to unrelated branches, merge, approve a successor, or invoke
77
80
  another role. Do not discover or load a Pi Skill.
78
81
 
@@ -7,6 +7,8 @@
7
7
  // evidence reservation.
8
8
 
9
9
  import { createHash, randomUUID } from "node:crypto";
10
+ import type { FindingEvidence, TaskFinding } from "../kernel/types";
11
+ import { anchorForEvidence } from "../kernel/refutation";
10
12
  import {
11
13
  findingsDigest,
12
14
  type FrozenRunner,
@@ -88,10 +90,40 @@ export interface AssuranceVerdict {
88
90
  kind: "blocking" | "advisory";
89
91
  acceptance_id: string | null;
90
92
  summary: string;
93
+ /**
94
+ * Derived by `parseAssuranceVerdict` for Review-role rework findings. QA
95
+ * findings and pass verdicts never carry it.
96
+ */
97
+ anchor?: string;
98
+ evidence?: FindingEvidence;
99
+ /** Digest this plugin's verdict parser derives for one finding. */
91
100
  findings_digest: string;
92
101
  }>;
93
102
  }
94
103
 
104
+ /**
105
+ * The single Review-verdict → Kernel-finding mapping both Host adapters use.
106
+ * Keeping it here is what makes the two Hosts produce the same
107
+ * `findingsDigestV2` for the same verdict: the reviewer's anchor and evidence
108
+ * are forwarded unchanged into the capability binding and the Kernel action.
109
+ */
110
+ export function reviewReworkFindings(verdict: AssuranceVerdict): TaskFinding[] {
111
+ if (verdict.decision !== "rework" || !verdict.findings?.length)
112
+ throw new Error("review rework findings require a rework verdict");
113
+ return verdict.findings.map((finding) => ({
114
+ id: finding.id,
115
+ kind: finding.kind,
116
+ status: "open" as const,
117
+ acceptance_id: finding.acceptance_id,
118
+ source: "review" as const,
119
+ review_round: null,
120
+ summary: finding.summary,
121
+ anchor: finding.anchor ?? null,
122
+ evidence: finding.evidence ?? null,
123
+ counterevidence: null,
124
+ }));
125
+ }
126
+
95
127
  export interface ReviewRevisionIdentity {
96
128
  contract: "assurance_kernel/review_revision_identity/v1";
97
129
  base_head: string;
@@ -314,12 +346,57 @@ export function buildReviewPrompt(snapshot: SnapshotDescriptor, evidencePath?: s
314
346
  : `Intent revision ${snapshot.intent_revision} (hash ${snapshot.intent_content_hash}), diff ${snapshot.diff_hash}, review bundle ${snapshot.review_bundle_digest}, state ${snapshot.lifecycle}:${snapshot.artifact_state}.`,
315
347
  "Acceptance assertions:", acceptance,
316
348
  "Reserve the final turn for exactly one strict JSON verdict. Reply with ONLY that object, without markdown fences or commentary.",
349
+ `Every rework finding must carry machine-checkable provenance: evidence.trigger (the concrete inputs or state that reach the defect), a non-empty evidence.caller_chain (ordered repository paths or symbols), and evidence.violated {kind: "acceptance"|"security_boundary", ref}. The anchor is derived from that evidence; a finding without it is rejected and the correction must be resubmitted.`,
317
350
  `PASS shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"pass","approval":{"kind":"review","authority_role":"reviewer","summary":"<one line>"}}`,
318
- `REWORK shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"rework","findings":[{"id":"review-1","kind":"blocking|advisory","acceptance_id":"<id|null>","summary":"<one line>"}]}`,
351
+ `REWORK shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"rework","findings":[{"id":"review-1","kind":"blocking|advisory","acceptance_id":"<id|null>","summary":"<one line>","evidence":{"trigger":"<concrete inputs or state>","caller_chain":["<path-or-symbol>"],"violated":{"kind":"acceptance|security_boundary","ref":"<acceptance id or boundary>"}}}]}`,
319
352
  `REWORK verdicts must omit the approval field entirely; do not emit "approval": null.`,
320
353
  ].join("\n");
321
354
  }
322
355
 
356
+ function parseVerdictEvidence(value: unknown, index: number): FindingEvidence {
357
+ if (!value || typeof value !== "object" || Array.isArray(value))
358
+ throw new Error(`finding ${index} evidence is required`);
359
+ const evidence = value as Record<string, unknown>;
360
+ const unknown = Object.keys(evidence).find(
361
+ (key) => !["trigger", "caller_chain", "violated"].includes(key),
362
+ );
363
+ if (unknown) throw new Error(`finding ${index} evidence has unknown field: ${unknown}`);
364
+ if (typeof evidence.trigger !== "string" || !evidence.trigger.trim())
365
+ throw new Error(`finding ${index} evidence.trigger must be a non-empty string`);
366
+ const chain = evidence.caller_chain;
367
+ if (
368
+ !Array.isArray(chain) ||
369
+ chain.length === 0 ||
370
+ chain.some((entry) => typeof entry !== "string" || !entry.trim())
371
+ )
372
+ throw new Error(
373
+ `finding ${index} evidence.caller_chain must be a non-empty list of paths or symbols`,
374
+ );
375
+ const violated = evidence.violated;
376
+ if (!violated || typeof violated !== "object" || Array.isArray(violated))
377
+ throw new Error(`finding ${index} evidence.violated is required`);
378
+ const violatedRecord = violated as Record<string, unknown>;
379
+ const unknownViolated = Object.keys(violatedRecord).find(
380
+ (key) => !["kind", "ref"].includes(key),
381
+ );
382
+ if (unknownViolated)
383
+ throw new Error(`finding ${index} evidence.violated has unknown field: ${unknownViolated}`);
384
+ if (violatedRecord.kind !== "acceptance" && violatedRecord.kind !== "security_boundary")
385
+ throw new Error(
386
+ `finding ${index} evidence.violated.kind must be acceptance or security_boundary`,
387
+ );
388
+ if (typeof violatedRecord.ref !== "string" || !violatedRecord.ref.trim())
389
+ throw new Error(`finding ${index} evidence.violated.ref must be a non-empty string`);
390
+ return {
391
+ trigger: evidence.trigger,
392
+ caller_chain: chain as string[],
393
+ violated: {
394
+ kind: violatedRecord.kind,
395
+ ref: violatedRecord.ref,
396
+ },
397
+ };
398
+ }
399
+
323
400
  export function parseAssuranceVerdict(input: unknown, snapshot: SnapshotDescriptor): AssuranceVerdict {
324
401
  let raw: Record<string, unknown>;
325
402
  if (typeof input === "string") {
@@ -353,12 +430,33 @@ export function parseAssuranceVerdict(input: unknown, snapshot: SnapshotDescript
353
430
  if (raw.approval !== undefined && raw.approval !== null) throw new Error("rework verdict must omit approval");
354
431
  const findings = raw.findings.map((item, index) => {
355
432
  const finding = item as Record<string, unknown>;
356
- const unknownFinding = Object.keys(finding).find((key) => !["id", "kind", "acceptance_id", "summary"].includes(key));
433
+ const allowedFindingKeys =
434
+ snapshot.role === "review"
435
+ ? ["id", "kind", "acceptance_id", "summary", "evidence"]
436
+ : ["id", "kind", "acceptance_id", "summary"];
437
+ const unknownFinding = Object.keys(finding).find(
438
+ (key) => !allowedFindingKeys.includes(key),
439
+ );
357
440
  if (unknownFinding) throw new Error(`finding ${index} has unknown field: ${unknownFinding}`);
358
441
  if (typeof finding.id !== "string" || !finding.id.trim() || (finding.kind !== "blocking" && finding.kind !== "advisory") || (finding.acceptance_id !== null && typeof finding.acceptance_id !== "string") || typeof finding.summary !== "string" || !finding.summary.trim()) throw new Error(`finding ${index} is invalid`);
359
442
  const id = `review-${snapshotDigest(snapshot).slice(7, 19)}-${index + 1}-${finding.id.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 48)}`;
360
443
  const normalized = { id, kind: finding.kind as "blocking" | "advisory", acceptance_id: finding.acceptance_id as string | null, summary: finding.summary as string };
361
- return { ...normalized, findings_digest: findingsDigest([normalized]) };
444
+ if (snapshot.role !== "review")
445
+ return { ...normalized, findings_digest: findingsDigest([normalized]) };
446
+ // Review findings must carry evidence; the anchor is derived here so a
447
+ // capability minted for one anchor set cannot be spent on another and
448
+ // the reviewer can never assert the anchor directly.
449
+ const evidence = parseVerdictEvidence(finding.evidence, index);
450
+ // The digest that decides refutation inheritance is derived here so the
451
+ // reviewer can never assert the anchor directly; the Kernel recomputes it
452
+ // from the same evidence before admitting the finding.
453
+ const anchor = anchorForEvidence(evidence);
454
+ return {
455
+ ...normalized,
456
+ anchor,
457
+ evidence,
458
+ findings_digest: findingsDigest([normalized]),
459
+ };
362
460
  });
363
461
  return { contract: "assurance_kernel/assurance_verdict/v2", role: snapshot.role, task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "rework", findings };
364
462
  }
@@ -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
  }
@@ -5,6 +5,7 @@ export const PRIVILEGED_OPERATIONS = [
5
5
  "request_authorization",
6
6
  "approve_breaking_intent_revision",
7
7
  "stop",
8
+ "start_unattended_batch",
8
9
  ] as const;
9
10
 
10
11
  export type PrivilegedOperation = (typeof PRIVILEGED_OPERATIONS)[number];
@@ -66,12 +67,22 @@ export function evaluateNativeGate(input: NativeGateInput): NativeGateResult {
66
67
 
67
68
  export interface NativeConfirmationInput {
68
69
  operation: PrivilegedOperation;
69
- taskId: string;
70
+ taskId?: string;
71
+ initiativeSlug?: string;
70
72
  toolCallId: string;
71
73
  risk?: string;
72
74
  intentRevision?: number;
73
75
  intentContentHash?: string;
74
76
  bindingDigest?: string;
77
+ planDigest?: string;
78
+ batchDetails?: {
79
+ initiative_slug: string;
80
+ batch_branch: string;
81
+ children: Array<{ task_id: string; slice_id: string; risk?: string }>;
82
+ excluded: Array<{ task_id: string; slice_id: string; reason: string }>;
83
+ budget: { max_children: number; deadline_at: string; qa_failure_limit: number };
84
+ expires_at: string;
85
+ };
75
86
  signal?: AbortSignal;
76
87
  }
77
88
 
@@ -87,13 +98,15 @@ export function confirmationRef(input: {
87
98
  toolCallId: string;
88
99
  requestId: string;
89
100
  operation: string;
90
- taskId: string;
101
+ taskId?: string;
102
+ initiativeSlug?: string;
91
103
  intentRevision?: number;
92
104
  intentContentHash?: string;
93
105
  bindingDigest?: string;
106
+ planDigest?: string;
94
107
  }): string {
95
108
  return `claude-confirm-${createHash("sha256")
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 ?? ""}`)
109
+ .update(`${input.connectionId}\0${input.toolCallId}\0${input.requestId}\0${input.operation}\0${input.taskId ?? ""}\0${input.initiativeSlug ?? ""}\0${input.intentRevision ?? ""}\0${input.intentContentHash ?? ""}\0${input.bindingDigest ?? ""}\0${input.planDigest ?? ""}`)
97
110
  .digest("hex")
98
111
  .slice(0, 16)}`;
99
112
  }