immune-brain 3.4.0 → 3.5.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/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +84 -1
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +124 -6
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +8 -2
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +7 -1
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { execFileSync } from "node:child_process";
|
|
18
18
|
import { createHash, randomUUID } from "node:crypto";
|
|
19
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { join, resolve } from "node:path";
|
|
21
21
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
22
|
import { Type } from "typebox";
|
|
@@ -58,12 +58,15 @@ import {
|
|
|
58
58
|
clearTerminalTaskRailOnInput,
|
|
59
59
|
loopResultDetails,
|
|
60
60
|
notifyOnce,
|
|
61
|
+
presentTaskOverviewOverlay,
|
|
61
62
|
presentTaskRail,
|
|
62
63
|
presentTaskRailResult,
|
|
63
64
|
renderStructuredCall,
|
|
64
65
|
renderStructuredResult,
|
|
65
66
|
requestAuthorityDialog,
|
|
66
67
|
resetInteractionPresentation,
|
|
68
|
+
type TaskOverviewEntry,
|
|
69
|
+
type TaskRailState,
|
|
67
70
|
type UserAttentionEventV1,
|
|
68
71
|
type UserAttentionReason,
|
|
69
72
|
} from "./pi-canary-interaction";
|
|
@@ -346,6 +349,23 @@ export default function (
|
|
|
346
349
|
return { action: "continue" } as const;
|
|
347
350
|
});
|
|
348
351
|
|
|
352
|
+
pi.registerCommand("imm-tasks", {
|
|
353
|
+
handler: async (_args: string, ctx?: ExtensionContext) => {
|
|
354
|
+
if (!ctx || ctx.mode !== "tui") return;
|
|
355
|
+
try {
|
|
356
|
+
const view = await buildTaskOverview(ctx.cwd);
|
|
357
|
+
await presentTaskOverviewOverlay(ctx, view);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
notifyOnce(
|
|
360
|
+
ctx,
|
|
361
|
+
"task-overview:command",
|
|
362
|
+
`Task overview failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
363
|
+
"warning",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
349
369
|
pi.on("session_start", async (_event: unknown, ctx?: ExtensionContext) => {
|
|
350
370
|
progression.onSessionStart();
|
|
351
371
|
if (!ctx) return;
|
|
@@ -1107,6 +1127,69 @@ function diffHashOf(root: string, record: NonNullable<TaskRecordRead["record"]>)
|
|
|
1107
1127
|
// Kernel module; this wrapper only binds the host diff provider. The retired
|
|
1108
1128
|
// active-v2 migrator is gone: a v2 TaskRecord in the state layout is a
|
|
1109
1129
|
// fail-closed projection error, never an automatic migration trigger.
|
|
1130
|
+
/**
|
|
1131
|
+
* Read-only task overview for the /imm-tasks overlay: the active claim's
|
|
1132
|
+
* task plus every not-enrolled docs/plans/*.intent.json draft. Settled
|
|
1133
|
+
* history stays on the CLI (BR-DEC-3); no second state source is created.
|
|
1134
|
+
*/
|
|
1135
|
+
async function buildTaskOverview(root: string): Promise<{
|
|
1136
|
+
active: TaskOverviewEntry | null;
|
|
1137
|
+
pending: TaskOverviewEntry[];
|
|
1138
|
+
}> {
|
|
1139
|
+
const claim = await readBackendClaim(root);
|
|
1140
|
+
let active: TaskOverviewEntry | null = null;
|
|
1141
|
+
if (claim) {
|
|
1142
|
+
const projection = await projectAssuranceState(root, claim.task_id);
|
|
1143
|
+
if (!projection.error) {
|
|
1144
|
+
const state = projection.projection;
|
|
1145
|
+
const obligation = String(state.next_obligation);
|
|
1146
|
+
active = {
|
|
1147
|
+
task_id: claim.task_id,
|
|
1148
|
+
state: overviewRailState(state.lifecycle, obligation),
|
|
1149
|
+
result: `Assurance: ${obligation.replace(/_/g, " ")}`,
|
|
1150
|
+
next: `${state.fresh_acceptance_ids.length}/${state.fresh_acceptance_ids.length + state.missing_acceptance_ids.length} acceptance fresh · ${state.artifact_state}`,
|
|
1151
|
+
};
|
|
1152
|
+
} else {
|
|
1153
|
+
active = {
|
|
1154
|
+
task_id: claim.task_id,
|
|
1155
|
+
state: "Blocked",
|
|
1156
|
+
result: projection.error,
|
|
1157
|
+
next: "inspect authority state",
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const pending: TaskOverviewEntry[] = [];
|
|
1162
|
+
const plansDir = resolve(root, "docs/plans");
|
|
1163
|
+
if (existsSync(plansDir)) {
|
|
1164
|
+
for (const name of readdirSync(plansDir).sort()) {
|
|
1165
|
+
if (!name.endsWith(".intent.json")) continue;
|
|
1166
|
+
const taskId = name.slice(0, -".intent.json".length);
|
|
1167
|
+
if (claim && taskId === claim.task_id) continue;
|
|
1168
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(taskId)) continue;
|
|
1169
|
+
let summary: string;
|
|
1170
|
+
try {
|
|
1171
|
+
if (await readTaskTombstone(root, taskId)) continue;
|
|
1172
|
+
const intent = await parseTaskIntentV1(JSON.parse(readFileSync(join(plansDir, name), "utf8")));
|
|
1173
|
+
if (intent.task_id !== taskId) continue;
|
|
1174
|
+
summary = intent.goal;
|
|
1175
|
+
} catch {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
pending.push({ task_id: taskId, state: "Planning", result: summary, next: "not enrolled" });
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return { active, pending };
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function overviewRailState(lifecycle: string, obligation: string): TaskRailState {
|
|
1185
|
+
if (lifecycle === "done") return "Completed";
|
|
1186
|
+
if (lifecycle === "stopped") return "Stopped";
|
|
1187
|
+
if (obligation === "run_review") return "Reviewing";
|
|
1188
|
+
if (obligation === "submit_assurance" || obligation === "run_qa") return "Verifying";
|
|
1189
|
+
if (obligation === "resolve_findings" || obligation === "resolve_user_decision" || obligation === "revise_intent") return "Blocked";
|
|
1190
|
+
return "Working";
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1110
1193
|
async function projectAssuranceState(root: string, taskId: string): Promise<AssuranceProjectionResult> {
|
|
1111
1194
|
return projectAssurance(root, taskId, diffSnapshotOf);
|
|
1112
1195
|
}
|
|
@@ -28,11 +28,35 @@ export type TaskRailState =
|
|
|
28
28
|
| "Completed"
|
|
29
29
|
| "Stopped";
|
|
30
30
|
|
|
31
|
+
export interface TaskRailAcceptanceProgress {
|
|
32
|
+
current: number;
|
|
33
|
+
total: number;
|
|
34
|
+
acceptance_id: string;
|
|
35
|
+
state: "running" | "passed" | "failed";
|
|
36
|
+
elapsed_ms?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
export interface TaskRailView {
|
|
32
40
|
task_id: string;
|
|
33
41
|
state: TaskRailState;
|
|
34
42
|
result: string;
|
|
35
43
|
next: string;
|
|
44
|
+
/** Assurance phase label derived from the normalized Rail state. */
|
|
45
|
+
phase?: string;
|
|
46
|
+
/** Latest per-descriptor QA fact; rendered only while present. */
|
|
47
|
+
acceptance_progress?: TaskRailAcceptanceProgress;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TaskOverviewEntry {
|
|
51
|
+
task_id: string;
|
|
52
|
+
state: TaskRailState;
|
|
53
|
+
result: string;
|
|
54
|
+
next: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TaskOverviewView {
|
|
58
|
+
active: TaskOverviewEntry | null;
|
|
59
|
+
pending: TaskOverviewEntry[];
|
|
36
60
|
}
|
|
37
61
|
|
|
38
62
|
export interface AuthorityDialogAction<T extends string> {
|
|
@@ -187,11 +211,35 @@ export function presentTaskRailResult(
|
|
|
187
211
|
const rawState = string(details.state);
|
|
188
212
|
const result = string(details.result) ?? string(details.reason) ?? operation ?? rawState ?? "Task state updated";
|
|
189
213
|
const next = string(details.next_action) ?? "Follow the projected Obligation";
|
|
214
|
+
const current = details.current;
|
|
215
|
+
const total = details.total;
|
|
216
|
+
const acceptanceId = string(details.acceptance_id);
|
|
217
|
+
const progressPhase = string(details.acceptance_phase);
|
|
218
|
+
const hasAcceptanceProgress = typeof current === "number"
|
|
219
|
+
&& typeof total === "number"
|
|
220
|
+
&& acceptanceId !== undefined
|
|
221
|
+
&& (progressPhase === "running" || progressPhase === "passed" || progressPhase === "failed");
|
|
190
222
|
presentTaskRail(ctx, {
|
|
191
223
|
task_id: taskId,
|
|
192
|
-
state: railState({
|
|
224
|
+
state: railState({
|
|
225
|
+
lifecycle,
|
|
226
|
+
obligation: string(taskState?.next_obligation),
|
|
227
|
+
operation,
|
|
228
|
+
state: rawState,
|
|
229
|
+
stage: string(details.stage),
|
|
230
|
+
}),
|
|
193
231
|
result,
|
|
194
232
|
next,
|
|
233
|
+
phase: string(details.stage),
|
|
234
|
+
acceptance_progress: hasAcceptanceProgress
|
|
235
|
+
? {
|
|
236
|
+
current,
|
|
237
|
+
total,
|
|
238
|
+
acceptance_id: acceptanceId,
|
|
239
|
+
state: progressPhase,
|
|
240
|
+
elapsed_ms: typeof details.elapsed_ms === "number" ? details.elapsed_ms : undefined,
|
|
241
|
+
}
|
|
242
|
+
: undefined,
|
|
195
243
|
});
|
|
196
244
|
}
|
|
197
245
|
|
|
@@ -200,6 +248,33 @@ export function clearTerminalTaskRailOnInput(ctx: UiContext): void {
|
|
|
200
248
|
clearTaskRail(ctx);
|
|
201
249
|
}
|
|
202
250
|
|
|
251
|
+
export async function presentTaskOverviewOverlay(
|
|
252
|
+
ctx: UiContext & { mode?: string },
|
|
253
|
+
view: TaskOverviewView,
|
|
254
|
+
): Promise<void> {
|
|
255
|
+
if (ctx.mode !== undefined && ctx.mode !== "tui") {
|
|
256
|
+
// Non-TUI hosts have no overlay surface; the command stays a no-op.
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
await ctx.ui.custom<void>((_tui, theme, _keybindings, done) => {
|
|
261
|
+
const container = new Container();
|
|
262
|
+
const lines = renderTaskOverview(view, 120, theme);
|
|
263
|
+
for (const line of lines) container.addChild(new Text(line, 0, 0));
|
|
264
|
+
container.addChild(new Text(theme.fg("dim", "esc: close"), 1, 0));
|
|
265
|
+
return {
|
|
266
|
+
render: (width: number) => container.render(width),
|
|
267
|
+
invalidate: () => container.invalidate(),
|
|
268
|
+
handleInput: (data: string) => {
|
|
269
|
+
if (data === "\u001b" || data === "q") done(undefined);
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}, { overlay: true, overlayOptions: { anchor: "center", width: "80%", maxHeight: "80%" } });
|
|
273
|
+
} catch {
|
|
274
|
+
notifyOnce(ctx, "task-overview:render", "Task overview is unavailable; projections remain authoritative.", "warning");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
203
278
|
export function clearTaskRail(ctx: UiContext): void {
|
|
204
279
|
try {
|
|
205
280
|
ctx.ui.setWidget(TASK_RAIL_KEY, undefined);
|
|
@@ -315,20 +390,63 @@ function renderTaskRail(view: TaskRailView, width = 120, theme?: Theme): string[
|
|
|
315
390
|
const stateFormatted = formatTaskRailState(view.state, theme);
|
|
316
391
|
const label = (text: string) => (theme ? theme.fg("muted", text) : text);
|
|
317
392
|
const body = (text: string) => (theme ? theme.fg("dim", text) : text);
|
|
318
|
-
|
|
319
|
-
return [
|
|
393
|
+
const lines = [
|
|
320
394
|
`Task ${boundedMiddle(view.task_id, taskIdWidth)} · ${stateFormatted}`,
|
|
395
|
+
];
|
|
396
|
+
if (view.phase) {
|
|
397
|
+
lines.push(`${label("Phase:")} ${body(bounded(view.phase, availableContentWidth))}`);
|
|
398
|
+
}
|
|
399
|
+
if (view.acceptance_progress) {
|
|
400
|
+
const progress = view.acceptance_progress;
|
|
401
|
+
const symbol = progress.state === "passed" ? "✓" : progress.state === "failed" ? "✗" : "●";
|
|
402
|
+
const color = progress.state === "failed" ? "warning" : progress.state === "passed" ? "success" : "accent";
|
|
403
|
+
const elapsed = typeof progress.elapsed_ms === "number" ? ` ${progress.elapsed_ms}ms` : "";
|
|
404
|
+
const body = `${symbol} ${bounded(progress.acceptance_id, availableContentWidth - 12)}${elapsed}`;
|
|
405
|
+
lines.push(
|
|
406
|
+
theme
|
|
407
|
+
? `${label("Acceptance:")} ${theme.fg(color, `${progress.current}/${progress.total} `)}${theme.fg(color, body)}`
|
|
408
|
+
: `Acceptance: ${progress.current}/${progress.total} ${body}`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
lines.push(
|
|
321
412
|
`${label("Result:")} ${body(bounded(view.result, availableContentWidth))}`,
|
|
322
413
|
`${label("Next:")} ${body(bounded(view.next, availableContentWidth))}`,
|
|
323
|
-
|
|
414
|
+
);
|
|
415
|
+
return lines;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function renderTaskOverview(view: TaskOverviewView, width = 120, theme?: Theme): string[] {
|
|
419
|
+
const label = (text: string) => (theme ? theme.fg("muted", text) : text);
|
|
420
|
+
const head = (text: string) => (theme ? theme.fg("accent", theme.bold(text)) : text);
|
|
421
|
+
const lines = [head("Managed Tasks (read-only)")];
|
|
422
|
+
if (!view.active && view.pending.length === 0) {
|
|
423
|
+
lines.push(label("No active task; no pending TaskIntent drafts."));
|
|
424
|
+
return lines;
|
|
425
|
+
}
|
|
426
|
+
if (view.active) {
|
|
427
|
+
lines.push(`${label("Active:")} ${formatTaskRailState(view.active.state, theme)} ${bounded(view.active.task_id, 60)}`);
|
|
428
|
+
lines.push(`${label(" Result:")} ${bounded(view.active.result, 100)}`);
|
|
429
|
+
lines.push(`${label(" Next:")} ${bounded(view.active.next, 100)}`);
|
|
430
|
+
} else {
|
|
431
|
+
lines.push(label("Active: none"));
|
|
432
|
+
}
|
|
433
|
+
if (view.pending.length > 0) {
|
|
434
|
+
lines.push(label(`Pending enrollment (${view.pending.length}):`));
|
|
435
|
+
for (const entry of view.pending) {
|
|
436
|
+
lines.push(` ${formatTaskRailState(entry.state, theme)} ${bounded(entry.task_id, 60)} · ${bounded(entry.result, 60)}`);
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
lines.push(label("Pending enrollment: none"));
|
|
440
|
+
}
|
|
441
|
+
return lines;
|
|
324
442
|
}
|
|
325
443
|
|
|
326
|
-
function railState(input: { lifecycle?: string; obligation?: string; operation?: string; state?: string }): TaskRailState {
|
|
444
|
+
function railState(input: { lifecycle?: string; obligation?: string; operation?: string; state?: string; stage?: string }): TaskRailState {
|
|
327
445
|
if (input.state === "blocked" || input.state === "failed" || input.state === "settlement_unknown") return "Blocked";
|
|
328
446
|
if (input.lifecycle === "done") return "Completed";
|
|
329
447
|
if (input.lifecycle === "stopped") return "Stopped";
|
|
330
448
|
if (input.state === "awaiting_user" || input.operation === "request_authorization") return "Approval required";
|
|
331
|
-
if (input.operation === "advance_assurance") return "Verifying";
|
|
449
|
+
if (input.operation === "advance_assurance" || input.operation === "qa" || input.stage === "verifying") return "Verifying";
|
|
332
450
|
if (input.operation === "submit_review" || input.obligation === "run_review") return "Reviewing";
|
|
333
451
|
if (input.lifecycle === "active") return "Working";
|
|
334
452
|
if (input.state === "running") return "Planning";
|
|
@@ -42,7 +42,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// plugins/immune-brain/runtime/plugin_version.ts
|
|
45
|
-
var PLUGIN_VERSION = "3.
|
|
45
|
+
var PLUGIN_VERSION = "3.5.0";
|
|
46
46
|
|
|
47
47
|
// plugins/immune-brain/runtime/claude/interaction.ts
|
|
48
48
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -1380,7 +1380,13 @@ class AssuranceCoordinator {
|
|
|
1380
1380
|
ensureOperationLive();
|
|
1381
1381
|
qaVerdict = await this.ports.runQa(assurance.snapshot, assurance.descriptors, runner, {
|
|
1382
1382
|
signal: operationController.signal,
|
|
1383
|
-
onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
|
|
1383
|
+
onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
|
|
1384
|
+
current: item.index,
|
|
1385
|
+
total: item.total,
|
|
1386
|
+
acceptance_id: item.acceptance_id,
|
|
1387
|
+
acceptance_phase: item.phase,
|
|
1388
|
+
elapsed_ms: item.elapsed_ms
|
|
1389
|
+
})
|
|
1384
1390
|
});
|
|
1385
1391
|
ensureOperationLive();
|
|
1386
1392
|
const invocation = this.openInvocation(taskId);
|
|
@@ -559,7 +559,13 @@ export class AssuranceCoordinator {
|
|
|
559
559
|
ensureOperationLive();
|
|
560
560
|
qaVerdict = await this.ports.runQa(assurance.snapshot, assurance.descriptors, runner, {
|
|
561
561
|
signal: operationController.signal,
|
|
562
|
-
onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
|
|
562
|
+
onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
|
|
563
|
+
current: item.index,
|
|
564
|
+
total: item.total,
|
|
565
|
+
acceptance_id: item.acceptance_id,
|
|
566
|
+
acceptance_phase: item.phase,
|
|
567
|
+
elapsed_ms: item.elapsed_ms,
|
|
568
|
+
}),
|
|
563
569
|
});
|
|
564
570
|
ensureOperationLive();
|
|
565
571
|
const invocation = this.openInvocation(taskId);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/plugin_versioning.ts from the root package.json.
|
|
2
|
-
export const PLUGIN_VERSION = "3.
|
|
2
|
+
export const PLUGIN_VERSION = "3.5.0" as const;
|