parallel-codex-tui 0.1.0 → 0.1.4

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 (74) hide show
  1. package/.parallel-codex/config.example.toml +90 -3
  2. package/README.md +269 -12
  3. package/dist/bootstrap.js +50 -18
  4. package/dist/cli-args.js +96 -14
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-recovery.js +70 -0
  7. package/dist/cli-workspace-picker.js +330 -0
  8. package/dist/cli-workspace-transition.js +33 -0
  9. package/dist/cli-workspace.js +40 -0
  10. package/dist/cli.js +291 -35
  11. package/dist/core/app-root.js +8 -0
  12. package/dist/core/collaboration-timeline.js +261 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +191 -23
  15. package/dist/core/file-store.js +130 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +10 -0
  18. package/dist/core/process-identity.js +48 -0
  19. package/dist/core/process-mutation-turn.js +128 -0
  20. package/dist/core/process-ownership.js +276 -0
  21. package/dist/core/process-tree.js +90 -0
  22. package/dist/core/router-audit.js +155 -0
  23. package/dist/core/router-redaction.js +31 -0
  24. package/dist/core/router.js +473 -42
  25. package/dist/core/session-index.js +225 -30
  26. package/dist/core/session-manager.js +1182 -44
  27. package/dist/core/task-state-machine.js +17 -0
  28. package/dist/core/workspace-commit-recovery.js +118 -0
  29. package/dist/core/workspace.js +126 -0
  30. package/dist/doctor.js +384 -30
  31. package/dist/domain/schemas.js +127 -6
  32. package/dist/orchestrator/collaboration-channel.js +255 -4
  33. package/dist/orchestrator/feature-plan.js +70 -0
  34. package/dist/orchestrator/judge-artifacts.js +236 -0
  35. package/dist/orchestrator/orchestrator.js +1777 -212
  36. package/dist/orchestrator/prompts.js +126 -2
  37. package/dist/orchestrator/supervisor-summary.js +56 -2
  38. package/dist/orchestrator/workspace-sandbox.js +911 -0
  39. package/dist/tui/App.js +2838 -159
  40. package/dist/tui/AppShell.js +188 -23
  41. package/dist/tui/CollaborationTimelineView.js +327 -0
  42. package/dist/tui/FeatureBoardView.js +227 -0
  43. package/dist/tui/InputBar.js +514 -57
  44. package/dist/tui/RouterDiagnosticsView.js +469 -0
  45. package/dist/tui/StatusBar.js +610 -57
  46. package/dist/tui/TaskSessionsView.js +207 -0
  47. package/dist/tui/TerminalOutput.js +53 -9
  48. package/dist/tui/WorkerOutputView.js +1403 -161
  49. package/dist/tui/WorkerOverviewView.js +250 -0
  50. package/dist/tui/chat-history.js +25 -0
  51. package/dist/tui/chat-input.js +67 -19
  52. package/dist/tui/chat-paste.js +76 -0
  53. package/dist/tui/display-width.js +41 -3
  54. package/dist/tui/incremental-text-file.js +101 -0
  55. package/dist/tui/keyboard.js +46 -0
  56. package/dist/tui/markdown-text.js +14 -0
  57. package/dist/tui/raw-input-decoder.js +3 -0
  58. package/dist/tui/scrolling.js +2 -1
  59. package/dist/tui/status-line.js +318 -11
  60. package/dist/tui/task-memory.js +15 -0
  61. package/dist/tui/task-result.js +105 -0
  62. package/dist/tui/terminal-screen.js +13 -1
  63. package/dist/tui/theme-contrast.js +144 -0
  64. package/dist/tui/theme-preview.js +109 -0
  65. package/dist/tui/theme.js +158 -0
  66. package/dist/version.js +1 -1
  67. package/dist/workers/capabilities.js +212 -0
  68. package/dist/workers/live-probe.js +176 -0
  69. package/dist/workers/mock-adapter.js +39 -6
  70. package/dist/workers/native-attach.js +147 -8
  71. package/dist/workers/native-session-detection.js +17 -0
  72. package/dist/workers/process-adapter.js +580 -81
  73. package/dist/workers/registry.js +4 -2
  74. package/package.json +17 -2
@@ -0,0 +1,17 @@
1
+ const TASK_STATE_TRANSITIONS = {
2
+ created: new Set(["routed", "failed", "cancelled"]),
3
+ routed: new Set(["judging", "failed", "cancelled"]),
4
+ judging: new Set(["ready_for_pair", "failed", "cancelled"]),
5
+ ready_for_pair: new Set(["actor_running", "done", "failed", "cancelled"]),
6
+ actor_running: new Set(["critic_running", "verifying", "failed", "cancelled"]),
7
+ critic_running: new Set(["revision_needed", "integrating", "failed", "cancelled"]),
8
+ revision_needed: new Set(["actor_running", "failed", "cancelled"]),
9
+ integrating: new Set(["actor_running", "verifying", "done", "failed", "cancelled"]),
10
+ verifying: new Set(["revision_needed", "integrating", "failed", "cancelled"]),
11
+ done: new Set(["routed", "cancelled"]),
12
+ failed: new Set(["routed", "judging", "ready_for_pair"]),
13
+ cancelled: new Set(["routed", "judging", "ready_for_pair"])
14
+ };
15
+ export function taskStateTransitionAllowed(from, to) {
16
+ return from === to || TASK_STATE_TRANSITIONS[from].has(to);
17
+ }
@@ -0,0 +1,118 @@
1
+ import { readFile, readdir, rm } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { sessionsRoot } from "./paths.js";
4
+ const TASK_DIRECTORY_PATTERN = /^task-/;
5
+ const TURN_DIRECTORY_PATTERN = /^turn-(\d{4,})$/;
6
+ const WAVE_DIRECTORY_PATTERN = /^wave-(\d{4,})$/;
7
+ const COMMIT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/;
8
+ const COMMIT_PROTOCOL = "atomic-claim-v1";
9
+ export async function reconcileWorkspaceCommitIntents(projectRoot, dataDir) {
10
+ const recovery = { cleaned: 0, preserved: 0 };
11
+ const taskEntries = await directoryEntries(sessionsRoot(projectRoot, dataDir));
12
+ for (const taskEntry of taskEntries) {
13
+ if (!taskEntry.isDirectory() || !TASK_DIRECTORY_PATTERN.test(taskEntry.name)) {
14
+ continue;
15
+ }
16
+ const workspacesDir = join(sessionsRoot(projectRoot, dataDir), taskEntry.name, "workspaces");
17
+ for (const turnEntry of await directoryEntries(workspacesDir)) {
18
+ const turnMatch = turnEntry.isDirectory() ? TURN_DIRECTORY_PATTERN.exec(turnEntry.name) : null;
19
+ if (!turnMatch) {
20
+ continue;
21
+ }
22
+ for (const waveEntry of await directoryEntries(join(workspacesDir, turnEntry.name))) {
23
+ const waveMatch = waveEntry.isDirectory() ? WAVE_DIRECTORY_PATTERN.exec(waveEntry.name) : null;
24
+ if (!waveMatch) {
25
+ continue;
26
+ }
27
+ const waveDir = join(workspacesDir, turnEntry.name, waveEntry.name);
28
+ const pendingPath = join(waveDir, "integration.pending.json");
29
+ const pendingRecord = await readJsonRecord(pendingPath);
30
+ if (pendingRecord === undefined) {
31
+ continue;
32
+ }
33
+ const pending = parseCommitEvidence(pendingRecord, "committing");
34
+ const integrated = parseCommitEvidence(await readJsonRecord(join(waveDir, "integration.json")), "integrated");
35
+ const directoryTurnId = turnMatch[1];
36
+ const directoryWave = Number(waveMatch[1]);
37
+ if (!pending
38
+ || !integrated
39
+ || pending.turnId !== directoryTurnId
40
+ || pending.wave !== directoryWave
41
+ || !sameCommit(pending, integrated)) {
42
+ recovery.preserved += 1;
43
+ continue;
44
+ }
45
+ try {
46
+ await rm(pendingPath, { force: true });
47
+ recovery.cleaned += 1;
48
+ }
49
+ catch {
50
+ recovery.preserved += 1;
51
+ }
52
+ }
53
+ }
54
+ }
55
+ return recovery;
56
+ }
57
+ async function directoryEntries(path) {
58
+ try {
59
+ return await readdir(path, { withFileTypes: true });
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ }
65
+ async function readJsonRecord(path) {
66
+ try {
67
+ const value = JSON.parse(await readFile(path, "utf8"));
68
+ return value !== null && typeof value === "object" && !Array.isArray(value)
69
+ ? value
70
+ : null;
71
+ }
72
+ catch (error) {
73
+ return error.code === "ENOENT" ? undefined : null;
74
+ }
75
+ }
76
+ function parseCommitEvidence(record, state) {
77
+ const featureIds = stringArray(record?.feature_ids);
78
+ const changedPaths = stringArray(record?.changed_paths);
79
+ const commitId = record?.commit_id;
80
+ const commitProtocol = record?.commit_protocol;
81
+ if (record?.version !== 1
82
+ || record.state !== state
83
+ || typeof record.turn_id !== "string"
84
+ || !Number.isInteger(record.wave)
85
+ || record.wave < 1
86
+ || !featureIds
87
+ || !changedPaths
88
+ || (commitId !== undefined && (typeof commitId !== "string" || !COMMIT_ID_PATTERN.test(commitId)))
89
+ || (commitProtocol !== undefined && commitProtocol !== COMMIT_PROTOCOL)) {
90
+ return null;
91
+ }
92
+ return {
93
+ version: 1,
94
+ state,
95
+ turnId: record.turn_id,
96
+ wave: record.wave,
97
+ featureIds,
98
+ ...(typeof commitId === "string" ? { commitId } : {}),
99
+ ...(commitProtocol === COMMIT_PROTOCOL ? { commitProtocol } : {}),
100
+ changedPaths
101
+ };
102
+ }
103
+ function stringArray(value) {
104
+ return Array.isArray(value) && value.every((item) => typeof item === "string")
105
+ ? value
106
+ : null;
107
+ }
108
+ function sameCommit(pending, integrated) {
109
+ return pending.turnId === integrated.turnId
110
+ && pending.wave === integrated.wave
111
+ && sameStrings(pending.featureIds, integrated.featureIds)
112
+ && sameStrings(pending.changedPaths, integrated.changedPaths)
113
+ && (pending.commitId === undefined || pending.commitId === integrated.commitId)
114
+ && (pending.commitProtocol === undefined || pending.commitProtocol === integrated.commitProtocol);
115
+ }
116
+ function sameStrings(left, right) {
117
+ return left.length === right.length && left.every((value, index) => value === right[index]);
118
+ }
@@ -0,0 +1,126 @@
1
+ import { join, resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { z } from "zod";
4
+ import { ensureDir, pathExists, pathIsDirectory, readJson, readTextIfExists, writeJson, writeText } from "./file-store.js";
5
+ import { runWithLeaseFinalization } from "./lease-finalization.js";
6
+ import { acquireProcessMutationTurn } from "./process-mutation-turn.js";
7
+ const lastWorkspaceFile = "last-workspace";
8
+ const workspacesFile = "workspaces.json";
9
+ const maxRememberedWorkspaces = 20;
10
+ const workspaceRegistryIntentPrefix = ".workspace-registry-claim-";
11
+ const WorkspaceRegistrySchema = z.object({
12
+ version: z.literal(1).default(1),
13
+ workspaces: z
14
+ .array(z.object({
15
+ path: z.string().min(1),
16
+ last_used_at: z.string().min(1)
17
+ }))
18
+ .default([])
19
+ });
20
+ export async function resolveWorkspaceSelection(input) {
21
+ if (input.explicitWorkspace?.trim()) {
22
+ return resolveWorkspacePath(input.cwd, input.explicitWorkspace);
23
+ }
24
+ const [latest] = await listWorkspaceChoices(input.appRoot);
25
+ return latest ? latest.path : input.cwd;
26
+ }
27
+ export async function prepareWorkspace(appRoot, workspaceRoot) {
28
+ const resolved = resolveWorkspacePath(process.cwd(), workspaceRoot);
29
+ if ((await pathExists(resolved)) && !(await pathIsDirectory(resolved))) {
30
+ throw new Error(`Workspace path exists but is not a directory: ${resolved}`);
31
+ }
32
+ await ensureDir(resolved);
33
+ await ensureDir(join(resolved, ".parallel-codex"));
34
+ await rememberWorkspace(appRoot, resolved);
35
+ return resolved;
36
+ }
37
+ export async function hasSavedWorkspace(appRoot) {
38
+ return (await listWorkspaceChoices(appRoot)).length > 0;
39
+ }
40
+ export async function listWorkspaceChoices(appRoot) {
41
+ const entries = await readWorkspaceEntries(appRoot);
42
+ const legacy = (await readTextIfExists(lastWorkspacePath(appRoot))).trim();
43
+ if (legacy) {
44
+ entries.push({ path: legacy, last_used_at: "" });
45
+ }
46
+ const seen = new Set();
47
+ const unique = entries
48
+ .map((entry, index) => ({
49
+ path: resolveStoredWorkspacePath(appRoot, entry.path),
50
+ lastUsedAt: entry.last_used_at || null,
51
+ order: index
52
+ }))
53
+ .filter((entry) => {
54
+ if (seen.has(entry.path)) {
55
+ return false;
56
+ }
57
+ seen.add(entry.path);
58
+ return true;
59
+ })
60
+ .sort((left, right) => {
61
+ const byDate = (right.lastUsedAt ?? "").localeCompare(left.lastUsedAt ?? "");
62
+ return byDate === 0 ? left.order - right.order : byDate;
63
+ });
64
+ const choices = await Promise.all(unique.map(async (entry) => {
65
+ const exists = await pathExists(entry.path);
66
+ const isDirectory = exists && (await pathIsDirectory(entry.path));
67
+ return {
68
+ path: entry.path,
69
+ exists: isDirectory,
70
+ lastUsedAt: entry.lastUsedAt,
71
+ selectable: !exists || isDirectory
72
+ };
73
+ }));
74
+ return choices
75
+ .filter((choice) => choice.selectable)
76
+ .map(({ selectable: _selectable, ...choice }) => choice);
77
+ }
78
+ async function rememberWorkspace(appRoot, workspaceRoot) {
79
+ const resolved = resolveWorkspacePath(process.cwd(), workspaceRoot);
80
+ const mutationTurn = await acquireProcessMutationTurn(join(appRoot, ".parallel-codex"), {
81
+ intentPrefix: workspaceRegistryIntentPrefix,
82
+ timeoutMessage: "Timed out waiting to update the workspace registry."
83
+ });
84
+ await runWithLeaseFinalization("Workspace registry update", mutationTurn, async () => {
85
+ const current = await readWorkspaceEntries(appRoot);
86
+ const next = {
87
+ version: 1,
88
+ workspaces: [
89
+ { path: resolved, last_used_at: new Date().toISOString() },
90
+ ...current.filter((entry) => resolveStoredWorkspacePath(appRoot, entry.path) !== resolved)
91
+ ].slice(0, maxRememberedWorkspaces)
92
+ };
93
+ await writeJson(workspacesPath(appRoot), WorkspaceRegistrySchema.parse(next));
94
+ await writeText(lastWorkspacePath(appRoot), `${resolved}\n`);
95
+ });
96
+ }
97
+ async function readWorkspaceEntries(appRoot) {
98
+ const file = workspacesPath(appRoot);
99
+ if (!(await pathExists(file))) {
100
+ return [];
101
+ }
102
+ try {
103
+ return (await readJson(file, WorkspaceRegistrySchema)).workspaces;
104
+ }
105
+ catch {
106
+ return [];
107
+ }
108
+ }
109
+ function lastWorkspacePath(appRoot) {
110
+ return join(appRoot, ".parallel-codex", lastWorkspaceFile);
111
+ }
112
+ function workspacesPath(appRoot) {
113
+ return join(appRoot, ".parallel-codex", workspacesFile);
114
+ }
115
+ export function resolveWorkspacePath(cwd, value) {
116
+ if (value === "~") {
117
+ return homedir();
118
+ }
119
+ if (value.startsWith("~/")) {
120
+ return resolve(homedir(), value.slice(2));
121
+ }
122
+ return resolve(cwd, value);
123
+ }
124
+ function resolveStoredWorkspacePath(appRoot, value) {
125
+ return resolveWorkspacePath(appRoot, value);
126
+ }