parallel-codex-tui 0.1.0
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 +62 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/bootstrap.js +25 -0
- package/dist/cli-args.js +26 -0
- package/dist/cli.js +48 -0
- package/dist/core/config.js +304 -0
- package/dist/core/file-store.js +56 -0
- package/dist/core/paths.js +17 -0
- package/dist/core/router.js +151 -0
- package/dist/core/session-index.js +180 -0
- package/dist/core/session-manager.js +264 -0
- package/dist/doctor.js +121 -0
- package/dist/domain/schemas.js +78 -0
- package/dist/orchestrator/collaboration-channel.js +103 -0
- package/dist/orchestrator/orchestrator.js +545 -0
- package/dist/orchestrator/prompts.js +124 -0
- package/dist/orchestrator/supervisor-summary.js +38 -0
- package/dist/tui/App.js +740 -0
- package/dist/tui/AppShell.js +129 -0
- package/dist/tui/InputBar.js +141 -0
- package/dist/tui/StatusBar.js +288 -0
- package/dist/tui/TerminalOutput.js +22 -0
- package/dist/tui/WorkerOutputView.js +4015 -0
- package/dist/tui/chat-input.js +37 -0
- package/dist/tui/display-width.js +132 -0
- package/dist/tui/keyboard.js +38 -0
- package/dist/tui/native-input.js +120 -0
- package/dist/tui/raw-input-decoder.js +18 -0
- package/dist/tui/scrolling.js +25 -0
- package/dist/tui/status-line.js +90 -0
- package/dist/tui/task-memory.js +26 -0
- package/dist/tui/terminal-screen.js +168 -0
- package/dist/version.js +1 -0
- package/dist/workers/mock-adapter.js +62 -0
- package/dist/workers/native-attach.js +189 -0
- package/dist/workers/process-adapter.js +265 -0
- package/dist/workers/registry.js +32 -0
- package/dist/workers/types.js +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[router]
|
|
2
|
+
defaultMode = "auto"
|
|
3
|
+
|
|
4
|
+
[router.codex]
|
|
5
|
+
command = "codex"
|
|
6
|
+
args = ["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"]
|
|
7
|
+
timeoutMs = 120000
|
|
8
|
+
fallback = "complex"
|
|
9
|
+
|
|
10
|
+
[workers.codex]
|
|
11
|
+
command = "codex"
|
|
12
|
+
args = ["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"]
|
|
13
|
+
firstOutputTimeoutMs = 120000
|
|
14
|
+
|
|
15
|
+
[workers.codex.interactive]
|
|
16
|
+
command = "codex"
|
|
17
|
+
args = ["resume", "{sessionId}"]
|
|
18
|
+
|
|
19
|
+
[workers.codex.nativeSession]
|
|
20
|
+
fallback = "new"
|
|
21
|
+
|
|
22
|
+
[workers.claude]
|
|
23
|
+
command = "claude"
|
|
24
|
+
args = ["--print", "--permission-mode", "acceptEdits", "--output-format", "text"]
|
|
25
|
+
firstOutputTimeoutMs = 120000
|
|
26
|
+
|
|
27
|
+
[workers.claude.interactive]
|
|
28
|
+
command = "claude"
|
|
29
|
+
args = ["--resume", "{sessionId}"]
|
|
30
|
+
|
|
31
|
+
[workers.claude.nativeSession]
|
|
32
|
+
fallback = "new"
|
|
33
|
+
|
|
34
|
+
[workers.mock]
|
|
35
|
+
command = "mock"
|
|
36
|
+
args = []
|
|
37
|
+
|
|
38
|
+
[pairing]
|
|
39
|
+
main = "claude"
|
|
40
|
+
judge = "codex"
|
|
41
|
+
actor = "codex"
|
|
42
|
+
critic = "codex"
|
|
43
|
+
|
|
44
|
+
[roles.main]
|
|
45
|
+
title = "Main"
|
|
46
|
+
instructions = ["Answer simple chat and status questions directly."]
|
|
47
|
+
|
|
48
|
+
[roles.judge]
|
|
49
|
+
title = "Judge"
|
|
50
|
+
instructions = ["Clarify requirements and write task files. Do not implement code."]
|
|
51
|
+
|
|
52
|
+
[roles.actor]
|
|
53
|
+
title = "Actor"
|
|
54
|
+
instructions = ["Read Judge files, implement the requested change, and record your work."]
|
|
55
|
+
|
|
56
|
+
[roles.critic]
|
|
57
|
+
title = "Critic"
|
|
58
|
+
instructions = ["Review Actor work against Judge requirements. Lead with blocking findings."]
|
|
59
|
+
|
|
60
|
+
[ui]
|
|
61
|
+
showStatusBar = true
|
|
62
|
+
autoOpenFailedWorker = true
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 parallel-codex-tui contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# parallel-codex-tui
|
|
2
|
+
|
|
3
|
+
A standalone TypeScript TUI wrapper for routed parallel coding workflows. It keeps a main chat open while Codex routes larger tasks into Judge, Actor, and Critic workers that can write prompts, logs, session metadata, and outputs to disk.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 22.5+.
|
|
8
|
+
- Codex CLI available as `codex` for Codex routing and Codex workers.
|
|
9
|
+
- Claude CLI available as `claude` only when you configure Claude workers.
|
|
10
|
+
- A project workspace you are comfortable letting configured workers edit.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g parallel-codex-tui
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Run it against the project you want the workers to operate on:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd /path/to/project
|
|
22
|
+
parallel-codex-tui --init
|
|
23
|
+
parallel-codex-tui --doctor
|
|
24
|
+
parallel-codex-tui --workspace /path/to/project
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
From a source checkout, install dependencies and link the local binary:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
npm run build
|
|
32
|
+
npm link
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For development without linking, run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run dev -- --workspace /path/to/project
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Check available flags or the installed version without starting the TUI:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
parallel-codex-tui --help
|
|
45
|
+
parallel-codex-tui --doctor
|
|
46
|
+
parallel-codex-tui --version
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
Create a local config in the app root:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
parallel-codex-tui --init
|
|
55
|
+
parallel-codex-tui --doctor
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
From a source checkout, you can also copy the public example:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cp .parallel-codex/config.example.toml .parallel-codex/config.toml
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For deterministic local testing without real agent CLIs, set all roles to mock workers in `.parallel-codex/config.toml`:
|
|
65
|
+
|
|
66
|
+
```toml
|
|
67
|
+
[pairing]
|
|
68
|
+
judge = "mock"
|
|
69
|
+
actor = "mock"
|
|
70
|
+
critic = "mock"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then start the TUI:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
parallel-codex-tui --workspace /path/to/project
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Behavior
|
|
80
|
+
|
|
81
|
+
- Requests are routed by Codex by default, with a configured simple/complex fallback if the router process fails.
|
|
82
|
+
- Simple requests stay in the main TUI flow and do not create Judge, Actor, or Critic workers.
|
|
83
|
+
- Complex requests create a session under `.parallel-codex/sessions/`.
|
|
84
|
+
- Complex requests run Judge -> Actor -> Critic.
|
|
85
|
+
- Worker prompts, logs, status, and outputs are written to disk.
|
|
86
|
+
- The bottom status line shows the active task state.
|
|
87
|
+
|
|
88
|
+
## Router
|
|
89
|
+
|
|
90
|
+
Codex routing is enabled by default:
|
|
91
|
+
|
|
92
|
+
```toml
|
|
93
|
+
[router]
|
|
94
|
+
defaultMode = "auto"
|
|
95
|
+
|
|
96
|
+
[router.codex]
|
|
97
|
+
command = "codex"
|
|
98
|
+
args = ["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"]
|
|
99
|
+
timeoutMs = 120000
|
|
100
|
+
fallback = "complex"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Set `defaultMode = "simple"` / `defaultMode = "complex"` to force one path. In `auto` mode, routing is semantic through Codex. If the router process fails or returns invalid JSON, `fallback = "simple"` or `fallback = "complex"` decides the path; there is no keyword-only router.
|
|
104
|
+
|
|
105
|
+
## Mock Mode
|
|
106
|
+
|
|
107
|
+
For deterministic local testing, use this pairing in `.parallel-codex/config.toml`:
|
|
108
|
+
|
|
109
|
+
```toml
|
|
110
|
+
[pairing]
|
|
111
|
+
judge = "mock"
|
|
112
|
+
actor = "mock"
|
|
113
|
+
critic = "mock"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Real Worker Mode
|
|
117
|
+
|
|
118
|
+
Configure Codex and Claude commands in `.parallel-codex/config.toml`:
|
|
119
|
+
|
|
120
|
+
```toml
|
|
121
|
+
[workers.codex]
|
|
122
|
+
command = "codex"
|
|
123
|
+
args = ["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"]
|
|
124
|
+
|
|
125
|
+
[workers.codex.interactive]
|
|
126
|
+
command = "codex"
|
|
127
|
+
args = ["resume", "{sessionId}"]
|
|
128
|
+
|
|
129
|
+
[workers.claude]
|
|
130
|
+
command = "claude"
|
|
131
|
+
args = ["--print", "--permission-mode", "acceptEdits", "--output-format", "text"]
|
|
132
|
+
|
|
133
|
+
[workers.claude.interactive]
|
|
134
|
+
command = "claude"
|
|
135
|
+
args = ["--resume", "{sessionId}"]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Keep `[router.codex]` on `workspace-write`; routing only classifies requests and does not need host-level access. If a trusted local project needs Docker, OrbStack, or other host services, opt into broader worker permissions in your private `.parallel-codex/config.toml` rather than committing them.
|
|
139
|
+
|
|
140
|
+
The process adapter sends each role prompt to stdin and records stdout/stderr in `output.log`.
|
|
141
|
+
|
|
142
|
+
While viewing a worker log, press `Ctrl+O` to attach to the 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. Input is forwarded to the configured interactive command and output is shown in the native attach panel. Press `Ctrl+]` to detach and return to worker logs; `Ctrl+C` is forwarded to the native agent while attached. In chat and worker-log views, press `Ctrl+C` to exit the outer TUI.
|
|
143
|
+
|
|
144
|
+
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.
|
|
145
|
+
|
|
146
|
+
## Publishing Hygiene
|
|
147
|
+
|
|
148
|
+
- `.parallel-codex/config.toml` is local-only and ignored.
|
|
149
|
+
- `.parallel-codex/sessions/` contains task prompts, logs, native session ids, and worker output; never commit it.
|
|
150
|
+
- `docs/superpowers/` contains internal planning notes and is ignored for public releases.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { loadConfig } from "./core/config.js";
|
|
2
|
+
import { SessionIndex } from "./core/session-index.js";
|
|
3
|
+
import { SessionManager } from "./core/session-manager.js";
|
|
4
|
+
import { Orchestrator } from "./orchestrator/orchestrator.js";
|
|
5
|
+
import { createWorkerRegistry } from "./workers/registry.js";
|
|
6
|
+
export async function createRuntime(appRoot, workspaceRoot = appRoot) {
|
|
7
|
+
const config = await loadConfig(appRoot);
|
|
8
|
+
const index = await SessionIndex.open(workspaceRoot, config.dataDir);
|
|
9
|
+
await index.rebuildFromFiles();
|
|
10
|
+
const sessions = new SessionManager({
|
|
11
|
+
projectRoot: workspaceRoot,
|
|
12
|
+
dataDir: config.dataDir,
|
|
13
|
+
index
|
|
14
|
+
});
|
|
15
|
+
const workers = createWorkerRegistry(config);
|
|
16
|
+
const orchestrator = new Orchestrator(config, sessions, workers);
|
|
17
|
+
return {
|
|
18
|
+
config,
|
|
19
|
+
workspaceRoot,
|
|
20
|
+
index,
|
|
21
|
+
sessions,
|
|
22
|
+
workers,
|
|
23
|
+
orchestrator
|
|
24
|
+
};
|
|
25
|
+
}
|
package/dist/cli-args.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
export function parseCliArgs(args, cwd) {
|
|
3
|
+
const appRootFlagIndex = args.findIndex((arg) => arg === "--app-root");
|
|
4
|
+
const workspaceFlagIndex = args.findIndex((arg) => arg === "--workspace" || arg === "-w");
|
|
5
|
+
const taskFlagIndex = args.findIndex((arg) => arg === "--task" || arg === "-t");
|
|
6
|
+
const doctor = args.includes("--doctor");
|
|
7
|
+
const help = args.includes("--help") || args.includes("-h");
|
|
8
|
+
const init = args.includes("--init");
|
|
9
|
+
const version = args.includes("--version") || args.includes("-v");
|
|
10
|
+
const appRoot = appRootFlagIndex >= 0 && args[appRootFlagIndex + 1]
|
|
11
|
+
? resolve(cwd, args[appRootFlagIndex + 1])
|
|
12
|
+
: cwd;
|
|
13
|
+
const workspaceRoot = workspaceFlagIndex >= 0 && args[workspaceFlagIndex + 1]
|
|
14
|
+
? resolve(cwd, args[workspaceFlagIndex + 1])
|
|
15
|
+
: cwd;
|
|
16
|
+
const taskId = taskFlagIndex >= 0 && args[taskFlagIndex + 1] ? args[taskFlagIndex + 1] : null;
|
|
17
|
+
return {
|
|
18
|
+
appRoot,
|
|
19
|
+
doctor,
|
|
20
|
+
help,
|
|
21
|
+
init,
|
|
22
|
+
workspaceRoot,
|
|
23
|
+
taskId,
|
|
24
|
+
version
|
|
25
|
+
};
|
|
26
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { parseCliArgs } from "./cli-args.js";
|
|
5
|
+
import { createRuntime } from "./bootstrap.js";
|
|
6
|
+
import { configPath, writeDefaultConfig } from "./core/config.js";
|
|
7
|
+
import { pathExists } from "./core/file-store.js";
|
|
8
|
+
import { runDoctor } from "./doctor.js";
|
|
9
|
+
import { App } from "./tui/App.js";
|
|
10
|
+
import { version } from "./version.js";
|
|
11
|
+
const helpText = `Usage: parallel-codex-tui [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-w, --workspace <path> Project workspace for worker sessions and edits
|
|
15
|
+
--app-root <path> App root for configuration lookup
|
|
16
|
+
-t, --task <id> Open an existing task session
|
|
17
|
+
--init Write .parallel-codex/config.toml if missing
|
|
18
|
+
--doctor Check local configuration and agent commands
|
|
19
|
+
-v, --version Print the current version
|
|
20
|
+
-h, --help Print this help message`;
|
|
21
|
+
const cliArgs = parseCliArgs(process.argv.slice(2), process.cwd());
|
|
22
|
+
const localConfigPath = configPath(cliArgs.appRoot);
|
|
23
|
+
if (cliArgs.help) {
|
|
24
|
+
console.log(helpText);
|
|
25
|
+
}
|
|
26
|
+
else if (cliArgs.version) {
|
|
27
|
+
console.log(`parallel-codex-tui ${version}`);
|
|
28
|
+
}
|
|
29
|
+
else if (cliArgs.doctor) {
|
|
30
|
+
const result = await runDoctor(cliArgs.appRoot, cliArgs.workspaceRoot);
|
|
31
|
+
process.stdout.write(result.text);
|
|
32
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
33
|
+
}
|
|
34
|
+
else if (cliArgs.init) {
|
|
35
|
+
if (await pathExists(localConfigPath)) {
|
|
36
|
+
console.log(`Config already exists: ${localConfigPath}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
await writeDefaultConfig(cliArgs.appRoot);
|
|
40
|
+
console.log(`Wrote ${localConfigPath}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const runtime = await createRuntime(cliArgs.appRoot, cliArgs.workspaceRoot);
|
|
45
|
+
const latestTask = await runtime.sessions.latestTask();
|
|
46
|
+
const initialTaskId = cliArgs.taskId ?? latestTask?.id ?? null;
|
|
47
|
+
render(_jsx(App, { config: runtime.config, orchestrator: runtime.orchestrator, cwd: runtime.workspaceRoot, initialTaskId: initialTaskId }), { exitOnCtrlC: false });
|
|
48
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { parse, stringify } from "@iarna/toml";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { pathExists, readTextIfExists, writeText } from "./file-store.js";
|
|
5
|
+
const NativeSessionConfigSchema = z.object({
|
|
6
|
+
enabled: z.boolean().default(true),
|
|
7
|
+
resumeArgs: z.array(z.string()).default([]),
|
|
8
|
+
detectSessionId: z.boolean().default(true),
|
|
9
|
+
fallback: z.enum(["fail", "new"]).default("fail")
|
|
10
|
+
});
|
|
11
|
+
const WorkerModelConfigSchema = z.object({
|
|
12
|
+
name: z.string().default(""),
|
|
13
|
+
provider: z.string().default(""),
|
|
14
|
+
args: z.array(z.string()).default([]),
|
|
15
|
+
env: z.record(z.string()).default({})
|
|
16
|
+
});
|
|
17
|
+
const InteractiveCommandSchema = z.object({
|
|
18
|
+
command: z.string().min(1),
|
|
19
|
+
args: z.array(z.string()).default([])
|
|
20
|
+
});
|
|
21
|
+
const RolePromptConfigSchema = z.object({
|
|
22
|
+
title: z.string().min(1),
|
|
23
|
+
instructions: z.array(z.string()).default([])
|
|
24
|
+
});
|
|
25
|
+
const CodexRouterConfigSchema = z.object({
|
|
26
|
+
command: z.string().min(1).default("codex"),
|
|
27
|
+
args: z.array(z.string()).default(["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"]),
|
|
28
|
+
timeoutMs: z.number().int().positive().default(120000),
|
|
29
|
+
fallback: z.enum(["simple", "complex"]).default("complex")
|
|
30
|
+
});
|
|
31
|
+
const WorkerCommandSchema = z.object({
|
|
32
|
+
command: z.string().min(1),
|
|
33
|
+
args: z.array(z.string()).default([]),
|
|
34
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
35
|
+
idleTimeoutMs: z.number().int().positive().optional(),
|
|
36
|
+
firstOutputTimeoutMs: z.number().int().positive().optional(),
|
|
37
|
+
model: WorkerModelConfigSchema.default({}),
|
|
38
|
+
nativeSession: NativeSessionConfigSchema,
|
|
39
|
+
interactive: InteractiveCommandSchema
|
|
40
|
+
});
|
|
41
|
+
const AppConfigSchema = z.object({
|
|
42
|
+
projectRoot: z.string().min(1),
|
|
43
|
+
dataDir: z.string().min(1),
|
|
44
|
+
router: z.object({
|
|
45
|
+
defaultMode: z.enum(["auto", "simple", "complex"]),
|
|
46
|
+
codex: CodexRouterConfigSchema.default({})
|
|
47
|
+
}),
|
|
48
|
+
workers: z.object({
|
|
49
|
+
codex: WorkerCommandSchema,
|
|
50
|
+
claude: WorkerCommandSchema,
|
|
51
|
+
mock: WorkerCommandSchema
|
|
52
|
+
}),
|
|
53
|
+
pairing: z.object({
|
|
54
|
+
main: z.enum(["codex", "claude", "mock"]),
|
|
55
|
+
judge: z.enum(["codex", "claude", "mock"]),
|
|
56
|
+
actor: z.enum(["codex", "claude", "mock"]),
|
|
57
|
+
critic: z.enum(["codex", "claude", "mock"])
|
|
58
|
+
}),
|
|
59
|
+
roles: z.object({
|
|
60
|
+
main: RolePromptConfigSchema,
|
|
61
|
+
judge: RolePromptConfigSchema,
|
|
62
|
+
actor: RolePromptConfigSchema,
|
|
63
|
+
critic: RolePromptConfigSchema
|
|
64
|
+
}),
|
|
65
|
+
ui: z.object({
|
|
66
|
+
showStatusBar: z.boolean(),
|
|
67
|
+
autoOpenFailedWorker: z.boolean()
|
|
68
|
+
})
|
|
69
|
+
});
|
|
70
|
+
export function defaultConfig(projectRoot) {
|
|
71
|
+
return {
|
|
72
|
+
projectRoot,
|
|
73
|
+
dataDir: ".parallel-codex",
|
|
74
|
+
router: {
|
|
75
|
+
defaultMode: "auto",
|
|
76
|
+
codex: {
|
|
77
|
+
command: "codex",
|
|
78
|
+
args: ["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"],
|
|
79
|
+
timeoutMs: 120000,
|
|
80
|
+
fallback: "complex"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
workers: {
|
|
84
|
+
codex: {
|
|
85
|
+
command: "codex",
|
|
86
|
+
args: ["exec", "--skip-git-repo-check", "--sandbox", "workspace-write", "--color", "never", "-"],
|
|
87
|
+
timeoutMs: 45 * 60 * 1000,
|
|
88
|
+
idleTimeoutMs: 5 * 60 * 1000,
|
|
89
|
+
firstOutputTimeoutMs: 2 * 60 * 1000,
|
|
90
|
+
model: {
|
|
91
|
+
name: "",
|
|
92
|
+
provider: "",
|
|
93
|
+
args: [],
|
|
94
|
+
env: {}
|
|
95
|
+
},
|
|
96
|
+
nativeSession: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
resumeArgs: ["exec", "resume", "{sessionId}", "--skip-git-repo-check", "-"],
|
|
99
|
+
detectSessionId: true,
|
|
100
|
+
fallback: "new"
|
|
101
|
+
},
|
|
102
|
+
interactive: {
|
|
103
|
+
command: "codex",
|
|
104
|
+
args: ["resume", "{sessionId}"]
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
claude: {
|
|
108
|
+
command: "claude",
|
|
109
|
+
args: ["--print", "--permission-mode", "acceptEdits", "--output-format", "text"],
|
|
110
|
+
timeoutMs: 45 * 60 * 1000,
|
|
111
|
+
idleTimeoutMs: 5 * 60 * 1000,
|
|
112
|
+
firstOutputTimeoutMs: 2 * 60 * 1000,
|
|
113
|
+
model: {
|
|
114
|
+
name: "",
|
|
115
|
+
provider: "",
|
|
116
|
+
args: [],
|
|
117
|
+
env: {}
|
|
118
|
+
},
|
|
119
|
+
nativeSession: {
|
|
120
|
+
enabled: true,
|
|
121
|
+
resumeArgs: ["--print", "--resume", "{sessionId}", "--permission-mode", "acceptEdits", "--output-format", "text"],
|
|
122
|
+
detectSessionId: true,
|
|
123
|
+
fallback: "new"
|
|
124
|
+
},
|
|
125
|
+
interactive: {
|
|
126
|
+
command: "claude",
|
|
127
|
+
args: ["--resume", "{sessionId}"]
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
mock: {
|
|
131
|
+
command: "mock",
|
|
132
|
+
args: [],
|
|
133
|
+
model: {
|
|
134
|
+
name: "",
|
|
135
|
+
provider: "",
|
|
136
|
+
args: [],
|
|
137
|
+
env: {}
|
|
138
|
+
},
|
|
139
|
+
nativeSession: {
|
|
140
|
+
enabled: true,
|
|
141
|
+
resumeArgs: ["resume", "{sessionId}", "-"],
|
|
142
|
+
detectSessionId: true,
|
|
143
|
+
fallback: "fail"
|
|
144
|
+
},
|
|
145
|
+
interactive: {
|
|
146
|
+
command: "mock",
|
|
147
|
+
args: ["resume", "{sessionId}"]
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
pairing: {
|
|
152
|
+
main: "claude",
|
|
153
|
+
judge: "codex",
|
|
154
|
+
actor: "codex",
|
|
155
|
+
critic: "codex"
|
|
156
|
+
},
|
|
157
|
+
roles: {
|
|
158
|
+
main: {
|
|
159
|
+
title: "Main",
|
|
160
|
+
instructions: ["Answer the user directly for simple chat and explanation requests."]
|
|
161
|
+
},
|
|
162
|
+
judge: {
|
|
163
|
+
title: "Judge",
|
|
164
|
+
instructions: ["You clarify requirements and write task files. Do not implement code."]
|
|
165
|
+
},
|
|
166
|
+
actor: {
|
|
167
|
+
title: "Actor",
|
|
168
|
+
instructions: ["Read Judge files, implement the requested change, and record your work."]
|
|
169
|
+
},
|
|
170
|
+
critic: {
|
|
171
|
+
title: "Critic",
|
|
172
|
+
instructions: ["Review Actor work against Judge requirements. Lead with blocking findings."]
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
ui: {
|
|
176
|
+
showStatusBar: true,
|
|
177
|
+
autoOpenFailedWorker: true
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function configPath(projectRoot) {
|
|
182
|
+
return join(projectRoot, ".parallel-codex", "config.toml");
|
|
183
|
+
}
|
|
184
|
+
export async function loadConfig(projectRoot) {
|
|
185
|
+
const base = defaultConfig(projectRoot);
|
|
186
|
+
const file = configPath(projectRoot);
|
|
187
|
+
if (!(await pathExists(file))) {
|
|
188
|
+
return base;
|
|
189
|
+
}
|
|
190
|
+
const parsed = parse(await readTextIfExists(file));
|
|
191
|
+
const merged = {
|
|
192
|
+
...base,
|
|
193
|
+
...parsed,
|
|
194
|
+
projectRoot,
|
|
195
|
+
router: {
|
|
196
|
+
...base.router,
|
|
197
|
+
...(parsed.router ?? {}),
|
|
198
|
+
codex: {
|
|
199
|
+
...base.router.codex,
|
|
200
|
+
...(parsed.router?.codex ?? {})
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
workers: {
|
|
204
|
+
codex: {
|
|
205
|
+
...base.workers.codex,
|
|
206
|
+
...(parsed.workers?.codex ?? {}),
|
|
207
|
+
model: {
|
|
208
|
+
...base.workers.codex.model,
|
|
209
|
+
...(parsed.workers?.codex?.model ?? {}),
|
|
210
|
+
env: {
|
|
211
|
+
...base.workers.codex.model.env,
|
|
212
|
+
...(parsed.workers?.codex?.model?.env ?? {})
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
nativeSession: {
|
|
216
|
+
...base.workers.codex.nativeSession,
|
|
217
|
+
...(parsed.workers?.codex?.nativeSession ?? {})
|
|
218
|
+
},
|
|
219
|
+
interactive: {
|
|
220
|
+
...base.workers.codex.interactive,
|
|
221
|
+
...(parsed.workers?.codex?.interactive ?? {})
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
claude: {
|
|
225
|
+
...base.workers.claude,
|
|
226
|
+
...(parsed.workers?.claude ?? {}),
|
|
227
|
+
model: {
|
|
228
|
+
...base.workers.claude.model,
|
|
229
|
+
...(parsed.workers?.claude?.model ?? {}),
|
|
230
|
+
env: {
|
|
231
|
+
...base.workers.claude.model.env,
|
|
232
|
+
...(parsed.workers?.claude?.model?.env ?? {})
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
nativeSession: {
|
|
236
|
+
...base.workers.claude.nativeSession,
|
|
237
|
+
...(parsed.workers?.claude?.nativeSession ?? {})
|
|
238
|
+
},
|
|
239
|
+
interactive: {
|
|
240
|
+
...base.workers.claude.interactive,
|
|
241
|
+
...(parsed.workers?.claude?.interactive ?? {})
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
mock: {
|
|
245
|
+
...base.workers.mock,
|
|
246
|
+
...(parsed.workers?.mock ?? {}),
|
|
247
|
+
model: {
|
|
248
|
+
...base.workers.mock.model,
|
|
249
|
+
...(parsed.workers?.mock?.model ?? {}),
|
|
250
|
+
env: {
|
|
251
|
+
...base.workers.mock.model.env,
|
|
252
|
+
...(parsed.workers?.mock?.model?.env ?? {})
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
nativeSession: {
|
|
256
|
+
...base.workers.mock.nativeSession,
|
|
257
|
+
...(parsed.workers?.mock?.nativeSession ?? {})
|
|
258
|
+
},
|
|
259
|
+
interactive: {
|
|
260
|
+
...base.workers.mock.interactive,
|
|
261
|
+
...(parsed.workers?.mock?.interactive ?? {})
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
pairing: {
|
|
266
|
+
...base.pairing,
|
|
267
|
+
...(parsed.pairing ?? {})
|
|
268
|
+
},
|
|
269
|
+
roles: {
|
|
270
|
+
main: {
|
|
271
|
+
...base.roles.main,
|
|
272
|
+
...(parsed.roles?.main ?? {})
|
|
273
|
+
},
|
|
274
|
+
judge: {
|
|
275
|
+
...base.roles.judge,
|
|
276
|
+
...(parsed.roles?.judge ?? {})
|
|
277
|
+
},
|
|
278
|
+
actor: {
|
|
279
|
+
...base.roles.actor,
|
|
280
|
+
...(parsed.roles?.actor ?? {})
|
|
281
|
+
},
|
|
282
|
+
critic: {
|
|
283
|
+
...base.roles.critic,
|
|
284
|
+
...(parsed.roles?.critic ?? {})
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
ui: {
|
|
288
|
+
...base.ui,
|
|
289
|
+
...(parsed.ui ?? {})
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
return AppConfigSchema.parse(merged);
|
|
293
|
+
}
|
|
294
|
+
export async function writeDefaultConfig(projectRoot) {
|
|
295
|
+
const config = defaultConfig(projectRoot);
|
|
296
|
+
const tomlText = stringify({
|
|
297
|
+
router: config.router,
|
|
298
|
+
workers: config.workers,
|
|
299
|
+
pairing: config.pairing,
|
|
300
|
+
roles: config.roles,
|
|
301
|
+
ui: config.ui
|
|
302
|
+
});
|
|
303
|
+
await writeText(configPath(projectRoot), tomlText);
|
|
304
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
export async function ensureDir(path) {
|
|
4
|
+
await mkdir(path, { recursive: true });
|
|
5
|
+
}
|
|
6
|
+
export async function pathExists(path) {
|
|
7
|
+
try {
|
|
8
|
+
await stat(path);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error.code === "ENOENT") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function writeJson(path, value) {
|
|
19
|
+
const dir = dirname(path);
|
|
20
|
+
const tempPath = join(dir, `.${basename(path)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
21
|
+
await ensureDir(dir);
|
|
22
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
23
|
+
await rename(tempPath, path);
|
|
24
|
+
}
|
|
25
|
+
export async function readJson(path, schema) {
|
|
26
|
+
const text = await readFile(path, "utf8");
|
|
27
|
+
return schema.parse(JSON.parse(text));
|
|
28
|
+
}
|
|
29
|
+
export async function appendJsonLine(path, value) {
|
|
30
|
+
await ensureDir(dirname(path));
|
|
31
|
+
await writeFile(path, `${JSON.stringify(value)}\n`, { encoding: "utf8", flag: "a" });
|
|
32
|
+
}
|
|
33
|
+
export async function writeText(path, value) {
|
|
34
|
+
await ensureDir(dirname(path));
|
|
35
|
+
await writeFile(path, value, "utf8");
|
|
36
|
+
}
|
|
37
|
+
export async function appendText(path, value) {
|
|
38
|
+
await ensureDir(dirname(path));
|
|
39
|
+
await writeFile(path, value, { encoding: "utf8", flag: "a" });
|
|
40
|
+
}
|
|
41
|
+
export async function readTextIfExists(path) {
|
|
42
|
+
if (!(await pathExists(path))) {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
return readFile(path, "utf8");
|
|
46
|
+
}
|
|
47
|
+
export async function removeIfExists(path) {
|
|
48
|
+
try {
|
|
49
|
+
await unlink(path);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code !== "ENOENT") {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|