immune-brain 5.1.1 → 5.2.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 +13 -6
- package/README.zh-CN.md +12 -5
- 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 +14 -2
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +93 -0
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +81 -13
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +20 -0
- package/plugins/immune-brain/dist/BASELINE.md +1 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +8573 -7449
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +6 -6
- package/plugins/immune-brain/dist/imm-doc-prune.md +2 -1
- package/plugins/immune-brain/dist/imm-planner.md +63 -37
- package/plugins/immune-brain/dist/imm-run.md +137 -12
- package/plugins/immune-brain/dist/imm-test-prune.md +233 -0
- package/plugins/immune-brain/dist/registry.yaml +9 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +20 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +15 -0
- package/plugins/immune-brain/hooks/hooks.json +31 -1
- package/plugins/immune-brain/runtime/assurance/active_task_notice.ts +42 -0
- package/plugins/immune-brain/runtime/assurance/alias_file.ts +24 -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/assurance/phase_entry.ts +143 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +10 -1
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +32 -6
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +210 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +85 -7
- package/plugins/immune-brain/runtime/claude/review_host.ts +6 -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 +20 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +15 -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/BASELINE.md +1 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +3 -2
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -6
- package/plugins/immune-brain/skills/imm-run/SKILL.md +2 -1
- package/plugins/immune-brain/skills/imm-test-prune/SKILL.md +28 -0
- package/plugins/immune-brain/skills/registry.yaml +9 -0
|
@@ -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
|
+
}
|