parallel-codex-tui 0.3.0 → 0.3.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.
@@ -70,7 +70,7 @@ export async function createFeatureChannel(input) {
70
70
  await writeText(channel.actorRepliesPath, "");
71
71
  await writeText(channel.criticFindingsPath, "");
72
72
  if (input.actorEngine && input.criticEngine) {
73
- await writeFeatureAssignment(channel, input.actorEngine, input.criticEngine);
73
+ await writeFeatureAssignment(channel, input.actorEngine, input.criticEngine, input.actorModel, input.criticModel);
74
74
  }
75
75
  await updateFeatureStatus(channel, "created");
76
76
  await appendFeatureDialogue(channel, "feature.created", "actor", "Feature mailbox created for the current turn.");
@@ -89,7 +89,7 @@ async function ensureFeatureChannelFiles(input, channel, refreshDefinition) {
89
89
  }
90
90
  }
91
91
  if (!(await pathExists(channel.assignmentPath)) && input.actorEngine && input.criticEngine) {
92
- await writeFeatureAssignment(channel, input.actorEngine, input.criticEngine);
92
+ await writeFeatureAssignment(channel, input.actorEngine, input.criticEngine, input.actorModel, input.criticModel);
93
93
  }
94
94
  }
95
95
  export async function readFeatureAssignment(channel, fallback) {
@@ -101,15 +101,23 @@ export async function readFeatureAssignment(channel, fallback) {
101
101
  version: 1,
102
102
  actor_engine: fallback.actor,
103
103
  critic_engine: fallback.critic,
104
+ actor_model: "",
105
+ critic_model: "",
106
+ actor_override: false,
107
+ critic_override: false,
104
108
  updated_at: new Date(0).toISOString()
105
109
  });
106
110
  }
107
111
  }
108
- export async function writeFeatureAssignment(channel, actorEngine, criticEngine) {
112
+ export async function writeFeatureAssignment(channel, actorEngine, criticEngine, actorModel = "", criticModel = "", options = {}) {
109
113
  const assignment = FeatureAssignmentSchema.parse({
110
114
  version: 1,
111
115
  actor_engine: actorEngine,
112
116
  critic_engine: criticEngine,
117
+ actor_model: actorModel,
118
+ critic_model: criticModel,
119
+ actor_override: options.actorOverride ?? false,
120
+ critic_override: options.criticOverride ?? false,
113
121
  updated_at: new Date().toISOString()
114
122
  });
115
123
  await writeJson(channel.assignmentPath, assignment);
@@ -1,5 +1,7 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { cp, readdir } from "node:fs/promises";
2
3
  import { join } from "node:path";
4
+ import { runRuntimePreflight } from "../doctor.js";
3
5
  import { appendJsonLine, ensureDir, pathExists, readJson, readTextIfExists, removeIfExists, writeJson, writeText } from "../core/file-store.js";
4
6
  import { runWithLeaseFinalization } from "../core/lease-finalization.js";
5
7
  import { extractMainResponse } from "../core/main-response.js";
@@ -184,17 +186,25 @@ export class Orchestrator {
184
186
  };
185
187
  const assignmentPath = join(featureDir, "assignment.json");
186
188
  const current = await readFeatureAssignment({ assignmentPath }, fallback);
187
- const assignment = await writeFeatureAssignment({ assignmentPath }, input.role === "actor" ? input.engine : current.actor_engine, input.role === "critic" ? input.engine : current.critic_engine);
189
+ const nextModel = input.model === undefined
190
+ ? input.role === "actor"
191
+ ? current.actor_engine === input.engine ? current.actor_model : ""
192
+ : current.critic_engine === input.engine ? current.critic_model : ""
193
+ : input.model;
194
+ const assignment = await writeFeatureAssignment({ assignmentPath }, input.role === "actor" ? input.engine : current.actor_engine, input.role === "critic" ? input.engine : current.critic_engine, input.role === "actor" ? nextModel : current.actor_model, input.role === "critic" ? nextModel : current.critic_model, {
195
+ actorOverride: input.role === "actor" ? true : current.actor_override,
196
+ criticOverride: input.role === "critic" ? true : current.critic_override
197
+ });
188
198
  await appendJsonLine(join(task.dir, "dialogue", "actor-critic.jsonl"), {
189
199
  time: new Date().toISOString(),
190
200
  feature_id: input.featureId,
191
201
  turn_id: status.turn_id,
192
202
  type: "feature.assignment_changed",
193
203
  role: input.role,
194
- message: `${capitalize(input.role)} reassigned to ${input.engine}.`,
204
+ message: `${capitalize(input.role)} reassigned to ${input.engine}/${nextModel.trim() || "default"}.`,
195
205
  paths: { assignment: assignmentPath }
196
206
  });
197
- await this.sessions.appendEvent(task, "feature.assignment_changed", `${input.featureId} ${input.role} reassigned to ${input.engine}`);
207
+ await this.sessions.appendEvent(task, "feature.assignment_changed", `${input.featureId} ${input.role} reassigned to ${input.engine}/${nextModel.trim() || "default"}`);
198
208
  return { featureId: input.featureId, assignment };
199
209
  });
200
210
  }
@@ -205,6 +215,18 @@ export class Orchestrator {
205
215
  }
206
216
  return this.roleConfiguration.snapshot(task?.dir);
207
217
  }
218
+ async validateRoleConfiguration(roles) {
219
+ const parsed = this.roleConfiguration.validate(roles);
220
+ const candidate = structuredClone(this.config);
221
+ candidate.router.defaultMode = "auto";
222
+ candidate.pairing.main = parsed.main.engine;
223
+ candidate.pairing.judge = parsed.judge.engine;
224
+ candidate.pairing.actor = parsed.actor.engine;
225
+ candidate.pairing.critic = parsed.critic.engine;
226
+ const preflight = this.dependencies.roleConfigurationPreflight
227
+ ?? ((config, workspaceRoot) => runRuntimePreflight(config, workspaceRoot, process.env, { includeRouter: false }));
228
+ return preflight(candidate, this.roleConfiguration.workspaceRootPath());
229
+ }
208
230
  async updateRoleConfiguration(input) {
209
231
  if (input.scope !== "task") {
210
232
  await this.roleConfiguration.apply(input.scope, input.roles);
@@ -273,7 +295,15 @@ export class Orchestrator {
273
295
  if (!status || status.task_id !== task.id || status.state === "approved") {
274
296
  continue;
275
297
  }
276
- await writeFeatureAssignment({ assignmentPath: join(featureDir, "assignment.json") }, executionRoles.actor.engine, executionRoles.critic.engine);
298
+ const assignmentPath = join(featureDir, "assignment.json");
299
+ const current = await readFeatureAssignment({ assignmentPath }, {
300
+ actor: executionRoles.actor.engine,
301
+ critic: executionRoles.critic.engine
302
+ });
303
+ await writeFeatureAssignment({ assignmentPath }, current.actor_override ? current.actor_engine : executionRoles.actor.engine, current.critic_override ? current.critic_engine : executionRoles.critic.engine, current.actor_override ? current.actor_model : executionRoles.actor.model, current.critic_override ? current.critic_model : executionRoles.critic.model, {
304
+ actorOverride: current.actor_override,
305
+ criticOverride: current.critic_override
306
+ });
277
307
  }
278
308
  }
279
309
  async resumeTaskRun(input, pausedFeatureId) {
@@ -536,6 +566,8 @@ export class Orchestrator {
536
566
  feature,
537
567
  actorEngine: route.actor_engine,
538
568
  criticEngine: route.critic_engine,
569
+ actorModel: selectedRoleModel(input.roleSelection, "actor", route.actor_engine),
570
+ criticModel: selectedRoleModel(input.roleSelection, "critic", route.critic_engine),
539
571
  resume: input.retry
540
572
  })));
541
573
  return await this.runFeaturePlanWithConflictReplan(input, task, route, turn, workers, judge, featurePlan, features);
@@ -547,6 +579,8 @@ export class Orchestrator {
547
579
  judgeDir: judge.dir,
548
580
  actorEngine: route.actor_engine,
549
581
  criticEngine: route.critic_engine,
582
+ actorModel: selectedRoleModel(input.roleSelection, "actor", route.actor_engine),
583
+ criticModel: selectedRoleModel(input.roleSelection, "critic", route.critic_engine),
550
584
  resume: input.retry
551
585
  });
552
586
  features = [feature];
@@ -582,6 +616,8 @@ export class Orchestrator {
582
616
  feature,
583
617
  actorEngine: route.actor_engine,
584
618
  criticEngine: route.critic_engine,
619
+ actorModel: selectedRoleModel(input.roleSelection, "actor", route.actor_engine),
620
+ criticModel: selectedRoleModel(input.roleSelection, "critic", route.critic_engine),
585
621
  resume: input.retry
586
622
  })));
587
623
  return await this.runFeaturePlanWithConflictReplan(input, task, route, turn, workers, judge, featurePlan, features);
@@ -593,6 +629,8 @@ export class Orchestrator {
593
629
  judgeDir: judge.dir,
594
630
  actorEngine: route.actor_engine,
595
631
  criticEngine: route.critic_engine,
632
+ actorModel: selectedRoleModel(input.roleSelection, "actor", route.actor_engine),
633
+ criticModel: selectedRoleModel(input.roleSelection, "critic", route.critic_engine),
596
634
  resume: input.retry
597
635
  });
598
636
  features = [feature];
@@ -761,6 +799,8 @@ export class Orchestrator {
761
799
  feature,
762
800
  actorEngine: route.actor_engine,
763
801
  criticEngine: route.critic_engine,
802
+ actorModel: selectedRoleModel(currentInput.roleSelection, "actor", route.actor_engine),
803
+ criticModel: selectedRoleModel(currentInput.roleSelection, "critic", route.critic_engine),
764
804
  resume: true,
765
805
  refreshDefinition: true
766
806
  })));
@@ -845,7 +885,11 @@ export class Orchestrator {
845
885
  await this.sessions.updateTaskStatus(task, "actor_running");
846
886
  const actorRunById = new Map();
847
887
  if (restoredWave) {
848
- const restoredActors = await Promise.all(wave.map((definition) => this.loadCompletedFeatureActor(task, requiredFeatureAssignment(assignments, requiredChannel(channels, definition)).actor_engine, definition, requiredChannel(channels, definition))));
888
+ const restoredActors = await Promise.all(wave.map((definition) => {
889
+ const channel = requiredChannel(channels, definition);
890
+ const assignment = requiredFeatureAssignment(assignments, channel);
891
+ return this.loadCompletedFeatureActor(task, assignment.actor_engine, assignment.actor_model, definition, channel);
892
+ }));
849
893
  for (const actorRun of restoredActors) {
850
894
  if (actorRun) {
851
895
  actorRunById.set(actorRun.definition.id, actorRun);
@@ -864,7 +908,8 @@ export class Orchestrator {
864
908
  }
865
909
  const freshActorRuns = await mapWithConcurrency(pendingActors, concurrency, async (definition) => {
866
910
  const channel = requiredChannel(channels, definition);
867
- const actor = await this.runActor(input, task, requiredFeatureAssignment(assignments, channel).actor_engine, judge.dir, workers, turn, channel, undefined, true, requiredFeatureWorkspace(workspaceWave.featureDirs, channel));
911
+ const assignment = requiredFeatureAssignment(assignments, channel);
912
+ const actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, channel, assignment.actor_model, undefined, true, requiredFeatureWorkspace(workspaceWave.featureDirs, channel), true);
868
913
  actorCompleted += 1;
869
914
  reportProgress("actor", actorCompleted, wave.length, actorCompleted === wave.length ? "done" : "running", "waiting");
870
915
  return { definition, channel, actor };
@@ -883,7 +928,10 @@ export class Orchestrator {
883
928
  await this.sessions.updateTaskStatus(task, "critic_running");
884
929
  const pairRunById = new Map();
885
930
  if (restoredWave) {
886
- const restoredPairs = await Promise.all(actorRuns.map((actorRun) => this.loadCompletedFeaturePair(task, requiredFeatureAssignment(assignments, actorRun.channel).critic_engine, actorRun)));
931
+ const restoredPairs = await Promise.all(actorRuns.map((actorRun) => {
932
+ const assignment = requiredFeatureAssignment(assignments, actorRun.channel);
933
+ return this.loadCompletedFeaturePair(task, assignment.critic_engine, assignment.critic_model, actorRun);
934
+ }));
887
935
  for (const pairRun of restoredPairs) {
888
936
  if (pairRun) {
889
937
  pairRunById.set(pairRun.definition.id, pairRun);
@@ -898,8 +946,9 @@ export class Orchestrator {
898
946
  await this.sessions.appendEvent(task, "feature.wave_critic_checkpoints_reused", `Reused ${criticCompleted}/${actorRuns.length} completed Critic checkpoints in wave ${waveNumber}/${featureWaves.length}`);
899
947
  }
900
948
  const freshPairRuns = await mapWithConcurrency(pendingCritics, concurrency, async (actorRun) => {
949
+ const assignment = requiredFeatureAssignment(assignments, actorRun.channel);
901
950
  const reviewWorkspace = await workspaceManager.prepareFeatureReviewWorkspace(workspaceWave, actorRun.channel.id);
902
- const critic = await this.runCritic(input, task, requiredFeatureAssignment(assignments, actorRun.channel).critic_engine, judge.dir, actorRun.actor.dir, workers, turn, actorRun.channel, true, reviewWorkspace);
951
+ const critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, actorRun.actor.dir, workers, turn, actorRun.channel, assignment.critic_model, true, reviewWorkspace);
903
952
  criticCompleted += 1;
904
953
  reportProgress("critic", criticCompleted, actorRuns.length, "done", criticCompleted === actorRuns.length ? "done" : "running");
905
954
  return { ...actorRun, critic };
@@ -959,7 +1008,8 @@ export class Orchestrator {
959
1008
  let revisionCompleted = 0;
960
1009
  reportProgress("revision", revisionCompleted, revisionRuns.length, "revision", "done");
961
1010
  const revisedActors = await mapWithConcurrency(revisionRuns, concurrency, async (pair) => {
962
- const actor = await this.runActor(input, task, requiredFeatureAssignment(assignments, pair.channel).actor_engine, judge.dir, workers, turn, pair.channel, buildRevisionRequest(pair.review, pair.channel, revisionRound, this.config.orchestration.maxRevisionRounds), true, requiredFeatureWorkspace(workspaceWave.featureDirs, pair.channel));
1011
+ const assignment = requiredFeatureAssignment(assignments, pair.channel);
1012
+ const actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, pair.channel, assignment.actor_model, buildRevisionRequest(pair.review, pair.channel, revisionRound, this.config.orchestration.maxRevisionRounds), true, requiredFeatureWorkspace(workspaceWave.featureDirs, pair.channel));
963
1013
  await requireActorFindingReplies(pair.channel, [...(revisionFindingIds.get(pair.channel.id) ?? [])]);
964
1014
  revisionCompleted += 1;
965
1015
  reportProgress("revision", revisionCompleted, revisionRuns.length, "revision", "done");
@@ -970,8 +1020,9 @@ export class Orchestrator {
970
1020
  let recheckCompleted = 0;
971
1021
  reportProgress("critic", recheckCompleted, revisedActors.length, "done", "rerunning");
972
1022
  const revisedPairs = await mapWithConcurrency(revisedActors, concurrency, async (pair) => {
1023
+ const assignment = requiredFeatureAssignment(assignments, pair.channel);
973
1024
  const reviewWorkspace = await workspaceManager.prepareFeatureReviewWorkspace(workspaceWave, pair.channel.id);
974
- const critic = await this.runCritic(input, task, requiredFeatureAssignment(assignments, pair.channel).critic_engine, judge.dir, pair.actor.dir, workers, turn, pair.channel, true, reviewWorkspace);
1025
+ const critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, pair.actor.dir, workers, turn, pair.channel, assignment.critic_model, true, reviewWorkspace);
975
1026
  recheckCompleted += 1;
976
1027
  reportProgress("critic", recheckCompleted, revisedActors.length, "done", "rerunning");
977
1028
  return {
@@ -1101,6 +1152,10 @@ export class Orchestrator {
1101
1152
  };
1102
1153
  const workspaceRootDir = join(task.dir, "workspaces", `turn-${turn.turnId}`, "wave-0001");
1103
1154
  const integratedCheckpoint = input.retry && await waveIntegrationCheckpointMatches(workspaceRootDir, turn.turnId, 1, [feature.id]);
1155
+ const [actorExecution, criticExecution] = await Promise.all([
1156
+ this.featureWorkerExecution(turn, "actor", assignment.actor_engine, assignment.actor_model),
1157
+ this.featureWorkerExecution(turn, "critic", assignment.critic_engine, assignment.critic_model)
1158
+ ]);
1104
1159
  if (integratedCheckpoint) {
1105
1160
  const recovered = !(await featureIsApproved(feature));
1106
1161
  await this.sessions.appendEvent(task, recovered ? "feature.wave_checkpoint_recovered" : "feature.wave_checkpoint_reused", `${recovered ? "Recovered" : "Reused"} integrated checkpoint for single feature: ${feature.id}`);
@@ -1111,7 +1166,7 @@ export class Orchestrator {
1111
1166
  critic: "done",
1112
1167
  featureProgress: { wave: 1, waves: 1, phase: "integration", completed: 1, total: 1 }
1113
1168
  });
1114
- return this.completeTask(task, turn, judge, this.workerFiles(task, taskWorkerId("actor", assignment.actor_engine, turn.turnId)), this.workerFiles(task, taskWorkerId("critic", assignment.critic_engine, turn.turnId)), feature, input, workers, await integratedWaveChangedPaths(workspaceRootDir), workspaceManager, route.judge_engine);
1169
+ return this.completeTask(task, turn, judge, this.workerFiles(task, taskWorkerId("actor", assignment.actor_engine, turn.turnId, undefined, actorExecution.modelKey)), this.workerFiles(task, taskWorkerId("critic", assignment.critic_engine, turn.turnId, undefined, criticExecution.modelKey)), feature, input, workers, await integratedWaveChangedPaths(workspaceRootDir), workspaceManager, route.judge_engine);
1115
1170
  }
1116
1171
  const restoredWave = input.retry ? await workspaceManager.restoreWave(workspaceInput) : null;
1117
1172
  const workspaceWave = restoredWave ?? await workspaceManager.prepareWave(workspaceInput);
@@ -1122,7 +1177,7 @@ export class Orchestrator {
1122
1177
  throwIfCancelled(input.signal);
1123
1178
  await this.sessions.updateTaskStatus(task, "actor_running");
1124
1179
  const restoredActor = restoredWave
1125
- ? await this.loadCompletedActor(task, assignment.actor_engine, feature, false)
1180
+ ? await this.loadCompletedActor(task, assignment.actor_engine, assignment.actor_model, feature, false)
1126
1181
  : null;
1127
1182
  if (restoredActor) {
1128
1183
  await this.sessions.appendEvent(task, "feature.wave_actor_checkpoints_reused", "Reused 1/1 completed Actor checkpoint in wave 1/1");
@@ -1135,13 +1190,13 @@ export class Orchestrator {
1135
1190
  });
1136
1191
  let actor = restoredActor;
1137
1192
  if (!actor) {
1138
- actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, feature, undefined, false, workspaceDir, true);
1193
+ actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, feature, assignment.actor_model, undefined, false, workspaceDir, true);
1139
1194
  }
1140
1195
  await updateFeatureStatus(feature, "actor_done");
1141
1196
  throwIfCancelled(input.signal);
1142
1197
  await this.sessions.updateTaskStatus(task, "critic_running");
1143
1198
  const restoredCritic = restoredActor
1144
- ? await this.loadCompletedCritic(task, assignment.critic_engine, feature, false)
1199
+ ? await this.loadCompletedCritic(task, assignment.critic_engine, assignment.critic_model, feature, false)
1145
1200
  : null;
1146
1201
  if (restoredCritic) {
1147
1202
  await this.sessions.appendEvent(task, "feature.wave_critic_checkpoints_reused", "Reused 1/1 completed Critic checkpoint in wave 1/1");
@@ -1156,7 +1211,7 @@ export class Orchestrator {
1156
1211
  let critic = restoredCritic;
1157
1212
  if (!critic) {
1158
1213
  reviewWorkspace = await workspaceManager.prepareFeatureReviewWorkspace(workspaceWave, feature.id);
1159
- critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, actor.dir, workers, turn, feature, false, reviewWorkspace, true);
1214
+ critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, actor.dir, workers, turn, feature, assignment.critic_model, false, reviewWorkspace, true);
1160
1215
  }
1161
1216
  await updateFeatureStatus(feature, "critic_done");
1162
1217
  throwIfCancelled(input.signal);
@@ -1187,14 +1242,14 @@ export class Orchestrator {
1187
1242
  critic: "done"
1188
1243
  });
1189
1244
  await this.sessions.updateTaskStatus(task, "actor_running");
1190
- actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, feature, buildRevisionRequest(review, feature, revisionRound, this.config.orchestration.maxRevisionRounds), false, workspaceDir, true);
1245
+ actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, feature, assignment.actor_model, buildRevisionRequest(review, feature, revisionRound, this.config.orchestration.maxRevisionRounds), false, workspaceDir, true);
1191
1246
  await requireActorFindingReplies(feature, [...revisionFindingIds]);
1192
1247
  await updateFeatureStatus(feature, "actor_done");
1193
1248
  throwIfCancelled(input.signal);
1194
1249
  reviewWorkspace = await workspaceManager.prepareFeatureReviewWorkspace(workspaceWave, feature.id);
1195
1250
  await this.sessions.updateTaskStatus(task, "critic_running");
1196
1251
  input.onStatus?.({ taskId: task.id, judge: "done", actor: "done", critic: "rerunning" });
1197
- critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, actor.dir, workers, turn, feature, false, reviewWorkspace, true);
1252
+ critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, actor.dir, workers, turn, feature, assignment.critic_model, false, reviewWorkspace, true);
1198
1253
  await updateFeatureStatus(feature, "critic_done");
1199
1254
  throwIfCancelled(input.signal);
1200
1255
  review = await readTextIfExists(`${critic.dir}/review.md`);
@@ -1679,8 +1734,9 @@ export class Orchestrator {
1679
1734
  await this.sessions.appendEvent(task, "judge.final_approved", `Final Judge approved ${criterionIds.length} acceptance criteria`);
1680
1735
  input.onStatus?.({ taskId: task.id, judge: "done", actor: "done", critic: "done" });
1681
1736
  }
1682
- async runActor(input, task, engine, judgeDir, workers, turn, feature, revision, featureScoped = false, workspaceDir = input.cwd, isolatedWorkspace = featureScoped) {
1683
- const workerId = taskWorkerId("actor", engine, turn.turnId, featureScoped ? feature.id : undefined);
1737
+ async runActor(input, task, engine, judgeDir, workers, turn, feature, modelName = "", revision, featureScoped = false, workspaceDir = input.cwd, isolatedWorkspace = featureScoped) {
1738
+ const execution = await this.featureWorkerExecution(turn, "actor", engine, modelName);
1739
+ const workerId = taskWorkerId("actor", engine, turn.turnId, featureScoped ? feature.id : undefined, execution.modelKey);
1684
1740
  const workerFiles = this.workerFiles(task, workerId);
1685
1741
  const actor = await this.sessions.initializeWorker(task, {
1686
1742
  workerId,
@@ -1726,7 +1782,7 @@ export class Orchestrator {
1726
1782
  outputLogPath: actor.outputLogPath,
1727
1783
  statusPath: actor.statusPath,
1728
1784
  prompt: await readTextIfExists(actor.promptPath),
1729
- modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "actor", engine),
1785
+ modelConfig: execution.modelConfig,
1730
1786
  signal: input.signal
1731
1787
  }, task, feature, featureScoped
1732
1788
  ? undefined
@@ -1740,8 +1796,9 @@ export class Orchestrator {
1740
1796
  });
1741
1797
  return actor;
1742
1798
  }
1743
- async runCritic(input, task, engine, judgeDir, actorDir, workers, turn, feature, featureScoped = false, workspaceDir = input.cwd, isolatedWorkspace = featureScoped) {
1744
- const workerId = taskWorkerId("critic", engine, turn.turnId, featureScoped ? feature.id : undefined);
1799
+ async runCritic(input, task, engine, judgeDir, actorDir, workers, turn, feature, modelName = "", featureScoped = false, workspaceDir = input.cwd, isolatedWorkspace = featureScoped) {
1800
+ const execution = await this.featureWorkerExecution(turn, "critic", engine, modelName);
1801
+ const workerId = taskWorkerId("critic", engine, turn.turnId, featureScoped ? feature.id : undefined, execution.modelKey);
1745
1802
  const workerFiles = this.workerFiles(task, workerId);
1746
1803
  const critic = await this.sessions.initializeWorker(task, {
1747
1804
  workerId,
@@ -1787,7 +1844,7 @@ export class Orchestrator {
1787
1844
  outputLogPath: critic.outputLogPath,
1788
1845
  statusPath: critic.statusPath,
1789
1846
  prompt: await readTextIfExists(critic.promptPath),
1790
- modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "critic", engine),
1847
+ modelConfig: execution.modelConfig,
1791
1848
  signal: input.signal
1792
1849
  }, task, feature, featureScoped
1793
1850
  ? undefined
@@ -2058,35 +2115,39 @@ export class Orchestrator {
2058
2115
  }
2059
2116
  return undefined;
2060
2117
  }
2061
- async loadCompletedFeatureActor(task, engine, definition, channel) {
2062
- const actor = await this.loadCompletedActor(task, engine, channel, true);
2118
+ async loadCompletedFeatureActor(task, engine, modelName, definition, channel) {
2119
+ const actor = await this.loadCompletedActor(task, engine, modelName, channel, true);
2063
2120
  if (!actor) {
2064
2121
  return null;
2065
2122
  }
2066
2123
  return { definition, channel, actor };
2067
2124
  }
2068
- async loadCompletedFeaturePair(task, engine, actorRun) {
2069
- const critic = await this.loadCompletedCritic(task, engine, actorRun.channel, true);
2125
+ async loadCompletedFeaturePair(task, engine, modelName, actorRun) {
2126
+ const critic = await this.loadCompletedCritic(task, engine, modelName, actorRun.channel, true);
2070
2127
  return critic ? { ...actorRun, critic } : null;
2071
2128
  }
2072
- async loadCompletedActor(task, engine, channel, featureScoped) {
2073
- const actor = this.workerFiles(task, taskWorkerId("actor", engine, channel.turnId, featureScoped ? channel.id : undefined));
2129
+ async loadCompletedActor(task, engine, modelName, channel, featureScoped) {
2130
+ const execution = await this.featureWorkerExecution({ dir: join(task.dir, "turns", channel.turnId) }, "actor", engine, modelName);
2131
+ const actor = this.workerFiles(task, taskWorkerId("actor", engine, channel.turnId, featureScoped ? channel.id : undefined, execution.modelKey));
2074
2132
  const status = await readWorkerStatusIfValid(actor.statusPath);
2075
2133
  if (status?.state !== "done"
2076
2134
  || status.role !== "actor"
2077
2135
  || status.engine !== engine
2136
+ || (execution.modelKey && status.model_name !== execution.modelConfig.name)
2078
2137
  || (featureScoped ? status.feature_id !== channel.id : Boolean(status.feature_id))) {
2079
2138
  return null;
2080
2139
  }
2081
2140
  await mirrorWorkerFileToFeature(join(actor.dir, "worklog.md"), channel.actorWorklogPath);
2082
2141
  return actor;
2083
2142
  }
2084
- async loadCompletedCritic(task, engine, channel, featureScoped) {
2085
- const critic = this.workerFiles(task, taskWorkerId("critic", engine, channel.turnId, featureScoped ? channel.id : undefined));
2143
+ async loadCompletedCritic(task, engine, modelName, channel, featureScoped) {
2144
+ const execution = await this.featureWorkerExecution({ dir: join(task.dir, "turns", channel.turnId) }, "critic", engine, modelName);
2145
+ const critic = this.workerFiles(task, taskWorkerId("critic", engine, channel.turnId, featureScoped ? channel.id : undefined, execution.modelKey));
2086
2146
  const status = await readWorkerStatusIfValid(critic.statusPath);
2087
2147
  if (status?.state !== "done"
2088
2148
  || status.role !== "critic"
2089
2149
  || status.engine !== engine
2150
+ || (execution.modelKey && status.model_name !== execution.modelConfig.name)
2090
2151
  || (featureScoped ? status.feature_id !== channel.id : Boolean(status.feature_id))) {
2091
2152
  return null;
2092
2153
  }
@@ -2096,6 +2157,17 @@ export class Orchestrator {
2096
2157
  }
2097
2158
  return await featureCriticCheckpointIsReusable(channel, decision) ? critic : null;
2098
2159
  }
2160
+ async featureWorkerExecution(turn, role, engine, assignedModel) {
2161
+ const inherited = await this.roleConfiguration.modelForTurn(turn.dir, role, engine);
2162
+ const requested = assignedModel.trim();
2163
+ const modelConfig = requested
2164
+ ? this.roleConfiguration.modelForTarget({ engine, model: requested })
2165
+ : inherited;
2166
+ return {
2167
+ modelConfig,
2168
+ modelKey: requested && requested !== inherited.name ? requested : ""
2169
+ };
2170
+ }
2099
2171
  async promptTurnContext(task, turn) {
2100
2172
  return {
2101
2173
  turnId: turn.turnId,
@@ -2479,15 +2551,22 @@ function workerLabelForStatus(status) {
2479
2551
  return workerLabel(status.role, status.engine);
2480
2552
  }
2481
2553
  const base = `${status.role}-${status.engine}`;
2482
- const turnId = status.worker_id.match(new RegExp(`^${base}-(\\d{4,})$`))?.[1];
2554
+ const turnId = status.worker_id.match(new RegExp(`^${base}-(\\d{4,})(?:-model-[a-f0-9]{8})?$`))?.[1];
2483
2555
  return taskWorkerLabel(status.role, status.engine, turnId ?? "0001");
2484
2556
  }
2485
- function taskWorkerId(role, engine, turnId, featureId) {
2557
+ function taskWorkerId(role, engine, turnId, featureId, modelKey = "") {
2486
2558
  const base = `${role}-${engine}`;
2487
- if (featureId) {
2488
- return `${base}-${featureId}`;
2489
- }
2490
- return turnId === "0001" ? base : `${base}-${turnId}`;
2559
+ const scoped = featureId
2560
+ ? `${base}-${featureId}`
2561
+ : turnId === "0001" ? base : `${base}-${turnId}`;
2562
+ return modelKey ? `${scoped}-model-${workerModelKey(modelKey)}` : scoped;
2563
+ }
2564
+ function workerModelKey(model) {
2565
+ return createHash("sha256").update(model).digest("hex").slice(0, 8);
2566
+ }
2567
+ function selectedRoleModel(selection, role, engine) {
2568
+ const target = selection?.[role];
2569
+ return target?.engine === engine ? target.model : "";
2491
2570
  }
2492
2571
  function capitalize(value) {
2493
2572
  return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
@@ -2503,7 +2582,7 @@ function workerStageOrder(worker) {
2503
2582
  function workerTurnOrder(worker) {
2504
2583
  const featureTurn = worker.featureId?.match(/^(\d{4,})(?:-|$)/)?.[1];
2505
2584
  const waveTurn = worker.id.match(/-wave-(\d{4,})-/)?.[1];
2506
- const taskTurn = worker.id.match(/-(\d{4,})$/)?.[1];
2585
+ const taskTurn = worker.id.match(/-(\d{4,})(?:-model-[a-f0-9]{8})?$/)?.[1];
2507
2586
  const parsed = Number(featureTurn ?? waveTurn ?? taskTurn ?? "1");
2508
2587
  return Number.isInteger(parsed) && parsed > 0 ? parsed : 1;
2509
2588
  }