parallel-codex-tui 0.1.4 → 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 (40) hide show
  1. package/.parallel-codex/config.example.toml +46 -0
  2. package/README.md +76 -17
  3. package/dist/cli-startup-preflight.js +18 -0
  4. package/dist/cli-startup-recovery.js +13 -1
  5. package/dist/cli.js +18 -6
  6. package/dist/core/collaboration-timeline.js +9 -2
  7. package/dist/core/config.js +161 -103
  8. package/dist/core/router.js +1 -1
  9. package/dist/core/session-index.js +234 -61
  10. package/dist/core/session-manager.js +25 -1
  11. package/dist/core/task-session-details.js +175 -0
  12. package/dist/core/task-state-machine.js +10 -9
  13. package/dist/doctor.js +58 -39
  14. package/dist/domain/schemas.js +15 -1
  15. package/dist/orchestrator/collaboration-channel.js +35 -3
  16. package/dist/orchestrator/final-acceptance.js +86 -0
  17. package/dist/orchestrator/orchestrator.js +405 -69
  18. package/dist/orchestrator/prompts.js +42 -3
  19. package/dist/orchestrator/workspace-sandbox.js +16 -0
  20. package/dist/tui/App.js +401 -52
  21. package/dist/tui/AppShell.js +9 -3
  22. package/dist/tui/FeatureBoardView.js +7 -2
  23. package/dist/tui/InputBar.js +82 -15
  24. package/dist/tui/StatusBar.js +1 -1
  25. package/dist/tui/StatusDetailView.js +164 -0
  26. package/dist/tui/TaskSessionDetailView.js +222 -0
  27. package/dist/tui/TaskSessionsView.js +5 -2
  28. package/dist/tui/WorkerOutputView.js +1 -0
  29. package/dist/tui/WorkerOverviewView.js +23 -4
  30. package/dist/tui/keyboard.js +3 -0
  31. package/dist/tui/status-line.js +42 -0
  32. package/dist/version.js +1 -1
  33. package/dist/workers/capabilities.js +4 -3
  34. package/dist/workers/live-probe.js +4 -3
  35. package/dist/workers/mock-adapter.js +37 -5
  36. package/dist/workers/native-attach.js +32 -17
  37. package/dist/workers/process-adapter.js +2 -0
  38. package/dist/workers/provider.js +26 -0
  39. package/dist/workers/registry.js +12 -22
  40. package/package.json +7 -1
@@ -30,6 +30,8 @@ fallback = "simple"
30
30
  # Maximum isolated Actor or Critic feature workers running at the same time (1..8).
31
31
  # Each feature receives its own snapshot; a combined Wave Critic approves staged changes before atomic live commit.
32
32
  maxParallelFeatures = 3
33
+ # Maximum Actor/Critic revision rounds before the task is stopped for manual intervention (1..10).
34
+ maxRevisionRounds = 3
33
35
 
34
36
  [workers.codex]
35
37
  command = "codex"
@@ -63,6 +65,8 @@ freshSessionArgs = []
63
65
  [workers.codex.interactive]
64
66
  command = "codex"
65
67
  args = ["resume", "{sessionId}"]
68
+ # Used by the Task session detail view when branching from a recorded native session.
69
+ forkArgs = ["fork", "{sessionId}"]
66
70
 
67
71
  [workers.codex.nativeSession]
68
72
  fallback = "new"
@@ -93,6 +97,7 @@ freshSessionArgs = ["--session-id", "{sessionId}"]
93
97
  [workers.claude.interactive]
94
98
  command = "claude"
95
99
  args = ["--resume", "{sessionId}"]
100
+ forkArgs = ["--resume", "{sessionId}", "--fork-session"]
96
101
 
97
102
  [workers.claude.nativeSession]
98
103
  fallback = "new"
@@ -106,6 +111,47 @@ profile = "generic"
106
111
  writableDirArgs = []
107
112
  freshSessionArgs = []
108
113
 
114
+ # Add any number of named Worker providers. IDs use lowercase letters, digits, or _.
115
+ # A profile can inherit the Codex/Claude command protocol and override only role-specific values.
116
+ # [workers.openai_compat]
117
+ # extends = "codex"
118
+ # command = "openai-compatible-coder"
119
+ # assignable = true
120
+ #
121
+ # [workers.openai_compat.model]
122
+ # name = "third-party-model"
123
+ # provider = "openai-compatible"
124
+ # args = ["--model", "{model}", "--provider", "{provider}"]
125
+ #
126
+ # [workers.openai_compat.model.env]
127
+ # OPENAI_API_KEY = "{env:OPENAI_COMPAT_API_KEY}"
128
+ # OPENAI_BASE_URL = "{env:OPENAI_COMPAT_BASE_URL}"
129
+ #
130
+ # [workers.openai_compat.interactive]
131
+ # command = "openai-compatible-coder"
132
+ #
133
+ # [workers.anthropic_compat]
134
+ # extends = "claude"
135
+ # command = "anthropic-compatible-coder"
136
+ #
137
+ # [workers.anthropic_compat.model]
138
+ # name = "third-party-claude-model"
139
+ # provider = "anthropic-compatible"
140
+ #
141
+ # [workers.anthropic_compat.model.env]
142
+ # ANTHROPIC_API_KEY = "{env:ANTHROPIC_COMPAT_API_KEY}"
143
+ # ANTHROPIC_BASE_URL = "{env:ANTHROPIC_COMPAT_BASE_URL}"
144
+ #
145
+ # [workers.anthropic_compat.interactive]
146
+ # command = "anthropic-compatible-coder"
147
+
148
+ # A custom CLI starts from conservative generic defaults: isolated cwd, no injected
149
+ # vendor flags, and native resume disabled until explicitly configured.
150
+ # [workers.vendor]
151
+ # extends = "generic"
152
+ # command = "vendor-coder"
153
+ # args = ["run", "--stdin"]
154
+
109
155
  [pairing]
110
156
  main = "claude"
111
157
  judge = "codex"
package/README.md CHANGED
@@ -39,6 +39,7 @@ Startup resolves the worker project before routing:
39
39
  - Without `--workspace`, an interactive terminal shows remembered projects from `.parallel-codex/workspaces.json`; use Up/Down and Enter, a displayed number, or `N` to enter a new folder path. The picker clears before the main chat opens.
40
40
  - In non-interactive startup, the CLI reuses the last remembered workspace, falls back to the current directory if none was saved, and creates an explicit `--workspace` path if needed.
41
41
  - The selected workspace is prepared before any router or worker process starts.
42
+ - Every interactive open and in-place workspace switch runs a quota-free startup preflight before the TUI accepts work. It checks workspace read/write/search access, every CLI required by the active route and role configuration, referenced model environment variables, each named Worker's Codex, Claude, or declared generic CLI capabilities, configured proxy endpoint reachability, and native workspace trust policy. Healthy checks stay quiet; warnings and failures appear in chat with the `parallel-codex-tui --doctor` action. Live model requests remain opt-in through `--probe-agents` and `--probe-router`.
42
43
  - While the TUI is idle, press `Ctrl+P` to open the same project picker without exiting. `Esc` returns with the unsent draft intact; selecting or creating a folder locks duplicate picker input, shows the project being opened, rebuilds the runtime in place, and restores that workspace's latest task, route, workers, and chat. A failed open returns to the original view without replacing its runtime or draft.
43
44
  - Workspace chat, task files, and `session-index.sqlite` stay isolated under each project. Router configuration and `router/routes.jsonl` remain shared under the app root, so classifications across projects use one global Router audit.
44
45
 
@@ -68,7 +69,7 @@ parallel-codex-tui --doctor --probe-router
68
69
  parallel-codex-tui --version
69
70
  ```
70
71
 
71
- `--doctor` checks configured automated and interactive commands, `{env:NAME}` references, and the CLI help surfaces required for Codex exec/resume sandboxing and Claude print/resume permissions before workers start. Recognized standard CLI help that lacks a required option fails the check; an opaque wrapper keeps the compatible built-in profile's unverified warning, while an explicit `generic` capability contract is validated without guessing vendor-specific help. Doctor also rejects an explicitly read-only Codex interactive sandbox because feature attach requires its recorded writable roots, and reminds you that native workspace trust remains an interactive decision. It reports proxy host/port reachability as a local-endpoint check, not as proof that the proxy upstream or model API is healthy. Add `--probe-agents` to make one minimal fresh request and one same-session resume request through every configured Codex or Claude worker engine. This explicit probe uses model quota, removes successful probe artifacts, preserves failed artifacts under `.parallel-codex/probes/`, and exits non-zero when a fresh or resumed request cannot be proven. Fresh sessions use the configured `freshSessionArgs`; Claude defaults to a generated native `--session-id`, persisted only after the process emits output, so a silent failed launch cannot create a false resumable session. Add `--probe-router` to run one real classification through the configured Codex Router; the command exits non-zero when that live request falls back or fails. Doctor also reports the loaded TUI theme, core palette values, ANSI swatch previews, and color override values, including any temporary `--theme` override.
72
+ `--doctor` checks configured automated and interactive commands, `{env:NAME}` references, and the CLI help surfaces required for Codex exec/resume sandboxing and Claude print/resume permissions before workers start. Recognized standard CLI help that lacks a required option fails the check; an opaque wrapper keeps the compatible built-in profile's unverified warning, while an explicit `generic` capability contract is validated without guessing vendor-specific help. Doctor performs these checks for every named Worker Provider used by the active route, rejects an explicitly read-only Codex-compatible interactive sandbox when feature attach needs writable roots, and reminds you that native workspace trust remains an interactive decision. It reports proxy host/port reachability as a local-endpoint check, not as proof that the proxy upstream or model API is healthy. Add `--probe-agents` to make one minimal fresh request and, when configured, one same-session resume request through every active process Worker Provider. This explicit probe uses model quota, removes successful probe artifacts, preserves failed artifacts under `.parallel-codex/probes/`, and exits non-zero when a fresh or resumed request cannot be proven. Fresh sessions use the configured `freshSessionArgs`; Claude-compatible profiles default to a generated native `--session-id`, persisted only after the process emits output, so a silent failed launch cannot create a false resumable session. Add `--probe-router` to run one real classification through the configured Codex Router; the command exits non-zero when that live request falls back or fails. Doctor also reports the loaded TUI theme, core palette values, ANSI swatch previews, and color override values, including any temporary `--theme` override.
72
73
 
73
74
  ## Quick Start
74
75
 
@@ -107,10 +108,13 @@ Limit how many feature-level Actor or Critic agents may run at once:
107
108
  ```toml
108
109
  [orchestration]
109
110
  maxParallelFeatures = 3 # 1..8
111
+ maxRevisionRounds = 3 # 1..10
110
112
  ```
111
113
 
112
114
  Judge may plan up to eight dependency-aware features. The orchestrator keeps dependency order while running at most this many agents concurrently. When one worker fails, already-running peers are allowed to finish cleanly and queued features are not started.
113
115
 
116
+ Actor and Critic reuse their native sessions across revision rounds. Blocking findings and Actor replies remain in the Feature JSONL mailbox; the task stops with an explicit error when `maxRevisionRounds` is exhausted.
117
+
114
118
  Queued Feature workers remain `queued` until they acquire a concurrency slot. Their persisted state changes to `actor_running` or `critic_running` only after the Worker is registered as cancellable, then to `actor_done` or `critic_done` before that active registration is removed. As a result, only `actor_running` and `critic_running` expose Feature cancellation; queued and role-complete Features cannot present a cancel action that has no live process behind it.
115
119
 
116
120
  ## Theme
@@ -262,7 +266,7 @@ ALL_PROXY = "socks5h://127.0.0.1:7890"
262
266
  NO_PROXY = "localhost,127.0.0.1"
263
267
  ```
264
268
 
265
- `router.codex.env` applies only to semantic classification. `workers.codex.model.env` applies to fresh/resumed Codex workers and embedded native attach sessions. Keep these values in local `config.toml`, which is ignored by Git.
269
+ `router.codex.env` applies only to semantic classification. `workers.<id>.model.env` applies to fresh/resumed runs and embedded native attach for that named Worker Provider. Keep these values in local `config.toml`, which is ignored by Git.
266
270
 
267
271
  Run `parallel-codex-tui --doctor` after changing proxy settings. Doctor checks referenced environment variables, reports when a macOS system proxy is not inherited by Codex subprocesses, and labels configured proxy host/port checks as local-endpoint reachability without printing credentials. Then run `parallel-codex-tui --doctor --probe-router` when you also need to verify the real Codex Router path through that proxy; this explicit probe can take up to `router.codex.timeoutMs`. A timed-out request is identified as `first output timeout`, `idle timeout after stdout/stderr`, or `total timeout`; `via <proxy-host:port>` remains context, not a claim that the proxy caused the failure. The audit never exposes the proxy URL or credentials.
268
272
 
@@ -302,6 +306,7 @@ freshSessionArgs = []
302
306
  [workers.codex.interactive]
303
307
  command = "codex"
304
308
  args = ["resume", "{sessionId}"]
309
+ forkArgs = ["fork", "{sessionId}"]
305
310
 
306
311
  [workers.claude]
307
312
  command = "claude"
@@ -315,31 +320,81 @@ freshSessionArgs = ["--session-id", "{sessionId}"]
315
320
  [workers.claude.interactive]
316
321
  command = "claude"
317
322
  args = ["--resume", "{sessionId}"]
323
+ forkArgs = ["--resume", "{sessionId}", "--fork-session"]
318
324
  ```
319
325
 
320
326
  `model.args` and `model.env` apply to both automated worker runs and embedded native attach sessions. Native attach appends the rendered model arguments after `interactive.args`, so third-party `{model}` and `{provider}` selections remain active when you press `Ctrl+O`.
321
327
 
322
- `model.provider` names the remote model service; `capabilities.profile` describes the local CLI protocol. Keep the built-in `codex` or `claude` profile when a wrapper accepts that CLI's normal flags. For a third-party command with its own syntax, declare `generic` so parallel-codex-tui does not inject `--sandbox`, `--permission-mode`, or other built-in-only flags:
328
+ `model.provider` names the remote model service; `capabilities.profile` describes the local CLI protocol. Worker Provider IDs are lowercase names using letters, digits, or `_`; excluding `-` keeps generated Worker directory names unambiguous. Existing `codex`, `claude`, and `mock` IDs remain compatible, while additional profiles can inherit `codex`, `claude`, another named profile, or conservative `generic` defaults.
329
+
330
+ Each role can select an independent command, model, environment, and permission contract through `[pairing]`. This example uses an OpenAI-compatible coding CLI for Judge/Actor and an Anthropic-compatible coding CLI for Main/Critic:
323
331
 
324
332
  ```toml
325
- [workers.codex]
333
+ [workers.openai_compat]
334
+ extends = "codex"
335
+ command = "openai-compatible-coder"
336
+ args = ["exec", "--sandbox", "workspace-write", "-"]
337
+
338
+ [workers.openai_compat.model]
339
+ name = "third-party-code-model"
340
+ provider = "openai-compatible"
341
+ args = ["--model", "{model}", "--provider", "{provider}"]
342
+
343
+ [workers.openai_compat.model.env]
344
+ OPENAI_API_KEY = "{env:OPENAI_COMPAT_API_KEY}"
345
+ OPENAI_BASE_URL = "{env:OPENAI_COMPAT_BASE_URL}"
346
+
347
+ [workers.openai_compat.interactive]
348
+ command = "openai-compatible-coder"
349
+
350
+ [workers.anthropic_compat]
351
+ extends = "claude"
352
+ command = "anthropic-compatible-coder"
353
+
354
+ [workers.anthropic_compat.model]
355
+ name = "third-party-claude-model"
356
+ provider = "anthropic-compatible"
357
+
358
+ [workers.anthropic_compat.model.env]
359
+ ANTHROPIC_API_KEY = "{env:ANTHROPIC_COMPAT_API_KEY}"
360
+ ANTHROPIC_BASE_URL = "{env:ANTHROPIC_COMPAT_BASE_URL}"
361
+
362
+ [workers.anthropic_compat.interactive]
363
+ command = "anthropic-compatible-coder"
364
+
365
+ [pairing]
366
+ main = "anthropic_compat"
367
+ judge = "openai_compat"
368
+ actor = "openai_compat"
369
+ critic = "anthropic_compat"
370
+ ```
371
+
372
+ `extends` inherits the complete command protocol, including capabilities, native-session behavior, watchdogs, and interactive arguments. Override both `command` and `interactive.command` when a wrapper replaces both executables. `assignable = true` exposes a profile to Feature-level Provider cycling; custom profiles default to true, while the reserved deterministic `mock` profile defaults to false. Pairing an unknown profile, unsafe profile IDs, and inheritance cycles fail config validation before startup.
373
+
374
+ For a third-party command with its own syntax, inherit `generic` so parallel-codex-tui does not inject `--sandbox`, `--permission-mode`, or other built-in-only flags:
375
+
376
+ ```toml
377
+ [workers.vendor]
378
+ extends = "generic"
326
379
  command = "vendor-coder"
327
380
  args = ["run", "--stdin"]
328
381
 
329
- [workers.codex.capabilities]
382
+ [workers.vendor.capabilities]
330
383
  profile = "generic"
331
384
  writableDirArgs = ["--allow-root", "{dir}"]
332
385
  freshSessionArgs = ["--new-session", "{sessionId}"]
333
386
 
334
- [workers.codex.nativeSession]
387
+ [workers.vendor.nativeSession]
388
+ enabled = true
335
389
  resumeArgs = ["run", "--resume", "{sessionId}", "--stdin"]
336
390
 
337
- [workers.codex.interactive]
391
+ [workers.vendor.interactive]
338
392
  command = "vendor-coder"
339
393
  args = ["resume", "{sessionId}"]
394
+ forkArgs = ["branch", "{sessionId}"]
340
395
  ```
341
396
 
342
- Set either capability argument list to `[]` when the wrapper needs no extra argument. A non-empty `writableDirArgs` must include `{dir}`, and a non-empty `freshSessionArgs` must include `{sessionId}`. Doctor validates this declared contract without guessing the wrapper's `--help` format; `--doctor --probe-agents` remains the end-to-end fresh/resume check.
397
+ Generic profiles start with native resume disabled, no vendor flags, and the isolated Worker cwd. Set either capability argument list to `[]` when the wrapper needs no extra argument. A non-empty `writableDirArgs` must include `{dir}`, while non-empty `freshSessionArgs` and `interactive.forkArgs` must include `{sessionId}`. Set `forkArgs = []` when a generic CLI cannot fork a native conversation. Doctor validates the declared capability contract without guessing the wrapper's `--help` format; `--doctor --probe-agents` remains the end-to-end fresh/resume check. Every new Worker status records the actual profile ID plus its rendered model/provider snapshot, so later config changes do not relabel historical runs.
343
398
 
344
399
  Customize each role independently; the main role is applied to simple chat, while Judge, Actor, and Critic receive their configured instructions during complex work:
345
400
 
@@ -357,19 +412,23 @@ Keep `[router.codex]` on `read-only`; routing only classifies requests and does
357
412
 
358
413
  The process adapter sends each role prompt to stdin and records stdout/stderr in `output.log`. Worker success is separate from the operating-system exit code: a total, first-output, idle, or stdin watchdog failure remains failed even when the terminated CLI handles `SIGTERM` and exits with code `0`, so the next role never starts from a timed-out checkpoint. `firstOutputTimeoutMs` owns silent startup, `idleTimeoutMs` starts only after real stdout/stderr activity, and `timeoutMs` remains the hard ceiling; equal or longer secondary watchdogs do not race the total deadline.
359
414
 
360
- In chat, scroll long conversation history with the mouse wheel or PageUp/PageDown; sending a new message returns to the latest reply. After a complex task completes, use the same controls to move through its structured result and press `Ctrl+D` to toggle details. Press `Ctrl+N` to preserve the current session and start a new task, `Ctrl+W` to open worker logs, and `Tab` to cycle the selected worker when workers exist. While a request is running, press `Esc` to stop it. After a failed or cancelled task, press `Ctrl+R` to retry the same turn or `Ctrl+N` to start independently. In worker-log views, scroll with the mouse wheel or PageUp/PageDown, press `Tab` to cycle workers, `Ctrl+N` to start a new task, and `Esc` to return to chat.
415
+ In chat, scroll long conversation history with the mouse wheel or PageUp/PageDown; sending a new message returns to the latest reply. After a complex task completes, use the same controls to move through its structured result and press `Ctrl+D` to toggle details. Press `Ctrl+N` to preserve the current session and start a new task, `Ctrl+W` to open worker logs, and `Tab` to cycle the selected worker when workers exist. `Ctrl+S` opens the same status detail view from chat, logs, Worker overview, or an embedded native session; it keeps the footer compact while exposing the complete route, reason, selected Provider/model, phase, activity timestamp, and native session. Press `Ctrl+S` or `Esc` to return without interrupting the underlying Worker. While a request is running, press `Esc` to stop it. After a failed or cancelled task, press `Ctrl+R` to retry the same turn or `Ctrl+N` to start independently. In worker-log views, scroll with the mouse wheel or PageUp/PageDown, press `Tab` to cycle workers, `Ctrl+N` to start a new task, and `Esc` to return to chat.
361
416
 
362
- `Ctrl+B` opens a live Worker overview without replacing the `Ctrl+W` log shortcut. It summarizes every Judge, Actor, and Critic by engine, state, phase, latest summary, and native-session availability. The selected active worker gets a live activity line showing its first-output deadline while starting and its idle deadline after output begins. The line stays muted while healthy, changes to warning for the final 20% and danger once overdue. Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` changes the selected worker; Enter or `Ctrl+W` opens its rendered log, `Ctrl+O` attaches to its native session, and `Esc` returns to the originating chat or log view. Router diagnostics and the project picker also return to the overview when opened from there.
417
+ `Ctrl+B` opens a live Worker overview without replacing the `Ctrl+W` log shortcut. It summarizes every Judge, Actor, and Critic by turn, role, named Provider, persisted model, state, phase, latest summary, and native-session availability. The selected active worker gets a live activity line showing its first-output deadline while starting and its idle deadline after output begins. The line stays muted while healthy, changes to warning for the final 20% and danger once overdue. Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` changes the selected worker; Enter or `Ctrl+W` opens its rendered log, `Ctrl+O` attaches to its native session, and `Esc` returns to the originating chat or log view. Router diagnostics and the project picker also return to the overview when opened from there.
363
418
 
364
- From Worker overview, press `F` to open the file-backed Feature board. It summarizes every planned feature by turn, state, blocked dependencies and open Critic findings while preserving descriptions and Actor replies from the same collaboration files. Use Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` to select a feature; Enter opens the selected feature's collaboration timeline; `R` refreshes immediately; and `Esc` returns to Worker overview. When a timeline was opened from this board, `Esc` returns to the Feature board instead of skipping back to Workers. While the selected feature is running, press `X` twice to cancel only its active Actor or Critic process; the first press opens a confirmation and `Esc` keeps it running. After confirmation, already-running peers finish, queued workers stop, and integration remains blocked, so a partial wave never reaches the live workspace. Once the task settles as cancelled, `Ctrl+R` retries the cancelled task with persisted worker sessions; completed peers are loaded from their same-wave checkpoints instead of running again. Checkpoint load, reuse, and recovery events appear in the collaboration timeline.
419
+ From Worker overview, press `F` to open the file-backed Feature board. It summarizes every planned feature by turn, state, blocked dependencies and open Critic findings while preserving descriptions and Actor replies from the same collaboration files. Use Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` to select a feature; Enter opens the selected feature's collaboration timeline; `R` refreshes immediately; and `Esc` returns to Worker overview. When a timeline was opened from this board, `Esc` returns to the Feature board instead of skipping back to Workers. While the selected feature is running, press `P` twice to pause only its active Actor or Critic process, or press `X` twice to cancel it; the first press opens a confirmation and `Esc` keeps it running. A paused feature preserves completed peers and same-wave checkpoints; select it and press `Ctrl+R` to resume the same task, turn, and native Worker session. After cancellation, already-running peers finish, queued workers stop, and integration remains blocked, so a partial wave never reaches the live workspace. Once the task settles as cancelled, `Ctrl+R` retries the cancelled task with persisted worker sessions; completed peers are loaded from their same-wave checkpoints instead of running again. Checkpoint load, reuse, and recovery events appear in the collaboration timeline.
420
+
421
+ For a failed, cancelled, or paused task, select an unfinished Feature and press `M` to edit its Provider assignment. `A` cycles that Feature's Actor through every `assignable` Worker Provider, `C` cycles its Critic, and `M` or `Esc` closes the assignment prompt. The choice is persisted in `features/<turn>-<feature>/assignment.json`, appears on the Feature board and collaboration artifacts, and takes effect on the next `Ctrl+R` retry. A changed role starts a new Provider-specific Worker while every previous Worker log and native-session record remains available. Assignment writes hold the same task lease as execution, so another live TUI cannot change a Feature while it is resuming.
365
422
 
366
423
  From Worker overview, press `C` to open the file-backed Actor/Critic collaboration timeline. It merges `dialogue/actor-critic.jsonl`, feature status, Critic findings, Actor replies, finding resolution, and Wave events into one chronological view. The timeline follows new file evidence automatically and shows verified `fixed`/`open` counts when resolution evidence exists. Up/Down selects a collaboration event; `Enter` opens its complete event detail, including artifact paths from dialogue, status, Critic findings, Actor replies, and finding resolution; `U` filters to unresolved feature evidence; `Tab` cycles all features and each individual feature; `R` refreshes immediately; the mouse wheel or PageUp/PageDown scrolls history; and `Esc` returns to Worker overview.
367
424
 
368
- `Ctrl+T` opens the workspace's persisted Task sessions without exiting the outer TUI. The list shows status, creation time, turn and worker counts, plus the number of distinct native sessions after deduplicating reused `engine + session_id` bindings; `>` marks the selected row and `*` marks the active task. Use Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` to select, Enter to restore, `R` to rename with Unicode-safe cursor editing, `A` to archive or unarchive, `D` twice to confirm deletion, `E` to export, `H` to show or hide archived sessions, `Ctrl+N` to start with an empty task context, and `Esc` to return without losing the chat draft. Archived sessions are hidden by default, cannot be restored until unarchived, and are excluded from automatic startup selection. Archive, delete, and export require a terminal task with no live task lease; the active task cannot be archived or deleted. Exports are complete file-backed snapshots under `.parallel-codex/exports/<task>-<timestamp>/` with a versioned manifest and without transient lease files. Restoring a task reloads its route, workers, retry state, and recorded native session ids. The selected active task is stored in `session-index.sqlite` and survives process and workspace switches; `Ctrl+N` persists an intentionally empty active-task context instead of reopening an older task on restart. Existing SQLite catalogs gain the nullable archive column automatically, while `meta.json` remains the rebuild authority.
425
+ `Ctrl+T` opens the workspace's persisted Task sessions without exiting the outer TUI. The list shows status, creation time, turn and worker counts, plus the number of distinct native sessions after deduplicating reused `engine + session_id` bindings; `>` marks the selected row and `*` marks the active task. Use Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` to select, Enter to restore, `I` to inspect the complete session hierarchy, `R` to rename with Unicode-safe cursor editing, `A` to archive or unarchive, `D` twice to confirm deletion, `E` to export, `H` to show or hide archived sessions, `Ctrl+N` to start with an empty task context, and `Esc` to return without losing the chat draft. Archived sessions are hidden by default, cannot be restored until unarchived, and are excluded from automatic startup selection. Archive, delete, and export require a terminal task with no live task lease; the active task cannot be archived or deleted. Exports are complete file-backed snapshots under `.parallel-codex/exports/<task>-<timestamp>/` with a versioned manifest and without transient lease files. Restoring a task reloads its route, workers, retry state, and recorded native session ids. The selected active task is stored in `session-index.sqlite` and survives process and workspace switches; `Ctrl+N` persists an intentionally empty active-task context instead of reopening an older task on restart. SQLite schema changes run as ordered, transactional migrations. Existing catalogs receive a pre-migration snapshot, each successful startup/reindex refreshes a healthy backup, and an integrity failure restores that backup or rebuilds the catalog from authoritative task files while preserving the corrupt copy for inspection. `meta.json` and the Worker files remain the rebuild authority.
426
+
427
+ The Task detail view reads authoritative task files and presents `Project -> Task -> Turn -> Worker -> Native session`, including each request, role, engine, configured model, Worker state, native session id, working directory, and last activity time. Historical Workers from earlier turns remain selectable after same-task follow-ups. Use Up/Down or `Tab` to select a Worker, Enter to open its retained log, `C` or `Ctrl+O` to continue the original native session, `B` to ask the native CLI to fork it, `R` to refresh, and `Esc` to return to the Task list. Continue and fork restore the selected Worker's recorded cwd and writable roots while the outer TUI stays open. Codex and Claude have built-in fork commands; a generic CLI exposes `B` only when `interactive.forkArgs` is configured. The child fork is owned and persisted by the native CLI; the indexed parent session remains unchanged.
369
428
 
370
429
  In a Worker log, `Ctrl+F` searches the final rendered Worker log rather than raw file offsets. Type Unicode text normally; Enter moves to the next match and Up/Down moves backward or forward. `Esc` closes search. The current match is marked with `>` without shifting Diff or source line-number columns. With search closed, press `E` to cycle rendered error lines or `D` to cycle Diff files and hunks.
371
430
 
372
- In chat or worker-log views, press `Ctrl+O` to attach to the selected worker's native session inside `parallel-codex-tui`. Native attach runs through a PTY, so full-screen CLIs such as `codex resume {sessionId}` receive a real terminal instead of pipe stdin. Native attach follows outer terminal resize, updating both the rendered screen and child PTY so Codex or Claude reflows at the current dimensions. Input is forwarded to the configured interactive command and output is shown in the native attach panel. When a Codex feature session needs its recorded worker, turn, or feature directories and the interactive command does not already choose a sandbox, native attach adds `--sandbox workspace-write` before its `--add-dir` arguments; an explicitly configured native sandbox remains unchanged. Overlapping attach preparations keep only the latest request. Press `Ctrl+]` to return to worker logs; this and closing the outer App terminate the active PTY. An attach preparation that finishes after App shutdown is discarded instead of starting a detached agent. `Ctrl+C` is forwarded to the native agent while attached. In chat and worker-log views, press `Ctrl+C` to exit the outer TUI. An operating-system SIGINT first asks the App to abort its Router or Worker and clean up any PTY; a second SIGINT restores terminal modes and forces exit.
431
+ In chat or worker-log views, press `Ctrl+O` to attach to the selected worker's native session inside `parallel-codex-tui`. Native attach runs through a PTY, so full-screen CLIs such as `codex resume {sessionId}` receive a real terminal instead of pipe stdin. Native attach follows outer terminal resize, updating both the rendered screen and child PTY so Codex or Claude reflows at the current dimensions. Input is forwarded to the configured interactive command and output is shown in the native attach panel. When a Codex feature session needs its recorded worker, turn, or feature directories and the interactive command does not already choose a sandbox, native attach adds `--sandbox workspace-write` before its `--add-dir` arguments; an explicitly configured native sandbox remains unchanged. A non-zero native exit recognizes read-only sandbox/add-dir conflicts, untrusted directories, misplaced `--skip-git-repo-check`, missing PTY input, and host permission failures, then appends the exact configuration or terminal action before the exit line scrolls away. Overlapping attach preparations keep only the latest request. Press `Ctrl+]` to return to worker logs; this and closing the outer App terminate the active PTY. An attach preparation that finishes after App shutdown is discarded instead of starting a detached agent. `Ctrl+C` is forwarded to the native agent while attached. In chat and worker-log views, press `Ctrl+C` to exit the outer TUI. An operating-system SIGINT first asks the App to abort its Router or Worker and clean up any PTY; a second SIGINT restores terminal modes and forces exit.
373
432
 
374
433
  If a native resume fails because the underlying CLI reports that its context window is full, configure `fallback = "new"` under `[workers.<engine>.nativeSession]`. The old native session is archived as `native-session.retired.json`, removed from active use, and the worker is retried once with the normal fresh-session command. A valid retirement tombstone is also a cross-turn inheritance barrier: a retry or later turn cannot resurrect the same session from an older worker copy, while a newer replacement session remains reusable. A `process-cleanup-error` or `process-ownership-error` always blocks native-session fallback, even when the CLI output also mentions a full context window, so a fresh Worker cannot overlap an untracked or still-running resume process.
375
434
 
@@ -387,16 +446,16 @@ npm trust github parallel-codex-tui --repo allendred/parallel-codex-tui --file r
387
446
 
388
447
  Creating or listing trusted publishers may require npm two-factor authentication in the browser.
389
448
 
390
- The release job installs npm `^11.5.1`, runs on Node `26.x`, publishes the prepared tarball through OIDC, waits for the package to become visible on npm, installs it globally in a temporary prefix, and checks `parallel-codex-tui --version` before creating the GitHub Release. If npm returns `ENEEDAUTH` or `E401`, fix the npm Trusted Publishing package settings rather than adding a token fallback.
449
+ The release job installs npm `^11.5.1`, runs on Node `24.15.x`, publishes the prepared tarball through OIDC, waits for the package to become visible on npm, installs it globally in a temporary prefix, and checks `parallel-codex-tui --version` before creating the GitHub Release. If npm returns `ENEEDAUTH` or `E401`, fix the npm Trusted Publishing package settings rather than adding a token fallback.
391
450
 
392
451
  To publish a release, update `package.json` and `src/version.ts` to the same version, then push a matching tag:
393
452
 
394
453
  ```bash
395
- git tag v0.1.4
396
- git push origin v0.1.4
454
+ git tag v0.1.5
455
+ git push origin v0.1.5
397
456
  ```
398
457
 
399
- You can also run the Release workflow manually and enter the same tag value. The release tag must match `package.json`; for example, package version `0.1.4` requires tag `v0.1.4`.
458
+ You can also run the Release workflow manually and enter the same tag value. The release tag must match `package.json`; for example, package version `0.1.5` requires tag `v0.1.5`.
400
459
 
401
460
  ## Publishing Hygiene
402
461
 
@@ -0,0 +1,18 @@
1
+ export function startupPreflightMessages(preflight) {
2
+ if (preflight.ok) {
3
+ return [];
4
+ }
5
+ const issues = preflight.lines.filter(isStartupPreflightIssue);
6
+ if (issues.length === 0) {
7
+ return [];
8
+ }
9
+ const summary = issues.slice(0, 4).join(" · ");
10
+ const remainder = issues.length > 4 ? ` · ${issues.length - 4} more` : "";
11
+ return [{
12
+ from: "system",
13
+ text: `Startup preflight needs attention · ${summary}${remainder} · run parallel-codex-tui --doctor before starting workers`
14
+ }];
15
+ }
16
+ function isStartupPreflightIssue(line) {
17
+ return /(?:^|: )(?:warning\b|missing\b|incompatible\b|unreachable\b|invalid\b|denied\b|failed\b)/i.test(line);
18
+ }
@@ -1,9 +1,21 @@
1
- export function startupRecoveryMessages(recoveredTasks, activeTaskId, pendingTaskCreations) {
1
+ export function startupRecoveryMessages(recoveredTasks, activeTaskId, pendingTaskCreations, sessionIndexRecovery) {
2
2
  return [
3
+ ...sessionIndexRecoveryMessages(sessionIndexRecovery),
3
4
  ...interruptedTaskRecoveryMessages(recoveredTasks, activeTaskId),
4
5
  ...pendingTaskCreationMessages(pendingTaskCreations)
5
6
  ];
6
7
  }
8
+ function sessionIndexRecoveryMessages(recovery) {
9
+ if (!recovery) {
10
+ return [];
11
+ }
12
+ return [{
13
+ from: "system",
14
+ text: recovery.source === "backup"
15
+ ? `Recovered session catalog from the last healthy SQLite backup · task files reindexed · corrupt copy kept at ${recovery.quarantinedPath}`
16
+ : `Rebuilt session catalog from task files after SQLite integrity failure · corrupt copy kept at ${recovery.quarantinedPath}`
17
+ }];
18
+ }
7
19
  function interruptedTaskRecoveryMessages(recoveredTasks, activeTaskId) {
8
20
  if (recoveredTasks.length === 0) {
9
21
  return [];
package/dist/cli.js CHANGED
@@ -8,14 +8,16 @@ import { selectWorkspaceForCli } from "./cli-workspace.js";
8
8
  import { commitWorkspaceTransition } from "./cli-workspace-transition.js";
9
9
  import { WorkspaceSelectionCancelledError } from "./cli-workspace-picker.js";
10
10
  import { startupRecoveryMessages } from "./cli-startup-recovery.js";
11
+ import { startupPreflightMessages } from "./cli-startup-preflight.js";
11
12
  import { createRuntime } from "./bootstrap.js";
12
13
  import { prepareAppRoot } from "./core/app-root.js";
13
14
  import { formatConfigErrorMessage } from "./core/config-errors.js";
14
15
  import { configPath, loadConfig, withUiThemeOverride, writeDefaultConfig } from "./core/config.js";
15
16
  import { pathExists } from "./core/file-store.js";
16
17
  import { readRouterAudit } from "./core/router-audit.js";
18
+ import { loadTaskSessionDetails as loadPersistedTaskSessionDetails } from "./core/task-session-details.js";
17
19
  import { listWorkspaceChoices } from "./core/workspace.js";
18
- import { runDoctor } from "./doctor.js";
20
+ import { runDoctor, runRuntimePreflight } from "./doctor.js";
19
21
  import { helpText } from "./cli-help.js";
20
22
  import { App } from "./tui/App.js";
21
23
  import { formatTuiThemeCatalog } from "./tui/theme-preview.js";
@@ -102,7 +104,11 @@ async function main() {
102
104
  records,
103
105
  policy: routerDiagnosticsPolicy(latestConfig.router)
104
106
  };
105
- }, loadTaskSessions: (options) => state.runtime.index.listTasks(100, options), renameTaskSession: async (taskId, title) => {
107
+ }, loadTaskSessions: (options) => state.runtime.index.listTasks(100, options), loadTaskSessionDetails: (task) => loadPersistedTaskSessionDetails({
108
+ task,
109
+ taskDir: state.runtime.sessions.taskFromId(task.id).dir,
110
+ modelNames: Object.fromEntries(Object.entries(state.runtime.config.workers).map(([id, worker]) => [id, worker.model.name]))
111
+ }), renameTaskSession: async (taskId, title) => {
106
112
  await state.runtime.sessions.renameTask(taskId, title);
107
113
  }, setTaskSessionArchived: async (taskId, archived) => {
108
114
  await state.runtime.sessions.setTaskArchived(taskId, archived);
@@ -165,6 +171,10 @@ async function main() {
165
171
  async function loadInteractiveWorkspace(appRoot, workspaceRoot, requestedTaskId) {
166
172
  const runtime = await createRuntime(appRoot, workspaceRoot);
167
173
  try {
174
+ const preflightPromise = runRuntimePreflight(runtime.config, runtime.workspaceRoot, process.env).catch((error) => ({
175
+ ok: false,
176
+ lines: [`preflight: failed (${error instanceof Error ? error.message : String(error)})`]
177
+ }));
168
178
  if (requestedTaskId) {
169
179
  if (!(await runtime.sessions.hasTask(requestedTaskId))) {
170
180
  throw new Error(`Task session not found in workspace ${runtime.workspaceRoot}: ${requestedTaskId}`);
@@ -200,16 +210,17 @@ async function loadInteractiveWorkspace(appRoot, workspaceRoot, requestedTaskId)
200
210
  else if (!initialTaskId && typeof rememberedTaskId === "string") {
201
211
  await runtime.index.setActiveTaskId(null);
202
212
  }
203
- const [initialRoute, initialWorkers, initialCanRetryTask, initialHistory, workspaceChoices] = await Promise.all([
213
+ const [initialRoute, initialWorkers, initialCanRetryTask, initialHistory, workspaceChoices, preflight] = await Promise.all([
204
214
  initialTaskId
205
215
  ? runtime.sessions.readLatestRoute(runtime.sessions.taskFromId(initialTaskId))
206
216
  : null,
207
217
  initialTaskId ? runtime.orchestrator.listTaskWorkers(initialTaskId) : [],
208
218
  initialTaskId ? runtime.orchestrator.canRetryTask(initialTaskId) : false,
209
219
  runtime.sessions.readChatHistory(),
210
- listWorkspaceChoices(appRoot)
220
+ listWorkspaceChoices(appRoot),
221
+ preflightPromise
211
222
  ]);
212
- const recoveryMessages = startupRecoveryMessages(runtime.recoveredTasks, initialTaskId, runtime.pendingTaskCreations);
223
+ const recoveryMessages = startupRecoveryMessages(runtime.recoveredTasks, initialTaskId, runtime.pendingTaskCreations, runtime.index.recovery);
213
224
  return {
214
225
  runtime,
215
226
  initialTaskId,
@@ -222,7 +233,8 @@ async function loadInteractiveWorkspace(appRoot, workspaceRoot, requestedTaskId)
222
233
  text,
223
234
  ...(task_id ? { taskId: task_id } : {})
224
235
  })),
225
- ...recoveryMessages
236
+ ...recoveryMessages,
237
+ ...startupPreflightMessages(preflight)
226
238
  ],
227
239
  workspaceChoices
228
240
  };
@@ -1,7 +1,7 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
- import { EventRecordSchema, FeatureStatusSchema } from "../domain/schemas.js";
4
+ import { EventRecordSchema, FeatureAssignmentSchema, FeatureStatusSchema } from "../domain/schemas.js";
5
5
  import { readTextIfExists } from "./file-store.js";
6
6
  const FeatureDialogueSchema = z.object({
7
7
  time: z.string().datetime(),
@@ -107,8 +107,9 @@ async function readCollaborationFeatures(taskDir) {
107
107
  .filter((entry) => entry.isDirectory())
108
108
  .map(async (entry) => {
109
109
  const dir = join(root, entry.name);
110
- const [statusText, spec, findings, replies, resolutionText] = await Promise.all([
110
+ const [statusText, assignmentText, spec, findings, replies, resolutionText] = await Promise.all([
111
111
  readTextIfExists(join(dir, "status.json")),
112
+ readTextIfExists(join(dir, "assignment.json")),
112
113
  readTextIfExists(join(dir, "spec.md")),
113
114
  readTextIfExists(join(dir, "critic-findings.jsonl")),
114
115
  readTextIfExists(join(dir, "actor-replies.jsonl")),
@@ -121,6 +122,7 @@ async function readCollaborationFeatures(taskDir) {
121
122
  const findingEvidence = readMailboxEvidence(findings);
122
123
  const replyEvidence = readMailboxEvidence(replies);
123
124
  const resolution = parseJsonValue(resolutionText, FindingResolutionSchema);
125
+ const assignment = parseJsonValue(assignmentText, FeatureAssignmentSchema);
124
126
  return {
125
127
  id: status.feature_id,
126
128
  title: status.title?.trim() || featureSpecTitle(spec) || status.feature_id,
@@ -129,11 +131,16 @@ async function readCollaborationFeatures(taskDir) {
129
131
  turnId: status.turn_id,
130
132
  state: status.state,
131
133
  updatedAt: status.updated_at,
134
+ ...(assignment ? {
135
+ actorEngine: assignment.actor_engine,
136
+ criticEngine: assignment.critic_engine
137
+ } : {}),
132
138
  findings: findingEvidence.count,
133
139
  replies: replyEvidence.count,
134
140
  artifactRefs: [
135
141
  { label: "status", path: join(dir, "status.json") },
136
142
  { label: "spec", path: join(dir, "spec.md") },
143
+ ...(assignment ? [{ label: "assignment", path: join(dir, "assignment.json") }] : []),
137
144
  { label: "critic findings", path: join(dir, "critic-findings.jsonl") },
138
145
  { label: "actor replies", path: join(dir, "actor-replies.jsonl") },
139
146
  ...(resolution ? [{ label: "finding resolution", path: join(dir, "finding-resolution.json") }] : [])