immune-brain 5.1.2 → 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 +73 -0
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +15 -2
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +5 -0
- package/plugins/immune-brain/dist/BASELINE.md +1 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +425 -140
- package/plugins/immune-brain/dist/imm-brainstorm.md +2 -2
- package/plugins/immune-brain/dist/imm-doc-prune.md +2 -1
- package/plugins/immune-brain/dist/imm-planner.md +26 -33
- package/plugins/immune-brain/dist/imm-run.md +14 -3
- 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 +5 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +8 -0
- package/plugins/immune-brain/hooks/hooks.json +10 -1
- package/plugins/immune-brain/runtime/assurance/alias_file.ts +24 -0
- package/plugins/immune-brain/runtime/assurance/phase_entry.ts +143 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +5 -1
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +180 -3
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +21 -3
- package/plugins/immune-brain/runtime/claude/review_host.ts +6 -6
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +5 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +8 -0
- package/plugins/immune-brain/skills/BASELINE.md +1 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +2 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +5 -5
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { PHASE_ENTRY_RECOVERY } from "../assurance/phase_entry";
|
|
2
3
|
|
|
3
4
|
export const PRIVILEGED_OPERATIONS = [
|
|
4
5
|
"enroll",
|
|
@@ -17,7 +18,9 @@ export type NativeFailureCode =
|
|
|
17
18
|
| "user_cancelled"
|
|
18
19
|
| "correlation_missing"
|
|
19
20
|
| "unsupported_host"
|
|
20
|
-
| "workspace_changed"
|
|
21
|
+
| "workspace_changed"
|
|
22
|
+
| "run_entry_required"
|
|
23
|
+
| "phase_entry_required";
|
|
21
24
|
|
|
22
25
|
const RECOVERY_ACTIONS: Record<NativeFailureCode, string> = {
|
|
23
26
|
interaction_not_opened: "retry through a fresh native gate in the current Host",
|
|
@@ -26,6 +29,7 @@ const RECOVERY_ACTIONS: Record<NativeFailureCode, string> = {
|
|
|
26
29
|
correlation_missing: "retry through a fresh native gate in the current Host",
|
|
27
30
|
unsupported_host: "upgrade to a supported Claude Code version and retry in the current Host",
|
|
28
31
|
workspace_changed: "review the current workspace and retry through a fresh native gate",
|
|
32
|
+
...PHASE_ENTRY_RECOVERY,
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
export class NativeAuthorityError extends Error {
|
|
@@ -3,10 +3,55 @@
|
|
|
3
3
|
// runs, the same rule the Pi extension applies to `edit`/`write`. Bash is not
|
|
4
4
|
// intercepted. Outside a Lane the hook answers nothing and the tool runs as
|
|
5
5
|
// configured.
|
|
6
|
+
//
|
|
7
|
+
// Phase entry gate: UserPromptSubmit records which literal prompt entered
|
|
8
|
+
// planning or execution; PreToolUse denies `enroll`, `start_unattended_batch`
|
|
9
|
+
// and the three phase Skills without a matching entry, and otherwise leaves an
|
|
10
|
+
// admission the tool handler consumes by tool-use id.
|
|
11
|
+
import { lstatSync, rmSync } from "node:fs";
|
|
12
|
+
import { homedir, tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
6
14
|
import { laneWriteRefusal } from "../unattended/lane_workspace";
|
|
7
15
|
import { activeTaskNotice } from "../assurance/active_task_notice";
|
|
16
|
+
import { readAliasFile } from "../assurance/alias_file";
|
|
17
|
+
import {
|
|
18
|
+
admitPhaseOperation,
|
|
19
|
+
NO_PHASE_ENTRY,
|
|
20
|
+
packagedPhaseSkill,
|
|
21
|
+
phaseEntryLogLine,
|
|
22
|
+
phaseRefusalMessage,
|
|
23
|
+
PHASE_ENTRY_RECOVERY,
|
|
24
|
+
recognizePhaseEntry,
|
|
25
|
+
reducePhaseEntryLog,
|
|
26
|
+
RUN_ENTRY_REQUIRED,
|
|
27
|
+
type AliasReader,
|
|
28
|
+
type PhaseEntryState,
|
|
29
|
+
type PhaseOperation,
|
|
30
|
+
} from "../assurance/phase_entry";
|
|
31
|
+
import { readActiveClaimTaskId } from "../unattended/batch_preflight";
|
|
32
|
+
import { appendPrivate, cacheDir, ensurePrivateDir, ownedByUs, readPrivate, sessionHash } from "./review_host";
|
|
8
33
|
|
|
9
34
|
const FILE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
35
|
+
const GATED_MCP_TOOL = /^mcp__.*immune-brain.*__(enroll|start_unattended_batch)$/;
|
|
36
|
+
|
|
37
|
+
export type GatedTool = "enroll" | "start_unattended_batch";
|
|
38
|
+
|
|
39
|
+
/** Where the phase entry gate keeps its private files and finds user aliases. */
|
|
40
|
+
export interface PhaseEntryLocations {
|
|
41
|
+
/** Parent of the private cache directory. Defaults to the OS temp directory. */
|
|
42
|
+
root?: string;
|
|
43
|
+
/** The user's home, for `~/.claude/commands`. */
|
|
44
|
+
home?: string;
|
|
45
|
+
/** The private store's append and read. Default to the real file operations. */
|
|
46
|
+
append?: typeof appendPrivate;
|
|
47
|
+
read?: typeof readPrivate;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function denyPreToolUse(reason: string): string {
|
|
51
|
+
return JSON.stringify({
|
|
52
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
10
55
|
|
|
11
56
|
/** The hook's stdout for one PreToolUse payload, or null to stay silent. */
|
|
12
57
|
export function laneGuardHookOutput(payload: Record<string, unknown>): string | null {
|
|
@@ -15,9 +60,7 @@ export function laneGuardHookOutput(payload: Record<string, unknown>): string |
|
|
|
15
60
|
const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
|
|
16
61
|
const refusal = laneWriteRefusal(cwd, input.file_path ?? input.notebook_path);
|
|
17
62
|
if (!refusal) return null;
|
|
18
|
-
return
|
|
19
|
-
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: refusal },
|
|
20
|
-
});
|
|
63
|
+
return denyPreToolUse(refusal);
|
|
21
64
|
}
|
|
22
65
|
|
|
23
66
|
/**
|
|
@@ -31,3 +74,137 @@ export async function activeTaskHookOutput(payload: Record<string, unknown>): Pr
|
|
|
31
74
|
if (!notice) return null;
|
|
32
75
|
return JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: notice } });
|
|
33
76
|
}
|
|
77
|
+
|
|
78
|
+
function entryLogPath(sessionId: string, root: string): string {
|
|
79
|
+
return join(cacheDir(root), `${sessionHash(sessionId)}.entry`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function admissionPath(toolUseId: string, root: string): string {
|
|
83
|
+
return join(cacheDir(root), `${sessionHash(toolUseId)}.admit`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sessionIdOf(payload: Record<string, unknown>): string {
|
|
87
|
+
return typeof payload.session_id === "string" ? payload.session_id : "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cwdOf(payload: Record<string, unknown>): string {
|
|
91
|
+
return typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Project aliases first, then the user's, as Claude Code resolves commands. */
|
|
95
|
+
function claudeAliasReader(cwd: string, home: string): AliasReader {
|
|
96
|
+
return (name) => {
|
|
97
|
+
for (const dir of [join(cwd, ".claude", "commands"), join(home, ".claude", "commands")]) {
|
|
98
|
+
const body = readAliasFile(join(dir, `${name}.md`));
|
|
99
|
+
if (body !== null) return body;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The session's entry state: none recorded yet, or null when the store is unreadable. */
|
|
106
|
+
export function readPhaseEntryState(sessionId: string, root = tmpdir(), read: typeof readPrivate = readPrivate): PhaseEntryState | null {
|
|
107
|
+
if (!sessionId) return null;
|
|
108
|
+
const path = entryLogPath(sessionId, root);
|
|
109
|
+
try {
|
|
110
|
+
lstatSync(path);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT" ? NO_PHASE_ENTRY : null;
|
|
113
|
+
}
|
|
114
|
+
if (!ensurePrivateDir(cacheDir(root))) return null;
|
|
115
|
+
let text: string | undefined;
|
|
116
|
+
try { text = read(path); } catch { return null; }
|
|
117
|
+
return text === undefined ? null : reducePhaseEntryLog(text);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Removes one of our private files. Never follows a link or touches another owner's file. */
|
|
121
|
+
function dropPrivate(path: string): void {
|
|
122
|
+
try {
|
|
123
|
+
const stat = lstatSync(path);
|
|
124
|
+
if (ownedByUs(stat) && stat.isFile()) rmSync(path, { force: true });
|
|
125
|
+
} catch { /* already gone, or not ours to remove */ }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* UserPromptSubmit: record whether this literal prompt entered a phase. Every
|
|
130
|
+
* prompt is recorded so "the latest prompt" is decidable later. A prompt that
|
|
131
|
+
* could not be recorded drops the log: an earlier entry must not outlive a
|
|
132
|
+
* prompt the store never saw, so the session is back to no entry.
|
|
133
|
+
*/
|
|
134
|
+
export function recordPhaseEntryPrompt(payload: Record<string, unknown>, locations: PhaseEntryLocations = {}): boolean {
|
|
135
|
+
if (payload.hook_event_name !== "UserPromptSubmit") return false;
|
|
136
|
+
const sessionId = sessionIdOf(payload);
|
|
137
|
+
if (!sessionId) return false;
|
|
138
|
+
const root = locations.root ?? tmpdir();
|
|
139
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
140
|
+
const entry = recognizePhaseEntry(prompt, claudeAliasReader(cwdOf(payload), locations.home ?? homedir()));
|
|
141
|
+
const path = entryLogPath(sessionId, root);
|
|
142
|
+
const recorded = (locations.append ?? appendPrivate)(path, cacheDir(root), phaseEntryLogLine(entry));
|
|
143
|
+
if (!recorded) dropPrivate(path);
|
|
144
|
+
return recorded;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function gatedOperation(payload: Record<string, unknown>): PhaseOperation | null {
|
|
148
|
+
const toolName = String(payload.tool_name ?? "");
|
|
149
|
+
if (toolName === "Skill") {
|
|
150
|
+
const input = (payload.tool_input ?? {}) as { skill?: unknown };
|
|
151
|
+
const skill = typeof input.skill === "string" ? packagedPhaseSkill(input.skill) : null;
|
|
152
|
+
return skill ? { kind: "skill", skill } : null;
|
|
153
|
+
}
|
|
154
|
+
const mcp = GATED_MCP_TOOL.exec(toolName);
|
|
155
|
+
return mcp ? { kind: "tool", name: mcp[1] as GatedTool } : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* PreToolUse for a gated tool: deny without a matching entry; for the two MCP
|
|
160
|
+
* tools, otherwise write the admission the handler consumes. Any other tool
|
|
161
|
+
* answers null and runs as configured.
|
|
162
|
+
*/
|
|
163
|
+
export function phaseEntryGateHookOutput(payload: Record<string, unknown>, locations: PhaseEntryLocations = {}): string | null {
|
|
164
|
+
if (payload.hook_event_name !== "PreToolUse") return null;
|
|
165
|
+
const operation = gatedOperation(payload);
|
|
166
|
+
if (!operation) return null;
|
|
167
|
+
const root = locations.root ?? tmpdir();
|
|
168
|
+
let state: PhaseEntryState | null = null;
|
|
169
|
+
try { state = readPhaseEntryState(sessionIdOf(payload), root, locations.read); } catch { /* an unreadable store refuses */ }
|
|
170
|
+
let activeClaim = false;
|
|
171
|
+
try { activeClaim = readActiveClaimTaskId(cwdOf(payload)) !== null; } catch { /* unreadable authority is no claim */ }
|
|
172
|
+
const admission = admitPhaseOperation(operation, state, { activeClaim });
|
|
173
|
+
if (!admission.ok) return denyPreToolUse(phaseRefusalMessage(admission));
|
|
174
|
+
if (operation.kind === "skill") return null;
|
|
175
|
+
const toolUseId = typeof payload.tool_use_id === "string" ? payload.tool_use_id : "";
|
|
176
|
+
let recorded = false;
|
|
177
|
+
try {
|
|
178
|
+
recorded = toolUseId !== ""
|
|
179
|
+
&& (locations.append ?? appendPrivate)(admissionPath(toolUseId, root), cacheDir(root), `${JSON.stringify({ v: 1, operation: operation.name })}\n`);
|
|
180
|
+
} catch { /* an admission that could not be written refuses */ }
|
|
181
|
+
if (recorded) return null;
|
|
182
|
+
return denyPreToolUse(
|
|
183
|
+
`${RUN_ENTRY_REQUIRED}: the admission for this ${operation.name} call could not be recorded; recovery: ${PHASE_ENTRY_RECOVERY[RUN_ENTRY_REQUIRED]}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Tool side: take the admission the hook wrote for this exact call. One use;
|
|
189
|
+
* absent, unreadable or written for another operation all refuse. The hook may
|
|
190
|
+
* run more than once for one call (two enabled copies of the plugin), so
|
|
191
|
+
* repeated identical admissions count as one.
|
|
192
|
+
*/
|
|
193
|
+
export function consumePhaseAdmission(toolUseId: string, operation: GatedTool, root = tmpdir()): boolean {
|
|
194
|
+
if (!toolUseId || !ensurePrivateDir(cacheDir(root))) return false;
|
|
195
|
+
const path = admissionPath(toolUseId, root);
|
|
196
|
+
let text: string | undefined;
|
|
197
|
+
try { text = readPrivate(path); } catch { return false; }
|
|
198
|
+
if (text === undefined) return false;
|
|
199
|
+
dropPrivate(path);
|
|
200
|
+
const lines = text.split("\n").filter(Boolean);
|
|
201
|
+
if (lines.length === 0) return false;
|
|
202
|
+
return lines.every((line) => {
|
|
203
|
+
try {
|
|
204
|
+
const record = JSON.parse(line) as { v?: unknown; operation?: unknown } | null;
|
|
205
|
+
return record?.v === 1 && record.operation === operation;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type NativeConfirmationPort,
|
|
13
13
|
} from "./interaction";
|
|
14
14
|
import { ClaudeReviewHost, FileHookEventLog, parseHookStdin } from "./review_host";
|
|
15
|
-
import { activeTaskHookOutput, laneGuardHookOutput } from "./lane_guard";
|
|
15
|
+
import { activeTaskHookOutput, consumePhaseAdmission, laneGuardHookOutput, phaseEntryGateHookOutput, recordPhaseEntryPrompt, type GatedTool } from "./lane_guard";
|
|
16
16
|
import { ClaudeRuntime, type ToolMeta } from "./kernel_ports";
|
|
17
17
|
import { parseLaneInstruction, parseLaneOffers, parseMaxParallel, resolveLaneParallel } from "../unattended/batch_lanes";
|
|
18
18
|
import { parseFinalVerification } from "../unattended/batch_final_verification";
|
|
@@ -118,6 +118,11 @@ export interface McpRuntimeOptions {
|
|
|
118
118
|
batchKernel?: Partial<import("../unattended/batch_runner").BatchRunnerKernelPort>;
|
|
119
119
|
batchGit?: import("../unattended/batch_git").BatchRunnerGitPort;
|
|
120
120
|
readInitiative?: import("../unattended/types").InitiativeObservationReader;
|
|
121
|
+
/**
|
|
122
|
+
* Takes the admission the PreToolUse hook wrote for one `enroll` or
|
|
123
|
+
* `start_unattended_batch` call. Defaults to the private file store.
|
|
124
|
+
*/
|
|
125
|
+
consumePhaseAdmission?: (toolCallId: string, operation: GatedTool) => boolean;
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
@@ -139,6 +144,14 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
139
144
|
let negotiatedVersion: string | undefined;
|
|
140
145
|
let negotiatedInteractive = false;
|
|
141
146
|
const connectionId = randomUUID();
|
|
147
|
+
const consumeAdmission = options.consumePhaseAdmission ?? ((toolCallId: string, operation: GatedTool) => consumePhaseAdmission(toolCallId, operation));
|
|
148
|
+
// The phase entry gate, before any preparation or native dialog. The hook
|
|
149
|
+
// decided on the session's literal entry; no admission for this exact call
|
|
150
|
+
// means no hook saw it, which refuses.
|
|
151
|
+
const requireRunEntry = (operation: GatedTool, toolCallId: string) => {
|
|
152
|
+
if (consumeAdmission(toolCallId, operation)) return;
|
|
153
|
+
throw new NativeAuthorityError("run_entry_required", `${operation} needs a literal-user imm-run entry in this session`);
|
|
154
|
+
};
|
|
142
155
|
return {
|
|
143
156
|
runtime,
|
|
144
157
|
host,
|
|
@@ -164,6 +177,7 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
164
177
|
}
|
|
165
178
|
const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
|
|
166
179
|
if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
180
|
+
requireRunEntry(name, meta.toolCallId ?? `call-${name}`);
|
|
167
181
|
const toolMeta: ToolMeta = {
|
|
168
182
|
sessionId: meta.sessionId ?? connectionId,
|
|
169
183
|
toolCallId: meta.toolCallId ?? `call-${name}`,
|
|
@@ -230,7 +244,10 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
230
244
|
interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
|
|
231
245
|
signal: meta.signal,
|
|
232
246
|
};
|
|
233
|
-
if (name === "enroll")
|
|
247
|
+
if (name === "enroll") {
|
|
248
|
+
requireRunEntry(name, meta.toolCallId ?? `call-${name}`);
|
|
249
|
+
return runtime.enroll(taskId, toolMeta);
|
|
250
|
+
}
|
|
234
251
|
if (name === "advance_assurance") return runtime.advance(taskId, toolMeta.signal, toolMeta);
|
|
235
252
|
if (name === "submit_review") {
|
|
236
253
|
// ADR 0017: an absent verdict is the receipt-bound submission.
|
|
@@ -737,11 +754,12 @@ async function runHook(): Promise<void> {
|
|
|
737
754
|
let payload: Record<string, unknown> | null = null;
|
|
738
755
|
try { payload = JSON.parse(raw) as Record<string, unknown>; } catch { payload = null; }
|
|
739
756
|
if (payload?.hook_event_name === "PreToolUse") {
|
|
740
|
-
const decision = laneGuardHookOutput(payload);
|
|
757
|
+
const decision = laneGuardHookOutput(payload) ?? phaseEntryGateHookOutput(payload);
|
|
741
758
|
if (decision) process.stdout.write(`${decision}\n`);
|
|
742
759
|
return;
|
|
743
760
|
}
|
|
744
761
|
if (payload?.hook_event_name === "UserPromptSubmit") {
|
|
762
|
+
recordPhaseEntryPrompt(payload);
|
|
745
763
|
const notice = await activeTaskHookOutput(payload);
|
|
746
764
|
if (notice) process.stdout.write(`${notice}\n`);
|
|
747
765
|
return;
|
|
@@ -76,21 +76,21 @@ export class MemoryHookEventLog implements HookEventLog {
|
|
|
76
76
|
|
|
77
77
|
const CACHE_DIR = "immune-brain-claude";
|
|
78
78
|
|
|
79
|
-
function sessionHash(sessionId: string): string {
|
|
79
|
+
export function sessionHash(sessionId: string): string {
|
|
80
80
|
return createHash("sha256").update(sessionId).digest("hex");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function cacheDir(root: string): string {
|
|
83
|
+
export function cacheDir(root: string): string {
|
|
84
84
|
return join(root, CACHE_DIR);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function ownedByUs(stat: { uid: number; isSymbolicLink(): boolean }): boolean {
|
|
87
|
+
export function ownedByUs(stat: { uid: number; isSymbolicLink(): boolean }): boolean {
|
|
88
88
|
if (stat.isSymbolicLink()) return false;
|
|
89
89
|
const uid = process.getuid?.();
|
|
90
90
|
return uid === undefined || stat.uid === uid;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function ensurePrivateDir(dir: string): boolean {
|
|
93
|
+
export function ensurePrivateDir(dir: string): boolean {
|
|
94
94
|
try {
|
|
95
95
|
const existing = lstatSync(dir);
|
|
96
96
|
return existing.isDirectory() && ownedByUs(existing) && (existing.mode & 0o777) === 0o700;
|
|
@@ -106,7 +106,7 @@ function ensurePrivateDir(dir: string): boolean {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
function appendPrivate(path: string, dir: string, line: string): boolean {
|
|
109
|
+
export function appendPrivate(path: string, dir: string, line: string): boolean {
|
|
110
110
|
if (!ensurePrivateDir(dir)) return false;
|
|
111
111
|
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK;
|
|
112
112
|
let fd: number;
|
|
@@ -122,7 +122,7 @@ function appendPrivate(path: string, dir: string, line: string): boolean {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function readPrivate(path: string, maxBytes = Number.POSITIVE_INFINITY): string | undefined {
|
|
125
|
+
export function readPrivate(path: string, maxBytes = Number.POSITIVE_INFINITY): string | undefined {
|
|
126
126
|
let fd: number;
|
|
127
127
|
try { fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); } catch { return; }
|
|
128
128
|
try {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/plugin_versioning.ts from the root package.json.
|
|
2
|
-
export const PLUGIN_VERSION = "5.
|
|
2
|
+
export const PLUGIN_VERSION = "5.2.0" as const;
|
|
@@ -26,6 +26,11 @@ material task-local maintenance risks. Pure naming, length, complexity
|
|
|
26
26
|
thresholds, formatting, and design preference are not findings and must not
|
|
27
27
|
cause style-only rework.
|
|
28
28
|
|
|
29
|
+
Test-suite growth is advisory, never blocking: note a delivered test that
|
|
30
|
+
duplicates an existing assertion, asserts only private implementation, or
|
|
31
|
+
leaves a test protecting behavior the change retired. Weakened or missing
|
|
32
|
+
tests for an acceptance remain blocking under the rules above.
|
|
33
|
+
|
|
29
34
|
When a finding repeats an acceptance id or anchor that already produced rework in
|
|
30
35
|
this task, do not file it as a fresh single-trigger defect. Name the invariant
|
|
31
36
|
class it belongs to and state in `evidence.trigger` why the previously accepted
|
|
@@ -68,6 +68,14 @@ mock or hard-coded success, swallowed unexpected errors, missing validation at
|
|
|
68
68
|
external trust boundaries, invented dependencies or APIs, unauthorized
|
|
69
69
|
observable behavior changes, and production paths without a current caller.
|
|
70
70
|
Do not weaken tests or hide an incomplete result to make Verification pass.
|
|
71
|
+
|
|
72
|
+
Keep the test suite lean. Each added test pins one acceptance invariant or
|
|
73
|
+
finding case at its public seam; do not mirror private calls, mock order or
|
|
74
|
+
internal return values that a public assertion already covers. Before adding a
|
|
75
|
+
test, search existing tests for the same behavior and extend or parameterize
|
|
76
|
+
them instead of duplicating setup. When the change retires or replaces a
|
|
77
|
+
behavior, update or delete the tests that protected it within scope and name
|
|
78
|
+
the retired behavior or continuing owner in Delivery Evidence.
|
|
71
79
|
Treat naming, function length, parameter count, nesting, and abstraction taste
|
|
72
80
|
as contextual signals, never as automatic failure thresholds.
|
|
73
81
|
|
|
@@ -108,7 +108,7 @@ An existing grant for one of these never inherits another: a stage-only
|
|
|
108
108
|
instruction stays stage-only, a commit is not push or publication authority,
|
|
109
109
|
and a valid batch capability commits only its own scope-bounded child.
|
|
110
110
|
|
|
111
|
-
Routine Managed enrollment uses one current-Host native confirmation bound to the TaskIntent content hash after Planner validation.
|
|
111
|
+
Routine Managed enrollment is the first step of an explicit `imm-run` entry and uses one current-Host native confirmation bound to the TaskIntent content hash after Planner validation. Planner and Brainstorm stop with candidate artifacts and never invoke Enrollment, whether or not the request also asks for execution; explicit Plan-only requests stop the same way. `imm-run` opens the native gate directly without chat pre-confirmation. Enrollment validates intent, Git ownership, scope, workspace claim, and final authority preconditions without executing acceptance descriptors; deterministic QA executes them after implementation. Once the user has entered `imm-run`, the routine task proceeds from that single confirmation through enrollment, execution, and QA with no further confirmation. Do not request confirmation for local in-scope edits, local verification, ordinary Direct rework, scoped diff review, or completion reporting. Managed evidence, QA, Review, and completion authority remain governed by their Managed contracts; R2 does not weaken them. Managed native-authority failures fail closed with one stable reason and exactly one same-Host recovery action; never offer a Pi, Direct Path, cross-Host/worktree, unmanaged, or automatic-retry fallback.
|
|
112
112
|
|
|
113
113
|
For an unborn symbolic Git HEAD, the same Enrollment confirmation discloses an empty root commit. After approval, the Host creates that commit without staging project content or changing the index/worktree, then revalidates ownership and rehearses Enrollment against the new base. Git identity uses existing environment/config values with an explicit automation fallback, never config writes. Decline or pre-initialization cancellation leaves HEAD unborn; if later Enrollment fails or is cancelled, report the remaining commit. Invalid Git refs are errors, not initialization candidates.
|
|
114
114
|
|
|
@@ -25,7 +25,8 @@ or all references as an entry prerequisite.
|
|
|
25
25
|
- requested framing document: [Output Language Policy](../../dist/BASELINE.md#output-language-policy)
|
|
26
26
|
|
|
27
27
|
When framing discusses later execution, describe Enrollment only as the
|
|
28
|
-
current Host's native gate
|
|
28
|
+
current Host's native gate, opened as the first step of a user-launched
|
|
29
|
+
`imm-run`. Brainstorm stops after framing; the user launches `imm-planner`. A failed Managed authority interaction stays
|
|
29
30
|
fail-closed: report one same-Host recovery action, never another Host,
|
|
30
31
|
worktree, or unmanaged implementation.
|
|
31
32
|
|
|
@@ -42,11 +42,11 @@ own routes instead of these stages.
|
|
|
42
42
|
- retirement design: [Retirement Completion Contract](../../dist/imm-planner.md#retirement-completion-contract)
|
|
43
43
|
- optional research dispatch: [Research Dispatch](../../dist/imm-planner.md#research-dispatch)
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
Every request stops after candidate TaskIntent validation, plus Spec validation when the work is complex; Plan-only requests stop there too. The Planner
|
|
46
|
+
never invokes the current Host's native Enrollment gate, even when the request also asks for
|
|
47
|
+
execution: report the candidate and name a literal-user `imm-run` entry as the
|
|
48
|
+
only next step. Never suggest another Host, worktree, or unmanaged
|
|
49
|
+
implementation as a fallback.
|
|
50
50
|
|
|
51
51
|
Before that handoff, confirm complete mapping of every upstream item and
|
|
52
52
|
required phase with each Slice's result and the remaining gap; reject a plan
|
|
@@ -11,7 +11,8 @@ ordinary host input never resumes a Managed owner implicitly.
|
|
|
11
11
|
|
|
12
12
|
Mandatory constraints before any action: verify the active backend claim,
|
|
13
13
|
TaskIntent, and TaskRecord via `imm_kernel_canary` `status` first; invalid or
|
|
14
|
-
contradictory projections fail closed.
|
|
14
|
+
contradictory projections fail closed. With no active claim, the first step
|
|
15
|
+
is the native Enrollment gate for the validated candidate, invoked once. Resume only from those authority facts:
|
|
15
16
|
a compaction heading, HANDOFF summary, or Issue state is prose, never authority.
|
|
16
17
|
All Managed authority gates use the
|
|
17
18
|
current Host's native interaction. A failed gate stays fail-closed and
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: imm-test-prune
|
|
3
|
+
description: Use when the user explicitly requests Immune-Brain audit or pruning of redundant, bloated, or orphaned tests.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Immune-Brain: Test Prune
|
|
7
|
+
|
|
8
|
+
Use [`../../dist/imm-test-prune.md`](../../dist/imm-test-prune.md) as the canonical
|
|
9
|
+
contract index, not a whole-document read. This is a
|
|
10
|
+
standalone host-native maintenance entry, not a Managed Path continuation
|
|
11
|
+
and not an `imm-run` internal-role dispatch.
|
|
12
|
+
|
|
13
|
+
Mandatory constraints: audit is read-only. Mutation requires exact hash-bound
|
|
14
|
+
manifest approval and immediate revalidation. Preserve active Managed ownership;
|
|
15
|
+
use only the repository's own test and coverage commands; never install
|
|
16
|
+
dependencies, weaken a live assertion, change production source, or commit.
|
|
17
|
+
Interruption requires a fresh scan.
|
|
18
|
+
|
|
19
|
+
Section routes - load a section's instructions only when its branch applies.
|
|
20
|
+
Read each linked heading body up to the next heading; nested sections and
|
|
21
|
+
references load only under their own condition. Never read the whole contract
|
|
22
|
+
or all references as an entry prerequisite.
|
|
23
|
+
|
|
24
|
+
- common: [Authority Boundary](../../dist/imm-test-prune.md#authority-boundary), [Invocation](../../dist/imm-test-prune.md#invocation)
|
|
25
|
+
- audit or manifest preparation: [Inventory and Manifest](../../dist/imm-test-prune.md#inventory-and-manifest)
|
|
26
|
+
- approved mutation before any edit: [Mutation Envelope](../../dist/imm-test-prune.md#mutation-envelope), [Approved Mutation](../../dist/imm-test-prune.md#approved-mutation)
|
|
27
|
+
- completion: [Verify and Report](../../dist/imm-test-prune.md#verify-and-report)
|
|
28
|
+
- recovery or deletion recoverability: [Recovery](../../dist/imm-test-prune.md#recovery)
|
|
@@ -56,6 +56,15 @@ skills:
|
|
|
56
56
|
output_artifacts: [maintain_report]
|
|
57
57
|
next_actions: []
|
|
58
58
|
boundary: Minimize tracked agent-instruction context after explicit manifest approval; no Managed authority mutation, contract installation, or reference-document creation.
|
|
59
|
+
- name: imm-test-prune
|
|
60
|
+
path: skills/imm-test-prune/SKILL.md
|
|
61
|
+
role: execute
|
|
62
|
+
title: Test Prune
|
|
63
|
+
role_class: repair
|
|
64
|
+
canonical: true
|
|
65
|
+
output_artifacts: [test_prune_report]
|
|
66
|
+
next_actions: []
|
|
67
|
+
boundary: Audit and prune redundant tests after explicit manifest approval; no Managed authority mutation, production-source change, or assertion weakening.
|
|
59
68
|
- name: imm-retro
|
|
60
69
|
path: skills/imm-retro/SKILL.md
|
|
61
70
|
role: execute
|