parallel-codex-tui 0.1.4 → 0.1.6

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 (41) hide show
  1. package/.parallel-codex/config.example.toml +46 -0
  2. package/README.md +96 -19
  3. package/dist/cli-startup-preflight.js +18 -0
  4. package/dist/cli-startup-recovery.js +13 -1
  5. package/dist/cli.js +19 -7
  6. package/dist/core/clipboard.js +97 -0
  7. package/dist/core/collaboration-timeline.js +9 -2
  8. package/dist/core/config.js +161 -103
  9. package/dist/core/router.js +1 -1
  10. package/dist/core/session-index.js +234 -61
  11. package/dist/core/session-manager.js +25 -1
  12. package/dist/core/task-session-details.js +175 -0
  13. package/dist/core/task-state-machine.js +10 -9
  14. package/dist/doctor.js +58 -39
  15. package/dist/domain/schemas.js +15 -1
  16. package/dist/orchestrator/collaboration-channel.js +35 -3
  17. package/dist/orchestrator/final-acceptance.js +86 -0
  18. package/dist/orchestrator/orchestrator.js +405 -69
  19. package/dist/orchestrator/prompts.js +42 -3
  20. package/dist/orchestrator/workspace-sandbox.js +16 -0
  21. package/dist/tui/App.js +514 -56
  22. package/dist/tui/AppShell.js +9 -3
  23. package/dist/tui/FeatureBoardView.js +7 -2
  24. package/dist/tui/InputBar.js +87 -15
  25. package/dist/tui/StatusBar.js +1 -1
  26. package/dist/tui/StatusDetailView.js +164 -0
  27. package/dist/tui/TaskSessionDetailView.js +222 -0
  28. package/dist/tui/TaskSessionsView.js +5 -2
  29. package/dist/tui/WorkerOutputView.js +6 -1
  30. package/dist/tui/WorkerOverviewView.js +23 -4
  31. package/dist/tui/keyboard.js +6 -0
  32. package/dist/tui/status-line.js +42 -0
  33. package/dist/version.js +1 -1
  34. package/dist/workers/capabilities.js +4 -3
  35. package/dist/workers/live-probe.js +4 -3
  36. package/dist/workers/mock-adapter.js +37 -5
  37. package/dist/workers/native-attach.js +32 -17
  38. package/dist/workers/process-adapter.js +2 -0
  39. package/dist/workers/provider.js +26 -0
  40. package/dist/workers/registry.js +12 -22
  41. package/package.json +7 -1
@@ -2,6 +2,7 @@ import { parse } from "@iarna/toml";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { z } from "zod";
5
+ import { EngineNameSchema } from "../domain/schemas.js";
5
6
  import { pathExists, readTextIfExists, writeText } from "./file-store.js";
6
7
  import { normalizeTuiThemeColorValue, normalizeTuiThemeName, TUI_THEME_FIELDS, TUI_THEME_NAMES } from "../tui/theme.js";
7
8
  const NativeSessionConfigSchema = z.object({
@@ -38,7 +39,16 @@ const WorkerCapabilitiesConfigSchema = z.object({
38
39
  });
39
40
  const InteractiveCommandSchema = z.object({
40
41
  command: z.string().min(1),
41
- 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
+ }
42
52
  });
43
53
  const RolePromptConfigSchema = z.object({
44
54
  title: z.string().min(1),
@@ -75,7 +85,8 @@ const CodexRouterConfigSchema = z.object({
75
85
  env: z.record(z.string()).default({})
76
86
  });
77
87
  const OrchestrationConfigSchema = z.object({
78
- maxParallelFeatures: z.number().int().min(1).max(8)
88
+ maxParallelFeatures: z.number().int().min(1).max(8),
89
+ maxRevisionRounds: z.number().int().min(1).max(10)
79
90
  }).strict();
80
91
  const UiConfigSchema = z.object({
81
92
  showStatusBar: z.boolean(),
@@ -86,6 +97,7 @@ const UiConfigSchema = z.object({
86
97
  const WorkerCommandSchema = z.object({
87
98
  command: z.string().min(1),
88
99
  args: z.array(z.string()).default([]),
100
+ assignable: z.boolean().default(true),
89
101
  timeoutMs: z.number().int().positive().optional(),
90
102
  idleTimeoutMs: z.number().int().positive().optional(),
91
103
  firstOutputTimeoutMs: z.number().int().positive().optional(),
@@ -106,12 +118,12 @@ const AppConfigSchema = z.object({
106
118
  codex: WorkerCommandSchema,
107
119
  claude: WorkerCommandSchema,
108
120
  mock: WorkerCommandSchema
109
- }),
121
+ }).catchall(WorkerCommandSchema),
110
122
  pairing: z.object({
111
- main: z.enum(["codex", "claude", "mock"]),
112
- judge: z.enum(["codex", "claude", "mock"]),
113
- actor: z.enum(["codex", "claude", "mock"]),
114
- critic: z.enum(["codex", "claude", "mock"])
123
+ main: EngineNameSchema,
124
+ judge: EngineNameSchema,
125
+ actor: EngineNameSchema,
126
+ critic: EngineNameSchema
115
127
  }),
116
128
  roles: z.object({
117
129
  main: RolePromptConfigSchema,
@@ -120,6 +132,16 @@ const AppConfigSchema = z.object({
120
132
  critic: RolePromptConfigSchema
121
133
  }),
122
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
+ }
123
145
  });
124
146
  export function defaultConfig(projectRoot) {
125
147
  return {
@@ -154,12 +176,14 @@ export function defaultConfig(projectRoot) {
154
176
  }
155
177
  },
156
178
  orchestration: {
157
- maxParallelFeatures: 3
179
+ maxParallelFeatures: 3,
180
+ maxRevisionRounds: 3
158
181
  },
159
182
  workers: {
160
183
  codex: {
161
184
  command: "codex",
162
185
  args: ["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"],
186
+ assignable: true,
163
187
  timeoutMs: 45 * 60 * 1000,
164
188
  idleTimeoutMs: 5 * 60 * 1000,
165
189
  firstOutputTimeoutMs: 2 * 60 * 1000,
@@ -182,12 +206,14 @@ export function defaultConfig(projectRoot) {
182
206
  },
183
207
  interactive: {
184
208
  command: "codex",
185
- args: ["resume", "{sessionId}"]
209
+ args: ["resume", "{sessionId}"],
210
+ forkArgs: ["fork", "{sessionId}"]
186
211
  }
187
212
  },
188
213
  claude: {
189
214
  command: "claude",
190
215
  args: ["--print", "--permission-mode", "acceptEdits", "--output-format", "text"],
216
+ assignable: true,
191
217
  timeoutMs: 45 * 60 * 1000,
192
218
  idleTimeoutMs: 5 * 60 * 1000,
193
219
  firstOutputTimeoutMs: 2 * 60 * 1000,
@@ -210,12 +236,14 @@ export function defaultConfig(projectRoot) {
210
236
  },
211
237
  interactive: {
212
238
  command: "claude",
213
- args: ["--resume", "{sessionId}"]
239
+ args: ["--resume", "{sessionId}"],
240
+ forkArgs: ["--resume", "{sessionId}", "--fork-session"]
214
241
  }
215
242
  },
216
243
  mock: {
217
244
  command: "mock",
218
245
  args: [],
246
+ assignable: false,
219
247
  model: {
220
248
  name: "",
221
249
  provider: "",
@@ -235,7 +263,8 @@ export function defaultConfig(projectRoot) {
235
263
  },
236
264
  interactive: {
237
265
  command: "mock",
238
- args: ["resume", "{sessionId}"]
266
+ args: ["resume", "{sessionId}"],
267
+ forkArgs: []
239
268
  }
240
269
  }
241
270
  },
@@ -302,80 +331,7 @@ export async function loadConfig(projectRoot) {
302
331
  ...base.orchestration,
303
332
  ...(parsed.orchestration ?? {})
304
333
  },
305
- workers: {
306
- codex: {
307
- ...base.workers.codex,
308
- ...(parsed.workers?.codex ?? {}),
309
- model: {
310
- ...base.workers.codex.model,
311
- ...(parsed.workers?.codex?.model ?? {}),
312
- env: {
313
- ...base.workers.codex.model.env,
314
- ...(parsed.workers?.codex?.model?.env ?? {})
315
- }
316
- },
317
- capabilities: {
318
- ...base.workers.codex.capabilities,
319
- ...(parsed.workers?.codex?.capabilities ?? {})
320
- },
321
- nativeSession: {
322
- ...base.workers.codex.nativeSession,
323
- ...(parsed.workers?.codex?.nativeSession ?? {})
324
- },
325
- interactive: {
326
- ...base.workers.codex.interactive,
327
- ...(parsed.workers?.codex?.interactive ?? {})
328
- }
329
- },
330
- claude: {
331
- ...base.workers.claude,
332
- ...(parsed.workers?.claude ?? {}),
333
- model: {
334
- ...base.workers.claude.model,
335
- ...(parsed.workers?.claude?.model ?? {}),
336
- env: {
337
- ...base.workers.claude.model.env,
338
- ...(parsed.workers?.claude?.model?.env ?? {})
339
- }
340
- },
341
- capabilities: {
342
- ...base.workers.claude.capabilities,
343
- ...(parsed.workers?.claude?.capabilities ?? {})
344
- },
345
- nativeSession: {
346
- ...base.workers.claude.nativeSession,
347
- ...(parsed.workers?.claude?.nativeSession ?? {})
348
- },
349
- interactive: {
350
- ...base.workers.claude.interactive,
351
- ...(parsed.workers?.claude?.interactive ?? {})
352
- }
353
- },
354
- mock: {
355
- ...base.workers.mock,
356
- ...(parsed.workers?.mock ?? {}),
357
- model: {
358
- ...base.workers.mock.model,
359
- ...(parsed.workers?.mock?.model ?? {}),
360
- env: {
361
- ...base.workers.mock.model.env,
362
- ...(parsed.workers?.mock?.model?.env ?? {})
363
- }
364
- },
365
- capabilities: {
366
- ...base.workers.mock.capabilities,
367
- ...(parsed.workers?.mock?.capabilities ?? {})
368
- },
369
- nativeSession: {
370
- ...base.workers.mock.nativeSession,
371
- ...(parsed.workers?.mock?.nativeSession ?? {})
372
- },
373
- interactive: {
374
- ...base.workers.mock.interactive,
375
- ...(parsed.workers?.mock?.interactive ?? {})
376
- }
377
- }
378
- },
334
+ workers: resolveWorkerConfigs(base.workers, parsed.workers ?? {}),
379
335
  pairing: {
380
336
  ...base.pairing,
381
337
  ...(parsed.pairing ?? {})
@@ -409,6 +365,113 @@ export async function loadConfig(projectRoot) {
409
365
  };
410
366
  return AppConfigSchema.parse(merged);
411
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
+ }
412
475
  export function withUiThemeOverride(config, theme) {
413
476
  if (!theme) {
414
477
  return config;
@@ -428,24 +491,6 @@ function assertObjectSections(parsed) {
428
491
  ["router.codex.env", parsed.router?.codex?.env],
429
492
  ["orchestration", parsed.orchestration],
430
493
  ["workers", parsed.workers],
431
- ["workers.codex", parsed.workers?.codex],
432
- ["workers.codex.model", parsed.workers?.codex?.model],
433
- ["workers.codex.model.env", parsed.workers?.codex?.model?.env],
434
- ["workers.codex.capabilities", parsed.workers?.codex?.capabilities],
435
- ["workers.codex.nativeSession", parsed.workers?.codex?.nativeSession],
436
- ["workers.codex.interactive", parsed.workers?.codex?.interactive],
437
- ["workers.claude", parsed.workers?.claude],
438
- ["workers.claude.model", parsed.workers?.claude?.model],
439
- ["workers.claude.model.env", parsed.workers?.claude?.model?.env],
440
- ["workers.claude.capabilities", parsed.workers?.claude?.capabilities],
441
- ["workers.claude.nativeSession", parsed.workers?.claude?.nativeSession],
442
- ["workers.claude.interactive", parsed.workers?.claude?.interactive],
443
- ["workers.mock", parsed.workers?.mock],
444
- ["workers.mock.model", parsed.workers?.mock?.model],
445
- ["workers.mock.model.env", parsed.workers?.mock?.model?.env],
446
- ["workers.mock.capabilities", parsed.workers?.mock?.capabilities],
447
- ["workers.mock.nativeSession", parsed.workers?.mock?.nativeSession],
448
- ["workers.mock.interactive", parsed.workers?.mock?.interactive],
449
494
  ["pairing", parsed.pairing],
450
495
  ["roles", parsed.roles],
451
496
  ["roles.main", parsed.roles?.main],
@@ -455,6 +500,19 @@ function assertObjectSections(parsed) {
455
500
  ["ui", parsed.ui],
456
501
  ["ui.colors", parsed.ui?.colors]
457
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
+ }
458
516
  for (const [path, value] of sections) {
459
517
  if (value !== undefined && !isPlainObject(value)) {
460
518
  throw new Error(`Invalid config section [${path}]: expected a table`);
@@ -93,7 +93,7 @@ function annotateRoute(route, source, startedAt, telemetry, proxyContext, router
93
93
  ...routerProxyRouteFields(proxyContext),
94
94
  ...(routerCommand ? { router_command: routerCommand } : {}),
95
95
  source,
96
- duration_ms: Math.max(0, Date.now() - startedAt)
96
+ duration_ms: source === "forced" ? 0 : Math.max(0, Date.now() - startedAt)
97
97
  };
98
98
  }
99
99
  export function routerCommandLabel(command) {