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.
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +20 -0
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +66 -11
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +15 -0
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +8256 -7417
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +4 -4
- package/plugins/immune-brain/dist/imm-planner.md +37 -4
- package/plugins/immune-brain/dist/imm-run.md +123 -9
- package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +22 -1
- package/plugins/immune-brain/runtime/assurance/active_task_notice.ts +42 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +9 -0
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +4 -1
- package/plugins/immune-brain/runtime/claude/interaction.ts +5 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +32 -6
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +33 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +66 -6
- package/plugins/immune-brain/runtime/commands/kernel.ts +54 -1
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +235 -7
- package/plugins/immune-brain/runtime/kernel/runtime_contracts.json +7 -0
- package/plugins/immune-brain/runtime/kernel/runtime_contracts.ts +75 -0
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +7 -0
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +160 -0
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +72 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +396 -23
- package/plugins/immune-brain/runtime/unattended/batch_leak_restore.ts +131 -0
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +25 -4
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +3 -2
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +42 -0
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +40 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +155 -0
- package/plugins/immune-brain/runtime/unattended/lane_workspace.ts +58 -0
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +1 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -1
package/package.json
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|