parallel-codex-tui 0.1.3 → 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 (72) hide show
  1. package/.parallel-codex/config.example.toml +90 -3
  2. package/README.md +240 -21
  3. package/dist/bootstrap.js +37 -17
  4. package/dist/cli-args.js +34 -3
  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 +7 -71
  10. package/dist/cli.js +221 -23
  11. package/dist/core/collaboration-timeline.js +261 -0
  12. package/dist/core/config-errors.js +14 -0
  13. package/dist/core/config.js +154 -24
  14. package/dist/core/file-store.js +119 -6
  15. package/dist/core/lease-finalization.js +22 -0
  16. package/dist/core/paths.js +7 -0
  17. package/dist/core/process-identity.js +48 -0
  18. package/dist/core/process-mutation-turn.js +128 -0
  19. package/dist/core/process-ownership.js +276 -0
  20. package/dist/core/process-tree.js +90 -0
  21. package/dist/core/router-audit.js +155 -0
  22. package/dist/core/router-redaction.js +31 -0
  23. package/dist/core/router.js +462 -35
  24. package/dist/core/session-index.js +188 -37
  25. package/dist/core/session-manager.js +1086 -40
  26. package/dist/core/task-state-machine.js +17 -0
  27. package/dist/core/workspace-commit-recovery.js +118 -0
  28. package/dist/core/workspace.js +19 -11
  29. package/dist/doctor.js +343 -23
  30. package/dist/domain/schemas.js +127 -6
  31. package/dist/orchestrator/collaboration-channel.js +255 -4
  32. package/dist/orchestrator/feature-plan.js +70 -0
  33. package/dist/orchestrator/judge-artifacts.js +236 -0
  34. package/dist/orchestrator/orchestrator.js +1749 -202
  35. package/dist/orchestrator/prompts.js +126 -2
  36. package/dist/orchestrator/supervisor-summary.js +56 -2
  37. package/dist/orchestrator/workspace-sandbox.js +911 -0
  38. package/dist/tui/App.js +2830 -153
  39. package/dist/tui/AppShell.js +188 -23
  40. package/dist/tui/CollaborationTimelineView.js +327 -0
  41. package/dist/tui/FeatureBoardView.js +227 -0
  42. package/dist/tui/InputBar.js +514 -57
  43. package/dist/tui/RouterDiagnosticsView.js +469 -0
  44. package/dist/tui/StatusBar.js +610 -57
  45. package/dist/tui/TaskSessionsView.js +207 -0
  46. package/dist/tui/TerminalOutput.js +53 -9
  47. package/dist/tui/WorkerOutputView.js +1403 -161
  48. package/dist/tui/WorkerOverviewView.js +250 -0
  49. package/dist/tui/chat-history.js +25 -0
  50. package/dist/tui/chat-input.js +67 -19
  51. package/dist/tui/chat-paste.js +76 -0
  52. package/dist/tui/display-width.js +41 -3
  53. package/dist/tui/incremental-text-file.js +101 -0
  54. package/dist/tui/keyboard.js +46 -0
  55. package/dist/tui/markdown-text.js +14 -0
  56. package/dist/tui/raw-input-decoder.js +3 -0
  57. package/dist/tui/scrolling.js +2 -1
  58. package/dist/tui/status-line.js +318 -11
  59. package/dist/tui/task-memory.js +13 -1
  60. package/dist/tui/task-result.js +105 -0
  61. package/dist/tui/terminal-screen.js +13 -1
  62. package/dist/tui/theme-contrast.js +144 -0
  63. package/dist/tui/theme-preview.js +109 -0
  64. package/dist/tui/theme.js +158 -0
  65. package/dist/version.js +1 -1
  66. package/dist/workers/capabilities.js +212 -0
  67. package/dist/workers/live-probe.js +176 -0
  68. package/dist/workers/mock-adapter.js +39 -6
  69. package/dist/workers/native-attach.js +78 -3
  70. package/dist/workers/process-adapter.js +570 -77
  71. package/dist/workers/registry.js +4 -2
  72. package/package.json +5 -2
package/dist/cli.js CHANGED
@@ -1,29 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { join } from "node:path";
3
4
  import { render } from "ink";
4
5
  import { ZodError } from "zod";
5
6
  import { parseCliArgs, validateCliArgs } from "./cli-args.js";
6
7
  import { selectWorkspaceForCli } from "./cli-workspace.js";
8
+ import { commitWorkspaceTransition } from "./cli-workspace-transition.js";
9
+ import { WorkspaceSelectionCancelledError } from "./cli-workspace-picker.js";
10
+ import { startupRecoveryMessages } from "./cli-startup-recovery.js";
7
11
  import { createRuntime } from "./bootstrap.js";
8
12
  import { prepareAppRoot } from "./core/app-root.js";
9
- import { configPath, writeDefaultConfig } from "./core/config.js";
13
+ import { formatConfigErrorMessage } from "./core/config-errors.js";
14
+ import { configPath, loadConfig, withUiThemeOverride, writeDefaultConfig } from "./core/config.js";
10
15
  import { pathExists } from "./core/file-store.js";
16
+ import { readRouterAudit } from "./core/router-audit.js";
17
+ import { listWorkspaceChoices } from "./core/workspace.js";
11
18
  import { runDoctor } from "./doctor.js";
19
+ import { helpText } from "./cli-help.js";
12
20
  import { App } from "./tui/App.js";
21
+ import { formatTuiThemeCatalog } from "./tui/theme-preview.js";
22
+ import { configureTuiTheme } from "./tui/theme.js";
23
+ import { routerDiagnosticsPolicy } from "./tui/RouterDiagnosticsView.js";
13
24
  import { version } from "./version.js";
14
- const helpText = `Usage: parallel-codex-tui [options]
15
-
16
- Options:
17
- -w, --workspace <path> Project workspace for worker sessions and edits
18
- --app-root <path> App root for configuration lookup
19
- -t, --task <id> Open an existing task session
20
- --init Write .parallel-codex/config.toml if missing
21
- --doctor Check local configuration and agent commands
22
- -v, --version Print the current version
23
- -h, --help Print this help message
24
-
25
- Options with values also accept --name=value and -x=value forms.`;
26
25
  main().catch((error) => {
26
+ if (error instanceof WorkspaceSelectionCancelledError) {
27
+ return;
28
+ }
27
29
  process.stderr.write(`${formatStartupError(error)}\n`);
28
30
  process.exit(1);
29
31
  });
@@ -35,7 +37,7 @@ async function main() {
35
37
  process.exit(1);
36
38
  }
37
39
  const cliArgs = parseCliArgs(rawArgs, process.cwd());
38
- if (!cliArgs.help && !cliArgs.version) {
40
+ if (!cliArgs.help && !cliArgs.themes && !cliArgs.version) {
39
41
  await prepareAppRoot(cliArgs.appRoot);
40
42
  }
41
43
  const localConfigPath = configPath(cliArgs.appRoot);
@@ -45,6 +47,9 @@ async function main() {
45
47
  else if (cliArgs.version) {
46
48
  console.log(`parallel-codex-tui ${version}`);
47
49
  }
50
+ else if (cliArgs.themes) {
51
+ console.log(formatTuiThemeCatalog(cliArgs.theme ? [cliArgs.theme] : undefined).join("\n"));
52
+ }
48
53
  else if (cliArgs.doctor) {
49
54
  const workspaceRoot = await selectWorkspaceForCli({
50
55
  appRoot: cliArgs.appRoot,
@@ -52,7 +57,11 @@ async function main() {
52
57
  explicitWorkspace: cliArgs.explicitWorkspace,
53
58
  interactive: false
54
59
  });
55
- const result = await runDoctor(cliArgs.appRoot, workspaceRoot);
60
+ const result = await runDoctor(cliArgs.appRoot, workspaceRoot, process.env, {
61
+ probeAgents: cliArgs.probeAgents,
62
+ probeRouter: cliArgs.probeRouter,
63
+ theme: cliArgs.theme
64
+ });
56
65
  process.stdout.write(result.text);
57
66
  process.exitCode = result.ok ? 0 : 1;
58
67
  }
@@ -66,28 +75,217 @@ async function main() {
66
75
  }
67
76
  }
68
77
  else {
78
+ const startupConfig = await loadConfig(cliArgs.appRoot);
79
+ configureTuiTheme({
80
+ theme: cliArgs.theme ?? startupConfig.ui.theme,
81
+ colors: startupConfig.ui.colors
82
+ });
69
83
  const workspaceRoot = await selectWorkspaceForCli({
70
84
  appRoot: cliArgs.appRoot,
71
85
  cwd: process.cwd(),
72
86
  explicitWorkspace: cliArgs.explicitWorkspace
73
87
  });
74
- const runtime = await createRuntime(cliArgs.appRoot, workspaceRoot);
75
- if (cliArgs.taskId && !(await runtime.sessions.hasTask(cliArgs.taskId))) {
76
- throw new Error(`Task session not found in workspace ${runtime.workspaceRoot}: ${cliArgs.taskId}`);
77
- }
88
+ let current = await loadInteractiveWorkspace(cliArgs.appRoot, workspaceRoot, cliArgs.taskId);
78
89
  if (!canRenderInteractiveTui()) {
90
+ current.runtime.index.close();
79
91
  throw new Error("parallel-codex-tui requires an interactive terminal. Use --help, --version, --init, or --doctor for non-interactive command modes.");
80
92
  }
81
- const latestTask = await runtime.sessions.latestTask();
82
- const initialTaskId = cliArgs.taskId ?? latestTask?.id ?? null;
83
- render(_jsx(App, { config: runtime.config, orchestrator: runtime.orchestrator, cwd: runtime.workspaceRoot, initialTaskId: initialTaskId }), { exitOnCtrlC: false });
93
+ let instance = null;
94
+ const shutdownController = new AbortController();
95
+ const deferredWorkspaceClosures = new Set();
96
+ const appElement = (state) => (_jsx(App, { config: withUiThemeOverride(state.runtime.config, cliArgs.theme), orchestrator: state.runtime.orchestrator, cwd: state.runtime.workspaceRoot, initialTaskId: state.initialTaskId, initialRoute: state.initialRoute, initialWorkers: state.initialWorkers, initialCanRetryTask: state.initialCanRetryTask, initialMessages: state.initialMessages, workspaceChoices: state.workspaceChoices, shutdownSignal: shutdownController.signal, loadRouterDiagnostics: async () => {
97
+ const [records, latestConfig] = await Promise.all([
98
+ readRouterAudit(join(state.runtime.routerCwd, "routes.jsonl"), 100),
99
+ loadConfig(cliArgs.appRoot)
100
+ ]);
101
+ return {
102
+ records,
103
+ policy: routerDiagnosticsPolicy(latestConfig.router)
104
+ };
105
+ }, loadTaskSessions: (options) => state.runtime.index.listTasks(100, options), renameTaskSession: async (taskId, title) => {
106
+ await state.runtime.sessions.renameTask(taskId, title);
107
+ }, setTaskSessionArchived: async (taskId, archived) => {
108
+ await state.runtime.sessions.setTaskArchived(taskId, archived);
109
+ }, deleteTaskSession: async (taskId) => {
110
+ await state.runtime.sessions.deleteTask(taskId);
111
+ }, exportTaskSession: async (taskId) => (await state.runtime.sessions.exportTask(taskId)).path, loadCollaborationTimeline: (taskId) => state.runtime.sessions.readCollaborationTimeline(taskId), activateTaskSession: async (taskId) => {
112
+ if (!taskId) {
113
+ await state.runtime.index.setActiveTaskId(null);
114
+ return null;
115
+ }
116
+ if (!(await state.runtime.sessions.hasTask(taskId))) {
117
+ throw new Error(`Task session not found in workspace ${state.runtime.workspaceRoot}: ${taskId}`);
118
+ }
119
+ const task = state.runtime.sessions.taskFromId(taskId);
120
+ const meta = await state.runtime.sessions.readMeta(task);
121
+ if (meta.archived_at) {
122
+ throw new Error(`Task session is archived: ${taskId}`);
123
+ }
124
+ const [route, workers, canRetry] = await Promise.all([
125
+ state.runtime.sessions.readLatestRoute(task),
126
+ state.runtime.orchestrator.listTaskWorkers(taskId),
127
+ state.runtime.orchestrator.canRetryTask(taskId)
128
+ ]);
129
+ await state.runtime.index.setActiveTaskId(taskId);
130
+ return { taskId, route, workers, canRetry };
131
+ }, switchWorkspace: async (workspace) => {
132
+ if (workspace === current.runtime.workspaceRoot) {
133
+ return;
134
+ }
135
+ retryDeferredWorkspaceClosures(deferredWorkspaceClosures);
136
+ const next = await loadInteractiveWorkspace(cliArgs.appRoot, workspace, null);
137
+ const previous = current;
138
+ current = commitWorkspaceTransition({
139
+ previous,
140
+ next,
141
+ render: (state) => {
142
+ if (!instance) {
143
+ throw new Error("Interactive TUI is not ready to switch workspaces.");
144
+ }
145
+ instance.rerender(appElement(state));
146
+ },
147
+ close: closeInteractiveWorkspace,
148
+ deferClose: (state) => deferredWorkspaceClosures.add(state)
149
+ });
150
+ }, persistChatMessage: (message, taskId) => state.runtime.sessions.appendChatMessage({
151
+ ...message,
152
+ taskId
153
+ }) }, state.runtime.workspaceRoot));
154
+ const removeSigintHandler = installInteractiveSigintExitHandler(() => shutdownController.abort());
155
+ try {
156
+ instance = render(appElement(current), { exitOnCtrlC: false });
157
+ await instance.waitUntilExit();
158
+ }
159
+ finally {
160
+ removeSigintHandler();
161
+ retryDeferredWorkspaceClosures(deferredWorkspaceClosures);
162
+ }
163
+ }
164
+ }
165
+ async function loadInteractiveWorkspace(appRoot, workspaceRoot, requestedTaskId) {
166
+ const runtime = await createRuntime(appRoot, workspaceRoot);
167
+ try {
168
+ if (requestedTaskId) {
169
+ if (!(await runtime.sessions.hasTask(requestedTaskId))) {
170
+ throw new Error(`Task session not found in workspace ${runtime.workspaceRoot}: ${requestedTaskId}`);
171
+ }
172
+ const requestedMeta = await runtime.sessions.readMeta(runtime.sessions.taskFromId(requestedTaskId));
173
+ if (requestedMeta.archived_at) {
174
+ throw new Error(`Task session is archived in workspace ${runtime.workspaceRoot}: ${requestedTaskId}`);
175
+ }
176
+ }
177
+ const [latestTask, rememberedTaskId] = await Promise.all([
178
+ runtime.sessions.latestTask(),
179
+ runtime.index.activeTaskId()
180
+ ]);
181
+ const rememberedTaskIsRestorable = typeof rememberedTaskId === "string"
182
+ && await runtime.sessions.hasTask(rememberedTaskId)
183
+ && !(await runtime.sessions.readMeta(runtime.sessions.taskFromId(rememberedTaskId))).archived_at;
184
+ let initialTaskId;
185
+ if (requestedTaskId) {
186
+ initialTaskId = requestedTaskId;
187
+ }
188
+ else if (rememberedTaskId === null) {
189
+ initialTaskId = null;
190
+ }
191
+ else if (rememberedTaskId && rememberedTaskIsRestorable) {
192
+ initialTaskId = rememberedTaskId;
193
+ }
194
+ else {
195
+ initialTaskId = latestTask?.id ?? null;
196
+ }
197
+ if (initialTaskId && initialTaskId !== rememberedTaskId) {
198
+ await runtime.index.setActiveTaskId(initialTaskId);
199
+ }
200
+ else if (!initialTaskId && typeof rememberedTaskId === "string") {
201
+ await runtime.index.setActiveTaskId(null);
202
+ }
203
+ const [initialRoute, initialWorkers, initialCanRetryTask, initialHistory, workspaceChoices] = await Promise.all([
204
+ initialTaskId
205
+ ? runtime.sessions.readLatestRoute(runtime.sessions.taskFromId(initialTaskId))
206
+ : null,
207
+ initialTaskId ? runtime.orchestrator.listTaskWorkers(initialTaskId) : [],
208
+ initialTaskId ? runtime.orchestrator.canRetryTask(initialTaskId) : false,
209
+ runtime.sessions.readChatHistory(),
210
+ listWorkspaceChoices(appRoot)
211
+ ]);
212
+ const recoveryMessages = startupRecoveryMessages(runtime.recoveredTasks, initialTaskId, runtime.pendingTaskCreations);
213
+ return {
214
+ runtime,
215
+ initialTaskId,
216
+ initialRoute,
217
+ initialWorkers,
218
+ initialCanRetryTask,
219
+ initialMessages: [
220
+ ...initialHistory.map(({ from, text, task_id }) => ({
221
+ from,
222
+ text,
223
+ ...(task_id ? { taskId: task_id } : {})
224
+ })),
225
+ ...recoveryMessages
226
+ ],
227
+ workspaceChoices
228
+ };
229
+ }
230
+ catch (error) {
231
+ runtime.index.close();
232
+ throw error;
233
+ }
234
+ }
235
+ function closeInteractiveWorkspace(state) {
236
+ state.runtime.index.close();
237
+ }
238
+ function retryDeferredWorkspaceClosures(states) {
239
+ for (const state of states) {
240
+ try {
241
+ closeInteractiveWorkspace(state);
242
+ states.delete(state);
243
+ }
244
+ catch {
245
+ // Process exit remains the final cleanup boundary if an index cannot be closed yet.
246
+ }
84
247
  }
85
248
  }
86
249
  function canRenderInteractiveTui() {
87
250
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
88
251
  }
252
+ function installInteractiveSigintExitHandler(requestGracefulExit) {
253
+ let interrupted = false;
254
+ const onSigint = () => {
255
+ if (!interrupted) {
256
+ interrupted = true;
257
+ try {
258
+ requestGracefulExit();
259
+ return;
260
+ }
261
+ catch {
262
+ // Fall through to the force-exit path when graceful shutdown cannot start.
263
+ }
264
+ }
265
+ restoreInteractiveTerminal();
266
+ process.exit(0);
267
+ };
268
+ process.on("SIGINT", onSigint);
269
+ return () => process.off("SIGINT", onSigint);
270
+ }
271
+ function restoreInteractiveTerminal() {
272
+ if (process.stdin.isTTY && process.stdin.isRaw && typeof process.stdin.setRawMode === "function") {
273
+ try {
274
+ process.stdin.setRawMode(false);
275
+ }
276
+ catch {
277
+ // The active view may already be releasing raw mode.
278
+ }
279
+ }
280
+ process.stdin.pause();
281
+ if (process.stdout.isTTY) {
282
+ process.stdout.write("\x1b[?1006l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[?25h");
283
+ }
284
+ }
89
285
  function formatStartupError(error) {
90
- const message = error instanceof Error ? error.message : String(error);
286
+ const message = isConfigStartupError(error)
287
+ ? formatConfigErrorMessage(error)
288
+ : error instanceof Error ? error.message : String(error);
91
289
  if (isConfigStartupError(error)) {
92
290
  return `Config error: ${message}\nRun parallel-codex-tui --doctor for details.`;
93
291
  }
@@ -0,0 +1,261 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { EventRecordSchema, FeatureStatusSchema } from "../domain/schemas.js";
5
+ import { readTextIfExists } from "./file-store.js";
6
+ const FeatureDialogueSchema = z.object({
7
+ time: z.string().datetime(),
8
+ feature_id: z.string().min(1),
9
+ turn_id: z.string().min(1),
10
+ type: z.string().min(1),
11
+ role: z.enum(["actor", "critic"]),
12
+ message: z.string().default(""),
13
+ paths: z.record(z.string()).default({})
14
+ });
15
+ const FindingResolutionSchema = z.object({
16
+ version: z.literal(1),
17
+ decision: z.enum(["pending", "approved", "inconsistent"]),
18
+ fixed_ids: z.array(z.string().min(1)),
19
+ unresolved_ids: z.array(z.string().min(1))
20
+ });
21
+ export async function loadCollaborationTimeline(taskId, taskDir) {
22
+ const [features, dialogueText, taskEventsText] = await Promise.all([
23
+ readCollaborationFeatures(taskDir),
24
+ readTextIfExists(join(taskDir, "dialogue", "actor-critic.jsonl")),
25
+ readTextIfExists(join(taskDir, "events.jsonl"))
26
+ ]);
27
+ const featureById = new Map(features.map((feature) => [feature.id, feature]));
28
+ const dialogue = parseJsonLines(dialogueText, FeatureDialogueSchema);
29
+ const taskEvents = parseJsonLines(taskEventsText, EventRecordSchema)
30
+ .filter((event) => event.type.startsWith("feature.wave_"));
31
+ const events = [
32
+ ...dialogue.map((event, index) => {
33
+ const feature = featureById.get(event.feature_id);
34
+ const artifactRefs = collaborationArtifactRefs(event.paths ?? {});
35
+ return {
36
+ id: `dialogue-${index}-${event.time}`,
37
+ time: event.time,
38
+ type: event.type,
39
+ role: event.role,
40
+ action: collaborationDialogueAction(event.type),
41
+ message: event.type === "critic.revision_requested" && feature?.latestFinding
42
+ ? appendCollaborationEvidence(event.message ?? "", feature.latestFinding)
43
+ : event.message ?? "",
44
+ turnId: event.turn_id,
45
+ featureId: event.feature_id,
46
+ featureTitle: feature?.title ?? event.feature_id,
47
+ findings: feature?.findings ?? 0,
48
+ replies: feature?.replies ?? 0,
49
+ ...(typeof feature?.resolvedFindings === "number"
50
+ ? { resolvedFindings: feature.resolvedFindings }
51
+ : {}),
52
+ ...(typeof feature?.unresolvedFindings === "number"
53
+ ? { unresolvedFindings: feature.unresolvedFindings }
54
+ : {}),
55
+ artifacts: artifactRefs.map((artifact) => artifact.label),
56
+ artifactRefs
57
+ };
58
+ }),
59
+ ...taskEvents.map((event, index) => ({
60
+ id: `task-${index}-${event.time}`,
61
+ time: event.time,
62
+ type: event.type,
63
+ role: "supervisor",
64
+ action: collaborationWaveAction(event.type),
65
+ message: event.message ?? "",
66
+ artifacts: ["task events"],
67
+ artifactRefs: [{ label: "task events", path: join(taskDir, "events.jsonl") }]
68
+ })),
69
+ ...features.map((feature) => ({
70
+ id: `state-${feature.id}-${feature.updatedAt}`,
71
+ time: feature.updatedAt,
72
+ type: "feature.state",
73
+ role: "supervisor",
74
+ action: collaborationFeatureStateAction(feature.state),
75
+ message: [
76
+ `${feature.title} · ${humanizeFeatureState(feature.state)}`,
77
+ ...(feature.latestFinding ? [`finding: ${feature.latestFinding}`] : []),
78
+ ...(feature.latestReply ? [`reply: ${feature.latestReply}`] : [])
79
+ ].join(" · "),
80
+ turnId: feature.turnId,
81
+ featureId: feature.id,
82
+ featureTitle: feature.title,
83
+ findings: feature.findings,
84
+ replies: feature.replies,
85
+ ...(typeof feature.resolvedFindings === "number"
86
+ ? { resolvedFindings: feature.resolvedFindings }
87
+ : {}),
88
+ ...(typeof feature.unresolvedFindings === "number"
89
+ ? { unresolvedFindings: feature.unresolvedFindings }
90
+ : {}),
91
+ artifacts: feature.artifactRefs.map((artifact) => artifact.label),
92
+ artifactRefs: feature.artifactRefs
93
+ }))
94
+ ].sort((left, right) => left.time.localeCompare(right.time) || left.id.localeCompare(right.id));
95
+ return { taskId, features, events };
96
+ }
97
+ async function readCollaborationFeatures(taskDir) {
98
+ const root = join(taskDir, "features");
99
+ let entries;
100
+ try {
101
+ entries = await readdir(root, { withFileTypes: true });
102
+ }
103
+ catch {
104
+ return [];
105
+ }
106
+ const features = await Promise.all(entries
107
+ .filter((entry) => entry.isDirectory())
108
+ .map(async (entry) => {
109
+ const dir = join(root, entry.name);
110
+ const [statusText, spec, findings, replies, resolutionText] = await Promise.all([
111
+ readTextIfExists(join(dir, "status.json")),
112
+ readTextIfExists(join(dir, "spec.md")),
113
+ readTextIfExists(join(dir, "critic-findings.jsonl")),
114
+ readTextIfExists(join(dir, "actor-replies.jsonl")),
115
+ readTextIfExists(join(dir, "finding-resolution.json"))
116
+ ]);
117
+ const status = parseJsonValue(statusText, FeatureStatusSchema);
118
+ if (!status) {
119
+ return null;
120
+ }
121
+ const findingEvidence = readMailboxEvidence(findings);
122
+ const replyEvidence = readMailboxEvidence(replies);
123
+ const resolution = parseJsonValue(resolutionText, FindingResolutionSchema);
124
+ return {
125
+ id: status.feature_id,
126
+ title: status.title?.trim() || featureSpecTitle(spec) || status.feature_id,
127
+ description: status.description?.trim() ?? "",
128
+ dependsOn: status.depends_on ?? [],
129
+ turnId: status.turn_id,
130
+ state: status.state,
131
+ updatedAt: status.updated_at,
132
+ findings: findingEvidence.count,
133
+ replies: replyEvidence.count,
134
+ artifactRefs: [
135
+ { label: "status", path: join(dir, "status.json") },
136
+ { label: "spec", path: join(dir, "spec.md") },
137
+ { label: "critic findings", path: join(dir, "critic-findings.jsonl") },
138
+ { label: "actor replies", path: join(dir, "actor-replies.jsonl") },
139
+ ...(resolution ? [{ label: "finding resolution", path: join(dir, "finding-resolution.json") }] : [])
140
+ ],
141
+ ...(resolution
142
+ ? {
143
+ resolvedFindings: new Set(resolution.fixed_ids).size,
144
+ unresolvedFindings: new Set(resolution.unresolved_ids).size
145
+ }
146
+ : {}),
147
+ ...(findingEvidence.latest ? { latestFinding: findingEvidence.latest } : {}),
148
+ ...(replyEvidence.latest ? { latestReply: replyEvidence.latest } : {})
149
+ };
150
+ }));
151
+ return features
152
+ .filter((feature) => feature !== null)
153
+ .sort((left, right) => left.turnId.localeCompare(right.turnId) || left.id.localeCompare(right.id));
154
+ }
155
+ function collaborationArtifactRefs(paths) {
156
+ return Object.entries(paths)
157
+ .map(([label, path]) => ({ label: label.trim(), path: path.trim() }))
158
+ .filter((artifact) => artifact.label && artifact.path)
159
+ .sort((left, right) => left.label.localeCompare(right.label) || left.path.localeCompare(right.path));
160
+ }
161
+ function parseJsonLines(text, schema) {
162
+ const records = [];
163
+ for (const line of text.split(/\r?\n/)) {
164
+ if (!line.trim()) {
165
+ continue;
166
+ }
167
+ try {
168
+ const parsed = schema.safeParse(JSON.parse(line));
169
+ if (parsed.success) {
170
+ records.push(parsed.data);
171
+ }
172
+ }
173
+ catch {
174
+ // A partial mailbox write must not hide earlier collaboration evidence.
175
+ }
176
+ }
177
+ return records;
178
+ }
179
+ function parseJsonValue(text, schema) {
180
+ try {
181
+ const parsed = schema.safeParse(JSON.parse(text));
182
+ return parsed.success ? parsed.data : null;
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ }
188
+ function readMailboxEvidence(text) {
189
+ let count = 0;
190
+ let latest = null;
191
+ for (const line of text.split(/\r?\n/)) {
192
+ if (!line.trim()) {
193
+ continue;
194
+ }
195
+ try {
196
+ const value = JSON.parse(line);
197
+ if (value && typeof value === "object" && !Array.isArray(value)) {
198
+ count += 1;
199
+ latest = mailboxRecordSummary(value) || latest;
200
+ }
201
+ }
202
+ catch {
203
+ // Ignore partial or malformed mailbox rows.
204
+ }
205
+ }
206
+ return { count, latest };
207
+ }
208
+ function mailboxRecordSummary(value) {
209
+ for (const key of ["message", "summary", "notes", "resolution", "title", "description", "issue", "status"]) {
210
+ const field = value[key];
211
+ if (typeof field === "string" && field.trim()) {
212
+ return field.replace(/\s+/g, " ").trim();
213
+ }
214
+ }
215
+ return "";
216
+ }
217
+ function appendCollaborationEvidence(message, evidence) {
218
+ const base = message.trim();
219
+ if (!base || base.includes(evidence)) {
220
+ return base || evidence;
221
+ }
222
+ return `${base} · ${evidence}`;
223
+ }
224
+ function featureSpecTitle(spec) {
225
+ const title = spec.match(/^Title:\s*(.+)$/m)?.[1]?.trim();
226
+ return title || null;
227
+ }
228
+ function collaborationDialogueAction(type) {
229
+ if (type === "feature.created") {
230
+ return "mailbox created";
231
+ }
232
+ if (type === "actor.completed") {
233
+ return "implementation completed";
234
+ }
235
+ if (type === "critic.completed") {
236
+ return "review completed";
237
+ }
238
+ if (type === "critic.revision_requested") {
239
+ return "revision requested";
240
+ }
241
+ return humanizeCollaborationType(type);
242
+ }
243
+ function collaborationWaveAction(type) {
244
+ const action = type.replace(/^feature\.wave_/, "wave ");
245
+ return humanizeCollaborationType(action);
246
+ }
247
+ function collaborationFeatureStateAction(state) {
248
+ if (state === "approved") {
249
+ return "feature approved";
250
+ }
251
+ if (state === "revision_needed") {
252
+ return "revision pending";
253
+ }
254
+ return `feature ${humanizeFeatureState(state)}`;
255
+ }
256
+ function humanizeFeatureState(state) {
257
+ return state.replaceAll("_", " ");
258
+ }
259
+ function humanizeCollaborationType(type) {
260
+ return type.replace(/[._-]+/g, " ").replace(/\s+/g, " ").trim();
261
+ }
@@ -0,0 +1,14 @@
1
+ import { ZodError } from "zod";
2
+ export function formatConfigErrorMessage(error) {
3
+ if (error instanceof ZodError) {
4
+ return error.issues
5
+ .map((issue) => ({
6
+ message: issue.message,
7
+ path: issue.path.map(String).join(".") || "config"
8
+ }))
9
+ .sort((left, right) => left.path.localeCompare(right.path))
10
+ .map((issue) => `${issue.path}: ${issue.message}`)
11
+ .join("\n");
12
+ }
13
+ return error instanceof Error ? error.message : String(error);
14
+ }