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.
Files changed (74) hide show
  1. package/.parallel-codex/config.example.toml +90 -3
  2. package/README.md +269 -12
  3. package/dist/bootstrap.js +50 -18
  4. package/dist/cli-args.js +96 -14
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-recovery.js +70 -0
  7. package/dist/cli-workspace-picker.js +330 -0
  8. package/dist/cli-workspace-transition.js +33 -0
  9. package/dist/cli-workspace.js +40 -0
  10. package/dist/cli.js +291 -35
  11. package/dist/core/app-root.js +8 -0
  12. package/dist/core/collaboration-timeline.js +261 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +191 -23
  15. package/dist/core/file-store.js +130 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +10 -0
  18. package/dist/core/process-identity.js +48 -0
  19. package/dist/core/process-mutation-turn.js +128 -0
  20. package/dist/core/process-ownership.js +276 -0
  21. package/dist/core/process-tree.js +90 -0
  22. package/dist/core/router-audit.js +155 -0
  23. package/dist/core/router-redaction.js +31 -0
  24. package/dist/core/router.js +473 -42
  25. package/dist/core/session-index.js +225 -30
  26. package/dist/core/session-manager.js +1182 -44
  27. package/dist/core/task-state-machine.js +17 -0
  28. package/dist/core/workspace-commit-recovery.js +118 -0
  29. package/dist/core/workspace.js +126 -0
  30. package/dist/doctor.js +384 -30
  31. package/dist/domain/schemas.js +127 -6
  32. package/dist/orchestrator/collaboration-channel.js +255 -4
  33. package/dist/orchestrator/feature-plan.js +70 -0
  34. package/dist/orchestrator/judge-artifacts.js +236 -0
  35. package/dist/orchestrator/orchestrator.js +1777 -212
  36. package/dist/orchestrator/prompts.js +126 -2
  37. package/dist/orchestrator/supervisor-summary.js +56 -2
  38. package/dist/orchestrator/workspace-sandbox.js +911 -0
  39. package/dist/tui/App.js +2838 -159
  40. package/dist/tui/AppShell.js +188 -23
  41. package/dist/tui/CollaborationTimelineView.js +327 -0
  42. package/dist/tui/FeatureBoardView.js +227 -0
  43. package/dist/tui/InputBar.js +514 -57
  44. package/dist/tui/RouterDiagnosticsView.js +469 -0
  45. package/dist/tui/StatusBar.js +610 -57
  46. package/dist/tui/TaskSessionsView.js +207 -0
  47. package/dist/tui/TerminalOutput.js +53 -9
  48. package/dist/tui/WorkerOutputView.js +1403 -161
  49. package/dist/tui/WorkerOverviewView.js +250 -0
  50. package/dist/tui/chat-history.js +25 -0
  51. package/dist/tui/chat-input.js +67 -19
  52. package/dist/tui/chat-paste.js +76 -0
  53. package/dist/tui/display-width.js +41 -3
  54. package/dist/tui/incremental-text-file.js +101 -0
  55. package/dist/tui/keyboard.js +46 -0
  56. package/dist/tui/markdown-text.js +14 -0
  57. package/dist/tui/raw-input-decoder.js +3 -0
  58. package/dist/tui/scrolling.js +2 -1
  59. package/dist/tui/status-line.js +318 -11
  60. package/dist/tui/task-memory.js +15 -0
  61. package/dist/tui/task-result.js +105 -0
  62. package/dist/tui/terminal-screen.js +13 -1
  63. package/dist/tui/theme-contrast.js +144 -0
  64. package/dist/tui/theme-preview.js +109 -0
  65. package/dist/tui/theme.js +158 -0
  66. package/dist/version.js +1 -1
  67. package/dist/workers/capabilities.js +212 -0
  68. package/dist/workers/live-probe.js +176 -0
  69. package/dist/workers/mock-adapter.js +39 -6
  70. package/dist/workers/native-attach.js +147 -8
  71. package/dist/workers/native-session-detection.js +17 -0
  72. package/dist/workers/process-adapter.js +580 -81
  73. package/dist/workers/registry.js +4 -2
  74. 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 { configPath, loadConfig } from "./core/config.js";
5
- import { pathExists } from "./core/file-store.js";
6
- export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
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 22.5+)`);
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
- config = await loadConfig(appRoot);
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}; ${error.message})`);
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
- for (const command of configuredCommands(config)) {
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 isSupportedNodeVersion(version) {
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 > 22 || (major === 22 && minor >= 5);
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 configuredCommands(config) {
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 (config.router.defaultMode === "auto") {
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(new Set(Array.from(engines)
83
- .map((engine) => commandForEngine(config, engine))
84
- .filter((command) => Boolean(command) && command !== "mock")));
322
+ return Array.from(engines);
85
323
  }
86
- function commandForEngine(config, engine) {
87
- if (engine === "router-codex") {
88
- return config.router.codex.command;
89
- }
90
- if (engine === "codex") {
91
- return config.workers.codex.command;
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
- if (engine === "claude") {
94
- return config.workers.claude.command;
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 null;
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
+ }
@@ -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: z.string().min(1),
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: z.string().min(1),
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.literal("task"),
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: z.string().optional()
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
  });