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
package/dist/doctor.js CHANGED
@@ -16,6 +16,7 @@ import { formatTuiThemePreview } from "./tui/theme-preview.js";
16
16
  import { resolveTuiTheme } from "./tui/theme.js";
17
17
  import { diagnoseAgentCapabilities } from "./workers/capabilities.js";
18
18
  import { runLiveAgentProbes } from "./workers/live-probe.js";
19
+ import { workerProvider } from "./workers/provider.js";
19
20
  export async function runDoctor(appRoot, workspaceRoot, env = process.env, options = {}) {
20
21
  const lines = ["parallel-codex-tui doctor"];
21
22
  let ok = true;
@@ -63,6 +64,51 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env, optio
63
64
  lines.push(`router retry: ${config.router.codex.maxAttempts} attempts; transient only; ${config.router.codex.retryDelayMs}ms backoff (TUI routing; live probe runs once)`);
64
65
  lines.push(`router budget: total ${config.router.codex.timeoutMs}ms; follow-up ${config.router.codex.followUpTimeoutMs}ms; first output ${config.router.codex.firstOutputTimeoutMs}ms; idle ${config.router.codex.idleTimeoutMs}ms`);
65
66
  }
67
+ const preflight = await runRuntimePreflight(config, preparedWorkspace, env, {
68
+ includeRouter,
69
+ ...(options.capabilityRunner ? { capabilityRunner: options.capabilityRunner } : {}),
70
+ ...(options.capabilityTimeoutMs ? { capabilityTimeoutMs: options.capabilityTimeoutMs } : {})
71
+ });
72
+ lines.push(...preflight.lines);
73
+ ok = ok && preflight.ok;
74
+ if (options.probeAgents) {
75
+ if (ok) {
76
+ const liveAgents = await runLiveAgentProbes(config, preparedWorkspace, configuredWorkerEngines(config), options.liveAgentProbeOptions);
77
+ lines.push(...liveAgents.lines);
78
+ ok = ok && liveAgents.ok;
79
+ }
80
+ else {
81
+ lines.push("agent live probe: skipped (preflight failed)");
82
+ }
83
+ }
84
+ else {
85
+ lines.push("agent live probe: not run (add --probe-agents; may use model quota)");
86
+ }
87
+ if (options.probeRouter) {
88
+ const probe = await runRouterProbe(config, appRoot, options.routeRunner);
89
+ lines.push(probe.line);
90
+ ok = ok && probe.ok;
91
+ }
92
+ else if (config.router.defaultMode === "auto") {
93
+ lines.push("router live probe: not run (add --probe-router)");
94
+ }
95
+ return {
96
+ ok,
97
+ text: `${lines.join("\n")}\n`
98
+ };
99
+ }
100
+ export async function runRuntimePreflight(config, workspaceRoot, env = process.env, options = {}) {
101
+ const includeRouter = options.includeRouter ?? config.router.defaultMode === "auto";
102
+ const lines = [];
103
+ let ok = true;
104
+ try {
105
+ await access(workspaceRoot, constants.R_OK | constants.W_OK | constants.X_OK);
106
+ lines.push("workspace permissions: ok (read/write/search)");
107
+ }
108
+ catch {
109
+ ok = false;
110
+ lines.push(`workspace permissions: denied (${workspaceRoot}; need read/write/search)`);
111
+ }
66
112
  const availableCommands = new Set();
67
113
  for (const command of configuredCommands(config, includeRouter)) {
68
114
  if (await commandExists(command, env)) {
@@ -92,45 +138,24 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env, optio
92
138
  });
93
139
  lines.push(...capabilityDiagnostics.lines);
94
140
  ok = ok && capabilityDiagnostics.ok;
95
- const proxyDiagnostics = await diagnoseProxyEnvironment(config, env, await detectMacSystemProxy(), canConnectProxy, { includeRouter });
141
+ const systemProxy = options.systemProxy !== undefined
142
+ ? options.systemProxy
143
+ : await detectMacSystemProxy();
144
+ const proxyDiagnostics = await diagnoseProxyEnvironment(config, env, systemProxy, options.proxyConnector ?? canConnectProxy, { includeRouter });
96
145
  lines.push(...proxyDiagnostics.lines);
97
146
  ok = ok && proxyDiagnostics.ok;
98
- if (options.probeAgents) {
99
- if (ok) {
100
- const liveAgents = await runLiveAgentProbes(config, preparedWorkspace, configuredWorkerEngines(config), options.liveAgentProbeOptions);
101
- lines.push(...liveAgents.lines);
102
- ok = ok && liveAgents.ok;
103
- }
104
- else {
105
- lines.push("agent live probe: skipped (preflight failed)");
106
- }
107
- }
108
- else {
109
- lines.push("agent live probe: not run (add --probe-agents; may use model quota)");
110
- }
111
- if (options.probeRouter) {
112
- const probe = await runRouterProbe(config, appRoot, options.routeRunner);
113
- lines.push(probe.line);
114
- ok = ok && probe.ok;
115
- }
116
- else if (config.router.defaultMode === "auto") {
117
- lines.push("router live probe: not run (add --probe-router)");
118
- }
119
- return {
120
- ok,
121
- text: `${lines.join("\n")}\n`
122
- };
147
+ return { ok, lines };
123
148
  }
124
149
  export async function diagnoseProxyEnvironment(config, env, systemProxy, connect = canConnectProxy, options = {}) {
125
150
  const contexts = [];
126
151
  if (options.includeRouter ?? config.router.defaultMode === "auto") {
127
152
  contexts.push({ label: "router proxy", env: config.router.codex.env, table: "router.codex.env" });
128
153
  }
129
- if (configuredWorkerEngines(config).includes("codex")) {
154
+ for (const engine of configuredWorkerEngines(config)) {
130
155
  contexts.push({
131
- label: "workers.codex proxy",
132
- env: config.workers.codex.model.env,
133
- table: "workers.codex.model.env"
156
+ label: `workers.${engine} proxy`,
157
+ env: workerProvider(config, engine).config.model.env,
158
+ table: `workers.${engine}.model.env`
134
159
  });
135
160
  }
136
161
  const connectionCache = new Map();
@@ -300,11 +325,8 @@ function themeSummary(effectiveTheme, configTheme, cliTheme) {
300
325
  }
301
326
  return effectiveTheme;
302
327
  }
303
- function configuredEngines(config, options) {
328
+ function configuredWorkerEngines(config) {
304
329
  const engines = new Set();
305
- if (options.includeRouter) {
306
- engines.add("router-codex");
307
- }
308
330
  if (config.router.defaultMode === "auto") {
309
331
  engines.add(config.pairing.main);
310
332
  engines.add(config.pairing.judge);
@@ -319,10 +341,7 @@ function configuredEngines(config, options) {
319
341
  engines.add(config.pairing.actor);
320
342
  engines.add(config.pairing.critic);
321
343
  }
322
- return Array.from(engines);
323
- }
324
- function configuredWorkerEngines(config) {
325
- return configuredEngines(config, { includeRouter: false }).filter((engine) => engine === "codex" || engine === "claude");
344
+ return [...engines].filter((engine) => engine !== "mock");
326
345
  }
327
346
  function configuredEnvironmentChecks(config, env, includeRouter) {
328
347
  const checks = [];
@@ -338,7 +357,7 @@ function configuredEnvironmentChecks(config, env, includeRouter) {
338
357
  }
339
358
  }
340
359
  for (const engine of configuredWorkerEngines(config)) {
341
- const worker = engine === "codex" ? config.workers.codex : config.workers.claude;
360
+ const worker = workerProvider(config, engine).config;
342
361
  for (const [key, value] of Object.entries(worker.model.env)) {
343
362
  for (const envName of referencedEnvNames(value)) {
344
363
  checks.push({
@@ -1,6 +1,10 @@
1
1
  import { z } from "zod";
2
2
  export const RouteModeSchema = z.enum(["simple", "complex"]);
3
- export const EngineNameSchema = z.enum(["codex", "claude", "mock"]);
3
+ export const EngineNameSchema = z
4
+ .string()
5
+ .min(1)
6
+ .max(48)
7
+ .regex(/^[a-z][a-z0-9_]*$/, "Worker id must start with a lowercase letter and contain only lowercase letters, digits or _");
4
8
  export const WorkerRoleSchema = z.enum(["main", "judge", "actor", "critic"]);
5
9
  export const RouterFailureStageSchema = z.enum([
6
10
  "spawn",
@@ -49,6 +53,7 @@ export const TaskStateSchema = z.enum([
49
53
  "revision_needed",
50
54
  "integrating",
51
55
  "verifying",
56
+ "paused",
52
57
  "done",
53
58
  "failed",
54
59
  "cancelled"
@@ -80,6 +85,7 @@ export const FeatureStateSchema = z.enum([
80
85
  "revision_needed",
81
86
  "integrating",
82
87
  "verifying",
88
+ "paused",
83
89
  "approved",
84
90
  "failed",
85
91
  "cancelled"
@@ -141,6 +147,8 @@ export const WorkerStatusSchema = z.object({
141
147
  feature_title: z.string().min(1).optional(),
142
148
  role: WorkerRoleSchema,
143
149
  engine: EngineNameSchema,
150
+ model_name: z.string().optional(),
151
+ model_provider: z.string().optional(),
144
152
  state: WorkerStateSchema,
145
153
  phase: z.string().min(1),
146
154
  last_event_at: z.string().datetime(),
@@ -157,6 +165,12 @@ export const FeatureStatusSchema = z.object({
157
165
  state: FeatureStateSchema,
158
166
  updated_at: z.string().datetime()
159
167
  });
168
+ export const FeatureAssignmentSchema = z.object({
169
+ version: z.literal(1),
170
+ actor_engine: EngineNameSchema,
171
+ critic_engine: EngineNameSchema,
172
+ updated_at: z.string().datetime()
173
+ });
160
174
  export const TurnMetaSchema = z.object({
161
175
  task_id: TaskIdSchema,
162
176
  turn_id: z.string().regex(/^\d{4}$/),
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { z } from "zod";
3
3
  import { appendJsonLine, ensureDir, pathExists, readJson, readTextIfExists, writeJson, writeText } from "../core/file-store.js";
4
- import { FeatureStatusSchema } from "../domain/schemas.js";
4
+ import { FeatureAssignmentSchema, FeatureStatusSchema } from "../domain/schemas.js";
5
5
  const CriticFindingRecordSchema = z.object({
6
6
  id: z.string().trim().min(1),
7
7
  severity: z.string().trim().optional(),
@@ -52,7 +52,8 @@ export async function createFeatureChannel(input) {
52
52
  actorRepliesPath: join(dir, "actor-replies.jsonl"),
53
53
  criticFindingsPath: join(dir, "critic-findings.jsonl"),
54
54
  findingResolutionPath: join(dir, "finding-resolution.json"),
55
- decisionsPath: join(dir, "decisions.md")
55
+ decisionsPath: join(dir, "decisions.md"),
56
+ assignmentPath: join(dir, "assignment.json")
56
57
  };
57
58
  await ensureDir(dir);
58
59
  await ensureDir(join(input.task.dir, "dialogue"));
@@ -68,6 +69,9 @@ export async function createFeatureChannel(input) {
68
69
  await writeText(channel.actorWorklogPath, "");
69
70
  await writeText(channel.actorRepliesPath, "");
70
71
  await writeText(channel.criticFindingsPath, "");
72
+ if (input.actorEngine && input.criticEngine) {
73
+ await writeFeatureAssignment(channel, input.actorEngine, input.criticEngine);
74
+ }
71
75
  await updateFeatureStatus(channel, "created");
72
76
  await appendFeatureDialogue(channel, "feature.created", "actor", "Feature mailbox created for the current turn.");
73
77
  return channel;
@@ -84,6 +88,32 @@ async function ensureFeatureChannelFiles(input, channel) {
84
88
  await writeText(path, initialContent);
85
89
  }
86
90
  }
91
+ if (!(await pathExists(channel.assignmentPath)) && input.actorEngine && input.criticEngine) {
92
+ await writeFeatureAssignment(channel, input.actorEngine, input.criticEngine);
93
+ }
94
+ }
95
+ export async function readFeatureAssignment(channel, fallback) {
96
+ try {
97
+ return await readJson(channel.assignmentPath, FeatureAssignmentSchema);
98
+ }
99
+ catch {
100
+ return FeatureAssignmentSchema.parse({
101
+ version: 1,
102
+ actor_engine: fallback.actor,
103
+ critic_engine: fallback.critic,
104
+ updated_at: new Date(0).toISOString()
105
+ });
106
+ }
107
+ }
108
+ export async function writeFeatureAssignment(channel, actorEngine, criticEngine) {
109
+ const assignment = FeatureAssignmentSchema.parse({
110
+ version: 1,
111
+ actor_engine: actorEngine,
112
+ critic_engine: criticEngine,
113
+ updated_at: new Date().toISOString()
114
+ });
115
+ await writeJson(channel.assignmentPath, assignment);
116
+ return assignment;
87
117
  }
88
118
  export function featurePromptContext(channel) {
89
119
  return {
@@ -97,7 +127,8 @@ export function featurePromptContext(channel) {
97
127
  actorWorklogPath: channel.actorWorklogPath,
98
128
  actorRepliesPath: channel.actorRepliesPath,
99
129
  criticFindingsPath: channel.criticFindingsPath,
100
- decisionsPath: channel.decisionsPath
130
+ decisionsPath: channel.decisionsPath,
131
+ assignmentPath: channel.assignmentPath
101
132
  };
102
133
  }
103
134
  export async function updateFeatureStatus(channel, state) {
@@ -339,6 +370,7 @@ function buildFeatureSpec(input, channel) {
339
370
  '- Critic writes one JSON object per blocking issue to critic-findings.jsonl: {"id":"C-001","severity":"blocker","summary":"what must change"}.',
340
371
  '- Actor replies to each fixed finding in actor-replies.jsonl: {"finding_id":"C-001","status":"fixed","notes":"what changed"}.',
341
372
  "- Supervisor writes the final decision summary to decisions.md.",
373
+ "- assignment.json records the Actor and Critic engines selected for retries.",
342
374
  ""
343
375
  ].join("\n");
344
376
  }
@@ -0,0 +1,86 @@
1
+ import { z } from "zod";
2
+ const FinalAcceptanceItemSchema = z.object({
3
+ criterion_id: z.string().regex(/^A-\d{1,4}$/),
4
+ status: z.enum(["passed", "failed"]),
5
+ evidence: z.string().trim().min(1).max(4000)
6
+ }).strict();
7
+ export const FinalJudgeAcceptanceSchema = z.object({
8
+ version: z.literal(1),
9
+ decision: z.enum(["approved", "rejected"]),
10
+ summary: z.string().trim().min(1).max(4000),
11
+ acceptance: z.array(FinalAcceptanceItemSchema).min(1).max(100),
12
+ changed_paths: z.array(z.string().trim().min(1).max(1000)).max(10000)
13
+ }).strict();
14
+ export function validateFinalJudgeAcceptance(input, expectedCriterionIds, expectedChangedPaths) {
15
+ const parsed = FinalJudgeAcceptanceSchema.safeParse(input);
16
+ if (!parsed.success) {
17
+ return {
18
+ acceptance: null,
19
+ report: {
20
+ version: 1,
21
+ state: "invalid",
22
+ decision: "unknown",
23
+ issues: parsed.error.issues.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`)
24
+ }
25
+ };
26
+ }
27
+ const acceptance = parsed.data;
28
+ const issues = [];
29
+ const actualIds = acceptance.acceptance.map((item) => item.criterion_id);
30
+ const duplicateIds = repeated(actualIds);
31
+ if (duplicateIds.length > 0) {
32
+ issues.push(`duplicate acceptance criteria: ${duplicateIds.join(", ")}`);
33
+ }
34
+ const expectedIds = [...new Set(expectedCriterionIds)].sort();
35
+ const uniqueActualIds = [...new Set(actualIds)].sort();
36
+ const missingIds = expectedIds.filter((id) => !uniqueActualIds.includes(id));
37
+ const unknownIds = uniqueActualIds.filter((id) => !expectedIds.includes(id));
38
+ if (missingIds.length > 0) {
39
+ issues.push(`missing acceptance criteria: ${missingIds.join(", ")}`);
40
+ }
41
+ if (unknownIds.length > 0) {
42
+ issues.push(`unknown acceptance criteria: ${unknownIds.join(", ")}`);
43
+ }
44
+ const failedIds = acceptance.acceptance
45
+ .filter((item) => item.status === "failed")
46
+ .map((item) => item.criterion_id);
47
+ if (acceptance.decision === "approved" && failedIds.length > 0) {
48
+ issues.push(`approved decision contains failed criteria: ${failedIds.join(", ")}`);
49
+ }
50
+ if (acceptance.decision === "rejected" && failedIds.length === 0) {
51
+ issues.push("rejected decision must identify at least one failed criterion");
52
+ }
53
+ const expectedPaths = [...new Set(expectedChangedPaths)].sort();
54
+ const actualPaths = [...new Set(acceptance.changed_paths)].sort();
55
+ if (acceptance.changed_paths.length !== actualPaths.length) {
56
+ issues.push(`duplicate changed paths: ${repeated(acceptance.changed_paths).join(", ")}`);
57
+ }
58
+ const missingPaths = expectedPaths.filter((path) => !actualPaths.includes(path));
59
+ const unknownPaths = actualPaths.filter((path) => !expectedPaths.includes(path));
60
+ if (missingPaths.length > 0) {
61
+ issues.push(`missing changed paths: ${missingPaths.join(", ")}`);
62
+ }
63
+ if (unknownPaths.length > 0) {
64
+ issues.push(`unknown changed paths: ${unknownPaths.join(", ")}`);
65
+ }
66
+ return {
67
+ acceptance,
68
+ report: {
69
+ version: 1,
70
+ state: issues.length === 0 ? "valid" : "invalid",
71
+ decision: acceptance.decision,
72
+ issues
73
+ }
74
+ };
75
+ }
76
+ function repeated(values) {
77
+ const seen = new Set();
78
+ const duplicates = new Set();
79
+ for (const value of values) {
80
+ if (seen.has(value)) {
81
+ duplicates.add(value);
82
+ }
83
+ seen.add(value);
84
+ }
85
+ return [...duplicates].sort();
86
+ }