parallel-codex-tui 0.1.0 → 0.1.4
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/.parallel-codex/config.example.toml +90 -3
- package/README.md +269 -12
- package/dist/bootstrap.js +50 -18
- package/dist/cli-args.js +96 -14
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-recovery.js +70 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +40 -0
- package/dist/cli.js +291 -35
- package/dist/core/app-root.js +8 -0
- package/dist/core/collaboration-timeline.js +261 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +191 -23
- package/dist/core/file-store.js +130 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +10 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +473 -42
- package/dist/core/session-index.js +225 -30
- package/dist/core/session-manager.js +1182 -44
- package/dist/core/task-state-machine.js +17 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +126 -0
- package/dist/doctor.js +384 -30
- package/dist/domain/schemas.js +127 -6
- package/dist/orchestrator/collaboration-channel.js +255 -4
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +1777 -212
- package/dist/orchestrator/prompts.js +126 -2
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +911 -0
- package/dist/tui/App.js +2838 -159
- package/dist/tui/AppShell.js +188 -23
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +227 -0
- package/dist/tui/InputBar.js +514 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/TaskSessionsView.js +207 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1403 -161
- package/dist/tui/WorkerOverviewView.js +250 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +46 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +318 -11
- package/dist/tui/task-memory.js +15 -0
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +212 -0
- package/dist/workers/live-probe.js +176 -0
- package/dist/workers/mock-adapter.js +39 -6
- package/dist/workers/native-attach.js +147 -8
- package/dist/workers/native-session-detection.js +17 -0
- package/dist/workers/process-adapter.js +580 -81
- package/dist/workers/registry.js +4 -2
- package/package.json +17 -2
package/dist/doctor.js
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { constants } from "node:fs";
|
|
2
3
|
import { access } from "node:fs/promises";
|
|
4
|
+
import { createConnection } from "node:net";
|
|
3
5
|
import { delimiter, join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
6
|
+
import { prepareAppRoot } from "./core/app-root.js";
|
|
7
|
+
import { formatConfigErrorMessage } from "./core/config-errors.js";
|
|
8
|
+
import { configPath, loadConfig, withUiThemeOverride } from "./core/config.js";
|
|
9
|
+
import { ensureDir, pathExists } from "./core/file-store.js";
|
|
10
|
+
import { routerRuntimeDir } from "./core/paths.js";
|
|
11
|
+
import { diagnoseRouterFailure } from "./core/router-audit.js";
|
|
12
|
+
import { routeRequestWithCodex, routerProxyConfigured } from "./core/router.js";
|
|
13
|
+
import { prepareWorkspace } from "./core/workspace.js";
|
|
14
|
+
import { auditTuiThemeContrast, TUI_THEME_MIN_CONTRAST_RATIO } from "./tui/theme-contrast.js";
|
|
15
|
+
import { formatTuiThemePreview } from "./tui/theme-preview.js";
|
|
16
|
+
import { resolveTuiTheme } from "./tui/theme.js";
|
|
17
|
+
import { diagnoseAgentCapabilities } from "./workers/capabilities.js";
|
|
18
|
+
import { runLiveAgentProbes } from "./workers/live-probe.js";
|
|
19
|
+
export async function runDoctor(appRoot, workspaceRoot, env = process.env, options = {}) {
|
|
7
20
|
const lines = ["parallel-codex-tui doctor"];
|
|
8
21
|
let ok = true;
|
|
22
|
+
await prepareAppRoot(appRoot);
|
|
23
|
+
const preparedWorkspace = await prepareWorkspace(appRoot, workspaceRoot);
|
|
9
24
|
if (isSupportedNodeVersion(process.versions.node)) {
|
|
10
25
|
lines.push(`Node.js: ok (${process.versions.node})`);
|
|
11
26
|
}
|
|
12
27
|
else {
|
|
13
28
|
ok = false;
|
|
14
|
-
lines.push(`Node.js: unsupported (${process.versions.node}; need
|
|
15
|
-
}
|
|
16
|
-
if (await pathExists(workspaceRoot)) {
|
|
17
|
-
lines.push(`workspace: ok (${workspaceRoot})`);
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
ok = false;
|
|
21
|
-
lines.push(`workspace: missing (${workspaceRoot})`);
|
|
29
|
+
lines.push(`Node.js: unsupported (${process.versions.node}; need 24.15+)`);
|
|
22
30
|
}
|
|
31
|
+
lines.push(`workspace: ok (${preparedWorkspace})`);
|
|
23
32
|
const localConfigPath = configPath(appRoot);
|
|
24
33
|
if (!(await pathExists(localConfigPath))) {
|
|
25
34
|
ok = false;
|
|
@@ -31,19 +40,33 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
|
|
|
31
40
|
}
|
|
32
41
|
let config;
|
|
33
42
|
try {
|
|
34
|
-
|
|
43
|
+
const loadedConfig = await loadConfig(appRoot);
|
|
44
|
+
config = withUiThemeOverride(loadedConfig, options.theme ?? null);
|
|
35
45
|
lines.push(`config: ok (${localConfigPath})`);
|
|
46
|
+
lines.push(`theme: ok (${themeSummary(config.ui.theme, loadedConfig.ui.theme, options.theme ?? null)}; ${themeOverrideSummary(config.ui.colors)})`);
|
|
47
|
+
const theme = resolveTuiTheme({ theme: config.ui.theme, colors: config.ui.colors });
|
|
48
|
+
lines.push(`palette: ${themePaletteSummary(theme)}`);
|
|
49
|
+
lines.push(...formatTuiThemePreview(theme));
|
|
50
|
+
lines.push(...formatThemeContrastAudit(auditTuiThemeContrast(theme)));
|
|
36
51
|
}
|
|
37
52
|
catch (error) {
|
|
38
53
|
ok = false;
|
|
39
|
-
lines.push(`config: invalid (${localConfigPath}
|
|
54
|
+
lines.push(`config: invalid (${localConfigPath})`);
|
|
55
|
+
lines.push(...formatConfigErrorMessage(error).split("\n").map((line) => `config error: ${line}`));
|
|
40
56
|
return {
|
|
41
57
|
ok,
|
|
42
58
|
text: `${lines.join("\n")}\n`
|
|
43
59
|
};
|
|
44
60
|
}
|
|
45
|
-
|
|
61
|
+
const includeRouter = config.router.defaultMode === "auto" || options.probeRouter === true;
|
|
62
|
+
if (includeRouter) {
|
|
63
|
+
lines.push(`router retry: ${config.router.codex.maxAttempts} attempts; transient only; ${config.router.codex.retryDelayMs}ms backoff (TUI routing; live probe runs once)`);
|
|
64
|
+
lines.push(`router budget: total ${config.router.codex.timeoutMs}ms; follow-up ${config.router.codex.followUpTimeoutMs}ms; first output ${config.router.codex.firstOutputTimeoutMs}ms; idle ${config.router.codex.idleTimeoutMs}ms`);
|
|
65
|
+
}
|
|
66
|
+
const availableCommands = new Set();
|
|
67
|
+
for (const command of configuredCommands(config, includeRouter)) {
|
|
46
68
|
if (await commandExists(command, env)) {
|
|
69
|
+
availableCommands.add(command);
|
|
47
70
|
lines.push(`${command}: ok`);
|
|
48
71
|
}
|
|
49
72
|
else {
|
|
@@ -51,21 +74,238 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
|
|
|
51
74
|
lines.push(`${command}: missing`);
|
|
52
75
|
}
|
|
53
76
|
}
|
|
77
|
+
for (const check of configuredEnvironmentChecks(config, env, includeRouter)) {
|
|
78
|
+
if (check.ok) {
|
|
79
|
+
lines.push(`${check.label}: ok`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
ok = false;
|
|
83
|
+
lines.push(`${check.label}: missing env ${check.envName}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const capabilityDiagnostics = await diagnoseAgentCapabilities(config, env, {
|
|
87
|
+
includeRouter,
|
|
88
|
+
workerEngines: configuredWorkerEngines(config),
|
|
89
|
+
availableCommands,
|
|
90
|
+
...(options.capabilityRunner ? { runner: options.capabilityRunner } : {}),
|
|
91
|
+
...(options.capabilityTimeoutMs ? { timeoutMs: options.capabilityTimeoutMs } : {})
|
|
92
|
+
});
|
|
93
|
+
lines.push(...capabilityDiagnostics.lines);
|
|
94
|
+
ok = ok && capabilityDiagnostics.ok;
|
|
95
|
+
const proxyDiagnostics = await diagnoseProxyEnvironment(config, env, await detectMacSystemProxy(), canConnectProxy, { includeRouter });
|
|
96
|
+
lines.push(...proxyDiagnostics.lines);
|
|
97
|
+
ok = ok && proxyDiagnostics.ok;
|
|
98
|
+
if (options.probeAgents) {
|
|
99
|
+
if (ok) {
|
|
100
|
+
const liveAgents = await runLiveAgentProbes(config, preparedWorkspace, configuredWorkerEngines(config), options.liveAgentProbeOptions);
|
|
101
|
+
lines.push(...liveAgents.lines);
|
|
102
|
+
ok = ok && liveAgents.ok;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
lines.push("agent live probe: skipped (preflight failed)");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
lines.push("agent live probe: not run (add --probe-agents; may use model quota)");
|
|
110
|
+
}
|
|
111
|
+
if (options.probeRouter) {
|
|
112
|
+
const probe = await runRouterProbe(config, appRoot, options.routeRunner);
|
|
113
|
+
lines.push(probe.line);
|
|
114
|
+
ok = ok && probe.ok;
|
|
115
|
+
}
|
|
116
|
+
else if (config.router.defaultMode === "auto") {
|
|
117
|
+
lines.push("router live probe: not run (add --probe-router)");
|
|
118
|
+
}
|
|
54
119
|
return {
|
|
55
120
|
ok,
|
|
56
121
|
text: `${lines.join("\n")}\n`
|
|
57
122
|
};
|
|
58
123
|
}
|
|
59
|
-
function
|
|
124
|
+
export async function diagnoseProxyEnvironment(config, env, systemProxy, connect = canConnectProxy, options = {}) {
|
|
125
|
+
const contexts = [];
|
|
126
|
+
if (options.includeRouter ?? config.router.defaultMode === "auto") {
|
|
127
|
+
contexts.push({ label: "router proxy", env: config.router.codex.env, table: "router.codex.env" });
|
|
128
|
+
}
|
|
129
|
+
if (configuredWorkerEngines(config).includes("codex")) {
|
|
130
|
+
contexts.push({
|
|
131
|
+
label: "workers.codex proxy",
|
|
132
|
+
env: config.workers.codex.model.env,
|
|
133
|
+
table: "workers.codex.model.env"
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const connectionCache = new Map();
|
|
137
|
+
const lines = [];
|
|
138
|
+
let ok = true;
|
|
139
|
+
for (const context of contexts) {
|
|
140
|
+
const endpoint = configuredProxyEndpoint(context.env, env);
|
|
141
|
+
if (!endpoint) {
|
|
142
|
+
if (systemProxy) {
|
|
143
|
+
lines.push(`${context.label}: warning (macOS system proxy ${formatProxyEndpoint(systemProxy)} is not inherited; configure [${context.table}])`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
lines.push(`${context.label}: direct (no proxy configured)`);
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (endpoint === "invalid") {
|
|
151
|
+
ok = false;
|
|
152
|
+
lines.push(`${context.label}: invalid proxy URL`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const key = formatProxyEndpoint(endpoint);
|
|
156
|
+
const reachable = await cachedProxyConnection(connectionCache, key, () => connect(endpoint.host, endpoint.port));
|
|
157
|
+
if (reachable) {
|
|
158
|
+
lines.push(`${context.label}: reachable (${key}; local endpoint only)`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
ok = false;
|
|
162
|
+
lines.push(`${context.label}: unreachable (${key})`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { ok, lines };
|
|
166
|
+
}
|
|
167
|
+
async function runRouterProbe(config, appRoot, runner) {
|
|
168
|
+
const cwd = routerRuntimeDir(appRoot, config.dataDir);
|
|
169
|
+
await ensureDir(cwd);
|
|
170
|
+
const probeConfig = {
|
|
171
|
+
...config,
|
|
172
|
+
router: {
|
|
173
|
+
...config.router,
|
|
174
|
+
defaultMode: "auto"
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
const route = await routeRequestWithCodex("hello", probeConfig, runner, cwd);
|
|
179
|
+
if (route.source === "codex") {
|
|
180
|
+
const trace = formatRouterProbeTrace(route, false);
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
line: `router live probe: ok (${route.mode} in ${Math.round(route.duration_ms ?? 0)}ms${trace ? `; ${trace}` : ""})`
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const trace = formatRouterProbeTrace(route, true);
|
|
187
|
+
const diagnosis = diagnoseRouterFailure({
|
|
188
|
+
...route,
|
|
189
|
+
reason: route.reason,
|
|
190
|
+
proxy_configured: routerProxyConfigured(config.router.codex.env)
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
line: `router live probe: failed (${sanitizeDiagnosticText(route.reason)}${trace ? `; ${trace}` : ""}; diagnosis ${diagnosis.summary}; next ${diagnosis.action})`
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
line: `router live probe: failed (${sanitizeDiagnosticText(message)})`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function formatRouterProbeTrace(route, includeStage) {
|
|
206
|
+
return [
|
|
207
|
+
...(includeStage && route.router_failure_stage ? [`stage ${route.router_failure_stage}`] : []),
|
|
208
|
+
...(includeStage && route.router_timeout_kind ? [`timeout ${route.router_timeout_kind}`] : []),
|
|
209
|
+
...(typeof route.router_dispatch_ms === "number" ? [`dispatch ${Math.round(route.router_dispatch_ms)}ms`] : []),
|
|
210
|
+
...(typeof route.router_spawn_ms === "number" ? [`spawn ${Math.round(route.router_spawn_ms)}ms`] : []),
|
|
211
|
+
...formatRouterProbeFirstOutput(route),
|
|
212
|
+
...(typeof route.router_process_ms === "number" ? [`process ${Math.round(route.router_process_ms)}ms`] : []),
|
|
213
|
+
...(typeof route.router_parse_ms === "number" ? [`parse ${Math.round(route.router_parse_ms)}ms`] : []),
|
|
214
|
+
...(typeof route.duration_ms === "number" ? [`total ${Math.round(route.duration_ms)}ms`] : []),
|
|
215
|
+
...(typeof route.router_stdout_bytes === "number" ? [`stdout ${formatRouterProbeBytes(route.router_stdout_bytes)}`] : []),
|
|
216
|
+
...(typeof route.router_stderr_bytes === "number" ? [`stderr ${formatRouterProbeBytes(route.router_stderr_bytes)}`] : [])
|
|
217
|
+
].join("; ");
|
|
218
|
+
}
|
|
219
|
+
function formatRouterProbeFirstOutput(route) {
|
|
220
|
+
const streams = [
|
|
221
|
+
...(typeof route.router_first_stdout_ms === "number"
|
|
222
|
+
? [{ at: route.router_first_stdout_ms, text: `first stdout ${Math.round(route.router_first_stdout_ms)}ms` }]
|
|
223
|
+
: []),
|
|
224
|
+
...(typeof route.router_first_stderr_ms === "number"
|
|
225
|
+
? [{ at: route.router_first_stderr_ms, text: `first stderr ${Math.round(route.router_first_stderr_ms)}ms` }]
|
|
226
|
+
: [])
|
|
227
|
+
];
|
|
228
|
+
if (streams.length > 0) {
|
|
229
|
+
return streams.sort((left, right) => left.at - right.at).map((stream) => stream.text);
|
|
230
|
+
}
|
|
231
|
+
if (typeof route.router_first_output_ms === "number") {
|
|
232
|
+
return [`first output ${Math.round(route.router_first_output_ms)}ms`];
|
|
233
|
+
}
|
|
234
|
+
return route.router_failure_stage === "waiting-output" ? ["first output none"] : [];
|
|
235
|
+
}
|
|
236
|
+
function formatRouterProbeBytes(bytes) {
|
|
237
|
+
if (bytes < 1024) {
|
|
238
|
+
return `${Math.round(bytes)}B`;
|
|
239
|
+
}
|
|
240
|
+
return `${(bytes / 1024).toFixed(bytes < 10 * 1024 ? 1 : 0)}KB`;
|
|
241
|
+
}
|
|
242
|
+
function sanitizeDiagnosticText(value) {
|
|
243
|
+
return value
|
|
244
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
245
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
246
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ")
|
|
247
|
+
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)([^@\s/]+)@/gi, "$1***@")
|
|
248
|
+
.replace(/\b(api[-_\s]?key|token|authorization)(\s*[:=]\s*)(\S+)/gi, "$1$2***")
|
|
249
|
+
.replace(/\s+/g, " ")
|
|
250
|
+
.trim()
|
|
251
|
+
.slice(0, 400) || "unknown error";
|
|
252
|
+
}
|
|
253
|
+
export function isSupportedNodeVersion(version) {
|
|
60
254
|
const [majorRaw = "0", minorRaw = "0"] = version.split(".");
|
|
61
255
|
const major = Number.parseInt(majorRaw, 10);
|
|
62
256
|
const minor = Number.parseInt(minorRaw, 10);
|
|
63
|
-
return major >
|
|
257
|
+
return major > 24 || (major === 24 && minor >= 15);
|
|
258
|
+
}
|
|
259
|
+
function configuredCommands(config, includeRouter) {
|
|
260
|
+
const commands = includeRouter ? [config.router.codex.command] : [];
|
|
261
|
+
for (const engine of configuredWorkerEngines(config)) {
|
|
262
|
+
const worker = config.workers[engine];
|
|
263
|
+
commands.push(worker.command);
|
|
264
|
+
if (worker.nativeSession.enabled) {
|
|
265
|
+
commands.push(worker.interactive.command);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return [...new Set(commands.filter((command) => command && command !== "mock"))];
|
|
269
|
+
}
|
|
270
|
+
function themeOverrideSummary(colors) {
|
|
271
|
+
const entries = Object.entries(colors).sort(([left], [right]) => left.localeCompare(right));
|
|
272
|
+
if (entries.length === 0) {
|
|
273
|
+
return "no color overrides";
|
|
274
|
+
}
|
|
275
|
+
return `colors: ${entries.map(([key, value]) => `${key}=${value}`).join(", ")}`;
|
|
276
|
+
}
|
|
277
|
+
function themePaletteSummary(theme) {
|
|
278
|
+
return [
|
|
279
|
+
`chrome=${theme.chrome}`,
|
|
280
|
+
`surface=${theme.surface}`,
|
|
281
|
+
`rail=${theme.rail}`,
|
|
282
|
+
`accent=${theme.accent}`
|
|
283
|
+
].join(", ");
|
|
284
|
+
}
|
|
285
|
+
function formatThemeContrastAudit(audit) {
|
|
286
|
+
if (audit.issues.length === 0) {
|
|
287
|
+
return [`theme contrast: ok (minimum ${formatContrastRatio(audit.minimumRatio)}:1 across ${audit.measurements.length} rendered pairs)`];
|
|
288
|
+
}
|
|
289
|
+
return [
|
|
290
|
+
`theme contrast: warning (${audit.issues.length} of ${audit.measurements.length} rendered pairs below ${TUI_THEME_MIN_CONTRAST_RATIO}:1)`,
|
|
291
|
+
...audit.issues.map(({ foreground, background, ratio }) => `theme contrast issue: ${foreground}/${background} ${formatContrastRatio(ratio)}:1`)
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
function formatContrastRatio(ratio) {
|
|
295
|
+
return ratio.toFixed(2);
|
|
64
296
|
}
|
|
65
|
-
function
|
|
297
|
+
function themeSummary(effectiveTheme, configTheme, cliTheme) {
|
|
298
|
+
if (cliTheme && cliTheme !== configTheme) {
|
|
299
|
+
return `${effectiveTheme} via --theme; config ${configTheme}`;
|
|
300
|
+
}
|
|
301
|
+
return effectiveTheme;
|
|
302
|
+
}
|
|
303
|
+
function configuredEngines(config, options) {
|
|
66
304
|
const engines = new Set();
|
|
67
|
-
if (
|
|
305
|
+
if (options.includeRouter) {
|
|
68
306
|
engines.add("router-codex");
|
|
307
|
+
}
|
|
308
|
+
if (config.router.defaultMode === "auto") {
|
|
69
309
|
engines.add(config.pairing.main);
|
|
70
310
|
engines.add(config.pairing.judge);
|
|
71
311
|
engines.add(config.pairing.actor);
|
|
@@ -79,21 +319,40 @@ function configuredCommands(config) {
|
|
|
79
319
|
engines.add(config.pairing.actor);
|
|
80
320
|
engines.add(config.pairing.critic);
|
|
81
321
|
}
|
|
82
|
-
return Array.from(
|
|
83
|
-
.map((engine) => commandForEngine(config, engine))
|
|
84
|
-
.filter((command) => Boolean(command) && command !== "mock")));
|
|
322
|
+
return Array.from(engines);
|
|
85
323
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
324
|
+
function configuredWorkerEngines(config) {
|
|
325
|
+
return configuredEngines(config, { includeRouter: false }).filter((engine) => engine === "codex" || engine === "claude");
|
|
326
|
+
}
|
|
327
|
+
function configuredEnvironmentChecks(config, env, includeRouter) {
|
|
328
|
+
const checks = [];
|
|
329
|
+
if (includeRouter) {
|
|
330
|
+
for (const [key, value] of Object.entries(config.router.codex.env)) {
|
|
331
|
+
for (const envName of referencedEnvNames(value)) {
|
|
332
|
+
checks.push({
|
|
333
|
+
label: `router.codex.env.${key}`,
|
|
334
|
+
envName,
|
|
335
|
+
ok: Boolean(env[envName])
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
92
339
|
}
|
|
93
|
-
|
|
94
|
-
|
|
340
|
+
for (const engine of configuredWorkerEngines(config)) {
|
|
341
|
+
const worker = engine === "codex" ? config.workers.codex : config.workers.claude;
|
|
342
|
+
for (const [key, value] of Object.entries(worker.model.env)) {
|
|
343
|
+
for (const envName of referencedEnvNames(value)) {
|
|
344
|
+
checks.push({
|
|
345
|
+
label: `workers.${engine}.model.env.${key}`,
|
|
346
|
+
envName,
|
|
347
|
+
ok: Boolean(env[envName])
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
95
351
|
}
|
|
96
|
-
return
|
|
352
|
+
return checks;
|
|
353
|
+
}
|
|
354
|
+
function referencedEnvNames(value) {
|
|
355
|
+
return Array.from(value.matchAll(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g), (match) => match[1] ?? "");
|
|
97
356
|
}
|
|
98
357
|
async function commandExists(command, env) {
|
|
99
358
|
if (command.includes("/")) {
|
|
@@ -119,3 +378,98 @@ async function canExecute(path) {
|
|
|
119
378
|
return false;
|
|
120
379
|
}
|
|
121
380
|
}
|
|
381
|
+
function configuredProxyEndpoint(configured, processEnvironment) {
|
|
382
|
+
const effective = {
|
|
383
|
+
...processEnvironment,
|
|
384
|
+
...Object.fromEntries(Object.entries(configured).map(([name, value]) => [name, renderEnvironmentValue(value, processEnvironment)]))
|
|
385
|
+
};
|
|
386
|
+
const value = [
|
|
387
|
+
effective.HTTPS_PROXY,
|
|
388
|
+
effective.https_proxy,
|
|
389
|
+
effective.ALL_PROXY,
|
|
390
|
+
effective.all_proxy,
|
|
391
|
+
effective.HTTP_PROXY,
|
|
392
|
+
effective.http_proxy
|
|
393
|
+
].find((candidate) => candidate?.trim())?.trim();
|
|
394
|
+
if (!value) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const url = new URL(value.includes("://") ? value : `http://${value}`);
|
|
399
|
+
const port = url.port ? Number(url.port) : defaultProxyPort(url.protocol);
|
|
400
|
+
if (!url.hostname || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
401
|
+
return "invalid";
|
|
402
|
+
}
|
|
403
|
+
return { host: url.hostname, port };
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return "invalid";
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function renderEnvironmentValue(value, env) {
|
|
410
|
+
return value.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => env[name] ?? "");
|
|
411
|
+
}
|
|
412
|
+
function defaultProxyPort(protocol) {
|
|
413
|
+
if (protocol === "https:") {
|
|
414
|
+
return 443;
|
|
415
|
+
}
|
|
416
|
+
if (protocol.startsWith("socks")) {
|
|
417
|
+
return 1080;
|
|
418
|
+
}
|
|
419
|
+
return 80;
|
|
420
|
+
}
|
|
421
|
+
function formatProxyEndpoint(endpoint) {
|
|
422
|
+
const host = endpoint.host.includes(":") ? `[${endpoint.host}]` : endpoint.host;
|
|
423
|
+
return `${host}:${endpoint.port}`;
|
|
424
|
+
}
|
|
425
|
+
async function cachedProxyConnection(cache, key, connect) {
|
|
426
|
+
const existing = cache.get(key);
|
|
427
|
+
if (existing) {
|
|
428
|
+
return existing;
|
|
429
|
+
}
|
|
430
|
+
const pending = connect();
|
|
431
|
+
cache.set(key, pending);
|
|
432
|
+
return pending;
|
|
433
|
+
}
|
|
434
|
+
async function detectMacSystemProxy() {
|
|
435
|
+
if (process.platform !== "darwin") {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
const output = await execFileText("scutil", ["--proxy"]);
|
|
439
|
+
if (!output) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const https = parseScutilProxy(output, "HTTPS");
|
|
443
|
+
return https ?? parseScutilProxy(output, "HTTP");
|
|
444
|
+
}
|
|
445
|
+
function parseScutilProxy(output, prefix) {
|
|
446
|
+
const enabled = output.match(new RegExp(`${prefix}Enable\\s*:\\s*(\\d+)`))?.[1] === "1";
|
|
447
|
+
const host = output.match(new RegExp(`${prefix}Proxy\\s*:\\s*(\\S+)`))?.[1];
|
|
448
|
+
const port = Number(output.match(new RegExp(`${prefix}Port\\s*:\\s*(\\d+)`))?.[1]);
|
|
449
|
+
return enabled && host && Number.isInteger(port) && port > 0 ? { host, port } : null;
|
|
450
|
+
}
|
|
451
|
+
function execFileText(command, args) {
|
|
452
|
+
return new Promise((resolve) => {
|
|
453
|
+
execFile(command, args, { timeout: 1000 }, (error, stdout) => {
|
|
454
|
+
resolve(error ? "" : stdout);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function canConnectProxy(host, port) {
|
|
459
|
+
return new Promise((resolve) => {
|
|
460
|
+
const socket = createConnection({ host, port });
|
|
461
|
+
let settled = false;
|
|
462
|
+
const finish = (reachable) => {
|
|
463
|
+
if (settled) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
settled = true;
|
|
467
|
+
socket.destroy();
|
|
468
|
+
resolve(reachable);
|
|
469
|
+
};
|
|
470
|
+
socket.setTimeout(750);
|
|
471
|
+
socket.once("connect", () => finish(true));
|
|
472
|
+
socket.once("error", () => finish(false));
|
|
473
|
+
socket.once("timeout", () => finish(false));
|
|
474
|
+
});
|
|
475
|
+
}
|
package/dist/domain/schemas.js
CHANGED
|
@@ -2,6 +2,43 @@ import { z } from "zod";
|
|
|
2
2
|
export const RouteModeSchema = z.enum(["simple", "complex"]);
|
|
3
3
|
export const EngineNameSchema = z.enum(["codex", "claude", "mock"]);
|
|
4
4
|
export const WorkerRoleSchema = z.enum(["main", "judge", "actor", "critic"]);
|
|
5
|
+
export const RouterFailureStageSchema = z.enum([
|
|
6
|
+
"spawn",
|
|
7
|
+
"input",
|
|
8
|
+
"waiting-output",
|
|
9
|
+
"streaming",
|
|
10
|
+
"exit",
|
|
11
|
+
"response"
|
|
12
|
+
]);
|
|
13
|
+
export const RouterFallbackResolutionSchema = z.enum([
|
|
14
|
+
"main",
|
|
15
|
+
"parallel",
|
|
16
|
+
"retry",
|
|
17
|
+
"auto-retry",
|
|
18
|
+
"cancelled",
|
|
19
|
+
"configured"
|
|
20
|
+
]);
|
|
21
|
+
export const RouterFailureKindSchema = z.enum([
|
|
22
|
+
"timeout",
|
|
23
|
+
"auth",
|
|
24
|
+
"rate-limit",
|
|
25
|
+
"proxy",
|
|
26
|
+
"network",
|
|
27
|
+
"unavailable",
|
|
28
|
+
"invalid-output",
|
|
29
|
+
"exit",
|
|
30
|
+
"input",
|
|
31
|
+
"unknown"
|
|
32
|
+
]);
|
|
33
|
+
export const RouterProxySourceSchema = z.enum(["router-config", "environment"]);
|
|
34
|
+
export const RouterTimeoutKindSchema = z.enum(["first-output", "idle", "total"]);
|
|
35
|
+
export const RouterRecoveryTriggerSchema = z.enum(["retry", "auto-retry"]);
|
|
36
|
+
export const TaskIdSchema = z
|
|
37
|
+
.string()
|
|
38
|
+
.min(1)
|
|
39
|
+
.max(255)
|
|
40
|
+
.regex(/^task-[A-Za-z0-9][A-Za-z0-9._-]*$/);
|
|
41
|
+
export const TaskSessionIdSchema = z.union([z.literal("main"), TaskIdSchema]);
|
|
5
42
|
export const TaskStateSchema = z.enum([
|
|
6
43
|
"created",
|
|
7
44
|
"routed",
|
|
@@ -10,11 +47,20 @@ export const TaskStateSchema = z.enum([
|
|
|
10
47
|
"actor_running",
|
|
11
48
|
"critic_running",
|
|
12
49
|
"revision_needed",
|
|
50
|
+
"integrating",
|
|
13
51
|
"verifying",
|
|
14
52
|
"done",
|
|
15
53
|
"failed",
|
|
16
54
|
"cancelled"
|
|
17
55
|
]);
|
|
56
|
+
export const TaskStatusTransitionSchema = z.object({
|
|
57
|
+
id: z.string().min(1),
|
|
58
|
+
from: TaskStateSchema,
|
|
59
|
+
to: TaskStateSchema,
|
|
60
|
+
at: z.string().datetime()
|
|
61
|
+
}).refine((transition) => transition.from !== transition.to, {
|
|
62
|
+
message: "Task status transition must change state"
|
|
63
|
+
});
|
|
18
64
|
export const WorkerStateSchema = z.enum([
|
|
19
65
|
"idle",
|
|
20
66
|
"starting",
|
|
@@ -24,24 +70,75 @@ export const WorkerStateSchema = z.enum([
|
|
|
24
70
|
"failed",
|
|
25
71
|
"cancelled"
|
|
26
72
|
]);
|
|
73
|
+
export const FeatureStateSchema = z.enum([
|
|
74
|
+
"created",
|
|
75
|
+
"queued",
|
|
76
|
+
"actor_running",
|
|
77
|
+
"actor_done",
|
|
78
|
+
"critic_running",
|
|
79
|
+
"critic_done",
|
|
80
|
+
"revision_needed",
|
|
81
|
+
"integrating",
|
|
82
|
+
"verifying",
|
|
83
|
+
"approved",
|
|
84
|
+
"failed",
|
|
85
|
+
"cancelled"
|
|
86
|
+
]);
|
|
27
87
|
export const RouteDecisionSchema = z.object({
|
|
28
88
|
mode: RouteModeSchema,
|
|
29
89
|
reason: z.string().min(1),
|
|
90
|
+
source: z.enum(["codex", "forced", "fallback"]).optional(),
|
|
91
|
+
duration_ms: z.number().nonnegative().optional(),
|
|
92
|
+
router_dispatch_ms: z.number().nonnegative().optional(),
|
|
93
|
+
router_spawn_ms: z.number().nonnegative().optional(),
|
|
94
|
+
router_first_output_ms: z.number().nonnegative().optional(),
|
|
95
|
+
router_first_stdout_ms: z.number().nonnegative().optional(),
|
|
96
|
+
router_first_stderr_ms: z.number().nonnegative().optional(),
|
|
97
|
+
router_process_ms: z.number().nonnegative().optional(),
|
|
98
|
+
router_parse_ms: z.number().nonnegative().optional(),
|
|
99
|
+
router_stdout_bytes: z.number().int().nonnegative().optional(),
|
|
100
|
+
router_stderr_bytes: z.number().int().nonnegative().optional(),
|
|
101
|
+
router_command: z.string().min(1).max(80).optional(),
|
|
102
|
+
router_failure_stage: RouterFailureStageSchema.optional(),
|
|
103
|
+
router_failure_kind: RouterFailureKindSchema.optional(),
|
|
104
|
+
router_attempt: z.number().int().positive().optional(),
|
|
105
|
+
router_total_duration_ms: z.number().nonnegative().optional(),
|
|
106
|
+
router_fallback_resolution: RouterFallbackResolutionSchema.optional(),
|
|
107
|
+
router_timeout_kind: RouterTimeoutKindSchema.optional(),
|
|
108
|
+
router_recovered_from: RouterFailureKindSchema.optional(),
|
|
109
|
+
router_recovered_via: RouterRecoveryTriggerSchema.optional(),
|
|
110
|
+
router_recovered_timeout_kind: RouterTimeoutKindSchema.optional(),
|
|
111
|
+
router_recovered_failure_stage: RouterFailureStageSchema.optional(),
|
|
112
|
+
proxy_configured: z.boolean().optional(),
|
|
113
|
+
proxy_source: RouterProxySourceSchema.optional(),
|
|
114
|
+
proxy_variable: z.string().regex(/^(?:HTTP|HTTPS|ALL)_PROXY$/i).optional(),
|
|
115
|
+
proxy_endpoint: z.string().min(1).max(255).optional(),
|
|
30
116
|
suggested_roles: z.array(WorkerRoleSchema).default([]),
|
|
31
117
|
judge_engine: EngineNameSchema.default("codex"),
|
|
32
118
|
actor_engine: EngineNameSchema.default("codex"),
|
|
33
119
|
critic_engine: EngineNameSchema.default("claude")
|
|
34
120
|
});
|
|
35
121
|
export const TaskMetaSchema = z.object({
|
|
36
|
-
id:
|
|
37
|
-
title: z.string().min(1),
|
|
122
|
+
id: TaskIdSchema,
|
|
123
|
+
title: z.string().min(1).max(160),
|
|
38
124
|
created_at: z.string().datetime(),
|
|
39
125
|
cwd: z.string().min(1),
|
|
40
126
|
mode: RouteModeSchema,
|
|
41
|
-
status: TaskStateSchema
|
|
127
|
+
status: TaskStateSchema,
|
|
128
|
+
archived_at: z.string().datetime().optional(),
|
|
129
|
+
status_transition: TaskStatusTransitionSchema.optional().catch(undefined)
|
|
130
|
+
}).transform((meta) => {
|
|
131
|
+
if (meta.status_transition && meta.status_transition.to !== meta.status) {
|
|
132
|
+
const nextMeta = { ...meta };
|
|
133
|
+
delete nextMeta.status_transition;
|
|
134
|
+
return nextMeta;
|
|
135
|
+
}
|
|
136
|
+
return meta;
|
|
42
137
|
});
|
|
43
138
|
export const WorkerStatusSchema = z.object({
|
|
44
139
|
worker_id: z.string().min(1),
|
|
140
|
+
feature_id: z.string().min(1).optional(),
|
|
141
|
+
feature_title: z.string().min(1).optional(),
|
|
45
142
|
role: WorkerRoleSchema,
|
|
46
143
|
engine: EngineNameSchema,
|
|
47
144
|
state: WorkerStateSchema,
|
|
@@ -50,8 +147,18 @@ export const WorkerStatusSchema = z.object({
|
|
|
50
147
|
summary: z.string(),
|
|
51
148
|
native_session_id: z.string().min(1).optional()
|
|
52
149
|
});
|
|
150
|
+
export const FeatureStatusSchema = z.object({
|
|
151
|
+
feature_id: z.string().min(1),
|
|
152
|
+
task_id: TaskIdSchema,
|
|
153
|
+
turn_id: z.string().min(1),
|
|
154
|
+
title: z.string().min(1).optional(),
|
|
155
|
+
description: z.string().default(""),
|
|
156
|
+
depends_on: z.array(z.string().min(1)).default([]),
|
|
157
|
+
state: FeatureStateSchema,
|
|
158
|
+
updated_at: z.string().datetime()
|
|
159
|
+
});
|
|
53
160
|
export const TurnMetaSchema = z.object({
|
|
54
|
-
task_id:
|
|
161
|
+
task_id: TaskIdSchema,
|
|
55
162
|
turn_id: z.string().regex(/^\d{4}$/),
|
|
56
163
|
created_at: z.string().datetime(),
|
|
57
164
|
request_path: z.string().min(1)
|
|
@@ -62,17 +169,31 @@ export const NativeSessionSchema = z.object({
|
|
|
62
169
|
role: WorkerRoleSchema,
|
|
63
170
|
worker_id: z.string().min(1),
|
|
64
171
|
session_id: z.string().min(1),
|
|
65
|
-
scope: z.
|
|
172
|
+
scope: z.enum(["main", "task"]),
|
|
66
173
|
cwd: z.string().min(1),
|
|
174
|
+
writable_dirs: z.array(z.string().min(1)).optional(),
|
|
67
175
|
created_at: z.string().datetime(),
|
|
68
176
|
last_used_at: z.string().datetime(),
|
|
69
177
|
source: NativeSessionSourceSchema
|
|
70
178
|
});
|
|
179
|
+
export const RetiredNativeSessionSchema = NativeSessionSchema.extend({
|
|
180
|
+
retired_at: z.string().datetime(),
|
|
181
|
+
retired_reason: z.string().min(1)
|
|
182
|
+
});
|
|
183
|
+
export const ChatRecordSchema = z.object({
|
|
184
|
+
time: z.string().datetime(),
|
|
185
|
+
from: z.enum(["user", "system"]),
|
|
186
|
+
text: z.string(),
|
|
187
|
+
task_id: TaskIdSchema.optional()
|
|
188
|
+
});
|
|
71
189
|
export const EventRecordSchema = z.object({
|
|
72
190
|
time: z.string().datetime(),
|
|
73
191
|
type: z.string().min(1),
|
|
74
192
|
message: z.string().optional(),
|
|
75
193
|
worker: z.string().optional(),
|
|
76
194
|
engine: EngineNameSchema.optional(),
|
|
77
|
-
task_id:
|
|
195
|
+
task_id: TaskSessionIdSchema.optional(),
|
|
196
|
+
transition_id: z.string().min(1).optional(),
|
|
197
|
+
from_state: TaskStateSchema.optional(),
|
|
198
|
+
to_state: TaskStateSchema.optional()
|
|
78
199
|
});
|