immune-brain 3.2.2 → 3.4.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 +2 -2
- package/README.zh-CN.md +3 -3
- package/package.json +5 -3
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +19 -73
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +1 -2
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +2 -2
- package/plugins/immune-brain/dist/BASELINE.md +1 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +317 -316
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +15 -10
- package/plugins/immune-brain/dist/imm-planner.md +22 -17
- package/plugins/immune-brain/hooks/hooks.json +0 -10
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +0 -8
- package/plugins/immune-brain/runtime/claude/capability.ts +4 -10
- package/plugins/immune-brain/runtime/claude/interaction.ts +55 -22
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +63 -73
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +233 -55
- package/plugins/immune-brain/runtime/claude/review_host.ts +2 -138
- package/plugins/immune-brain/runtime/kernel/application.ts +0 -1
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +1 -7
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +0 -7
- package/plugins/immune-brain/runtime/kernel/completion.ts +1 -3
- package/plugins/immune-brain/runtime/kernel/reducer.ts +0 -31
- package/plugins/immune-brain/runtime/kernel/types.ts +0 -2
- package/plugins/immune-brain/runtime/kernel/validation.ts +1 -3
- package/plugins/immune-brain/runtime/plugin_version.ts +2 -0
- package/plugins/immune-brain/skills/BASELINE.md +1 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -0
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +6 -0
|
@@ -45,23 +45,17 @@ export type ClaudeHookEvent =
|
|
|
45
45
|
taskId?: string;
|
|
46
46
|
operationId?: string;
|
|
47
47
|
}
|
|
48
|
-
| { type: "SessionEnd"; sessionId: string }
|
|
49
|
-
| { type: "ElicitationResult"; sessionId: string; toolCallId: string; decision: "accept" | "deny" | "cancel" }
|
|
50
|
-
| { type: "ElicitationConsumed"; sessionId: string; toolCallId: string };
|
|
48
|
+
| { type: "SessionEnd"; sessionId: string };
|
|
51
49
|
|
|
52
50
|
export interface HookEventLog {
|
|
53
51
|
append(event: ClaudeHookEvent): boolean;
|
|
54
52
|
list(sessionId?: string): ClaudeHookEvent[];
|
|
55
53
|
sessions(): string[];
|
|
56
54
|
clear(sessionId?: string): void;
|
|
57
|
-
/** Persists consumed identity durably; true only after the record is verified. */
|
|
58
|
-
consumeElicitation(sessionId: string, toolCallId: string): boolean;
|
|
59
|
-
consumedKeys(): string[];
|
|
60
55
|
}
|
|
61
56
|
|
|
62
57
|
export class MemoryHookEventLog implements HookEventLog {
|
|
63
58
|
private readonly events: ClaudeHookEvent[] = [];
|
|
64
|
-
private readonly consumed: string[] = [];
|
|
65
59
|
append(event: ClaudeHookEvent): boolean { this.events.push(event); return true; }
|
|
66
60
|
list(sessionId?: string): ClaudeHookEvent[] {
|
|
67
61
|
return sessionId ? this.events.filter((event) => event.sessionId === sessionId) : [...this.events];
|
|
@@ -78,17 +72,9 @@ export class MemoryHookEventLog implements HookEventLog {
|
|
|
78
72
|
if (this.events[i].sessionId === sessionId) this.events.splice(i, 1);
|
|
79
73
|
}
|
|
80
74
|
}
|
|
81
|
-
consumeElicitation(sessionId: string, toolCallId: string): boolean {
|
|
82
|
-
this.consumed.push(`${sessionId}\0${toolCallId}`);
|
|
83
|
-
this.events.push({ type: "ElicitationConsumed", sessionId, toolCallId });
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
consumedKeys(): string[] { return [...this.consumed]; }
|
|
87
75
|
}
|
|
88
76
|
|
|
89
77
|
const CACHE_DIR = "immune-brain-claude";
|
|
90
|
-
const CONSUMED_FILE = "consumed.jsonl";
|
|
91
|
-
const CONSUMED_DIR = "consumed";
|
|
92
78
|
|
|
93
79
|
function sessionHash(sessionId: string): string {
|
|
94
80
|
return createHash("sha256").update(sessionId).digest("hex");
|
|
@@ -120,25 +106,6 @@ function ensurePrivateDir(dir: string): boolean {
|
|
|
120
106
|
}
|
|
121
107
|
}
|
|
122
108
|
|
|
123
|
-
function claimAtomicKey(dir: string, keyName: string): boolean {
|
|
124
|
-
const claimsDir = join(dir, CONSUMED_DIR);
|
|
125
|
-
if (!ensurePrivateDir(claimsDir)) return false;
|
|
126
|
-
const claimPath = join(claimsDir, `${keyName}.claim`);
|
|
127
|
-
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_NONBLOCK;
|
|
128
|
-
let fd: number;
|
|
129
|
-
try {
|
|
130
|
-
fd = openSync(claimPath, flags, 0o600);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
const stat = fstatSync(fd);
|
|
136
|
-
if (!stat.isFile() || !ownedByUs(stat) || (stat.mode & 0o777) !== 0o600) return false;
|
|
137
|
-
return true;
|
|
138
|
-
} finally {
|
|
139
|
-
closeSync(fd);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
109
|
function appendPrivate(path: string, dir: string, line: string): boolean {
|
|
143
110
|
if (!ensurePrivateDir(dir)) return false;
|
|
144
111
|
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK;
|
|
@@ -185,7 +152,7 @@ export class FileHookEventLog implements HookEventLog {
|
|
|
185
152
|
if (!ensurePrivateDir(dir)) return [];
|
|
186
153
|
try {
|
|
187
154
|
return readdirSync(dir)
|
|
188
|
-
.filter((name) => name.endsWith(".jsonl")
|
|
155
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
189
156
|
.flatMap((name) => this.readFile(join(dir, name)));
|
|
190
157
|
} catch {
|
|
191
158
|
return [];
|
|
@@ -218,34 +185,6 @@ export class FileHookEventLog implements HookEventLog {
|
|
|
218
185
|
rmSync(path, { force: true });
|
|
219
186
|
} catch { /* cleanup cannot create authority */ }
|
|
220
187
|
}
|
|
221
|
-
consumeElicitation(sessionId: string, toolCallId: string): boolean {
|
|
222
|
-
// The durable consumed identity is claimed atomically (O_EXCL per-key)
|
|
223
|
-
// and committed to consumed.jsonl BEFORE any session evidence is touched:
|
|
224
|
-
// concurrent consumers cannot double-claim the same identity.
|
|
225
|
-
const dir = cacheDir(this.root);
|
|
226
|
-
const claimKey = createHash("sha256").update(`${sessionId}\0${toolCallId}`).digest("hex");
|
|
227
|
-
const keyClaimed = claimAtomicKey(dir, claimKey);
|
|
228
|
-
if (!keyClaimed) return false;
|
|
229
|
-
if (!appendPrivate(join(dir, CONSUMED_FILE), dir, `${JSON.stringify({ sessionId, toolCallId })}\n`)) {
|
|
230
|
-
// Roll back the claim file if the durable record append failed
|
|
231
|
-
try { rmSync(join(dir, CONSUMED_DIR, `${claimKey}.claim`), { force: true }); } catch { /* ignore */ }
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
if (!this.consumedKeys().includes(`${sessionId}\0${toolCallId}`)) return false;
|
|
235
|
-
return this.append({ type: "ElicitationConsumed", sessionId, toolCallId });
|
|
236
|
-
}
|
|
237
|
-
consumedKeys(): string[] {
|
|
238
|
-
if (!ensurePrivateDir(cacheDir(this.root))) return [];
|
|
239
|
-
const text = readPrivate(join(cacheDir(this.root), CONSUMED_FILE));
|
|
240
|
-
if (!text) return [];
|
|
241
|
-
try {
|
|
242
|
-
return text.split("\n").filter(Boolean)
|
|
243
|
-
.map((line) => JSON.parse(line) as { sessionId: string; toolCallId: string })
|
|
244
|
-
.map((item) => `${item.sessionId}\0${item.toolCallId}`);
|
|
245
|
-
} catch {
|
|
246
|
-
return [];
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
188
|
}
|
|
250
189
|
|
|
251
190
|
interface PendingReview {
|
|
@@ -287,11 +226,8 @@ export class ClaudeReviewHost implements AssuranceHostPort {
|
|
|
287
226
|
readonly host = "claude-code" as const;
|
|
288
227
|
private readonly pending = new Map<string, PendingReview>();
|
|
289
228
|
private readonly appliedBySession = new Map<string, number>();
|
|
290
|
-
private readonly consumedElicitations = new Set<string>();
|
|
291
229
|
constructor(private readonly log: HookEventLog = new MemoryHookEventLog()) {}
|
|
292
230
|
|
|
293
|
-
private readonly confirmations = new Map<string, "accept" | "deny" | "cancel">();
|
|
294
|
-
|
|
295
231
|
prepareReview(request: ReviewRequest): HostReviewReservation {
|
|
296
232
|
const initialCursors = new Map<string, number>();
|
|
297
233
|
const sessionCursors = new Map<string, number>();
|
|
@@ -322,7 +258,6 @@ export class ClaudeReviewHost implements AssuranceHostPort {
|
|
|
322
258
|
}
|
|
323
259
|
|
|
324
260
|
private drain(): void {
|
|
325
|
-
for (const key of this.log.consumedKeys()) this.consumedElicitations.add(key);
|
|
326
261
|
for (const sessionId of this.log.sessions()) {
|
|
327
262
|
const events = this.log.list(sessionId);
|
|
328
263
|
let start = this.appliedBySession.get(sessionId) ?? 0;
|
|
@@ -330,34 +265,10 @@ export class ClaudeReviewHost implements AssuranceHostPort {
|
|
|
330
265
|
let ended = false;
|
|
331
266
|
for (let i = start; i < events.length; i++) {
|
|
332
267
|
const event = events[i];
|
|
333
|
-
if (event.type === "ElicitationResult") {
|
|
334
|
-
const key = `${event.sessionId}\0${event.toolCallId}`;
|
|
335
|
-
if (this.consumedElicitations.has(key)) continue;
|
|
336
|
-
if (this.confirmations.has(key)) {
|
|
337
|
-
if (this.log.consumeElicitation(event.sessionId, event.toolCallId)) {
|
|
338
|
-
this.confirmations.delete(key);
|
|
339
|
-
this.consumedElicitations.add(key);
|
|
340
|
-
}
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
this.confirmations.set(key, event.decision);
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
if (event.type === "ElicitationConsumed") {
|
|
347
|
-
const key = `${event.sessionId}\0${event.toolCallId}`;
|
|
348
|
-
this.consumedElicitations.add(key);
|
|
349
|
-
this.confirmations.delete(key);
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
268
|
if (event.type === "SessionEnd") {
|
|
353
269
|
this.log.clear(event.sessionId);
|
|
354
270
|
ended = true;
|
|
355
271
|
this.appliedBySession.delete(event.sessionId);
|
|
356
|
-
// A confirmation cached before the session ended must never
|
|
357
|
-
// authorize a later privileged call.
|
|
358
|
-
for (const key of this.confirmations.keys()) {
|
|
359
|
-
if (key.startsWith(`${event.sessionId}\0`)) this.confirmations.delete(key);
|
|
360
|
-
}
|
|
361
272
|
for (const [id, state] of this.pending) {
|
|
362
273
|
if (state.startEvent?.sessionId === event.sessionId || state.postEvent?.sessionId === event.sessionId || state.stopEvent?.sessionId === event.sessionId) {
|
|
363
274
|
this.pending.delete(id);
|
|
@@ -394,38 +305,6 @@ export class ClaudeReviewHost implements AssuranceHostPort {
|
|
|
394
305
|
}
|
|
395
306
|
}
|
|
396
307
|
|
|
397
|
-
peekConfirmation(sessionId: string, toolCallId: string): boolean {
|
|
398
|
-
this.drain();
|
|
399
|
-
return this.confirmations.has(`${sessionId}\0${toolCallId}`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
takeConfirmation(sessionId: string, toolCallId: string): "accept" | "deny" | "cancel" | undefined {
|
|
403
|
-
this.drain();
|
|
404
|
-
const key = `${sessionId}\0${toolCallId}`;
|
|
405
|
-
const decision = this.confirmations.get(key);
|
|
406
|
-
if (!decision) return undefined;
|
|
407
|
-
if (!this.log.consumeElicitation(sessionId, toolCallId)) {
|
|
408
|
-
this.confirmations.delete(key);
|
|
409
|
-
return undefined;
|
|
410
|
-
}
|
|
411
|
-
this.confirmations.delete(key);
|
|
412
|
-
this.consumedElicitations.add(key);
|
|
413
|
-
this.drain();
|
|
414
|
-
return decision;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
sessionOfElicitation(toolCallId: string): string | undefined {
|
|
418
|
-
this.drain();
|
|
419
|
-
const sessions: string[] = [];
|
|
420
|
-
for (const key of this.confirmations.keys()) {
|
|
421
|
-
const sep = key.lastIndexOf("\0");
|
|
422
|
-
if (sep >= 0 && key.slice(sep + 1) === toolCallId) sessions.push(key.slice(0, sep));
|
|
423
|
-
}
|
|
424
|
-
// Ambiguous correlation across sessions fails closed.
|
|
425
|
-
if (sessions.length !== 1) return undefined;
|
|
426
|
-
return sessions[0];
|
|
427
|
-
}
|
|
428
|
-
|
|
429
308
|
private applyReviewEvent(event: ClaudeHookEvent, state: PendingReview): void {
|
|
430
309
|
if (state.error) return;
|
|
431
310
|
if (event.type === "SubagentStart") {
|
|
@@ -543,10 +422,6 @@ export function parseHookStdin(raw: string): ClaudeHookEvent | null {
|
|
|
543
422
|
try { payload = JSON.parse(raw) as Record<string, unknown>; }
|
|
544
423
|
catch { return null; }
|
|
545
424
|
const hookType = String(payload.hook_event_name ?? payload.type ?? "");
|
|
546
|
-
// ElicitationResult is native authority evidence: it must carry the Host's
|
|
547
|
-
// own session_id verbatim. Alias or environment fallbacks are never
|
|
548
|
-
// accepted for this event; anything else is malformed and fails closed.
|
|
549
|
-
if (hookType === "ElicitationResult" && (typeof payload.session_id !== "string" || !payload.session_id)) return null;
|
|
550
425
|
const sessionId = String(payload.session_id ?? payload.sessionId ?? process.env.CLAUDE_SESSION_ID ?? "");
|
|
551
426
|
if (!sessionId) return null;
|
|
552
427
|
const agent = String(payload.agent_type ?? payload.agent ?? payload.subagent_type ?? "");
|
|
@@ -630,16 +505,5 @@ export function parseHookStdin(raw: string): ClaudeHookEvent | null {
|
|
|
630
505
|
};
|
|
631
506
|
}
|
|
632
507
|
if (hookType === "SessionEnd") return { type: "SessionEnd", sessionId };
|
|
633
|
-
if (hookType === "ElicitationResult") {
|
|
634
|
-
const rawToolCallId = payload.tool_use_id ?? payload.toolCallId;
|
|
635
|
-
if (typeof rawToolCallId !== "string" || !rawToolCallId) return null;
|
|
636
|
-
const toolCallId = rawToolCallId;
|
|
637
|
-
const raw = payload.decision ?? payload.result ?? payload.action;
|
|
638
|
-
// Only the exact MCP elicitation decision strings are native evidence;
|
|
639
|
-
// booleans and aliases like "yes" are malformed and fail closed.
|
|
640
|
-
const decision = raw === "accept" ? "accept" : raw === "deny" ? "deny" : raw === "cancel" ? "cancel" : null;
|
|
641
|
-
if (!toolCallId || !decision) return null;
|
|
642
|
-
return { type: "ElicitationResult", sessionId, toolCallId, decision };
|
|
643
|
-
}
|
|
644
508
|
return null;
|
|
645
509
|
}
|
|
@@ -158,7 +158,6 @@ export function applyTaskAction(
|
|
|
158
158
|
|
|
159
159
|
const privileged =
|
|
160
160
|
action.type === "record_approval" ||
|
|
161
|
-
action.type === "record_user_approval" ||
|
|
162
161
|
action.type === "approve_breaking_intent_revision" ||
|
|
163
162
|
action.type === "request_rework" ||
|
|
164
163
|
action.type === "stop" ||
|
|
@@ -25,13 +25,9 @@ export interface AssuranceAuthorizationReadiness {
|
|
|
25
25
|
/**
|
|
26
26
|
* Kernel-decidable authorization readiness:
|
|
27
27
|
* - "resolve_user_decision": exactly one open unresolved-user-decision finding;
|
|
28
|
-
* - "record_user_approval": Kernel projects authorize_user after fresh QA
|
|
29
|
-
* and Review attestations for a critical task;
|
|
30
28
|
* - "none": nothing uniquely decidable from Kernel facts.
|
|
31
|
-
* A pending Pi native Review verdict is a session fact and is NOT visible
|
|
32
|
-
* here; the host composes it before this readiness.
|
|
33
29
|
*/
|
|
34
|
-
state: "resolve_user_decision" | "
|
|
30
|
+
state: "resolve_user_decision" | "none";
|
|
35
31
|
/** Non-null only when Kernel facts prove the authorization is blocked. */
|
|
36
32
|
blocked: string | null;
|
|
37
33
|
}
|
|
@@ -79,8 +75,6 @@ export function deriveAssuranceAuthorization(input: {
|
|
|
79
75
|
state: "none",
|
|
80
76
|
blocked: `resolve-user-decision requires exactly one open user decision; found ${input.open_user_decision_count}`,
|
|
81
77
|
};
|
|
82
|
-
if (input.next_obligation === "authorize_user")
|
|
83
|
-
return { state: "record_user_approval", blocked: null };
|
|
84
78
|
return { state: "none", blocked: null };
|
|
85
79
|
}
|
|
86
80
|
|
|
@@ -51,7 +51,6 @@ export type CanaryOperation =
|
|
|
51
51
|
| { op: "resolve_finding"; finding_id: string; actor_id: string }
|
|
52
52
|
| { op: "request_rework"; capability: object; findings: TaskFinding[]; actor_id: string }
|
|
53
53
|
| { op: "record_approval"; capability: object; approval: TaskApprovalV2; actor_id: string }
|
|
54
|
-
| { op: "record_user_approval"; capability: object; approval: TaskApprovalV2; actor_id: string }
|
|
55
54
|
| { op: "revise_intent"; next_intent: TaskIntentV1; actor_id: string }
|
|
56
55
|
| { op: "approve_breaking_intent_revision"; capability: object; next_intent: TaskIntentV1; actor_id: string }
|
|
57
56
|
| { op: "complete"; actor_id: string }
|
|
@@ -141,8 +140,6 @@ export function capabilityActionFor(input: {
|
|
|
141
140
|
switch (input.op) {
|
|
142
141
|
case "record_approval":
|
|
143
142
|
return { ...base, approval: input.approval } as TaskAction;
|
|
144
|
-
case "record_user_approval":
|
|
145
|
-
return { ...base, approval: input.approval } as TaskAction;
|
|
146
143
|
case "request_rework":
|
|
147
144
|
return { ...base, findings: input.findings } as TaskAction;
|
|
148
145
|
case "stop":
|
|
@@ -373,10 +370,6 @@ export function createCanaryApplication(
|
|
|
373
370
|
capability = operation.capability;
|
|
374
371
|
action = { ...base, type: "record_approval", approval: operation.approval };
|
|
375
372
|
break;
|
|
376
|
-
case "record_user_approval":
|
|
377
|
-
capability = operation.capability;
|
|
378
|
-
action = { ...base, type: "record_user_approval", approval: operation.approval };
|
|
379
|
-
break;
|
|
380
373
|
case "revise_intent":
|
|
381
374
|
action = {
|
|
382
375
|
...base,
|
|
@@ -12,7 +12,7 @@ import { assertKernelInvariantsV3, KernelInvariantError } from "./validation";
|
|
|
12
12
|
const REQUIRED_ATTESTATIONS: Record<TaskIntentV1["risk"], ApprovalKind[]> = {
|
|
13
13
|
routine: ["qa"],
|
|
14
14
|
material: ["qa", "review"],
|
|
15
|
-
critical: ["qa", "review"
|
|
15
|
+
critical: ["qa", "review"],
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
function archiveActivePlanningPath(path: string): string | null {
|
|
@@ -199,8 +199,6 @@ export function projectTask(
|
|
|
199
199
|
nextObligation = "run_qa";
|
|
200
200
|
} else if (decision.missing_approval_kinds.includes("review")) {
|
|
201
201
|
nextObligation = "run_review";
|
|
202
|
-
} else if (decision.missing_approval_kinds.includes("user")) {
|
|
203
|
-
nextObligation = "authorize_user";
|
|
204
202
|
} else if (decision.complete) {
|
|
205
203
|
nextObligation = "complete";
|
|
206
204
|
}
|
|
@@ -176,7 +176,6 @@ function intentRefMatches(intent: TaskIntentV1, ref: TaskIntentRefV3): boolean {
|
|
|
176
176
|
function hasPrivilegedKind(action: TaskAction): boolean {
|
|
177
177
|
return (
|
|
178
178
|
action.type === "record_approval" ||
|
|
179
|
-
action.type === "record_user_approval" ||
|
|
180
179
|
action.type === "approve_breaking_intent_revision" ||
|
|
181
180
|
action.type === "request_rework" ||
|
|
182
181
|
action.type === "stop" ||
|
|
@@ -374,36 +373,6 @@ export function reduceTask(
|
|
|
374
373
|
appendHistory(record, action, from, approval.id, authorityAudit);
|
|
375
374
|
break;
|
|
376
375
|
}
|
|
377
|
-
case "record_user_approval": {
|
|
378
|
-
if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
|
|
379
|
-
throw new KernelInvariantError([
|
|
380
|
-
`cannot record user approval while state is ${stateOf(record)}`,
|
|
381
|
-
]);
|
|
382
|
-
const approval = action.approval;
|
|
383
|
-
if (approval.kind !== "user")
|
|
384
|
-
throw new KernelInvariantError([
|
|
385
|
-
"record_user_approval requires kind user",
|
|
386
|
-
]);
|
|
387
|
-
if (!authorityAudit || authorityAudit.authority_kind !== "user")
|
|
388
|
-
throw new KernelInvariantError([
|
|
389
|
-
"record_user_approval requires user authority",
|
|
390
|
-
]);
|
|
391
|
-
if (approval.task_revision !== record.intent_snapshot.revision)
|
|
392
|
-
throw new KernelInvariantError(["approval task_revision must equal the current intent revision"]);
|
|
393
|
-
if (approval.intent_content_hash !== record.intent_ref.content_hash)
|
|
394
|
-
throw new KernelInvariantError(["approval intent_content_hash must equal the current intent hash"]);
|
|
395
|
-
if (approval.diff_hash !== diffHash)
|
|
396
|
-
throw new KernelInvariantError(["approval diff_hash must equal the action diff hash"]);
|
|
397
|
-
if (record.attestations.some((item) => item.id === approval.id))
|
|
398
|
-
throw new KernelInvariantError([
|
|
399
|
-
`attestations contains duplicate id ${approval.id}`,
|
|
400
|
-
]);
|
|
401
|
-
if (approval.review_revision)
|
|
402
|
-
throw new KernelInvariantError(["review_revision is only valid on review approvals"]);
|
|
403
|
-
record.attestations.push({ ...approval, acceptance_results: [] });
|
|
404
|
-
appendHistory(record, action, from, approval.id, authorityAudit);
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
376
|
case "revise_intent":
|
|
408
377
|
case "approve_breaking_intent_revision": {
|
|
409
378
|
if (record.lifecycle !== "active")
|
|
@@ -57,7 +57,6 @@ export type AssuranceObligation =
|
|
|
57
57
|
| "submit_assurance"
|
|
58
58
|
| "run_qa"
|
|
59
59
|
| "run_review"
|
|
60
|
-
| "authorize_user"
|
|
61
60
|
| "complete"
|
|
62
61
|
| "none";
|
|
63
62
|
|
|
@@ -276,7 +275,6 @@ export type TaskAction =
|
|
|
276
275
|
| (TaskActionBase & { type: "record_finding"; finding: TaskFinding })
|
|
277
276
|
| (TaskActionBase & { type: "resolve_finding"; finding_id: string })
|
|
278
277
|
| (TaskActionBase & { type: "record_approval"; approval: TaskApprovalV2 })
|
|
279
|
-
| (TaskActionBase & { type: "record_user_approval"; approval: TaskApprovalV2 })
|
|
280
278
|
| (TaskActionBase & {
|
|
281
279
|
type: "revise_intent";
|
|
282
280
|
next_intent: TaskIntentV1;
|
|
@@ -730,7 +730,6 @@ const ACTION_V2_TYPES = [
|
|
|
730
730
|
"record_finding",
|
|
731
731
|
"resolve_finding",
|
|
732
732
|
"record_approval",
|
|
733
|
-
"record_user_approval",
|
|
734
733
|
"revise_intent",
|
|
735
734
|
"approve_breaking_intent_revision",
|
|
736
735
|
"request_rework",
|
|
@@ -831,8 +830,7 @@ export function parseTaskAction(raw: unknown): TaskAction {
|
|
|
831
830
|
};
|
|
832
831
|
break;
|
|
833
832
|
}
|
|
834
|
-
case "record_approval":
|
|
835
|
-
case "record_user_approval": {
|
|
833
|
+
case "record_approval": {
|
|
836
834
|
rejectUnknown(
|
|
837
835
|
value,
|
|
838
836
|
[...ACTION_BASE_FIELDS, "approval"],
|
|
@@ -59,7 +59,7 @@ Require exact host confirmation only for privileged effects:
|
|
|
59
59
|
override; and
|
|
60
60
|
- external writes whose target or impact cannot be safely reversed locally.
|
|
61
61
|
|
|
62
|
-
Routine Managed enrollment uses one
|
|
62
|
+
Routine Managed enrollment uses one current-Host native confirmation bound to the TaskIntent content hash after Planner validation. Explicit Plan-only requests stop with candidate artifacts and do not invoke Enrollment; execution-bearing requests open 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. The routine task proceeds from that single confirmation through enrollment, execution, and QA without a second human stop. 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.
|
|
63
63
|
|
|
64
64
|
## Parallel Read-Only Dispatch
|
|
65
65
|
|
|
@@ -64,3 +64,7 @@ collect completed child outputs, and feed them to
|
|
|
64
64
|
background work, mutate state, or own final Spec/Plan authority.
|
|
65
65
|
Agreement becomes framing evidence, Disagreement becomes decision criteria or
|
|
66
66
|
`BR-Q-*`, and strong-model blockers become risks or verification requirements.
|
|
67
|
+
|
|
68
|
+
When framing discusses later execution, describe Enrollment only as the current
|
|
69
|
+
Host's native gate. Never recommend another Host, worktree, or unmanaged
|
|
70
|
+
implementation as a fallback for a failed Managed authority interaction.
|
|
@@ -7,3 +7,7 @@ description: Use to run an enrolled TaskIntent to completion through Kernel-gove
|
|
|
7
7
|
|
|
8
8
|
Load [`../../dist/imm-loop.md`](../../dist/imm-loop.md), then follow that
|
|
9
9
|
canonical contract in the current host conversation.
|
|
10
|
+
|
|
11
|
+
All Managed authority gates use the current Host's native interaction. A failed
|
|
12
|
+
gate stays fail-closed and reports one same-Host recovery action; never suggest
|
|
13
|
+
another Host, worktree, or unmanaged implementation as a fallback.
|
|
@@ -7,3 +7,9 @@ description: Use to create or revise a spec and TaskIntent from requirements; ow
|
|
|
7
7
|
|
|
8
8
|
Load [`../../dist/imm-planner.md`](../../dist/imm-planner.md), then follow that
|
|
9
9
|
canonical contract. `mode: page_design` selects its page-design branch.
|
|
10
|
+
|
|
11
|
+
Plan-only requests stop after candidate Spec/TaskIntent validation. Requests that
|
|
12
|
+
include execution invoke the current Host's native Enrollment gate directly,
|
|
13
|
+
without chat pre-confirmation. Native-gate failure stays fail-closed in that Host:
|
|
14
|
+
report its reason and one retry action only; never suggest another Host, worktree,
|
|
15
|
+
or unmanaged implementation as a fallback.
|