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.
Files changed (40) hide show
  1. package/.parallel-codex/config.example.toml +62 -0
  2. package/LICENSE +21 -0
  3. package/README.md +150 -0
  4. package/dist/bootstrap.js +25 -0
  5. package/dist/cli-args.js +26 -0
  6. package/dist/cli.js +48 -0
  7. package/dist/core/config.js +304 -0
  8. package/dist/core/file-store.js +56 -0
  9. package/dist/core/paths.js +17 -0
  10. package/dist/core/router.js +151 -0
  11. package/dist/core/session-index.js +180 -0
  12. package/dist/core/session-manager.js +264 -0
  13. package/dist/doctor.js +121 -0
  14. package/dist/domain/schemas.js +78 -0
  15. package/dist/orchestrator/collaboration-channel.js +103 -0
  16. package/dist/orchestrator/orchestrator.js +545 -0
  17. package/dist/orchestrator/prompts.js +124 -0
  18. package/dist/orchestrator/supervisor-summary.js +38 -0
  19. package/dist/tui/App.js +740 -0
  20. package/dist/tui/AppShell.js +129 -0
  21. package/dist/tui/InputBar.js +141 -0
  22. package/dist/tui/StatusBar.js +288 -0
  23. package/dist/tui/TerminalOutput.js +22 -0
  24. package/dist/tui/WorkerOutputView.js +4015 -0
  25. package/dist/tui/chat-input.js +37 -0
  26. package/dist/tui/display-width.js +132 -0
  27. package/dist/tui/keyboard.js +38 -0
  28. package/dist/tui/native-input.js +120 -0
  29. package/dist/tui/raw-input-decoder.js +18 -0
  30. package/dist/tui/scrolling.js +25 -0
  31. package/dist/tui/status-line.js +90 -0
  32. package/dist/tui/task-memory.js +26 -0
  33. package/dist/tui/terminal-screen.js +168 -0
  34. package/dist/version.js +1 -0
  35. package/dist/workers/mock-adapter.js +62 -0
  36. package/dist/workers/native-attach.js +189 -0
  37. package/dist/workers/process-adapter.js +265 -0
  38. package/dist/workers/registry.js +32 -0
  39. package/dist/workers/types.js +1 -0
  40. package/package.json +53 -0
@@ -0,0 +1,545 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { ensureDir, pathExists, readJson, readTextIfExists, writeJson, writeText } from "../core/file-store.js";
4
+ import { routeRequestWithCodex } from "../core/router.js";
5
+ import { TaskMetaSchema, WorkerStatusSchema } from "../domain/schemas.js";
6
+ import { getAdapter } from "../workers/registry.js";
7
+ import { appendFeatureDialogue, createFeatureChannel, featurePromptContext, updateFeatureStatus, writeFeatureDecision } from "./collaboration-channel.js";
8
+ import { buildActorPrompt, buildCriticPrompt, buildJudgePrompt } from "./prompts.js";
9
+ import { buildSupervisorSummary } from "./supervisor-summary.js";
10
+ export class Orchestrator {
11
+ config;
12
+ sessions;
13
+ workers;
14
+ routeRunner;
15
+ constructor(config, sessions, workers, routeRunner) {
16
+ this.config = config;
17
+ this.sessions = sessions;
18
+ this.workers = workers;
19
+ this.routeRunner = routeRunner;
20
+ }
21
+ async handleRequest(input) {
22
+ const route = await routeRequestWithCodex(input.request, this.config, this.routeRunner, input.cwd);
23
+ const workers = [];
24
+ if (route.mode === "simple") {
25
+ input.onStatus?.({ taskId: "main", main: "running" });
26
+ const output = await this.runMain(input, workers);
27
+ input.onStatus?.({ taskId: "main", main: "done" });
28
+ return {
29
+ mode: "simple",
30
+ taskId: null,
31
+ summary: extractMainResponse(output) || emptyMainResponseSummary(),
32
+ workers
33
+ };
34
+ }
35
+ const task = await this.sessions.createTask({
36
+ request: input.request,
37
+ cwd: input.cwd,
38
+ route
39
+ });
40
+ const turn = (await this.sessions.latestTurn(task)) ?? {
41
+ turnId: "0001",
42
+ dir: join(task.dir, "turns", "0001"),
43
+ metaPath: join(task.dir, "turns", "0001", "turn.json"),
44
+ userPath: join(task.dir, "turns", "0001", "user.md"),
45
+ routePath: join(task.dir, "turns", "0001", "route.json")
46
+ };
47
+ try {
48
+ await this.sessions.updateTaskStatus(task, "judging");
49
+ input.onStatus?.({ taskId: task.id, judge: "running", actor: "waiting", critic: "waiting" });
50
+ const judge = await this.runJudge(input, task, route.judge_engine, workers, turn);
51
+ const feature = await createFeatureChannel({
52
+ task,
53
+ turn,
54
+ request: input.request,
55
+ judgeDir: judge.dir
56
+ });
57
+ await this.sessions.updateTaskStatus(task, "actor_running");
58
+ await updateFeatureStatus(feature, "actor_running");
59
+ input.onStatus?.({ taskId: task.id, judge: "done", actor: "running", critic: "waiting" });
60
+ let actor = await this.runActor(input, task, route.actor_engine, judge.dir, workers, turn, feature);
61
+ await this.sessions.updateTaskStatus(task, "critic_running");
62
+ await updateFeatureStatus(feature, "critic_running");
63
+ input.onStatus?.({ taskId: task.id, judge: "done", actor: "done", critic: "running" });
64
+ let critic = await this.runCritic(input, task, route.critic_engine, judge.dir, actor.dir, workers, turn, feature);
65
+ const review = await readTextIfExists(`${critic.dir}/review.md`);
66
+ if (review.includes("REVISION_REQUIRED")) {
67
+ await this.sessions.updateTaskStatus(task, "revision_needed");
68
+ await updateFeatureStatus(feature, "revision_needed");
69
+ await appendFeatureDialogue(feature, "critic.revision_requested", "critic", "Critic requested Actor revision.", {
70
+ review: join(critic.dir, "review.md"),
71
+ findings: feature.criticFindingsPath
72
+ });
73
+ input.onStatus?.({ taskId: task.id, judge: "done", actor: "revision", critic: "done" });
74
+ actor = await this.runActor(input, task, route.actor_engine, judge.dir, workers, turn, feature, buildRevisionRequest(review, feature));
75
+ await this.sessions.updateTaskStatus(task, "critic_running");
76
+ await updateFeatureStatus(feature, "critic_running");
77
+ input.onStatus?.({ taskId: task.id, judge: "done", actor: "done", critic: "rerunning" });
78
+ critic = await this.runCritic(input, task, route.critic_engine, judge.dir, actor.dir, workers, turn, feature);
79
+ }
80
+ await this.sessions.updateTaskStatus(task, "done");
81
+ input.onStatus?.({ taskId: task.id, judge: "done", actor: "done", critic: "done" });
82
+ const summary = await buildSupervisorSummary({
83
+ judgeDir: judge.dir,
84
+ actorDir: actor.dir,
85
+ criticDir: critic.dir,
86
+ turnDir: turn.dir,
87
+ featureActorWorklogPath: feature.actorWorklogPath,
88
+ featureCriticFindingsPath: feature.criticFindingsPath
89
+ });
90
+ await writeText(join(turn.dir, "supervisor-summary.md"), `${summary}\n`);
91
+ await writeFeatureDecision(feature, summary);
92
+ await updateFeatureStatus(feature, "approved");
93
+ return {
94
+ mode: "complex",
95
+ taskId: task.id,
96
+ summary,
97
+ workers
98
+ };
99
+ }
100
+ catch (error) {
101
+ await this.sessions.updateTaskStatus(task, "failed");
102
+ throw error;
103
+ }
104
+ }
105
+ async handleTaskTurn(input) {
106
+ const task = this.sessions.taskFromId(input.taskId);
107
+ const route = await routeRequestWithCodex(input.request, this.config, this.routeRunner, input.cwd);
108
+ const turn = await this.sessions.appendTurn(task, {
109
+ request: input.request,
110
+ route
111
+ });
112
+ const workers = [];
113
+ const judgeEngine = route.judge_engine;
114
+ const actorEngine = route.actor_engine;
115
+ const criticEngine = route.critic_engine;
116
+ const judge = this.workerFiles(task, `judge-${judgeEngine}`);
117
+ const feature = await createFeatureChannel({
118
+ task,
119
+ turn,
120
+ request: input.request,
121
+ judgeDir: judge.dir
122
+ });
123
+ try {
124
+ await this.sessions.updateTaskStatus(task, "actor_running");
125
+ await updateFeatureStatus(feature, "actor_running");
126
+ input.onStatus?.({ taskId: task.id, judge: "done", actor: "running", critic: "waiting" });
127
+ let actor = await this.runActor(input, task, actorEngine, judge.dir, workers, turn, feature);
128
+ await this.sessions.updateTaskStatus(task, "critic_running");
129
+ await updateFeatureStatus(feature, "critic_running");
130
+ input.onStatus?.({ taskId: task.id, judge: "done", actor: "done", critic: "running" });
131
+ let critic = await this.runCritic(input, task, criticEngine, judge.dir, actor.dir, workers, turn, feature);
132
+ const review = await readTextIfExists(`${critic.dir}/review.md`);
133
+ if (review.includes("REVISION_REQUIRED")) {
134
+ await this.sessions.updateTaskStatus(task, "revision_needed");
135
+ await updateFeatureStatus(feature, "revision_needed");
136
+ await appendFeatureDialogue(feature, "critic.revision_requested", "critic", "Critic requested Actor revision.", {
137
+ review: join(critic.dir, "review.md"),
138
+ findings: feature.criticFindingsPath
139
+ });
140
+ input.onStatus?.({ taskId: task.id, judge: "done", actor: "revision", critic: "done" });
141
+ actor = await this.runActor(input, task, actorEngine, judge.dir, workers, turn, feature, buildRevisionRequest(review, feature));
142
+ await this.sessions.updateTaskStatus(task, "critic_running");
143
+ await updateFeatureStatus(feature, "critic_running");
144
+ input.onStatus?.({ taskId: task.id, judge: "done", actor: "done", critic: "rerunning" });
145
+ critic = await this.runCritic(input, task, criticEngine, judge.dir, actor.dir, workers, turn, feature);
146
+ }
147
+ await this.sessions.updateTaskStatus(task, "done");
148
+ input.onStatus?.({ taskId: task.id, judge: "done", actor: "done", critic: "done" });
149
+ const summary = await buildSupervisorSummary({
150
+ judgeDir: judge.dir,
151
+ actorDir: actor.dir,
152
+ criticDir: critic.dir,
153
+ turnDir: turn.dir,
154
+ featureActorWorklogPath: feature.actorWorklogPath,
155
+ featureCriticFindingsPath: feature.criticFindingsPath
156
+ });
157
+ await writeText(join(turn.dir, "supervisor-summary.md"), `${summary}\n`);
158
+ await writeFeatureDecision(feature, summary);
159
+ await updateFeatureStatus(feature, "approved");
160
+ return {
161
+ mode: "complex",
162
+ taskId: task.id,
163
+ summary,
164
+ workers
165
+ };
166
+ }
167
+ catch (error) {
168
+ await this.sessions.updateTaskStatus(task, "failed");
169
+ throw error;
170
+ }
171
+ }
172
+ async routeTaskFollowUp(input) {
173
+ const task = this.sessions.taskFromId(input.taskId);
174
+ const meta = (await pathExists(task.metaPath)) ? await readJson(task.metaPath, TaskMetaSchema) : null;
175
+ const route = await routeRequestWithCodex([
176
+ "Active task follow-up routing.",
177
+ `Task id: ${input.taskId}`,
178
+ `Task status: ${meta?.status ?? "unknown"}`,
179
+ "",
180
+ "Choose simple for status/log/reason/explanation questions that can be answered from session files.",
181
+ "Choose complex for requests that change direction, ask for code changes, reruns, fixes, new experiments, or implementation work.",
182
+ "",
183
+ "Follow-up request:",
184
+ input.request
185
+ ].join("\n"), this.config, this.routeRunner, input.cwd);
186
+ return {
187
+ mode: route.mode,
188
+ taskId: route.mode === "complex" ? input.taskId : null,
189
+ reason: route.reason
190
+ };
191
+ }
192
+ async answerTaskQuestion(input) {
193
+ const task = this.sessions.taskFromId(input.taskId);
194
+ const meta = (await pathExists(task.metaPath)) ? await readJson(task.metaPath, TaskMetaSchema) : null;
195
+ const workerSummaries = await Promise.all(["judge", "actor", "critic"].map((role) => this.readLatestWorkerQuestionSummary(task, role)));
196
+ const workers = workerSummaries.filter((worker) => worker !== null);
197
+ const failed = workers.find((worker) => worker.status.state === "failed");
198
+ const latest = failed ?? workers.at(-1);
199
+ const lines = [
200
+ `Task ${task.id}${meta ? ` is ${meta.status}` : ""}.`,
201
+ latest
202
+ ? `${labelWorker(latest.status)}: ${latest.status.state}/${latest.status.phase}: ${latest.status.summary}`
203
+ : "No worker status files found for this task."
204
+ ];
205
+ if (latest?.logTail) {
206
+ lines.push("", "Latest worker log:", latest.logTail);
207
+ }
208
+ return {
209
+ mode: "simple",
210
+ taskId: task.id,
211
+ summary: lines.join("\n"),
212
+ workers: []
213
+ };
214
+ }
215
+ async listTaskWorkers(taskId) {
216
+ const task = this.sessions.taskFromId(taskId);
217
+ if (!(await pathExists(task.dir))) {
218
+ return [];
219
+ }
220
+ const entries = await readdir(task.dir, { withFileTypes: true });
221
+ const workers = [];
222
+ for (const entry of entries) {
223
+ if (!entry.isDirectory()) {
224
+ continue;
225
+ }
226
+ const dir = join(task.dir, entry.name);
227
+ const statusPath = join(dir, "status.json");
228
+ if (!(await pathExists(statusPath))) {
229
+ continue;
230
+ }
231
+ const status = await readJson(statusPath, WorkerStatusSchema);
232
+ workers.push({
233
+ id: status.worker_id,
234
+ role: status.role,
235
+ engine: status.engine,
236
+ label: `${capitalize(status.role)} (${status.engine})`,
237
+ logPath: join(dir, "output.log"),
238
+ statusPath
239
+ });
240
+ }
241
+ return workers.sort((left, right) => workerRoleOrder(left.role) - workerRoleOrder(right.role));
242
+ }
243
+ async runMain(input, workers) {
244
+ const engine = this.config.pairing.main;
245
+ const dir = this.sessions.mainSessionDir();
246
+ const workerId = `main-${engine}`;
247
+ const filesDir = join(dir, workerId);
248
+ const promptPath = join(filesDir, "prompt.md");
249
+ const outputLogPath = join(filesDir, "output.log");
250
+ const statusPath = join(filesDir, "status.json");
251
+ await ensureDir(filesDir);
252
+ await writeText(promptPath, input.request);
253
+ await writeText(outputLogPath, "");
254
+ await writeJson(statusPath, {
255
+ worker_id: workerId,
256
+ role: "main",
257
+ engine,
258
+ state: "idle",
259
+ phase: "initialized",
260
+ last_event_at: new Date().toISOString(),
261
+ summary: "Main chat worker initialized"
262
+ });
263
+ this.recordWorker(input, workers, {
264
+ id: workerId,
265
+ role: "main",
266
+ engine,
267
+ label: `Main (${engine})`,
268
+ logPath: outputLogPath,
269
+ statusPath
270
+ });
271
+ await getAdapter(this.workers, engine).run({
272
+ workerId,
273
+ role: "main",
274
+ engine,
275
+ cwd: input.cwd,
276
+ filesDir,
277
+ promptPath,
278
+ outputLogPath,
279
+ statusPath,
280
+ prompt: input.request
281
+ });
282
+ return readTextIfExists(outputLogPath);
283
+ }
284
+ async runJudge(input, task, engine, workers, turn) {
285
+ const workerId = `judge-${engine}`;
286
+ const judgeFiles = this.workerFiles(task, workerId);
287
+ const judge = await this.sessions.initializeWorker(task, {
288
+ workerId,
289
+ role: "judge",
290
+ engine,
291
+ prompt: buildJudgePrompt({
292
+ request: input.request,
293
+ taskDir: task.dir,
294
+ workerDir: judgeFiles.dir,
295
+ turn: this.promptTurnContext(turn),
296
+ role: this.config.roles.judge
297
+ })
298
+ });
299
+ this.recordWorker(input, workers, {
300
+ id: judge.workerId,
301
+ role: "judge",
302
+ engine,
303
+ label: `Judge (${engine})`,
304
+ logPath: judge.outputLogPath,
305
+ statusPath: judge.statusPath
306
+ });
307
+ const result = await this.runWorkerWithNativeSession(engine, {
308
+ workerId: judge.workerId,
309
+ role: "judge",
310
+ engine,
311
+ cwd: input.cwd,
312
+ filesDir: judge.dir,
313
+ promptPath: judge.promptPath,
314
+ outputLogPath: judge.outputLogPath,
315
+ statusPath: judge.statusPath,
316
+ prompt: await readTextIfExists(judge.promptPath)
317
+ });
318
+ ensureWorkerSuccess(result.workerId, result.exitCode);
319
+ return judge;
320
+ }
321
+ async runActor(input, task, engine, judgeDir, workers, turn, feature, revision) {
322
+ const actor = await this.sessions.initializeWorker(task, {
323
+ workerId: `actor-${engine}`,
324
+ role: "actor",
325
+ engine,
326
+ prompt: buildActorPrompt({
327
+ request: input.request,
328
+ taskDir: task.dir,
329
+ judgeDir,
330
+ turn: this.promptTurnContext(turn),
331
+ feature: featurePromptContext(feature),
332
+ revision,
333
+ role: this.config.roles.actor
334
+ })
335
+ });
336
+ this.recordWorker(input, workers, {
337
+ id: actor.workerId,
338
+ role: "actor",
339
+ engine,
340
+ label: `Actor (${engine})`,
341
+ logPath: actor.outputLogPath,
342
+ statusPath: actor.statusPath
343
+ });
344
+ const result = await this.runWorkerWithNativeSession(engine, {
345
+ workerId: actor.workerId,
346
+ role: "actor",
347
+ engine,
348
+ cwd: input.cwd,
349
+ filesDir: actor.dir,
350
+ promptPath: actor.promptPath,
351
+ outputLogPath: actor.outputLogPath,
352
+ statusPath: actor.statusPath,
353
+ prompt: await readTextIfExists(actor.promptPath)
354
+ });
355
+ ensureWorkerSuccess(result.workerId, result.exitCode);
356
+ await mirrorWorkerFileToFeature(join(actor.dir, "worklog.md"), feature.actorWorklogPath);
357
+ await appendFeatureDialogue(feature, "actor.completed", "actor", "Actor completed feature work.", {
358
+ worklog: actor.outputLogPath,
359
+ feature_worklog: feature.actorWorklogPath,
360
+ replies: feature.actorRepliesPath
361
+ });
362
+ return actor;
363
+ }
364
+ async runCritic(input, task, engine, judgeDir, actorDir, workers, turn, feature) {
365
+ const critic = await this.sessions.initializeWorker(task, {
366
+ workerId: `critic-${engine}`,
367
+ role: "critic",
368
+ engine,
369
+ prompt: buildCriticPrompt({
370
+ request: input.request,
371
+ taskDir: task.dir,
372
+ judgeDir,
373
+ actorDir,
374
+ turn: this.promptTurnContext(turn),
375
+ feature: featurePromptContext(feature),
376
+ role: this.config.roles.critic
377
+ })
378
+ });
379
+ this.recordWorker(input, workers, {
380
+ id: critic.workerId,
381
+ role: "critic",
382
+ engine,
383
+ label: `Critic (${engine})`,
384
+ logPath: critic.outputLogPath,
385
+ statusPath: critic.statusPath
386
+ });
387
+ const result = await this.runWorkerWithNativeSession(engine, {
388
+ workerId: critic.workerId,
389
+ role: "critic",
390
+ engine,
391
+ cwd: input.cwd,
392
+ filesDir: critic.dir,
393
+ promptPath: critic.promptPath,
394
+ outputLogPath: critic.outputLogPath,
395
+ statusPath: critic.statusPath,
396
+ prompt: await readTextIfExists(critic.promptPath)
397
+ });
398
+ ensureWorkerSuccess(result.workerId, result.exitCode);
399
+ await appendFeatureDialogue(feature, "critic.completed", "critic", "Critic completed feature review.", {
400
+ review: join(critic.dir, "review.md"),
401
+ findings: feature.criticFindingsPath
402
+ });
403
+ return critic;
404
+ }
405
+ recordWorker(input, workers, worker) {
406
+ const existingIndex = workers.findIndex((item) => item.id === worker.id);
407
+ if (existingIndex >= 0) {
408
+ workers[existingIndex] = worker;
409
+ }
410
+ else {
411
+ workers.push(worker);
412
+ }
413
+ input.onWorker?.(worker);
414
+ }
415
+ async runWorkerWithNativeSession(engine, spec) {
416
+ const adapter = getAdapter(this.workers, engine);
417
+ const workerFiles = {
418
+ workerId: spec.workerId,
419
+ dir: spec.filesDir,
420
+ promptPath: spec.promptPath,
421
+ outputLogPath: spec.outputLogPath,
422
+ statusPath: spec.statusPath
423
+ };
424
+ const existing = await this.sessions.readNativeSession(workerFiles);
425
+ if (existing) {
426
+ await this.sessions.writeNativeSession(workerFiles, {
427
+ ...existing,
428
+ last_used_at: new Date().toISOString()
429
+ });
430
+ }
431
+ return adapter.run({
432
+ ...spec,
433
+ nativeSession: existing,
434
+ nativeSessionConfig: this.config.workers[engine].nativeSession,
435
+ onNativeSession: async (sessionId) => {
436
+ const now = new Date().toISOString();
437
+ const previous = await this.sessions.readNativeSession(workerFiles);
438
+ const record = {
439
+ engine,
440
+ role: spec.role,
441
+ worker_id: spec.workerId,
442
+ session_id: sessionId,
443
+ scope: "task",
444
+ cwd: spec.cwd,
445
+ created_at: previous?.created_at ?? now,
446
+ last_used_at: now,
447
+ source: previous?.source ?? "output-detected"
448
+ };
449
+ await this.sessions.writeNativeSession(workerFiles, record);
450
+ },
451
+ onNativeSessionRetired: async (_sessionId, reason) => {
452
+ await this.sessions.retireNativeSession(workerFiles, summarizeRetirementReason(reason));
453
+ }
454
+ });
455
+ }
456
+ workerFiles(task, workerId) {
457
+ const dir = join(task.dir, workerId);
458
+ return {
459
+ workerId,
460
+ dir,
461
+ promptPath: join(dir, "prompt.md"),
462
+ outputLogPath: join(dir, "output.log"),
463
+ statusPath: join(dir, "status.json")
464
+ };
465
+ }
466
+ promptTurnContext(turn) {
467
+ return {
468
+ turnId: turn.turnId,
469
+ turnDir: turn.dir,
470
+ previousSummaries: []
471
+ };
472
+ }
473
+ async readLatestWorkerQuestionSummary(task, role) {
474
+ for (const engine of ["codex", "claude", "mock"]) {
475
+ const files = this.workerFiles(task, `${role}-${engine}`);
476
+ if (!(await pathExists(files.statusPath))) {
477
+ continue;
478
+ }
479
+ const status = await readJson(files.statusPath, WorkerStatusSchema);
480
+ return {
481
+ status,
482
+ logTail: tailText(await readTextIfExists(files.outputLogPath), 8)
483
+ };
484
+ }
485
+ return null;
486
+ }
487
+ }
488
+ function ensureWorkerSuccess(workerId, exitCode) {
489
+ if (exitCode !== 0) {
490
+ throw new Error(`${workerId} failed with exit code ${exitCode}`);
491
+ }
492
+ }
493
+ function summarizeRetirementReason(reason) {
494
+ const lines = reason
495
+ .split(/\r?\n/)
496
+ .map((line) => line.trim())
497
+ .filter(Boolean);
498
+ const contextLine = lines.find((line) => /context window|ran out of room|clear earlier history|start a new thread/i.test(line));
499
+ const summary = contextLine ?? lines.at(-1) ?? "Native session retired";
500
+ return summary.length > 240 ? `${summary.slice(0, 237)}...` : summary;
501
+ }
502
+ function buildRevisionRequest(review, feature) {
503
+ return [
504
+ review.trim(),
505
+ "",
506
+ "Feature mailbox:",
507
+ `- Critic findings: ${feature.criticFindingsPath}`,
508
+ `- Actor replies: ${feature.actorRepliesPath}`,
509
+ "Reply to each fixed finding in actor-replies.jsonl."
510
+ ].join("\n");
511
+ }
512
+ async function mirrorWorkerFileToFeature(sourcePath, targetPath) {
513
+ const content = await readTextIfExists(sourcePath);
514
+ if (content.trim()) {
515
+ await writeText(targetPath, content);
516
+ }
517
+ }
518
+ function extractMainResponse(outputLog) {
519
+ return outputLog
520
+ .split("\n")
521
+ .filter((line) => !line.startsWith("$ "))
522
+ .filter((line) => !line.startsWith("[mock:main]"))
523
+ .join("\n")
524
+ .trim();
525
+ }
526
+ function emptyMainResponseSummary() {
527
+ return "简单对话通道没有收到可显示回复。";
528
+ }
529
+ function labelWorker(status) {
530
+ return `${capitalize(status.role)} (${status.engine})`;
531
+ }
532
+ function capitalize(value) {
533
+ return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
534
+ }
535
+ function workerRoleOrder(role) {
536
+ return ["main", "judge", "actor", "critic"].indexOf(role);
537
+ }
538
+ function tailText(text, lines) {
539
+ return text
540
+ .split(/\r?\n/)
541
+ .filter((line) => line.trim())
542
+ .slice(-lines)
543
+ .join("\n")
544
+ .trim();
545
+ }
@@ -0,0 +1,124 @@
1
+ export function buildJudgePrompt(input) {
2
+ const role = roleConfig(input.role, "Judge", [
3
+ "You clarify requirements and write task files. Do not implement code."
4
+ ]);
5
+ return [
6
+ `# Role: ${role.title}`,
7
+ "",
8
+ ...instructionLines(role.instructions),
9
+ "",
10
+ `Task directory: ${input.taskDir}`,
11
+ ...(input.workerDir ? [`Worker directory: ${input.workerDir}`] : []),
12
+ ...turnLines(input.turn),
13
+ "",
14
+ "Write these files in the worker directory above:",
15
+ "- requirements.md",
16
+ "- plan.md",
17
+ "- acceptance.md",
18
+ "- actor-brief.md",
19
+ "- critic-brief.md",
20
+ "",
21
+ "User request:",
22
+ input.request,
23
+ ""
24
+ ].join("\n");
25
+ }
26
+ export function buildActorPrompt(input) {
27
+ const role = roleConfig(input.role, "Actor", [
28
+ "Read Judge files, implement the requested change, and record your work."
29
+ ]);
30
+ return [
31
+ `# Role: ${role.title}`,
32
+ "",
33
+ ...instructionLines(role.instructions),
34
+ "",
35
+ `Task directory: ${input.taskDir}`,
36
+ `Judge directory: ${input.judgeDir}`,
37
+ ...turnLines(input.turn),
38
+ ...featureLines(input.feature),
39
+ "",
40
+ "Read:",
41
+ "- requirements.md",
42
+ "- plan.md",
43
+ "- acceptance.md",
44
+ "- actor-brief.md",
45
+ "",
46
+ "Write in your worker directory:",
47
+ "- worklog.md",
48
+ "- patch.diff when a diff is available",
49
+ "",
50
+ "Feature mailbox writes:",
51
+ "- actor-worklog.md with implementation notes for this feature.",
52
+ "- actor-replies.jsonl with one JSON object per Critic finding you fixed.",
53
+ "",
54
+ input.revision ? `Revision request:\n${input.revision}` : "No Critic revision request is active.",
55
+ "",
56
+ "User request:",
57
+ input.request,
58
+ ""
59
+ ].join("\n");
60
+ }
61
+ export function buildCriticPrompt(input) {
62
+ const role = roleConfig(input.role, "Critic", [
63
+ "Review Actor work against Judge requirements. Lead with blocking findings."
64
+ ]);
65
+ return [
66
+ `# Role: ${role.title}`,
67
+ "",
68
+ ...instructionLines(role.instructions),
69
+ "",
70
+ `Task directory: ${input.taskDir}`,
71
+ `Judge directory: ${input.judgeDir}`,
72
+ `Actor directory: ${input.actorDir ?? ""}`,
73
+ ...turnLines(input.turn),
74
+ ...featureLines(input.feature),
75
+ "",
76
+ "Read Judge files and Actor output.",
77
+ "Read actor-replies.jsonl when reviewing a revision.",
78
+ "",
79
+ "Write review.md in your worker directory. Include APPROVED when no blocking findings remain.",
80
+ "If revision is required, include REVISION_REQUIRED and a concise fix list.",
81
+ "Write critic-findings.jsonl in the feature mailbox with one JSON object per blocking issue.",
82
+ "",
83
+ "User request:",
84
+ input.request,
85
+ ""
86
+ ].join("\n");
87
+ }
88
+ function roleConfig(role, title, instructions) {
89
+ return {
90
+ title: role?.title ?? title,
91
+ instructions: role?.instructions?.length ? role.instructions : instructions
92
+ };
93
+ }
94
+ function instructionLines(instructions) {
95
+ if (instructions.length === 1) {
96
+ return [instructions[0]];
97
+ }
98
+ return instructions.map((instruction) => `- ${instruction}`);
99
+ }
100
+ function featureLines(feature) {
101
+ if (!feature) {
102
+ return [];
103
+ }
104
+ return [
105
+ `Feature id: ${feature.featureId}`,
106
+ `Feature directory: ${feature.featureDir}`,
107
+ `Actor/Critic dialogue log: ${feature.dialoguePath}`,
108
+ `Actor feature worklog: ${feature.actorWorklogPath}`,
109
+ `Critic findings: ${feature.criticFindingsPath}`,
110
+ `Actor replies: ${feature.actorRepliesPath}`,
111
+ `Feature decisions: ${feature.decisionsPath}`
112
+ ];
113
+ }
114
+ function turnLines(turn) {
115
+ if (!turn) {
116
+ return [];
117
+ }
118
+ return [
119
+ `Current turn: ${turn.turnId}`,
120
+ `Current turn directory: ${turn.turnDir}`,
121
+ "Previous turn summaries:",
122
+ ...(turn.previousSummaries?.length ? turn.previousSummaries.map((summary) => `- ${summary}`) : ["- (none)"])
123
+ ];
124
+ }