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
@@ -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
@@ -4,6 +4,23 @@ A standalone TypeScript TUI wrapper for routed parallel coding workflows. It kee
4
4
 
5
5
  Built with Codex-assisted development.
6
6
 
7
+ ## Current Release
8
+
9
+ `v0.1.6` is available from [npm](https://www.npmjs.com/package/parallel-codex-tui/v/0.1.6) and as a [GitHub Release](https://github.com/allendred/parallel-codex-tui/releases/tag/v0.1.6). It keeps terminal scrolling and copying available at the same time: mouse reporting now matches Claude Code's click/wheel protocol (`1000` + `1006`) without enabling drag-event tracking (`1002`), so normal terminal selection and system copy remain available while the app receives wheel input. `Ctrl+Y` is an additional fallback that copies the visible chat, rendered Worker log, native Agent screen, or structured overview without changing mouse mode. Unicode text and Diff indentation are preserved.
10
+
11
+ The release also includes the stability, collaboration, session-management, Provider, TUI, and release work completed in `v0.1.5`:
12
+
13
+ - Runtime preflight checks Codex, Claude, named third-party Providers, proxy reachability, workspace access, CLI capabilities, and native attach policy before work starts.
14
+ - Judge produces validated requirements, acceptance criteria, and a dependency-aware Feature plan. Actor/Critic pairs exchange checked JSONL findings and replies across revision rounds, followed by Wave Critic and final Judge acceptance.
15
+ - Tasks retain every Turn, Worker log, native session binding, model snapshot, and collaboration artifact across follow-ups and restarts. Sessions can be inspected, continued, forked, renamed, archived, exported, restored, or deleted from the TUI.
16
+ - Named Worker Providers support Codex-compatible, Claude-compatible, OpenAI-compatible, Anthropic-compatible, and custom generic commands with independent role, model, environment, permission, resume, and interactive settings.
17
+ - Worker overview, Feature board, collaboration timeline, status details, rendered Markdown/Diff/error logs, Unicode search, keyboard navigation, mouse scrolling, and configurable themes share one terminal UI system.
18
+ - SQLite migrations and recovery backups, owned-process cleanup, ten-turn Worker-history recovery, packaged CLI installation, and cross-platform CI are covered by automated tests.
19
+
20
+ Release acceptance included a real three-Feature Tetris task with parallel Actor/Critic waves, final integration review, 43 project tests, and a clean build. The repository suite passed 1,240 tests across 119 files, including PTY coverage proving wheel delivery without drag-event tracking. The npm package was independently installed and returned `parallel-codex-tui 0.1.6`.
21
+
22
+ Real Provider probes still depend on valid local CLI credentials. In particular, authenticate the Claude CLI before selecting a Claude-compatible Worker, then run `parallel-codex-tui --doctor --probe-agents` to prove fresh and resumed calls on that machine.
23
+
7
24
  ## Requirements
8
25
 
9
26
  - Node.js 24.15+.
@@ -39,6 +56,7 @@ Startup resolves the worker project before routing:
39
56
  - 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
57
  - 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
58
  - The selected workspace is prepared before any router or worker process starts.
59
+ - 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
60
  - 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
61
  - 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
62
 
@@ -68,7 +86,7 @@ parallel-codex-tui --doctor --probe-router
68
86
  parallel-codex-tui --version
69
87
  ```
70
88
 
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.
89
+ `--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
90
 
73
91
  ## Quick Start
74
92
 
@@ -107,10 +125,13 @@ Limit how many feature-level Actor or Critic agents may run at once:
107
125
  ```toml
108
126
  [orchestration]
109
127
  maxParallelFeatures = 3 # 1..8
128
+ maxRevisionRounds = 3 # 1..10
110
129
  ```
111
130
 
112
131
  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
132
 
133
+ 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.
134
+
114
135
  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
136
 
116
137
  ## Theme
@@ -262,7 +283,7 @@ ALL_PROXY = "socks5h://127.0.0.1:7890"
262
283
  NO_PROXY = "localhost,127.0.0.1"
263
284
  ```
264
285
 
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.
286
+ `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
287
 
267
288
  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
289
 
@@ -302,6 +323,7 @@ freshSessionArgs = []
302
323
  [workers.codex.interactive]
303
324
  command = "codex"
304
325
  args = ["resume", "{sessionId}"]
326
+ forkArgs = ["fork", "{sessionId}"]
305
327
 
306
328
  [workers.claude]
307
329
  command = "claude"
@@ -315,31 +337,81 @@ freshSessionArgs = ["--session-id", "{sessionId}"]
315
337
  [workers.claude.interactive]
316
338
  command = "claude"
317
339
  args = ["--resume", "{sessionId}"]
340
+ forkArgs = ["--resume", "{sessionId}", "--fork-session"]
318
341
  ```
319
342
 
320
343
  `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
344
 
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:
345
+ `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.
346
+
347
+ 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
348
 
324
349
  ```toml
325
- [workers.codex]
350
+ [workers.openai_compat]
351
+ extends = "codex"
352
+ command = "openai-compatible-coder"
353
+ args = ["exec", "--sandbox", "workspace-write", "-"]
354
+
355
+ [workers.openai_compat.model]
356
+ name = "third-party-code-model"
357
+ provider = "openai-compatible"
358
+ args = ["--model", "{model}", "--provider", "{provider}"]
359
+
360
+ [workers.openai_compat.model.env]
361
+ OPENAI_API_KEY = "{env:OPENAI_COMPAT_API_KEY}"
362
+ OPENAI_BASE_URL = "{env:OPENAI_COMPAT_BASE_URL}"
363
+
364
+ [workers.openai_compat.interactive]
365
+ command = "openai-compatible-coder"
366
+
367
+ [workers.anthropic_compat]
368
+ extends = "claude"
369
+ command = "anthropic-compatible-coder"
370
+
371
+ [workers.anthropic_compat.model]
372
+ name = "third-party-claude-model"
373
+ provider = "anthropic-compatible"
374
+
375
+ [workers.anthropic_compat.model.env]
376
+ ANTHROPIC_API_KEY = "{env:ANTHROPIC_COMPAT_API_KEY}"
377
+ ANTHROPIC_BASE_URL = "{env:ANTHROPIC_COMPAT_BASE_URL}"
378
+
379
+ [workers.anthropic_compat.interactive]
380
+ command = "anthropic-compatible-coder"
381
+
382
+ [pairing]
383
+ main = "anthropic_compat"
384
+ judge = "openai_compat"
385
+ actor = "openai_compat"
386
+ critic = "anthropic_compat"
387
+ ```
388
+
389
+ `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.
390
+
391
+ 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:
392
+
393
+ ```toml
394
+ [workers.vendor]
395
+ extends = "generic"
326
396
  command = "vendor-coder"
327
397
  args = ["run", "--stdin"]
328
398
 
329
- [workers.codex.capabilities]
399
+ [workers.vendor.capabilities]
330
400
  profile = "generic"
331
401
  writableDirArgs = ["--allow-root", "{dir}"]
332
402
  freshSessionArgs = ["--new-session", "{sessionId}"]
333
403
 
334
- [workers.codex.nativeSession]
404
+ [workers.vendor.nativeSession]
405
+ enabled = true
335
406
  resumeArgs = ["run", "--resume", "{sessionId}", "--stdin"]
336
407
 
337
- [workers.codex.interactive]
408
+ [workers.vendor.interactive]
338
409
  command = "vendor-coder"
339
410
  args = ["resume", "{sessionId}"]
411
+ forkArgs = ["branch", "{sessionId}"]
340
412
  ```
341
413
 
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.
414
+ 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
415
 
344
416
  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
417
 
@@ -357,46 +429,51 @@ Keep `[router.codex]` on `read-only`; routing only classifies requests and does
357
429
 
358
430
  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
431
 
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.
432
+ In chat, scroll long conversation history with the mouse wheel or PageUp/PageDown; sending a new message returns to the latest reply. Mouse reporting matches Claude Code's click/wheel protocol and does not enable drag-event tracking, so ordinary terminal drag selection and system copy remain available while wheel scrolling stays active. `Ctrl+Y` also copies the current visible chat, rendered Worker log, native Agent screen, or structured overview without changing mouse mode. macOS uses `pbcopy`; Linux uses `wl-copy`, `xclip`, or `xsel` when available and falls back to OSC 52. 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.
433
+
434
+ `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.
361
435
 
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.
436
+ 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.
363
437
 
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.
438
+ 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
439
 
366
440
  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
441
 
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.
442
+ `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.
443
+
444
+ 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
445
 
370
446
  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
447
 
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.
448
+ 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
449
 
374
450
  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
451
 
376
452
  ## Release
377
453
 
378
- GitHub Actions runs CI on Ubuntu with Node.js 24.15 and 26, plus macOS with Node.js 26, for pushes and pull requests to `main`. Every job also packs the release artifact, installs that tarball into a clean global prefix, and executes the installed `--version` and `--help` entrypoints.
454
+ GitHub Actions runs CI on Ubuntu with Node.js 24.15 and 26, plus macOS with Node.js 24.15, for pushes and pull requests to `main`. Every job also packs the release artifact, installs that tarball into a clean global prefix, and executes the installed `--version` and `--help` entrypoints.
379
455
 
380
456
  Releases publish to npm through npm Trusted Publishing with GitHub OIDC. Do not configure `NPM_TOKEN` for the release workflow. In npm, configure Trusted Publishing for organization/user `allendred`, repository `parallel-codex-tui`, workflow filename `release.yml`, and allowed action `npm publish`. The package already exists on npm, so future releases can use Trusted Publishing directly. You can also configure the trust relationship from an authenticated npm CLI session:
381
457
 
382
458
  ```bash
383
- npm install -g npm@^11.15.0
459
+ npm install -g npm@^11.5.1
384
460
  npm trust github parallel-codex-tui --repo allendred/parallel-codex-tui --file release.yml --allow-publish --dry-run
385
461
  npm trust github parallel-codex-tui --repo allendred/parallel-codex-tui --file release.yml --allow-publish --yes
386
462
  ```
387
463
 
388
464
  Creating or listing trusted publishers may require npm two-factor authentication in the browser.
389
465
 
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.
466
+ 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
467
 
392
468
  To publish a release, update `package.json` and `src/version.ts` to the same version, then push a matching tag:
393
469
 
394
470
  ```bash
395
- git tag v0.1.4
396
- git push origin v0.1.4
471
+ VERSION=0.1.6
472
+ git tag "v$VERSION"
473
+ git push origin "v$VERSION"
397
474
  ```
398
475
 
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`.
476
+ 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.6` requires tag `v0.1.6`. Published tags such as `v0.1.5` are immutable and must not be moved or reused.
400
477
 
401
478
  ## Publishing Hygiene
402
479
 
@@ -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
  };
@@ -279,7 +291,7 @@ function restoreInteractiveTerminal() {
279
291
  }
280
292
  process.stdin.pause();
281
293
  if (process.stdout.isTTY) {
282
- process.stdout.write("\x1b[?1006l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[?25h");
294
+ process.stdout.write("\x1b[?1006l\x1b[?1000l\x1b[?2004l\x1b[?25h");
283
295
  }
284
296
  }
285
297
  function formatStartupError(error) {
@@ -0,0 +1,97 @@
1
+ import { spawn } from "node:child_process";
2
+ const MAX_CLIPBOARD_CHARACTERS = 200_000;
3
+ export async function copyTextToClipboard(text, options = {}) {
4
+ const value = normalizeClipboardText(text);
5
+ if (!value) {
6
+ throw new Error("No visible text to copy");
7
+ }
8
+ const platform = options.platform ?? process.platform;
9
+ const env = options.env ?? process.env;
10
+ const runCommand = options.runCommand ?? runClipboardCommand;
11
+ for (const candidate of clipboardCommands(platform, env)) {
12
+ try {
13
+ await runCommand(candidate.command, candidate.args, value);
14
+ return { method: candidate.method, characters: Array.from(value).length };
15
+ }
16
+ catch {
17
+ // Try the next local clipboard integration before falling back to OSC 52.
18
+ }
19
+ }
20
+ const writeTerminal = options.writeTerminal ?? ((sequence) => {
21
+ process.stdout.write(sequence);
22
+ });
23
+ writeTerminal(osc52ClipboardSequence(value));
24
+ return { method: "osc52", characters: Array.from(value).length };
25
+ }
26
+ export function normalizeClipboardText(text) {
27
+ const lines = text
28
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
29
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
30
+ .replace(/\r\n?/g, "\n")
31
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
32
+ .split("\n")
33
+ .map((line) => line.trimEnd());
34
+ while (lines[0] === "") {
35
+ lines.shift();
36
+ }
37
+ while (lines.at(-1) === "") {
38
+ lines.pop();
39
+ }
40
+ const clean = lines.join("\n");
41
+ return Array.from(clean).slice(0, MAX_CLIPBOARD_CHARACTERS).join("");
42
+ }
43
+ export function osc52ClipboardSequence(text) {
44
+ return `\x1b]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`;
45
+ }
46
+ function clipboardCommands(platform, env) {
47
+ if (platform === "darwin") {
48
+ return [{ method: "pbcopy", command: "/usr/bin/pbcopy", args: [] }];
49
+ }
50
+ if (platform !== "linux") {
51
+ return [];
52
+ }
53
+ return [
54
+ ...(env.WAYLAND_DISPLAY
55
+ ? [{ method: "wl-copy", command: "wl-copy", args: [] }]
56
+ : []),
57
+ ...(env.DISPLAY
58
+ ? [
59
+ { method: "xclip", command: "xclip", args: ["-selection", "clipboard"] },
60
+ { method: "xsel", command: "xsel", args: ["--clipboard", "--input"] }
61
+ ]
62
+ : [])
63
+ ];
64
+ }
65
+ function runClipboardCommand(command, args, text) {
66
+ return new Promise((resolve, reject) => {
67
+ const child = spawn(command, args, {
68
+ env: process.env,
69
+ stdio: ["pipe", "ignore", "pipe"]
70
+ });
71
+ let stderr = "";
72
+ let settled = false;
73
+ const finish = (error) => {
74
+ if (settled) {
75
+ return;
76
+ }
77
+ settled = true;
78
+ if (error) {
79
+ reject(error);
80
+ }
81
+ else {
82
+ resolve();
83
+ }
84
+ };
85
+ child.stderr?.on("data", (chunk) => {
86
+ stderr += String(chunk);
87
+ });
88
+ child.on("error", (error) => finish(error));
89
+ child.on("close", (code) => {
90
+ finish(code === 0
91
+ ? undefined
92
+ : new Error(`${command} exited ${code ?? "without a code"}${stderr.trim() ? `: ${stderr.trim()}` : ""}`));
93
+ });
94
+ child.stdin?.on("error", (error) => finish(error));
95
+ child.stdin?.end(text);
96
+ });
97
+ }
@@ -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") }] : [])