peon-mem 1.0.7 → 1.0.8
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/dist/compression.js +4 -3
- package/dist/config.d.ts +13 -0
- package/dist/config.js +26 -0
- package/dist/daemon.d.ts +0 -3
- package/dist/daemon.js +12 -137
- package/dist/embedding-store.d.ts +10 -0
- package/dist/embedding-store.js +97 -19
- package/dist/entity-extraction.js +4 -3
- package/dist/global-extraction.js +4 -3
- package/dist/hyde.js +4 -3
- package/dist/logger.d.ts +20 -0
- package/dist/logger.js +73 -6
- package/dist/memory-store.d.ts +5 -1
- package/dist/memory-store.js +158 -12
- package/dist/monitor.js +14 -224
- package/dist/overview.d.ts +18 -6
- package/dist/overview.js +92 -14
- package/dist/processor.d.ts +36 -0
- package/dist/processor.js +85 -2
- package/dist/quality.d.ts +17 -1
- package/dist/quality.js +152 -25
- package/dist/recuration.js +4 -3
- package/dist/reranker.js +4 -3
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +27 -1
- package/package.json +3 -3
- package/scripts/peon-operations-watch.mjs +80 -0
- package/dist/roundtable.d.ts +0 -104
- package/dist/roundtable.js +0 -787
package/dist/roundtable.js
DELETED
|
@@ -1,787 +0,0 @@
|
|
|
1
|
-
import { access, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { constants as fsConstants, existsSync } from "node:fs";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { homedir, tmpdir } from "node:os";
|
|
5
|
-
import { basename, isAbsolute, join } from "node:path";
|
|
6
|
-
const MAX_QUESTION_CHARS = 4_000;
|
|
7
|
-
const MAX_AGENT_OUTPUT_CHARS = 20_000;
|
|
8
|
-
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
9
|
-
const AUTONOMOUS_AGENDA = "Using the current Peon project memory, identify the single most important unresolved problem blocking the project's main objective. Debate the best next move, with special attention to prior failed ideas, available evidence, novelty, measurable impact, and the smallest decisive test. Do not run the test or change anything.";
|
|
10
|
-
export class RoundtableManager {
|
|
11
|
-
options;
|
|
12
|
-
runs = new Map();
|
|
13
|
-
subscribers = new Set();
|
|
14
|
-
runner;
|
|
15
|
-
now;
|
|
16
|
-
queue = Promise.resolve();
|
|
17
|
-
persistQueue = Promise.resolve();
|
|
18
|
-
constructor(options) {
|
|
19
|
-
this.options = options;
|
|
20
|
-
this.runner = options.runner ?? new LocalSubscriptionRunner();
|
|
21
|
-
this.now = options.now ?? (() => new Date().toISOString());
|
|
22
|
-
}
|
|
23
|
-
async initialize() {
|
|
24
|
-
await mkdir(this.options.stateDir, { recursive: true });
|
|
25
|
-
const names = await readdir(this.options.stateDir).catch(() => []);
|
|
26
|
-
for (const name of names.filter((value) => value.endsWith(".json"))) {
|
|
27
|
-
try {
|
|
28
|
-
const parsed = JSON.parse(await readFile(join(this.options.stateDir, name), "utf8"));
|
|
29
|
-
if (!parsed?.id || !parsed.projectPath || !Array.isArray(parsed.turns))
|
|
30
|
-
continue;
|
|
31
|
-
if (!Array.isArray(parsed.activities))
|
|
32
|
-
parsed.activities = [];
|
|
33
|
-
const interrupted = parsed.status === "queued" || parsed.status === "running";
|
|
34
|
-
const restartFailure = parsed.status === "failed" && parsed.error === "Peon restarted before this discussion finished. Start a new roundtable to continue.";
|
|
35
|
-
if (interrupted || restartFailure) {
|
|
36
|
-
parsed.status = "open";
|
|
37
|
-
parsed.stage = "complete";
|
|
38
|
-
delete parsed.error;
|
|
39
|
-
parsed.updatedAt = this.now();
|
|
40
|
-
await this.persist(parsed);
|
|
41
|
-
}
|
|
42
|
-
this.runs.set(parsed.id, parsed);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
// One damaged history file must not prevent the daemon from starting.
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
availability() {
|
|
50
|
-
return this.runner.availability();
|
|
51
|
-
}
|
|
52
|
-
list(projectPath) {
|
|
53
|
-
return [...this.runs.values()]
|
|
54
|
-
.filter((run) => !projectPath || run.projectPath === projectPath)
|
|
55
|
-
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
56
|
-
}
|
|
57
|
-
get(id) {
|
|
58
|
-
return this.runs.get(id);
|
|
59
|
-
}
|
|
60
|
-
subscribe(listener) {
|
|
61
|
-
this.subscribers.add(listener);
|
|
62
|
-
return () => this.subscribers.delete(listener);
|
|
63
|
-
}
|
|
64
|
-
async start(projectPath, rawQuestion) {
|
|
65
|
-
if (!isAbsolute(projectPath))
|
|
66
|
-
throw new Error("projectPath must be absolute");
|
|
67
|
-
if (rawQuestion !== undefined && typeof rawQuestion !== "string") {
|
|
68
|
-
throw new Error("question must be a string");
|
|
69
|
-
}
|
|
70
|
-
const supplied = rawQuestion?.trim() ?? "";
|
|
71
|
-
const question = supplied || AUTONOMOUS_AGENDA;
|
|
72
|
-
if (question.length > MAX_QUESTION_CHARS)
|
|
73
|
-
throw new Error(`question must be at most ${MAX_QUESTION_CHARS} characters`);
|
|
74
|
-
const createdAt = this.now();
|
|
75
|
-
const run = {
|
|
76
|
-
id: crypto.randomUUID(),
|
|
77
|
-
projectPath,
|
|
78
|
-
question,
|
|
79
|
-
mode: supplied ? "focused" : "autonomous",
|
|
80
|
-
status: "queued",
|
|
81
|
-
stage: "queued",
|
|
82
|
-
createdAt,
|
|
83
|
-
updatedAt: createdAt,
|
|
84
|
-
turns: [],
|
|
85
|
-
activities: []
|
|
86
|
-
};
|
|
87
|
-
this.runs.set(run.id, run);
|
|
88
|
-
await this.persist(run);
|
|
89
|
-
this.emit(run);
|
|
90
|
-
// One discussion at a time keeps subscription usage predictable and prevents
|
|
91
|
-
// several local agent processes from competing for the same project.
|
|
92
|
-
this.queue = this.queue.then(() => this.execute(run.id), () => this.execute(run.id));
|
|
93
|
-
return run;
|
|
94
|
-
}
|
|
95
|
-
async approve(id) {
|
|
96
|
-
const run = this.runs.get(id);
|
|
97
|
-
if (!run)
|
|
98
|
-
throw new Error("Roundtable discussion not found");
|
|
99
|
-
if (run.status !== "awaiting_approval")
|
|
100
|
-
throw new Error("This proposal is not waiting for approval");
|
|
101
|
-
const recommendation = latestRecommendation(run);
|
|
102
|
-
if (!recommendation)
|
|
103
|
-
throw new Error("This proposal has no final recommendation");
|
|
104
|
-
await this.options.memory.saveDecision(run.projectPath, run.question, recommendation);
|
|
105
|
-
run.status = "executing";
|
|
106
|
-
run.stage = "implementation";
|
|
107
|
-
run.approvedAt = this.now();
|
|
108
|
-
await this.touch(run);
|
|
109
|
-
this.queue = this.queue.then(() => this.executeApproved(run.id), () => this.executeApproved(run.id));
|
|
110
|
-
return run;
|
|
111
|
-
}
|
|
112
|
-
async message(id, rawMessage) {
|
|
113
|
-
const run = this.runs.get(id);
|
|
114
|
-
if (!run)
|
|
115
|
-
throw new Error("Roundtable discussion not found");
|
|
116
|
-
const startsFollowUp = run.status === "open" ||
|
|
117
|
-
run.status === "awaiting_approval" ||
|
|
118
|
-
run.status === "complete" ||
|
|
119
|
-
run.status === "rejected" ||
|
|
120
|
-
run.status === "failed";
|
|
121
|
-
if (!startsFollowUp) {
|
|
122
|
-
throw new Error("Wait for the current room step to finish before sending a message");
|
|
123
|
-
}
|
|
124
|
-
if (typeof rawMessage !== "string")
|
|
125
|
-
throw new Error("message must be a string");
|
|
126
|
-
const message = rawMessage.trim();
|
|
127
|
-
if (!message)
|
|
128
|
-
throw new Error("message is required");
|
|
129
|
-
if (message.length > MAX_QUESTION_CHARS) {
|
|
130
|
-
throw new Error(`message must be at most ${MAX_QUESTION_CHARS} characters`);
|
|
131
|
-
}
|
|
132
|
-
run.turns.push(makeTurn("user", "discussion", message, this.now()));
|
|
133
|
-
run.status = "running";
|
|
134
|
-
run.stage = "discussion";
|
|
135
|
-
await this.touch(run);
|
|
136
|
-
const next = isCasualGreeting(message)
|
|
137
|
-
? () => this.executeCasualReply(run.id, message)
|
|
138
|
-
: () => this.executeRevision(run.id);
|
|
139
|
-
this.queue = this.queue.then(next, next);
|
|
140
|
-
return run;
|
|
141
|
-
}
|
|
142
|
-
async propose(id) {
|
|
143
|
-
const run = this.runs.get(id);
|
|
144
|
-
if (!run)
|
|
145
|
-
throw new Error("Roundtable discussion not found");
|
|
146
|
-
if (run.status !== "open")
|
|
147
|
-
throw new Error("Wait until the room is open before asking for an agreed action");
|
|
148
|
-
run.status = "running";
|
|
149
|
-
run.stage = "synthesis";
|
|
150
|
-
await this.touch(run);
|
|
151
|
-
this.queue = this.queue.then(() => this.executeProposal(run.id), () => this.executeProposal(run.id));
|
|
152
|
-
return run;
|
|
153
|
-
}
|
|
154
|
-
async continueDiscussion(id) {
|
|
155
|
-
const run = this.runs.get(id);
|
|
156
|
-
if (!run)
|
|
157
|
-
throw new Error("Roundtable discussion not found");
|
|
158
|
-
if (run.status !== "open")
|
|
159
|
-
throw new Error("The room must be open before continuing the discussion");
|
|
160
|
-
run.status = "running";
|
|
161
|
-
run.stage = "discussion";
|
|
162
|
-
await this.touch(run);
|
|
163
|
-
this.queue = this.queue.then(() => this.executeRevision(run.id), () => this.executeRevision(run.id));
|
|
164
|
-
return run;
|
|
165
|
-
}
|
|
166
|
-
async reject(id) {
|
|
167
|
-
const run = this.runs.get(id);
|
|
168
|
-
if (!run)
|
|
169
|
-
throw new Error("Roundtable discussion not found");
|
|
170
|
-
if (run.status !== "awaiting_approval")
|
|
171
|
-
throw new Error("This proposal is not waiting for approval");
|
|
172
|
-
run.status = "rejected";
|
|
173
|
-
run.stage = "complete";
|
|
174
|
-
await this.touch(run);
|
|
175
|
-
return run;
|
|
176
|
-
}
|
|
177
|
-
async execute(id) {
|
|
178
|
-
const run = this.runs.get(id);
|
|
179
|
-
if (!run)
|
|
180
|
-
return;
|
|
181
|
-
try {
|
|
182
|
-
const available = await this.runner.availability();
|
|
183
|
-
const missing = ["codex", "claude"].filter((agent) => !available[agent].available);
|
|
184
|
-
if (missing.length)
|
|
185
|
-
throw new Error(`Unavailable local client: ${missing.join(" and ")}`);
|
|
186
|
-
run.status = "running";
|
|
187
|
-
run.stage = "discussion";
|
|
188
|
-
await this.touch(run);
|
|
189
|
-
const context = await this.options.memory.getContext({
|
|
190
|
-
projectPath: run.projectPath,
|
|
191
|
-
query: run.question,
|
|
192
|
-
maxChars: 7_000
|
|
193
|
-
});
|
|
194
|
-
const sharedBrief = buildSharedBrief(run.question, context);
|
|
195
|
-
const addRoomTurn = async (agent, prompt) => {
|
|
196
|
-
const output = await this.runner.run(agent, prompt, run.projectPath);
|
|
197
|
-
run.turns.push(makeTurn(agent, "discussion", output, this.now()));
|
|
198
|
-
await this.touch(run);
|
|
199
|
-
return output;
|
|
200
|
-
};
|
|
201
|
-
await addRoomTurn("codex", roomOpeningPrompt(sharedBrief));
|
|
202
|
-
await addRoomTurn("claude", roomReplyPrompt("Claude", sharedBrief, run.turns, "Respond directly to Codex. Challenge weak evidence, add missing context, and suggest a stronger shared direction."));
|
|
203
|
-
await addRoomTurn("codex", roomReplyPrompt("Codex", sharedBrief, run.turns, "Answer Claude's challenges directly. Revise or defend the design, and narrow it to a decisive experiment."));
|
|
204
|
-
const latest = await addRoomTurn("claude", roomReplyPrompt("Claude", sharedBrief, run.turns, "Test Codex's revised direction against the evidence. State a concrete disagreement or unresolved risk when one exists; otherwise explain exactly what evidence earns agreement."));
|
|
205
|
-
run.liveSummary = firstSentence(latest);
|
|
206
|
-
run.status = "open";
|
|
207
|
-
run.stage = "complete";
|
|
208
|
-
await this.touch(run);
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
run.status = "failed";
|
|
212
|
-
run.stage = "failed";
|
|
213
|
-
run.error = safeError(error);
|
|
214
|
-
await this.touch(run);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
async executeApproved(id) {
|
|
218
|
-
const run = this.runs.get(id);
|
|
219
|
-
if (!run || run.status !== "executing")
|
|
220
|
-
return;
|
|
221
|
-
try {
|
|
222
|
-
const recommendation = latestRecommendation(run);
|
|
223
|
-
if (!recommendation)
|
|
224
|
-
throw new Error("Approved proposal has no recommendation to implement");
|
|
225
|
-
const context = await this.options.memory.getContext({
|
|
226
|
-
projectPath: run.projectPath,
|
|
227
|
-
query: run.question,
|
|
228
|
-
maxChars: 7_000
|
|
229
|
-
});
|
|
230
|
-
const sharedBrief = buildSharedBrief(run.question, context);
|
|
231
|
-
run.stage = "implementation";
|
|
232
|
-
const collaboratorActivity = await this.startActivity(run, "claude", "review", "Claude is defining independent checks for the approved work");
|
|
233
|
-
const collaboratorPlan = await this.runner.run("claude", collaborationImplementationPrompt(sharedBrief, recommendation), run.projectPath, "review");
|
|
234
|
-
run.turns.push(makeTurn("claude", "implementation", collaboratorPlan, this.now()));
|
|
235
|
-
await this.finishActivity(run, collaboratorActivity, "passed", collaboratorPlan);
|
|
236
|
-
const implementationActivity = await this.startActivity(run, "codex", "code", "Codex is implementing the approved work");
|
|
237
|
-
const changesBeforeImplementation = await collectWorkspaceChanges(run.projectPath);
|
|
238
|
-
const implementation = await this.runner.run("codex", implementationPrompt(sharedBrief, recommendation, collaboratorPlan), run.projectPath, "implement");
|
|
239
|
-
run.turns.push(makeTurn("codex", "implementation", implementation, this.now()));
|
|
240
|
-
await this.finishActivity(run, implementationActivity, "passed", implementation);
|
|
241
|
-
const changesAfterImplementation = await collectWorkspaceChanges(run.projectPath);
|
|
242
|
-
const implementationChanges = changedWorkspaceSnapshot(changesBeforeImplementation, changesAfterImplementation);
|
|
243
|
-
if (implementationChanges.length) {
|
|
244
|
-
await this.addActivity(run, {
|
|
245
|
-
agent: "codex",
|
|
246
|
-
type: "artifact",
|
|
247
|
-
title: `${implementationChanges.length} workspace ${implementationChanges.length === 1 ? "change" : "changes"} visible after implementation`,
|
|
248
|
-
detail: implementationChanges.join("\n"),
|
|
249
|
-
state: "info"
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
run.stage = "review";
|
|
253
|
-
const reviewActivity = await this.startActivity(run, "claude", "review", "Claude is independently checking the implementation");
|
|
254
|
-
const review = await this.runner.run("claude", reviewPrompt(sharedBrief, recommendation, implementation), run.projectPath, "review");
|
|
255
|
-
run.turns.push(makeTurn("claude", "review", review, this.now()));
|
|
256
|
-
await this.finishActivity(run, reviewActivity, "passed", review);
|
|
257
|
-
run.stage = "verification";
|
|
258
|
-
const verificationActivity = await this.startActivity(run, "codex", "test", "Codex is addressing findings and running verification");
|
|
259
|
-
const verification = await this.runner.run("codex", verificationPrompt(sharedBrief, recommendation, implementation, review), run.projectPath, "verify");
|
|
260
|
-
run.turns.push(makeTurn("codex", "verification", verification, this.now()));
|
|
261
|
-
await this.finishActivity(run, verificationActivity, reportFailed(verification) ? "failed" : "passed", verification);
|
|
262
|
-
await this.options.memory.saveResult(run.projectPath, run.question, verification);
|
|
263
|
-
await this.addActivity(run, {
|
|
264
|
-
agent: "room",
|
|
265
|
-
type: "result",
|
|
266
|
-
title: "Local work finished",
|
|
267
|
-
detail: firstSentence(verification),
|
|
268
|
-
state: reportFailed(verification) ? "failed" : "passed"
|
|
269
|
-
});
|
|
270
|
-
run.status = "complete";
|
|
271
|
-
run.stage = "complete";
|
|
272
|
-
await this.touch(run);
|
|
273
|
-
}
|
|
274
|
-
catch (error) {
|
|
275
|
-
await this.failRunningActivities(run, safeError(error));
|
|
276
|
-
run.status = "failed";
|
|
277
|
-
run.stage = "failed";
|
|
278
|
-
run.error = safeError(error);
|
|
279
|
-
await this.touch(run);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
async startActivity(run, agent, type, title) {
|
|
283
|
-
return this.addActivity(run, { agent, type, title, state: "running" });
|
|
284
|
-
}
|
|
285
|
-
async addActivity(run, input) {
|
|
286
|
-
const createdAt = this.now();
|
|
287
|
-
const activity = {
|
|
288
|
-
id: crypto.randomUUID(),
|
|
289
|
-
agent: input.agent,
|
|
290
|
-
type: input.type,
|
|
291
|
-
title: input.title,
|
|
292
|
-
detail: input.detail?.trim().slice(0, MAX_AGENT_OUTPUT_CHARS),
|
|
293
|
-
state: input.state,
|
|
294
|
-
createdAt,
|
|
295
|
-
updatedAt: createdAt
|
|
296
|
-
};
|
|
297
|
-
(run.activities ??= []).push(activity);
|
|
298
|
-
await this.touch(run);
|
|
299
|
-
return activity.id;
|
|
300
|
-
}
|
|
301
|
-
async finishActivity(run, id, state, detail) {
|
|
302
|
-
const activity = (run.activities ?? []).find((item) => item.id === id);
|
|
303
|
-
if (!activity)
|
|
304
|
-
return;
|
|
305
|
-
activity.state = state;
|
|
306
|
-
activity.detail = detail?.trim().slice(0, MAX_AGENT_OUTPUT_CHARS);
|
|
307
|
-
activity.updatedAt = this.now();
|
|
308
|
-
await this.touch(run);
|
|
309
|
-
}
|
|
310
|
-
async failRunningActivities(run, detail) {
|
|
311
|
-
let changed = false;
|
|
312
|
-
for (const activity of run.activities ?? []) {
|
|
313
|
-
if (activity.state !== "running")
|
|
314
|
-
continue;
|
|
315
|
-
activity.state = "failed";
|
|
316
|
-
activity.detail = detail;
|
|
317
|
-
activity.updatedAt = this.now();
|
|
318
|
-
changed = true;
|
|
319
|
-
}
|
|
320
|
-
if (changed)
|
|
321
|
-
await this.touch(run);
|
|
322
|
-
}
|
|
323
|
-
async concludeAgreement(run, sharedBrief, jointDraft) {
|
|
324
|
-
run.stage = "synthesis";
|
|
325
|
-
await this.touch(run);
|
|
326
|
-
const confirmation = await this.runner.run("codex", agreementPrompt(sharedBrief, run.turns), run.projectPath);
|
|
327
|
-
run.turns.push(makeTurn("codex", "discussion", confirmation, this.now()));
|
|
328
|
-
if (/^AGREED\b/i.test(confirmation.trim())) {
|
|
329
|
-
const jointProposal = `${jointDraft.trim()}\n\nAgreement check\n- Claude prepared the shared draft after the discussion.\n- Codex confirmed: ${confirmation.trim()}`;
|
|
330
|
-
run.turns.push(makeTurn("joint", "recommendation", jointProposal, this.now()));
|
|
331
|
-
await this.options.memory.saveProposal(run.projectPath, run.question, jointProposal);
|
|
332
|
-
run.status = "awaiting_approval";
|
|
333
|
-
run.liveSummary = "Codex and Claude have an agreed action ready for your review.";
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
run.status = "open";
|
|
337
|
-
}
|
|
338
|
-
run.stage = "complete";
|
|
339
|
-
await this.touch(run);
|
|
340
|
-
}
|
|
341
|
-
async executeRevision(id) {
|
|
342
|
-
const run = this.runs.get(id);
|
|
343
|
-
if (!run || run.status !== "running")
|
|
344
|
-
return;
|
|
345
|
-
try {
|
|
346
|
-
const context = await this.options.memory.getContext({
|
|
347
|
-
projectPath: run.projectPath,
|
|
348
|
-
query: run.question,
|
|
349
|
-
maxChars: 7_000
|
|
350
|
-
});
|
|
351
|
-
const sharedBrief = buildSharedBrief(run.question, context);
|
|
352
|
-
const latestAgent = [...run.turns].reverse().find((turn) => turn.agent === "codex" || turn.agent === "claude")?.agent;
|
|
353
|
-
let agent = latestAgent === "claude" ? "codex" : "claude";
|
|
354
|
-
let settledInARow = 0;
|
|
355
|
-
for (let turn = 0; turn < 8; turn += 1) {
|
|
356
|
-
const instruction = turn === 0
|
|
357
|
-
? "Respond naturally to the user's newest message. Answer it directly and begin a useful discussion with the other agent."
|
|
358
|
-
: "Reply directly to the other agent's latest point. Challenge it or add useful evidence when needed; do not repeat points already made.";
|
|
359
|
-
const raw = await this.runner.run(agent, debateReplyPrompt(agent === "codex" ? "Codex" : "Claude", sharedBrief, run.turns, instruction), run.projectPath);
|
|
360
|
-
const parsed = parseDebateReply(raw);
|
|
361
|
-
run.turns.push(makeTurn(agent, "discussion", parsed.content, this.now()));
|
|
362
|
-
if (parsed.summary)
|
|
363
|
-
run.liveSummary = parsed.summary;
|
|
364
|
-
await this.touch(run);
|
|
365
|
-
settledInARow = parsed.settled ? settledInARow + 1 : 0;
|
|
366
|
-
if (settledInARow >= 2)
|
|
367
|
-
break;
|
|
368
|
-
agent = agent === "claude" ? "codex" : "claude";
|
|
369
|
-
}
|
|
370
|
-
run.status = "open";
|
|
371
|
-
run.stage = "complete";
|
|
372
|
-
await this.touch(run);
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
run.status = "failed";
|
|
376
|
-
run.stage = "failed";
|
|
377
|
-
run.error = safeError(error);
|
|
378
|
-
await this.touch(run);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
async executeCasualReply(id, message) {
|
|
382
|
-
const run = this.runs.get(id);
|
|
383
|
-
if (!run || run.status !== "running")
|
|
384
|
-
return;
|
|
385
|
-
try {
|
|
386
|
-
const reply = async (agent) => {
|
|
387
|
-
const output = await this.runner.run(agent, casualReplyPrompt(agent, message), run.projectPath, "chat");
|
|
388
|
-
run.turns.push(makeTurn(agent, "discussion", output, this.now()));
|
|
389
|
-
await this.touch(run);
|
|
390
|
-
};
|
|
391
|
-
await Promise.all([reply("codex"), reply("claude")]);
|
|
392
|
-
run.status = "open";
|
|
393
|
-
run.stage = "complete";
|
|
394
|
-
await this.touch(run);
|
|
395
|
-
}
|
|
396
|
-
catch (error) {
|
|
397
|
-
run.status = "failed";
|
|
398
|
-
run.stage = "failed";
|
|
399
|
-
run.error = safeError(error);
|
|
400
|
-
await this.touch(run);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
async executeProposal(id) {
|
|
404
|
-
const run = this.runs.get(id);
|
|
405
|
-
if (!run || run.status !== "running" || run.stage !== "synthesis")
|
|
406
|
-
return;
|
|
407
|
-
try {
|
|
408
|
-
const context = await this.options.memory.getContext({
|
|
409
|
-
projectPath: run.projectPath,
|
|
410
|
-
query: run.question,
|
|
411
|
-
maxChars: 7_000
|
|
412
|
-
});
|
|
413
|
-
const sharedBrief = buildSharedBrief(run.question, context);
|
|
414
|
-
const jointDraft = await this.runner.run("claude", roomReplyPrompt("Claude", sharedBrief, run.turns, "The user explicitly asked the room to form an agreed action. Draft the smallest concrete action supported by the conversation. Preserve any disagreement instead of hiding it."), run.projectPath);
|
|
415
|
-
run.turns.push(makeTurn("claude", "discussion", jointDraft, this.now()));
|
|
416
|
-
await this.touch(run);
|
|
417
|
-
await this.concludeAgreement(run, sharedBrief, jointDraft);
|
|
418
|
-
}
|
|
419
|
-
catch (error) {
|
|
420
|
-
run.status = "failed";
|
|
421
|
-
run.stage = "failed";
|
|
422
|
-
run.error = safeError(error);
|
|
423
|
-
await this.touch(run);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
async touch(run) {
|
|
427
|
-
run.updatedAt = this.now();
|
|
428
|
-
await this.persist(run);
|
|
429
|
-
this.emit(run);
|
|
430
|
-
}
|
|
431
|
-
emit(run) {
|
|
432
|
-
for (const listener of this.subscribers) {
|
|
433
|
-
try {
|
|
434
|
-
listener(run);
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
// A disconnected monitor must never interrupt the research workflow.
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
async persist(run) {
|
|
442
|
-
// Snapshot now, then serialize writes. Approval can arrive immediately after
|
|
443
|
-
// synthesis and would otherwise race the final proposal persist on the same
|
|
444
|
-
// `.tmp` path, occasionally losing or renaming the other writer's file.
|
|
445
|
-
const content = `${JSON.stringify(run, null, 2)}\n`;
|
|
446
|
-
const path = join(this.options.stateDir, `${run.id}.json`);
|
|
447
|
-
const task = this.persistQueue.then(async () => {
|
|
448
|
-
await mkdir(this.options.stateDir, { recursive: true });
|
|
449
|
-
const tempPath = `${path}.${crypto.randomUUID()}.tmp`;
|
|
450
|
-
await writeFile(tempPath, content, "utf8");
|
|
451
|
-
await rename(tempPath, path);
|
|
452
|
-
});
|
|
453
|
-
this.persistQueue = task.catch(() => undefined);
|
|
454
|
-
await task;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
export class LocalSubscriptionRunner {
|
|
458
|
-
async availability() {
|
|
459
|
-
const [codex, claude] = await Promise.all([resolveCodexBinary(), resolveClaudeBinary()]);
|
|
460
|
-
const [codexAuthenticated, claudeAuthenticated] = await Promise.all([
|
|
461
|
-
codex ? isCodexAuthenticated(codex) : Promise.resolve(false),
|
|
462
|
-
claude ? isClaudeAuthenticated(claude) : Promise.resolve(false)
|
|
463
|
-
]);
|
|
464
|
-
return {
|
|
465
|
-
codex: {
|
|
466
|
-
available: Boolean(codex && codexAuthenticated),
|
|
467
|
-
label: !codex ? "Codex client not found" : codexAuthenticated ? "Codex via ChatGPT login" : "Codex login required"
|
|
468
|
-
},
|
|
469
|
-
claude: {
|
|
470
|
-
available: Boolean(claude && claudeAuthenticated),
|
|
471
|
-
label: !claude ? "Claude client not found" : claudeAuthenticated ? "Claude Code via Claude login" : "Claude login required"
|
|
472
|
-
}
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
async run(agent, prompt, projectPath, mode = "research") {
|
|
476
|
-
if (agent === "codex")
|
|
477
|
-
return runCodex(prompt, projectPath, mode);
|
|
478
|
-
return runClaude(prompt, projectPath, mode);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
function makeTurn(agent, kind, raw, createdAt) {
|
|
482
|
-
const content = raw.trim().slice(0, MAX_AGENT_OUTPUT_CHARS);
|
|
483
|
-
if (!content)
|
|
484
|
-
throw new Error(`${agent} returned an empty response`);
|
|
485
|
-
return { id: crypto.randomUUID(), agent, kind, content, createdAt };
|
|
486
|
-
}
|
|
487
|
-
function latestRecommendation(run) {
|
|
488
|
-
return [...run.turns].reverse().find((turn) => turn.kind === "recommendation")?.content;
|
|
489
|
-
}
|
|
490
|
-
function isCasualGreeting(message) {
|
|
491
|
-
const normalized = message.toLowerCase().replace(/[^a-z\s']/g, " ").replace(/\s+/g, " ").trim();
|
|
492
|
-
return /^(hi|hello|hey|hiya|good morning|good afternoon|good evening|how are you|what's up|whats up)( everyone| both| guys| codex| claude| there)?$/.test(normalized);
|
|
493
|
-
}
|
|
494
|
-
function casualReplyPrompt(agent, message) {
|
|
495
|
-
return `You are ${agent === "codex" ? "Codex" : "Claude"} in a three-person thesis room with the user and the other agent. The user said: ${JSON.stringify(message)}. Reply like a normal friendly teammate in no more than two short sentences. This is casual conversation: do not analyze the thesis, load project context, use tools, propose work, or make a decision.`;
|
|
496
|
-
}
|
|
497
|
-
function buildSharedBrief(question, context) {
|
|
498
|
-
const sections = [
|
|
499
|
-
["Project summary", context.summary],
|
|
500
|
-
["Relevant memory", context.memories],
|
|
501
|
-
["Prior decisions", context.decisions],
|
|
502
|
-
["Preferences", context.preferences],
|
|
503
|
-
["Open questions", context.openQuestions],
|
|
504
|
-
["Artifacts", context.artifacts]
|
|
505
|
-
]
|
|
506
|
-
.filter((entry) => entry[1]?.trim())
|
|
507
|
-
.map((entry) => `## ${entry[0]}\n${entry[1]}`)
|
|
508
|
-
.join("\n\n");
|
|
509
|
-
return `# Question\n${question}\n\n# Communication style\nSpeak as a helpful thesis teammate, not as a paper or technical report. Use plain everyday language and keep the whole message under 220 words. Start with the practical meaning for the thesis. Do not show an evidence ledger, table, long numbered list, or dense technical detail. Avoid jargon and unexplained acronyms. If a technical term is unavoidable, explain it with a one-sentence example. For a proposed action, use only these short labels: What, Why, How, Success, Stop.\n\n# Repository isolation\nIf an isolated \`workspace/\` directory exists, treat it as the repository root for every inspection, edit, and test. The parent project directory is the copied Peon memory container and must not be treated as the source checkout.\n\n# Shared Peon context\n${sections || "No relevant project memory was found."}`;
|
|
510
|
-
}
|
|
511
|
-
function roomOpeningPrompt(sharedBrief) {
|
|
512
|
-
return `${sharedBrief}\n\n# Shared-room opening\nYou are Codex, the first speaker in a live research room with Claude. Privately check the project's objective, supervisor constraints, reliable baseline, completed and failed experiments, current work, blockers, and relevant research. Search the copied Peon files and isolated repository only when the summary is insufficient. Do not print that audit or a technical report. In the room, say only the most important unresolved problem, why it matters in simple words, and one concrete question or tentative direction for Claude. Do not present a final proposal yet. Use read-only inspection and primary sources when useful. Do not modify files, submit jobs, contact anyone, or reveal private chain-of-thought.`;
|
|
513
|
-
}
|
|
514
|
-
function formatRoomTranscript(turns) {
|
|
515
|
-
return turns.map((turn) => `## ${turn.agent}\n${turn.content}`).join("\n\n");
|
|
516
|
-
}
|
|
517
|
-
function roomReplyPrompt(name, sharedBrief, turns, instruction) {
|
|
518
|
-
return `${sharedBrief}\n\n# Live room transcript\n${formatRoomTranscript(turns)}\n\n# Your next room message\nYou are ${name}, an equal research partner in this room. ${instruction} Respond to the newest room messages, including any message from the user, and speak to the other agent directly. Build on the transcript rather than restarting the analysis. Do not agree merely to be cooperative: test the strongest assumption, identify contrary evidence, and say clearly when the evidence is insufficient. If you agree, name the concrete evidence that earns agreement and any uncertainty that remains. Cite concrete evidence when it changes the decision. Keep the discussion focused on the thesis objective and a publishable, measurable next direction. Do not modify files, submit jobs, contact anyone, invent results, or reveal private chain-of-thought.`;
|
|
519
|
-
}
|
|
520
|
-
function debateReplyPrompt(name, sharedBrief, turns, instruction) {
|
|
521
|
-
return `${roomReplyPrompt(name, sharedBrief, turns, instruction)}\n\n# Natural stopping signal\nAfter your visible reply, add exactly one plain sentence summarizing where the debate now stands inside [[ROOM_SUMMARY: ...]]. Then end with exactly [[ROOM:CONTINUE]] when there is still a useful disagreement, unanswered question, or new evidence for the other agent to address. End with exactly [[ROOM:SETTLED]] only when you have no useful correction or addition left. Both markers are hidden from the user. Never create a proposal merely because the discussion is settled.`;
|
|
522
|
-
}
|
|
523
|
-
function parseDebateReply(raw) {
|
|
524
|
-
const trimmed = raw.trim();
|
|
525
|
-
const full = trimmed.match(/\[\[ROOM_SUMMARY:\s*([\s\S]*?)\]\]\s*\[\[ROOM:(CONTINUE|SETTLED)\]\]\s*$/i);
|
|
526
|
-
if (full) {
|
|
527
|
-
return {
|
|
528
|
-
content: trimmed.slice(0, full.index).trim(),
|
|
529
|
-
settled: full[2].toUpperCase() === "SETTLED",
|
|
530
|
-
summary: full[1].replace(/\s+/g, " ").trim().slice(0, 280)
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
const marker = trimmed.match(/\[\[ROOM:(CONTINUE|SETTLED)\]\]\s*$/i);
|
|
534
|
-
return {
|
|
535
|
-
content: marker ? trimmed.slice(0, marker.index).trim() : trimmed,
|
|
536
|
-
settled: marker?.[1].toUpperCase() === "SETTLED"
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
function firstSentence(value) {
|
|
540
|
-
const plain = value.replace(/[#>*_`|\[\]()]/g, " ").replace(/\s+/g, " ").trim();
|
|
541
|
-
const sentence = plain.match(/^.{1,280}?(?:[.!?](?:\s|$)|$)/)?.[0] ?? plain.slice(0, 280);
|
|
542
|
-
return sentence.trim().slice(0, 280);
|
|
543
|
-
}
|
|
544
|
-
function agreementPrompt(sharedBrief, turns) {
|
|
545
|
-
return `${sharedBrief}\n\n# Complete live room transcript\n${formatRoomTranscript(turns)}\n\n# Agreement check\nYou are Codex, still speaking inside the same room. Check whether Claude's newest joint draft honestly reflects the evidence and the points both agents reached together. Start your answer with exactly AGREED if you accept the draft as the shared next action, or CHANGES NEEDED if you do not. Then explain the reason briefly in plain language. Do not rewrite or synthesize the draft, modify anything, or pretend agreement.`;
|
|
546
|
-
}
|
|
547
|
-
function collaborationImplementationPrompt(sharedBrief, recommendation) {
|
|
548
|
-
return `${sharedBrief}\n\n# User-approved proposal\n${recommendation}\n\n# Equal-partner implementation check\nYou are Claude, an equal implementation partner—not a passive approver. Inspect the isolated workspace read-only. Translate the approved action into concrete acceptance checks, identify the most likely implementation mistake, and point to the files or evidence Codex should inspect. Challenge any unsupported assumption in the agreed action. Do not edit files or run external jobs. Return a short, actionable handoff that Codex must address.`;
|
|
549
|
-
}
|
|
550
|
-
function implementationPrompt(sharedBrief, recommendation, collaboratorPlan) {
|
|
551
|
-
return `${sharedBrief}\n\n# User-approved proposal\n${recommendation}\n\n# Claude's independent implementation checks\n${collaboratorPlan}\n\n# Approved implementation\nYou are Codex, an equal implementation partner. Address Claude's concrete checks and explicitly explain any point you reject. Implement the smallest safe local version of the approved proposal in this project. Preserve unrelated and pre-existing changes. Follow repository instructions. Run proportionate local tests and report the exact files changed and test results. Do not submit cluster or cloud jobs, spend money, contact anyone, expose secrets, delete data, or commit/push. If the approved work needs any of those actions, stop before that step and report the blocker clearly.`;
|
|
552
|
-
}
|
|
553
|
-
function reviewPrompt(sharedBrief, recommendation, implementation) {
|
|
554
|
-
return `${sharedBrief}\n\n# Approved proposal\n${recommendation}\n\n# Codex implementation report\n${implementation}\n\n# Independent review\nInspect the current project read-only and review whether the implementation matches the approved scope. Check correctness, experimental leakage, confounds, missing tests, novelty claims, and whether unrelated work was disturbed. Do not edit files or run external jobs. Return only actionable findings, ordered by severity, and say explicitly if no blocking problem is found.`;
|
|
555
|
-
}
|
|
556
|
-
function verificationPrompt(sharedBrief, recommendation, implementation, review) {
|
|
557
|
-
return `${sharedBrief}\n\n# Approved proposal\n${recommendation}\n\n# Initial implementation report\n${implementation}\n\n# Claude review\n${review}\n\n# Verification and final report\nAddress valid review findings within the approved scope, then run the relevant local tests again. Preserve unrelated changes. Report what is now implemented, exact tests and results, remaining risks, and the next external experiment if one is still needed. Do not submit external jobs, spend money, contact anyone, commit, or push.`;
|
|
558
|
-
}
|
|
559
|
-
async function runCodex(prompt, projectPath, mode) {
|
|
560
|
-
const binary = await resolveCodexBinary();
|
|
561
|
-
if (!binary)
|
|
562
|
-
throw new Error("Codex local client was not found");
|
|
563
|
-
const isolatedWorkspace = join(projectPath, "workspace");
|
|
564
|
-
const executionRoot = (mode === "implement" || mode === "verify") && existsSync(join(isolatedWorkspace, ".git"))
|
|
565
|
-
? isolatedWorkspace
|
|
566
|
-
: projectPath;
|
|
567
|
-
const tempDir = await mkdtemp(join(tmpdir(), "peon-roundtable-codex-"));
|
|
568
|
-
const outputPath = join(tempDir, "last-message.txt");
|
|
569
|
-
try {
|
|
570
|
-
const args = [
|
|
571
|
-
"exec",
|
|
572
|
-
"--ephemeral",
|
|
573
|
-
"--ignore-user-config",
|
|
574
|
-
"--sandbox",
|
|
575
|
-
mode === "implement" || mode === "verify" ? "workspace-write" : "read-only",
|
|
576
|
-
"--skip-git-repo-check",
|
|
577
|
-
"--cd",
|
|
578
|
-
executionRoot,
|
|
579
|
-
"--output-last-message",
|
|
580
|
-
outputPath,
|
|
581
|
-
"-"
|
|
582
|
-
];
|
|
583
|
-
if (mode === "chat")
|
|
584
|
-
args.splice(3, 0, "--config", 'model_reasoning_effort="low"');
|
|
585
|
-
await spawnWithInput(binary, args, prompt, executionRoot, { rejectOnOverflow: false });
|
|
586
|
-
return await readFile(outputPath, "utf8");
|
|
587
|
-
}
|
|
588
|
-
finally {
|
|
589
|
-
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
async function runClaude(prompt, projectPath, mode) {
|
|
593
|
-
const binary = await resolveClaudeBinary();
|
|
594
|
-
if (!binary)
|
|
595
|
-
throw new Error("Claude local client was not found");
|
|
596
|
-
const args = [
|
|
597
|
-
"-p",
|
|
598
|
-
"--safe-mode",
|
|
599
|
-
"--output-format",
|
|
600
|
-
"text",
|
|
601
|
-
"--permission-mode",
|
|
602
|
-
"plan",
|
|
603
|
-
"--tools",
|
|
604
|
-
"Read,Glob,Grep,WebSearch,WebFetch",
|
|
605
|
-
"--no-session-persistence"
|
|
606
|
-
];
|
|
607
|
-
if (mode === "chat")
|
|
608
|
-
args.push("--effort", "low");
|
|
609
|
-
const result = await spawnWithInput(binary, args, prompt, projectPath);
|
|
610
|
-
return result.stdout;
|
|
611
|
-
}
|
|
612
|
-
async function spawnWithInput(binary, args, input, cwd, options = {}) {
|
|
613
|
-
const configured = Number.parseInt(process.env.PEON_ROUNDTABLE_TIMEOUT_MS ?? "", 10);
|
|
614
|
-
const timeoutMs = Number.isFinite(configured) && configured >= 10_000 ? configured : DEFAULT_TIMEOUT_MS;
|
|
615
|
-
return new Promise((resolve, reject) => {
|
|
616
|
-
const child = spawn(binary, args, { cwd, env: process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
617
|
-
let stdout = "";
|
|
618
|
-
let stderr = "";
|
|
619
|
-
let overflow = false;
|
|
620
|
-
const timer = setTimeout(() => {
|
|
621
|
-
child.kill("SIGTERM");
|
|
622
|
-
setTimeout(() => child.kill("SIGKILL"), 2_000).unref();
|
|
623
|
-
}, timeoutMs);
|
|
624
|
-
timer.unref();
|
|
625
|
-
child.stdout.setEncoding("utf8");
|
|
626
|
-
child.stderr.setEncoding("utf8");
|
|
627
|
-
child.stdout.on("data", (chunk) => {
|
|
628
|
-
const combined = stdout + chunk;
|
|
629
|
-
if (combined.length > MAX_AGENT_OUTPUT_CHARS * 4)
|
|
630
|
-
overflow = true;
|
|
631
|
-
stdout = combined.slice(-MAX_AGENT_OUTPUT_CHARS * 4);
|
|
632
|
-
});
|
|
633
|
-
child.stderr.on("data", (chunk) => {
|
|
634
|
-
const combined = stderr + chunk;
|
|
635
|
-
if (combined.length > MAX_AGENT_OUTPUT_CHARS * 4)
|
|
636
|
-
overflow = true;
|
|
637
|
-
stderr = combined.slice(-MAX_AGENT_OUTPUT_CHARS * 4);
|
|
638
|
-
});
|
|
639
|
-
child.on("error", (error) => {
|
|
640
|
-
clearTimeout(timer);
|
|
641
|
-
reject(error);
|
|
642
|
-
});
|
|
643
|
-
child.on("close", (code, signal) => {
|
|
644
|
-
clearTimeout(timer);
|
|
645
|
-
if (overflow && options.rejectOnOverflow !== false) {
|
|
646
|
-
reject(new Error(`${basename(binary)} produced too much output`));
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
if (signal) {
|
|
650
|
-
reject(new Error(`${basename(binary)} did not finish before the local timeout`));
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
if (code !== 0) {
|
|
654
|
-
reject(new Error(`${basename(binary)} failed: ${stderr.trim().slice(0, 500) || `exit ${code}`}`));
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
|
|
658
|
-
});
|
|
659
|
-
child.stdin.end(input);
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
async function resolveCodexBinary() {
|
|
663
|
-
return firstExecutable([
|
|
664
|
-
process.env.PEON_CODEX_BIN,
|
|
665
|
-
"/Applications/ChatGPT.app/Contents/Resources/codex",
|
|
666
|
-
"codex"
|
|
667
|
-
]);
|
|
668
|
-
}
|
|
669
|
-
async function resolveClaudeBinary() {
|
|
670
|
-
return firstExecutable([
|
|
671
|
-
process.env.PEON_CLAUDE_BIN,
|
|
672
|
-
join(homedir(), ".local", "bin", "claude"),
|
|
673
|
-
"claude"
|
|
674
|
-
]);
|
|
675
|
-
}
|
|
676
|
-
async function isCodexAuthenticated(binary) {
|
|
677
|
-
const result = await probeCommand(binary, ["login", "status"]);
|
|
678
|
-
return result.code === 0 && /logged in/i.test(`${result.stdout}\n${result.stderr}`);
|
|
679
|
-
}
|
|
680
|
-
async function isClaudeAuthenticated(binary) {
|
|
681
|
-
const result = await probeCommand(binary, ["auth", "status"]);
|
|
682
|
-
if (result.code !== 0)
|
|
683
|
-
return false;
|
|
684
|
-
try {
|
|
685
|
-
const parsed = JSON.parse(result.stdout);
|
|
686
|
-
return parsed.loggedIn === true;
|
|
687
|
-
}
|
|
688
|
-
catch {
|
|
689
|
-
return false;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
async function probeCommand(binary, args) {
|
|
693
|
-
return new Promise((resolve) => {
|
|
694
|
-
const child = spawn(binary, args, { cwd: homedir(), env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
695
|
-
let stdout = "";
|
|
696
|
-
let stderr = "";
|
|
697
|
-
const timer = setTimeout(() => child.kill("SIGTERM"), 5_000);
|
|
698
|
-
timer.unref();
|
|
699
|
-
child.stdout.setEncoding("utf8");
|
|
700
|
-
child.stderr.setEncoding("utf8");
|
|
701
|
-
child.stdout.on("data", (chunk) => {
|
|
702
|
-
stdout = `${stdout}${chunk}`.slice(-4_000);
|
|
703
|
-
});
|
|
704
|
-
child.stderr.on("data", (chunk) => {
|
|
705
|
-
stderr = `${stderr}${chunk}`.slice(-4_000);
|
|
706
|
-
});
|
|
707
|
-
child.on("error", () => {
|
|
708
|
-
clearTimeout(timer);
|
|
709
|
-
resolve({ code: null, stdout, stderr });
|
|
710
|
-
});
|
|
711
|
-
child.on("close", (code) => {
|
|
712
|
-
clearTimeout(timer);
|
|
713
|
-
resolve({ code, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
714
|
-
});
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
async function firstExecutable(candidates) {
|
|
718
|
-
const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
719
|
-
for (const candidate of candidates) {
|
|
720
|
-
if (!candidate)
|
|
721
|
-
continue;
|
|
722
|
-
const paths = isAbsolute(candidate) ? [candidate] : pathEntries.map((entry) => join(entry, candidate));
|
|
723
|
-
for (const path of paths) {
|
|
724
|
-
try {
|
|
725
|
-
await access(path, fsConstants.X_OK);
|
|
726
|
-
return path;
|
|
727
|
-
}
|
|
728
|
-
catch {
|
|
729
|
-
// Try the next candidate.
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
return null;
|
|
734
|
-
}
|
|
735
|
-
function safeError(error) {
|
|
736
|
-
const message = error instanceof Error ? error.message : "Unknown roundtable error";
|
|
737
|
-
return message.replace(/[\r\n]+/g, " ").slice(0, 700);
|
|
738
|
-
}
|
|
739
|
-
function reportFailed(report) {
|
|
740
|
-
return /(?:\btests?\s+failed\b|\bfailing\b|\bverification\s+failed\b|\bnot\s+pass(?:ed)?\b)/i.test(report);
|
|
741
|
-
}
|
|
742
|
-
async function collectWorkspaceChanges(projectPath) {
|
|
743
|
-
const isolatedWorkspace = join(projectPath, "workspace");
|
|
744
|
-
const root = existsSync(join(isolatedWorkspace, ".git"))
|
|
745
|
-
? isolatedWorkspace
|
|
746
|
-
: existsSync(join(projectPath, ".git"))
|
|
747
|
-
? projectPath
|
|
748
|
-
: null;
|
|
749
|
-
if (!root)
|
|
750
|
-
return [];
|
|
751
|
-
const result = await probeCommandAt("git", ["status", "--short", "--untracked-files=all"], root);
|
|
752
|
-
if (result.code !== 0)
|
|
753
|
-
return [];
|
|
754
|
-
return result.stdout
|
|
755
|
-
.split("\n")
|
|
756
|
-
.map((line) => line.trimEnd())
|
|
757
|
-
.filter(Boolean)
|
|
758
|
-
.slice(0, 120);
|
|
759
|
-
}
|
|
760
|
-
function changedWorkspaceSnapshot(before, after) {
|
|
761
|
-
return before.join("\n") === after.join("\n") ? [] : after;
|
|
762
|
-
}
|
|
763
|
-
async function probeCommandAt(binary, args, cwd) {
|
|
764
|
-
return new Promise((resolve) => {
|
|
765
|
-
const child = spawn(binary, args, { cwd, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
766
|
-
let stdout = "";
|
|
767
|
-
let stderr = "";
|
|
768
|
-
const timer = setTimeout(() => child.kill("SIGTERM"), 10_000);
|
|
769
|
-
timer.unref();
|
|
770
|
-
child.stdout.setEncoding("utf8");
|
|
771
|
-
child.stderr.setEncoding("utf8");
|
|
772
|
-
child.stdout.on("data", (chunk) => {
|
|
773
|
-
stdout = `${stdout}${chunk}`.slice(-40_000);
|
|
774
|
-
});
|
|
775
|
-
child.stderr.on("data", (chunk) => {
|
|
776
|
-
stderr = `${stderr}${chunk}`.slice(-4_000);
|
|
777
|
-
});
|
|
778
|
-
child.on("error", () => {
|
|
779
|
-
clearTimeout(timer);
|
|
780
|
-
resolve({ code: null, stdout, stderr });
|
|
781
|
-
});
|
|
782
|
-
child.on("close", (code) => {
|
|
783
|
-
clearTimeout(timer);
|
|
784
|
-
resolve({ code, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
785
|
-
});
|
|
786
|
-
});
|
|
787
|
-
}
|