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
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { configPath, loadConfig } from "./core/config.js";
|
|
5
|
+
import { pathExists } from "./core/file-store.js";
|
|
6
|
+
export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
|
|
7
|
+
const lines = ["parallel-codex-tui doctor"];
|
|
8
|
+
let ok = true;
|
|
9
|
+
if (isSupportedNodeVersion(process.versions.node)) {
|
|
10
|
+
lines.push(`Node.js: ok (${process.versions.node})`);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
ok = false;
|
|
14
|
+
lines.push(`Node.js: unsupported (${process.versions.node}; need 22.5+)`);
|
|
15
|
+
}
|
|
16
|
+
if (await pathExists(workspaceRoot)) {
|
|
17
|
+
lines.push(`workspace: ok (${workspaceRoot})`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
ok = false;
|
|
21
|
+
lines.push(`workspace: missing (${workspaceRoot})`);
|
|
22
|
+
}
|
|
23
|
+
const localConfigPath = configPath(appRoot);
|
|
24
|
+
if (!(await pathExists(localConfigPath))) {
|
|
25
|
+
ok = false;
|
|
26
|
+
lines.push(`config: missing (${localConfigPath}; run parallel-codex-tui --init)`);
|
|
27
|
+
return {
|
|
28
|
+
ok,
|
|
29
|
+
text: `${lines.join("\n")}\n`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
let config;
|
|
33
|
+
try {
|
|
34
|
+
config = await loadConfig(appRoot);
|
|
35
|
+
lines.push(`config: ok (${localConfigPath})`);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
ok = false;
|
|
39
|
+
lines.push(`config: invalid (${localConfigPath}; ${error.message})`);
|
|
40
|
+
return {
|
|
41
|
+
ok,
|
|
42
|
+
text: `${lines.join("\n")}\n`
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
for (const command of configuredCommands(config)) {
|
|
46
|
+
if (await commandExists(command, env)) {
|
|
47
|
+
lines.push(`${command}: ok`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
ok = false;
|
|
51
|
+
lines.push(`${command}: missing`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
ok,
|
|
56
|
+
text: `${lines.join("\n")}\n`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function isSupportedNodeVersion(version) {
|
|
60
|
+
const [majorRaw = "0", minorRaw = "0"] = version.split(".");
|
|
61
|
+
const major = Number.parseInt(majorRaw, 10);
|
|
62
|
+
const minor = Number.parseInt(minorRaw, 10);
|
|
63
|
+
return major > 22 || (major === 22 && minor >= 5);
|
|
64
|
+
}
|
|
65
|
+
function configuredCommands(config) {
|
|
66
|
+
const engines = new Set();
|
|
67
|
+
if (config.router.defaultMode === "auto") {
|
|
68
|
+
engines.add("router-codex");
|
|
69
|
+
engines.add(config.pairing.main);
|
|
70
|
+
engines.add(config.pairing.judge);
|
|
71
|
+
engines.add(config.pairing.actor);
|
|
72
|
+
engines.add(config.pairing.critic);
|
|
73
|
+
}
|
|
74
|
+
else if (config.router.defaultMode === "simple") {
|
|
75
|
+
engines.add(config.pairing.main);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
engines.add(config.pairing.judge);
|
|
79
|
+
engines.add(config.pairing.actor);
|
|
80
|
+
engines.add(config.pairing.critic);
|
|
81
|
+
}
|
|
82
|
+
return Array.from(new Set(Array.from(engines)
|
|
83
|
+
.map((engine) => commandForEngine(config, engine))
|
|
84
|
+
.filter((command) => Boolean(command) && command !== "mock")));
|
|
85
|
+
}
|
|
86
|
+
function commandForEngine(config, engine) {
|
|
87
|
+
if (engine === "router-codex") {
|
|
88
|
+
return config.router.codex.command;
|
|
89
|
+
}
|
|
90
|
+
if (engine === "codex") {
|
|
91
|
+
return config.workers.codex.command;
|
|
92
|
+
}
|
|
93
|
+
if (engine === "claude") {
|
|
94
|
+
return config.workers.claude.command;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
async function commandExists(command, env) {
|
|
99
|
+
if (command.includes("/")) {
|
|
100
|
+
return canExecute(command);
|
|
101
|
+
}
|
|
102
|
+
const pathValue = env.PATH ?? "";
|
|
103
|
+
const extensions = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
104
|
+
for (const dir of pathValue.split(delimiter).filter(Boolean)) {
|
|
105
|
+
for (const extension of extensions) {
|
|
106
|
+
if (await canExecute(join(dir, `${command}${extension}`))) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
async function canExecute(path) {
|
|
114
|
+
try {
|
|
115
|
+
await access(path, constants.X_OK);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const RouteModeSchema = z.enum(["simple", "complex"]);
|
|
3
|
+
export const EngineNameSchema = z.enum(["codex", "claude", "mock"]);
|
|
4
|
+
export const WorkerRoleSchema = z.enum(["main", "judge", "actor", "critic"]);
|
|
5
|
+
export const TaskStateSchema = z.enum([
|
|
6
|
+
"created",
|
|
7
|
+
"routed",
|
|
8
|
+
"judging",
|
|
9
|
+
"ready_for_pair",
|
|
10
|
+
"actor_running",
|
|
11
|
+
"critic_running",
|
|
12
|
+
"revision_needed",
|
|
13
|
+
"verifying",
|
|
14
|
+
"done",
|
|
15
|
+
"failed",
|
|
16
|
+
"cancelled"
|
|
17
|
+
]);
|
|
18
|
+
export const WorkerStateSchema = z.enum([
|
|
19
|
+
"idle",
|
|
20
|
+
"starting",
|
|
21
|
+
"running",
|
|
22
|
+
"waiting",
|
|
23
|
+
"done",
|
|
24
|
+
"failed",
|
|
25
|
+
"cancelled"
|
|
26
|
+
]);
|
|
27
|
+
export const RouteDecisionSchema = z.object({
|
|
28
|
+
mode: RouteModeSchema,
|
|
29
|
+
reason: z.string().min(1),
|
|
30
|
+
suggested_roles: z.array(WorkerRoleSchema).default([]),
|
|
31
|
+
judge_engine: EngineNameSchema.default("codex"),
|
|
32
|
+
actor_engine: EngineNameSchema.default("codex"),
|
|
33
|
+
critic_engine: EngineNameSchema.default("claude")
|
|
34
|
+
});
|
|
35
|
+
export const TaskMetaSchema = z.object({
|
|
36
|
+
id: z.string().min(1),
|
|
37
|
+
title: z.string().min(1),
|
|
38
|
+
created_at: z.string().datetime(),
|
|
39
|
+
cwd: z.string().min(1),
|
|
40
|
+
mode: RouteModeSchema,
|
|
41
|
+
status: TaskStateSchema
|
|
42
|
+
});
|
|
43
|
+
export const WorkerStatusSchema = z.object({
|
|
44
|
+
worker_id: z.string().min(1),
|
|
45
|
+
role: WorkerRoleSchema,
|
|
46
|
+
engine: EngineNameSchema,
|
|
47
|
+
state: WorkerStateSchema,
|
|
48
|
+
phase: z.string().min(1),
|
|
49
|
+
last_event_at: z.string().datetime(),
|
|
50
|
+
summary: z.string(),
|
|
51
|
+
native_session_id: z.string().min(1).optional()
|
|
52
|
+
});
|
|
53
|
+
export const TurnMetaSchema = z.object({
|
|
54
|
+
task_id: z.string().min(1),
|
|
55
|
+
turn_id: z.string().regex(/^\d{4}$/),
|
|
56
|
+
created_at: z.string().datetime(),
|
|
57
|
+
request_path: z.string().min(1)
|
|
58
|
+
});
|
|
59
|
+
export const NativeSessionSourceSchema = z.enum(["output-detected", "config", "manual", "claude-project-log", "unknown"]);
|
|
60
|
+
export const NativeSessionSchema = z.object({
|
|
61
|
+
engine: EngineNameSchema,
|
|
62
|
+
role: WorkerRoleSchema,
|
|
63
|
+
worker_id: z.string().min(1),
|
|
64
|
+
session_id: z.string().min(1),
|
|
65
|
+
scope: z.literal("task"),
|
|
66
|
+
cwd: z.string().min(1),
|
|
67
|
+
created_at: z.string().datetime(),
|
|
68
|
+
last_used_at: z.string().datetime(),
|
|
69
|
+
source: NativeSessionSourceSchema
|
|
70
|
+
});
|
|
71
|
+
export const EventRecordSchema = z.object({
|
|
72
|
+
time: z.string().datetime(),
|
|
73
|
+
type: z.string().min(1),
|
|
74
|
+
message: z.string().optional(),
|
|
75
|
+
worker: z.string().optional(),
|
|
76
|
+
engine: EngineNameSchema.optional(),
|
|
77
|
+
task_id: z.string().optional()
|
|
78
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { appendJsonLine, ensureDir, writeJson, writeText } from "../core/file-store.js";
|
|
3
|
+
export async function createFeatureChannel(input) {
|
|
4
|
+
const id = featureIdForTurn(input.turn, input.request);
|
|
5
|
+
const dir = join(input.task.dir, "features", id);
|
|
6
|
+
const dialoguePath = join(input.task.dir, "dialogue", "actor-critic.jsonl");
|
|
7
|
+
const channel = {
|
|
8
|
+
id,
|
|
9
|
+
taskId: input.task.id,
|
|
10
|
+
turnId: input.turn.turnId,
|
|
11
|
+
dir,
|
|
12
|
+
specPath: join(dir, "spec.md"),
|
|
13
|
+
statusPath: join(dir, "status.json"),
|
|
14
|
+
dialoguePath,
|
|
15
|
+
actorWorklogPath: join(dir, "actor-worklog.md"),
|
|
16
|
+
actorRepliesPath: join(dir, "actor-replies.jsonl"),
|
|
17
|
+
criticFindingsPath: join(dir, "critic-findings.jsonl"),
|
|
18
|
+
decisionsPath: join(dir, "decisions.md")
|
|
19
|
+
};
|
|
20
|
+
await ensureDir(dir);
|
|
21
|
+
await ensureDir(join(input.task.dir, "dialogue"));
|
|
22
|
+
await writeText(channel.specPath, buildFeatureSpec(input, channel));
|
|
23
|
+
await writeText(channel.actorWorklogPath, "");
|
|
24
|
+
await writeText(channel.actorRepliesPath, "");
|
|
25
|
+
await writeText(channel.criticFindingsPath, "");
|
|
26
|
+
await updateFeatureStatus(channel, "created");
|
|
27
|
+
await appendFeatureDialogue(channel, "feature.created", "actor", "Feature mailbox created for the current turn.");
|
|
28
|
+
return channel;
|
|
29
|
+
}
|
|
30
|
+
export function featurePromptContext(channel) {
|
|
31
|
+
return {
|
|
32
|
+
featureId: channel.id,
|
|
33
|
+
featureDir: channel.dir,
|
|
34
|
+
dialoguePath: channel.dialoguePath,
|
|
35
|
+
actorWorklogPath: channel.actorWorklogPath,
|
|
36
|
+
actorRepliesPath: channel.actorRepliesPath,
|
|
37
|
+
criticFindingsPath: channel.criticFindingsPath,
|
|
38
|
+
decisionsPath: channel.decisionsPath
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export async function updateFeatureStatus(channel, state) {
|
|
42
|
+
await writeJson(channel.statusPath, {
|
|
43
|
+
feature_id: channel.id,
|
|
44
|
+
task_id: channel.taskId,
|
|
45
|
+
turn_id: channel.turnId,
|
|
46
|
+
state,
|
|
47
|
+
updated_at: new Date().toISOString()
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export async function appendFeatureDialogue(channel, type, role, message, paths = {}) {
|
|
51
|
+
await appendJsonLine(channel.dialoguePath, {
|
|
52
|
+
time: new Date().toISOString(),
|
|
53
|
+
feature_id: channel.id,
|
|
54
|
+
turn_id: channel.turnId,
|
|
55
|
+
type,
|
|
56
|
+
role,
|
|
57
|
+
message,
|
|
58
|
+
paths
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export async function writeFeatureDecision(channel, summary) {
|
|
62
|
+
await writeText(channel.decisionsPath, [
|
|
63
|
+
"# Decisions",
|
|
64
|
+
"",
|
|
65
|
+
`Feature: ${channel.id}`,
|
|
66
|
+
`Turn: ${channel.turnId}`,
|
|
67
|
+
"",
|
|
68
|
+
"Supervisor summary:",
|
|
69
|
+
summary.trim() || "(empty)",
|
|
70
|
+
""
|
|
71
|
+
].join("\n"));
|
|
72
|
+
}
|
|
73
|
+
function buildFeatureSpec(input, channel) {
|
|
74
|
+
return [
|
|
75
|
+
"# Feature Mailbox",
|
|
76
|
+
"",
|
|
77
|
+
`Feature: ${channel.id}`,
|
|
78
|
+
`Task: ${input.task.id}`,
|
|
79
|
+
`Turn: ${input.turn.turnId}`,
|
|
80
|
+
`Turn directory: ${input.turn.dir}`,
|
|
81
|
+
`Judge directory: ${input.judgeDir}`,
|
|
82
|
+
"",
|
|
83
|
+
"User request:",
|
|
84
|
+
input.request.trim(),
|
|
85
|
+
"",
|
|
86
|
+
"Protocol:",
|
|
87
|
+
"- Actor writes implementation notes to actor-worklog.md.",
|
|
88
|
+
"- Critic writes one JSON object per blocking issue to critic-findings.jsonl.",
|
|
89
|
+
"- Actor replies to each critic finding in actor-replies.jsonl.",
|
|
90
|
+
"- Supervisor writes the final decision summary to decisions.md.",
|
|
91
|
+
""
|
|
92
|
+
].join("\n");
|
|
93
|
+
}
|
|
94
|
+
function featureIdForTurn(turn, request) {
|
|
95
|
+
const slug = request
|
|
96
|
+
.toLowerCase()
|
|
97
|
+
.normalize("NFKD")
|
|
98
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
99
|
+
.replace(/^-+|-+$/g, "")
|
|
100
|
+
.slice(0, 48)
|
|
101
|
+
.replace(/-+$/g, "");
|
|
102
|
+
return slug ? `${turn.turnId}-${slug}` : turn.turnId;
|
|
103
|
+
}
|