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.
- package/README.md +19 -11
- package/dist/cli.js +3 -1
- package/dist/core/collaboration-timeline.js +3 -1
- package/dist/core/role-configuration.js +47 -8
- package/dist/core/session-index.js +123 -10
- package/dist/core/session-manager.js +15 -4
- package/dist/core/task-report.js +510 -0
- package/dist/core/task-search.js +142 -0
- package/dist/domain/schemas.js +9 -0
- package/dist/orchestrator/collaboration-channel.js +11 -3
- package/dist/orchestrator/orchestrator.js +117 -38
- package/dist/tui/App.js +125 -14
- package/dist/tui/FeatureBoardView.js +4 -1
- package/dist/tui/InputBar.js +22 -7
- package/dist/tui/StatusDetailView.js +27 -6
- package/dist/tui/TaskSessionsView.js +30 -9
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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) =>
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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,})
|
|
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
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
return
|
|
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,})
|
|
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
|
}
|