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 +2 -2
- 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 +84 -1
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +124 -6
- package/plugins/immune-brain/dist/BASELINE.md +1 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +307 -242
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +8 -7
- package/plugins/immune-brain/dist/imm-planner.md +22 -17
- package/plugins/immune-brain/hooks/hooks.json +0 -10
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +7 -1
- package/plugins/immune-brain/runtime/claude/capability.ts +4 -10
- package/plugins/immune-brain/runtime/claude/interaction.ts +55 -21
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +59 -50
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +222 -48
- package/plugins/immune-brain/runtime/claude/review_host.ts +2 -138
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/skills/BASELINE.md +1 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -0
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +6 -0
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 之外使用吗?**
|
|
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
|
@@ -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";
|
|
@@ -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
|
|
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
|
|