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 +3 -3
- package/README.zh-CN.md +3 -3
- 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 +87 -97
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +144 -11
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +2 -2
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +102 -53
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +17 -8
- package/plugins/immune-brain/dist/imm-loop.md +134 -150
- package/plugins/immune-brain/dist/imm-planner.md +71 -77
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +26 -6
- package/plugins/immune-brain/runtime/assurance/qa.ts +85 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +1 -45
- package/plugins/immune-brain/runtime/kernel/intent.ts +7 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
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` (
|
|
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` |
|
|
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
|
|
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
|
|
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` |
|
|
153
|
+
| Initiative 载体 | `local` / `github` | 无默认,Planner 询问 | 仅当提案拆分为多个 TaskIntent 时生效 |
|
|
154
154
|
| Advisory subagent | 允许 / 单人 | 允许 | 受 Pi host 策略与用户显式指令约束 |
|
|
155
155
|
|
|
156
|
-
优先级:**当前消息 > 仓库
|
|
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
|
@@ -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 {
|
|
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 "
|
|
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({
|
|
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(
|
|
269
|
-
`${theme.fg("muted", "Result:")} ${theme.fg(
|
|
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 (
|
|
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: "
|
|
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: "
|
|
53
|
+
isolation: "off",
|
|
54
54
|
run_in_background: false,
|
|
55
55
|
max_turns: input.max_turns ?? 16,
|
|
56
56
|
resume: "",
|