immune-brain 4.4.1 → 4.6.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.
- package/README.md +3 -2
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +0 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +47 -2
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +23 -18
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -3
- package/plugins/immune-brain/dist/BASELINE.md +18 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +1401 -1101
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -0
- package/plugins/immune-brain/dist/imm-brainstorm.md +21 -0
- package/plugins/immune-brain/dist/imm-loop.md +52 -6
- package/plugins/immune-brain/dist/imm-planner.md +102 -13
- package/plugins/immune-brain/dist/role-prompts/code-review.md +19 -0
- package/plugins/immune-brain/dist/role-prompts/qa.md +13 -1
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +55 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +73 -27
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +3 -4
- package/plugins/immune-brain/runtime/claude/review_host.ts +15 -0
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +956 -634
- package/plugins/immune-brain/runtime/kernel/application.ts +2 -5
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +1 -2
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +21 -12
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
- package/plugins/immune-brain/runtime/kernel/storage.ts +8 -3
- package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
- package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +19 -0
- package/plugins/immune-brain/runtime/prompts/qa.md +13 -1
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +9 -1
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +152 -46
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +193 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +112 -122
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +57 -16
- package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
- package/plugins/immune-brain/runtime/v4_runtime.ts +54 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +75 -12
- package/plugins/immune-brain/skills/BASELINE.md +18 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +5 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -2
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +10 -1
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
|
@@ -44,7 +44,6 @@ import { preparePiCanary, revalidatePiCanary } from "../kernel/pi_canary_prepare
|
|
|
44
44
|
import { runDeterministicQa } from "../assurance/qa";
|
|
45
45
|
import { taskDiffIdentity, taskRevisionIdentity } from "../workspace_scope";
|
|
46
46
|
import { batchReason } from "../unattended/batch_reasons";
|
|
47
|
-
import { startConfirmationDeadline } from "../unattended/confirmation_deadline";
|
|
48
47
|
import { deriveAuthorizationOperation } from "../authorization_operation";
|
|
49
48
|
import { LITERAL_USER_ACTOR_ID, canonicalActorId } from "../kernel/actor_identity";
|
|
50
49
|
import {
|
|
@@ -59,6 +58,7 @@ import {
|
|
|
59
58
|
projectBatchPreflight,
|
|
60
59
|
readActiveClaimTaskId,
|
|
61
60
|
isOwnBatchClaim,
|
|
61
|
+
findResumableBatchSlugForTask,
|
|
62
62
|
} from "../unattended/batch_preflight";
|
|
63
63
|
import {
|
|
64
64
|
startBatch,
|
|
@@ -391,7 +391,6 @@ async function mintCapability(
|
|
|
391
391
|
diff_hash: input.diff_hash,
|
|
392
392
|
actor_id: input.actor_id,
|
|
393
393
|
confirmation_ref: input.confirmation_ref,
|
|
394
|
-
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
395
394
|
findings_digest: input.action_kind === "request_rework"
|
|
396
395
|
? findingsDigestV2(input.findings as TaskFinding[])
|
|
397
396
|
: null,
|
|
@@ -587,7 +586,6 @@ export class ClaudeRuntime {
|
|
|
587
586
|
preparation_digest: preparation.digest,
|
|
588
587
|
actor_id: LITERAL_USER_ACTOR_ID,
|
|
589
588
|
confirmation_ref: gate.confirmation_ref,
|
|
590
|
-
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
591
589
|
nonce,
|
|
592
590
|
};
|
|
593
591
|
const capability = this.enrollmentRegistry.issue(binding);
|
|
@@ -611,20 +609,65 @@ export class ClaudeRuntime {
|
|
|
611
609
|
}
|
|
612
610
|
}
|
|
613
611
|
|
|
614
|
-
async advance(taskId: string, signal?: AbortSignal) {
|
|
615
|
-
return this.
|
|
612
|
+
async advance(taskId: string, signal?: AbortSignal, meta?: ToolMeta) {
|
|
613
|
+
return this.closeOutBatchChild(
|
|
616
614
|
taskId,
|
|
617
|
-
await this.coordinator.advance(taskId, { cwd: this.cwd }, signal),
|
|
615
|
+
await this.withTerminalTracker(taskId, await this.coordinator.advance(taskId, { cwd: this.cwd }, signal)),
|
|
616
|
+
meta,
|
|
618
617
|
);
|
|
619
618
|
}
|
|
620
619
|
|
|
621
|
-
async submitReview(taskId: string, verdictInput: unknown) {
|
|
622
|
-
return this.
|
|
620
|
+
async submitReview(taskId: string, verdictInput: unknown, meta?: ToolMeta) {
|
|
621
|
+
return this.closeOutBatchChild(
|
|
623
622
|
taskId,
|
|
624
|
-
await
|
|
623
|
+
await this.withTerminalTracker(
|
|
624
|
+
taskId,
|
|
625
|
+
await submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput),
|
|
626
|
+
),
|
|
627
|
+
meta,
|
|
625
628
|
);
|
|
626
629
|
}
|
|
627
630
|
|
|
631
|
+
/**
|
|
632
|
+
* Gate-free closeout: a batch child that reached Kernel `done` in the
|
|
633
|
+
* foreground re-enters the batch in the same call, reusing its authorization.
|
|
634
|
+
* Only `done` continues; a stopped child, a parked child and a live Review
|
|
635
|
+
* obligation end where they do today. The continuation is transport: the
|
|
636
|
+
* Kernel result is returned unchanged, and any failure is reported beside it
|
|
637
|
+
* with one retry action, never thrown into a path that could read as "the
|
|
638
|
+
* mutation did not happen".
|
|
639
|
+
*/
|
|
640
|
+
private async closeOutBatchChild<T>(taskId: string, result: T, meta?: ToolMeta): Promise<T> {
|
|
641
|
+
if (!meta || result === null || typeof result !== "object") return result;
|
|
642
|
+
const lifecycle = (result as { record?: { lifecycle?: unknown } }).record?.lifecycle;
|
|
643
|
+
if ((result as { state?: unknown }).state !== "completed" && lifecycle !== "done") return result;
|
|
644
|
+
let slug: string | null;
|
|
645
|
+
try {
|
|
646
|
+
slug = findResumableBatchSlugForTask(this.cwd, taskId);
|
|
647
|
+
} catch {
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
if (!slug) return result;
|
|
651
|
+
try {
|
|
652
|
+
// The batch commit takes exactly the scope envelope plus this task's own
|
|
653
|
+
// audit evidence, and its preflight refuses unstaged changes: stage only
|
|
654
|
+
// that evidence, the step an Executor otherwise does by hand.
|
|
655
|
+
if (existsSync(join(this.cwd, ".imm", "audit", taskId)))
|
|
656
|
+
execFileSync("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
|
|
657
|
+
const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
|
|
658
|
+
return { ...(result as object), batch } as T;
|
|
659
|
+
} catch (error) {
|
|
660
|
+
return {
|
|
661
|
+
...(result as object),
|
|
662
|
+
batch: {
|
|
663
|
+
state: "rejected",
|
|
664
|
+
reason: `batch continuation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
665
|
+
recovery_action: "call start_unattended_batch with the same Initiative to continue the batch",
|
|
666
|
+
},
|
|
667
|
+
} as T;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
628
671
|
/**
|
|
629
672
|
* Post-settlement GitHub tracker projection: the same shared step the Pi Host
|
|
630
673
|
* runs, so an opted-in terminal projection no longer depends on which Host
|
|
@@ -987,15 +1030,18 @@ export class ClaudeRuntime {
|
|
|
987
1030
|
async startUnattendedBatch(
|
|
988
1031
|
initiativeSlug: string,
|
|
989
1032
|
meta: ToolMeta,
|
|
1033
|
+
options: { reuseOnly?: boolean } = {},
|
|
990
1034
|
): Promise<ClaudeBatchStartResult> {
|
|
991
1035
|
throwIfCancelled(meta.signal);
|
|
1036
|
+
const reuseOnly = options.reuseOnly === true;
|
|
992
1037
|
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
993
1038
|
if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
1039
|
+
// A reuse-only continuation never opens a gate, so it needs no confirmation port.
|
|
994
1040
|
const interactive = meta.interactive ?? this.interactive;
|
|
995
|
-
if (!interactive) throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
1041
|
+
if (!reuseOnly && !interactive) throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
996
1042
|
// The text is table-owned; only the form (a thrown native error rather than
|
|
997
1043
|
// a returned envelope) is this Host's.
|
|
998
|
-
if (!this.requestConfirmation)
|
|
1044
|
+
if (!reuseOnly && !this.requestConfirmation)
|
|
999
1045
|
throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
|
|
1000
1046
|
|
|
1001
1047
|
// 1. Host-independent batch preflight: claim ownership, branch
|
|
@@ -1022,7 +1068,7 @@ export class ClaudeRuntime {
|
|
|
1022
1068
|
const budget = preflight.projection.budget;
|
|
1023
1069
|
const planDigest = preflight.projection.plan_digest;
|
|
1024
1070
|
const recoveryChildren = preflight.projection.recovery_children;
|
|
1025
|
-
// 5. Literal-user gate plus the shared reuse
|
|
1071
|
+
// 5. Literal-user gate plus the shared reuse decision, the post-gate
|
|
1026
1072
|
// claim/drift cascade, and the Batch Authorization binding. The Host supplies
|
|
1027
1073
|
// only its gate, its confirmation reference, and its binding nonce.
|
|
1028
1074
|
const authorization = await authorizeBatch<ClaudeBatchStartResult>({
|
|
@@ -1033,9 +1079,19 @@ export class ClaudeRuntime {
|
|
|
1033
1079
|
readInitiative: this.readInitiative ?? observeGithubInitiative,
|
|
1034
1080
|
nonce: enrollmentNonce(),
|
|
1035
1081
|
gate: async (facts) => {
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
|
|
1082
|
+
// A continuation after a foreground child never opens a gate: when the
|
|
1083
|
+
// authorization cannot be reused, the user re-enters the batch tool.
|
|
1084
|
+
if (reuseOnly)
|
|
1085
|
+
return {
|
|
1086
|
+
kind: "host_rejection",
|
|
1087
|
+
value: {
|
|
1088
|
+
state: "blocked",
|
|
1089
|
+
reason: `batch authorization cannot be reused (${facts.reuse_blockers.join(", ")})`,
|
|
1090
|
+
recovery_action: "call start_unattended_batch with the same Initiative to confirm a fresh authorization",
|
|
1091
|
+
},
|
|
1092
|
+
};
|
|
1093
|
+
// The gate settles only on the literal user's answer or the caller's
|
|
1094
|
+
// cancellation signal.
|
|
1039
1095
|
let confirmationResult: { decision: NativeDecision; requestId: string };
|
|
1040
1096
|
try {
|
|
1041
1097
|
confirmationResult = await this.requestConfirmation!({
|
|
@@ -1057,7 +1113,6 @@ export class ClaudeRuntime {
|
|
|
1057
1113
|
reason: child.reason,
|
|
1058
1114
|
})),
|
|
1059
1115
|
budget: facts.budget,
|
|
1060
|
-
expires_at: facts.expires_at,
|
|
1061
1116
|
...(facts.reuse_blockers.length > 0
|
|
1062
1117
|
? {
|
|
1063
1118
|
re_confirmation_required: facts.reuse_blockers,
|
|
@@ -1065,10 +1120,9 @@ export class ClaudeRuntime {
|
|
|
1065
1120
|
}
|
|
1066
1121
|
: {}),
|
|
1067
1122
|
},
|
|
1068
|
-
signal:
|
|
1123
|
+
signal: meta.signal,
|
|
1069
1124
|
});
|
|
1070
1125
|
} catch (err) {
|
|
1071
|
-
if (deadline.timedOut()) return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
|
|
1072
1126
|
if (meta.signal?.aborted)
|
|
1073
1127
|
return { kind: "host_rejection", value: batchReason("cancelled_before_execution") };
|
|
1074
1128
|
if (err instanceof NativeAuthorityError) {
|
|
@@ -1088,13 +1142,8 @@ export class ClaudeRuntime {
|
|
|
1088
1142
|
kind: "host_rejection",
|
|
1089
1143
|
value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err)),
|
|
1090
1144
|
};
|
|
1091
|
-
} finally {
|
|
1092
|
-
deadline.clear();
|
|
1093
1145
|
}
|
|
1094
1146
|
|
|
1095
|
-
// A transport that answers "cancel" on abort is still a timeout, not a user
|
|
1096
|
-
// decision: the Host that owns the transport reports which one it was.
|
|
1097
|
-
if (deadline.timedOut()) return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
|
|
1098
1147
|
if (confirmationResult.decision === "cancel" && meta.signal?.aborted)
|
|
1099
1148
|
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
1100
1149
|
if (meta.signal?.aborted)
|
|
@@ -1122,14 +1171,12 @@ export class ClaudeRuntime {
|
|
|
1122
1171
|
});
|
|
1123
1172
|
if (authorization.outcome === "host_rejection") return authorization.value;
|
|
1124
1173
|
if (authorization.outcome === "rejected") return authorization.rejection;
|
|
1125
|
-
const { binding, batch_id: batchId
|
|
1174
|
+
const { binding, batch_id: batchId } = authorization;
|
|
1126
1175
|
|
|
1127
1176
|
// review-1: verify cancellation signal right before authority issuance and startBatch
|
|
1128
1177
|
if (meta.signal?.aborted) return batchReason("cancelled_before_execution");
|
|
1129
1178
|
|
|
1130
1179
|
now = new Date().toISOString();
|
|
1131
|
-
if (Date.parse(expiresAt) <= Date.parse(now))
|
|
1132
|
-
return batchReason("confirmation_failed", "confirmed batch deadline expired before authorization issuance");
|
|
1133
1180
|
const capability = this.batchRegistry.issue(binding, recoveryChildren as any, now);
|
|
1134
1181
|
|
|
1135
1182
|
const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
|
|
@@ -1153,7 +1200,6 @@ export class ClaudeRuntime {
|
|
|
1153
1200
|
plan_digest: planDigest,
|
|
1154
1201
|
base_head: binding.base_head,
|
|
1155
1202
|
confirmation_time: now,
|
|
1156
|
-
authorization_expires_at: expiresAt,
|
|
1157
1203
|
budget,
|
|
1158
1204
|
now,
|
|
1159
1205
|
kernel: kernelPort,
|
|
@@ -160,10 +160,10 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
160
160
|
signal: meta.signal,
|
|
161
161
|
};
|
|
162
162
|
if (name === "enroll") return runtime.enroll(taskId, toolMeta);
|
|
163
|
-
if (name === "advance_assurance") return runtime.advance(taskId, toolMeta.signal);
|
|
163
|
+
if (name === "advance_assurance") return runtime.advance(taskId, toolMeta.signal, toolMeta);
|
|
164
164
|
if (name === "submit_review") {
|
|
165
165
|
if (!Object.hasOwn(args, "verdict")) throw new Error("verdict is required");
|
|
166
|
-
return runtime.submitReview(taskId, args.verdict);
|
|
166
|
+
return runtime.submitReview(taskId, args.verdict, toolMeta);
|
|
167
167
|
}
|
|
168
168
|
if (name === "revise_intent") {
|
|
169
169
|
if (!Object.hasOwn(args, "next_intent")) throw new Error("next_intent is required");
|
|
@@ -369,8 +369,7 @@ export function elicitationParams(input: NativeConfirmationInput) {
|
|
|
369
369
|
input.planDigest ? `Plan digest: ${input.planDigest}` : null,
|
|
370
370
|
b?.children ? `Ordered children (${b.children.length}):\n${b.children.map((c) => ` - ${c.task_id} (${c.slice_id}) [risk: ${c.risk ?? "unknown"}]`).join("\n")}` : null,
|
|
371
371
|
b?.excluded && b.excluded.length > 0 ? `Excluded children (${b.excluded.length}):\n${b.excluded.map((e) => ` - ${e.task_id} (${e.slice_id}): ${e.reason}`).join("\n")}` : null,
|
|
372
|
-
b?.budget ? `Budget: max_children=${b.budget.max_children},
|
|
373
|
-
b?.expires_at ? `Expires at: ${b.expires_at}` : null,
|
|
372
|
+
b?.budget ? `Budget: max_children=${b.budget.max_children}, qa_failure_limit=${b.budget.qa_failure_limit}` : null,
|
|
374
373
|
].filter(Boolean);
|
|
375
374
|
return {
|
|
376
375
|
mode: "form",
|
|
@@ -169,12 +169,21 @@ export function parseAsyncAgentLaunch(result: string): AsyncAgentLaunch | null {
|
|
|
169
169
|
return { agentId, outputFile };
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
const HANDBACK_TOOL = "SubagentHandback";
|
|
173
|
+
|
|
172
174
|
/**
|
|
173
175
|
* Extract the reviewer's terminal message from its own transcript.
|
|
174
176
|
*
|
|
175
177
|
* Each record carries the `agentId` that wrote it, so the caller's independently
|
|
176
178
|
* observed id is matched per record rather than trusted for the file as a whole;
|
|
177
179
|
* a transcript that interleaves another agent cannot contribute its text.
|
|
180
|
+
*
|
|
181
|
+
* A subagent ends its run either with an assistant text message or, on Hosts
|
|
182
|
+
* that hand the report back through a tool, with a `SubagentHandback` call whose
|
|
183
|
+
* `input.message` is that same report and which leaves no text block behind.
|
|
184
|
+
* Both are the reviewer's own record in its own transcript, so whichever the
|
|
185
|
+
* observed agent wrote last is its result — across records and across the
|
|
186
|
+
* ordered content blocks of a single record.
|
|
178
187
|
*/
|
|
179
188
|
export function readAgentTranscriptResult(transcript: string, agentId: string): string | null {
|
|
180
189
|
let last: string | null = null;
|
|
@@ -192,6 +201,12 @@ export function readAgentTranscriptResult(transcript: string, agentId: string):
|
|
|
192
201
|
if (!block || typeof block !== "object") continue;
|
|
193
202
|
const part = block as Record<string, unknown>;
|
|
194
203
|
if (part.type === "text" && typeof part.text === "string") text += part.text;
|
|
204
|
+
if (part.type === "tool_use" && part.name === HANDBACK_TOOL && part.input && typeof part.input === "object") {
|
|
205
|
+
const report = (part.input as Record<string, unknown>).message;
|
|
206
|
+
// Blocks are ordered, so text before the report is a preamble and
|
|
207
|
+
// only text written after it can supersede it.
|
|
208
|
+
if (typeof report === "string" && report.trim()) { last = report; text = ""; }
|
|
209
|
+
}
|
|
195
210
|
}
|
|
196
211
|
if (text.trim()) last = text;
|
|
197
212
|
}
|