@ferris1225/pi-subagents 4.1.13 → 4.1.15

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.
@@ -42,6 +42,7 @@ import {
42
42
  failedStartResult,
43
43
  formatCompletionBlock,
44
44
  modelLevelTakeoverNote,
45
+ reviewFailFollowUpNote,
45
46
  queuedResult,
46
47
  } from "./format.ts";
47
48
  import {
@@ -220,16 +221,19 @@ export function ownsResumeReservation(
220
221
  );
221
222
  }
222
223
 
223
- /** Fire-and-forget durable checkpoint write. The live session keeps working
224
- * when the manifest is unwritable; only cross-reload resume is degraded. */
224
+ /** Fire-and-forget durable checkpoint. Parked threads stay resumable across
225
+ * reloads; a settled thread drops its record so the manifest only exists
226
+ * while unfinished work needs it. The live session keeps working when the
227
+ * manifest is unwritable; only cross-reload resume is degraded. */
225
228
  export function persistThreadCheckpoint(
226
229
  runtime: SubagentRuntime,
227
230
  thread: SubagentThread,
228
231
  state: "parked" | "completed" | "failed",
229
232
  ): void {
230
- void upsertThreadRecord(runtime.configPath, threadRecordFromThread(thread, state)).catch(
231
- () => undefined,
232
- );
233
+ const write = state === "parked"
234
+ ? upsertThreadRecord(runtime.configPath, threadRecordFromThread(thread, state))
235
+ : removeThreadRecord(runtime.configPath, thread.id);
236
+ void write.catch(() => undefined);
233
237
  }
234
238
 
235
239
  const WORKTREE_ISOLATION_INSTRUCTIONS =
@@ -246,27 +250,6 @@ export function isWorktreeCapableAgent(agent: AgentConfig): boolean {
246
250
  return isWriteCapableAgent(agent);
247
251
  }
248
252
 
249
- /** A direct reviewer otherwise cannot infer enabled-role availability from its
250
- * isolated task. Managed internal gates receive the same contract in their
251
- * generated briefs. Advisory reviews still emit neither machine marker. */
252
- function withEnabledDocumenterReviewContract(agent: AgentConfig): AgentConfig {
253
- return {
254
- ...agent,
255
- systemPrompt: `${agent.systemPrompt.trimEnd()}\n\nRuntime workflow context: documenter is enabled. In gate reviews, documentation drift is non-gating: emit DOCUMENTATION: NEEDED with ## Documentation notes, or DOCUMENTATION: CLEAN when no sync is needed. Advisory reviews still emit neither VERDICT nor DOCUMENTATION markers.`.trim(),
256
- };
257
- }
258
-
259
- /** The dispatcher explicitly requested a report-only review: forbid the gate
260
- * markers at the source and (in dispatch) refuse to chain on them anyway. */
261
- function withAdvisoryReviewContract(agent: AgentConfig): AgentConfig {
262
- return {
263
- ...agent,
264
- systemPrompt: `${agent.systemPrompt.trimEnd()}
265
-
266
- Runtime workflow context: this dispatch is advisory. Report findings only; do not emit VERDICT or DOCUMENTATION markers — the runtime will not act on them.`.trim(),
267
- };
268
- }
269
-
270
253
  export interface DispatchEnvironment {
271
254
  ctx: ExtensionContext;
272
255
  config: SubagentsConfig;
@@ -296,7 +279,6 @@ export interface StartBackgroundOptions {
296
279
  environment?: DispatchEnvironment;
297
280
  seed?: SessionSeed;
298
281
  resumeReservation?: ResumeReservation;
299
- advisoryReview?: boolean;
300
282
  }
301
283
 
302
284
  export type StartBackgroundInternal = (
@@ -426,15 +408,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
426
408
  if (!discoveredAgent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
427
409
  const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
428
410
  resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
429
- const resolvedAgent = resolveLiveAgentTools(discoveredAgent);
430
- const advisoryReview = startOptions.advisoryReview ?? existingThread?.advisoryReview ?? false;
431
- const agent = agentName === "reviewer"
432
- ? advisoryReview
433
- ? withAdvisoryReviewContract(resolvedAgent)
434
- : runAgents.some((candidate) => candidate.name === "documenter")
435
- ? withEnabledDocumenterReviewContract(resolvedAgent)
436
- : resolvedAgent
437
- : resolvedAgent;
411
+ const agent = resolveLiveAgentTools(discoveredAgent);
438
412
  if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
439
413
  return {
440
414
  ...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker, cleaner, or documenter.`),
@@ -528,7 +502,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
528
502
  thread.executionCwd = executionCwd;
529
503
  thread.thinkingLevel = thinkingLevel;
530
504
  thread.isolation = isolation;
531
- thread.advisoryReview = advisoryReview;
532
505
  thread.worktree = worktree;
533
506
  thread.state = "queued";
534
507
  thread.control = control;
@@ -552,7 +525,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
552
525
  executionCwd,
553
526
  thinkingLevel,
554
527
  isolation,
555
- advisoryReview,
556
528
  worktree,
557
529
  state: "queued",
558
530
  control,
@@ -670,13 +642,13 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
670
642
  if (thread.lifecycleOperation === "stop") return;
671
643
 
672
644
  // A shutdown can win in the microtask gap after the top-level RPC
673
- // settles. Do not launch an obsolete documenter/reviewer or replace the
645
+ // settles. Do not launch an obsolete downstream gate or replace the
674
646
  // stable top-level session with an aborted downstream attempt.
675
647
  if (backgroundSignal.aborted || lifecycleInterrupted() || !runtime.sessionActive) return;
676
648
 
677
649
  if (thread.retireOnSettle) runtime.retireThreadSession(thread);
678
650
  let workflowOutcome: ManagedWorkflowOutcome | undefined;
679
- const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability, thread.advisoryReview);
651
+ const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability);
680
652
  if (workflowPlan && runtime.sessionActive) {
681
653
  // The continuation is runtime-initiated (gate review,
682
654
  // documentation sync): release this generation's
@@ -801,6 +773,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
801
773
  if (needsFullFinal) {
802
774
  block += `\n\n${formatCompletionBlock(result, runConfig.maxResultLines, originalCwd)}`;
803
775
  }
776
+ if (finalVerdict === "fail") {
777
+ block += `\n\n${reviewFailFollowUpNote()}`;
778
+ }
804
779
  if (modelLevel) block += `\n\n${modelLevelTakeoverNote(result, { runId })}`;
805
780
  runtime.sendCompletionGroup([{
806
781
  agent: `managed workflow (${result.agent})`,
@@ -816,7 +791,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
816
791
  agent: result.agent,
817
792
  block: modelLevel
818
793
  ? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
819
- : formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd),
794
+ : result.agent === "reviewer" && reviewVerdict(getResultOutput(result)) === "fail"
795
+ ? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${reviewFailFollowUpNote()}`
796
+ : formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd),
820
797
  triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
821
798
  usage: result.usage,
822
799
  };
@@ -1290,7 +1267,6 @@ function createRestoredThread(
1290
1267
  executionCwd: record.executionCwd,
1291
1268
  ...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel as ThinkingLevel } : {}),
1292
1269
  isolation: record.isolation,
1293
- advisoryReview: false,
1294
1270
  worktree,
1295
1271
  state,
1296
1272
  control: new RpcRunControl(record.task, record.generation),
@@ -1320,15 +1296,20 @@ function createRestoredThread(
1320
1296
  return thread;
1321
1297
  }
1322
1298
 
1323
- /** Rebuild parked and settled threads from the durable manifest after a reload
1324
- * or restart. Orphaned children recorded by the previous process are killed
1325
- * first; records whose retained session vanished drop out with their
1326
- * artifacts. Returns the restored run ids. */
1299
+ /** Rebuild interrupted (parked) threads from the durable manifest after a
1300
+ * reload or restart. Orphaned children recorded by the previous process are
1301
+ * killed first; records whose retained session vanished and settled records
1302
+ * left by older versions, which hold no work worth resuming — drop out with
1303
+ * their artifacts. Returns the restored run ids. */
1327
1304
  export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<number[]> {
1328
1305
  const records = await readThreadRecords(runtime.configPath);
1329
1306
  const restoredIds: number[] = [];
1330
1307
  for (const record of records) {
1331
1308
  if (runtime.threads.has(record.runId) || monitor.findRun(record.runId)) continue;
1309
+ if (record.state !== "parked") {
1310
+ await discardRestoredRecord(runtime, record);
1311
+ continue;
1312
+ }
1332
1313
  // A child orphaned by reload/crash may still hold the retained session.
1333
1314
  // The on-disk session checkpoint is what survives; kill the writer.
1334
1315
  for (const pid of record.childPids) {
@@ -1348,33 +1329,29 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
1348
1329
  // A worktree thread whose isolated filesystem is gone cannot continue its
1349
1330
  // isolation invariant; surface it as failed instead of pretending.
1350
1331
  const state: ThreadState = worktree
1351
- ? record.state
1332
+ ? "parked"
1352
1333
  : record.isolation === "worktree" && record.worktree
1353
1334
  ? "failed"
1354
- : record.state;
1335
+ : "parked";
1355
1336
  const thread = createRestoredThread(runtime, record, worktree, state);
1356
1337
  runtime.threads.set(record.runId, thread);
1357
1338
  runtime.sessionDirs.add(record.sessionDir!);
1358
- if (state === "parked") {
1359
- monitor.restoreRun({
1360
- id: record.runId,
1361
- agent: record.agentName,
1362
- task: record.task,
1363
- status: "parked",
1364
- elapsedMs: record.elapsedMs,
1365
- isolation: record.isolation,
1366
- ...(record.worktree
1367
- ? {
1368
- integrationStatus: record.worktree.state === "active"
1369
- ? ("pending" as const)
1370
- : record.worktree.state,
1371
- ...(worktree ? { worktreeId: worktreeGroupId(worktree) } : {}),
1372
- }
1373
- : {}),
1374
- });
1375
- } else if (thread.lastResult) {
1376
- runtime.registerRunResult(record.runId, thread.lastResult);
1377
- }
1339
+ monitor.restoreRun({
1340
+ id: record.runId,
1341
+ agent: record.agentName,
1342
+ task: record.task,
1343
+ status: "parked",
1344
+ elapsedMs: record.elapsedMs,
1345
+ isolation: record.isolation,
1346
+ ...(record.worktree
1347
+ ? {
1348
+ integrationStatus: record.worktree.state === "active"
1349
+ ? ("pending" as const)
1350
+ : record.worktree.state,
1351
+ ...(worktree ? { worktreeId: worktreeGroupId(worktree) } : {}),
1352
+ }
1353
+ : {}),
1354
+ });
1378
1355
  restoredIds.push(record.runId);
1379
1356
  }
1380
1357
  return restoredIds;
package/src/tools.ts CHANGED
@@ -106,7 +106,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
106
106
  const context = hadRetainedSession
107
107
  ? "the same retained session and prior context are preserved"
108
108
  : "no prior child session existed, so only the logical run and objective are continued";
109
- return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. Completion will arrive automatically.` }], details: {}, terminate: true };
109
+ return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. It runs in the background — keep working; the result resumes you automatically.` }], details: {} };
110
110
  }
111
111
  }
112
112
  } catch (error) {
package/src/workflow.ts CHANGED
@@ -1,16 +1,15 @@
1
1
  /**
2
2
  * Managed workflow policy and handoff formatting.
3
3
  *
4
- * Successful top-level worker/cleaner runs continue through an independent
5
- * code review gate. A passing gate may run one conditional documentation
6
- * sync; a failing gate is delivered to the main agent, which owns the fix
7
- * decision the runtime never edits code on a reviewer's behalf. Reviewers
8
- * classify documentation drift explicitly, so the low-cost final documenter
9
- * runs only when needed (or conservatively when an older/custom reviewer
10
- * omits the marker). Direct passing gates use the same policy. Top-level
11
- * documenters are explicit standalone writing tasks. Internal steps are
12
- * launched by dispatch directly, so they never re-enter this policy or wake
13
- * the main agent mid-chain.
4
+ * Successful top-level worker/cleaner runs continue through one independent
5
+ * code review gate. A failing managed gate continues into the reviewer fix
6
+ * stage: the same retained reviewer session gets write access and applies its
7
+ * own fix instructions, so nobody outside the gate has to guess what satisfies
8
+ * it. Direct reviewer results never chain a failing direct gate returns its
9
+ * findings to the main agent, which owns that fix decision. Documentation
10
+ * drift is an ordinary gate finding; dispatching a documenter stays the main
11
+ * agent's call. Internal steps are launched by dispatch directly, so they
12
+ * never re-enter this policy or wake the main agent mid-chain.
14
13
  */
15
14
 
16
15
  import { isWriteCapableAgent, type AgentConfig } from "./agents.ts";
@@ -18,8 +17,6 @@ import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } fro
18
17
  import { formatUsageCompact, sumUsage } from "./monitor.ts";
19
18
 
20
19
  export interface WorkflowAgentAvailability {
21
- cleaner: boolean;
22
- documenter: boolean;
23
20
  reviewer: boolean;
24
21
  writer: boolean;
25
22
  }
@@ -29,131 +26,122 @@ export function workflowAgentAvailability(
29
26
  ): WorkflowAgentAvailability {
30
27
  const names = new Set(agents.map((agent) => agent.name));
31
28
  return {
32
- cleaner: names.has("cleaner"),
33
- documenter: names.has("documenter"),
34
29
  reviewer: names.has("reviewer"),
35
30
  writer: agents.some(isWriteCapableAgent),
36
31
  };
37
32
  }
38
33
 
39
- export type DocumentationDisposition = "clean" | "needed";
40
-
41
- /** Only the last standalone documentation disposition line counts. Inline
42
- * examples and prose are ignored so a prompt echo cannot suppress a needed
43
- * conservative sync. */
44
- export function documentationDisposition(output: string): DocumentationDisposition | undefined {
45
- const lines = output.split("\n");
46
- for (let index = lines.length - 1; index >= 0; index--) {
47
- const match = /^\s*DOCUMENTATION:\s*(CLEAN|NEEDED)\s*$/i.exec(lines[index]);
48
- if (match) return match[1].toUpperCase() === "CLEAN" ? "clean" : "needed";
49
- }
50
- return undefined;
51
- }
52
-
53
- export type ManagedWorkflowKind = "post-writer" | "review-pass-sync";
54
-
55
34
  export interface ManagedWorkflowPlan {
56
- kind: ManagedWorkflowKind;
57
35
  initialRelation: string;
58
36
  }
59
37
 
38
+ /** Fixed cap on reviewer fix → re-review rounds inside one managed workflow.
39
+ * The loop converges by itself when reviews pass; the cap only stops
40
+ * pathological burn and hands the still-failing gate back to the main agent. */
41
+ export const MAX_REVIEW_FIX_ROUNDS = 3;
42
+
60
43
  /** Conservative pre-run check used to reserve one shared-repository lane
61
- * around a complete writer workflow or a reviewer that needs a stable diff.
62
- * The actual result is classified again by getManagedWorkflowPlan before a
63
- * downstream child starts. */
44
+ * around a complete writer workflow or a reviewer that needs a stable diff. */
64
45
  export function canStartManagedWorkflow(
65
46
  agent: Pick<AgentConfig, "name" | "tools">,
66
47
  availability: WorkflowAgentAvailability,
67
48
  ): boolean {
68
49
  // Every shared write-capable role—including custom agents—owns the repository
69
50
  // lane even when no downstream role is enabled. Otherwise its edits can race
70
- // a managed writer's documentation snapshot.
51
+ // a managed writer's pending diff.
71
52
  if (isWriteCapableAgent(agent)) return true;
72
53
  if (agent.name === "reviewer") {
73
54
  // Hold a stable diff snapshot against every discoverable writer even when
74
- // this review is advisory. Classification happens only after the read-only
75
- // child returns, too late to acquire the lane safely.
55
+ // this review is advisory: a gate over a moving diff is unsound.
76
56
  return availability.writer;
77
57
  }
78
58
  return false;
79
59
  }
80
60
 
81
- /** Classify only healthy top-level results. In particular, a reviewer without a
82
- * machine verdict is advisory and cannot start any write-capable child; a
83
- * failing gate is not a workflow at all its findings are delivered to the
84
- * main agent, which decides the fixes. */
61
+ /** Classify only healthy top-level writer results; everything else delivers
62
+ * directly, including every reviewer result a direct reviewer dispatch never
63
+ * starts another child. A failing managed gate is expanded by the workflow
64
+ * itself into the reviewer fix stage. */
85
65
  export function getManagedWorkflowPlan(
86
66
  result: SingleResult,
87
67
  availability: WorkflowAgentAvailability,
88
- advisoryReview = false,
89
68
  ): ManagedWorkflowPlan | undefined {
90
69
  if (result.dispatchFailed || isFailedResult(result)) return undefined;
91
70
  if (result.agent === "worker" || result.agent === "cleaner") {
92
- if (!availability.documenter && !availability.reviewer) return undefined;
71
+ if (!availability.reviewer) return undefined;
93
72
  return {
94
- kind: "post-writer",
95
73
  initialRelation: result.agent === "cleaner" ? "initial cleanup" : "initial implementation",
96
74
  };
97
75
  }
98
- // A top-level documenter is already an explicit docs/comments write task. It
99
- // owns the writer lane but delivers directly without an automatic code gate.
100
- if (result.agent === "documenter") return undefined;
101
- if (result.agent !== "reviewer") return undefined;
102
- // An advisory dispatch never chains: the caller asked for a report, so even
103
- // a stray gate verdict must be delivered rather than acted on.
104
- if (advisoryReview) return undefined;
105
-
106
- const output = getResultOutput(result);
107
- // The pass stands as the code gate. Run the conditional documenter only for
108
- // explicit drift or when an older/custom reviewer omitted the marker.
109
- if (
110
- reviewVerdict(output) === "pass" &&
111
- availability.documenter &&
112
- documentationDisposition(output) !== "clean"
113
- ) {
114
- return { kind: "review-pass-sync", initialRelation: "pre-documentation review" };
115
- }
116
76
  return undefined;
117
77
  }
118
78
 
119
- /** Build the single documentation handoff selected after the review gate
120
- * settles or as the reviewer-disabled fallback. The top-level writer's report
121
- * and the terminal gate review are leads; the pending diff stays authoritative.
122
- * At most one of the two is undefined in every managed flow. */
123
- export function buildFinalDocumenterBrief(
124
- lastWriterResult?: SingleResult,
125
- finalReviewResult?: SingleResult,
126
- ): string {
127
- const reportSections = [
128
- ...(lastWriterResult
129
- ? [
130
- `The last writer (${lastWriterResult.agent}) reported:`,
131
- `---`,
132
- getResultOutput(lastWriterResult),
133
- `---`,
134
- ``,
135
- ]
136
- : []),
137
- ...(finalReviewResult
138
- ? [
139
- `The final gate review reported:`,
140
- `---`,
141
- getResultOutput(finalReviewResult),
142
- `---`,
143
- ``,
144
- ]
145
- : []),
146
- ];
79
+ /** Build the code gate that runs directly after a top-level writer. Reports
80
+ * carry intent; the actual pending diff remains authoritative. */
81
+ export function buildFinalReviewBrief(initialResult: SingleResult): string {
82
+ return [
83
+ `Fresh code gate for a managed ${initialResult.agent} workflow.`,
84
+ ``,
85
+ `The top-level ${initialResult.agent}'s full report:`,
86
+ `---`,
87
+ getResultOutput(initialResult),
88
+ `---`,
89
+ ``,
90
+ `Run \`git status\` and \`git diff\` and inspect the actual pending code; the report is context, not proof.`,
91
+ `Remain read-only. Attach a concrete fix instruction to EVERY gate finding — including documentation drift —:`,
92
+ `what to change, where, and how to verify the fix. A failing gate continues into your own write-enabled fix stage,`,
93
+ `so make every instruction executable exactly as written.`,
94
+ `Surface the COMPLETE finding set in this one pass — scan the full changed surface before the verdict;`,
95
+ `do not ration findings across later rounds.`,
96
+ `This is an acceptance gate, not an advisory audit. End with exactly one standalone machine verdict line:`,
97
+ `VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
98
+ ].join("\n");
99
+ }
100
+
101
+ /** Build the follow-up brief for the reviewer fix stage: the same retained
102
+ * reviewer session continues with its read-only boundary lifted and applies
103
+ * its own fix instructions. The workflow continues with a fresh re-review. */
104
+ export function buildReviewerFixBrief(gateOutput: string): string {
105
+ return [
106
+ `Fix stage: your gate review returned REVIEW_FAIL. You now have full write access in this same session.`,
107
+ `Apply every one of your own fix instructions now — exactly the changes you specified, nothing broader.`,
108
+ `Then re-check the code your fixes touch, so the next scan does not open with your own regression, and run`,
109
+ `the narrowest decisive checks (type check, focused tests) to verify.`,
110
+ ``,
111
+ `Your gate review:`,
112
+ `---`,
113
+ gateOutput,
114
+ `---`,
115
+ ``,
116
+ `Report:`,
117
+ `## Fixed`,
118
+ `- each finding → the exact fix applied (path + what changed)`,
119
+ `## Verification`,
120
+ `- checks actually run and their results`,
121
+ `Do not emit another VERDICT; a fresh gate re-reviews the diff after you.`,
122
+ ].join("\n");
123
+ }
124
+
125
+ /** Fresh gate over the updated diff after a fix round. The re-review runs in a
126
+ * brand-new context so it cannot inherit the previous pass's blind spots, and
127
+ * it must rescan the complete surface so new findings surface now, not in a
128
+ * later round. */
129
+ export function buildReReviewBrief(fixResult: SingleResult, round: number): string {
147
130
  return [
148
- `Final documentation sync: the review gate settled and you are the last managed stage before delivery.`,
131
+ `Fresh re-review after fix round ${round}: re-scan the COMPLETE pending diff from scratch.`,
132
+ `Earlier reviews and fix reports are context, not proof — do not inherit their conclusions.`,
149
133
  ``,
150
- ...reportSections,
151
- `Inspect the actual git diff and relevant implementation; the reports are only leads.`,
152
- `Apply every documentation note the reviews recorded, then synchronize stale README/docs, examples, API comments, docstrings,`,
153
- `and explanatory comments with the behavior that will be committed.`,
154
- `Change documentation surfaces only; make zero edits when the diff creates no documentation drift.`,
155
- `The workflow delivers directly after you; no fresh reviewer runs.`,
156
- `Report exact documentation/comment paths changed, or state explicitly that no sync was needed.`,
134
+ `The fix stage reported:`,
135
+ `---`,
136
+ getResultOutput(fixResult),
137
+ `---`,
138
+ ``,
139
+ `Run \`git status\` and \`git diff\` and judge the actual pending code, including side effects of the fixes.`,
140
+ `Surface the complete finding set in this one pass a defect this scan should have caught must not appear later.`,
141
+ `Remain read-only. Attach a concrete fix instruction to EVERY finding; a failing gate continues into another`,
142
+ `write-enabled fix stage.`,
143
+ `End with exactly one standalone machine verdict line:`,
144
+ `VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
157
145
  ].join("\n");
158
146
  }
159
147
 
@@ -161,8 +149,8 @@ export function buildFinalDocumenterBrief(
161
149
  * One step of a managed workflow as delivered: the run id (so the condensed
162
150
  * summary can point at per-run detail via subagent_status), the result, and
163
151
  * the human-readable role within the workflow ("initial implementation",
164
- * "final review", "final documentation sync"). runId is optional only for
165
- * synthetic steps that never spawned a child.
152
+ * "final review"). runId is optional only for synthetic steps that never
153
+ * spawned a child.
166
154
  */
167
155
  export interface ChainStep {
168
156
  runId?: number;
@@ -171,12 +159,14 @@ export interface ChainStep {
171
159
  }
172
160
 
173
161
  export interface ManagedWorkflowOutcome {
174
- kind: ManagedWorkflowKind;
175
162
  steps: ChainStep[];
176
163
  }
177
164
 
178
- function workflowResultStatus(result: SingleResult): string {
165
+ function workflowResultStatus(result: SingleResult, relation?: string): string {
179
166
  if (isFailedResult(result)) return "failed";
167
+ // The fix stage is reviewer-named writer work: judge it by outcome, not by
168
+ // the verdict contract its review stage was held to.
169
+ if (relation === "review fix") return "completed";
180
170
  if (result.agent === "reviewer") {
181
171
  const verdict = reviewVerdict(getResultOutput(result));
182
172
  return verdict ? verdict.toUpperCase() : "NO_VERDICT";
@@ -186,7 +176,7 @@ function workflowResultStatus(result: SingleResult): string {
186
176
 
187
177
  function workflowStepLine(step: ChainStep): string {
188
178
  const id = step.runId !== undefined ? `#${step.runId} ` : "";
189
- return `- ${id}${step.result.agent} · ${step.relation} · ${workflowResultStatus(step.result)}`;
179
+ return `- ${id}${step.result.agent} · ${step.relation} · ${workflowResultStatus(step.result, step.relation)}`;
190
180
  }
191
181
 
192
182
  function appendWorkflowFooter(lines: string[], steps: readonly ChainStep[]): void {
@@ -197,52 +187,14 @@ function appendWorkflowFooter(lines: string[], steps: readonly ChainStep[]): voi
197
187
  lines.push(`Per-run details: subagent_status ${ids.join(" ")}`);
198
188
  }
199
189
 
200
- /** One clear final delivery for post-writer and direct reviewer documenter workflows. */
190
+ /** One clear final delivery for managed writer → gate workflows. */
201
191
  export function formatManagedWorkflowSummary(
202
192
  steps: readonly ChainStep[],
203
193
  terminalResult: SingleResult = steps[steps.length - 1]!.result,
194
+ terminalRelation: string = steps[steps.length - 1]!.relation,
204
195
  ): string {
205
196
  const route = steps.map((step) => step.result.agent).join(" → ");
206
- const lines = [`## Managed workflow: ${route} — final ${workflowResultStatus(terminalResult)}`, "", ...steps.map(workflowStepLine)];
197
+ const lines = [`## Managed workflow: ${route} — final ${workflowResultStatus(terminalResult, terminalRelation)}`, "", ...steps.map(workflowStepLine)];
207
198
  appendWorkflowFooter(lines, steps);
208
199
  return lines.join("\n");
209
200
  }
210
-
211
- export interface GateBriefOptions {
212
- /** A conditional final documenter is available after the gate settles.
213
- * Documentation drift is then routed to it as non-gating notes instead of
214
- * failing the code gate. */
215
- documenterPending: boolean;
216
- }
217
-
218
- /** Build the code gate that runs directly after a top-level writer, before any
219
- * documentation. Reports carry intent; the actual pending diff remains
220
- * authoritative. */
221
- export function buildFinalReviewBrief(
222
- initialResult: SingleResult,
223
- options: GateBriefOptions,
224
- ): string {
225
- return [
226
- `Fresh code gate for a managed ${initialResult.agent} workflow.`,
227
- ``,
228
- `The top-level ${initialResult.agent}'s full report:`,
229
- `---`,
230
- getResultOutput(initialResult),
231
- `---`,
232
- ``,
233
- `Run \`git status\` and \`git diff\` and inspect the actual pending code; the report is context, not proof.`,
234
- `Remain read-only. Attach a concrete fix instruction to EVERY gate finding: what to change, where, and how to verify the fix`,
235
- `— the report returns to the main agent, which uses your instructions to drive the fix.`,
236
- ...(options.documenterPending
237
- ? [
238
- `A conditional documentation sync is available AFTER this gate, so documentation drift is not a code-gate finding:`,
239
- `record it under "## Documentation notes" and emit the standalone line DOCUMENTATION: NEEDED,`,
240
- `or DOCUMENTATION: CLEAN when no documentation update is needed.`,
241
- ]
242
- : [
243
- `No documenter is pending, so documentation drift is an ordinary gate finding.`,
244
- ]),
245
- `This is an acceptance gate, not an advisory audit. End with exactly one standalone machine verdict line:`,
246
- `VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
247
- ].join("\n");
248
- }