immune-brain 5.1.1 → 5.1.2

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 (39) hide show
  1. package/package.json +1 -1
  2. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  3. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +20 -0
  4. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +66 -11
  5. package/plugins/immune-brain/agents/immune-brain-reviewer.md +15 -0
  6. package/plugins/immune-brain/dist/claude/mcp-server.mjs +8256 -7417
  7. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  8. package/plugins/immune-brain/dist/imm-brainstorm.md +4 -4
  9. package/plugins/immune-brain/dist/imm-planner.md +37 -4
  10. package/plugins/immune-brain/dist/imm-run.md +123 -9
  11. package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
  12. package/plugins/immune-brain/dist/role-prompts/executor.md +7 -0
  13. package/plugins/immune-brain/hooks/hooks.json +22 -1
  14. package/plugins/immune-brain/runtime/assurance/active_task_notice.ts +42 -0
  15. package/plugins/immune-brain/runtime/assurance/coordinator.ts +9 -0
  16. package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +4 -1
  17. package/plugins/immune-brain/runtime/claude/interaction.ts +5 -0
  18. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +32 -6
  19. package/plugins/immune-brain/runtime/claude/lane_guard.ts +33 -0
  20. package/plugins/immune-brain/runtime/claude/mcp_server.ts +66 -6
  21. package/plugins/immune-brain/runtime/commands/kernel.ts +54 -1
  22. package/plugins/immune-brain/runtime/github_issue_tracker.ts +235 -7
  23. package/plugins/immune-brain/runtime/kernel/runtime_contracts.json +7 -0
  24. package/plugins/immune-brain/runtime/kernel/runtime_contracts.ts +75 -0
  25. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  26. package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
  27. package/plugins/immune-brain/runtime/prompts/executor.md +7 -0
  28. package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +160 -0
  29. package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +72 -0
  30. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +396 -23
  31. package/plugins/immune-brain/runtime/unattended/batch_leak_restore.ts +131 -0
  32. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +25 -4
  33. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +3 -2
  34. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +42 -0
  35. package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +40 -0
  36. package/plugins/immune-brain/runtime/unattended/batch_state.ts +155 -0
  37. package/plugins/immune-brain/runtime/unattended/lane_workspace.ts +58 -0
  38. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +1 -1
  39. package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "5.1.1",
3
+ "version": "5.1.2",
4
4
  "description": "Immune-Brain agent skill system",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "5.1.1",
3
+ "version": "5.1.2",
4
4
  "description": "Immune-Brain Claude Code Host: native Enrollment, QA, Review, and Kernel settlement.",
5
5
  "author": {
6
6
  "name": "Immune-Brain Team"
@@ -130,6 +130,8 @@ import {
130
130
  type GithubTrackerResult,
131
131
  } from "../runtime/github_issue_tracker";
132
132
  import { reviewReworkFindings } from "../runtime/assurance/coordinator";
133
+ import { laneWriteRefusal } from "../runtime/unattended/lane_workspace";
134
+ import { activeTaskNotice } from "../runtime/assurance/active_task_notice";
133
135
 
134
136
  const LOOP_OWNERS = ["plan", "kernel", "brainstorm", "planner", "loop"] as const;
135
137
  const LOOP_TARGETS = [
@@ -371,8 +373,16 @@ export default function (
371
373
  if (ctx.mode !== "tui") return;
372
374
  await refreshTaskRail(ctx);
373
375
  });
376
+ pi.on("before_agent_start", async (event: { prompt?: string; systemPrompt?: string }, ctx?: ExtensionContext) => {
377
+ // Advisory only: an active Managed task is announced, never resumed.
378
+ const notice = await activeTaskNotice(ctx?.cwd ?? process.cwd());
379
+ if (!notice || typeof event.systemPrompt !== "string") return undefined;
380
+ return { systemPrompt: `${event.systemPrompt}\n\n${notice}` };
381
+ });
374
382
  pi.on("tool_call", (event: { toolName?: string; input?: unknown; toolCallId?: string }, ctx?: ExtensionContext) => {
375
383
  if (ctx) railContext = ctx;
384
+ const laneRefusal = laneEditRefusal(event, ctx?.cwd ?? process.cwd());
385
+ if (laneRefusal) return { block: true, reason: laneRefusal };
376
386
  if (event.toolName === "Agent") {
377
387
  // ADR 0017: correlate a reserved reviewer dispatch to its reservation.
378
388
  progression.piReviewHost.observeReviewDispatch(event.input as { prompt?: unknown; subagent_type?: unknown } | undefined, event.toolCallId);
@@ -1162,6 +1172,16 @@ export function readTaskIntentForRecord(root: string, taskId: string): ReadTaskI
1162
1172
  return readTaskIntent(root, taskId, currentPath);
1163
1173
  }
1164
1174
 
1175
+ /**
1176
+ * In a batch Lane, an `edit` or `write` whose resolved target is outside the
1177
+ * Lane is refused before it runs; `bash` is not intercepted.
1178
+ */
1179
+ export function laneEditRefusal(event: { toolName?: string; input?: unknown }, cwd: string): string | null {
1180
+ if (event.toolName !== "edit" && event.toolName !== "write") return null;
1181
+ const input = (event.input ?? {}) as { path?: unknown; file_path?: unknown };
1182
+ return laneWriteRefusal(cwd, input.path ?? input.file_path);
1183
+ }
1184
+
1165
1185
  async function markGithubTaskTerminal(
1166
1186
  root: string,
1167
1187
  input: { task_id: string; phase: "done" | "stopped"; terminal_event_id: string },
@@ -15,13 +15,16 @@ import {
15
15
  startBatch,
16
16
  type BatchRunnerKernelPort,
17
17
  type BatchRunReport,
18
+ type BatchTrackerPort,
18
19
  } from "../runtime/unattended/batch_runner";
19
20
  import { createBatchKernelPort } from "../runtime/unattended/batch_kernel_port";
20
21
  import type { BatchRunnerGitPort } from "../runtime/unattended/batch_git";
21
22
  import type { InitiativeObservationReader } from "../runtime/unattended/types";
22
23
  import { advancePiTask } from "./imm-canary-work";
23
24
  import { batchReason } from "../runtime/unattended/batch_reasons";
24
- import { parseLaneOffers, parseMaxParallel, resolveLaneParallel, type LaneOffer } from "../runtime/unattended/batch_lanes";
25
+ import { parseFinalVerification } from "../runtime/unattended/batch_final_verification";
26
+ import { parseLaneRevision, REVISION_DELEGATION_TEXT, type LaneRevisionRequest } from "../runtime/unattended/batch_delegation";
27
+ import { createBatchTrackerPort, laneRuntimeContractRefusal, parseLaneInstruction, type LaneInstructionRequest, parseLaneOffers, parseMaxParallel, resolveLaneParallel, type LaneOffer } from "../runtime/unattended/batch_lanes";
25
28
  import {
26
29
  authorizeBatch,
27
30
  projectBatchPreflight,
@@ -70,8 +73,9 @@ export async function readSettledTaskRecord(
70
73
  * is the testable unit, and the direct-execution suites cover the decision
71
74
  * outcomes (decline -> rejected, cancel/Escape -> cancelled) with zero writes.
72
75
  */
73
- export function mapDialogSelection(selected: string | undefined): "accept" | "decline" | "cancel" {
76
+ export function mapDialogSelection(selected: string | undefined): "accept" | "accept_delegate" | "decline" | "cancel" {
74
77
  if (selected === "confirm") return "accept";
78
+ if (selected === "confirm_delegate") return "accept_delegate";
75
79
  if (selected === "decline") return "decline";
76
80
  return "cancel";
77
81
  }
@@ -107,8 +111,16 @@ export interface PiBatchExecutionOptions {
107
111
  max_parallel?: number;
108
112
  /** Lane mode: Lanes someone else created, offered to the batch. */
109
113
  lane_offers?: LaneOffer[];
114
+ /** Lane mode: the project's full verification commands, run once every child is integrated. */
115
+ final_verification?: string[];
116
+ /** Lane mode: one instruction for an idle Lane session, recorded before it is sent. */
117
+ lane_instruction?: LaneInstructionRequest;
118
+ /** Lane mode, ADR 0018: one breaking Intent revision the coordinator approves under the batch's grant. */
119
+ lane_revision?: LaneRevisionRequest;
110
120
  batchKernel?: Partial<BatchRunnerKernelPort>;
111
121
  batchGit?: BatchRunnerGitPort;
122
+ /** Lane mode: closes an integrated child's Issue; defaults to the GitHub projection. */
123
+ batchTracker?: BatchTrackerPort;
112
124
  readInitiative?: InitiativeObservationReader;
113
125
  confirmBatch?: (details: {
114
126
  title: string;
@@ -116,7 +128,9 @@ export interface PiBatchExecutionOptions {
116
128
  details: string;
117
129
  planDigest: string;
118
130
  signal?: AbortSignal;
119
- }) => Promise<"accept" | "decline" | "cancel">;
131
+ /** ADR 0018: offer the opt-in delegation choice (lane mode only). */
132
+ offerRevisionDelegation?: boolean;
133
+ }) => Promise<"accept" | "accept_delegate" | "decline" | "cancel">;
120
134
  }
121
135
 
122
136
  export type PiBatchExecutionResult =
@@ -148,7 +162,7 @@ export async function executePiUnattendedBatch(
148
162
  if (!reuseOnly && !interactive) return { state: "rejected", ...nonInteractiveRefusal() };
149
163
 
150
164
  // A lane_offers-only call resumes the recorded lane batch; refused before any gate otherwise.
151
- const resolvedParallel = resolveLaneParallel(root, initiativeSlug, options.max_parallel, options.lane_offers);
165
+ const resolvedParallel = resolveLaneParallel(root, initiativeSlug, options.max_parallel, options.lane_offers, options.lane_instruction ?? options.lane_revision);
152
166
  if (resolvedParallel !== undefined) options = { ...options, max_parallel: resolvedParallel };
153
167
 
154
168
  // 1. Host-independent batch preflight: claim ownership, branch availability,
@@ -171,6 +185,12 @@ export async function executePiUnattendedBatch(
171
185
  const budget = preflight.projection.budget;
172
186
  const planDigest = preflight.projection.plan_digest;
173
187
  const recoveryChildren = preflight.projection.recovery_children;
188
+ // A new lane batch whose Lane runtime cannot read the coordinator's Kernel
189
+ // contracts is refused before the gate opens; a match adds nothing.
190
+ if (options.max_parallel !== undefined && !isResuming) {
191
+ const contractRefusal = laneRuntimeContractRefusal(root);
192
+ if (contractRefusal) return batchReason("batch_run_rejected", contractRefusal);
193
+ }
174
194
 
175
195
  // 4. Literal-user gate plus the shared reuse decision, the post-gate
176
196
  // claim/drift cascade, and the Batch Authorization binding. The Host supplies
@@ -198,15 +218,16 @@ export async function executePiUnattendedBatch(
198
218
  }
199
219
  const confirmDetails = {
200
220
  title: `Authorize Unattended Batch: ${facts.initiative_slug}`,
201
- summary: `Initiative: ${facts.initiative_slug}\nBatch branch: ${facts.batch_branch}\nPlan digest: ${facts.plan_digest}\nBudget: max_children=${facts.budget.max_children}, qa_failure_limit=${facts.budget.qa_failure_limit}${options.max_parallel !== undefined ? `\nLane mode: max_parallel=${options.max_parallel}` : ""}`,
221
+ summary: `Initiative: ${facts.initiative_slug}\nBatch branch: ${facts.batch_branch}\nPlan digest: ${facts.plan_digest}\nBudget: max_children=${facts.budget.max_children}, qa_failure_limit=${facts.budget.qa_failure_limit}${options.max_parallel !== undefined ? `\nLane mode: max_parallel=${options.max_parallel}` : ""}${options.max_parallel !== undefined && options.final_verification ? `\nFinal verification: ${options.final_verification.join("; ")}` : ""}`,
202
222
  details: `Ordered children (${facts.children.length}):\n${facts.children.map((child) => ` - ${child.task_id} (${child.slice_id}) [risk: ${child.risk}] [status: ${child.status === "already_settled" ? "completed" : "pending execution"}]`).join("\n")}${facts.excluded.length > 0 ? `\n\nExcluded children:\n${facts.excluded.map((child) => ` - ${child.task_id} (${child.slice_id}): ${child.reason}`).join("\n")}` : ""}${options.max_parallel !== undefined ? `\n\nParallel groups (${facts.parallel_groups.length}):\n${facts.parallel_groups.map((group) => ` - ${group.join(", ")}`).join("\n")}${facts.scope_conflicts.length > 0 ? `\n\nSerialized by overlapping scope:\n${facts.scope_conflicts.map((c) => ` - ${c.task_id} after ${c.overlaps_with.join(", ")}`).join("\n")}` : ""}` : ""}${facts.reuse_blockers.length > 0 ? `\n\nRe-confirmation required: ${facts.reuse_blockers.join(", ")}.\nRecovery: confirm to issue a fresh authorization bound to the current plan and HEAD.` : ""}`,
203
223
  planDigest: facts.plan_digest,
204
224
  signal,
225
+ ...(options.max_parallel !== undefined ? { offerRevisionDelegation: true } : {}),
205
226
  };
206
227
 
207
228
  // The gate settles only on the literal user's answer or the caller's
208
229
  // cancellation signal.
209
- let decision: "accept" | "decline" | "cancel";
230
+ let decision: "accept" | "accept_delegate" | "decline" | "cancel";
210
231
  try {
211
232
  decision = await options.confirmBatch(confirmDetails);
212
233
  } catch (err) {
@@ -223,10 +244,14 @@ export async function executePiUnattendedBatch(
223
244
  if (decision === "decline") {
224
245
  return { kind: "host_rejection", value: batchReason("confirmation_declined") };
225
246
  }
226
- if (decision !== "accept") {
247
+ if (decision !== "accept" && decision !== "accept_delegate") {
227
248
  return { kind: "host_rejection", value: batchReason("confirmation_no_decision") };
228
249
  }
229
- return { kind: "confirmed", request_id: randomUUID() };
250
+ return {
251
+ kind: "confirmed",
252
+ request_id: randomUUID(),
253
+ ...(decision === "accept_delegate" && options.max_parallel !== undefined ? { delegate_revisions: true } : {}),
254
+ };
230
255
  },
231
256
  confirmationRef: ({ batch_id, request_id }) =>
232
257
  piConfirmationRef({
@@ -292,7 +317,16 @@ export async function executePiUnattendedBatch(
292
317
  now,
293
318
  kernel: kernelPort,
294
319
  git: options.batchGit,
295
- ...(options.max_parallel !== undefined ? { max_parallel: options.max_parallel } : {}),
320
+ ...(options.max_parallel !== undefined
321
+ ? {
322
+ max_parallel: options.max_parallel,
323
+ tracker: options.batchTracker ?? createBatchTrackerPort(root, initiativeSlug),
324
+ ...(options.final_verification ? { final_verification: options.final_verification } : {}),
325
+ ...(options.lane_instruction ? { lane_instruction: options.lane_instruction } : {}),
326
+ ...(options.lane_revision ? { lane_revision: options.lane_revision } : {}),
327
+ ...(authorization.revision_delegation !== undefined ? { revision_delegation: authorization.revision_delegation } : {}),
328
+ }
329
+ : {}),
296
330
  ...(options.lane_offers !== undefined ? { lane_offers: options.lane_offers } : {}),
297
331
  });
298
332
 
@@ -403,10 +437,21 @@ export default function (
403
437
  lane_offers: Type.Optional(Type.Array(
404
438
  Type.Object({ task_id: Type.String(), path: Type.String() }, { additionalProperties: false }),
405
439
  )),
440
+ final_verification: Type.Optional(Type.Array(Type.String(), { minItems: 1, maxItems: 8 })),
441
+ lane_instruction: Type.Optional(Type.Object({
442
+ task_id: Type.String(),
443
+ kind: Type.Optional(Type.Union([Type.Literal("instruction"), Type.Literal("correction")])),
444
+ text: Type.String(),
445
+ session_state: Type.Union([Type.Literal("idle"), Type.Literal("working"), Type.Literal("blocked"), Type.Literal("done")]),
446
+ }, { additionalProperties: false })),
447
+ lane_revision: Type.Optional(Type.Object({
448
+ task_id: Type.String(),
449
+ next_intent: Type.Object({}, { additionalProperties: true }),
450
+ }, { additionalProperties: false })),
406
451
  }, { additionalProperties: false }),
407
452
  execute: async (
408
453
  _toolCallId: string,
409
- params: { initiative_slug: string; max_parallel?: number; lane_offers?: Array<{ task_id: string; path: string }> },
454
+ params: { initiative_slug: string; max_parallel?: number; lane_offers?: Array<{ task_id: string; path: string }>; final_verification?: string[]; lane_instruction?: unknown; lane_revision?: unknown },
410
455
  signal: AbortSignal | undefined,
411
456
  _onUpdate: unknown,
412
457
  ctx: ExtensionContext,
@@ -427,12 +472,18 @@ export default function (
427
472
 
428
473
  const maxParallel = parseMaxParallel(params.max_parallel);
429
474
  const laneOffers = parseLaneOffers(params.lane_offers);
475
+ const finalVerification = parseFinalVerification(params.final_verification);
476
+ const laneInstruction = parseLaneInstruction(params.lane_instruction);
477
+ const laneRevision = parseLaneRevision(params.lane_revision);
430
478
 
431
479
  const result = await executePiUnattendedBatch({
432
480
  root: ctx.cwd,
433
481
  initiativeSlug,
434
482
  ...(maxParallel !== undefined ? { max_parallel: maxParallel } : {}),
435
483
  ...(laneOffers !== undefined ? { lane_offers: laneOffers } : {}),
484
+ ...(finalVerification !== undefined ? { final_verification: finalVerification } : {}),
485
+ ...(laneInstruction !== undefined ? { lane_instruction: laneInstruction } : {}),
486
+ ...(laneRevision !== undefined ? { lane_revision: laneRevision } : {}),
436
487
  interactive: ctx.mode === "tui",
437
488
  signal,
438
489
  readInitiative: dependencies.readInitiative,
@@ -461,6 +512,9 @@ export default function (
461
512
  signal: details.signal,
462
513
  actions: [
463
514
  { value: "confirm", label: "Authorize batch run", description: "Start the unattended serial batch run" },
515
+ ...(details.offerRevisionDelegation
516
+ ? [{ value: "confirm_delegate", label: "Authorize and delegate in-Lane revisions", description: REVISION_DELEGATION_TEXT }]
517
+ : []),
464
518
  { value: "decline", label: "Decline batch", description: "Reject this batch authorization; repository and authority stay unchanged" },
465
519
  { value: "cancel", label: "Cancel", description: "Leave repository and authority unchanged" },
466
520
  ],
@@ -573,7 +627,8 @@ export default function (
573
627
  ],
574
628
  },
575
629
  );
576
- return mapDialogSelection(selected);
630
+ const mapped = mapDialogSelection(selected);
631
+ return mapped === "accept_delegate" ? "accept" : mapped;
577
632
  },
578
633
  });
579
634
 
@@ -40,6 +40,13 @@ about the fix's scope, not a new bug. A trigger the accepted contract explicitly
40
40
  excludes is reported as an advisory note against that boundary, never as blocking
41
41
  rework.
42
42
 
43
+ A defect that lies inside the TaskIntent goal and `scope_hint` is a blocking
44
+ finding even when no acceptance assertion names its exact case word for word:
45
+ cite the acceptance whose principle it violates as `acceptance_id` and
46
+ `violated.ref`. Ask for the fix and a test that pins the case, never for the case
47
+ to be written into the acceptance text; a missing literal clause is not itself a
48
+ finding and does not call for an Intent revision.
49
+
43
50
  ## Invariant and Evidence Coverage
44
51
 
45
52
  Judge whether the change actually closes the invariant its acceptance names,
@@ -59,6 +66,14 @@ seam, or a seam silently replaced by a different one, is a finding carrying
59
66
  named, its absence is not a finding: candidates written before this rule carries
60
67
  no defect for lacking one.
61
68
 
69
+ When the Spec referenced by an acceptance maps an `automated` user scenario to
70
+ it, check that the delivered tests at that acceptance's agreed seam assert the
71
+ scenario's `Then`: a scenario whose observable result is asserted nowhere is a
72
+ finding carrying `violated.kind` of `acceptance` with `ref` the acceptance id.
73
+ A `manual` scenario, and an acceptance with no mapped scenario, produce no such
74
+ finding. This adds no finding kind and no verdict branch: it is the same
75
+ `acceptance` finding the seam rule above already defines.
76
+
62
77
  Before pass, enumerate the immutable change set and read each complete per-file
63
78
  diff, including deletions. Page bounded reads until every hunk is visible; a
64
79
  truncated aggregate output or summary is incomplete coverage. Trace changed