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
@@ -0,0 +1,75 @@
1
+ // The contract identifiers one runtime reads and writes in a worktree's
2
+ // Authority Store. A lane-mode batch reads every Lane's Kernel state with the
3
+ // coordinator's own runtime, so the coordinator and the runtime a Lane
4
+ // Executor loads must agree on these identifiers; a plugin version number
5
+ // proves nothing (two different builds can carry the same one).
6
+ //
7
+ // `runtime_contracts.json` is the same set as data, shipped next to this
8
+ // module, so a coordinator can read another runtime's identifiers without
9
+ // executing its code. A test keeps the two in step.
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { TASK_TOMBSTONE_CONTRACT } from "./backend_claim";
14
+ import { KERNEL_STORE_SCHEMA_VERSION } from "./storage_paths";
15
+ import { TASK_RECORD_CONTRACT_V4 } from "./types";
16
+
17
+ export const RUNTIME_CONTRACTS_MANIFEST = "runtime_contracts.json";
18
+
19
+ export interface RuntimeContracts {
20
+ contract: "immune_brain/runtime_contracts/v1";
21
+ task_record: string;
22
+ task_tombstone: string;
23
+ assurance_projection: string;
24
+ kernel_store_schema: number;
25
+ }
26
+
27
+ /** The identifiers this running runtime supports. */
28
+ export const RUNTIME_CONTRACTS: RuntimeContracts = {
29
+ contract: "immune_brain/runtime_contracts/v1",
30
+ task_record: TASK_RECORD_CONTRACT_V4,
31
+ task_tombstone: TASK_TOMBSTONE_CONTRACT,
32
+ assurance_projection: "assurance_kernel/assurance_projection/v1",
33
+ kernel_store_schema: KERNEL_STORE_SCHEMA_VERSION,
34
+ };
35
+
36
+ const KEYS = ["task_record", "task_tombstone", "assurance_projection", "kernel_store_schema"] as const;
37
+
38
+ /** Where this running runtime's code was loaded from, for a refusal that names both sides. */
39
+ export function runningRuntimeSource(): string {
40
+ try {
41
+ return dirname(fileURLToPath(import.meta.url));
42
+ } catch {
43
+ return "unknown";
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Read another runtime's identifiers from its manifest. `runtimeDir` is either
49
+ * the `runtime/kernel` directory or a plugin root that contains one. Null when
50
+ * no manifest exists there; a malformed manifest throws.
51
+ */
52
+ export function readRuntimeContracts(runtimeDir: string): { contracts: RuntimeContracts; path: string } | null {
53
+ const candidates = [
54
+ join(runtimeDir, RUNTIME_CONTRACTS_MANIFEST),
55
+ join(runtimeDir, "kernel", RUNTIME_CONTRACTS_MANIFEST),
56
+ join(runtimeDir, "runtime", "kernel", RUNTIME_CONTRACTS_MANIFEST),
57
+ join(runtimeDir, "plugins", "immune-brain", "runtime", "kernel", RUNTIME_CONTRACTS_MANIFEST),
58
+ ];
59
+ const path = candidates.find((candidate) => existsSync(candidate));
60
+ if (!path) return null;
61
+ const raw = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
62
+ if (raw.contract !== "immune_brain/runtime_contracts/v1")
63
+ throw new Error(`${path} is not an immune_brain/runtime_contracts/v1 manifest`);
64
+ for (const key of KEYS) {
65
+ const value = raw[key];
66
+ if (key === "kernel_store_schema" ? !Number.isSafeInteger(value) : typeof value !== "string" || !value)
67
+ throw new Error(`${path} has an invalid ${key}`);
68
+ }
69
+ return { contracts: raw as unknown as RuntimeContracts, path: resolve(path) };
70
+ }
71
+
72
+ /** The identifiers on which two runtimes disagree; empty when they can read each other's state. */
73
+ export function runtimeContractDifferences(a: RuntimeContracts, b: RuntimeContracts): string[] {
74
+ return KEYS.filter((key) => a[key] !== b[key]).map((key) => `${key}: ${String(a[key])} != ${String(b[key])}`);
75
+ }
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/plugin_versioning.ts from the root package.json.
2
- export const PLUGIN_VERSION = "5.1.1" as const;
2
+ export const PLUGIN_VERSION = "5.1.2" as const;
@@ -34,6 +34,13 @@ about the fix's scope, not a new bug. A trigger the accepted contract explicitly
34
34
  excludes is reported as an advisory note against that boundary, never as blocking
35
35
  rework.
36
36
 
37
+ A defect that lies inside the TaskIntent goal and `scope_hint` is a blocking
38
+ finding even when no acceptance assertion names its exact case word for word:
39
+ cite the acceptance whose principle it violates as `acceptance_id` and
40
+ `violated.ref`. Ask for the fix and a test that pins the case, never for the case
41
+ to be written into the acceptance text; a missing literal clause is not itself a
42
+ finding and does not call for an Intent revision.
43
+
37
44
  ## Invariant and Evidence Coverage
38
45
 
39
46
  Judge whether the change actually closes the invariant its acceptance names,
@@ -53,6 +60,14 @@ seam, or a seam silently replaced by a different one, is a finding carrying
53
60
  named, its absence is not a finding: candidates written before this rule carries
54
61
  no defect for lacking one.
55
62
 
63
+ When the Spec referenced by an acceptance maps an `automated` user scenario to
64
+ it, check that the delivered tests at that acceptance's agreed seam assert the
65
+ scenario's `Then`: a scenario whose observable result is asserted nowhere is a
66
+ finding carrying `violated.kind` of `acceptance` with `ref` the acceptance id.
67
+ A `manual` scenario, and an acceptance with no mapped scenario, produce no such
68
+ finding. This adds no finding kind and no verdict branch: it is the same
69
+ `acceptance` finding the seam rule above already defines.
70
+
56
71
  Before pass, enumerate the immutable change set and read each complete per-file
57
72
  diff, including deletions. Page bounded reads until every hunk is visible; a
58
73
  truncated aggregate output or summary is incomplete coverage. Trace changed
@@ -54,6 +54,13 @@ Record missing evidence as blocked and repair it before handoff. The Loop owns
54
54
  verdict submission, finding disposition and the second-rework generalize-or-refute
55
55
  rule.
56
56
 
57
+ A finding whose defect lies inside the TaskIntent goal and `scope_hint` is
58
+ ordinary rework even when no acceptance names its exact case: fix the code and
59
+ add the test that pins the case. Do not propose an Intent revision for it, and
60
+ never append a single input case to an acceptance assertion to close a finding;
61
+ acceptance states principles, tests enumerate cases. Route to `imm-planner` only
62
+ when the goal, the scope, or a user-visible commitment must change.
63
+
57
64
  ## Code Quality Guard
58
65
 
59
66
  Before handoff, check the implementation for real implementation rather than
@@ -0,0 +1,160 @@
1
+ // Delegated in-Lane Intent revision (ADR 0018). A lane batch whose literal-user
2
+ // confirmation granted `revision_delegation` lets the coordinator approve a
3
+ // Lane child's breaking Intent revision that stays inside the child's TaskIntent
4
+ // as authorized for the batch. The approval is the ordinary Kernel
5
+ // `approve_breaking_intent_revision`, applied in the Lane's own Authority Store
6
+ // with user authority whose actor and confirmation reference name the batch, so
7
+ // the audit history tells it apart from an approval by the user in person.
8
+ // Out-of-bounds revisions are refused here and stay with the user's own gate.
9
+ import { execFileSync } from "node:child_process";
10
+ import { join } from "node:path";
11
+ import { writeFileSync } from "node:fs";
12
+ import { projectAssurance } from "../kernel/assurance_projection";
13
+ import { createMutationAuthorityRegistry, digestOfAction, type CapabilityBindingV2 } from "../kernel/authority_port";
14
+ import { capabilityActionFor, createCanaryApplication } from "../kernel/canary_application";
15
+ import { canonicalIntentHash, classifyIntentRevision, parseTaskIntentV1, readTaskIntent } from "../kernel/intent";
16
+ import { boundSpecPath } from "../kernel/spec_binding";
17
+ import { readTaskRecordRaw, recoverKernelStoreFollowUps } from "../kernel/storage";
18
+ import type { TaskIntentV1 } from "../kernel/types";
19
+ import { stagePlanningArtifactTransition } from "../assurance/verdict_authority";
20
+ import { captureStagedIntent, restoreStagedIntent } from "../staged_intent";
21
+ import { taskDeliveryIdentity } from "../workspace_scope";
22
+
23
+ /** The grant and its bounds, stated in the gate that grants it (both Hosts). */
24
+ export const REVISION_DELEGATION_TEXT =
25
+ "Optional, off by default: also let the batch coordinator approve a Lane child's breaking Intent revisions that stay inside that child's authorized TaskIntent (acceptance changes, a narrower scope_hint). Widening scope, changing risk, goal or the bound Spec still needs your own confirmation. The grant ends with this batch.";
26
+
27
+ /** The actor every delegated approval records; never the literal user. */
28
+ export const DELEGATED_ACTOR_ID = "batch-coordinator";
29
+
30
+ export interface RevisionDelegationGrant {
31
+ /** The confirmation that granted it; a fresh confirmation replaces it. */
32
+ confirmation_time: string;
33
+ }
34
+
35
+ export interface DelegatedRevisionRecord {
36
+ at: string;
37
+ task_id: string;
38
+ from_revision: number;
39
+ to_revision: number;
40
+ intent_content_hash: string;
41
+ confirmation_ref: string;
42
+ }
43
+
44
+ export interface LaneRevisionRequest {
45
+ task_id: string;
46
+ next_intent: TaskIntentV1;
47
+ }
48
+
49
+ /** The confirmation reference that traces a delegated approval to its batch authorization. */
50
+ export function delegatedConfirmationRef(batchId: string, grant: RevisionDelegationGrant): string {
51
+ return `delegated-batch:${batchId}@${grant.confirmation_time}`;
52
+ }
53
+
54
+ /** Untrusted tool input: shape only; the bounds are decided against Kernel facts. */
55
+ export function parseLaneRevision(value: unknown): LaneRevisionRequest | undefined {
56
+ if (value === undefined || value === null) return undefined;
57
+ if (typeof value !== "object" || Array.isArray(value)) throw new Error("invalid lane_revision: expected an object");
58
+ const { task_id: taskId, next_intent: nextIntent, ...rest } = value as Record<string, unknown>;
59
+ if (Object.keys(rest).length > 0) throw new Error("invalid lane_revision: unknown field");
60
+ if (typeof taskId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(taskId))
61
+ throw new Error("invalid lane_revision: task_id is not a valid task id");
62
+ const intent = parseTaskIntentV1(nextIntent);
63
+ if (intent.task_id !== taskId) throw new Error("invalid lane_revision: next_intent belongs to another task");
64
+ return { task_id: taskId, next_intent: intent };
65
+ }
66
+
67
+ /**
68
+ * Whether the delegation covers this revision. `authorized` is the child's
69
+ * TaskIntent as the batch authorized it; `current` is the Lane's enrolled one.
70
+ * Null means covered.
71
+ */
72
+ export function delegatedRevisionRefusal(authorized: TaskIntentV1, current: TaskIntentV1, next: TaskIntentV1): string | null {
73
+ if (classifyIntentRevision(current, next) !== "breaking") return "only a breaking revision needs this approval; apply a compatible one with revise_intent";
74
+ if (next.revision <= current.revision) return "the revision number must increase";
75
+ if (next.goal !== authorized.goal || next.owner !== authorized.owner || next.task_id !== authorized.task_id)
76
+ return "goal, owner and task identity are outside the delegation";
77
+ if (next.risk !== authorized.risk || next.risk !== current.risk) return "a risk change is outside the delegation";
78
+ if ((boundSpecPath(next) ?? null) !== (boundSpecPath(authorized) ?? null)) return "a change of the bound Spec is outside the delegation";
79
+ const allowed = new Set(authorized.scope_hint);
80
+ const widened = next.scope_hint.filter((entry) => !allowed.has(entry));
81
+ if (widened.length > 0) return `widening scope_hint is outside the delegation: ${widened.join(", ")}`;
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * Apply one delegated breaking revision in the Lane, exactly as a Host applies
87
+ * an approved one: write and stage the next sidecar, re-read the projection,
88
+ * issue a user capability bound to that snapshot, execute, and stage the
89
+ * planning transition. The prior sidecar bytes and index entry come back on any
90
+ * failure before commit.
91
+ */
92
+ export function applyDelegatedIntentRevision(input: {
93
+ lane_root: string;
94
+ task_id: string;
95
+ next_intent: TaskIntentV1;
96
+ confirmation_ref: string;
97
+ now: string;
98
+ }): Promise<{ from_revision: number; to_revision: number; intent_content_hash: string }> {
99
+ return (async () => {
100
+ const { lane_root: root, task_id: taskId, next_intent: nextIntent, now } = input;
101
+ recoverKernelStoreFollowUps(root, taskId);
102
+ const prior = readTaskIntent(root, taskId, readTaskRecordRaw(root, taskId).record?.intent_ref.path);
103
+ const before = await projectAssurance(root, taskId, taskDeliveryIdentity);
104
+ if (before.error || before.claim?.task_id !== taskId || before.projection.lifecycle !== "active")
105
+ throw new Error(`the Lane does not hold an active run of ${taskId}`);
106
+ const nextHash = canonicalIntentHash(nextIntent);
107
+ const nextRef = { path: `docs/plans/${taskId}.intent.json`, content_hash: nextHash };
108
+ const snapshot = captureStagedIntent(root, prior.intent_ref.path);
109
+ try {
110
+ writeFileSync(join(root, prior.intent_ref.path), `${JSON.stringify(nextIntent, null, 2)}\n`);
111
+ execFileSync("git", ["add", "--", prior.intent_ref.path], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
112
+ const prepared = await projectAssurance(root, taskId, taskDeliveryIdentity);
113
+ if (prepared.error || prepared.projection.record_revision !== before.projection.record_revision)
114
+ throw new Error("the Lane's TaskRecord changed while the delegated revision was prepared");
115
+ const registry = createMutationAuthorityRegistry();
116
+ const action = capabilityActionFor({
117
+ op: "approve_breaking_intent_revision",
118
+ task_id: taskId,
119
+ at: now,
120
+ actor_id: DELEGATED_ACTOR_ID,
121
+ next_intent: nextIntent,
122
+ next_intent_ref: nextRef,
123
+ });
124
+ const binding: CapabilityBindingV2 = {
125
+ authority_kind: "user",
126
+ task_id: taskId,
127
+ ...(prepared.projection.run_id ? { run_id: prepared.projection.run_id } : {}),
128
+ action_digest: digestOfAction(action),
129
+ expected_record_hash: prepared.projection.record_revision,
130
+ intent_revision: nextIntent.revision,
131
+ intent_content_hash: nextHash,
132
+ diff_hash: prepared.projection.diff_hash,
133
+ actor_id: DELEGATED_ACTOR_ID,
134
+ confirmation_ref: input.confirmation_ref,
135
+ findings_digest: null,
136
+ };
137
+ const capability = registry.issue(binding);
138
+ const result = createCanaryApplication(registry).execute({
139
+ root,
140
+ task_id: taskId,
141
+ operation: {
142
+ op: "approve_breaking_intent_revision",
143
+ capability,
144
+ actor_id: DELEGATED_ACTOR_ID,
145
+ next_intent: nextIntent,
146
+ next_intent_ref: nextRef,
147
+ } as never,
148
+ prior_intent_token: prior.token,
149
+ diffProvider: taskDeliveryIdentity,
150
+ now,
151
+ });
152
+ stagePlanningArtifactTransition(root, result.record);
153
+ return { from_revision: prior.intent.revision, to_revision: nextIntent.revision, intent_content_hash: nextHash };
154
+ } catch (error) {
155
+ const current = readTaskRecordRaw(root, taskId);
156
+ if (current.record?.intent_snapshot.revision === prior.intent.revision) restoreStagedIntent(root, snapshot);
157
+ throw error;
158
+ }
159
+ })();
160
+ }
@@ -0,0 +1,72 @@
1
+ // Full verification of the batch branch once every child is integrated. The
2
+ // commands are the project's own convention, resolved by the Parent from its
3
+ // instruction files and recorded on the batch record when the batch starts, so
4
+ // the completing tick runs exactly what the literal user saw at confirmation.
5
+ // A failure marks the completion report as not passed; integrated commits are
6
+ // never rolled back. Only exit status and timing are recorded, never output.
7
+ import { spawn, spawnSync } from "node:child_process";
8
+
9
+ export const MAX_FINAL_VERIFICATION_COMMANDS = 8;
10
+ const MAX_COMMAND_LENGTH = 512;
11
+ const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
12
+
13
+ export interface FinalVerificationResult {
14
+ command: string;
15
+ exit_code: number | null;
16
+ signal: string | null;
17
+ passed: boolean;
18
+ duration_ms: number;
19
+ }
20
+
21
+ export interface FinalVerificationReport {
22
+ passed: boolean;
23
+ head: string | null;
24
+ results: FinalVerificationResult[];
25
+ }
26
+
27
+ /** Untrusted tool input: a short list of plain commands, split on whitespace and never run through a shell. */
28
+ export function parseFinalVerification(value: unknown): string[] | undefined {
29
+ if (value === undefined || value === null) return undefined;
30
+ if (!Array.isArray(value) || value.length === 0 || value.length > MAX_FINAL_VERIFICATION_COMMANDS)
31
+ throw new Error(`invalid final_verification: expected 1 to ${MAX_FINAL_VERIFICATION_COMMANDS} commands`);
32
+ return value.map((command) => {
33
+ if (typeof command !== "string" || !command.trim() || command.length > MAX_COMMAND_LENGTH || /[\0\n\r]/.test(command))
34
+ throw new Error("invalid final_verification: each command must be one non-empty line");
35
+ if (/[|&;<>`$(){}"'\\]/.test(command))
36
+ throw new Error("invalid final_verification: commands run without a shell and are split on spaces, so shell syntax and quoting are not allowed");
37
+ return command.trim().replace(/\s+/g, " ");
38
+ });
39
+ }
40
+
41
+ /** One command, asynchronously, so a long suite never blocks the Host's event loop. */
42
+ function runOne(root: string, command: string, timeoutMs: number): Promise<FinalVerificationResult> {
43
+ const [executable, ...argv] = command.split(" ");
44
+ const started = Date.now();
45
+ return new Promise((resolve) => {
46
+ let settled = false;
47
+ const finish = (exitCode: number | null, signal: string | null) => {
48
+ if (settled) return;
49
+ settled = true;
50
+ clearTimeout(timer);
51
+ resolve({ command, exit_code: exitCode, signal, passed: exitCode === 0, duration_ms: Date.now() - started });
52
+ };
53
+ const child = spawn(executable!, argv, { cwd: root, stdio: "ignore", env: process.env });
54
+ const timer = setTimeout(() => {
55
+ child.kill("SIGKILL");
56
+ finish(null, "timeout");
57
+ }, timeoutMs);
58
+ child.once("error", (error) => finish(null, String((error as NodeJS.ErrnoException).code ?? "spawn_failed")));
59
+ child.once("close", (code, signal) => finish(code, signal));
60
+ });
61
+ }
62
+
63
+ export async function runFinalVerification(
64
+ root: string,
65
+ commands: readonly string[],
66
+ options: { timeoutMs?: number } = {},
67
+ ): Promise<FinalVerificationReport> {
68
+ const head = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
69
+ const results: FinalVerificationResult[] = [];
70
+ for (const command of commands) results.push(await runOne(root, command, options.timeoutMs ?? DEFAULT_TIMEOUT_MS));
71
+ return { passed: results.every((result) => result.passed), head: head.status === 0 ? head.stdout.trim() : null, results };
72
+ }