immune-brain 5.1.0 → 5.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +37 -5
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +230 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +66 -3
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +8712 -7410
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +8 -6
- package/plugins/immune-brain/dist/imm-brainstorm.md +4 -4
- package/plugins/immune-brain/dist/imm-planner.md +41 -5
- package/plugins/immune-brain/dist/imm-run.md +123 -9
- package/plugins/immune-brain/dist/role-prompts/code-review.md +25 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +22 -1
- package/plugins/immune-brain/runtime/assurance/active_task_notice.ts +42 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +31 -4
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +4 -1
- package/plugins/immune-brain/runtime/assurance/review_mediation.ts +107 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +3 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +15 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +120 -59
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +33 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +142 -31
- package/plugins/immune-brain/runtime/commands/kernel.ts +54 -1
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +235 -7
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -2
- package/plugins/immune-brain/runtime/kernel/completion.ts +24 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +15 -2
- package/plugins/immune-brain/runtime/kernel/runtime_contracts.json +7 -0
- package/plugins/immune-brain/runtime/kernel/runtime_contracts.ts +75 -0
- package/plugins/immune-brain/runtime/kernel/spec_binding.ts +6 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +20 -4
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +33 -3
- package/plugins/immune-brain/runtime/kernel/types.ts +7 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +13 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +25 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +7 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +1 -1
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +160 -0
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +222 -0
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +72 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +447 -37
- package/plugins/immune-brain/runtime/unattended/batch_leak_restore.ts +131 -0
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +33 -16
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +16 -3
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +44 -0
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +40 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +199 -6
- package/plugins/immune-brain/runtime/unattended/lane_workspace.ts +58 -0
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +1 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -1
package/package.json
CHANGED
|
@@ -130,6 +130,8 @@ import {
|
|
|
130
130
|
type GithubTrackerResult,
|
|
131
131
|
} from "../runtime/github_issue_tracker";
|
|
132
132
|
import { reviewReworkFindings } from "../runtime/assurance/coordinator";
|
|
133
|
+
import { laneWriteRefusal } from "../runtime/unattended/lane_workspace";
|
|
134
|
+
import { activeTaskNotice } from "../runtime/assurance/active_task_notice";
|
|
133
135
|
|
|
134
136
|
const LOOP_OWNERS = ["plan", "kernel", "brainstorm", "planner", "loop"] as const;
|
|
135
137
|
const LOOP_TARGETS = [
|
|
@@ -371,8 +373,20 @@ export default function (
|
|
|
371
373
|
if (ctx.mode !== "tui") return;
|
|
372
374
|
await refreshTaskRail(ctx);
|
|
373
375
|
});
|
|
374
|
-
pi.on("
|
|
376
|
+
pi.on("before_agent_start", async (event: { prompt?: string; systemPrompt?: string }, ctx?: ExtensionContext) => {
|
|
377
|
+
// Advisory only: an active Managed task is announced, never resumed.
|
|
378
|
+
const notice = await activeTaskNotice(ctx?.cwd ?? process.cwd());
|
|
379
|
+
if (!notice || typeof event.systemPrompt !== "string") return undefined;
|
|
380
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${notice}` };
|
|
381
|
+
});
|
|
382
|
+
pi.on("tool_call", (event: { toolName?: string; input?: unknown; toolCallId?: string }, ctx?: ExtensionContext) => {
|
|
375
383
|
if (ctx) railContext = ctx;
|
|
384
|
+
const laneRefusal = laneEditRefusal(event, ctx?.cwd ?? process.cwd());
|
|
385
|
+
if (laneRefusal) return { block: true, reason: laneRefusal };
|
|
386
|
+
if (event.toolName === "Agent") {
|
|
387
|
+
// ADR 0017: correlate a reserved reviewer dispatch to its reservation.
|
|
388
|
+
progression.piReviewHost.observeReviewDispatch(event.input as { prompt?: unknown; subagent_type?: unknown } | undefined, event.toolCallId);
|
|
389
|
+
}
|
|
376
390
|
if (event.toolName === "imm_canary_enrollment" && ctx) {
|
|
377
391
|
const input = event.input as { task_id?: string } | undefined;
|
|
378
392
|
if (input?.task_id) presentTaskRail(ctx, {
|
|
@@ -385,7 +399,14 @@ export default function (
|
|
|
385
399
|
});
|
|
386
400
|
pi.on("tool_result", (event: unknown, ctx?: ExtensionContext) => {
|
|
387
401
|
if (ctx) railContext = ctx;
|
|
388
|
-
const result = event as { toolName?: string; details?: Record<string, unknown> };
|
|
402
|
+
const result = event as { toolName?: string; details?: Record<string, unknown>; toolCallId?: string; content?: Array<{ type?: string; text?: string }> };
|
|
403
|
+
if (result.toolName === "Agent" && result.toolCallId) {
|
|
404
|
+
// ADR 0017: record the reviewer's own result bytes for receipt-bound submission.
|
|
405
|
+
const text = Array.isArray(result.content)
|
|
406
|
+
? result.content.filter((part) => typeof part?.text === "string").map((part) => part.text as string).join("\n")
|
|
407
|
+
: "";
|
|
408
|
+
progression.piReviewHost.observeReviewResult(result.toolCallId, text);
|
|
409
|
+
}
|
|
389
410
|
if (result.toolName === "imm_canary_enrollment" && ctx) {
|
|
390
411
|
const taskId = typeof result.details?.task_id === "string" ? result.details.task_id : undefined;
|
|
391
412
|
if (taskId) presentTaskRailResult(ctx, taskId, result.details);
|
|
@@ -406,7 +427,7 @@ export default function (
|
|
|
406
427
|
promptGuidelines: [
|
|
407
428
|
"Only the exact enrolled canary task is routable; verify the active backend claim first via status.",
|
|
408
429
|
"After implementation and focused verification, freeze the artifacts, call advance_assurance, and consume its direct terminal result; do not poll or create a detached job.",
|
|
409
|
-
"When advance_assurance returns review_ready, invoke the foreground Agent from agent_params once, then
|
|
430
|
+
"When advance_assurance returns review_ready, invoke the foreground Agent from agent_params once, then call submit_review: omit the verdict to apply the observed reviewer result, or relay its structured verdict exactly.",
|
|
410
431
|
"For a complete breaking revision, call approve_breaking_intent_revision with the complete next_intent directly; do not ask for chat pre-confirmation because the host opens the single native confirmation before applying it.",
|
|
411
432
|
"For a proven stale authority claim, call repair_authority_state directly; the Kernel revalidates and removes only the redundant claim without user interaction.",
|
|
412
433
|
"After awaiting_user, call request_authorization directly so the host opens the single native confirmation; do not ask for chat pre-confirmation or ask the user to copy or report a command.",
|
|
@@ -416,7 +437,7 @@ export default function (
|
|
|
416
437
|
action: Type.Union([
|
|
417
438
|
Type.Object({ op: Type.Literal("status") }),
|
|
418
439
|
Type.Object({ op: Type.Literal("advance_assurance") }),
|
|
419
|
-
Type.Object({ op: Type.Literal("submit_review"), verdict: Type.Unknown() }),
|
|
440
|
+
Type.Object({ op: Type.Literal("submit_review"), verdict: Type.Optional(Type.Unknown()) }),
|
|
420
441
|
Type.Object({ op: Type.Literal("request_authorization") }),
|
|
421
442
|
Type.Object({ op: Type.Literal("request_stop") }),
|
|
422
443
|
Type.Object({ op: Type.Literal("repair_authority_state") }),
|
|
@@ -549,7 +570,7 @@ export default function (
|
|
|
549
570
|
presentTaskRailResult(ctx, taskId, update.details as Record<string, unknown> | undefined);
|
|
550
571
|
})
|
|
551
572
|
: action.op === "submit_review"
|
|
552
|
-
? await progression.
|
|
573
|
+
? await progression.submitMediated(taskId, ctx, action.verdict)
|
|
553
574
|
: action.op === "approve_breaking_intent_revision"
|
|
554
575
|
? await authorizeExactOperation(
|
|
555
576
|
taskId,
|
|
@@ -1151,6 +1172,16 @@ export function readTaskIntentForRecord(root: string, taskId: string): ReadTaskI
|
|
|
1151
1172
|
return readTaskIntent(root, taskId, currentPath);
|
|
1152
1173
|
}
|
|
1153
1174
|
|
|
1175
|
+
/**
|
|
1176
|
+
* In a batch Lane, an `edit` or `write` whose resolved target is outside the
|
|
1177
|
+
* Lane is refused before it runs; `bash` is not intercepted.
|
|
1178
|
+
*/
|
|
1179
|
+
export function laneEditRefusal(event: { toolName?: string; input?: unknown }, cwd: string): string | null {
|
|
1180
|
+
if (event.toolName !== "edit" && event.toolName !== "write") return null;
|
|
1181
|
+
const input = (event.input ?? {}) as { path?: unknown; file_path?: unknown };
|
|
1182
|
+
return laneWriteRefusal(cwd, input.path ?? input.file_path);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1154
1185
|
async function markGithubTaskTerminal(
|
|
1155
1186
|
root: string,
|
|
1156
1187
|
input: { task_id: string; phase: "done" | "stopped"; terminal_event_id: string },
|
|
@@ -1509,6 +1540,7 @@ async function enrichAssuranceResult(
|
|
|
1509
1540
|
function nextActionForAssuranceResult(result: Record<string, unknown>, taskState: AssuranceTaskState): string {
|
|
1510
1541
|
const derived = result.recovery as { next_action: string } | undefined;
|
|
1511
1542
|
if (derived) return derived.next_action;
|
|
1543
|
+
if (typeof result.recovery_action === "string") return result.recovery_action;
|
|
1512
1544
|
const recovery = recoveryActionForAssuranceFailure(
|
|
1513
1545
|
"error" in taskState ? taskState.error : result.reason,
|
|
1514
1546
|
);
|
|
@@ -15,17 +15,23 @@ import {
|
|
|
15
15
|
startBatch,
|
|
16
16
|
type BatchRunnerKernelPort,
|
|
17
17
|
type BatchRunReport,
|
|
18
|
+
type BatchTrackerPort,
|
|
18
19
|
} from "../runtime/unattended/batch_runner";
|
|
19
20
|
import { createBatchKernelPort } from "../runtime/unattended/batch_kernel_port";
|
|
20
21
|
import type { BatchRunnerGitPort } from "../runtime/unattended/batch_git";
|
|
21
22
|
import type { InitiativeObservationReader } from "../runtime/unattended/types";
|
|
22
23
|
import { advancePiTask } from "./imm-canary-work";
|
|
23
24
|
import { batchReason } from "../runtime/unattended/batch_reasons";
|
|
24
|
-
import {
|
|
25
|
+
import { parseFinalVerification } from "../runtime/unattended/batch_final_verification";
|
|
26
|
+
import { parseLaneRevision, REVISION_DELEGATION_TEXT, type LaneRevisionRequest } from "../runtime/unattended/batch_delegation";
|
|
27
|
+
import { createBatchTrackerPort, laneRuntimeContractRefusal, parseLaneInstruction, type LaneInstructionRequest, parseLaneOffers, parseMaxParallel, resolveLaneParallel, type LaneOffer } from "../runtime/unattended/batch_lanes";
|
|
25
28
|
import {
|
|
26
29
|
authorizeBatch,
|
|
27
30
|
projectBatchPreflight,
|
|
28
31
|
} from "../runtime/unattended/batch_preflight";
|
|
32
|
+
import { retireStaleBatch, type StaleBatchFacts } from "../runtime/unattended/batch_disposition";
|
|
33
|
+
import type { BatchLaneRunReport } from "../runtime/unattended/batch_state";
|
|
34
|
+
|
|
29
35
|
import {
|
|
30
36
|
presentTaskRail,
|
|
31
37
|
renderStructuredCall,
|
|
@@ -67,8 +73,9 @@ export async function readSettledTaskRecord(
|
|
|
67
73
|
* is the testable unit, and the direct-execution suites cover the decision
|
|
68
74
|
* outcomes (decline -> rejected, cancel/Escape -> cancelled) with zero writes.
|
|
69
75
|
*/
|
|
70
|
-
export function mapDialogSelection(selected: string | undefined): "accept" | "decline" | "cancel" {
|
|
76
|
+
export function mapDialogSelection(selected: string | undefined): "accept" | "accept_delegate" | "decline" | "cancel" {
|
|
71
77
|
if (selected === "confirm") return "accept";
|
|
78
|
+
if (selected === "confirm_delegate") return "accept_delegate";
|
|
72
79
|
if (selected === "decline") return "decline";
|
|
73
80
|
return "cancel";
|
|
74
81
|
}
|
|
@@ -104,8 +111,16 @@ export interface PiBatchExecutionOptions {
|
|
|
104
111
|
max_parallel?: number;
|
|
105
112
|
/** Lane mode: Lanes someone else created, offered to the batch. */
|
|
106
113
|
lane_offers?: LaneOffer[];
|
|
114
|
+
/** Lane mode: the project's full verification commands, run once every child is integrated. */
|
|
115
|
+
final_verification?: string[];
|
|
116
|
+
/** Lane mode: one instruction for an idle Lane session, recorded before it is sent. */
|
|
117
|
+
lane_instruction?: LaneInstructionRequest;
|
|
118
|
+
/** Lane mode, ADR 0018: one breaking Intent revision the coordinator approves under the batch's grant. */
|
|
119
|
+
lane_revision?: LaneRevisionRequest;
|
|
107
120
|
batchKernel?: Partial<BatchRunnerKernelPort>;
|
|
108
121
|
batchGit?: BatchRunnerGitPort;
|
|
122
|
+
/** Lane mode: closes an integrated child's Issue; defaults to the GitHub projection. */
|
|
123
|
+
batchTracker?: BatchTrackerPort;
|
|
109
124
|
readInitiative?: InitiativeObservationReader;
|
|
110
125
|
confirmBatch?: (details: {
|
|
111
126
|
title: string;
|
|
@@ -113,7 +128,9 @@ export interface PiBatchExecutionOptions {
|
|
|
113
128
|
details: string;
|
|
114
129
|
planDigest: string;
|
|
115
130
|
signal?: AbortSignal;
|
|
116
|
-
|
|
131
|
+
/** ADR 0018: offer the opt-in delegation choice (lane mode only). */
|
|
132
|
+
offerRevisionDelegation?: boolean;
|
|
133
|
+
}) => Promise<"accept" | "accept_delegate" | "decline" | "cancel">;
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
export type PiBatchExecutionResult =
|
|
@@ -145,7 +162,7 @@ export async function executePiUnattendedBatch(
|
|
|
145
162
|
if (!reuseOnly && !interactive) return { state: "rejected", ...nonInteractiveRefusal() };
|
|
146
163
|
|
|
147
164
|
// A lane_offers-only call resumes the recorded lane batch; refused before any gate otherwise.
|
|
148
|
-
const resolvedParallel = resolveLaneParallel(root, initiativeSlug, options.max_parallel, options.lane_offers);
|
|
165
|
+
const resolvedParallel = resolveLaneParallel(root, initiativeSlug, options.max_parallel, options.lane_offers, options.lane_instruction ?? options.lane_revision);
|
|
149
166
|
if (resolvedParallel !== undefined) options = { ...options, max_parallel: resolvedParallel };
|
|
150
167
|
|
|
151
168
|
// 1. Host-independent batch preflight: claim ownership, branch availability,
|
|
@@ -168,6 +185,12 @@ export async function executePiUnattendedBatch(
|
|
|
168
185
|
const budget = preflight.projection.budget;
|
|
169
186
|
const planDigest = preflight.projection.plan_digest;
|
|
170
187
|
const recoveryChildren = preflight.projection.recovery_children;
|
|
188
|
+
// A new lane batch whose Lane runtime cannot read the coordinator's Kernel
|
|
189
|
+
// contracts is refused before the gate opens; a match adds nothing.
|
|
190
|
+
if (options.max_parallel !== undefined && !isResuming) {
|
|
191
|
+
const contractRefusal = laneRuntimeContractRefusal(root);
|
|
192
|
+
if (contractRefusal) return batchReason("batch_run_rejected", contractRefusal);
|
|
193
|
+
}
|
|
171
194
|
|
|
172
195
|
// 4. Literal-user gate plus the shared reuse decision, the post-gate
|
|
173
196
|
// claim/drift cascade, and the Batch Authorization binding. The Host supplies
|
|
@@ -195,15 +218,16 @@ export async function executePiUnattendedBatch(
|
|
|
195
218
|
}
|
|
196
219
|
const confirmDetails = {
|
|
197
220
|
title: `Authorize Unattended Batch: ${facts.initiative_slug}`,
|
|
198
|
-
summary: `Initiative: ${facts.initiative_slug}\nBatch branch: ${facts.batch_branch}\nPlan digest: ${facts.plan_digest}\nBudget: max_children=${facts.budget.max_children}, qa_failure_limit=${facts.budget.qa_failure_limit}${options.max_parallel !== undefined ? `\nLane mode: max_parallel=${options.max_parallel}` : ""}`,
|
|
221
|
+
summary: `Initiative: ${facts.initiative_slug}\nBatch branch: ${facts.batch_branch}\nPlan digest: ${facts.plan_digest}\nBudget: max_children=${facts.budget.max_children}, qa_failure_limit=${facts.budget.qa_failure_limit}${options.max_parallel !== undefined ? `\nLane mode: max_parallel=${options.max_parallel}` : ""}${options.max_parallel !== undefined && options.final_verification ? `\nFinal verification: ${options.final_verification.join("; ")}` : ""}`,
|
|
199
222
|
details: `Ordered children (${facts.children.length}):\n${facts.children.map((child) => ` - ${child.task_id} (${child.slice_id}) [risk: ${child.risk}] [status: ${child.status === "already_settled" ? "completed" : "pending execution"}]`).join("\n")}${facts.excluded.length > 0 ? `\n\nExcluded children:\n${facts.excluded.map((child) => ` - ${child.task_id} (${child.slice_id}): ${child.reason}`).join("\n")}` : ""}${options.max_parallel !== undefined ? `\n\nParallel groups (${facts.parallel_groups.length}):\n${facts.parallel_groups.map((group) => ` - ${group.join(", ")}`).join("\n")}${facts.scope_conflicts.length > 0 ? `\n\nSerialized by overlapping scope:\n${facts.scope_conflicts.map((c) => ` - ${c.task_id} after ${c.overlaps_with.join(", ")}`).join("\n")}` : ""}` : ""}${facts.reuse_blockers.length > 0 ? `\n\nRe-confirmation required: ${facts.reuse_blockers.join(", ")}.\nRecovery: confirm to issue a fresh authorization bound to the current plan and HEAD.` : ""}`,
|
|
200
223
|
planDigest: facts.plan_digest,
|
|
201
224
|
signal,
|
|
225
|
+
...(options.max_parallel !== undefined ? { offerRevisionDelegation: true } : {}),
|
|
202
226
|
};
|
|
203
227
|
|
|
204
228
|
// The gate settles only on the literal user's answer or the caller's
|
|
205
229
|
// cancellation signal.
|
|
206
|
-
let decision: "accept" | "decline" | "cancel";
|
|
230
|
+
let decision: "accept" | "accept_delegate" | "decline" | "cancel";
|
|
207
231
|
try {
|
|
208
232
|
decision = await options.confirmBatch(confirmDetails);
|
|
209
233
|
} catch (err) {
|
|
@@ -220,10 +244,14 @@ export async function executePiUnattendedBatch(
|
|
|
220
244
|
if (decision === "decline") {
|
|
221
245
|
return { kind: "host_rejection", value: batchReason("confirmation_declined") };
|
|
222
246
|
}
|
|
223
|
-
if (decision !== "accept") {
|
|
247
|
+
if (decision !== "accept" && decision !== "accept_delegate") {
|
|
224
248
|
return { kind: "host_rejection", value: batchReason("confirmation_no_decision") };
|
|
225
249
|
}
|
|
226
|
-
return {
|
|
250
|
+
return {
|
|
251
|
+
kind: "confirmed",
|
|
252
|
+
request_id: randomUUID(),
|
|
253
|
+
...(decision === "accept_delegate" && options.max_parallel !== undefined ? { delegate_revisions: true } : {}),
|
|
254
|
+
};
|
|
227
255
|
},
|
|
228
256
|
confirmationRef: ({ batch_id, request_id }) =>
|
|
229
257
|
piConfirmationRef({
|
|
@@ -289,7 +317,16 @@ export async function executePiUnattendedBatch(
|
|
|
289
317
|
now,
|
|
290
318
|
kernel: kernelPort,
|
|
291
319
|
git: options.batchGit,
|
|
292
|
-
...(options.max_parallel !== undefined
|
|
320
|
+
...(options.max_parallel !== undefined
|
|
321
|
+
? {
|
|
322
|
+
max_parallel: options.max_parallel,
|
|
323
|
+
tracker: options.batchTracker ?? createBatchTrackerPort(root, initiativeSlug),
|
|
324
|
+
...(options.final_verification ? { final_verification: options.final_verification } : {}),
|
|
325
|
+
...(options.lane_instruction ? { lane_instruction: options.lane_instruction } : {}),
|
|
326
|
+
...(options.lane_revision ? { lane_revision: options.lane_revision } : {}),
|
|
327
|
+
...(authorization.revision_delegation !== undefined ? { revision_delegation: authorization.revision_delegation } : {}),
|
|
328
|
+
}
|
|
329
|
+
: {}),
|
|
293
330
|
...(options.lane_offers !== undefined ? { lane_offers: options.lane_offers } : {}),
|
|
294
331
|
});
|
|
295
332
|
|
|
@@ -305,6 +342,69 @@ export async function executePiUnattendedBatch(
|
|
|
305
342
|
};
|
|
306
343
|
}
|
|
307
344
|
|
|
345
|
+
export type PiRetireStaleBatchResult =
|
|
346
|
+
| { state: "retired"; batch_id: string; report: BatchRunReport | BatchLaneRunReport }
|
|
347
|
+
| { state: "rejected" | "cancelled" | "blocked"; reason: string; recovery_action: string };
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The gate text for one batch disposition. Every fact rendered is the record's
|
|
351
|
+
* own evidence: what it delivered, what it never executed, and what retiring
|
|
352
|
+
* does and does not claim. Nothing here is a readiness certificate.
|
|
353
|
+
*/
|
|
354
|
+
function retireConfirmationDetails(facts: StaleBatchFacts): {
|
|
355
|
+
title: string;
|
|
356
|
+
summary: string;
|
|
357
|
+
details: string;
|
|
358
|
+
} {
|
|
359
|
+
return {
|
|
360
|
+
title: `Retire Stale Batch: ${facts.initiative_slug}`,
|
|
361
|
+
summary: `Batch: ${facts.batch_id}\nState: ${facts.batch_state}\nBatch branch: ${facts.branch}\nPlan digest: ${facts.plan_digest}\nRecorded child commits: ${facts.recorded_commits.length}\nConfirmed at: ${facts.confirmation_time}`,
|
|
362
|
+
details: `Children (${facts.children.length}):\n${facts.children
|
|
363
|
+
.map(
|
|
364
|
+
(child) =>
|
|
365
|
+
` - ${child.task_id} (${child.slice_id}) [state: ${child.state}]${child.commit ? ` [commit: ${child.commit}]` : " [no commit]"}${child.lane_branch ? ` [lane: ${child.lane_branch}]` : ""}`,
|
|
366
|
+
)
|
|
367
|
+
.join("\n")}\n\nRetiring writes this record to the terminal superseded state. Child states, recorded commits and the batch branch are preserved as evidence; nothing is deleted and no batch trailer or approval is manufactured.\n\nA child without a commit was never executed by this batch: it carries no delivery evidence, and the retired authorization is never reused for a revised plan.\n\nAfter retiring, run each remaining child with imm-run under its own authorization, or start a new batch for this Initiative.`,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export async function executePiRetireStaleBatch(options: {
|
|
372
|
+
root: string;
|
|
373
|
+
initiativeSlug: string;
|
|
374
|
+
signal?: AbortSignal;
|
|
375
|
+
confirmBatch: (details: { title: string; summary: string; details: string; signal?: AbortSignal }) => Promise<"accept" | "decline" | "cancel">;
|
|
376
|
+
}): Promise<PiRetireStaleBatchResult> {
|
|
377
|
+
const { root, initiativeSlug, signal } = options;
|
|
378
|
+
const outcome = await retireStaleBatch({
|
|
379
|
+
root,
|
|
380
|
+
initiative_slug: initiativeSlug,
|
|
381
|
+
now: new Date().toISOString(),
|
|
382
|
+
gate: async (facts) => {
|
|
383
|
+
const details = retireConfirmationDetails(facts);
|
|
384
|
+
let decision: "accept" | "decline" | "cancel";
|
|
385
|
+
try {
|
|
386
|
+
decision = await options.confirmBatch({ ...details, signal });
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (signal?.aborted) return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
389
|
+
return {
|
|
390
|
+
kind: "host_rejection",
|
|
391
|
+
value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err)),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (decision === "cancel" || signal?.aborted)
|
|
395
|
+
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
396
|
+
if (decision === "decline")
|
|
397
|
+
return { kind: "host_rejection", value: batchReason("confirmation_declined") };
|
|
398
|
+
if (decision !== "accept")
|
|
399
|
+
return { kind: "host_rejection", value: batchReason("confirmation_no_decision") };
|
|
400
|
+
return { kind: "confirmed", request_id: randomUUID() };
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
if (outcome.outcome === "host_rejection") return outcome.value;
|
|
404
|
+
if (outcome.outcome === "rejected") return outcome.rejection;
|
|
405
|
+
return { state: "retired", batch_id: outcome.batch_id, report: outcome.report };
|
|
406
|
+
}
|
|
407
|
+
|
|
308
408
|
/**
|
|
309
409
|
* Injectable seams for the batch Tool. Production passes nothing and gets the
|
|
310
410
|
* real Initiative reader, Kernel port and Git port; tests drive the registered
|
|
@@ -337,10 +437,21 @@ export default function (
|
|
|
337
437
|
lane_offers: Type.Optional(Type.Array(
|
|
338
438
|
Type.Object({ task_id: Type.String(), path: Type.String() }, { additionalProperties: false }),
|
|
339
439
|
)),
|
|
440
|
+
final_verification: Type.Optional(Type.Array(Type.String(), { minItems: 1, maxItems: 8 })),
|
|
441
|
+
lane_instruction: Type.Optional(Type.Object({
|
|
442
|
+
task_id: Type.String(),
|
|
443
|
+
kind: Type.Optional(Type.Union([Type.Literal("instruction"), Type.Literal("correction")])),
|
|
444
|
+
text: Type.String(),
|
|
445
|
+
session_state: Type.Union([Type.Literal("idle"), Type.Literal("working"), Type.Literal("blocked"), Type.Literal("done")]),
|
|
446
|
+
}, { additionalProperties: false })),
|
|
447
|
+
lane_revision: Type.Optional(Type.Object({
|
|
448
|
+
task_id: Type.String(),
|
|
449
|
+
next_intent: Type.Object({}, { additionalProperties: true }),
|
|
450
|
+
}, { additionalProperties: false })),
|
|
340
451
|
}, { additionalProperties: false }),
|
|
341
452
|
execute: async (
|
|
342
453
|
_toolCallId: string,
|
|
343
|
-
params: { initiative_slug: string; max_parallel?: number; lane_offers?: Array<{ task_id: string; path: string }
|
|
454
|
+
params: { initiative_slug: string; max_parallel?: number; lane_offers?: Array<{ task_id: string; path: string }>; final_verification?: string[]; lane_instruction?: unknown; lane_revision?: unknown },
|
|
344
455
|
signal: AbortSignal | undefined,
|
|
345
456
|
_onUpdate: unknown,
|
|
346
457
|
ctx: ExtensionContext,
|
|
@@ -361,12 +472,18 @@ export default function (
|
|
|
361
472
|
|
|
362
473
|
const maxParallel = parseMaxParallel(params.max_parallel);
|
|
363
474
|
const laneOffers = parseLaneOffers(params.lane_offers);
|
|
475
|
+
const finalVerification = parseFinalVerification(params.final_verification);
|
|
476
|
+
const laneInstruction = parseLaneInstruction(params.lane_instruction);
|
|
477
|
+
const laneRevision = parseLaneRevision(params.lane_revision);
|
|
364
478
|
|
|
365
479
|
const result = await executePiUnattendedBatch({
|
|
366
480
|
root: ctx.cwd,
|
|
367
481
|
initiativeSlug,
|
|
368
482
|
...(maxParallel !== undefined ? { max_parallel: maxParallel } : {}),
|
|
369
483
|
...(laneOffers !== undefined ? { lane_offers: laneOffers } : {}),
|
|
484
|
+
...(finalVerification !== undefined ? { final_verification: finalVerification } : {}),
|
|
485
|
+
...(laneInstruction !== undefined ? { lane_instruction: laneInstruction } : {}),
|
|
486
|
+
...(laneRevision !== undefined ? { lane_revision: laneRevision } : {}),
|
|
370
487
|
interactive: ctx.mode === "tui",
|
|
371
488
|
signal,
|
|
372
489
|
readInitiative: dependencies.readInitiative,
|
|
@@ -395,6 +512,9 @@ export default function (
|
|
|
395
512
|
signal: details.signal,
|
|
396
513
|
actions: [
|
|
397
514
|
{ value: "confirm", label: "Authorize batch run", description: "Start the unattended serial batch run" },
|
|
515
|
+
...(details.offerRevisionDelegation
|
|
516
|
+
? [{ value: "confirm_delegate", label: "Authorize and delegate in-Lane revisions", description: REVISION_DELEGATION_TEXT }]
|
|
517
|
+
: []),
|
|
398
518
|
{ value: "decline", label: "Decline batch", description: "Reject this batch authorization; repository and authority stay unchanged" },
|
|
399
519
|
{ value: "cancel", label: "Cancel", description: "Leave repository and authority unchanged" },
|
|
400
520
|
],
|
|
@@ -439,4 +559,104 @@ export default function (
|
|
|
439
559
|
);
|
|
440
560
|
},
|
|
441
561
|
});
|
|
562
|
+
// The batch disposition: one explicit literal-user decision that closes a
|
|
563
|
+
// batch the plan moved past, preserving its evidence instead of leaving the
|
|
564
|
+
// record parked forever or deleting it by hand. Grants no handoff.
|
|
565
|
+
pi.registerTool({
|
|
566
|
+
name: "retire_stale_batch",
|
|
567
|
+
label: "Retire stale batch",
|
|
568
|
+
description: "Retire an Initiative's parked or drifted batch record to the terminal superseded state after native confirmation.",
|
|
569
|
+
promptSnippet: "Retire stale batch: invoke once in foreground after the stale batch record is inspected.",
|
|
570
|
+
promptGuidelines: [
|
|
571
|
+
"Call only after inspecting the batch record; execute once in the foreground and consume the direct terminal result.",
|
|
572
|
+
"Do not run this Tool in background, poll for completion, or issue a cancel subcommand.",
|
|
573
|
+
],
|
|
574
|
+
parameters: Type.Object({
|
|
575
|
+
initiative_slug: Type.String({ pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" }),
|
|
576
|
+
}, { additionalProperties: false }),
|
|
577
|
+
execute: async (
|
|
578
|
+
_toolCallId: string,
|
|
579
|
+
params: { initiative_slug: string },
|
|
580
|
+
signal: AbortSignal | undefined,
|
|
581
|
+
_onUpdate: unknown,
|
|
582
|
+
ctx: ExtensionContext,
|
|
583
|
+
) => {
|
|
584
|
+
const { initiative_slug: initiativeSlug } = params;
|
|
585
|
+
if (ctx.mode !== "tui") {
|
|
586
|
+
const refusal = nonInteractiveRefusal();
|
|
587
|
+
throwToolFailure({
|
|
588
|
+
tool: "imm_canary_enrollment",
|
|
589
|
+
task_id: initiativeSlug,
|
|
590
|
+
operation: "retire_stale_batch",
|
|
591
|
+
state: "blocked",
|
|
592
|
+
code: "unsupported_host",
|
|
593
|
+
message: refusal.reason,
|
|
594
|
+
next_action: refusal.recovery_action,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const result = await executePiRetireStaleBatch({
|
|
599
|
+
root: ctx.cwd,
|
|
600
|
+
initiativeSlug,
|
|
601
|
+
signal,
|
|
602
|
+
confirmBatch: async (details) => {
|
|
603
|
+
presentTaskRail(ctx, {
|
|
604
|
+
task_id: initiativeSlug,
|
|
605
|
+
state: "Approval required",
|
|
606
|
+
result: details.title,
|
|
607
|
+
next: "Review batch record evidence",
|
|
608
|
+
});
|
|
609
|
+
const selected = await requestAuthorityDialog(
|
|
610
|
+
pi,
|
|
611
|
+
ctx,
|
|
612
|
+
{
|
|
613
|
+
attention_id: randomUUID(),
|
|
614
|
+
task_id: initiativeSlug,
|
|
615
|
+
reason: "enrollment",
|
|
616
|
+
label: details.title,
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
title: details.title,
|
|
620
|
+
summary: details.summary,
|
|
621
|
+
details: details.details,
|
|
622
|
+
signal: details.signal,
|
|
623
|
+
actions: [
|
|
624
|
+
{ value: "confirm", label: "Retire batch record", description: "Write the terminal superseded state and its report" },
|
|
625
|
+
{ value: "decline", label: "Keep batch parked", description: "Leave the batch record and its authorization unchanged" },
|
|
626
|
+
{ value: "cancel", label: "Cancel", description: "Leave repository and authority unchanged" },
|
|
627
|
+
],
|
|
628
|
+
},
|
|
629
|
+
);
|
|
630
|
+
const mapped = mapDialogSelection(selected);
|
|
631
|
+
return mapped === "accept_delegate" ? "accept" : mapped;
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (result.state !== "retired") {
|
|
636
|
+
throwToolFailure({
|
|
637
|
+
tool: "imm_canary_enrollment",
|
|
638
|
+
task_id: initiativeSlug,
|
|
639
|
+
operation: "retire_stale_batch",
|
|
640
|
+
state: result.state === "blocked" ? "blocked" : "failed",
|
|
641
|
+
code: `batch_${result.state}`,
|
|
642
|
+
message: result.reason,
|
|
643
|
+
next_action: result.recovery_action,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
648
|
+
details: result,
|
|
649
|
+
};
|
|
650
|
+
},
|
|
651
|
+
renderCall(args, theme) {
|
|
652
|
+
const params = args as { initiative_slug?: string };
|
|
653
|
+
return renderStructuredCall("retire_stale_batch", "retire", params.initiative_slug, theme);
|
|
654
|
+
},
|
|
655
|
+
renderResult(result, _options, theme) {
|
|
656
|
+
return renderStructuredResult(
|
|
657
|
+
result as Parameters<typeof renderStructuredResult>[0],
|
|
658
|
+
theme,
|
|
659
|
+
);
|
|
660
|
+
},
|
|
661
|
+
});
|
|
442
662
|
}
|
|
@@ -46,9 +46,10 @@ export type {
|
|
|
46
46
|
ReviewTimingProfile,
|
|
47
47
|
} from "../runtime/assurance/coordinator";
|
|
48
48
|
|
|
49
|
-
import { AssuranceCoordinator, type AssuranceCoordinatorPorts } from "../runtime/assurance/coordinator";
|
|
49
|
+
import { AssuranceCoordinator, type AssuranceCoordinatorPorts, type HostContext, type AssuranceSubmitReviewResult } from "../runtime/assurance/coordinator";
|
|
50
50
|
import type { AssuranceHostPort, HostReviewReservation, ReviewRequest } from "../runtime/assurance/host_port";
|
|
51
|
-
import {
|
|
51
|
+
import { submitMediatedReview, type ReviewObservation } from "../runtime/assurance/review_mediation";
|
|
52
|
+
import { reservedAgentParams, promptDigest } from "./pi-canary-native-review";
|
|
52
53
|
|
|
53
54
|
import type { VerdictAuthority } from "../runtime/assurance/verdict_authority";
|
|
54
55
|
|
|
@@ -57,9 +58,18 @@ export type AssuranceProgressionPorts = Omit<AssuranceCoordinatorPorts, "host">
|
|
|
57
58
|
authorityOverrides?: Partial<VerdictAuthority>;
|
|
58
59
|
};
|
|
59
60
|
|
|
61
|
+
interface RecordedReviewReceipt {
|
|
62
|
+
taskId: string;
|
|
63
|
+
promptDigest: string;
|
|
64
|
+
toolCallId?: string;
|
|
65
|
+
result?: string;
|
|
66
|
+
error?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
class PiReviewHost implements AssuranceHostPort {
|
|
61
70
|
readonly host = "pi" as const;
|
|
62
71
|
private readonly pending = new Set<string>();
|
|
72
|
+
private readonly receipts = new Map<string, RecordedReviewReceipt>();
|
|
63
73
|
|
|
64
74
|
prepareReview(request: ReviewRequest): HostReviewReservation {
|
|
65
75
|
const params = reservedAgentParams({
|
|
@@ -69,17 +79,70 @@ class PiReviewHost implements AssuranceHostPort {
|
|
|
69
79
|
max_turns: request.maxTurns,
|
|
70
80
|
});
|
|
71
81
|
this.pending.add(request.operationId);
|
|
82
|
+
this.receipts.set(request.operationId, { taskId: request.taskId, promptDigest: promptDigest(params.prompt) });
|
|
72
83
|
return { id: request.operationId, dispatch: params };
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
releaseReview(reservation: HostReviewReservation): void {
|
|
76
87
|
this.pending.delete(reservation.id);
|
|
88
|
+
this.receipts.delete(reservation.id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Extension event seam: correlate one observed `Agent` dispatch to its reservation (ADR 0017). */
|
|
92
|
+
observeReviewDispatch(input: { prompt?: unknown; subagent_type?: unknown } | undefined, toolCallId: string | undefined): void {
|
|
93
|
+
if (typeof input?.prompt !== "string") return;
|
|
94
|
+
for (const record of this.receipts.values()) {
|
|
95
|
+
if (record.promptDigest !== promptDigest(input.prompt)) continue;
|
|
96
|
+
if (input.subagent_type !== "Review") {
|
|
97
|
+
record.error = "the reserved reviewer prompt was dispatched with a different Agent type";
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (record.toolCallId !== undefined && record.toolCallId !== toolCallId) {
|
|
101
|
+
record.error = "the reserved reviewer was dispatched more than once";
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
record.toolCallId = toolCallId;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Extension event seam: store the observed reviewer result bytes (ADR 0017). */
|
|
110
|
+
observeReviewResult(toolCallId: string | undefined, bytes: string): void {
|
|
111
|
+
if (!toolCallId) return;
|
|
112
|
+
for (const record of this.receipts.values()) {
|
|
113
|
+
if (record.toolCallId !== toolCallId) continue;
|
|
114
|
+
if (record.result === undefined) record.result = bytes;
|
|
115
|
+
else if (record.result !== bytes) record.error = "the reserved reviewer returned conflicting result bytes";
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
inspectReview(taskId: string): ReviewObservation {
|
|
121
|
+
for (const record of this.receipts.values()) {
|
|
122
|
+
if (record.taskId !== taskId) continue;
|
|
123
|
+
if (record.error) return { ok: false, release: false, reason: record.error };
|
|
124
|
+
if (record.toolCallId === undefined)
|
|
125
|
+
return { ok: false, release: false, reason: "the reserved foreground Agent was not observed in this session" };
|
|
126
|
+
if (record.result === undefined)
|
|
127
|
+
return { ok: false, release: false, reason: "the reserved foreground reviewer has not returned its result yet; wait for it, then call submit_review again" };
|
|
128
|
+
return { ok: true, receipt: { actorId: "pi-review-agent", result: record.result } };
|
|
129
|
+
}
|
|
130
|
+
return { ok: false, release: true, reason: "the reserved foreground Agent was not observed in this session" };
|
|
77
131
|
}
|
|
78
132
|
}
|
|
79
133
|
|
|
80
134
|
export class AssuranceProgression extends AssuranceCoordinator {
|
|
135
|
+
readonly piReviewHost: PiReviewHost;
|
|
136
|
+
|
|
81
137
|
constructor(ports: AssuranceProgressionPorts) {
|
|
82
|
-
|
|
138
|
+
const host = new PiReviewHost();
|
|
139
|
+
(ports as AssuranceCoordinatorPorts).host = host;
|
|
83
140
|
super(ports as AssuranceCoordinatorPorts, ports.authorityOverrides);
|
|
141
|
+
this.piReviewHost = host;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** ADR 0017: verdict-source resolution happens here, before the shared submit path. */
|
|
145
|
+
submitMediated(taskId: string, ctx: HostContext, verdictInput: unknown): Promise<AssuranceSubmitReviewResult> {
|
|
146
|
+
return submitMediatedReview(this, ctx, taskId, verdictInput, () => this.piReviewHost.inspectReview(taskId));
|
|
84
147
|
}
|
|
85
148
|
}
|
|
@@ -40,6 +40,13 @@ about the fix's scope, not a new bug. A trigger the accepted contract explicitly
|
|
|
40
40
|
excludes is reported as an advisory note against that boundary, never as blocking
|
|
41
41
|
rework.
|
|
42
42
|
|
|
43
|
+
A defect that lies inside the TaskIntent goal and `scope_hint` is a blocking
|
|
44
|
+
finding even when no acceptance assertion names its exact case word for word:
|
|
45
|
+
cite the acceptance whose principle it violates as `acceptance_id` and
|
|
46
|
+
`violated.ref`. Ask for the fix and a test that pins the case, never for the case
|
|
47
|
+
to be written into the acceptance text; a missing literal clause is not itself a
|
|
48
|
+
finding and does not call for an Intent revision.
|
|
49
|
+
|
|
43
50
|
## Invariant and Evidence Coverage
|
|
44
51
|
|
|
45
52
|
Judge whether the change actually closes the invariant its acceptance names,
|
|
@@ -59,6 +66,24 @@ seam, or a seam silently replaced by a different one, is a finding carrying
|
|
|
59
66
|
named, its absence is not a finding: candidates written before this rule carries
|
|
60
67
|
no defect for lacking one.
|
|
61
68
|
|
|
69
|
+
When the Spec referenced by an acceptance maps an `automated` user scenario to
|
|
70
|
+
it, check that the delivered tests at that acceptance's agreed seam assert the
|
|
71
|
+
scenario's `Then`: a scenario whose observable result is asserted nowhere is a
|
|
72
|
+
finding carrying `violated.kind` of `acceptance` with `ref` the acceptance id.
|
|
73
|
+
A `manual` scenario, and an acceptance with no mapped scenario, produce no such
|
|
74
|
+
finding. This adds no finding kind and no verdict branch: it is the same
|
|
75
|
+
`acceptance` finding the seam rule above already defines.
|
|
76
|
+
|
|
77
|
+
Before pass, enumerate the immutable change set and read each complete per-file
|
|
78
|
+
diff, including deletions. Page bounded reads until every hunk is visible; a
|
|
79
|
+
truncated aggregate output or summary is incomplete coverage. Trace changed
|
|
80
|
+
high-risk behavior through its callers and state owner, and inspect a concrete
|
|
81
|
+
negative case against the embedded acceptance. If immutable evidence remains
|
|
82
|
+
unavailable or incomplete, report the concrete acceptance coverage gap in a
|
|
83
|
+
rework finding with required evidence. `inspected_paths` declares coverage; it
|
|
84
|
+
does not itself prove tool reading. Judge quality by evidence, not elapsed time
|
|
85
|
+
or tool-call count.
|
|
86
|
+
|
|
62
87
|
A required check that is skipped, that matches zero tests, or whose prerequisite
|
|
63
88
|
is absent does not prove the acceptance; do not accept it as evidence. Full
|
|
64
89
|
suite coverage reported at a coarse pass count is not per-invariant evidence.
|
|
@@ -96,5 +121,5 @@ Every rework finding must carry machine-checkable provenance: evidence.trigger (
|
|
|
96
121
|
|
|
97
122
|
A pass verdict's approval must carry `inspected_paths`: an array of unique repository-relative path strings listing every path of the reviewed change set (changed_paths for a Git review revision, dirty_files for a bundle), deleted paths included; an empty change set is listed as an empty array. A path may be listed only after its diff was read. A pass that omits any changed path, lists a path outside the change set, or duplicates a path is rejected as a correctable invalid verdict.
|
|
98
123
|
|
|
99
|
-
A dispatched reviewer is never continued or re-prompted, including through SendMessage. The reserved prompt is dispatched verbatim. A blocked `submit_review` is recovered only through its returned `recovery_action`.
|
|
124
|
+
A dispatched reviewer is never continued or re-prompted, including through SendMessage. The reserved prompt is dispatched verbatim. The host binds this reviewer's final result bytes to the reservation, so a Parent may submit them without transcribing them. A blocked `submit_review` is recovered only through its returned `recovery_action`.
|
|
100
125
|
|