parallel-codex-tui 0.1.3 → 0.1.5

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 (78) hide show
  1. package/.parallel-codex/config.example.toml +136 -3
  2. package/README.md +299 -21
  3. package/dist/bootstrap.js +37 -17
  4. package/dist/cli-args.js +34 -3
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-preflight.js +18 -0
  7. package/dist/cli-startup-recovery.js +82 -0
  8. package/dist/cli-workspace-picker.js +330 -0
  9. package/dist/cli-workspace-transition.js +33 -0
  10. package/dist/cli-workspace.js +7 -71
  11. package/dist/cli.js +234 -24
  12. package/dist/core/collaboration-timeline.js +268 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +297 -109
  15. package/dist/core/file-store.js +119 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +7 -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 +462 -35
  25. package/dist/core/session-index.js +412 -88
  26. package/dist/core/session-manager.js +1110 -40
  27. package/dist/core/task-session-details.js +175 -0
  28. package/dist/core/task-state-machine.js +18 -0
  29. package/dist/core/workspace-commit-recovery.js +118 -0
  30. package/dist/core/workspace.js +19 -11
  31. package/dist/doctor.js +373 -34
  32. package/dist/domain/schemas.js +142 -7
  33. package/dist/orchestrator/collaboration-channel.js +289 -6
  34. package/dist/orchestrator/feature-plan.js +70 -0
  35. package/dist/orchestrator/final-acceptance.js +86 -0
  36. package/dist/orchestrator/judge-artifacts.js +236 -0
  37. package/dist/orchestrator/orchestrator.js +2086 -203
  38. package/dist/orchestrator/prompts.js +168 -5
  39. package/dist/orchestrator/supervisor-summary.js +56 -2
  40. package/dist/orchestrator/workspace-sandbox.js +927 -0
  41. package/dist/tui/App.js +3187 -161
  42. package/dist/tui/AppShell.js +196 -25
  43. package/dist/tui/CollaborationTimelineView.js +327 -0
  44. package/dist/tui/FeatureBoardView.js +232 -0
  45. package/dist/tui/InputBar.js +581 -57
  46. package/dist/tui/RouterDiagnosticsView.js +469 -0
  47. package/dist/tui/StatusBar.js +610 -57
  48. package/dist/tui/StatusDetailView.js +164 -0
  49. package/dist/tui/TaskSessionDetailView.js +222 -0
  50. package/dist/tui/TaskSessionsView.js +210 -0
  51. package/dist/tui/TerminalOutput.js +53 -9
  52. package/dist/tui/WorkerOutputView.js +1404 -161
  53. package/dist/tui/WorkerOverviewView.js +269 -0
  54. package/dist/tui/chat-history.js +25 -0
  55. package/dist/tui/chat-input.js +67 -19
  56. package/dist/tui/chat-paste.js +76 -0
  57. package/dist/tui/display-width.js +41 -3
  58. package/dist/tui/incremental-text-file.js +101 -0
  59. package/dist/tui/keyboard.js +49 -0
  60. package/dist/tui/markdown-text.js +14 -0
  61. package/dist/tui/raw-input-decoder.js +3 -0
  62. package/dist/tui/scrolling.js +2 -1
  63. package/dist/tui/status-line.js +360 -11
  64. package/dist/tui/task-memory.js +13 -1
  65. package/dist/tui/task-result.js +105 -0
  66. package/dist/tui/terminal-screen.js +13 -1
  67. package/dist/tui/theme-contrast.js +144 -0
  68. package/dist/tui/theme-preview.js +109 -0
  69. package/dist/tui/theme.js +158 -0
  70. package/dist/version.js +1 -1
  71. package/dist/workers/capabilities.js +213 -0
  72. package/dist/workers/live-probe.js +177 -0
  73. package/dist/workers/mock-adapter.js +73 -8
  74. package/dist/workers/native-attach.js +106 -16
  75. package/dist/workers/process-adapter.js +572 -77
  76. package/dist/workers/provider.js +26 -0
  77. package/dist/workers/registry.js +12 -20
  78. package/package.json +11 -2
@@ -1,7 +1,10 @@
1
- import { parse, stringify } from "@iarna/toml";
1
+ import { parse } from "@iarna/toml";
2
+ import { readFile } from "node:fs/promises";
2
3
  import { join } from "node:path";
3
4
  import { z } from "zod";
5
+ import { EngineNameSchema } from "../domain/schemas.js";
4
6
  import { pathExists, readTextIfExists, writeText } from "./file-store.js";
7
+ import { normalizeTuiThemeColorValue, normalizeTuiThemeName, TUI_THEME_FIELDS, TUI_THEME_NAMES } from "../tui/theme.js";
5
8
  const NativeSessionConfigSchema = z.object({
6
9
  enabled: z.boolean().default(true),
7
10
  resumeArgs: z.array(z.string()).default([]),
@@ -14,27 +17,92 @@ const WorkerModelConfigSchema = z.object({
14
17
  args: z.array(z.string()).default([]),
15
18
  env: z.record(z.string()).default({})
16
19
  });
20
+ const WorkerCapabilitiesConfigSchema = z.object({
21
+ profile: z.enum(["codex", "claude", "generic"]),
22
+ writableDirArgs: z.array(z.string()).default([]),
23
+ freshSessionArgs: z.array(z.string()).default([])
24
+ }).strict().superRefine((value, context) => {
25
+ if (value.writableDirArgs.length > 0 && !value.writableDirArgs.some((arg) => arg.includes("{dir}"))) {
26
+ context.addIssue({
27
+ code: z.ZodIssueCode.custom,
28
+ path: ["writableDirArgs"],
29
+ message: "writableDirArgs must include a {dir} template"
30
+ });
31
+ }
32
+ if (value.freshSessionArgs.length > 0 && !value.freshSessionArgs.some((arg) => arg.includes("{sessionId}"))) {
33
+ context.addIssue({
34
+ code: z.ZodIssueCode.custom,
35
+ path: ["freshSessionArgs"],
36
+ message: "freshSessionArgs must include a {sessionId} template"
37
+ });
38
+ }
39
+ });
17
40
  const InteractiveCommandSchema = z.object({
18
41
  command: z.string().min(1),
19
- args: z.array(z.string()).default([])
42
+ args: z.array(z.string()).default([]),
43
+ forkArgs: z.array(z.string()).default([])
44
+ }).superRefine((value, context) => {
45
+ if (value.forkArgs.length > 0 && !value.forkArgs.some((arg) => arg.includes("{sessionId}"))) {
46
+ context.addIssue({
47
+ code: z.ZodIssueCode.custom,
48
+ path: ["forkArgs"],
49
+ message: "forkArgs must include a {sessionId} template"
50
+ });
51
+ }
20
52
  });
21
53
  const RolePromptConfigSchema = z.object({
22
54
  title: z.string().min(1),
23
55
  instructions: z.array(z.string()).default([])
24
56
  });
57
+ const TuiColorValueSchema = z.string().min(1).refine((value) => normalizeTuiThemeColorValue(value) !== null, {
58
+ message: "Invalid TUI color value. Use a Chalk color name, #rgb/#rrggbb, rgb(r,g,b), or ansi256(0..255)."
59
+ }).transform((value) => normalizeTuiThemeColorValue(value) ?? value.trim());
60
+ const TuiThemeNameSchema = z.preprocess((value) => typeof value === "string" ? normalizeTuiThemeName(value) ?? value.trim() : value, z.enum(TUI_THEME_NAMES)).default("codex");
61
+ const UiColorOverridesSchema = z.object(Object.fromEntries(TUI_THEME_FIELDS.map((field) => [field, TuiColorValueSchema.optional()]))).strict().default({});
25
62
  const CodexRouterConfigSchema = z.object({
26
63
  command: z.string().min(1).default("codex"),
27
- args: z.array(z.string()).default(["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"]),
28
- timeoutMs: z.number().int().positive().default(120000),
29
- fallback: z.enum(["simple", "complex"]).default("complex")
64
+ args: z.array(z.string()).default([
65
+ "exec",
66
+ "--ephemeral",
67
+ "--ignore-rules",
68
+ "-c",
69
+ "model_reasoning_effort=low",
70
+ "--skip-git-repo-check",
71
+ "--sandbox",
72
+ "read-only",
73
+ "--color",
74
+ "never",
75
+ "-"
76
+ ]),
77
+ timeoutMs: z.number().int().positive().default(30000),
78
+ firstOutputTimeoutMs: z.number().int().positive().default(15000),
79
+ idleTimeoutMs: z.number().int().positive().default(15000),
80
+ maxOutputBytes: z.number().int().min(1024).max(16 * 1024 * 1024).default(1024 * 1024),
81
+ maxAttempts: z.number().int().min(1).max(3).default(2),
82
+ retryDelayMs: z.number().int().min(0).max(10000).default(500),
83
+ followUpTimeoutMs: z.number().int().positive().max(120000).default(20000),
84
+ fallback: z.enum(["simple", "complex"]).default("simple"),
85
+ env: z.record(z.string()).default({})
30
86
  });
87
+ const OrchestrationConfigSchema = z.object({
88
+ maxParallelFeatures: z.number().int().min(1).max(8),
89
+ maxRevisionRounds: z.number().int().min(1).max(10)
90
+ }).strict();
91
+ const UiConfigSchema = z.object({
92
+ showStatusBar: z.boolean(),
93
+ autoOpenFailedWorker: z.boolean(),
94
+ theme: TuiThemeNameSchema,
95
+ colors: UiColorOverridesSchema
96
+ }).strict();
31
97
  const WorkerCommandSchema = z.object({
32
98
  command: z.string().min(1),
33
99
  args: z.array(z.string()).default([]),
100
+ assignable: z.boolean().default(true),
34
101
  timeoutMs: z.number().int().positive().optional(),
35
102
  idleTimeoutMs: z.number().int().positive().optional(),
36
103
  firstOutputTimeoutMs: z.number().int().positive().optional(),
37
104
  model: WorkerModelConfigSchema.default({}),
105
+ capabilities: WorkerCapabilitiesConfigSchema,
38
106
  nativeSession: NativeSessionConfigSchema,
39
107
  interactive: InteractiveCommandSchema
40
108
  });
@@ -45,16 +113,17 @@ const AppConfigSchema = z.object({
45
113
  defaultMode: z.enum(["auto", "simple", "complex"]),
46
114
  codex: CodexRouterConfigSchema.default({})
47
115
  }),
116
+ orchestration: OrchestrationConfigSchema,
48
117
  workers: z.object({
49
118
  codex: WorkerCommandSchema,
50
119
  claude: WorkerCommandSchema,
51
120
  mock: WorkerCommandSchema
52
- }),
121
+ }).catchall(WorkerCommandSchema),
53
122
  pairing: z.object({
54
- main: z.enum(["codex", "claude", "mock"]),
55
- judge: z.enum(["codex", "claude", "mock"]),
56
- actor: z.enum(["codex", "claude", "mock"]),
57
- critic: z.enum(["codex", "claude", "mock"])
123
+ main: EngineNameSchema,
124
+ judge: EngineNameSchema,
125
+ actor: EngineNameSchema,
126
+ critic: EngineNameSchema
58
127
  }),
59
128
  roles: z.object({
60
129
  main: RolePromptConfigSchema,
@@ -62,10 +131,17 @@ const AppConfigSchema = z.object({
62
131
  actor: RolePromptConfigSchema,
63
132
  critic: RolePromptConfigSchema
64
133
  }),
65
- ui: z.object({
66
- showStatusBar: z.boolean(),
67
- autoOpenFailedWorker: z.boolean()
68
- })
134
+ ui: UiConfigSchema
135
+ }).superRefine((config, context) => {
136
+ for (const [role, workerId] of Object.entries(config.pairing)) {
137
+ if (!config.workers[workerId]) {
138
+ context.addIssue({
139
+ code: z.ZodIssueCode.custom,
140
+ path: ["pairing", role],
141
+ message: `Unknown Worker profile: ${workerId}`
142
+ });
143
+ }
144
+ }
69
145
  });
70
146
  export function defaultConfig(projectRoot) {
71
147
  return {
@@ -75,15 +151,39 @@ export function defaultConfig(projectRoot) {
75
151
  defaultMode: "auto",
76
152
  codex: {
77
153
  command: "codex",
78
- args: ["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"],
79
- timeoutMs: 120000,
80
- fallback: "complex"
154
+ args: [
155
+ "exec",
156
+ "--ephemeral",
157
+ "--ignore-rules",
158
+ "-c",
159
+ "model_reasoning_effort=low",
160
+ "--skip-git-repo-check",
161
+ "--sandbox",
162
+ "read-only",
163
+ "--color",
164
+ "never",
165
+ "-"
166
+ ],
167
+ timeoutMs: 30000,
168
+ firstOutputTimeoutMs: 15000,
169
+ idleTimeoutMs: 15000,
170
+ maxOutputBytes: 1024 * 1024,
171
+ maxAttempts: 2,
172
+ retryDelayMs: 500,
173
+ followUpTimeoutMs: 20000,
174
+ fallback: "simple",
175
+ env: {}
81
176
  }
82
177
  },
178
+ orchestration: {
179
+ maxParallelFeatures: 3,
180
+ maxRevisionRounds: 3
181
+ },
83
182
  workers: {
84
183
  codex: {
85
184
  command: "codex",
86
185
  args: ["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"],
186
+ assignable: true,
87
187
  timeoutMs: 45 * 60 * 1000,
88
188
  idleTimeoutMs: 5 * 60 * 1000,
89
189
  firstOutputTimeoutMs: 2 * 60 * 1000,
@@ -93,6 +193,11 @@ export function defaultConfig(projectRoot) {
93
193
  args: [],
94
194
  env: {}
95
195
  },
196
+ capabilities: {
197
+ profile: "codex",
198
+ writableDirArgs: ["--add-dir", "{dir}"],
199
+ freshSessionArgs: []
200
+ },
96
201
  nativeSession: {
97
202
  enabled: true,
98
203
  resumeArgs: ["exec", "resume", "{sessionId}", "--skip-git-repo-check", "-"],
@@ -101,12 +206,14 @@ export function defaultConfig(projectRoot) {
101
206
  },
102
207
  interactive: {
103
208
  command: "codex",
104
- args: ["resume", "{sessionId}"]
209
+ args: ["resume", "{sessionId}"],
210
+ forkArgs: ["fork", "{sessionId}"]
105
211
  }
106
212
  },
107
213
  claude: {
108
214
  command: "claude",
109
215
  args: ["--print", "--permission-mode", "acceptEdits", "--output-format", "text"],
216
+ assignable: true,
110
217
  timeoutMs: 45 * 60 * 1000,
111
218
  idleTimeoutMs: 5 * 60 * 1000,
112
219
  firstOutputTimeoutMs: 2 * 60 * 1000,
@@ -116,6 +223,11 @@ export function defaultConfig(projectRoot) {
116
223
  args: [],
117
224
  env: {}
118
225
  },
226
+ capabilities: {
227
+ profile: "claude",
228
+ writableDirArgs: ["--add-dir", "{dir}"],
229
+ freshSessionArgs: ["--session-id", "{sessionId}"]
230
+ },
119
231
  nativeSession: {
120
232
  enabled: true,
121
233
  resumeArgs: ["--print", "--resume", "{sessionId}", "--permission-mode", "acceptEdits", "--output-format", "text"],
@@ -124,18 +236,25 @@ export function defaultConfig(projectRoot) {
124
236
  },
125
237
  interactive: {
126
238
  command: "claude",
127
- args: ["--resume", "{sessionId}"]
239
+ args: ["--resume", "{sessionId}"],
240
+ forkArgs: ["--resume", "{sessionId}", "--fork-session"]
128
241
  }
129
242
  },
130
243
  mock: {
131
244
  command: "mock",
132
245
  args: [],
246
+ assignable: false,
133
247
  model: {
134
248
  name: "",
135
249
  provider: "",
136
250
  args: [],
137
251
  env: {}
138
252
  },
253
+ capabilities: {
254
+ profile: "generic",
255
+ writableDirArgs: [],
256
+ freshSessionArgs: []
257
+ },
139
258
  nativeSession: {
140
259
  enabled: true,
141
260
  resumeArgs: ["resume", "{sessionId}", "-"],
@@ -144,7 +263,8 @@ export function defaultConfig(projectRoot) {
144
263
  },
145
264
  interactive: {
146
265
  command: "mock",
147
- args: ["resume", "{sessionId}"]
266
+ args: ["resume", "{sessionId}"],
267
+ forkArgs: []
148
268
  }
149
269
  }
150
270
  },
@@ -174,7 +294,9 @@ export function defaultConfig(projectRoot) {
174
294
  },
175
295
  ui: {
176
296
  showStatusBar: true,
177
- autoOpenFailedWorker: true
297
+ autoOpenFailedWorker: true,
298
+ theme: "codex",
299
+ colors: {}
178
300
  }
179
301
  };
180
302
  }
@@ -198,71 +320,18 @@ export async function loadConfig(projectRoot) {
198
320
  ...(parsed.router ?? {}),
199
321
  codex: {
200
322
  ...base.router.codex,
201
- ...(parsed.router?.codex ?? {})
202
- }
203
- },
204
- workers: {
205
- codex: {
206
- ...base.workers.codex,
207
- ...(parsed.workers?.codex ?? {}),
208
- model: {
209
- ...base.workers.codex.model,
210
- ...(parsed.workers?.codex?.model ?? {}),
211
- env: {
212
- ...base.workers.codex.model.env,
213
- ...(parsed.workers?.codex?.model?.env ?? {})
214
- }
215
- },
216
- nativeSession: {
217
- ...base.workers.codex.nativeSession,
218
- ...(parsed.workers?.codex?.nativeSession ?? {})
219
- },
220
- interactive: {
221
- ...base.workers.codex.interactive,
222
- ...(parsed.workers?.codex?.interactive ?? {})
223
- }
224
- },
225
- claude: {
226
- ...base.workers.claude,
227
- ...(parsed.workers?.claude ?? {}),
228
- model: {
229
- ...base.workers.claude.model,
230
- ...(parsed.workers?.claude?.model ?? {}),
231
- env: {
232
- ...base.workers.claude.model.env,
233
- ...(parsed.workers?.claude?.model?.env ?? {})
234
- }
235
- },
236
- nativeSession: {
237
- ...base.workers.claude.nativeSession,
238
- ...(parsed.workers?.claude?.nativeSession ?? {})
239
- },
240
- interactive: {
241
- ...base.workers.claude.interactive,
242
- ...(parsed.workers?.claude?.interactive ?? {})
243
- }
244
- },
245
- mock: {
246
- ...base.workers.mock,
247
- ...(parsed.workers?.mock ?? {}),
248
- model: {
249
- ...base.workers.mock.model,
250
- ...(parsed.workers?.mock?.model ?? {}),
251
- env: {
252
- ...base.workers.mock.model.env,
253
- ...(parsed.workers?.mock?.model?.env ?? {})
254
- }
255
- },
256
- nativeSession: {
257
- ...base.workers.mock.nativeSession,
258
- ...(parsed.workers?.mock?.nativeSession ?? {})
259
- },
260
- interactive: {
261
- ...base.workers.mock.interactive,
262
- ...(parsed.workers?.mock?.interactive ?? {})
323
+ ...(parsed.router?.codex ?? {}),
324
+ env: {
325
+ ...base.router.codex.env,
326
+ ...(parsed.router?.codex?.env ?? {})
263
327
  }
264
328
  }
265
329
  },
330
+ orchestration: {
331
+ ...base.orchestration,
332
+ ...(parsed.orchestration ?? {})
333
+ },
334
+ workers: resolveWorkerConfigs(base.workers, parsed.workers ?? {}),
266
335
  pairing: {
267
336
  ...base.pairing,
268
337
  ...(parsed.pairing ?? {})
@@ -287,39 +356,163 @@ export async function loadConfig(projectRoot) {
287
356
  },
288
357
  ui: {
289
358
  ...base.ui,
290
- ...(parsed.ui ?? {})
359
+ ...(parsed.ui ?? {}),
360
+ colors: {
361
+ ...base.ui.colors,
362
+ ...(parsed.ui?.colors ?? {})
363
+ }
291
364
  }
292
365
  };
293
366
  return AppConfigSchema.parse(merged);
294
367
  }
368
+ function resolveWorkerConfigs(builtins, configured) {
369
+ const resolved = new Map();
370
+ const resolving = new Set();
371
+ const ids = [...new Set([...Object.keys(builtins), ...Object.keys(configured)])];
372
+ const resolve = (id) => {
373
+ const cached = resolved.get(id);
374
+ if (cached) {
375
+ return cached;
376
+ }
377
+ if (!EngineNameSchema.safeParse(id).success) {
378
+ throw new Error(`Invalid Worker profile id: ${id}`);
379
+ }
380
+ if (resolving.has(id)) {
381
+ throw new Error(`Circular Worker profile inheritance: ${[...resolving, id].join(" -> ")}`);
382
+ }
383
+ resolving.add(id);
384
+ try {
385
+ const override = configured[id] ?? {};
386
+ const builtin = builtins[id];
387
+ if (builtin && override.extends) {
388
+ throw new Error(`Built-in Worker profile ${id} cannot declare extends`);
389
+ }
390
+ let parent = builtin;
391
+ if (!parent) {
392
+ const parentId = override.extends?.trim();
393
+ if (!parentId || parentId === "generic") {
394
+ parent = genericWorkerConfig(id, override.command);
395
+ }
396
+ else {
397
+ if (!builtins[parentId] && !configured[parentId]) {
398
+ throw new Error(`Unknown Worker profile inherited by ${id}: ${parentId}`);
399
+ }
400
+ parent = resolve(parentId);
401
+ }
402
+ }
403
+ const worker = mergeWorkerConfig(parent, override);
404
+ resolved.set(id, worker);
405
+ return worker;
406
+ }
407
+ finally {
408
+ resolving.delete(id);
409
+ }
410
+ };
411
+ for (const id of ids) {
412
+ resolve(id);
413
+ }
414
+ return Object.fromEntries(resolved);
415
+ }
416
+ function genericWorkerConfig(id, command = id) {
417
+ return {
418
+ command,
419
+ args: [],
420
+ assignable: true,
421
+ timeoutMs: 45 * 60 * 1000,
422
+ idleTimeoutMs: 5 * 60 * 1000,
423
+ firstOutputTimeoutMs: 2 * 60 * 1000,
424
+ model: {
425
+ name: "",
426
+ provider: "",
427
+ args: [],
428
+ env: {}
429
+ },
430
+ capabilities: {
431
+ profile: "generic",
432
+ writableDirArgs: [],
433
+ freshSessionArgs: []
434
+ },
435
+ nativeSession: {
436
+ enabled: false,
437
+ resumeArgs: [],
438
+ detectSessionId: false,
439
+ fallback: "fail"
440
+ },
441
+ interactive: {
442
+ command,
443
+ args: [],
444
+ forkArgs: []
445
+ }
446
+ };
447
+ }
448
+ function mergeWorkerConfig(base, override) {
449
+ const { extends: _extends, ...values } = override;
450
+ return {
451
+ ...base,
452
+ ...values,
453
+ model: {
454
+ ...base.model,
455
+ ...(override.model ?? {}),
456
+ env: {
457
+ ...base.model.env,
458
+ ...(override.model?.env ?? {})
459
+ }
460
+ },
461
+ capabilities: {
462
+ ...base.capabilities,
463
+ ...(override.capabilities ?? {})
464
+ },
465
+ nativeSession: {
466
+ ...base.nativeSession,
467
+ ...(override.nativeSession ?? {})
468
+ },
469
+ interactive: {
470
+ ...base.interactive,
471
+ ...(override.interactive ?? {})
472
+ }
473
+ };
474
+ }
475
+ export function withUiThemeOverride(config, theme) {
476
+ if (!theme) {
477
+ return config;
478
+ }
479
+ return {
480
+ ...config,
481
+ ui: {
482
+ ...config.ui,
483
+ theme
484
+ }
485
+ };
486
+ }
295
487
  function assertObjectSections(parsed) {
296
488
  const sections = [
297
489
  ["router", parsed.router],
298
490
  ["router.codex", parsed.router?.codex],
491
+ ["router.codex.env", parsed.router?.codex?.env],
492
+ ["orchestration", parsed.orchestration],
299
493
  ["workers", parsed.workers],
300
- ["workers.codex", parsed.workers?.codex],
301
- ["workers.codex.model", parsed.workers?.codex?.model],
302
- ["workers.codex.model.env", parsed.workers?.codex?.model?.env],
303
- ["workers.codex.nativeSession", parsed.workers?.codex?.nativeSession],
304
- ["workers.codex.interactive", parsed.workers?.codex?.interactive],
305
- ["workers.claude", parsed.workers?.claude],
306
- ["workers.claude.model", parsed.workers?.claude?.model],
307
- ["workers.claude.model.env", parsed.workers?.claude?.model?.env],
308
- ["workers.claude.nativeSession", parsed.workers?.claude?.nativeSession],
309
- ["workers.claude.interactive", parsed.workers?.claude?.interactive],
310
- ["workers.mock", parsed.workers?.mock],
311
- ["workers.mock.model", parsed.workers?.mock?.model],
312
- ["workers.mock.model.env", parsed.workers?.mock?.model?.env],
313
- ["workers.mock.nativeSession", parsed.workers?.mock?.nativeSession],
314
- ["workers.mock.interactive", parsed.workers?.mock?.interactive],
315
494
  ["pairing", parsed.pairing],
316
495
  ["roles", parsed.roles],
317
496
  ["roles.main", parsed.roles?.main],
318
497
  ["roles.judge", parsed.roles?.judge],
319
498
  ["roles.actor", parsed.roles?.actor],
320
499
  ["roles.critic", parsed.roles?.critic],
321
- ["ui", parsed.ui]
500
+ ["ui", parsed.ui],
501
+ ["ui.colors", parsed.ui?.colors]
322
502
  ];
503
+ if (isPlainObject(parsed.workers)) {
504
+ for (const [id, value] of Object.entries(parsed.workers)) {
505
+ sections.push([`workers.${id}`, value]);
506
+ if (!isPlainObject(value)) {
507
+ continue;
508
+ }
509
+ const worker = value;
510
+ sections.push([`workers.${id}.model`, worker.model], [`workers.${id}.capabilities`, worker.capabilities], [`workers.${id}.nativeSession`, worker.nativeSession], [`workers.${id}.interactive`, worker.interactive]);
511
+ if (isPlainObject(worker.model)) {
512
+ sections.push([`workers.${id}.model.env`, worker.model.env]);
513
+ }
514
+ }
515
+ }
323
516
  for (const [path, value] of sections) {
324
517
  if (value !== undefined && !isPlainObject(value)) {
325
518
  throw new Error(`Invalid config section [${path}]: expected a table`);
@@ -330,13 +523,8 @@ function isPlainObject(value) {
330
523
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
331
524
  }
332
525
  export async function writeDefaultConfig(projectRoot) {
333
- const config = defaultConfig(projectRoot);
334
- const tomlText = stringify({
335
- router: config.router,
336
- workers: config.workers,
337
- pairing: config.pairing,
338
- roles: config.roles,
339
- ui: config.ui
340
- });
341
- await writeText(configPath(projectRoot), tomlText);
526
+ await writeText(configPath(projectRoot), await readExampleConfig());
527
+ }
528
+ async function readExampleConfig() {
529
+ return readFile(new URL("../../.parallel-codex/config.example.toml", import.meta.url), "utf8");
342
530
  }