@henryqw/pi-subagent 5.0.0 → 6.1.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.
@@ -1,5 +1,6 @@
1
- import type { Usage } from "@earendil-works/pi-ai";
1
+ import { StringEnum, type Usage } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { PROFILE_NAMES } from "@henryqw/pi-task-models";
3
4
  import {
4
5
  addUsage,
5
6
  capEphemeralSubagentOutput as capOutput,
@@ -9,6 +10,7 @@ import {
9
10
  inspectWorktreeDirty,
10
11
  prepareExactReviewEvidence,
11
12
  WorktreeSetupError,
13
+ type EphemeralSubagentActivityEvent,
12
14
  type EphemeralSubagentExecutor,
13
15
  type EphemeralSubagentResult,
14
16
  type ResolvedRoleLaunch,
@@ -18,6 +20,7 @@ import {
18
20
  import { Type, type Static } from "typebox";
19
21
  import { Check } from "typebox/value";
20
22
  import { runDelegation } from "./delegation.ts";
23
+ import { renderToolLines } from "./tool-render.ts";
21
24
 
22
25
  const MAX_UNITS = 8;
23
26
  const GIT_TIMEOUT_MS = 30_000;
@@ -28,10 +31,14 @@ const ValidationSchema = Type.Object({
28
31
  args: Type.Array(Type.String()),
29
32
  }, { additionalProperties: false });
30
33
 
34
+ const ModelClassSchema = StringEnum(PROFILE_NAMES, { description: "Task model profile" });
35
+
31
36
  const UnitSchema = Type.Object({
32
37
  id: Type.String({ minLength: 1 }),
33
38
  task: Type.String({ minLength: 1 }),
34
39
  validation: Type.Array(ValidationSchema, { minItems: 1 }),
40
+ modelClass: Type.Optional(ModelClassSchema),
41
+ review: Type.Optional(Type.String({ minLength: 1 })),
35
42
  }, { additionalProperties: false });
36
43
 
37
44
  export const DelegateFlowSchema = Type.Object({
@@ -40,10 +47,12 @@ export const DelegateFlowSchema = Type.Object({
40
47
 
41
48
  export const DelegateFlowContinueSchema = Type.Object({
42
49
  guidance: Type.String({ minLength: 1 }),
50
+ modelClass: Type.Optional(ModelClassSchema),
43
51
  }, { additionalProperties: false });
44
52
 
45
53
  type FlowRequest = Static<typeof DelegateFlowSchema>;
46
54
  type FlowUnitRequest = Static<typeof UnitSchema>;
55
+ type FlowModelClass = FlowUnitRequest["modelClass"];
47
56
  type FlowClassification = "setup" | "implementer" | "validation" | "reviewer_findings" | "main" | "infrastructure" | "integration";
48
57
  type FlowPhase = "running" | "blocked";
49
58
  type WidgetStatus = "success" | "failure" | "aborted";
@@ -54,6 +63,7 @@ type ChildSettlement =
54
63
 
55
64
  type UnitState = {
56
65
  request: FlowUnitRequest;
66
+ modelClass: FlowModelClass;
57
67
  worktree: WorktreeInfo;
58
68
  base: string;
59
69
  implementation?: ChildSettlement;
@@ -87,7 +97,7 @@ type FlowState = {
87
97
  generation: number;
88
98
  sessionController: AbortController;
89
99
  implementer: Role;
90
- reviewer: Role;
100
+ reviewer?: Role;
91
101
  main?: MainState;
92
102
  units: UnitState[];
93
103
  setupRecoveries: SetupRecovery[];
@@ -111,7 +121,7 @@ export interface DelegateFlowRuntime {
111
121
  maxRuntimeMs: number;
112
122
  getSessionGeneration: () => number;
113
123
  loadRoles: () => Role[];
114
- resolveLaunch: (role: Role, ctx: ExtensionContext) => ResolvedRoleLaunch;
124
+ resolveLaunch: (role: Role, modelClass: FlowModelClass, ctx: ExtensionContext) => ResolvedRoleLaunch;
115
125
  startWidget: (
116
126
  id: string,
117
127
  role: string,
@@ -121,6 +131,7 @@ export interface DelegateFlowRuntime {
121
131
  ctx: ExtensionContext,
122
132
  ) => void;
123
133
  updateWidgetTokens: (id: string, tokens: number) => void;
134
+ updateWidgetActivity: (id: string, event: EphemeralSubagentActivityEvent) => void;
124
135
  finishWidget: (id: string, status: WidgetStatus) => void;
125
136
  }
126
137
 
@@ -150,6 +161,8 @@ export function parseDelegateFlow(value: unknown): FlowRequest {
150
161
  command: text(validation.command, `units[${unitIndex}].validation[${validationIndex}].command`),
151
162
  args: validation.args.map((value, argumentIndex) => argument(value, `units[${unitIndex}].validation[${validationIndex}].args[${argumentIndex}]`)),
152
163
  })),
164
+ ...(unit.modelClass === undefined ? {} : { modelClass: unit.modelClass }),
165
+ ...(unit.review === undefined ? {} : { review: text(unit.review, `units[${unitIndex}].review`) }),
153
166
  };
154
167
  }),
155
168
  };
@@ -157,7 +170,39 @@ export function parseDelegateFlow(value: unknown): FlowRequest {
157
170
 
158
171
  export function parseDelegateFlowContinue(value: unknown): Static<typeof DelegateFlowContinueSchema> {
159
172
  if (!Check(DelegateFlowContinueSchema, value)) throw new Error("delegate_flow_continue must match the declared tool schema.");
160
- return { guidance: text(value.guidance, "guidance") };
173
+ return {
174
+ guidance: text(value.guidance, "guidance"),
175
+ ...(value.modelClass === undefined ? {} : { modelClass: value.modelClass }),
176
+ };
177
+ }
178
+
179
+ function flowCallLabel(args: { units?: unknown }): string {
180
+ const count = Array.isArray(args.units) ? args.units.length : 0;
181
+ return `delegate_flow · working: ${count} unit${count === 1 ? "" : "s"}`;
182
+ }
183
+
184
+ function flowResultLines(text: string): string[] {
185
+ const lines = text.split(/\r?\n/).filter((line) => line.trim());
186
+ const diagnosticHeader = lines.findIndex((line) => line.trim() === "Diagnostic:");
187
+ const diagnostic = diagnosticHeader === -1 ? undefined : lines[diagnosticHeader + 1];
188
+ const recoveryHeader = lines.findIndex((line) => line.trim() === "Retained Flow state:" || line.trim() === "Attempted allocations preserved without cleanup:");
189
+ const recovery = recoveryHeader === -1 || !lines[recoveryHeader + 1]?.trim().startsWith("- unit=")
190
+ ? undefined
191
+ : lines[recoveryHeader + 1];
192
+ if (diagnostic === undefined) {
193
+ if (recovery === undefined) return lines;
194
+ const recoveryIndex = lines.indexOf(recovery);
195
+ return [lines[0]!, recovery, ...lines.filter((_, index) => index !== 0 && index !== recoveryIndex)];
196
+ }
197
+ const leading = lines.slice(0, Math.min(1, diagnosticHeader));
198
+ if (recovery !== undefined) return [...leading, `Diagnostic: ${diagnostic}`, recovery];
199
+ // Promote the first diagnostic ahead of the result cap.
200
+ return [
201
+ ...leading,
202
+ `Diagnostic: ${diagnostic}`,
203
+ ...lines.slice(leading.length, diagnosticHeader),
204
+ ...lines.slice(diagnosticHeader + 2),
205
+ ];
161
206
  }
162
207
 
163
208
  function errorText(error: unknown): string {
@@ -176,6 +221,11 @@ function implementerTask(unit: FlowUnitRequest): string {
176
221
  return [
177
222
  `Flow Unit ${JSON.stringify(unit.id)} requirements:`,
178
223
  unit.task,
224
+ ...(unit.review === undefined ? [] : [
225
+ "",
226
+ "Review criterion to satisfy; the Reviewer alone decides approval:",
227
+ unit.review,
228
+ ]),
179
229
  "",
180
230
  "Authoritative Flow validation (do not duplicate this final gate):",
181
231
  ...unit.validation.map((validation) => `- ${JSON.stringify(validation)}`),
@@ -188,6 +238,11 @@ function repairTask(unit: FlowUnitRequest, blocked: BlockedState, guidance: stri
188
238
  "",
189
239
  "Original requirements:",
190
240
  unit.task,
241
+ ...(unit.review === undefined ? [] : [
242
+ "",
243
+ "Review criterion to satisfy; the Reviewer alone decides approval:",
244
+ unit.review,
245
+ ]),
191
246
  "",
192
247
  "Authoritative Flow validation (do not duplicate this final gate):",
193
248
  ...unit.validation.map((validation) => `- ${JSON.stringify(validation)}`),
@@ -200,16 +255,19 @@ function repairTask(unit: FlowUnitRequest, blocked: BlockedState, guidance: stri
200
255
  ].join("\n");
201
256
  }
202
257
 
203
- function reviewerTask(unit: FlowUnitRequest, packet: { base: string; tip: string; patchPath: string }): string {
258
+ function reviewerTask(unit: FlowUnitRequest, review: string, packet: { base: string; tip: string; patchPath: string }): string {
204
259
  return [
205
- `Review Flow Unit ${JSON.stringify(unit.id)} against these requirements:`,
260
+ `Review Flow Unit ${JSON.stringify(unit.id)} for this explicit judgment criterion:`,
261
+ review,
262
+ "",
263
+ "Original requirements (context only):",
206
264
  unit.task,
207
265
  "",
208
- "Declared validation already passed:",
266
+ "Declared validation already passed and is authoritative for objective verification:",
209
267
  ...unit.validation.map((validation) => `- ${JSON.stringify(validation)}`),
210
268
  "",
211
269
  `Review Packet: ${JSON.stringify(packet)}`,
212
- "Read the exact patch as authoritative and emit exactly PASS only when there are zero findings.",
270
+ "Review only the criterion above. Read the exact patch as authoritative and emit exactly PASS only when there are zero findings.",
213
271
  ].join("\n");
214
272
  }
215
273
 
@@ -308,7 +366,9 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
308
366
  const runChild = async (
309
367
  flow: FlowState,
310
368
  role: Role,
369
+ modelClass: FlowModelClass,
311
370
  task: string,
371
+ widgetTask: string,
312
372
  cwd: string,
313
373
  widgetId: string,
314
374
  signal: AbortSignal | undefined,
@@ -321,13 +381,14 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
321
381
  const result = await runDelegation(runtime.executor, {
322
382
  signal,
323
383
  onTokens: (tokens) => runtime.updateWidgetTokens(widgetId, tokens),
384
+ onActivity: (event) => runtime.updateWidgetActivity(widgetId, event),
324
385
  prepare: async () => {
325
386
  assertCurrent(flow);
326
- const launch = runtime.resolveLaunch(role, ctx);
387
+ const launch = runtime.resolveLaunch(role, modelClass, ctx);
327
388
  if (launch.missingSkills.length) {
328
389
  ctx.ui.notify(`Subagent role ${role.name} skipped unavailable Pi skills: ${launch.missingSkills.join(", ")}.`, "warning");
329
390
  }
330
- runtime.startWidget(widgetId, role.name, launch.model.id, launch.thinkingLevel, task, ctx);
391
+ runtime.startWidget(widgetId, role.name, launch.model.id, launch.thinkingLevel, widgetTask, ctx);
331
392
  started = true;
332
393
  return { launch, task, cwd };
333
394
  },
@@ -437,6 +498,14 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
437
498
  const lines = [
438
499
  `Flow ${outcome}.`,
439
500
  flow.completed.length ? `Completed units: ${flow.completed.map(({ id, noOp }) => `${JSON.stringify(id)}${noOp ? " (no-op)" : ""}`).join(", ")}` : "Completed units: none.",
501
+ ...(flow.setupRecoveries.length ? [
502
+ "Attempted allocations preserved without cleanup:",
503
+ ...flow.setupRecoveries.map((recovery) => `- unit=${JSON.stringify(recovery.id)} path=${JSON.stringify(recovery.path)} branch=${JSON.stringify(recovery.branch)} base=${recovery.base}`),
504
+ ] : []),
505
+ ...(retainedUnits.length ? [
506
+ "Retained Flow state:",
507
+ ...retainedUnits.map((unit) => `- unit=${JSON.stringify(unit.id)} path=${JSON.stringify(unit.path)} branch=${JSON.stringify(unit.branch)} base=${unit.base} worktree=${unit.worktreeRetained} branch_ref=${unit.branchRetained}`),
508
+ ] : []),
440
509
  ...(blocked ? [
441
510
  `Blocked unit: ${JSON.stringify(blocked.unit.request.id)}.`,
442
511
  `Classification: ${blocked.classification}.`,
@@ -446,14 +515,6 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
446
515
  ] : []),
447
516
  ...(failure ? [`Classification: ${failure.classification}.`, `Diagnostic:\n${failure.diagnostic}`] : []),
448
517
  ...(flow.warnings.length ? ["Warnings:", ...flow.warnings.map((warning) => `- ${warning}`)] : []),
449
- ...(flow.setupRecoveries.length ? [
450
- "Attempted allocations preserved without cleanup:",
451
- ...flow.setupRecoveries.map((recovery) => `- unit=${JSON.stringify(recovery.id)} path=${JSON.stringify(recovery.path)} branch=${JSON.stringify(recovery.branch)} base=${recovery.base}`),
452
- ] : []),
453
- ...(retainedUnits.length ? [
454
- "Retained Flow state:",
455
- ...retainedUnits.map((unit) => `- unit=${JSON.stringify(unit.id)} path=${JSON.stringify(unit.path)} branch=${JSON.stringify(unit.branch)} base=${unit.base} worktree=${unit.worktreeRetained} branch_ref=${unit.branchRetained}`),
456
- ] : []),
457
518
  ];
458
519
  return {
459
520
  content: [{ type: "text" as const, text: capOutput(lines.join("\n")) }],
@@ -624,45 +685,54 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
624
685
  continue;
625
686
  }
626
687
 
627
- let evidence;
628
- try {
629
- evidence = await prepareExactReviewEvidence({ base: main.expectedHead, tip, worktree: unit.worktree.path }, signal);
630
- } catch (error) {
631
- return terminal(flow, "infrastructure", errorText(error), meter);
632
- }
633
-
634
- let review: ChildSettlement;
635
- let cleanupError: unknown;
636
- try {
637
- assertCurrent(flow);
638
- review = await runChild(
639
- flow,
640
- flow.reviewer,
641
- reviewerTask(unit.request, { base: evidence.base, tip: evidence.tip, patchPath: evidence.patchPath }),
642
- unit.worktree.cwd,
643
- `${toolCallId}:flow:${flow.index}:review`,
644
- signal,
645
- ctx,
646
- meter,
647
- );
648
- } finally {
688
+ let approvedTip = tip;
689
+ const reviewCriterion = unit.request.review;
690
+ if (reviewCriterion !== undefined) {
691
+ const reviewer = flow.reviewer;
692
+ if (!reviewer) return terminal(flow, "infrastructure", "Flow Reviewer was not resolved for a unit that requires review.", meter);
693
+ let evidence;
649
694
  try {
650
- await evidence.cleanup();
695
+ evidence = await prepareExactReviewEvidence({ base: main.expectedHead, tip, worktree: unit.worktree.path }, signal);
651
696
  } catch (error) {
652
- cleanupError = error;
697
+ return terminal(flow, "infrastructure", errorText(error), meter);
653
698
  }
654
- }
655
- if (cleanupError !== undefined) return terminal(flow, "infrastructure", errorText(cleanupError), meter);
656
- assertCurrent(flow);
657
- if ("error" in review) return terminal(flow, "infrastructure", errorText(review.error), meter);
658
- if (review.result.outcome !== "success") {
659
- return terminal(flow, "infrastructure", settlementFailure(review)!, meter);
660
- }
661
- if (TRUNCATED_OUTPUT.test(review.result.output)) {
662
- return terminal(flow, "infrastructure", "Reviewer transport output was truncated; approval is invalid.", meter);
663
- }
664
- if (review.result.output.trim() !== "PASS") {
665
- return block(flow, unit, "reviewer_findings", review.result.output || "Reviewer returned no PASS approval.", meter);
699
+
700
+ let review: ChildSettlement;
701
+ let cleanupError: unknown;
702
+ try {
703
+ assertCurrent(flow);
704
+ review = await runChild(
705
+ flow,
706
+ reviewer,
707
+ unit.modelClass,
708
+ reviewerTask(unit.request, reviewCriterion, { base: evidence.base, tip: evidence.tip, patchPath: evidence.patchPath }),
709
+ unit.request.task,
710
+ unit.worktree.cwd,
711
+ `${toolCallId}:flow:${flow.index}:review`,
712
+ signal,
713
+ ctx,
714
+ meter,
715
+ );
716
+ } finally {
717
+ try {
718
+ await evidence.cleanup();
719
+ } catch (error) {
720
+ cleanupError = error;
721
+ }
722
+ }
723
+ if (cleanupError !== undefined) return terminal(flow, "infrastructure", errorText(cleanupError), meter);
724
+ assertCurrent(flow);
725
+ if ("error" in review) return terminal(flow, "infrastructure", errorText(review.error), meter);
726
+ if (review.result.outcome !== "success") {
727
+ return terminal(flow, "infrastructure", settlementFailure(review)!, meter);
728
+ }
729
+ if (TRUNCATED_OUTPUT.test(review.result.output)) {
730
+ return terminal(flow, "infrastructure", "Reviewer transport output was truncated; approval is invalid.", meter);
731
+ }
732
+ if (review.result.output.trim() !== "PASS") {
733
+ return block(flow, unit, "reviewer_findings", review.result.output || "Reviewer returned no PASS approval.", meter);
734
+ }
735
+ approvedTip = evidence.tip;
666
736
  }
667
737
 
668
738
  try {
@@ -671,12 +741,12 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
671
741
  return terminal(flow, "main", errorText(error), meter);
672
742
  }
673
743
  assertCurrent(flow);
674
- const merged = await git(["merge", "--no-overwrite-ignore", "--ff-only", evidence.tip], main.root, signal);
744
+ const merged = await git(["merge", "--no-overwrite-ignore", "--ff-only", approvedTip], main.root, signal);
675
745
  assertCurrent(flow);
676
746
  if (merged.code !== 0 || merged.killed) {
677
- const diagnostic = commandFailure(`git merge --no-overwrite-ignore --ff-only ${evidence.tip}`, merged);
747
+ const diagnostic = commandFailure(`git merge --no-overwrite-ignore --ff-only ${approvedTip}`, merged);
678
748
  const previousHead = main.expectedHead;
679
- main.expectedHead = evidence.tip;
749
+ main.expectedHead = approvedTip;
680
750
  try {
681
751
  await checkMain(main);
682
752
  } catch (error) {
@@ -688,7 +758,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
688
758
  ].join("\n")), meter);
689
759
  }
690
760
  flow.warnings.push(capOutput(`Unit ${JSON.stringify(unit.request.id)} integrated after merge reported failure: ${diagnostic}`));
691
- } else main.expectedHead = evidence.tip;
761
+ } else main.expectedHead = approvedTip;
692
762
  flow.completed.push({ id: unit.request.id, noOp: false });
693
763
  try {
694
764
  await checkMain(main);
@@ -696,7 +766,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
696
766
  return terminal(flow, "integration", errorText(error), meter);
697
767
  }
698
768
  assertCurrent(flow);
699
- const cleanupWarning = await cleanupUnit(unit, main, evidence.tip, flow.sessionController.signal);
769
+ const cleanupWarning = await cleanupUnit(unit, main, approvedTip, flow.sessionController.signal);
700
770
  assertCurrent(flow);
701
771
  if (cleanupWarning) flow.warnings.push(`Unit ${JSON.stringify(unit.request.id)} integrated, but cleanup refused: ${cleanupWarning}`);
702
772
  flow.index += 1;
@@ -709,28 +779,38 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
709
779
  pi.registerTool({
710
780
  name: "delegate_flow",
711
781
  label: "Delegate Flow",
712
- description: "Run 1–8 independent Implementers in isolated Unit Worktrees, then validate, exactly review, and serially fast-forward approved units.",
713
- promptSnippet: "Run a deterministic parallel-implementation, serial-review Flow",
782
+ description: "Run 1–8 independent Implementers in isolated Unit Worktrees, validate and serially fast-forward each tip, with exact review only for units that declare a judgment criterion.",
783
+ promptSnippet: "Run a deterministic parallel-implementation, serial-verification Flow",
714
784
  promptGuidelines: [
715
785
  "Use delegate_flow only for cohesive units expected to commute; combine work that overlaps files, APIs, schemas, generated output, package metadata, lockfiles, or invariants.",
716
- "Each unit must include explicit bounded requirements and its authoritative direct command/argument validation gate.",
717
- "If a Flow blocks, inspect its classification and call delegate_flow_continue once with explicit repair guidance.",
786
+ "Each unit must include explicit bounded requirements and its authoritative direct command/argument validation gate. Add review only for an explicit judgment that validation cannot establish.",
787
+ "If a Flow blocks, inspect its classification and call delegate_flow_continue once with explicit repair guidance; modelClass may replace that one repair's current class.",
718
788
  ],
719
789
  parameters: DelegateFlowSchema,
790
+ renderShell: "self",
791
+ renderCall(args, theme, _context) {
792
+ return renderToolLines([theme.fg("toolTitle", flowCallLabel(args))], theme);
793
+ },
794
+ renderResult(result, _options, theme, _context) {
795
+ const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
796
+ return renderToolLines(flowResultLines(text), theme);
797
+ },
720
798
  prepareArguments: parseDelegateFlow,
721
799
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
722
800
  const request = parseDelegateFlow(params);
723
801
  if (active) throw new Error("delegate_flow rejected because another Flow is active.");
724
802
  const roles = runtime.loadRoles();
725
803
  const implementer = roles.find(({ name }) => name === "implementer");
726
- const reviewer = roles.find(({ name }) => name === "reviewer");
727
- if (!implementer || !reviewer) throw new Error("delegate_flow requires implementer and reviewer Roles.");
804
+ const needsReviewer = request.units.some(({ review }) => review !== undefined);
805
+ const reviewer = needsReviewer ? roles.find(({ name }) => name === "reviewer") : undefined;
806
+ if (!implementer) throw new Error("delegate_flow requires an implementer Role.");
807
+ if (needsReviewer && !reviewer) throw new Error("delegate_flow requires a reviewer Role when a unit declares review.");
728
808
  const flow: FlowState = {
729
809
  phase: "running",
730
810
  generation: runtime.getSessionGeneration(),
731
811
  sessionController: new AbortController(),
732
812
  implementer,
733
- reviewer,
813
+ ...(reviewer === undefined ? {} : { reviewer }),
734
814
  units: [],
735
815
  setupRecoveries: [],
736
816
  index: 0,
@@ -762,6 +842,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
762
842
  if (!worktree) throw new Error("Flow Unit Worktrees require a Git repository with a committed HEAD; generic cwd fallback is disabled.");
763
843
  flow.units.push({
764
844
  request: unit,
845
+ modelClass: unit.modelClass,
765
846
  worktree,
766
847
  base: worktree.baseCommit,
767
848
  repairUsed: false,
@@ -777,7 +858,9 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
777
858
  const settlements = await Promise.all(flow.units.map((unit, index) => runChild(
778
859
  flow,
779
860
  flow.implementer,
861
+ unit.modelClass,
780
862
  implementerTask(unit.request),
863
+ unit.request.task,
781
864
  unit.worktree.cwd,
782
865
  `${toolCallId}:flow:${index}:implement`,
783
866
  operationSignal,
@@ -805,13 +888,21 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
805
888
  pi.registerTool({
806
889
  name: "delegate_flow_continue",
807
890
  label: "Continue Delegate Flow",
808
- description: "Repair the one blocked Flow Unit in its existing Unit Worktree, then resume declared-order validation, exact review, and integration.",
891
+ description: "Repair the one blocked Flow Unit in its existing Unit Worktree, optionally replace its model class, then resume declared-order validation, conditional exact review, and integration.",
809
892
  promptSnippet: "Repair and continue the blocked deterministic Flow",
810
893
  promptGuidelines: ["Call delegate_flow_continue only after delegate_flow reports a repairable block, with explicit guidance addressing that block."],
811
894
  parameters: DelegateFlowContinueSchema,
895
+ renderShell: "self",
896
+ renderCall(_args, theme, _context) {
897
+ return renderToolLines([theme.fg("toolTitle", "delegate_flow_continue · working: repair continuation")], theme);
898
+ },
899
+ renderResult(result, _options, theme, _context) {
900
+ const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
901
+ return renderToolLines(flowResultLines(text), theme);
902
+ },
812
903
  prepareArguments: parseDelegateFlowContinue,
813
904
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
814
- const { guidance } = parseDelegateFlowContinue(params);
905
+ const { guidance, modelClass } = parseDelegateFlowContinue(params);
815
906
  const flow = active;
816
907
  if (!flow) throw new Error("delegate_flow_continue requires an active blocked Flow.");
817
908
  assertCurrent(flow);
@@ -822,13 +913,16 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
822
913
  flow.phase = "running";
823
914
  flow.blocked = undefined;
824
915
  unit.repairUsed = true;
916
+ if (modelClass !== undefined) unit.modelClass = modelClass;
825
917
  const operationSignal = bindSignal(flow, signal);
826
918
  const meter: UsageMeter = {};
827
919
  try {
828
920
  unit.implementation = await runChild(
829
921
  flow,
830
922
  flow.implementer,
923
+ unit.modelClass,
831
924
  repairTask(unit.request, blocked, guidance),
925
+ unit.request.task,
832
926
  unit.worktree.cwd,
833
927
  `${toolCallId}:flow:${flow.index}:repair`,
834
928
  operationSignal,