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.
- package/.parallel-codex/config.example.toml +46 -0
- package/README.md +76 -17
- package/dist/cli-startup-preflight.js +18 -0
- package/dist/cli-startup-recovery.js +13 -1
- package/dist/cli.js +18 -6
- package/dist/core/collaboration-timeline.js +9 -2
- package/dist/core/config.js +161 -103
- package/dist/core/router.js +1 -1
- package/dist/core/session-index.js +234 -61
- package/dist/core/session-manager.js +25 -1
- package/dist/core/task-session-details.js +175 -0
- package/dist/core/task-state-machine.js +10 -9
- package/dist/doctor.js +58 -39
- package/dist/domain/schemas.js +15 -1
- package/dist/orchestrator/collaboration-channel.js +35 -3
- package/dist/orchestrator/final-acceptance.js +86 -0
- package/dist/orchestrator/orchestrator.js +405 -69
- package/dist/orchestrator/prompts.js +42 -3
- package/dist/orchestrator/workspace-sandbox.js +16 -0
- package/dist/tui/App.js +401 -52
- package/dist/tui/AppShell.js +9 -3
- package/dist/tui/FeatureBoardView.js +7 -2
- package/dist/tui/InputBar.js +82 -15
- package/dist/tui/StatusBar.js +1 -1
- package/dist/tui/StatusDetailView.js +164 -0
- package/dist/tui/TaskSessionDetailView.js +222 -0
- package/dist/tui/TaskSessionsView.js +5 -2
- package/dist/tui/WorkerOutputView.js +1 -0
- package/dist/tui/WorkerOverviewView.js +23 -4
- package/dist/tui/keyboard.js +3 -0
- package/dist/tui/status-line.js +42 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +4 -3
- package/dist/workers/live-probe.js +4 -3
- package/dist/workers/mock-adapter.js +37 -5
- package/dist/workers/native-attach.js +32 -17
- package/dist/workers/process-adapter.js +2 -0
- package/dist/workers/provider.js +26 -0
- package/dist/workers/registry.js +12 -22
- 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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
382
|
+
[workers.vendor.capabilities]
|
|
330
383
|
profile = "generic"
|
|
331
384
|
writableDirArgs = ["--allow-root", "{dir}"]
|
|
332
385
|
freshSessionArgs = ["--new-session", "{sessionId}"]
|
|
333
386
|
|
|
334
|
-
[workers.
|
|
387
|
+
[workers.vendor.nativeSession]
|
|
388
|
+
enabled = true
|
|
335
389
|
resumeArgs = ["run", "--resume", "{sessionId}", "--stdin"]
|
|
336
390
|
|
|
337
|
-
[workers.
|
|
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}`,
|
|
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
|
|
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 `
|
|
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
|
|
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 `
|
|
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.
|
|
396
|
-
git push origin v0.1.
|
|
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.
|
|
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),
|
|
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") }] : [])
|