immune-brain 2.8.3 → 3.0.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 (66) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/package.json +9 -2
  5. package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
  6. package/plugins/immune-brain/.mcp.json +8 -0
  7. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
  9. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
  10. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
  11. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
  12. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
  13. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
  14. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
  15. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
  16. package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
  17. package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
  18. package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
  19. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
  20. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  21. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
  22. package/plugins/immune-brain/dist/imm-loop.md +6 -7
  23. package/plugins/immune-brain/dist/imm-planner.md +1 -1
  24. package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
  25. package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
  26. package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
  27. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
  28. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
  29. package/plugins/immune-brain/hooks/hooks.json +55 -0
  30. package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
  31. package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
  32. package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
  33. package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
  34. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
  35. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
  36. package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
  37. package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
  38. package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
  39. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
  40. package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
  41. package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
  42. package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
  43. package/plugins/immune-brain/runtime/github_issue_tracker.ts +2 -2
  44. package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
  45. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
  46. package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
  47. package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
  48. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
  49. package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
  50. package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
  51. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
  52. package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
  53. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
  54. package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
  55. package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
  56. package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
  57. package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
  58. package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
  59. package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
  60. package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
  61. package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
  62. package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
  63. package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
  64. package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
  65. package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
  66. package/plugins/immune-brain/skills/imm-planner/SKILL.md +2 -0
@@ -0,0 +1,233 @@
1
+ // Host-neutral verification descriptor runner.
2
+ //
3
+ // The strict canonical parser for `assurance_kernel/verification_descriptor/v1`
4
+ // is `runtime/verification_descriptor.ts`. This module binds the frozen Bun
5
+ // runner, compatibility gate, fixed execution, and findings digest.
6
+
7
+ import { createHash } from "node:crypto";
8
+ import { execFileSync, spawn } from "node:child_process";
9
+ import { realpathSync, statSync } from "node:fs";
10
+ import { isAbsolute, join, resolve, sep, relative } from "node:path";
11
+
12
+
13
+ export {
14
+ VERIFICATION_DESCRIPTOR_CONTRACT,
15
+ parseVerificationDescriptor,
16
+ canonicalDescriptorBytes,
17
+ VerificationDescriptorError,
18
+ VERIFICATION_DESCRIPTOR_BOUNDS,
19
+ type VerificationDescriptor,
20
+ } from "../verification_descriptor";
21
+ import {
22
+ VERIFICATION_DESCRIPTOR_BOUNDS,
23
+ VerificationDescriptorError,
24
+ type VerificationDescriptor,
25
+ } from "../verification_descriptor";
26
+
27
+ /** Host-owned frozen runner identity: absolute realpath, device/inode, content hash, version. */
28
+ export interface FrozenRunner {
29
+ runner_id: "bun";
30
+ path: string;
31
+ dev: number;
32
+ ino: number;
33
+ content_hash: string;
34
+ version: string;
35
+ }
36
+
37
+ export function resolveBunRunner(): FrozenRunner {
38
+ let executable: string;
39
+ try {
40
+ executable = execFileSync("which", ["bun"], { encoding: "utf8" }).trim();
41
+ } catch {
42
+ throw new VerificationDescriptorError("bun runner is unavailable on this host");
43
+ }
44
+ let real: string;
45
+ try {
46
+ real = realpathSync(executable);
47
+ } catch {
48
+ throw new VerificationDescriptorError("bun runner realpath is unresolvable");
49
+ }
50
+ let stat: ReturnType<typeof statSync>;
51
+ try {
52
+ stat = statSync(real);
53
+ } catch {
54
+ throw new VerificationDescriptorError("bun runner is unreadable");
55
+ }
56
+ if (!stat.isFile())
57
+ throw new VerificationDescriptorError("bun runner is not a regular file");
58
+ const { readFileSync } = require("node:fs") as typeof import("node:fs");
59
+ const contentHash = `sha256:${createHash("sha256").update(readFileSync(real)).digest("hex")}`;
60
+ let version = "";
61
+ try {
62
+ version = execFileSync(real, ["--version"], { encoding: "utf8" }).trim();
63
+ } catch {
64
+ throw new VerificationDescriptorError("bun runner version is unreadable");
65
+ }
66
+ return {
67
+ runner_id: "bun",
68
+ path: real,
69
+ dev: stat.dev,
70
+ ino: stat.ino,
71
+ content_hash: contentHash,
72
+ version,
73
+ };
74
+ }
75
+
76
+ /** The descriptor's claimed runner version must equal the frozen host runner version. */
77
+ export function assertRunnerCompatible(
78
+ descriptor: VerificationDescriptor,
79
+ runner: FrozenRunner,
80
+ ): void {
81
+ if (descriptor.runner_version !== runner.version)
82
+ throw new VerificationDescriptorError(
83
+ `frozen runner version mismatch: descriptor claims ${descriptor.runner_version}, host has ${runner.version}; assurance unavailable`,
84
+ );
85
+ }
86
+
87
+ /** Raised when the host cancels a running fixed verification. */
88
+ export class VerificationAbortedError extends Error {
89
+ constructor() {
90
+ super("fixed verification aborted");
91
+ this.name = "VerificationAbortedError";
92
+ }
93
+ }
94
+
95
+ /** Run the frozen runner with the descriptor argv/cwd under minimal environment. */
96
+ export async function runFixedVerification(
97
+ root: string,
98
+ descriptor: VerificationDescriptor,
99
+ runner: FrozenRunner,
100
+ options: { signal?: AbortSignal } = {},
101
+ ): Promise<{ exit_code: number; stdout: string; stderr: string; timed_out: boolean }> {
102
+ const canonicalRoot = resolve(root);
103
+ const cwd = resolve(canonicalRoot, descriptor.cwd);
104
+ if (cwd === canonicalRoot ? false : !relative(canonicalRoot, cwd).startsWith(".") === false)
105
+ throw new VerificationDescriptorError("verification cwd escapes the repository");
106
+ if (isAbsolute(descriptor.cwd) || relative(canonicalRoot, cwd).startsWith(`..${sep}`) || cwd === resolve(canonicalRoot, ".."))
107
+ throw new VerificationDescriptorError("verification cwd escapes the repository");
108
+ if (options.signal?.aborted) throw new VerificationAbortedError();
109
+ if (process.platform === "win32")
110
+ throw new VerificationDescriptorError("fixed verification process-group isolation requires a POSIX host");
111
+ const maxOutput = Math.min(descriptor.max_output_bytes, VERIFICATION_DESCRIPTOR_BOUNDS.max_output_bytes);
112
+ return new Promise((resolvePromise, rejectPromise) => {
113
+ let settled = false;
114
+ let stdout = Buffer.alloc(0);
115
+ let stderr = Buffer.alloc(0);
116
+ let capturedBytes = 0;
117
+ const child = spawn(runner.path, descriptor.argv, {
118
+ cwd,
119
+ env: { PATH: process.env.PATH ?? "", HOME: process.env.HOME ?? "" },
120
+ detached: process.platform !== "win32",
121
+ stdio: ["ignore", "pipe", "pipe"],
122
+ });
123
+
124
+ const killTree = () => {
125
+ if (child.pid === undefined) return;
126
+ try {
127
+ if (process.platform !== "win32") process.kill(-child.pid, "SIGKILL");
128
+ else child.kill("SIGKILL");
129
+ } catch {
130
+ // The process already exited.
131
+ }
132
+ };
133
+ const cleanup = () => {
134
+ clearTimeout(timeout);
135
+ options.signal?.removeEventListener("abort", onAbort);
136
+ };
137
+ const resolveOnce = (result: { exit_code: number; stdout: string; stderr: string; timed_out: boolean }) => {
138
+ if (settled) return;
139
+ settled = true;
140
+ cleanup();
141
+ resolvePromise(result);
142
+ };
143
+ const rejectOnce = (error: Error) => {
144
+ if (settled) return;
145
+ settled = true;
146
+ cleanup();
147
+ rejectPromise(error);
148
+ };
149
+ const appendBounded = (current: Buffer<ArrayBuffer>, chunk: Buffer<ArrayBuffer>): { content: Buffer<ArrayBuffer>; exceeded: boolean } => {
150
+ const available = Math.max(0, maxOutput - capturedBytes);
151
+ const accepted = chunk.subarray(0, available);
152
+ capturedBytes += accepted.length;
153
+ return {
154
+ content: accepted.length > 0 ? Buffer.concat([current, accepted]) : current,
155
+ exceeded: chunk.length > available,
156
+ };
157
+ };
158
+ const outputLimitExceeded = () => {
159
+ const marker = Buffer.from("verification output limit exceeded\n", "utf8");
160
+ stdout = stdout.subarray(0, Math.max(0, maxOutput - marker.length));
161
+ stderr = marker.subarray(0, maxOutput - stdout.length);
162
+ capturedBytes = stdout.length + stderr.length;
163
+ killTree();
164
+ child.stdout?.destroy();
165
+ child.stderr?.destroy();
166
+ resolveOnce({
167
+ exit_code: 1,
168
+ stdout: stdout.toString("utf8"),
169
+ stderr: stderr.toString("utf8"),
170
+ timed_out: false,
171
+ });
172
+ };
173
+ child.stdout?.on("data", (chunk: Buffer<ArrayBuffer>) => {
174
+ const appended = appendBounded(stdout, chunk);
175
+ stdout = appended.content;
176
+ if (appended.exceeded) outputLimitExceeded();
177
+ });
178
+ child.stderr?.on("data", (chunk: Buffer<ArrayBuffer>) => {
179
+ const appended = appendBounded(stderr, chunk);
180
+ stderr = appended.content;
181
+ if (appended.exceeded) outputLimitExceeded();
182
+ });
183
+ child.once("error", (error) => {
184
+ resolveOnce({
185
+ exit_code: 1,
186
+ stdout: stdout.toString("utf8"),
187
+ stderr: `${stderr.toString("utf8")}runner spawn failed: ${error.message}`.slice(0, maxOutput),
188
+ timed_out: false,
189
+ });
190
+ });
191
+ child.once("close", (code) => {
192
+ resolveOnce({
193
+ exit_code: code ?? 1,
194
+ stdout: stdout.toString("utf8"),
195
+ stderr: stderr.toString("utf8"),
196
+ timed_out: false,
197
+ });
198
+ });
199
+ const onAbort = () => {
200
+ killTree();
201
+ child.stdout?.destroy();
202
+ child.stderr?.destroy();
203
+ rejectOnce(new VerificationAbortedError());
204
+ };
205
+ options.signal?.addEventListener("abort", onAbort, { once: true });
206
+ const timeout = setTimeout(() => {
207
+ killTree();
208
+ child.stdout?.destroy();
209
+ child.stderr?.destroy();
210
+ resolveOnce({
211
+ exit_code: 1,
212
+ stdout: stdout.toString("utf8"),
213
+ stderr: stderr.toString("utf8"),
214
+ timed_out: true,
215
+ });
216
+ }, descriptor.timeout_ms);
217
+ });
218
+ }
219
+
220
+ /** Normalized findings digest: stable-sorted keys over id/kind/acceptance_id/summary. */
221
+ export function findingsDigest(
222
+ findings: Array<{ id: string; kind: string; acceptance_id: string | null; summary: string }>,
223
+ ): string {
224
+ const normalized = findings.map((f) =>
225
+ JSON.stringify({
226
+ acceptance_id: f.acceptance_id,
227
+ id: f.id,
228
+ kind: f.kind,
229
+ summary: f.summary,
230
+ }),
231
+ );
232
+ return `sha256:${createHash("sha256").update(`[${normalized.join(",")}]`).digest("hex")}`;
233
+ }
@@ -0,0 +1,67 @@
1
+ export const MIN_CLAUDE_CODE_VERSION = "2.1.199";
2
+ export const HOST_ID = "claude-code" as const;
3
+ export const CORE_CONTRACT = "assurance_kernel/host_adapter/claude-code/v1";
4
+ export const SUPPORTED_PLATFORMS = ["darwin", "linux"] as const;
5
+
6
+ export type PermissionMode = "manual" | "acceptEdits" | "auto" | "bypassPermissions" | "dontAsk";
7
+
8
+ export type HostProbe =
9
+ | { ok: true; version: string; permissionMode: PermissionMode; platform: string }
10
+ | { ok: false; reason: string };
11
+
12
+ function parseSemver(value: string): [number, number, number] | null {
13
+ // Prerelease and build suffixes are rejected: an unstable build such as
14
+ // 2.1.199-alpha never satisfies the stable minimum.
15
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(value.trim());
16
+ if (!match) return null;
17
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
18
+ }
19
+
20
+ export function compareSemver(left: string, right: string): number {
21
+ const a = parseSemver(left);
22
+ const b = parseSemver(right);
23
+ if (!a || !b) throw new Error(`invalid semver: ${!a ? left : right}`);
24
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
25
+ }
26
+
27
+ export function parsePermissionMode(raw: unknown): PermissionMode | null {
28
+ if (
29
+ raw === "manual"
30
+ || raw === "acceptEdits"
31
+ || raw === "auto"
32
+ || raw === "bypassPermissions"
33
+ || raw === "dontAsk"
34
+ ) return raw;
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Trusted version sources, in priority order: the MCP handshake clientInfo
40
+ * version announced by the connected Host (hostVersion), then the explicit
41
+ * environment override. An undeclared Host version fails closed.
42
+ */
43
+ export function probeHost(
44
+ env: Record<string, string | undefined> = process.env,
45
+ platform = process.platform,
46
+ hostVersion?: string,
47
+ ): HostProbe {
48
+ const version = hostVersion ?? env.CLAUDE_CODE_VERSION ?? env.CLAUDE_CLI_VERSION;
49
+ if (!version) return { ok: false, reason: "Claude Code version is unavailable" };
50
+ if (!parseSemver(version)) return { ok: false, reason: `Claude Code version is invalid: ${version}` };
51
+ if (compareSemver(version, MIN_CLAUDE_CODE_VERSION) < 0) {
52
+ return {
53
+ ok: false,
54
+ reason: `Claude Code ${version} is below the minimum supported ${MIN_CLAUDE_CODE_VERSION}`,
55
+ };
56
+ }
57
+ if (platform !== "darwin" && platform !== "linux") {
58
+ return { ok: false, reason: `unsupported platform ${platform}; native Windows is out of scope` };
59
+ }
60
+ const rawPermissionMode = env.CLAUDE_CODE_PERMISSION_MODE;
61
+ if (rawPermissionMode !== undefined && rawPermissionMode !== "") {
62
+ const permissionMode = parsePermissionMode(rawPermissionMode);
63
+ if (!permissionMode) return { ok: false, reason: `unsupported permission mode ${rawPermissionMode}` };
64
+ return { ok: true, version, permissionMode, platform };
65
+ }
66
+ return { ok: true, version, permissionMode: "manual", platform };
67
+ }
@@ -0,0 +1,70 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { parsePermissionMode, type PermissionMode } from "./capability";
3
+
4
+ export const PRIVILEGED_OPERATIONS = [
5
+ "enroll",
6
+ "request_authorization",
7
+ "approve_breaking_intent_revision",
8
+ "stop",
9
+ "repair_authority_state",
10
+ ] as const;
11
+
12
+ export type PrivilegedOperation = (typeof PRIVILEGED_OPERATIONS)[number];
13
+ export type NativeDecision = "accept" | "deny" | "cancel";
14
+
15
+ export function isPrivilegedOperation(operation: string): operation is PrivilegedOperation {
16
+ return (PRIVILEGED_OPERATIONS as readonly string[]).includes(operation);
17
+ }
18
+
19
+ export function privilegedAnnotations(): Record<string, unknown> {
20
+ return {
21
+ destructiveHint: true,
22
+ "anthropic/requiresUserInteraction": true,
23
+ };
24
+ }
25
+
26
+ export interface NativeGateInput {
27
+ operation: string;
28
+ permissionMode: PermissionMode | string;
29
+ requiresUserInteraction: boolean;
30
+ interactive: boolean;
31
+ decision?: NativeDecision;
32
+ }
33
+
34
+ export type NativeGateResult =
35
+ | { ok: true }
36
+ | { ok: false; reason: string };
37
+
38
+ export function evaluateNativeGate(input: NativeGateInput): NativeGateResult {
39
+ if (!isPrivilegedOperation(input.operation)) return { ok: true };
40
+ if (!input.interactive) return { ok: false, reason: "non-interactive execution cannot mint authority" };
41
+ const mode = parsePermissionMode(input.permissionMode);
42
+ if (!mode) return { ok: false, reason: `unsupported permission mode ${String(input.permissionMode)}` };
43
+ if (mode === "dontAsk") return { ok: false, reason: "dontAsk cannot mint authority" };
44
+ if (!input.requiresUserInteraction) {
45
+ return { ok: false, reason: "privileged operation requires anthropic/requiresUserInteraction" };
46
+ }
47
+ if (input.decision === "deny") return { ok: false, reason: "native interaction denied" };
48
+ if (input.decision === "cancel") return { ok: false, reason: "native interaction cancelled" };
49
+ if (input.decision !== "accept") return { ok: false, reason: "native interaction missing" };
50
+ return { ok: true };
51
+ }
52
+
53
+ export function confirmationRef(input: {
54
+ sessionId: string;
55
+ toolCallId: string;
56
+ operation: string;
57
+ taskId: string;
58
+ intentRevision?: number;
59
+ intentContentHash?: string;
60
+ bindingDigest?: string;
61
+ }): string {
62
+ return `claude-confirm-${createHash("sha256")
63
+ .update(`${input.sessionId}\0${input.toolCallId}\0${input.operation}\0${input.taskId}\0${input.intentRevision ?? ""}\0${input.intentContentHash ?? ""}\0${input.bindingDigest ?? ""}`)
64
+ .digest("hex")
65
+ .slice(0, 16)}`;
66
+ }
67
+
68
+ export function enrollmentNonce(): string {
69
+ return randomUUID();
70
+ }