immune-brain 3.4.0 → 3.6.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 CHANGED
@@ -139,7 +139,7 @@ Key invariants:
139
139
 
140
140
  ## Configuration
141
141
 
142
- Immune-Brain has **no separate config file**. Preferences live in `AGENTS.md` (repo root or `~/.pi/agent/AGENTS.md`):
142
+ Immune-Brain has **no separate config file**. Preferences live in your host's agent instruction file at the repo root — `AGENTS.md` (Pi) or `CLAUDE.md` (Claude Code):
143
143
 
144
144
  ```md
145
145
  ## Immune-Brain Preferences
@@ -150,10 +150,10 @@ Immune-Brain has **no separate config file**. Preferences live in `AGENTS.md` (r
150
150
  | Preference | Options | Default | Notes |
151
151
  |---|---|---|---|
152
152
  | Reply language | any natural language | repo `AGENTS.md` | Machine contracts / paths stay literal |
153
- | Initiative carrier | `local` / `github` | `github` | Only matters when a proposal splits across multiple TaskIntents |
153
+ | Initiative carrier | `local` / `github` | none — Planner asks | Only matters when a proposal splits across multiple TaskIntents |
154
154
  | Advisory subagents | allowed / solo | allowed | Respects Pi host policy + explicit user instruction |
155
155
 
156
- Precedence: **current message > repo `AGENTS.md` > `~/.pi/agent/AGENTS.md` > skill default**.
156
+ Precedence: **current message > repo agent instruction file > user-level agent instruction file > ask**. Skills read these files directly, so a preference works even when the host does not auto-load that file.
157
157
 
158
158
  See [`docs/reference/immune-brain-config.md`](docs/reference/immune-brain-config.md) for details.
159
159
 
package/README.zh-CN.md CHANGED
@@ -139,7 +139,7 @@ Executor、QA、Review、Compounder 等为 `imm-loop` 内部调度的角色,
139
139
 
140
140
  ## 配置
141
141
 
142
- Immune-Brain **没有独立配置文件**,偏好设置写在 `AGENTS.md`(仓库根目录或 `~/.pi/agent/AGENTS.md`):
142
+ Immune-Brain **没有独立配置文件**,偏好设置写在仓库根目录下当前 Host 的 agent 指令文件里——`AGENTS.md`(Pi)或 `CLAUDE.md`(Claude Code):
143
143
 
144
144
  ```md
145
145
  ## Immune-Brain Preferences
@@ -150,10 +150,10 @@ Immune-Brain **没有独立配置文件**,偏好设置写在 `AGENTS.md`(仓
150
150
  | 偏好 | 选项 | 默认 | 说明 |
151
151
  |---|---|---|---|
152
152
  | 回复语言 | 任意自然语言 | 仓库 `AGENTS.md` | 机器契约/路径/标识符保持原文 |
153
- | Initiative 载体 | `local` / `github` | `github` | 仅当提案拆分为多个 TaskIntent 时生效 |
153
+ | Initiative 载体 | `local` / `github` | 无默认,Planner 询问 | 仅当提案拆分为多个 TaskIntent 时生效 |
154
154
  | Advisory subagent | 允许 / 单人 | 允许 | 受 Pi host 策略与用户显式指令约束 |
155
155
 
156
- 优先级:**当前消息 > 仓库 `AGENTS.md` > `~/.pi/agent/AGENTS.md` > Skill 默认值**。
156
+ 优先级:**当前消息 > 仓库 agent 指令文件 > 用户级 agent 指令文件 > 询问**。Skill 会直接读取这些文件,因此即使 Host 不自动加载该文件,偏好依然生效。
157
157
 
158
158
  详见 [`docs/reference/immune-brain-config.md`](docs/reference/immune-brain-config.md)。
159
159
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.4.0",
3
+ "version": "3.6.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.6.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";
@@ -26,8 +26,6 @@ import {
26
26
  canonicalDescriptorBytes,
27
27
  resolveBunRunner,
28
28
  assertRunnerCompatible,
29
- runFixedVerification,
30
- VerificationAbortedError,
31
29
  findingsDigest,
32
30
  type FrozenRunner,
33
31
  type VerificationDescriptor,
@@ -43,7 +41,7 @@ import {
43
41
  type ReviewRevision,
44
42
  } from "./pi-canary-review-bundle";
45
43
  import type { InvocationToken } from "./pi-canary-invocations";
46
- import { qaFindingId } from "./pi-canary-qa-findings";
44
+ import { runDeterministicQa } from "../runtime/assurance/qa";
47
45
  import {
48
46
  reservedAgentParams,
49
47
  type ReservedAgentParams,
@@ -58,12 +56,15 @@ import {
58
56
  clearTerminalTaskRailOnInput,
59
57
  loopResultDetails,
60
58
  notifyOnce,
59
+ presentTaskOverviewOverlay,
61
60
  presentTaskRail,
62
61
  presentTaskRailResult,
63
62
  renderStructuredCall,
64
63
  renderStructuredResult,
65
64
  requestAuthorityDialog,
66
65
  resetInteractionPresentation,
66
+ type TaskOverviewEntry,
67
+ type TaskRailState,
67
68
  type UserAttentionEventV1,
68
69
  type UserAttentionReason,
69
70
  } from "./pi-canary-interaction";
@@ -346,6 +347,23 @@ export default function (
346
347
  return { action: "continue" } as const;
347
348
  });
348
349
 
350
+ pi.registerCommand("imm-tasks", {
351
+ handler: async (_args: string, ctx?: ExtensionContext) => {
352
+ if (!ctx || ctx.mode !== "tui") return;
353
+ try {
354
+ const view = await buildTaskOverview(ctx.cwd);
355
+ await presentTaskOverviewOverlay(ctx, view);
356
+ } catch (error) {
357
+ notifyOnce(
358
+ ctx,
359
+ "task-overview:command",
360
+ `Task overview failed: ${error instanceof Error ? error.message : String(error)}`,
361
+ "warning",
362
+ );
363
+ }
364
+ },
365
+ });
366
+
349
367
  pi.on("session_start", async (_event: unknown, ctx?: ExtensionContext) => {
350
368
  progression.onSessionStart();
351
369
  if (!ctx) return;
@@ -1107,6 +1125,69 @@ function diffHashOf(root: string, record: NonNullable<TaskRecordRead["record"]>)
1107
1125
  // Kernel module; this wrapper only binds the host diff provider. The retired
1108
1126
  // active-v2 migrator is gone: a v2 TaskRecord in the state layout is a
1109
1127
  // fail-closed projection error, never an automatic migration trigger.
1128
+ /**
1129
+ * Read-only task overview for the /imm-tasks overlay: the active claim's
1130
+ * task plus every not-enrolled docs/plans/*.intent.json draft. Settled
1131
+ * history stays on the CLI (BR-DEC-3); no second state source is created.
1132
+ */
1133
+ async function buildTaskOverview(root: string): Promise<{
1134
+ active: TaskOverviewEntry | null;
1135
+ pending: TaskOverviewEntry[];
1136
+ }> {
1137
+ const claim = await readBackendClaim(root);
1138
+ let active: TaskOverviewEntry | null = null;
1139
+ if (claim) {
1140
+ const projection = await projectAssuranceState(root, claim.task_id);
1141
+ if (!projection.error) {
1142
+ const state = projection.projection;
1143
+ const obligation = String(state.next_obligation);
1144
+ active = {
1145
+ task_id: claim.task_id,
1146
+ state: overviewRailState(state.lifecycle, obligation),
1147
+ result: `Assurance: ${obligation.replace(/_/g, " ")}`,
1148
+ next: `${state.fresh_acceptance_ids.length}/${state.fresh_acceptance_ids.length + state.missing_acceptance_ids.length} acceptance fresh · ${state.artifact_state}`,
1149
+ };
1150
+ } else {
1151
+ active = {
1152
+ task_id: claim.task_id,
1153
+ state: "Blocked",
1154
+ result: projection.error,
1155
+ next: "inspect authority state",
1156
+ };
1157
+ }
1158
+ }
1159
+ const pending: TaskOverviewEntry[] = [];
1160
+ const plansDir = resolve(root, "docs/plans");
1161
+ if (existsSync(plansDir)) {
1162
+ for (const name of readdirSync(plansDir).sort()) {
1163
+ if (!name.endsWith(".intent.json")) continue;
1164
+ const taskId = name.slice(0, -".intent.json".length);
1165
+ if (claim && taskId === claim.task_id) continue;
1166
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(taskId)) continue;
1167
+ let summary: string;
1168
+ try {
1169
+ if (await readTaskTombstone(root, taskId)) continue;
1170
+ const intent = await parseTaskIntentV1(JSON.parse(readFileSync(join(plansDir, name), "utf8")));
1171
+ if (intent.task_id !== taskId) continue;
1172
+ summary = intent.goal;
1173
+ } catch {
1174
+ continue;
1175
+ }
1176
+ pending.push({ task_id: taskId, state: "Planning", result: summary, next: "not enrolled" });
1177
+ }
1178
+ }
1179
+ return { active, pending };
1180
+ }
1181
+
1182
+ function overviewRailState(lifecycle: string, obligation: string): TaskRailState {
1183
+ if (lifecycle === "done") return "Completed";
1184
+ if (lifecycle === "stopped") return "Stopped";
1185
+ if (obligation === "run_review") return "Reviewing";
1186
+ if (obligation === "submit_assurance" || obligation === "run_qa") return "Verifying";
1187
+ if (obligation === "resolve_findings" || obligation === "resolve_user_decision" || obligation === "revise_intent") return "Blocked";
1188
+ return "Working";
1189
+ }
1190
+
1110
1191
  async function projectAssuranceState(root: string, taskId: string): Promise<AssuranceProjectionResult> {
1111
1192
  return projectAssurance(root, taskId, diffSnapshotOf);
1112
1193
  }
@@ -1185,97 +1266,6 @@ async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: str
1185
1266
  return reconcileReviewRefs(root, live);
1186
1267
  }
1187
1268
 
1188
- export interface QaVerificationProgressInput {
1189
- index: number;
1190
- total: number;
1191
- acceptance_id: string;
1192
- phase: "running" | "passed" | "failed";
1193
- elapsed_ms: number;
1194
- }
1195
-
1196
- export function boundedVerificationFailureDetail(stdout: string, stderr: string): string {
1197
- const output = (stderr || stdout).trim();
1198
- const limit = 500;
1199
- if (output.length <= limit) return output;
1200
- const marker = "\n... output omitted ...\n";
1201
- const available = limit - marker.length;
1202
- const headLength = Math.floor(available / 3);
1203
- const tailLength = available - headLength;
1204
- return `${output.slice(0, headLength)}${marker}${output.slice(-tailLength)}`;
1205
- }
1206
-
1207
- export async function runDeterministicQa(
1208
- snapshot: SnapshotDescriptor,
1209
- descriptors: Map<string, VerificationDescriptor>,
1210
- runner: FrozenRunner,
1211
- options: {
1212
- signal?: AbortSignal;
1213
- onProgress?: (progress: QaVerificationProgressInput) => void;
1214
- runVerification?: typeof runFixedVerification;
1215
- } = {},
1216
- ): Promise<AssuranceVerdict> {
1217
- if (snapshot.role !== "qa") throw new Error("deterministic QA requires qa role");
1218
- if (options.signal?.aborted) throw new VerificationAbortedError();
1219
- const findings: NonNullable<AssuranceVerdict["findings"]> = [];
1220
- const runVerification = options.runVerification ?? runFixedVerification;
1221
- for (const [offset, item] of snapshot.acceptance.entries()) {
1222
- if (options.signal?.aborted) throw new VerificationAbortedError();
1223
- const descriptor = descriptors.get(item.id);
1224
- if (!descriptor) throw new Error(`verification descriptor missing for ${item.id}`);
1225
- const startedAt = Date.now();
1226
- options.onProgress?.({
1227
- index: offset + 1,
1228
- total: snapshot.acceptance.length,
1229
- acceptance_id: item.id,
1230
- phase: "running",
1231
- elapsed_ms: 0,
1232
- });
1233
- const result = await runVerification(snapshot.root, descriptor, runner, {
1234
- signal: options.signal,
1235
- });
1236
- const failed = result.exit_code !== 0 || result.timed_out;
1237
- options.onProgress?.({
1238
- index: offset + 1,
1239
- total: snapshot.acceptance.length,
1240
- acceptance_id: item.id,
1241
- phase: failed ? "failed" : "passed",
1242
- elapsed_ms: Date.now() - startedAt,
1243
- });
1244
- if (failed) {
1245
- const detail = boundedVerificationFailureDetail(result.stdout, result.stderr);
1246
- findings.push({
1247
- id: qaFindingId(item.id, snapshotDigest(snapshot)),
1248
- kind: "blocking",
1249
- acceptance_id: item.id,
1250
- summary: `verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""}) stdout=${result.stdout.length}B stderr=${result.stderr.length}B${detail ? `: ${detail}` : ""}`,
1251
- findings_digest: "",
1252
- });
1253
- }
1254
- }
1255
- if (findings.length > 0) {
1256
- return {
1257
- contract: "assurance_kernel/assurance_verdict/v2",
1258
- role: "qa",
1259
- task_id: snapshot.task_id,
1260
- snapshot_digest: snapshotDigest(snapshot),
1261
- decision: "rework",
1262
- findings,
1263
- };
1264
- }
1265
- return {
1266
- contract: "assurance_kernel/assurance_verdict/v2",
1267
- role: "qa",
1268
- task_id: snapshot.task_id,
1269
- snapshot_digest: snapshotDigest(snapshot),
1270
- decision: "pass",
1271
- approval: {
1272
- kind: "qa",
1273
- authority_role: "qa",
1274
- summary: `all ${snapshot.acceptance.length} fixed verification descriptor(s) passed`,
1275
- },
1276
- };
1277
- }
1278
-
1279
1269
  async function applyAssuranceVerdict(
1280
1270
  ctx: ExtensionContext,
1281
1271
  snapshot: SnapshotDescriptor,
@@ -1712,7 +1702,7 @@ async function enrichAssuranceResult(
1712
1702
 
1713
1703
  function nextActionForAssuranceResult(result: Record<string, unknown>, taskState: AssuranceTaskState): string {
1714
1704
  if ("error" in taskState) return "inspect authority state";
1715
- if (result.state === "review_preparation_failed") return "retry advance_assurance";
1705
+ if (result.state === "review_preparation_failed") return "repair Review preparation, then retry advance_assurance; QA is already committed";
1716
1706
  if (result.code === "verdict_invalid") return "fix the verdict payload and resubmit submit_review; the Review reservation remains active; do not re-dispatch the reviewer";
1717
1707
  if (taskState.lifecycle === "done" || taskState.lifecycle === "stopped") return "none";
1718
1708
  switch (result.state) {
@@ -1723,7 +1713,7 @@ function nextActionForAssuranceResult(result: Record<string, unknown>, taskState
1723
1713
  case "stopped": return "none";
1724
1714
  case "rework": return "repair findings, then advance assurance";
1725
1715
  case "cancelled": return "retry the interrupted foreground operation";
1726
- case "settlement_unknown": return "inspect authority state";
1716
+ case "settlement_unknown": return "advance_assurance to reconcile Kernel state; do not replay the uncertain write";
1727
1717
  case "blocked":
1728
1718
  case "failed":
1729
1719
  default: return taskState.next_obligation;
@@ -28,11 +28,36 @@ 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
+ recovery?: string;
47
+ /** Latest per-descriptor QA fact; rendered only while present. */
48
+ acceptance_progress?: TaskRailAcceptanceProgress;
49
+ }
50
+
51
+ export interface TaskOverviewEntry {
52
+ task_id: string;
53
+ state: TaskRailState;
54
+ result: string;
55
+ next: string;
56
+ }
57
+
58
+ export interface TaskOverviewView {
59
+ active: TaskOverviewEntry | null;
60
+ pending: TaskOverviewEntry[];
36
61
  }
37
62
 
38
63
  export interface AuthorityDialogAction<T extends string> {
@@ -185,13 +210,38 @@ export function presentTaskRailResult(
185
210
  const lifecycle = string(taskState?.lifecycle) ?? string(details.lifecycle) ?? string(details.stage);
186
211
  const operation = string(details.operation);
187
212
  const rawState = string(details.state);
188
- const result = string(details.result) ?? string(details.reason) ?? operation ?? rawState ?? "Task state updated";
213
+ const result = string(details.result) ?? string(details.reason) ?? string(details.summary) ?? operation ?? rawState ?? "Task state updated";
189
214
  const next = string(details.next_action) ?? "Follow the projected Obligation";
215
+ const current = details.current;
216
+ const total = details.total;
217
+ const acceptanceId = string(details.acceptance_id);
218
+ const progressPhase = string(details.acceptance_phase);
219
+ const hasAcceptanceProgress = typeof current === "number"
220
+ && typeof total === "number"
221
+ && acceptanceId !== undefined
222
+ && (progressPhase === "running" || progressPhase === "passed" || progressPhase === "failed");
190
223
  presentTaskRail(ctx, {
191
224
  task_id: taskId,
192
- state: railState({ lifecycle, obligation: string(taskState?.next_obligation), operation, state: rawState }),
225
+ state: railState({
226
+ lifecycle,
227
+ obligation: string(taskState?.next_obligation),
228
+ operation,
229
+ state: rawState,
230
+ stage: string(details.stage),
231
+ }),
193
232
  result,
194
233
  next,
234
+ phase: string(details.stage),
235
+ recovery: recoveryHint(details),
236
+ acceptance_progress: hasAcceptanceProgress
237
+ ? {
238
+ current,
239
+ total,
240
+ acceptance_id: acceptanceId,
241
+ state: progressPhase,
242
+ elapsed_ms: typeof details.elapsed_ms === "number" ? details.elapsed_ms : undefined,
243
+ }
244
+ : undefined,
195
245
  });
196
246
  }
197
247
 
@@ -200,6 +250,33 @@ export function clearTerminalTaskRailOnInput(ctx: UiContext): void {
200
250
  clearTaskRail(ctx);
201
251
  }
202
252
 
253
+ export async function presentTaskOverviewOverlay(
254
+ ctx: UiContext & { mode?: string },
255
+ view: TaskOverviewView,
256
+ ): Promise<void> {
257
+ if (ctx.mode !== undefined && ctx.mode !== "tui") {
258
+ // Non-TUI hosts have no overlay surface; the command stays a no-op.
259
+ return;
260
+ }
261
+ try {
262
+ await ctx.ui.custom<void>((_tui, theme, _keybindings, done) => {
263
+ const container = new Container();
264
+ const lines = renderTaskOverview(view, 120, theme);
265
+ for (const line of lines) container.addChild(new Text(line, 0, 0));
266
+ container.addChild(new Text(theme.fg("dim", "esc: close"), 1, 0));
267
+ return {
268
+ render: (width: number) => container.render(width),
269
+ invalidate: () => container.invalidate(),
270
+ handleInput: (data: string) => {
271
+ if (data === "\u001b" || data === "q") done(undefined);
272
+ },
273
+ };
274
+ }, { overlay: true, overlayOptions: { anchor: "center", width: "80%", maxHeight: "80%" } });
275
+ } catch {
276
+ notifyOnce(ctx, "task-overview:render", "Task overview is unavailable; projections remain authoritative.", "warning");
277
+ }
278
+ }
279
+
203
280
  export function clearTaskRail(ctx: UiContext): void {
204
281
  try {
205
282
  ctx.ui.setWidget(TASK_RAIL_KEY, undefined);
@@ -261,14 +338,17 @@ export function renderStructuredResult(
261
338
  const taskState = record(details.task_state);
262
339
  const lifecycle = string(taskState?.lifecycle) ?? string(details.lifecycle) ?? string(details.stage);
263
340
  const state = string(details.state) ?? "unknown";
264
- const summary = string(details.result) ?? string(details.reason) ?? string(details.operation) ?? state;
341
+ const summary = string(details.result) ?? string(details.reason) ?? string(details.summary) ?? string(details.operation) ?? state;
265
342
  const next = string(details.next_action) ?? "No action reported";
266
343
  const terminal = lifecycle === "done" || lifecycle === "stopped";
344
+ const blocked = railState({ state }) === "Blocked";
267
345
  const lines = [
268
- `${theme.fg("muted", "State:")} ${theme.fg(state === "blocked" || state === "failed" ? "warning" : "accent", lifecycle ?? state)}`,
269
- `${theme.fg("muted", "Result:")} ${theme.fg(state === "blocked" || state === "failed" ? "warning" : "dim", summary)}`,
346
+ `${theme.fg("muted", "State:")} ${theme.fg(blocked ? "warning" : "accent", blocked ? "Blocked" : lifecycle ?? state)}`,
347
+ `${theme.fg("muted", "Result:")} ${theme.fg(blocked ? "warning" : "dim", summary)}`,
270
348
  `${theme.fg("muted", "Next:")} ${theme.fg("dim", next)}`,
271
349
  ];
350
+ const recovery = recoveryHint(details);
351
+ if (recovery) lines.push(`${theme.fg("muted", "Recovery:")} ${theme.fg("dim", recovery)}`);
272
352
  if (terminal && taskState) lines.push(...renderFinalLines(taskState, theme));
273
353
  return new Text(lines.join("\n"), 0, 0);
274
354
  }
@@ -315,20 +395,73 @@ function renderTaskRail(view: TaskRailView, width = 120, theme?: Theme): string[
315
395
  const stateFormatted = formatTaskRailState(view.state, theme);
316
396
  const label = (text: string) => (theme ? theme.fg("muted", text) : text);
317
397
  const body = (text: string) => (theme ? theme.fg("dim", text) : text);
318
-
319
- return [
398
+ const lines = [
320
399
  `Task ${boundedMiddle(view.task_id, taskIdWidth)} · ${stateFormatted}`,
400
+ ];
401
+ if (view.phase) {
402
+ lines.push(`${label("Phase:")} ${body(bounded(view.phase, availableContentWidth))}`);
403
+ }
404
+ if (view.recovery) lines.push(`${label("Recovery:")} ${body(bounded(view.recovery, availableContentWidth))}`);
405
+ if (view.acceptance_progress) {
406
+ const progress = view.acceptance_progress;
407
+ const symbol = progress.state === "passed" ? "✓" : progress.state === "failed" ? "✗" : "●";
408
+ const color = progress.state === "failed" ? "warning" : progress.state === "passed" ? "success" : "accent";
409
+ const elapsed = typeof progress.elapsed_ms === "number" ? ` ${progress.elapsed_ms}ms` : "";
410
+ const body = `${symbol} ${bounded(progress.acceptance_id, availableContentWidth - 12)}${elapsed}`;
411
+ lines.push(
412
+ theme
413
+ ? `${label("Acceptance:")} ${theme.fg(color, `${progress.current}/${progress.total} `)}${theme.fg(color, body)}`
414
+ : `Acceptance: ${progress.current}/${progress.total} ${body}`,
415
+ );
416
+ }
417
+ lines.push(
321
418
  `${label("Result:")} ${body(bounded(view.result, availableContentWidth))}`,
322
419
  `${label("Next:")} ${body(bounded(view.next, availableContentWidth))}`,
323
- ];
420
+ );
421
+ return lines;
422
+ }
423
+
424
+ export function renderTaskOverview(view: TaskOverviewView, width = 120, theme?: Theme): string[] {
425
+ const label = (text: string) => (theme ? theme.fg("muted", text) : text);
426
+ const head = (text: string) => (theme ? theme.fg("accent", theme.bold(text)) : text);
427
+ const lines = [head("Managed Tasks (read-only)")];
428
+ if (!view.active && view.pending.length === 0) {
429
+ lines.push(label("No active task; no pending TaskIntent drafts."));
430
+ return lines;
431
+ }
432
+ if (view.active) {
433
+ lines.push(`${label("Active:")} ${formatTaskRailState(view.active.state, theme)} ${bounded(view.active.task_id, 60)}`);
434
+ lines.push(`${label(" Result:")} ${bounded(view.active.result, 100)}`);
435
+ lines.push(`${label(" Next:")} ${bounded(view.active.next, 100)}`);
436
+ } else {
437
+ lines.push(label("Active: none"));
438
+ }
439
+ if (view.pending.length > 0) {
440
+ lines.push(label(`Pending enrollment (${view.pending.length}):`));
441
+ for (const entry of view.pending) {
442
+ lines.push(` ${formatTaskRailState(entry.state, theme)} ${bounded(entry.task_id, 60)} · ${bounded(entry.result, 60)}`);
443
+ }
444
+ } else {
445
+ lines.push(label("Pending enrollment: none"));
446
+ }
447
+ return lines;
448
+ }
449
+
450
+ function recoveryHint(details: Record<string, unknown>): string | undefined {
451
+ if (details.code === "verdict_invalid") return "Correct Review payload; reservation retained";
452
+ if (details.state === "review_preparation_failed") return "Repair Review preparation; QA already committed";
453
+ if (details.state === "settlement_unknown") return "Reconcile Kernel state; do not replay writes";
454
+ if (details.state === "authority_conflict") return "Resolve authority conflict before resuming";
455
+ if (details.state === "rework") return "Repair blocking findings; retain unrelated fresh evidence";
456
+ return undefined;
324
457
  }
325
458
 
326
- function railState(input: { lifecycle?: string; obligation?: string; operation?: string; state?: string }): TaskRailState {
327
- if (input.state === "blocked" || input.state === "failed" || input.state === "settlement_unknown") return "Blocked";
459
+ function railState(input: { lifecycle?: string; obligation?: string; operation?: string; state?: string; stage?: string }): TaskRailState {
460
+ if (["blocked", "failed", "settlement_unknown", "review_preparation_failed", "authority_conflict", "rework"].includes(input.state ?? "")) return "Blocked";
328
461
  if (input.lifecycle === "done") return "Completed";
329
462
  if (input.lifecycle === "stopped") return "Stopped";
330
463
  if (input.state === "awaiting_user" || input.operation === "request_authorization") return "Approval required";
331
- if (input.operation === "advance_assurance") return "Verifying";
464
+ if (input.operation === "advance_assurance" || input.operation === "qa" || input.stage === "verifying") return "Verifying";
332
465
  if (input.operation === "submit_review" || input.obligation === "run_review") return "Reviewing";
333
466
  if (input.lifecycle === "active") return "Working";
334
467
  if (input.state === "running") return "Planning";
@@ -9,7 +9,7 @@ export interface ReservedAgentParams {
9
9
  thinking: "";
10
10
  inherit_context: false;
11
11
  isolated: true;
12
- isolation: "worktree";
12
+ isolation: "off";
13
13
  run_in_background: false;
14
14
  max_turns: number;
15
15
  resume: "";
@@ -50,7 +50,7 @@ export function reservedAgentParams(input: {
50
50
  thinking: "",
51
51
  inherit_context: false,
52
52
  isolated: true,
53
- isolation: "worktree",
53
+ isolation: "off",
54
54
  run_in_background: false,
55
55
  max_turns: input.max_turns ?? 16,
56
56
  resume: "",