@ferris1225/pi-subagents 4.1.23 → 4.2.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/src/setup.ts CHANGED
@@ -40,11 +40,7 @@ import { promptSelectMany, promptSelectOne } from "./ui.ts";
40
40
  /** Short, selection-friendly descriptions for the built-in agents. */
41
41
  const MODULE_HINTS: Record<string, string> = {
42
42
  explorer: "read-only codebase recon (fast, read-only tools)",
43
- worker: "implement / fix / refactor / test (full tools)",
44
- cleaner: "apply proven cleanup and deduplicate code (full tools)",
45
- documenter: "sync diff or whole-codebase comments/docs (docs write)",
46
- synthesizer: "merge fan-out results/long sources into one brief (read-only)",
47
- reviewer: "read-only audits and pre-commit gates",
43
+ executor: "implement / fix / clean up / docs sync / merge results (full tools)",
48
44
  };
49
45
 
50
46
  function moduleLabel(name: string): string {
@@ -248,10 +244,12 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
248
244
 
249
245
  const next: SubagentsConfig = {
250
246
  enabledAgents: enabled,
247
+ // The wizard surfaces every built-in, so an untoggled one was seen and
248
+ // deliberately left off — record them all as known.
249
+ knownAgents: [...BUILTIN_AGENT_NAMES],
251
250
  agentModels,
252
251
  // Full setup returns every agent to capability-aware Auto thinking.
253
252
  agentThinkingLevels: {},
254
- notifyOnReviewPass: base.notifyOnReviewPass,
255
253
  maxResultLines: base.maxResultLines,
256
254
  agentScope: base.agentScope,
257
255
  idleTimeoutSec: base.idleTimeoutSec,
@@ -283,23 +281,17 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
283
281
  const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
284
282
  if (enabled === undefined) continue;
285
283
  next.enabledAgents = enabled;
286
- // A newly enabled role inherits a kindred role's configured model and
284
+ // A newly enabled role inherits explorer's configured model and
287
285
  // thinking level, so the file reflects what it will actually run
288
- // instead of silently falling back to the current main model: cleaner
289
- // follows the reviewer; documenter and synthesizer intentionally
290
- // follow the faster explorer route.
291
- const modelInheritance: ReadonlyArray<[agent: string, from: string]> = [
292
- ["cleaner", "reviewer"],
293
- ["documenter", "explorer"],
294
- ["synthesizer", "explorer"],
295
- ];
296
- for (const [agent, from] of modelInheritance) {
297
- if (config.enabledAgents.includes(agent) || !enabled.includes(agent)) continue;
298
- if (!next.agentModels[agent] && config.agentModels[from]) {
299
- next.agentModels[agent] = config.agentModels[from];
286
+ // instead of silently falling back to the current main model: these
287
+ // roles do light migration-grade work on the fast explorer lane.
288
+ for (const agent of enabled) {
289
+ if (agent === "explorer" || config.enabledAgents.includes(agent)) continue;
290
+ if (!next.agentModels[agent] && config.agentModels.explorer) {
291
+ next.agentModels[agent] = config.agentModels.explorer;
300
292
  }
301
- if (!next.agentThinkingLevels[agent] && config.agentThinkingLevels[from]) {
302
- next.agentThinkingLevels[agent] = config.agentThinkingLevels[from];
293
+ if (!next.agentThinkingLevels[agent] && config.agentThinkingLevels.explorer) {
294
+ next.agentThinkingLevels[agent] = config.agentThinkingLevels.explorer;
303
295
  }
304
296
  }
305
297
  next.agentModels = keepAgentEntries(next.agentModels, enabled);
package/src/spawn.ts CHANGED
@@ -87,16 +87,6 @@ export function getFinalOutput(messages: Message[]): string {
87
87
  return "";
88
88
  }
89
89
 
90
- /** Only the last standalone reviewer verdict line counts. */
91
- export function reviewVerdict(output: string): "pass" | "fail" | undefined {
92
- const lines = output.split("\n");
93
- for (let index = lines.length - 1; index >= 0; index--) {
94
- const match = /^\s*VERDICT:\s*REVIEW_(PASS|FAIL)\s*$/i.exec(lines[index]);
95
- if (match) return match[1].toUpperCase() === "PASS" ? "pass" : "fail";
96
- }
97
- return undefined;
98
- }
99
-
100
90
  export const RESULT_LINE_MAX = 200;
101
91
 
102
92
  export interface TruncatedOutput {
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Stable logical-thread generation lifecycle for background sub-agents.
3
3
  *
4
- * Dispatch owns workflow policy, the live stage projection, and internal role
5
- * briefs; this module owns one
4
+ * Dispatch owns tool policy and role briefs; this module owns one
6
5
  * stable parent generation end to end: managed-repository lane use,
7
- * worktree setup/finalization after downstream review, queue/process ownership,
6
+ * worktree setup/finalization, queue/process ownership,
8
7
  * retained-session resume, and guarded one-time terminal publication.
9
8
  */
10
9
 
@@ -19,7 +18,7 @@ import {
19
18
  resolveAgentTools,
20
19
  type AgentConfig,
21
20
  } from "./agents.ts";
22
- import { completionTriggersTurn, type CompletionMessageItem } from "./completion.ts";
21
+ import { type CompletionMessageItem } from "./completion.ts";
23
22
  import {
24
23
  DEFAULT_THINKING_LEVEL,
25
24
  loadConfig,
@@ -43,18 +42,8 @@ import {
43
42
  failedStartResult,
44
43
  formatCompletionBlock,
45
44
  modelLevelTakeoverNote,
46
- reviewFailFollowUpNote,
47
45
  queuedResult,
48
46
  } from "./format.ts";
49
- import {
50
- canStartManagedWorkflow,
51
- formatManagedWorkflowSummary,
52
- getManagedWorkflowPlan,
53
- workflowAgentAvailability,
54
- type ManagedWorkflowOutcome,
55
- type ManagedWorkflowPlan,
56
- type ReviewMode,
57
- } from "./workflow.ts";
58
47
  import {
59
48
  availableModelsInScope,
60
49
  currentModelRef,
@@ -63,19 +52,17 @@ import {
63
52
  resolveAgentModelRoute,
64
53
  resolveThinkingLevel,
65
54
  } from "./models.ts";
66
- import { monitor, sumUsage } from "./monitor.ts";
55
+ import { monitor } from "./monitor.ts";
67
56
  import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
68
57
  import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
69
58
  import { forkRetainedSession } from "./session-fork.ts";
70
59
  import {
71
60
  buildResumePrompt,
72
61
  getProjectRoot,
73
- getResultOutput,
74
62
  PROJECT_ROOTS_DIR_NAME,
75
63
  RpcRunControl,
76
64
  isFailedResult,
77
65
  isModelLevelFailure,
78
- reviewVerdict,
79
66
  runSingleAgentWithMainFallback,
80
67
  sessionExists,
81
68
  sweepProjectResultArtifacts,
@@ -136,10 +123,10 @@ async function canonicalManagedRepositoryRoot(cwd: string): Promise<string> {
136
123
 
137
124
  /** Run one operation under the canonical original-repository lane.
138
125
  *
139
- * Shared managed generations use the abortable overload for their complete
140
- * writer/reviewer workflow. Isolated generations use the non-abortable overload
141
- * only for their final worktree apply, so model work remains parallel while the
142
- * original checkout mutation cannot race a shared writer or reviewer snapshot.
126
+ * Shared write-capable generations use the abortable overload for their whole
127
+ * run. Isolated generations use the non-abortable overload only for their
128
+ * final worktree apply, so model work remains parallel while the original
129
+ * checkout mutation cannot race a shared writer.
143
130
  */
144
131
  export async function runInManagedRepositoryLane<T>(
145
132
  cwd: string,
@@ -285,8 +272,6 @@ export interface StartBackgroundOptions {
285
272
  environment?: DispatchEnvironment;
286
273
  seed?: SessionSeed;
287
274
  resumeReservation?: ResumeReservation;
288
- /** Gate intensity for this dispatch; a resume keeps the thread's mode. */
289
- review?: ReviewMode;
290
275
  }
291
276
 
292
277
  export type StartBackgroundInternal = (
@@ -341,20 +326,6 @@ export function resolveDispatchModelRoute(
341
326
  };
342
327
  }
343
328
 
344
- export interface ManagedWorkflowRequest extends DispatchEnvironment {
345
- plan: ManagedWorkflowPlan;
346
- initialResult: SingleResult;
347
- groupId: string;
348
- parentRunId: number;
349
- executionCwd: string;
350
- projectCwd: string;
351
- isolation: IsolationMode;
352
- /** Short identity of the isolated worktree shared by every workflow stage. */
353
- worktreeId?: string;
354
- signal: AbortSignal;
355
- rememberLatest: (result: SingleResult) => void;
356
- }
357
-
358
329
  interface BackgroundDispatcherOptions {
359
330
  runtime: SubagentRuntime;
360
331
  /** Live dispatch environment; resolved lazily so control operations work
@@ -373,7 +344,6 @@ interface BackgroundDispatcherOptions {
373
344
  mode: "single" | "parallel",
374
345
  background?: boolean,
375
346
  ) => (results: SingleResult[]) => SubagentDetails;
376
- runManagedWorkflow: (request: ManagedWorkflowRequest) => Promise<ManagedWorkflowOutcome>;
377
347
  }
378
348
 
379
349
  export function createBackgroundDispatcher(options: BackgroundDispatcherOptions): StartBackgroundInternal {
@@ -383,7 +353,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
383
353
  finishRun,
384
354
  makeLiveHandler,
385
355
  makeDetails,
386
- runManagedWorkflow,
387
356
  } = options;
388
357
 
389
358
  const startBackground: StartBackgroundInternal = async (
@@ -400,10 +369,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
400
369
  seed,
401
370
  resumeReservation,
402
371
  } = startOptions;
403
- // The dispatch-time gate choice is a thread property: resumes of a
404
- // review-exempt task stay exempt instead of surprising the caller with a
405
- // full gate after a reload.
406
- const review: ReviewMode = startOptions.review ?? existingThread?.review ?? "gate";
407
372
  if (!runtime.sessionActive) {
408
373
  return failedStartResult(agentName, task, "Parent session shut down before this subagent generation could start.");
409
374
  }
@@ -421,7 +386,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
421
386
  const agent = resolveLiveAgentTools(discoveredAgent);
422
387
  if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
423
388
  return {
424
- ...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker, cleaner, or documenter.`),
389
+ ...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as executor.`),
425
390
  isolation,
426
391
  };
427
392
  }
@@ -516,7 +481,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
516
481
  thread.executionCwd = executionCwd;
517
482
  thread.thinkingLevel = thinkingLevel;
518
483
  thread.isolation = isolation;
519
- thread.review = review;
520
484
  thread.worktree = worktree;
521
485
  thread.state = "queued";
522
486
  thread.control = control;
@@ -540,7 +504,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
540
504
  executionCwd,
541
505
  thinkingLevel,
542
506
  isolation,
543
- review,
544
507
  worktree,
545
508
  state: "queued",
546
509
  control,
@@ -561,9 +524,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
561
524
  });
562
525
 
563
526
  const onLive = makeLiveHandler(runId, generation);
564
- const workflowAvailability = workflowAgentAvailability(runAgents);
565
- const reserveManagedLane =
566
- isolation === "shared" && canStartManagedWorkflow(agent, workflowAvailability);
527
+ // Shared write-capable runs serialize on the repository lane so their
528
+ // edits cannot race; the lane wait releases the process slot because it
529
+ // is write serialization, not pool pacing.
530
+ const reserveManagedLane = isolation === "shared" && isWriteCapableAgent(agent);
567
531
  const runGeneration = async (backgroundSignal: AbortSignal, controller: AbortController): Promise<void> => {
568
532
  if (runtime.threads.get(runId)?.generation !== generation) return;
569
533
  // The generation body owns a process slot from here (a lane wait, if
@@ -660,82 +624,12 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
660
624
  // exactly one aborted result.
661
625
  if (thread.lifecycleOperation === "stop") return;
662
626
 
663
- // A shutdown can win in the microtask gap after the top-level RPC
664
- // settles. Do not launch an obsolete downstream gate or replace the
665
- // stable top-level session with an aborted downstream attempt.
627
+ // A shutdown can win in the microtask gap after the child RPC
628
+ // settles. Never replace the stable top-level session with an
629
+ // aborted partial.
666
630
  if (backgroundSignal.aborted || lifecycleInterrupted() || !runtime.sessionActive) return;
667
631
 
668
632
  if (thread.retireOnSettle) runtime.retireThreadSession(thread);
669
- let workflowOutcome: ManagedWorkflowOutcome | undefined;
670
- const workflowPlan = getManagedWorkflowPlan(
671
- result,
672
- workflowAvailability,
673
- review,
674
- await producedWorkspaceChanges(thread),
675
- );
676
- if (workflowPlan && runtime.sessionActive) {
677
- // The continuation is runtime-initiated (gate review,
678
- // documentation sync): release this generation's
679
- // concurrency slot so managed chains never starve manual
680
- // dispatches. Cancellation and quiescence guarantees are
681
- // unchanged — the task stays abortable and awaited.
682
- runtime.backgroundQueue.suspend(controller);
683
- thread.state = "running";
684
- // The stable parent row now represents workflow ownership, not whichever
685
- // model stage ran most recently. Internal rows own their exact role/model/
686
- // thinking/timing telemetry and remain independently queryable.
687
- monitor.setManagedWorkflow(runId, true);
688
- monitor.setStatus(runId, "running");
689
- monitor.setActivity(runId, "managed workflow running");
690
- workflowOutcome = await runManagedWorkflow({
691
- plan: workflowPlan,
692
- initialResult: result,
693
- groupId: `workflow-${runId}`,
694
- parentRunId: runId,
695
- executionCwd: thread.executionCwd,
696
- projectCwd: originalCwd,
697
- isolation,
698
- ...(worktree ? { worktreeId: worktreeGroupId(worktree) } : {}),
699
- signal: backgroundSignal,
700
- ctx: runCtx,
701
- config: runConfig,
702
- agents: runAgents,
703
- rememberLatest: (latest) => {
704
- if (runtime.threads.get(runId) !== thread || thread.generation !== generation) return;
705
- thread.lastResult = latest;
706
- // Retained control follows the newest child session, but the live parent
707
- // row keeps the original top-level role/model/usage. The active internal
708
- // row already owns the current stage's role and telemetry.
709
- thread.agentName = latest.agent;
710
- thread.task = latest.task;
711
- thread.sessionId = latest.sessionId;
712
- thread.sessionDir = latest.sessionDir;
713
- runtime.retainSession(latest);
714
- if (latest.sessionId && latest.sessionDir) {
715
- persistThreadCheckpoint(runtime, thread, "parked");
716
- }
717
- },
718
- });
719
-
720
- // Park/stop/shutdown owns this generation once it cancels the queue
721
- // signal. The newest internal partial is already on thread.lastResult;
722
- // never replace it with the old top-level result or publish stale output.
723
- if (backgroundSignal.aborted || lifecycleInterrupted() || !runtime.sessionActive) return;
724
-
725
- const finalStep = workflowOutcome.steps[workflowOutcome.steps.length - 1]!;
726
- result = {
727
- ...finalStep.result,
728
- runId,
729
- projectCwd: originalCwd,
730
- isolation,
731
- };
732
- thread.lastResult = result;
733
- thread.agentName = result.agent;
734
- thread.task = result.task;
735
- thread.sessionId = result.sessionId;
736
- thread.sessionDir = result.sessionDir;
737
- runtime.retainSession(result);
738
- }
739
633
  // Claim terminal settlement synchronously before the first slow await.
740
634
  // Park therefore either wins while RPC is still active, or is rejected
741
635
  // once settlement owns the generation. Destructive stop may supersede
@@ -749,89 +643,43 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
749
643
  thread.lifecycleOperation === "settle" &&
750
644
  !thread.retired;
751
645
  try {
752
- // For isolated writers this is deliberately after the managed reviewer
753
- // and any needed documentation stage: every child sees the same worktree,
754
- // then one lifecycle owner integrates the complete settled state exactly once.
755
- await thread.finalizeIsolation(generation, result);
756
- if (!ownsSettlement()) return;
757
- if (workflowOutcome && isolation === "worktree") {
758
- for (const step of workflowOutcome.steps) {
759
- step.result.integrationStatus = result.integrationStatus;
760
- step.result.integrationApplied = result.integrationApplied;
761
- step.result.integrationError = result.integrationError;
762
- step.result.integrationWorktreePath = result.integrationWorktreePath;
763
- step.result.integrationPatchPath = result.integrationPatchPath;
764
- }
765
- }
646
+ // For isolated writers the apply runs only after the child settled,
647
+ // so this lifecycle owner integrates the complete settled state
648
+ // exactly once under the repository lane.
649
+ await thread.finalizeIsolation(generation, result);
650
+ if (!ownsSettlement()) return;
766
651
 
767
- const failed = isFailedResult(result);
768
- thread.state = failed ? "failed" : "completed";
769
- // Stamp the terminal monitor state before projecting it. This gives every
770
- // path a fixed endedAt even when the row is removed immediately.
771
- monitor.setStatus(runId, failed ? "failed" : "done");
772
- thread.elapsedMs = monitor.getElapsedMs(runId) ?? thread.elapsedMs;
773
- // Persist before the sessionActive check: a shutdown that won the
774
- // lifecycle race must still leave the settled record on disk.
775
- persistThreadCheckpoint(runtime, thread, failed ? "failed" : "completed");
776
- if (!runtime.sessionActive || !ownsSettlement()) return;
777
-
778
- const modelLevel = failed && isModelLevelFailure(result);
779
- const dispatchFailed = result.dispatchFailed === true;
780
- const ownedController = thread.queueController;
781
- if (runtime.runControllers.get(runId) === ownedController) runtime.runControllers.delete(runId);
782
- thread.queueController = undefined;
783
- finishRun(
784
- runId,
785
- failed ? "failed" : "done",
786
- workflowOutcome || modelLevel || dispatchFailed ? { silent: true } : undefined,
787
- );
788
- runtime.registerRunResult(runId, result);
652
+ const failed = isFailedResult(result);
653
+ thread.state = failed ? "failed" : "completed";
654
+ // Stamp the terminal monitor state before projecting it. This gives every
655
+ // path a fixed endedAt even when the row is removed immediately.
656
+ monitor.setStatus(runId, failed ? "failed" : "done");
657
+ thread.elapsedMs = monitor.getElapsedMs(runId) ?? thread.elapsedMs;
658
+ // Persist before the sessionActive check: a shutdown that won the
659
+ // lifecycle race must still leave the settled record on disk.
660
+ persistThreadCheckpoint(runtime, thread, failed ? "failed" : "completed");
661
+ if (!runtime.sessionActive || !ownsSettlement()) return;
789
662
 
790
- if (workflowOutcome) {
791
- const lastStep = workflowOutcome.steps[workflowOutcome.steps.length - 1]!;
792
- const finalVerdict = lastStep.result.agent === "reviewer"
793
- ? reviewVerdict(getResultOutput(lastStep.result))
794
- : undefined;
795
- // The delivery is the model's only view of the workflow. On success
796
- // the writer's handoff is the actionable detail (overlaid with the
797
- // parent's integration outcome); on failure the terminal result is.
798
- const writerHandoff = !failed && finalVerdict === "pass"
799
- ? {
800
- ...workflowOutcome.steps[0]!.result,
801
- runId: result.runId ?? workflowOutcome.steps[0]!.result.runId,
802
- isolation: result.isolation,
803
- integrationStatus: result.integrationStatus,
804
- integrationApplied: result.integrationApplied,
805
- integrationWorktreePath: result.integrationWorktreePath,
806
- integrationPatchPath: result.integrationPatchPath,
807
- integrationError: result.integrationError,
808
- }
809
- : result;
810
- let block = `${formatManagedWorkflowSummary(workflowOutcome.steps, result)}\n\n${formatCompletionBlock(writerHandoff, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}`;
811
- if (finalVerdict === "fail") {
812
- block += `\n\n${reviewFailFollowUpNote()}`;
813
- }
814
- if (modelLevel) block += `\n\n${modelLevelTakeoverNote(result, { runId })}`;
815
- runtime.sendCompletionGroup([{
816
- agent: `managed workflow (${result.agent})`,
817
- block,
818
- triggerTurn: true,
819
- usage: sumUsage(workflowOutcome.steps.map((step) => step.result.usage)),
820
- }]);
821
- runtime.completionBatcher.flush();
822
- return;
823
- }
663
+ const modelLevel = failed && isModelLevelFailure(result);
664
+ const dispatchFailed = result.dispatchFailed === true;
665
+ const ownedController = thread.queueController;
666
+ if (runtime.runControllers.get(runId) === ownedController) runtime.runControllers.delete(runId);
667
+ thread.queueController = undefined;
668
+ finishRun(
669
+ runId,
670
+ failed ? "failed" : "done",
671
+ modelLevel || dispatchFailed ? { silent: true } : undefined,
672
+ );
673
+ runtime.registerRunResult(runId, result);
824
674
 
825
- const completion: CompletionMessageItem = {
826
- agent: result.agent,
827
- block: modelLevel
828
- ? `${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}\n\n${modelLevelTakeoverNote(result, { runId })}`
829
- : result.agent === "reviewer" && reviewVerdict(getResultOutput(result)) === "fail"
830
- ? `${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}\n\n${reviewFailFollowUpNote()}`
831
- : formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) }),
832
- triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
833
- usage: result.usage,
834
- };
675
+ const completion: CompletionMessageItem = {
676
+ agent: result.agent,
677
+ block: modelLevel
678
+ ? `${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}\n\n${modelLevelTakeoverNote(result, { runId })}`
679
+ : formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) }),
680
+ triggerTurn: true,
681
+ usage: result.usage,
682
+ };
835
683
  if (modelLevel) {
836
684
  const detail = result.errorMessage?.trim() || "model unavailable or broken";
837
685
  runCtx.ui.notify(`✗ ${result.agent} dispatch failed: ${detail} — task handed to the main window`, "error");
@@ -911,7 +759,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
911
759
  isolation,
912
760
  exitCode: 1,
913
761
  stopReason: "error",
914
- errorMessage: `Managed workflow dispatch failed: ${errorMessage}`,
762
+ errorMessage: `Subagent dispatch failed: ${errorMessage}`,
915
763
  dispatchFailed: true,
916
764
  }
917
765
  : {
@@ -1299,21 +1147,6 @@ async function discardRestoredRecord(runtime: SubagentRuntime, record: ThreadRec
1299
1147
  await removeThreadRecord(runtime.configPath, record.runId).catch(() => undefined);
1300
1148
  }
1301
1149
 
1302
- /** Whether a settled generation left anything for a gate to review.
1303
- *
1304
- * Only an isolated worktree can answer: it starts from the integration base, so a
1305
- * diff against that base is precisely this generation's own output. A shared
1306
- * checkout is shared with the user and their editor, so nothing in it can be
1307
- * attributed to one run, and undefined keeps the gate. Wrongly skipping a review
1308
- * is the only failure here that costs correctness rather than a run, so an
1309
- * undecidable case always pays for the run. */
1310
- async function producedWorkspaceChanges(
1311
- thread: SubagentThread,
1312
- ): Promise<boolean | undefined> {
1313
- if (!thread.worktree) return undefined;
1314
- return thread.worktree.hasPendingChanges().catch(() => undefined);
1315
- }
1316
-
1317
1150
  /** Project-scoped <projectRoot>/results for a completion's artifacts. */
1318
1151
  export function projectResultsRoot(configPath: string, cwd: string | undefined): string {
1319
1152
  return join(getProjectRoot(configPath, cwd), "results");
@@ -1334,7 +1167,6 @@ function createRestoredThread(
1334
1167
  executionCwd: record.executionCwd,
1335
1168
  ...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel as ThinkingLevel } : {}),
1336
1169
  isolation: record.isolation,
1337
- ...(record.review ? { review: record.review } : {}),
1338
1170
  worktree,
1339
1171
  state,
1340
1172
  control: new RpcRunControl(record.task, record.generation),