parallel-codex-tui 0.3.0 → 0.3.1
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 +12 -10
- package/dist/core/collaboration-timeline.js +3 -1
- package/dist/core/role-configuration.js +47 -8
- 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 +63 -9
- package/dist/tui/FeatureBoardView.js +4 -1
- package/dist/tui/InputBar.js +11 -4
- package/dist/tui/StatusDetailView.js +27 -6
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@ Built with Codex-assisted development.
|
|
|
6
6
|
|
|
7
7
|
## Current Release
|
|
8
8
|
|
|
9
|
-
`v0.3.
|
|
9
|
+
`v0.3.1` is available from [npm](https://www.npmjs.com/package/parallel-codex-tui/v/0.3.1) and as a [GitHub Release](https://github.com/allendred/parallel-codex-tui/releases/tag/v0.3.1). `Ctrl+E` opens runtime role/model control for Main, Judge, Actor, and Critic. The complete matrix can apply once to the next request, persist for retries and follow-ups in the current Task, or become the default for future requests. Saving now validates every selected Provider before it writes configuration, and concurrent TUI instances atomically consume a one-shot matrix exactly once.
|
|
10
|
+
|
|
11
|
+
Feature assignment now controls both Provider and model. From a retryable Feature, press `M`, use `A`/`C` to cycle the Actor/Critic Provider, and use `1`/`2` to edit the Actor/Critic model. A model change starts a distinct Worker identity, keeps all earlier logs and native sessions, and remains an explicit Feature override when Task defaults change. Status details show separate `actual`, `next`, and `future` role matrices alongside the selected historical Worker's persisted model.
|
|
10
12
|
|
|
11
13
|
`Ctrl+T`, then `C`, opens every persisted Main conversation and provides the same durable management lifecycle as Task sessions: rename, archive, unarchive, scoped export, and confirmed deletion. Restoring one switches the exact `conversation_id`, reloads only that conversation's file-backed history, and restores its archived native Codex or Claude session ids when they are available. `Ctrl+N` creates an explicit, durable Main conversation boundary without deleting chat, Tasks, or Worker logs.
|
|
12
14
|
|
|
@@ -14,7 +16,7 @@ A real Claude acceptance retires the first native session, inserts 16 newer reco
|
|
|
14
16
|
|
|
15
17
|
Complex task memory now retains the root Turn plus the latest 19 previous Turn summaries within a 12,000-character budget. When a longer task omits intermediate summaries inline, the prompt points the Worker to the complete immutable Turn history on disk. Main chat still extracts only the final Codex answer while retaining the complete CLI transcript in `output.log`; legacy transcript-shaped chat records are cleaned when displayed without rewriting their file-backed evidence. Workspace integration ignores known host metadata such as `.DS_Store` and AppleDouble files while continuing to reject real concurrent project edits.
|
|
16
18
|
|
|
17
|
-
The default role map is Main/Judge/Actor on Codex and Critic on Claude. Status details show the
|
|
19
|
+
The default role map is Main/Judge/Actor on Codex and Critic on Claude. Status details show the actual, next, and future role maps separately from each historical Worker's persisted Provider/model. Config monitoring labels Router-only edits as active on the next request and marks role, Worker, model, permission, or UI changes as requiring restart. Claude text/JSON print runs are recognized as buffered work, use the total deadline instead of a false first-output deadline, and run safe isolated tools through non-interactive `auto` permissions. The `v0.2.4` non-interactive Codex permission placement remains included. The release keeps terminal scrolling and copying available at the same time without requiring Shift and preserves the embedded native Agent scrollback across status-detail round trips and real PTY resizes.
|
|
18
20
|
|
|
19
21
|
Highlights:
|
|
20
22
|
|
|
@@ -27,7 +29,7 @@ Highlights:
|
|
|
27
29
|
- Named Worker Providers support Codex-compatible, Claude-compatible, OpenAI-compatible, Anthropic-compatible, and custom generic commands with independent role, model, environment, permission, resume, and interactive settings.
|
|
28
30
|
- Worker overview, Feature board, collaboration timeline, Task and Main conversation centers, status details, rendered Markdown/Diff/error logs, Unicode search, keyboard navigation, mouse scrolling, and configurable themes share one terminal UI system.
|
|
29
31
|
|
|
30
|
-
Release acceptance includes a real three-Feature Tetris task with parallel Actor/Critic waves and final integration review. A clean `v0.2.5` task also ran Codex Judge and Actor, a buffered Claude Critic that independently executed `node --test` and `npm test`, atomic integration, and a resumed Codex Judge that independently passed all seven acceptance criteria. Real Codex and Claude probes both proved fresh and same-session resume calls; Codex fresh and resume runs executed workspace writes with root-level `-a never`, and Claude automated sessions executed safe Bash tools with `auto` permissions. The semantic Router completed a live classification, and one TUI completed Main calls in two workspaces before restoring the first workspace without leaking chat state. PTY coverage runs in Apple Terminal, tmux, and Zellij profiles at narrow and wide sizes, including status/log equivalence, preserving the native output tail across status-detail round trips, and proving both inline and on-demand file memory after native-session rollover. The deterministic repository suite contains 1,
|
|
32
|
+
Release acceptance includes a real three-Feature Tetris task with parallel Actor/Critic waves and final integration review. A clean `v0.2.5` task also ran Codex Judge and Actor, a buffered Claude Critic that independently executed `node --test` and `npm test`, atomic integration, and a resumed Codex Judge that independently passed all seven acceptance criteria. Real Codex and Claude probes both proved fresh and same-session resume calls; Codex fresh and resume runs executed workspace writes with root-level `-a never`, and Claude automated sessions executed safe Bash tools with `auto` permissions. The semantic Router completed a live classification, and one TUI completed Main calls in two workspaces before restoring the first workspace without leaking chat state. PTY coverage runs in Apple Terminal, tmux, and Zellij profiles at narrow and wide sizes, including status/log equivalence, preserving the native output tail across status-detail round trips, and proving both inline and on-demand file memory after native-session rollover. The deterministic repository suite contains 1,338 tests across 136 files: 1,337 pass by default, while one quota-consuming real-Agent test is skipped and passes through `npm run test:real-agents`.
|
|
31
33
|
|
|
32
34
|
Real Provider probes depend on valid local CLI credentials. In particular, authenticate the Claude CLI before selecting a Claude-compatible Worker, then run `parallel-codex-tui --doctor --probe-agents` to prove fresh and resumed calls on that machine.
|
|
33
35
|
|
|
@@ -420,15 +422,15 @@ critic = "anthropic_compat"
|
|
|
420
422
|
|
|
421
423
|
## Runtime Role Control
|
|
422
424
|
|
|
423
|
-
While the TUI is idle, press `Ctrl+E` to configure Main, Judge, Actor, and Critic without editing TOML or restarting. Up/Down selects a role, Left/Right (or `[`/`]`) cycles its configured Worker Provider, `M` edits the model name with a Unicode-safe cursor, and `Tab` changes the persistence scope. Press Enter to
|
|
425
|
+
While the TUI is idle, press `Ctrl+E` to configure Main, Judge, Actor, and Critic without editing TOML or restarting. Up/Down selects a role, Left/Right (or `[`/`]`) cycles its configured Worker Provider, `M` edits the model name with a Unicode-safe cursor, and `Tab` changes the persistence scope. Press Enter to validate every selected CLI, environment, capability contract, and proxy before saving the complete matrix. `X` clears the saved override and inherits its parent scope, `R` discards unsaved edits, and `Esc` or `Ctrl+E` returns. `Ctrl+Y` copies the visible matrix. A Provider switch resets that role to the Provider's configured default model; a blank model edit also means the Provider default.
|
|
424
426
|
|
|
425
427
|
The three scopes are explicit:
|
|
426
428
|
|
|
427
|
-
- `next request` is stored in the current workspace at `.parallel-codex/role-configuration.next.json`, overrides every role once, and is removed only when
|
|
428
|
-
- `current task` is stored beside that Task's `meta.json`, applies to retries and same-Task follow-ups, and updates unfinished Feature assignments without deleting earlier Workers or logs.
|
|
429
|
+
- `next request` is stored in the current workspace at `.parallel-codex/role-configuration.next.json`, overrides every role once, and is removed only when one routed request atomically claims it. Concurrent TUI instances cannot both consume it.
|
|
430
|
+
- `current task` is stored beside that Task's `meta.json`, applies to retries and same-Task follow-ups, and updates inherited unfinished Feature assignments without deleting earlier Workers or logs. Explicit Feature overrides remain unchanged.
|
|
429
431
|
- `future requests` is stored in the app root at `.parallel-codex/role-configuration.json` and becomes the default for new Main calls and Tasks across restarts.
|
|
430
432
|
|
|
431
|
-
Every complex Turn writes the exact consumed matrix to `turns/<turn>/role-configuration.json`. Retries use that evidence instead of silently adopting a pending one-shot selection, native attach uses the selected Worker's persisted Provider/model, and status details show the
|
|
433
|
+
Every complex Turn writes the exact consumed matrix to `turns/<turn>/role-configuration.json`. Retries use that evidence instead of silently adopting a pending one-shot selection, native attach uses the selected Worker's persisted Provider/model, and status details show the actual Turn, next-request, and future-default matrices separately from historical Worker identity.
|
|
432
434
|
|
|
433
435
|
For a third-party command with its own syntax, inherit `generic` so parallel-codex-tui does not inject `--sandbox`, `--permission-mode`, or other built-in-only flags:
|
|
434
436
|
|
|
@@ -477,7 +479,7 @@ In chat, scroll long conversation history with the mouse wheel or PageUp/PageDow
|
|
|
477
479
|
|
|
478
480
|
From Worker overview, press `F` to open the file-backed Feature board. It summarizes every planned feature by turn, state, blocked dependencies and open Critic findings while preserving descriptions and Actor replies from the same collaboration files. Use Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` to select a feature; Enter opens the selected feature's collaboration timeline; `R` refreshes immediately; and `Esc` returns to Worker overview. When a timeline was opened from this board, `Esc` returns to the Feature board instead of skipping back to Workers. While the selected feature is running, press `P` twice to pause only its active Actor or Critic process, or press `X` twice to cancel it; the first press opens a confirmation and `Esc` keeps it running. A paused feature preserves completed peers and same-wave checkpoints; select it and press `Ctrl+R` to resume the same task, turn, and native Worker session. After cancellation, already-running peers finish, queued workers stop, and integration remains blocked, so a partial wave never reaches the live workspace. Once the task settles as cancelled, `Ctrl+R` retries the cancelled task with persisted worker sessions; completed peers are loaded from their same-wave checkpoints instead of running again. Checkpoint load, reuse, and recovery events appear in the collaboration timeline.
|
|
479
481
|
|
|
480
|
-
For a failed, cancelled, or paused task, select an unfinished Feature and press `M` to edit its
|
|
482
|
+
For a failed, cancelled, or paused task, select an unfinished Feature and press `M` to edit its assignment. `A` cycles that Feature's Actor through every `assignable` Worker Provider, `C` cycles its Critic, `1` edits the Actor model, `2` edits the Critic model, and `M` or `Esc` closes the prompt. The choice is persisted in `features/<turn>-<feature>/assignment.json`, appears on the Feature board and collaboration artifacts, and takes effect on the next `Ctrl+R` retry. A changed Provider or model starts a distinct Worker while every previous Worker log and native-session record remains available. Feature overrides survive later Task-default changes. Assignment writes hold the same task lease as execution, so another live TUI cannot change a Feature while it is resuming.
|
|
481
483
|
|
|
482
484
|
From Worker overview, press `C` to open the file-backed Actor/Critic collaboration timeline. It merges `dialogue/actor-critic.jsonl`, feature status, Critic findings, Actor replies, finding resolution, and Wave events into one chronological view. The timeline follows new file evidence automatically and shows verified `fixed`/`open` counts when resolution evidence exists. Up/Down selects a collaboration event; `Enter` opens its complete event detail, including artifact paths from dialogue, status, Critic findings, Actor replies, and finding resolution; `U` filters to unresolved feature evidence; `Tab` cycles all features and each individual feature; `R` refreshes immediately; the mouse wheel or PageUp/PageDown scrolls history; and `Esc` returns to Worker overview.
|
|
483
485
|
|
|
@@ -537,12 +539,12 @@ The release job installs npm `^11.5.1`, runs on Node `24.15.x`, publishes the pr
|
|
|
537
539
|
To publish a release, update `package.json` and `src/version.ts` to the same version, then push a matching tag:
|
|
538
540
|
|
|
539
541
|
```bash
|
|
540
|
-
VERSION=0.3.
|
|
542
|
+
VERSION=0.3.1
|
|
541
543
|
git tag "v$VERSION"
|
|
542
544
|
git push origin "v$VERSION"
|
|
543
545
|
```
|
|
544
546
|
|
|
545
|
-
You can also run the Release workflow manually and enter the same tag value. The release tag must match `package.json`; for example, package version `0.3.
|
|
547
|
+
You can also run the Release workflow manually and enter the same tag value. The release tag must match `package.json`; for example, package version `0.3.1` requires tag `v0.3.1`. Published tags such as `v0.2.10` are immutable and must not be moved or reused.
|
|
546
548
|
|
|
547
549
|
## Publishing Hygiene
|
|
548
550
|
|
|
@@ -133,7 +133,9 @@ async function readCollaborationFeatures(taskDir) {
|
|
|
133
133
|
updatedAt: status.updated_at,
|
|
134
134
|
...(assignment ? {
|
|
135
135
|
actorEngine: assignment.actor_engine,
|
|
136
|
-
criticEngine: assignment.critic_engine
|
|
136
|
+
criticEngine: assignment.critic_engine,
|
|
137
|
+
actorModel: assignment.actor_model,
|
|
138
|
+
criticModel: assignment.critic_model
|
|
137
139
|
} : {}),
|
|
138
140
|
findings: findingEvidence.count,
|
|
139
141
|
replies: replyEvidence.count,
|
|
@@ -2,11 +2,12 @@ import { readdir } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { pathExists, readJson, removeIfExists, writeJson } from "./file-store.js";
|
|
5
|
-
import {
|
|
5
|
+
import { claimTaskRunLease, TaskRunLeaseConflictError } from "./process-ownership.js";
|
|
6
|
+
import { EngineNameSchema, WorkerModelNameSchema } from "../domain/schemas.js";
|
|
6
7
|
export const CONFIGURABLE_ROLES = ["main", "judge", "actor", "critic"];
|
|
7
8
|
const RoleExecutionTargetSchema = z.object({
|
|
8
9
|
engine: EngineNameSchema,
|
|
9
|
-
model:
|
|
10
|
+
model: WorkerModelNameSchema
|
|
10
11
|
}).strict();
|
|
11
12
|
const RoleExecutionSelectionSchema = z.object({
|
|
12
13
|
main: RoleExecutionTargetSchema,
|
|
@@ -67,7 +68,7 @@ export class RoleConfigurationManager {
|
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
70
|
if (scope === "next") {
|
|
70
|
-
await writeRoleConfiguration(this.nextPath(), parsed);
|
|
71
|
+
await this.withNextConfigurationLease(() => writeRoleConfiguration(this.nextPath(), parsed));
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
73
74
|
if (!taskDir) {
|
|
@@ -83,7 +84,7 @@ export class RoleConfigurationManager {
|
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
if (scope === "next") {
|
|
86
|
-
await removeIfExists(this.nextPath());
|
|
87
|
+
await this.withNextConfigurationLease(() => removeIfExists(this.nextPath()));
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
if (!taskDir) {
|
|
@@ -93,12 +94,14 @@ export class RoleConfigurationManager {
|
|
|
93
94
|
}
|
|
94
95
|
async selectionForRequest(taskDir) {
|
|
95
96
|
const taskSelection = await this.selectionForTask(taskDir);
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
return this.withNextConfigurationLease(async () => {
|
|
98
|
+
const next = await readRoleConfigurationIfValid(this.nextPath(), this.config);
|
|
99
|
+
if (!next) {
|
|
100
|
+
return taskSelection;
|
|
101
|
+
}
|
|
98
102
|
await removeIfExists(this.nextPath());
|
|
99
103
|
return next;
|
|
100
|
-
}
|
|
101
|
-
return taskSelection;
|
|
104
|
+
});
|
|
102
105
|
}
|
|
103
106
|
async selectionForTask(taskDir) {
|
|
104
107
|
const taskSelection = taskDir
|
|
@@ -126,12 +129,45 @@ export class RoleConfigurationManager {
|
|
|
126
129
|
baselineRoles() {
|
|
127
130
|
return cloneRoleSelection(this.baselineSelection);
|
|
128
131
|
}
|
|
132
|
+
validate(roles) {
|
|
133
|
+
return parseRoleSelection(roles, this.config);
|
|
134
|
+
}
|
|
135
|
+
workspaceRootPath() {
|
|
136
|
+
return this.workspaceRoot;
|
|
137
|
+
}
|
|
129
138
|
futurePath() {
|
|
130
139
|
return roleFutureConfigurationPath(this.appRoot, this.config.dataDir);
|
|
131
140
|
}
|
|
132
141
|
nextPath() {
|
|
133
142
|
return roleNextConfigurationPath(this.workspaceRoot, this.config.dataDir);
|
|
134
143
|
}
|
|
144
|
+
nextLeaseDir() {
|
|
145
|
+
return join(this.workspaceRoot, this.config.dataDir, ".role-configuration-next-lock");
|
|
146
|
+
}
|
|
147
|
+
async withNextConfigurationLease(run) {
|
|
148
|
+
let lease = null;
|
|
149
|
+
for (let attempt = 0; attempt < 80; attempt += 1) {
|
|
150
|
+
try {
|
|
151
|
+
lease = await claimTaskRunLease(this.nextLeaseDir());
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (!(error instanceof TaskRunLeaseConflictError) || attempt === 79) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
await delay(25);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!lease) {
|
|
162
|
+
throw new Error("Timed out waiting to update the next-request role configuration.");
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
return await run();
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await lease.release();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
135
171
|
async readLatestTurnSelection(taskDir) {
|
|
136
172
|
const turnsDir = join(taskDir, "turns");
|
|
137
173
|
if (!(await pathExists(turnsDir))) {
|
|
@@ -236,3 +272,6 @@ function applyRoleEngines(config, roles) {
|
|
|
236
272
|
config.pairing[role] = roles[role].engine;
|
|
237
273
|
}
|
|
238
274
|
}
|
|
275
|
+
function delay(milliseconds) {
|
|
276
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
277
|
+
}
|
package/dist/domain/schemas.js
CHANGED
|
@@ -170,10 +170,19 @@ export const FeatureStatusSchema = z.object({
|
|
|
170
170
|
state: FeatureStateSchema,
|
|
171
171
|
updated_at: z.string().datetime()
|
|
172
172
|
});
|
|
173
|
+
export const WorkerModelNameSchema = z.string()
|
|
174
|
+
.max(200)
|
|
175
|
+
.refine((value) => !/[\u0000-\u001f\u007f]/.test(value), {
|
|
176
|
+
message: "Model name cannot contain control characters"
|
|
177
|
+
});
|
|
173
178
|
export const FeatureAssignmentSchema = z.object({
|
|
174
179
|
version: z.literal(1),
|
|
175
180
|
actor_engine: EngineNameSchema,
|
|
176
181
|
critic_engine: EngineNameSchema,
|
|
182
|
+
actor_model: WorkerModelNameSchema.default(""),
|
|
183
|
+
critic_model: WorkerModelNameSchema.default(""),
|
|
184
|
+
actor_override: z.boolean().default(false),
|
|
185
|
+
critic_override: z.boolean().default(false),
|
|
177
186
|
updated_at: z.string().datetime()
|
|
178
187
|
});
|
|
179
188
|
export const TurnMetaSchema = z.object({
|
|
@@ -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
|
}
|
package/dist/tui/App.js
CHANGED
|
@@ -264,7 +264,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
264
264
|
&& (config.workers[selectedTaskSessionDetailWorker.engine]?.interactive.forkArgs.length ?? 0) > 0);
|
|
265
265
|
const featureCanCancel = busy && (selectedBoardFeature?.state === "actor_running"
|
|
266
266
|
|| selectedBoardFeature?.state === "critic_running");
|
|
267
|
-
const featureCanReassign = assignableProviderIds.length >
|
|
267
|
+
const featureCanReassign = assignableProviderIds.length > 0
|
|
268
268
|
&& !busy
|
|
269
269
|
&& canRetryTask
|
|
270
270
|
&& selectedBoardFeature?.state !== "approved";
|
|
@@ -1396,6 +1396,24 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1396
1396
|
const pendingAssignment = featureAssignmentPromptRef.current;
|
|
1397
1397
|
if (pendingAssignment) {
|
|
1398
1398
|
for (const featureChunk of featureChunks) {
|
|
1399
|
+
const modelEdit = featureAssignmentPromptRef.current?.modelEdit;
|
|
1400
|
+
if (modelEdit) {
|
|
1401
|
+
if (featureChunk === "\x1b") {
|
|
1402
|
+
updateFeatureAssignmentPrompt({ ...pendingAssignment, modelEdit: undefined });
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const update = applyChatInputChunk(modelEdit.value, featureChunk, modelEdit.cursor);
|
|
1406
|
+
if (update.submit !== null) {
|
|
1407
|
+
void reassignSelectedFeatureRef.current(pendingAssignment, modelEdit.role, update.submit);
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
updateFeatureAssignmentPrompt({
|
|
1411
|
+
...pendingAssignment,
|
|
1412
|
+
modelEdit: { ...modelEdit, value: update.value, cursor: update.cursor }
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1399
1417
|
if (featureChunk === "\x1b" || featureChunk === "m" || featureChunk === "M") {
|
|
1400
1418
|
updateFeatureAssignmentPrompt(null);
|
|
1401
1419
|
setFeatureBoardNotice(null);
|
|
@@ -1409,6 +1427,16 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1409
1427
|
void reassignSelectedFeatureRef.current(pendingAssignment, "critic");
|
|
1410
1428
|
return;
|
|
1411
1429
|
}
|
|
1430
|
+
if (featureChunk === "1" || featureChunk === "2") {
|
|
1431
|
+
const feature = collaborationTimelineRef.current?.features.find((item) => item.id === pendingAssignment.featureId);
|
|
1432
|
+
const role = featureChunk === "1" ? "actor" : "critic";
|
|
1433
|
+
const value = role === "actor" ? feature?.actorModel ?? "" : feature?.criticModel ?? "";
|
|
1434
|
+
updateFeatureAssignmentPrompt({
|
|
1435
|
+
...pendingAssignment,
|
|
1436
|
+
modelEdit: { role, value, cursor: Array.from(value).length }
|
|
1437
|
+
});
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1412
1440
|
}
|
|
1413
1441
|
return;
|
|
1414
1442
|
}
|
|
@@ -1468,7 +1496,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1468
1496
|
featureId: feature.id,
|
|
1469
1497
|
title: feature.title
|
|
1470
1498
|
});
|
|
1471
|
-
setFeatureBoardNotice(`Reassign ${feature.title} · A Actor (${feature.actorEngine ?? config.pairing.actor}) · C Critic (${feature.criticEngine ?? config.pairing.critic})`);
|
|
1499
|
+
setFeatureBoardNotice(`Reassign ${feature.title} · A Actor (${featureTargetDisplay(config, feature.actorEngine ?? config.pairing.actor, feature.actorModel)}) · C Critic (${featureTargetDisplay(config, feature.criticEngine ?? config.pairing.critic, feature.criticModel)})`);
|
|
1472
1500
|
return;
|
|
1473
1501
|
}
|
|
1474
1502
|
if (featureChunk === "r" || featureChunk === "R") {
|
|
@@ -3087,7 +3115,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
3087
3115
|
featureAssignmentPromptRef.current = next;
|
|
3088
3116
|
setFeatureAssignmentPrompt(next);
|
|
3089
3117
|
}
|
|
3090
|
-
async function reassignSelectedFeature(prompt, role) {
|
|
3118
|
+
async function reassignSelectedFeature(prompt, role, model) {
|
|
3091
3119
|
const feature = collaborationTimelineRef.current?.features.find((item) => item.id === prompt.featureId);
|
|
3092
3120
|
if (!feature) {
|
|
3093
3121
|
setFeatureBoardNotice(`Feature no longer exists: ${prompt.featureId}`);
|
|
@@ -3097,15 +3125,22 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
3097
3125
|
const current = role === "actor"
|
|
3098
3126
|
? feature.actorEngine ?? config.pairing.actor
|
|
3099
3127
|
: feature.criticEngine ?? config.pairing.critic;
|
|
3100
|
-
const engine =
|
|
3128
|
+
const engine = model === undefined
|
|
3129
|
+
? nextAssignableFeatureEngine(current, assignableProviderIds)
|
|
3130
|
+
: current;
|
|
3101
3131
|
try {
|
|
3102
|
-
await orchestrator.reassignFeature({
|
|
3132
|
+
const result = await orchestrator.reassignFeature({
|
|
3103
3133
|
taskId: prompt.taskId,
|
|
3104
3134
|
featureId: prompt.featureId,
|
|
3105
3135
|
role,
|
|
3106
|
-
engine
|
|
3136
|
+
engine,
|
|
3137
|
+
...(model === undefined ? {} : { model })
|
|
3107
3138
|
});
|
|
3108
|
-
|
|
3139
|
+
updateFeatureAssignmentPrompt({ ...prompt, modelEdit: undefined });
|
|
3140
|
+
const assignedModel = role === "actor"
|
|
3141
|
+
? result.assignment.actor_model
|
|
3142
|
+
: result.assignment.critic_model;
|
|
3143
|
+
setFeatureBoardNotice(`${role === "actor" ? "Actor" : "Critic"} reassigned to ${featureTargetDisplay(config, engine, assignedModel)} · Ctrl+R resumes the task.`);
|
|
3109
3144
|
await refreshCollaborationTimeline(false);
|
|
3110
3145
|
}
|
|
3111
3146
|
catch (error) {
|
|
@@ -3383,6 +3418,12 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
3383
3418
|
if (typeof orchestrator.updateRoleConfiguration !== "function") {
|
|
3384
3419
|
throw new Error("Role configuration is unavailable in this runtime");
|
|
3385
3420
|
}
|
|
3421
|
+
if (typeof orchestrator.validateRoleConfiguration === "function") {
|
|
3422
|
+
const validation = await orchestrator.validateRoleConfiguration(draft);
|
|
3423
|
+
if (!validation.ok) {
|
|
3424
|
+
throw new Error(formatRoleConfigurationPreflightFailure(validation.lines));
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3386
3427
|
const snapshot = await orchestrator.updateRoleConfiguration({
|
|
3387
3428
|
scope,
|
|
3388
3429
|
roles: draft,
|
|
@@ -3478,7 +3519,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
3478
3519
|
if (view === "workspace") {
|
|
3479
3520
|
return (_jsx(Box, { flexDirection: "column", height: terminalSize.rows, children: _jsx(WorkspacePicker, { cwd: cwd, choices: workspaceChoices, terminalHeight: terminalSize.rows, terminalWidth: terminalWidth, onCancel: closeWorkspacePicker, onSelect: (workspace) => void selectWorkspace(workspace) }) }));
|
|
3480
3521
|
}
|
|
3481
|
-
return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: [visibleTaskStatus, visibleRouteSummary, configChange?.compact].filter(Boolean).join(" | "), contentHeight: contentHeight, showStatusBar: config.ui.showStatusBar, input: _jsx(InputBar, { mode: view === "worker" && workerSearch.open ? "worker-search" : view, ready: inputReady, busy: busy, routeFallback: Boolean(routeFallbackPrompt), collaborationDetail: collaborationDetailOpen, collaborationUnresolved: collaborationUnresolvedOnly, collaborationBack: collaborationReturnViewRef.current, featureCanCancel: featureCanCancel, featureCanPause: featureCanCancel, featureCanReassign: featureCanReassign, featureCancelConfirm: featureCancelPrompt?.action === "cancel", featurePauseConfirm: featureCancelPrompt?.action === "pause", featureAssignment: Boolean(featureAssignmentPrompt), taskSessionAction: sessionCenterMode === "conversations"
|
|
3522
|
+
return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: [visibleTaskStatus, visibleRouteSummary, configChange?.compact].filter(Boolean).join(" | "), contentHeight: contentHeight, showStatusBar: config.ui.showStatusBar, input: _jsx(InputBar, { mode: view === "worker" && workerSearch.open ? "worker-search" : view, ready: inputReady, busy: busy, routeFallback: Boolean(routeFallbackPrompt), collaborationDetail: collaborationDetailOpen, collaborationUnresolved: collaborationUnresolvedOnly, collaborationBack: collaborationReturnViewRef.current, featureCanCancel: featureCanCancel, featureCanPause: featureCanCancel, featureCanReassign: featureCanReassign, featureCancelConfirm: featureCancelPrompt?.action === "cancel", featurePauseConfirm: featureCancelPrompt?.action === "pause", featureAssignment: Boolean(featureAssignmentPrompt), featureEditingModel: featureAssignmentPrompt?.modelEdit ?? null, taskSessionAction: sessionCenterMode === "conversations"
|
|
3482
3523
|
? mainConversationAction?.type === "rename"
|
|
3483
3524
|
? { type: "rename", value: mainConversationAction.value, cursor: mainConversationAction.cursor }
|
|
3484
3525
|
: mainConversationAction?.type === "delete"
|
|
@@ -3494,7 +3535,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
3494
3535
|
? workerSearch.query
|
|
3495
3536
|
: view === "native" || view === "router" || view === "workers" || view === "features" || view === "sessions" || view === "collaboration" || view === "status" || view === "roles" ? "" : input, cursor: workerSearch.open && view === "worker"
|
|
3496
3537
|
? workerSearch.cursor
|
|
3497
|
-
: view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "roles" ? (_jsx(RoleConfigurationView, { snapshot: roleConfigurationSnapshot, draft: roleConfigurationDraft, scope: roleConfigurationScope, selectedRoleIndex: roleConfigurationSelectedIndex, loading: roleConfigurationLoading, saving: roleConfigurationSaving, notice: roleConfigurationNotice, error: roleConfigurationError, hasActiveTask: Boolean(activeTaskId), height: contentHeight, terminalWidth: terminalWidth })) : view === "status" ? (_jsx(StatusDetailView, { cwd: cwd, taskId: activeTaskId, mode: activeMode, busy: busy, canRetry: canRetryTask, taskStatus: visibleTaskStatus, routeStatus: visibleRouteStatus, routeReason: routePending ? undefined : lastRoute?.reason, pairing: config.pairing, roleSelection: activeRoleSelection, configStatus: configChange?.detail, configRestartRequired: configChange?.kind === "restart", workers: workers, selectedWorkerIndex: selectedWorkerIndex, height: contentHeight, terminalWidth: terminalWidth })) : view === "native" ? (_jsx(NativeAttachView, { attach: nativeAttach, viewportHeight: contentHeight })) : view === "sessions" ? (taskSessionDetails ? (_jsx(TaskSessionDetailView, { details: taskSessionDetails, selectedWorkerIndex: taskSessionDetailSelectedWorkerIndex, loading: taskSessionsLoading, error: taskSessionsError, notice: taskSessionDetailNotice, height: contentHeight, terminalWidth: terminalWidth })) : sessionCenterMode === "conversations" ? (_jsx(MainConversationsView, { conversations: mainConversations, selectedIndex: selectedMainConversationIndex, includeArchived: mainConversationsIncludeArchived, notice: taskSessionsNotice, action: mainConversationAction?.type === "rename"
|
|
3538
|
+
: view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "roles" ? (_jsx(RoleConfigurationView, { snapshot: roleConfigurationSnapshot, draft: roleConfigurationDraft, scope: roleConfigurationScope, selectedRoleIndex: roleConfigurationSelectedIndex, loading: roleConfigurationLoading, saving: roleConfigurationSaving, notice: roleConfigurationNotice, error: roleConfigurationError, hasActiveTask: Boolean(activeTaskId), height: contentHeight, terminalWidth: terminalWidth })) : view === "status" ? (_jsx(StatusDetailView, { cwd: cwd, taskId: activeTaskId, mode: activeMode, busy: busy, canRetry: canRetryTask, taskStatus: visibleTaskStatus, routeStatus: visibleRouteStatus, routeReason: routePending ? undefined : lastRoute?.reason, pairing: config.pairing, roleSelection: activeRoleSelection, roleConfigurationSnapshot: roleConfigurationSnapshot, configStatus: configChange?.detail, configRestartRequired: configChange?.kind === "restart", workers: workers, selectedWorkerIndex: selectedWorkerIndex, height: contentHeight, terminalWidth: terminalWidth })) : view === "native" ? (_jsx(NativeAttachView, { attach: nativeAttach, viewportHeight: contentHeight })) : view === "sessions" ? (taskSessionDetails ? (_jsx(TaskSessionDetailView, { details: taskSessionDetails, selectedWorkerIndex: taskSessionDetailSelectedWorkerIndex, loading: taskSessionsLoading, error: taskSessionsError, notice: taskSessionDetailNotice, height: contentHeight, terminalWidth: terminalWidth })) : sessionCenterMode === "conversations" ? (_jsx(MainConversationsView, { conversations: mainConversations, selectedIndex: selectedMainConversationIndex, includeArchived: mainConversationsIncludeArchived, notice: taskSessionsNotice, action: mainConversationAction?.type === "rename"
|
|
3498
3539
|
? { type: "rename", title: mainConversationAction.title }
|
|
3499
3540
|
: mainConversationAction?.type === "delete"
|
|
3500
3541
|
? { type: "delete", title: mainConversationAction.title }
|
|
@@ -4356,6 +4397,10 @@ function sessionDetailWorkerLabel(worker) {
|
|
|
4356
4397
|
function featureEngineDisplay(config, engine) {
|
|
4357
4398
|
return workerProviderLabel(config, engine);
|
|
4358
4399
|
}
|
|
4400
|
+
function featureTargetDisplay(config, engine, model) {
|
|
4401
|
+
const effectiveModel = model?.trim() || config.workers[engine]?.model.name || "default";
|
|
4402
|
+
return `${featureEngineDisplay(config, engine)}/${effectiveModel}`;
|
|
4403
|
+
}
|
|
4359
4404
|
function compactChatText(text, maxLength) {
|
|
4360
4405
|
if (maxLength <= 5) {
|
|
4361
4406
|
return takeChatTextStartByDisplayWidth(text, maxLength);
|
|
@@ -4745,6 +4790,15 @@ function compactNativeAttachRole(label) {
|
|
|
4745
4790
|
function compactNativeSessionForTitle(sessionId, maxLength) {
|
|
4746
4791
|
return compactEndByDisplayWidth(sessionId, Math.min(maxLength, 16));
|
|
4747
4792
|
}
|
|
4793
|
+
export function formatRoleConfigurationPreflightFailure(lines) {
|
|
4794
|
+
const clean = lines
|
|
4795
|
+
.map((line) => line.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim())
|
|
4796
|
+
.filter(Boolean);
|
|
4797
|
+
const actionable = clean.filter((line) => (/\b(?:denied|error|failed|invalid|missing|unavailable|unsupported)\b/i.test(line)
|
|
4798
|
+
|| /\bnot (?:found|supported|available)\b/i.test(line)));
|
|
4799
|
+
const detail = (actionable.length > 0 ? actionable : clean).slice(0, 4);
|
|
4800
|
+
return ["Provider preflight failed", ...detail].join(" · ");
|
|
4801
|
+
}
|
|
4748
4802
|
function isAbortError(error) {
|
|
4749
4803
|
return error instanceof Error && error.name === "AbortError";
|
|
4750
4804
|
}
|
|
@@ -142,7 +142,10 @@ function featureBoardEvidenceText(timeline, feature, width) {
|
|
|
142
142
|
? `deps ${dependencies.map((item) => safeFeatureBoardText(item.title)).join(", ")}`
|
|
143
143
|
: "independent";
|
|
144
144
|
const assignmentText = feature.actorEngine && feature.criticEngine
|
|
145
|
-
?
|
|
145
|
+
? [
|
|
146
|
+
`actor ${feature.actorEngine}/${feature.actorModel?.trim() || "default"}`,
|
|
147
|
+
`critic ${feature.criticEngine}/${feature.criticModel?.trim() || "default"}`
|
|
148
|
+
].join(" · ")
|
|
146
149
|
: "";
|
|
147
150
|
const evidence = feature.latestFinding
|
|
148
151
|
? `finding · ${safeFeatureBoardText(feature.latestFinding)}`
|
package/dist/tui/InputBar.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { compactEndByDisplayWidth, compactTailByDisplayWidth, displayWidth } from "./display-width.js";
|
|
4
4
|
import { TUI_THEME } from "./theme.js";
|
|
5
|
-
export function InputBar({ mode, ready = true, busy = false, routeFallback = false, collaborationDetail = false, collaborationUnresolved = false, collaborationBack = "workers", featureCanCancel = false, featureCanPause = false, featureCanReassign = false, featureCancelConfirm = false, featurePauseConfirm = false, featureAssignment = false, taskSessionAction = null, taskSessionsIncludeArchived = false, mainConversationSessions = false, taskSessionDetail = false, taskSessionDetailHasNative = false, taskSessionDetailCanFork = false, canRetry = false, hasWorkers = false, hasActiveTask = false, hasTaskResult = false, taskResultExpanded = false, chatScrollOffset = 0, chatMaxScrollOffset = 0, nativeClosed = false, searchMatchIndex = 0, searchMatchCount = 0, clipboardNotice = null, roleScope = "next", roleEditingModel = null, roleCanApply = true, roleSaving = false, roleHasOverride = false, value, cursor, terminalWidth: providedTerminalWidth, onChange, onSubmit }) {
|
|
5
|
+
export function InputBar({ mode, ready = true, busy = false, routeFallback = false, collaborationDetail = false, collaborationUnresolved = false, collaborationBack = "workers", featureCanCancel = false, featureCanPause = false, featureCanReassign = false, featureCancelConfirm = false, featurePauseConfirm = false, featureAssignment = false, featureEditingModel = null, taskSessionAction = null, taskSessionsIncludeArchived = false, mainConversationSessions = false, taskSessionDetail = false, taskSessionDetailHasNative = false, taskSessionDetailCanFork = false, canRetry = false, hasWorkers = false, hasActiveTask = false, hasTaskResult = false, taskResultExpanded = false, chatScrollOffset = 0, chatMaxScrollOffset = 0, nativeClosed = false, searchMatchIndex = 0, searchMatchCount = 0, clipboardNotice = null, roleScope = "next", roleEditingModel = null, roleCanApply = true, roleSaving = false, roleHasOverride = false, value, cursor, terminalWidth: providedTerminalWidth, onChange, onSubmit }) {
|
|
6
6
|
const terminalWidth = providedTerminalWidth ?? process.stdout.columns ?? 120;
|
|
7
7
|
const fillRail = providedTerminalWidth !== undefined || typeof process.stdout.columns === "number";
|
|
8
8
|
if (clipboardNotice) {
|
|
@@ -30,6 +30,13 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
|
|
|
30
30
|
const hints = roleConfigurationInputHints(terminalWidth, roleScope, roleCanApply, roleSaving, roleHasOverride);
|
|
31
31
|
return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: roleSaving ? TUI_THEME.warning : TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
|
|
32
32
|
}
|
|
33
|
+
if (mode === "features" && featureEditingModel) {
|
|
34
|
+
const prefix = `${featureEditingModel.role} model > `;
|
|
35
|
+
const valueWidth = Math.max(1, terminalWidth - displayWidth(prefix) - 3);
|
|
36
|
+
const display = chatInputDisplayParts(featureEditingModel.value, featureEditingModel.cursor, valueWidth);
|
|
37
|
+
const textWidth = displayWidth(`${prefix}${display.before}|${display.after}`);
|
|
38
|
+
return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: textWidth, fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: prefix }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: display.before }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: "|" }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: display.after })] }));
|
|
39
|
+
}
|
|
33
40
|
if (mode === "status") {
|
|
34
41
|
const hints = statusDetailInputHints(terminalWidth);
|
|
35
42
|
return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
|
|
@@ -481,9 +488,9 @@ function workerOverviewInputHints(width) {
|
|
|
481
488
|
function featureBoardInputHints(width, options) {
|
|
482
489
|
if (options.assignment) {
|
|
483
490
|
return selectInputHints(width, [
|
|
484
|
-
{ label: "assign
|
|
485
|
-
{ label: "assign", detail: " · A
|
|
486
|
-
{ label: "assign", detail: " · A
|
|
491
|
+
{ label: "assign", detail: " · A/C provider · 1/2 model · M/Esc done" },
|
|
492
|
+
{ label: "assign", detail: " · A/C provider · 1/2 model · Esc done" },
|
|
493
|
+
{ label: "assign", detail: " · A/C provider · 1/2 model" },
|
|
487
494
|
{ label: "provider", detail: " · A · C · Esc" },
|
|
488
495
|
{ label: "provider", detail: "" },
|
|
489
496
|
{ label: "M", detail: "" }
|
|
@@ -24,10 +24,10 @@ export function statusDetailDisplayLines(input, width, height) {
|
|
|
24
24
|
text: fitStatusDetailText(`workspace · ${basename(input.cwd) || input.cwd} · ${input.cwd}`, safeWidth),
|
|
25
25
|
tone: "muted"
|
|
26
26
|
},
|
|
27
|
-
{
|
|
28
|
-
text: fitStatusDetailText(
|
|
29
|
-
tone:
|
|
30
|
-
}
|
|
27
|
+
...roleMatrixDetailLines(input).map((line) => ({
|
|
28
|
+
text: fitStatusDetailText(line.text, safeWidth),
|
|
29
|
+
tone: line.tone
|
|
30
|
+
}))
|
|
31
31
|
];
|
|
32
32
|
if (input.configStatus?.trim()) {
|
|
33
33
|
lines.push({
|
|
@@ -59,15 +59,36 @@ export function statusDetailDisplayLines(input, width, height) {
|
|
|
59
59
|
}
|
|
60
60
|
return lines.slice(0, maxLines);
|
|
61
61
|
}
|
|
62
|
-
function
|
|
62
|
+
function roleMatrixDetail(label, pairing, selection) {
|
|
63
63
|
return [
|
|
64
|
-
|
|
64
|
+
label,
|
|
65
65
|
rolePairingEntry("main", selection?.main.engine ?? pairing.main, selection?.main.model),
|
|
66
66
|
rolePairingEntry("judge", selection?.judge.engine ?? pairing.judge, selection?.judge.model),
|
|
67
67
|
rolePairingEntry("actor", selection?.actor.engine ?? pairing.actor, selection?.actor.model),
|
|
68
68
|
rolePairingEntry("critic", selection?.critic.engine ?? pairing.critic, selection?.critic.model)
|
|
69
69
|
].join(" · ");
|
|
70
70
|
}
|
|
71
|
+
function roleMatrixDetailLines(input) {
|
|
72
|
+
const snapshot = input.roleConfigurationSnapshot;
|
|
73
|
+
const lines = [{
|
|
74
|
+
text: roleMatrixDetail("actual roles", input.pairing, input.roleSelection),
|
|
75
|
+
tone: "text"
|
|
76
|
+
}];
|
|
77
|
+
if (!snapshot) {
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
80
|
+
const next = snapshot.next ?? snapshot.task ?? snapshot.future;
|
|
81
|
+
const source = snapshot.next ? "one shot" : snapshot.task ? "task default" : "future default";
|
|
82
|
+
lines.push({
|
|
83
|
+
text: roleMatrixDetail(`next roles (${source})`, input.pairing, next),
|
|
84
|
+
tone: snapshot.next ? "active" : "muted"
|
|
85
|
+
});
|
|
86
|
+
lines.push({
|
|
87
|
+
text: roleMatrixDetail("future roles", input.pairing, snapshot.future),
|
|
88
|
+
tone: "muted"
|
|
89
|
+
});
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
71
92
|
function rolePairingEntry(role, engine, model) {
|
|
72
93
|
return [role, engine, model?.trim()].filter(Boolean).join("/");
|
|
73
94
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "0.3.
|
|
1
|
+
export const version = "0.3.1";
|