parallel-codex-tui 0.4.1 → 0.4.2

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/README.md CHANGED
@@ -6,7 +6,7 @@ Built with Codex-assisted development.
6
6
 
7
7
  ## Current Release
8
8
 
9
- `v0.4.1` is available from [npm](https://www.npmjs.com/package/parallel-codex-tui/v/0.4.1) and as a [GitHub Release](https://github.com/allendred/parallel-codex-tui/releases/tag/v0.4.1). It adds a non-interactive Supervisor control plane: `--runs` lists persisted background runs, `--runs --json` provides versioned machine-readable status, and `--cancel-run [id]` cancels a selected or latest active run without opening the TUI. The status payload excludes requests, prompts, logs, command arguments, and environment values.
9
+ `v0.4.2` is available from [npm](https://www.npmjs.com/package/parallel-codex-tui/v/0.4.2) and as a [GitHub Release](https://github.com/allendred/parallel-codex-tui/releases/tag/v0.4.2). It extends the non-interactive Supervisor control plane with `--wait-run [id]` and `--wait-timeout <seconds>`. A second terminal or automation can now wait for a detached run, receive a versioned JSON result, and distinguish completion, failure, cancellation, stale ownership, and local wait timeout without taking the TUI control lease or cancelling the underlying task.
10
10
 
11
11
  Main and parallel executions run in a detached, per-request Supervisor after the Router resolves in the foreground. `Ctrl+C` closes the outer TUI without stopping an attached run, reopening the same workspace restores its live events or terminal result, and `Esc` remains the explicit cancellation command. One TUI owns the control lease while additional TUIs observe the same run; an observer takes control automatically after the prior controller detaches or exits.
12
12
 
@@ -39,7 +39,7 @@ Highlights:
39
39
  - 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.
40
40
  - Worker overview, Feature board, collaboration timeline, Task and Main conversation centers, status details, rendered Markdown/Diff/error logs, Unicode search, keyboard navigation, mouse scrolling, and configurable themes share one terminal UI system.
41
41
 
42
- Release acceptance includes a real three-Feature Tetris task with parallel Actor/Critic waves and final integration review. A clean `v0.2.5` task also ran Codex Judge and Actor, a buffered Claude Critic that independently executed `node --test` and `npm test`, atomic integration, and a resumed Codex Judge that independently passed all seven acceptance criteria. Real Codex and Claude probes both proved fresh and same-session resume calls; Codex fresh and resume runs executed workspace writes with root-level `-a never`, and Claude automated sessions executed safe Bash tools with `auto` permissions. The semantic Router completed a live classification, and one TUI completed Main calls in two workspaces before restoring the first workspace without leaking chat state. PTY coverage runs in Apple Terminal, tmux, and Zellij profiles at narrow and wide sizes, including status/log equivalence, preserving the native output tail across status-detail round trips, and proving both inline and on-demand file memory after native-session rollover. Supervisor PTY coverage proves detached Main and complex execution, restart recovery, controller/observer takeover, explicit cancellation, process-owner crash recovery, and out-of-process status/cancellation commands. The deterministic repository suite contains 1,363 tests across 143 files: 1,362 pass by default, while one quota-consuming real-Agent test is skipped and passes through `npm run test:real-agents`.
42
+ Release acceptance includes a real three-Feature Tetris task with parallel Actor/Critic waves and final integration review. A clean `v0.2.5` task also ran Codex Judge and Actor, a buffered Claude Critic that independently executed `node --test` and `npm test`, atomic integration, and a resumed Codex Judge that independently passed all seven acceptance criteria. Real Codex and Claude probes both proved fresh and same-session resume calls; Codex fresh and resume runs executed workspace writes with root-level `-a never`, and Claude automated sessions executed safe Bash tools with `auto` permissions. The semantic Router completed a live classification, and one TUI completed Main calls in two workspaces before restoring the first workspace without leaking chat state. PTY coverage runs in Apple Terminal, tmux, and Zellij profiles at narrow and wide sizes, including status/log equivalence, preserving the native output tail across status-detail round trips, and proving both inline and on-demand file memory after native-session rollover. Supervisor PTY coverage proves detached Main and complex execution, restart recovery, controller/observer takeover, explicit cancellation, process-owner crash recovery, out-of-process status/cancellation, and detached wait/timeout commands. The deterministic repository suite contains 1,368 tests across 143 files: 1,367 pass by default, while one quota-consuming real-Agent test is skipped and passes through `npm run test:real-agents`.
43
43
 
44
44
  Real Provider probes 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.
45
45
 
@@ -70,6 +70,8 @@ parallel-codex-tui --diagnostics ./support-bundle
70
70
  parallel-codex-tui --workspace /path/to/project --runs
71
71
  parallel-codex-tui --workspace /path/to/project --runs --json
72
72
  parallel-codex-tui --workspace /path/to/project --cancel-run
73
+ parallel-codex-tui --workspace /path/to/project --wait-run
74
+ parallel-codex-tui --workspace /path/to/project --wait-run --wait-timeout 600 --json
73
75
  parallel-codex-tui --workspace /path/to/project
74
76
  parallel-codex-tui --theme aurora --workspace /path/to/project
75
77
  parallel-codex-tui --theme studio --workspace /path/to/project
@@ -116,6 +118,8 @@ parallel-codex-tui --workspace /path/to/project --runs
116
118
  parallel-codex-tui --workspace /path/to/project --runs --json
117
119
  parallel-codex-tui --workspace /path/to/project --cancel-run
118
120
  parallel-codex-tui --workspace /path/to/project --cancel-run run-20260721T000000Z-deadbeef
121
+ parallel-codex-tui --workspace /path/to/project --wait-run
122
+ parallel-codex-tui --workspace /path/to/project --wait-run run-20260721T000000Z-deadbeef --wait-timeout 600 --json
119
123
  parallel-codex-tui --version
120
124
  ```
121
125
 
@@ -155,6 +159,18 @@ parallel-codex-tui --workspace /path/to/project --cancel-run run-20260721T000000
155
159
 
156
160
  Cancellation appends a checked command to the run's existing Supervisor command stream. It does not signal Worker PIDs directly, so the normal orchestrator cancellation, process-tree cleanup, terminal state, chat persistence, and retry evidence remain authoritative. An explicit administrative cancellation can be sent while a TUI is observing or controlling the run. Missing, stale, and already-terminal targets fail without creating a new Workspace or changing historical run evidence.
157
161
 
162
+ Wait for the latest unfinished run, or address one explicitly:
163
+
164
+ ```bash
165
+ parallel-codex-tui --workspace /path/to/project --wait-run
166
+ parallel-codex-tui --workspace /path/to/project --wait-run run-20260721T000000Z-deadbeef
167
+ parallel-codex-tui --workspace /path/to/project --wait-run run-20260721T000000Z-deadbeef --wait-timeout 600 --json
168
+ ```
169
+
170
+ When no unfinished run exists, `--wait-run` returns the newest persisted terminal run immediately. The waiter is read-only: it does not claim `controller.json`, append to `commands.jsonl`, acknowledge the result, read the original request, or inspect Worker output. `--wait-timeout` accepts positive decimal seconds and stops only the waiting CLI; the Supervisor and Workers continue normally. `Ctrl+C` likewise exits the waiter without cancelling the run.
171
+
172
+ Wait results use exit code `0` for `completed`, `1` for `failed`, `2` for `cancelled`, `3` for `stale`, and `4` for `timeout`. Text and JSON results contain only the same bounded lifecycle metadata as `--runs`; JSON uses schema version `1`. This makes shell automation able to distinguish task outcomes while keeping requests, prompts, logs, command arguments, and environment values out of its output.
173
+
158
174
  ## Quick Start
159
175
 
160
176
  Create a local config in the app root:
@@ -579,12 +595,12 @@ The release job installs npm `^11.5.1`, runs on Node `24.15.x`, publishes the pr
579
595
  To publish a release, update `package.json` and `src/version.ts` to the same version, then push a matching tag:
580
596
 
581
597
  ```bash
582
- VERSION=0.4.1
598
+ VERSION=0.4.2
583
599
  git tag "v$VERSION"
584
600
  git push origin "v$VERSION"
585
601
  ```
586
602
 
587
- 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.4.1` requires tag `v0.4.1`. Published tags such as `v0.2.10` are immutable and must not be moved or reused.
603
+ 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.4.2` requires tag `v0.4.2`. Published tags such as `v0.2.10` are immutable and must not be moved or reused.
588
604
 
589
605
  ## Publishing Hygiene
590
606
 
package/dist/cli-args.js CHANGED
@@ -10,7 +10,9 @@ const allowedValueOptions = new Set([
10
10
  "-t",
11
11
  "--theme",
12
12
  "--diagnostics",
13
- "--cancel-run"
13
+ "--cancel-run",
14
+ "--wait-run",
15
+ "--wait-timeout"
14
16
  ]);
15
17
  const allowedBooleanOptions = new Set([
16
18
  "--doctor",
@@ -35,6 +37,14 @@ export function parseCliArgs(args, cwd) {
35
37
  const cancelRunFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--cancel-run" || arg.startsWith("--cancel-run="));
36
38
  const cancelRun = cancelRunFlagIndex >= 0;
37
39
  const cancelRunId = flagValue(optionArgs, cancelRunFlagIndex);
40
+ const waitRunFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--wait-run" || arg.startsWith("--wait-run="));
41
+ const waitTimeoutFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--wait-timeout" || arg.startsWith("--wait-timeout="));
42
+ const waitRun = waitRunFlagIndex >= 0;
43
+ const waitRunId = flagValue(optionArgs, waitRunFlagIndex);
44
+ const waitTimeoutSeconds = Number(flagValue(optionArgs, waitTimeoutFlagIndex));
45
+ const waitTimeoutMs = waitTimeoutFlagIndex >= 0 && Number.isFinite(waitTimeoutSeconds)
46
+ ? waitTimeoutSeconds * 1000
47
+ : null;
38
48
  const diagnostics = diagnosticsFlagIndex >= 0;
39
49
  const doctor = optionArgs.includes("--doctor");
40
50
  const help = optionArgs.includes("--help") || optionArgs.includes("-h");
@@ -67,6 +77,9 @@ export function parseCliArgs(args, cwd) {
67
77
  probeAgents,
68
78
  probeRouter,
69
79
  runs,
80
+ waitRun,
81
+ waitRunId,
82
+ waitTimeoutMs,
70
83
  workspaceRoot,
71
84
  taskId,
72
85
  theme,
@@ -120,15 +133,33 @@ export function validateCliArgs(args) {
120
133
  const cancelRunFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--cancel-run" || arg.startsWith("--cancel-run="));
121
134
  const cancelRun = cancelRunFlagIndex >= 0;
122
135
  const cancelRunId = flagValue(optionArgs, cancelRunFlagIndex);
123
- if (runs && cancelRun) {
124
- errors.push("--runs and --cancel-run cannot be used together");
136
+ const waitRunFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--wait-run" || arg.startsWith("--wait-run="));
137
+ const waitRun = waitRunFlagIndex >= 0;
138
+ const waitRunId = flagValue(optionArgs, waitRunFlagIndex);
139
+ const supervisorCommandCount = [runs, cancelRun, waitRun].filter(Boolean).length;
140
+ if (supervisorCommandCount > 1) {
141
+ errors.push("Only one of --runs, --cancel-run, or --wait-run may be used");
125
142
  }
126
- if (optionArgs.includes("--json") && !runs && !cancelRun) {
127
- errors.push("--json requires --runs or --cancel-run");
143
+ if (optionArgs.includes("--json") && supervisorCommandCount === 0) {
144
+ errors.push("--json requires --runs, --cancel-run, or --wait-run");
128
145
  }
129
146
  if (cancelRunId && !/^run-[A-Za-z0-9._-]+$/.test(cancelRunId)) {
130
147
  errors.push("Invalid --cancel-run: expected run- followed by letters, numbers, dot, underscore, or hyphen");
131
148
  }
149
+ if (waitRunId && !/^run-[A-Za-z0-9._-]+$/.test(waitRunId)) {
150
+ errors.push("Invalid --wait-run: expected run- followed by letters, numbers, dot, underscore, or hyphen");
151
+ }
152
+ const waitTimeoutFlagIndex = lastFlagIndex(optionArgs, (arg) => arg === "--wait-timeout" || arg.startsWith("--wait-timeout="));
153
+ if (waitTimeoutFlagIndex >= 0) {
154
+ const rawWaitTimeout = flagValue(optionArgs, waitTimeoutFlagIndex);
155
+ const waitTimeoutSeconds = Number(rawWaitTimeout);
156
+ if (!rawWaitTimeout || !Number.isFinite(waitTimeoutSeconds) || waitTimeoutSeconds <= 0) {
157
+ errors.push("Invalid --wait-timeout: expected a positive number of seconds");
158
+ }
159
+ if (!waitRun) {
160
+ errors.push("--wait-timeout requires --wait-run");
161
+ }
162
+ }
132
163
  return errors;
133
164
  }
134
165
  function argsBeforeTerminator(args) {
package/dist/cli-help.js CHANGED
@@ -13,7 +13,9 @@ Options:
13
13
  --diagnostics [dir] Export a sanitized support bundle; defaults inside the workspace
14
14
  --runs List persisted Supervisor runs without opening the TUI
15
15
  --cancel-run [id] Cancel a run by id, or the latest active run when omitted
16
- --json Emit machine-readable output with --runs or --cancel-run
16
+ --wait-run [id] Wait for a run by id, or the latest unfinished run
17
+ --wait-timeout <s> Stop waiting after this many seconds; never cancels the run
18
+ --json Emit machine-readable Supervisor command output
17
19
  --probe-agents With --doctor, run fresh + resume probes (uses model quota)
18
20
  --probe-router With --doctor, run one live Codex Router request
19
21
  -v, --version Print the current version
package/dist/cli.js CHANGED
@@ -28,7 +28,7 @@ import { configureTuiTheme } from "./tui/theme.js";
28
28
  import { routerDiagnosticsPolicy } from "./tui/RouterDiagnosticsView.js";
29
29
  import { version } from "./version.js";
30
30
  import { SupervisorOrchestrator } from "./supervisor/client.js";
31
- import { formatSupervisorCancellation, formatSupervisorRuns, inspectSupervisorRuns, requestSupervisorRunCancellation } from "./supervisor/operations.js";
31
+ import { formatSupervisorCancellation, formatSupervisorRuns, formatSupervisorWait, inspectSupervisorRuns, requestSupervisorRunCancellation, supervisorWaitExitCode, waitForSupervisorRun } from "./supervisor/operations.js";
32
32
  import { runSupervisorJob } from "./supervisor/runner.js";
33
33
  main().catch((error) => {
34
34
  if (error instanceof WorkspaceSelectionCancelledError) {
@@ -100,7 +100,7 @@ async function main() {
100
100
  runtime.index.close();
101
101
  }
102
102
  }
103
- else if (cliArgs.runs || cliArgs.cancelRun) {
103
+ else if (cliArgs.runs || cliArgs.cancelRun || cliArgs.waitRun) {
104
104
  const workspaceRoot = await selectWorkspaceForCli({
105
105
  appRoot: cliArgs.appRoot,
106
106
  cwd: process.cwd(),
@@ -113,10 +113,15 @@ async function main() {
113
113
  const report = await inspectSupervisorRuns(workspaceRoot, config.dataDir);
114
114
  console.log(cliArgs.json ? JSON.stringify(report, null, 2) : formatSupervisorRuns(report));
115
115
  }
116
- else {
116
+ else if (cliArgs.cancelRun) {
117
117
  const result = await requestSupervisorRunCancellation(workspaceRoot, config.dataDir, cliArgs.cancelRunId);
118
118
  console.log(cliArgs.json ? JSON.stringify(result, null, 2) : formatSupervisorCancellation(result));
119
119
  }
120
+ else {
121
+ const result = await waitForSupervisorRun(workspaceRoot, config.dataDir, cliArgs.waitRunId, { timeoutMs: cliArgs.waitTimeoutMs });
122
+ console.log(cliArgs.json ? JSON.stringify(result, null, 2) : formatSupervisorWait(result));
123
+ process.exitCode = supervisorWaitExitCode(result.outcome);
124
+ }
120
125
  }
121
126
  catch (error) {
122
127
  const message = error instanceof Error ? error.message : String(error);
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { appendSupervisorCommand, listSupervisorRuns, readSupervisorController, supervisorControllerIsActive, supervisorRunIsAcknowledged, supervisorRunIsTerminal, supervisorRunProcessIsActive } from "./store.js";
2
+ import { appendSupervisorCommand, listSupervisorRuns, readSupervisorController, readSupervisorRunState, supervisorControllerIsActive, supervisorRunIsAcknowledged, supervisorRunIsTerminal, supervisorRunProcessIsActive } from "./store.js";
3
3
  const QUEUED_START_GRACE_MS = 5000;
4
4
  export async function inspectSupervisorRuns(workspaceRoot, dataDir, now = new Date()) {
5
5
  const records = await listSupervisorRuns(workspaceRoot, dataDir);
@@ -52,6 +52,64 @@ export async function requestSupervisorRunCancellation(workspaceRoot, dataDir, r
52
52
  run: selected.view
53
53
  };
54
54
  }
55
+ export async function waitForSupervisorRun(workspaceRoot, dataDir, runId, options = {}) {
56
+ const startedAt = Date.now();
57
+ const now = options.now ?? (() => new Date());
58
+ const requestedPollIntervalMs = options.pollIntervalMs ?? 100;
59
+ if (!Number.isFinite(requestedPollIntervalMs) || requestedPollIntervalMs <= 0) {
60
+ throw new Error("Supervisor wait poll interval must be a positive number of milliseconds");
61
+ }
62
+ const pollIntervalMs = Math.max(10, requestedPollIntervalMs);
63
+ const timeoutMs = options.timeoutMs ?? null;
64
+ if (timeoutMs !== null && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
65
+ throw new Error("Supervisor wait timeout must be a positive number of milliseconds");
66
+ }
67
+ const records = await listSupervisorRuns(workspaceRoot, dataDir);
68
+ const inspected = await Promise.all(records.map(async (record) => ({
69
+ record,
70
+ view: await inspectSupervisorRun(record, now())
71
+ })));
72
+ const newestFirst = inspected.reverse();
73
+ const selected = runId
74
+ ? newestFirst.find(({ view }) => view.run_id === runId)
75
+ : newestFirst.find(({ view }) => !isTerminalStatus(view.status)) ?? newestFirst[0];
76
+ if (!selected) {
77
+ throw new Error(runId
78
+ ? `Supervisor run not found: ${runId}`
79
+ : `No Supervisor runs in workspace ${workspaceRoot}`);
80
+ }
81
+ while (true) {
82
+ const view = await inspectSupervisorRun(selected.record, now());
83
+ const waitedMs = Math.max(0, Date.now() - startedAt);
84
+ if (isTerminalStatus(view.status)) {
85
+ return {
86
+ version: 1,
87
+ outcome: view.status,
88
+ waited_ms: waitedMs,
89
+ run: view
90
+ };
91
+ }
92
+ if (view.control === "stale") {
93
+ return {
94
+ version: 1,
95
+ outcome: "stale",
96
+ waited_ms: waitedMs,
97
+ run: view
98
+ };
99
+ }
100
+ if (timeoutMs !== null && waitedMs >= timeoutMs) {
101
+ return {
102
+ version: 1,
103
+ outcome: "timeout",
104
+ waited_ms: waitedMs,
105
+ run: view
106
+ };
107
+ }
108
+ const remainingTimeoutMs = timeoutMs === null ? pollIntervalMs : Math.max(1, timeoutMs - waitedMs);
109
+ await delay(Math.min(pollIntervalMs, remainingTimeoutMs));
110
+ selected.record.state = await readSupervisorRunState(selected.record.files);
111
+ }
112
+ }
55
113
  export function formatSupervisorRuns(report) {
56
114
  if (report.runs.length === 0) {
57
115
  return `No Supervisor runs in ${report.workspace_root}.`;
@@ -72,6 +130,24 @@ export function formatSupervisorCancellation(result) {
72
130
  const target = result.run.task_id ? `task ${result.run.task_id}` : result.run.kind;
73
131
  return `Cancellation requested · ${result.run.run_id} · ${target}`;
74
132
  }
133
+ export function formatSupervisorWait(result) {
134
+ const target = result.run.task_id ? `task ${result.run.task_id}` : result.run.kind;
135
+ return `Run ${result.outcome} · ${result.run.run_id} · ${target} · waited ${formatDuration(result.waited_ms)}`;
136
+ }
137
+ export function supervisorWaitExitCode(outcome) {
138
+ switch (outcome) {
139
+ case "completed":
140
+ return 0;
141
+ case "failed":
142
+ return 1;
143
+ case "cancelled":
144
+ return 2;
145
+ case "stale":
146
+ return 3;
147
+ case "timeout":
148
+ return 4;
149
+ }
150
+ }
75
151
  async function inspectSupervisorRun(record, now) {
76
152
  const controller = await readSupervisorController(record.files);
77
153
  const [processActive, controllerActive, acknowledged] = await Promise.all([
@@ -114,3 +190,12 @@ function controlState(record, processActive, controllerActive, now) {
114
190
  function isTerminalStatus(status) {
115
191
  return status === "completed" || status === "failed" || status === "cancelled";
116
192
  }
193
+ function formatDuration(milliseconds) {
194
+ if (milliseconds < 1000) {
195
+ return `${milliseconds}ms`;
196
+ }
197
+ return `${(milliseconds / 1000).toFixed(milliseconds < 10000 ? 1 : 0)}s`;
198
+ }
199
+ function delay(milliseconds) {
200
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
201
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.4.1";
1
+ export const version = "0.4.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parallel-codex-tui",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "A TypeScript TUI wrapper for routed parallel coding with Codex and Claude workers.",
5
5
  "license": "MIT",
6
6
  "type": "module",