immune-brain 3.3.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/README.md CHANGED
@@ -14,7 +14,7 @@ Immune-Brain adds a structured engineering workflow on top of Pi:
14
14
  - **Plans become trackable tasks** (`TaskIntent` + `TaskRecord`) so progress survives across sessions, not just chat history.
15
15
  - **Quality is enforced by code, not promises** — automated QA and isolated review must pass before a task is marked done.
16
16
 
17
- Pi and Claude Code are the supported hosts. Undeclared adapters remain unsupported. Minimum Claude Code is `2.1.199`. Historical real-Host evidence is archived under [docs/verification/archive/](docs/verification/archive/); deterministic package and authority checks gate current releases. Either host can use the model provider you configure — Immune-Brain works on top of Kernel authority, not a vendor chat.
17
+ Pi and Claude Code are the supported hosts. Undeclared adapters remain unsupported. Minimum Claude Code is `2.1.236`, the lowest version verified with interactive server-initiated MCP elicitation. Current real-Host evidence is recorded in [Claude native elicitation conformance](docs/verification/claude-native-elicitation-authority-conformance.md); historical reports remain under [docs/verification/archive/](docs/verification/archive/). Either host can use the model provider you configure — Immune-Brain works on top of Kernel authority, not a vendor chat.
18
18
 
19
19
  ---
20
20
 
@@ -191,7 +191,7 @@ docs/specs/ # Living specs (updated in place)
191
191
 
192
192
  **QA failed — what now?** QA returns `rework` or `replan_required`. `imm-loop` routes back to the executor or to `imm-planner` for scope changes. No manual reset needed.
193
193
 
194
- **Can I use it outside Pi?** Yes. Local interactive Claude Code is supported from version `2.1.199`; use the Claude plugin for the same Kernel-backed workflow.
194
+ **Can I use it outside Pi?** Yes. Local interactive Claude Code is supported from version `2.1.236`; its plugin uses a digest-bound native MCP elicitation gate for the same Kernel-backed workflow.
195
195
 
196
196
  ---
197
197
 
package/README.zh-CN.md CHANGED
@@ -14,7 +14,7 @@ Immune-Brain 在 Pi 之上提供结构化的工程工作流:
14
14
  - **计划变为可追踪的任务**(`TaskIntent` + `TaskRecord`),进度落盘持久化,不依赖对话历史。
15
15
  - **质量由代码强制保障** — 自动化 QA 与隔离式 Review 必须通过,任务才会完成。
16
16
 
17
- Pi 与 Claude Code 是支持的宿主。未声明的适配器仍不受支持。Claude Code 最低版本为 `2.1.199`。历史真实 Host 证据归档于 `docs/verification/archive/`;当前发布由确定性的 package 与 authority 检查把关。
17
+ Pi 与 Claude Code 是支持的宿主。未声明的适配器仍不受支持。Claude Code 最低版本为 `2.1.236`,这是已通过交互式 server-initiated MCP elicitation 验证的最低版本。当前真实 Host 证据见 `docs/verification/claude-native-elicitation-authority-conformance.md`;历史报告归档于 `docs/verification/archive/`。
18
18
 
19
19
  ---
20
20
 
@@ -64,7 +64,7 @@ mise run check-dist-sync # 校验生成文档同步
64
64
 
65
65
  Pi 会自动路由:需求模糊走澄清,目标明确走规划。
66
66
 
67
- **2. 确认计划** — Planner 会在 `docs/plans/` 生成 `TaskIntent`(范围、风险等级、验收条件)。检查无误后在 TUI 弹窗中确认 Enrollment(所有风险等级都需要确认,确认前零写入)。
67
+ **2. 确认计划** — Planner 会在 `docs/plans/` 生成 `TaskIntent`(范围、风险等级、验收条件)。检查无误后由当前 Host 的原生 gate 确认 Enrollment(所有风险等级都需要确认,确认前零 authority 写入)。
68
68
 
69
69
  **3. 开始执行** — `imm-loop` 按计划执行、跑 QA、触发 Review。按提示暂存任务拥有的文件:
70
70
 
@@ -191,7 +191,7 @@ docs/specs/ # Living specs(原地更新)
191
191
 
192
192
  **QA 失败怎么办?** QA 返回 `rework` 或 `replan_required`,`imm-loop` 会自动路由回 Executor 或 `imm-planner` 调整范围,无需手动重置。
193
193
 
194
- **可以在 Pi 之外使用吗?** 可以在本地交互式 Claude Code 中使用同一套 Kernel;未声明的适配器不受支持。
194
+ **可以在 Pi 之外使用吗?** 可以从 `2.1.236` 起在本地交互式 Claude Code 中使用同一套 Kernel;Claude plugin 通过绑定 digest 的原生 MCP elicitation gate 获取 authority,未声明的适配器不受支持。
195
195
 
196
196
  ---
197
197
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.3.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.3.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";
@@ -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 host confirmation bound to the TaskIntent content hash at the Planner's final `ctx.ui.custom` gate. 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.
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