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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Immune-Brain agent skill system",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Immune-Brain Claude Code Host: native Enrollment, QA, Review, and Kernel settlement.",
5
5
  "author": {
6
6
  "name": "Immune-Brain Team"
@@ -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({ lifecycle, obligation: string(taskState?.next_obligation), operation, state: rawState }),
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.4.0";
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}`, { current: item.index, total: item.total, acceptance_id: item.acceptance_id })
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}`, { current: item.index, total: item.total, acceptance_id: item.acceptance_id }),
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.4.0" as const;
2
+ export const PLUGIN_VERSION = "3.5.0" as const;