jev-planner 0.0.1 → 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.
@@ -0,0 +1,2756 @@
1
+ let _typesafe_ai_sdk = require("@typesafe-ai/sdk");
2
+ let node_fs_promises = require("node:fs/promises");
3
+ let node_path = require("node:path");
4
+ let node_util = require("node:util");
5
+ let node_child_process = require("node:child_process");
6
+ let node_string_decoder = require("node:string_decoder");
7
+ let node_crypto = require("node:crypto");
8
+ //#region ../core/dist/index.mjs
9
+ const MAX_OUTPUT_BYTES = 8388608;
10
+ var ProcessError = class extends Error {
11
+ command;
12
+ exitCode;
13
+ stderr;
14
+ constructor(command, exitCode, stderr) {
15
+ const detail = stderr.trim().slice(-2e3);
16
+ super(`${command} failed${exitCode === null ? "" : ` with exit code ${String(exitCode)}`}${detail ? `:\n${detail}` : ""}`);
17
+ this.name = "ProcessError";
18
+ this.command = command;
19
+ this.exitCode = exitCode;
20
+ this.stderr = stderr;
21
+ }
22
+ };
23
+ function runProcess(command, args, options) {
24
+ return new Promise((resolve, reject) => {
25
+ if (options.signal?.aborted === true) {
26
+ reject(/* @__PURE__ */ new Error(`${command} was not started: the run no longer needs it`));
27
+ return;
28
+ }
29
+ const omit = new Set(options.omitEnv);
30
+ const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !omit.has(name)));
31
+ const child = (0, node_child_process.spawn)(command, args, {
32
+ cwd: options.cwd,
33
+ env,
34
+ stdio: [
35
+ "pipe",
36
+ "pipe",
37
+ "pipe"
38
+ ]
39
+ });
40
+ const stdout = [];
41
+ const stderr = [];
42
+ let outputBytes = 0;
43
+ let settled = false;
44
+ const decoders = {
45
+ stdout: new node_string_decoder.StringDecoder("utf8"),
46
+ stderr: new node_string_decoder.StringDecoder("utf8")
47
+ };
48
+ const partial = {
49
+ stdout: "",
50
+ stderr: ""
51
+ };
52
+ const emit = (stream, chunk) => {
53
+ const { onLine } = options;
54
+ if (!onLine) return;
55
+ const text = chunk ? decoders[stream].write(chunk) : decoders[stream].end();
56
+ const lines = `${partial[stream]}${text}`.split(/\r?\n/);
57
+ partial[stream] = chunk ? lines.pop() ?? "" : "";
58
+ for (const line of lines) if (line.trim()) onLine(line, stream);
59
+ };
60
+ const finish = (callback) => {
61
+ if (settled) return;
62
+ settled = true;
63
+ if (timer !== void 0) clearTimeout(timer);
64
+ options.signal?.removeEventListener("abort", abort);
65
+ callback();
66
+ };
67
+ const abort = () => {
68
+ child.kill("SIGTERM");
69
+ finish(() => {
70
+ reject(/* @__PURE__ */ new Error(`${command} was stopped: the run no longer needs it`));
71
+ });
72
+ };
73
+ options.signal?.addEventListener("abort", abort);
74
+ const collect = (target, chunk) => {
75
+ outputBytes += chunk.length;
76
+ if (outputBytes > MAX_OUTPUT_BYTES) {
77
+ child.kill("SIGTERM");
78
+ finish(() => {
79
+ reject(/* @__PURE__ */ new Error(`${command} exceeded the 8 MiB output limit`));
80
+ });
81
+ return;
82
+ }
83
+ target.push(chunk);
84
+ };
85
+ child.stdout.on("data", (chunk) => {
86
+ collect(stdout, chunk);
87
+ emit("stdout", chunk);
88
+ });
89
+ child.stderr.on("data", (chunk) => {
90
+ collect(stderr, chunk);
91
+ emit("stderr", chunk);
92
+ });
93
+ child.on("error", (error) => {
94
+ finish(() => {
95
+ reject(new Error(`Could not start ${command}: ${error.message}`, { cause: error }));
96
+ });
97
+ });
98
+ child.on("close", (exitCode) => {
99
+ if (!settled) {
100
+ emit("stdout");
101
+ emit("stderr");
102
+ }
103
+ finish(() => {
104
+ const result = {
105
+ stdout: Buffer.concat(stdout).toString("utf8"),
106
+ stderr: Buffer.concat(stderr).toString("utf8"),
107
+ exitCode: exitCode ?? 1
108
+ };
109
+ if (exitCode !== 0) {
110
+ reject(new ProcessError(command, exitCode, result.stderr));
111
+ return;
112
+ }
113
+ resolve(result);
114
+ });
115
+ });
116
+ const timer = options.timeoutMs === void 0 ? void 0 : setTimeout(() => {
117
+ child.kill("SIGTERM");
118
+ finish(() => {
119
+ reject(/* @__PURE__ */ new Error(`${command} timed out after ${String(options.timeoutMs)}ms`));
120
+ });
121
+ }, options.timeoutMs);
122
+ child.stdin.on("error", (error) => {
123
+ if (error.code !== "EPIPE") finish(() => {
124
+ reject(error);
125
+ });
126
+ });
127
+ child.stdin.end(options.input);
128
+ });
129
+ }
130
+ /** The first line of `text`, trimmed; the whole of a one-line message. */
131
+ function firstLine(text) {
132
+ return text.trim().replace(/\n[\s\S]*$/, "");
133
+ }
134
+ function errorDetail(error) {
135
+ return error instanceof Error ? firstLine(error.message) : String(error);
136
+ }
137
+ /** Passes when `command args` exits 0; the detail is the first line it printed. */
138
+ async function commandCheck(name, command, args, cwd) {
139
+ try {
140
+ const result = await runProcess(command, args, {
141
+ cwd,
142
+ timeoutMs: 15e3
143
+ });
144
+ return {
145
+ name,
146
+ ok: true,
147
+ detail: firstLine(result.stdout || result.stderr) || "available"
148
+ };
149
+ } catch (error) {
150
+ return {
151
+ name,
152
+ ok: false,
153
+ detail: errorDetail(error)
154
+ };
155
+ }
156
+ }
157
+ /** Passes when the variable is set to something other than whitespace. Never prints the value. */
158
+ function envCheck(name, variable, env) {
159
+ const set = Boolean(env[variable]?.trim());
160
+ return {
161
+ name,
162
+ ok: set,
163
+ detail: `${variable} is ${set ? "set" : "not set"}`
164
+ };
165
+ }
166
+ /**
167
+ * Each provider's own checks, in order. No paid call. The CLI adds a check for
168
+ * each variable its judge reads (`PlannerProgram.judgeEnv`).
169
+ */
170
+ async function runDoctor(cwd, providers, env = process.env) {
171
+ return (await Promise.all(providers.map((provider) => provider.doctor(cwd, env)))).flat();
172
+ }
173
+ /**
174
+ * Top-level files that say what a repository is and how it is built, read in
175
+ * this order. Tracked files only, so a `.env` or anything else ignored is never
176
+ * sent: the snapshot goes to a third-party API.
177
+ */
178
+ const SNAPSHOT_FILES = [
179
+ "AGENTS.md",
180
+ "CLAUDE.md",
181
+ "README.md",
182
+ "CONTRIBUTING.md",
183
+ "package.json",
184
+ "pnpm-workspace.yaml",
185
+ "tsconfig.json",
186
+ "pyproject.toml",
187
+ "Cargo.toml",
188
+ "go.mod"
189
+ ];
190
+ const MAX_SNAPSHOT_PATHS = 2e3;
191
+ const MAX_SNAPSHOT_FILE_CHARS = 16e3;
192
+ function clip(text, max) {
193
+ return text.length <= max ? text : `${text.slice(0, max)}\n[truncated]`;
194
+ }
195
+ /**
196
+ * What an agent without repository access is told about `cwd`: the files git
197
+ * tracks under it, and the contents of the `SNAPSHOT_FILES` among them, within
198
+ * fixed size budgets. Outside a git repository it says so rather than guessing
199
+ * which files are safe to send.
200
+ */
201
+ async function repoSnapshot(cwd) {
202
+ let paths;
203
+ try {
204
+ const { stdout } = await runProcess("git", ["ls-files"], {
205
+ cwd,
206
+ timeoutMs: 15e3
207
+ });
208
+ paths = stdout.split("\n").filter(Boolean);
209
+ } catch {
210
+ return "<repository-snapshot>\nNot a git repository: no files are available.\n</repository-snapshot>";
211
+ }
212
+ const listed = paths.slice(0, MAX_SNAPSHOT_PATHS);
213
+ const omitted = paths.length - listed.length;
214
+ let snapshot = `<repository-snapshot>\nTracked files (${String(paths.length)}):\n${listed.join("\n")}${omitted > 0 ? `\n[${String(omitted)} more not listed]` : ""}`;
215
+ const tracked = new Set(paths);
216
+ for (const file of SNAPSHOT_FILES) {
217
+ if (!tracked.has(file)) continue;
218
+ const contents = await (0, node_fs_promises.readFile)((0, node_path.join)(cwd, file), "utf8").catch(() => void 0);
219
+ if (contents === void 0) continue;
220
+ const block = `\n\n<file path="${file}">\n${clip(contents, MAX_SNAPSHOT_FILE_CHARS)}\n</file>`;
221
+ if (snapshot.length + block.length > 8e4) break;
222
+ snapshot += block;
223
+ }
224
+ return `${snapshot}\n</repository-snapshot>`;
225
+ }
226
+ /** One line of progress from a longer text: its first non-blank line, clipped. */
227
+ function brief(text, max = 160) {
228
+ const line = text.trim().split("\n", 1)[0]?.trim() ?? "";
229
+ return line.length <= max ? line : `${line.slice(0, max - 1)}…`;
230
+ }
231
+ /**
232
+ * An agent's label: the provider's own for an agent named after it, `Codex (sol)`
233
+ * for a named one, so two agents of one provider never share a label, which is
234
+ * how the judge and the prompts tell plans apart.
235
+ */
236
+ function agentLabel(provider, name) {
237
+ return name === provider.id ? provider.label : `${provider.label} (${name})`;
238
+ }
239
+ function requireOutput(label, output) {
240
+ const result = output.trim();
241
+ if (!result) throw new Error(`${label} returned an empty response`);
242
+ return result;
243
+ }
244
+ /** An agent CLI installed and logged in on this machine, run read-only in the repository. */
245
+ function cliProvider(config) {
246
+ return {
247
+ id: config.id,
248
+ label: config.label,
249
+ kind: "cli",
250
+ secretEnv: [],
251
+ effort: config.effort ?? false,
252
+ create: ({ name = config.id, label = agentLabel(config, name), model, effort, omitEnv }) => ({
253
+ name,
254
+ label,
255
+ readsRepository: true,
256
+ generate: async (request) => {
257
+ const callEffort = config.effort ? request.effort ?? effort : effort;
258
+ const overrides = {
259
+ ...model === void 0 ? {} : { model },
260
+ ...callEffort === void 0 ? {} : { effort: callEffort }
261
+ };
262
+ const { events, sessions } = config;
263
+ const read = (line, stream) => {
264
+ if (stream === "stderr") return { progress: line };
265
+ if (!events) return {};
266
+ let event;
267
+ try {
268
+ event = JSON.parse(line);
269
+ } catch {
270
+ return { progress: line };
271
+ }
272
+ return events(event);
273
+ };
274
+ const once = async (args, input) => {
275
+ let answer = "";
276
+ let session;
277
+ const { stdout } = await runProcess(config.command, args, {
278
+ cwd: request.cwd,
279
+ input,
280
+ timeoutMs: request.timeoutMs,
281
+ omitEnv,
282
+ ...request.signal ? { signal: request.signal } : {},
283
+ onLine: (line, stream) => {
284
+ const event = read(line, stream);
285
+ if (event.progress) request.onProgress?.(event.progress);
286
+ if (event.result !== void 0) answer = event.result;
287
+ if (event.session !== void 0) session = event.session;
288
+ }
289
+ });
290
+ return {
291
+ answer: requireOutput(label, events ? answer : stdout),
292
+ session
293
+ };
294
+ };
295
+ const { session } = request;
296
+ if (!sessions || !session) return (await once(config.args(overrides), request.prompt)).answer;
297
+ const start = async (conversation) => {
298
+ const id = (0, node_crypto.randomUUID)();
299
+ const result = await once(sessions.start(overrides, id), request.prompt);
300
+ conversation.id = result.session ?? id;
301
+ return result.answer;
302
+ };
303
+ if (session.id === void 0) return start(session);
304
+ try {
305
+ return (await once(sessions.resume(overrides, session.id), request.resumePrompt ?? request.prompt)).answer;
306
+ } catch (error) {
307
+ if (!(error instanceof ProcessError)) throw error;
308
+ const reason = brief(error.stderr.trim().split("\n").at(-1) ?? "") || error.message;
309
+ request.onProgress?.(`Could not continue the earlier session (${brief(reason)}); starting a new one`);
310
+ return start(session);
311
+ }
312
+ }
313
+ }),
314
+ doctor: (cwd) => {
315
+ const checks = [commandCheck(`${config.label} CLI`, config.command, ["--version"], cwd)];
316
+ if (typeof config.auth === "function") checks.push(config.auth(cwd));
317
+ else if (config.auth) checks.push(commandCheck(`${config.label} auth`, config.command, config.auth, cwd));
318
+ return Promise.all(checks);
319
+ }
320
+ };
321
+ }
322
+ /** The instruction an API agent gets in place of the repository access it does not have. */
323
+ const API_AGENT_SYSTEM_PROMPT = `You cannot run commands or open files. The repository snapshot in the task is all
324
+ you can see of the codebase: ground the plan in it, and name the files you would need to read wherever
325
+ the plan depends on code the snapshot does not show.`;
326
+ /**
327
+ * A chat API that speaks OpenAI's `/chat/completions`, as DeepSeek, Moonshot,
328
+ * Z.ai and most others do. It cannot read the repository, so each prompt is
329
+ * sent with a snapshot of it: the tracked file list and the top-level docs and
330
+ * manifests (see `repoSnapshot`). Given an `AgentSession`, it keeps the
331
+ * conversation and sends it back on the next call, so the snapshot is sent once
332
+ * and later prompts can leave out what the conversation already holds.
333
+ */
334
+ function openAICompatibleProvider(config) {
335
+ let baseEnd = config.baseUrl.length;
336
+ while (baseEnd > 0 && config.baseUrl.charAt(baseEnd - 1) === "/") baseEnd -= 1;
337
+ const endpoint = `${config.baseUrl.slice(0, baseEnd)}/chat/completions`;
338
+ return {
339
+ id: config.id,
340
+ label: config.label,
341
+ kind: "api",
342
+ secretEnv: [config.apiKeyEnv],
343
+ effort: false,
344
+ create: ({ name = config.id, label = agentLabel(config, name), model = config.model, env, fetch: send = fetch }) => {
345
+ const snapshots = /* @__PURE__ */ new Map();
346
+ const conversations = /* @__PURE__ */ new WeakMap();
347
+ return {
348
+ name,
349
+ label,
350
+ readsRepository: false,
351
+ generate: async (request) => {
352
+ const key = env[config.apiKeyEnv]?.trim();
353
+ if (!key) throw new Error(`${config.apiKeyEnv} is not set, so ${label} cannot run`);
354
+ const earlier = request.session && conversations.get(request.session);
355
+ let messages;
356
+ if (earlier) messages = [...earlier, {
357
+ role: "user",
358
+ content: request.resumePrompt ?? request.prompt
359
+ }];
360
+ else {
361
+ let snapshot = snapshots.get(request.cwd);
362
+ if (!snapshot) {
363
+ snapshot = repoSnapshot(request.cwd);
364
+ snapshots.set(request.cwd, snapshot);
365
+ }
366
+ messages = [{
367
+ role: "system",
368
+ content: API_AGENT_SYSTEM_PROMPT
369
+ }, {
370
+ role: "user",
371
+ content: `${await snapshot}\n\n${request.prompt}`
372
+ }];
373
+ }
374
+ request.onProgress?.(`Waiting for ${model} to answer…`);
375
+ const timeout = AbortSignal.timeout(request.timeoutMs);
376
+ const signal = request.signal ? AbortSignal.any([timeout, request.signal]) : timeout;
377
+ let response;
378
+ try {
379
+ response = await send(endpoint, {
380
+ method: "POST",
381
+ headers: {
382
+ "content-type": "application/json",
383
+ authorization: `Bearer ${key}`
384
+ },
385
+ body: JSON.stringify({
386
+ model,
387
+ messages
388
+ }),
389
+ signal
390
+ });
391
+ } catch (error) {
392
+ if (request.signal?.aborted === true) throw new Error(`${label} was stopped: the run no longer needs it`, { cause: error });
393
+ if (error instanceof Error && error.name === "TimeoutError") throw new Error(`${label} timed out after ${String(request.timeoutMs / 1e3)}s`, { cause: error });
394
+ throw error;
395
+ }
396
+ if (!response.ok) {
397
+ const detail = (await response.text()).trim().slice(0, 500);
398
+ throw new Error(`${label} API failed with status ${String(response.status)}${detail ? `: ${detail}` : ""}`);
399
+ }
400
+ const answer = requireOutput(label, (await response.json()).choices?.[0]?.message?.content ?? "");
401
+ if (request.session) conversations.set(request.session, [...messages, {
402
+ role: "assistant",
403
+ content: answer
404
+ }]);
405
+ return answer;
406
+ }
407
+ };
408
+ },
409
+ doctor: (_cwd, env) => Promise.resolve([envCheck(`${config.label} key`, config.apiKeyEnv, env)])
410
+ };
411
+ }
412
+ async function claudeAuth(cwd) {
413
+ try {
414
+ const result = await runProcess("claude", [
415
+ "auth",
416
+ "status",
417
+ "--json"
418
+ ], {
419
+ cwd,
420
+ timeoutMs: 15e3
421
+ });
422
+ const status = JSON.parse(result.stdout);
423
+ const detail = [status.authMethod, status.subscriptionType].filter(Boolean).join(", ");
424
+ return {
425
+ name: "Claude auth",
426
+ ok: status.loggedIn === true,
427
+ detail: status.loggedIn === true ? `Logged in${detail ? ` (${detail})` : ""}` : "Not logged in"
428
+ };
429
+ } catch (error) {
430
+ return {
431
+ name: "Claude auth",
432
+ ok: false,
433
+ detail: errorDetail(error)
434
+ };
435
+ }
436
+ }
437
+ /** What Codex is doing, from one of its JSON events; its last message is the answer. */
438
+ function codexEvent(event) {
439
+ const { type, item, error, message, thread_id: thread } = event;
440
+ if (type === "thread.started") return thread ? { session: thread } : {};
441
+ if (type === "turn.failed") return { progress: `failed: ${error?.message ?? "unknown error"}` };
442
+ if (type === "error") return { progress: `error: ${message ?? "unknown error"}` };
443
+ if (!item?.type) return {};
444
+ if (type === "item.started") return item.type === "command_execution" ? { progress: `$ ${brief(item.command ?? "")}` } : {};
445
+ if (type !== "item.completed") return {};
446
+ switch (item.type) {
447
+ case "agent_message": return {
448
+ progress: brief(item.text ?? ""),
449
+ result: item.text ?? ""
450
+ };
451
+ case "reasoning": return { progress: brief(item.text ?? "") };
452
+ case "command_execution": return item.exit_code === 0 ? {} : { progress: `exited ${String(item.exit_code)}` };
453
+ default: return { progress: item.type.replaceAll("_", " ") };
454
+ }
455
+ }
456
+ /** What Claude is doing, from one of its JSON events; the `result` event is the answer. */
457
+ function claudeEvent(event) {
458
+ const { type, message, result, is_error: isError } = event;
459
+ if (type === "result") return isError === true ? { progress: `error: ${result ?? "unknown error"}` } : { result: result ?? "" };
460
+ if (type !== "assistant") return {};
461
+ const lines = (message?.content ?? []).flatMap((block) => {
462
+ if (block.type === "text") return [brief(block.text ?? "")];
463
+ if (block.type !== "tool_use") return [];
464
+ const subject = Object.values(block.input ?? {}).find((value) => typeof value === "string");
465
+ return [`${block.name ?? "tool"}${typeof subject === "string" ? ` ${brief(subject)}` : ""}`];
466
+ });
467
+ return lines.length > 0 ? { progress: lines.join("\n") } : {};
468
+ }
469
+ function codexOverrides({ model, effort }) {
470
+ return [...model ? ["--model", model] : [], ...effort ? ["-c", `model_reasoning_effort=${JSON.stringify(effort)}`] : []];
471
+ }
472
+ /** `codex exec`, read-only, with `session` saying whether its session is kept. */
473
+ function codexArgs(session, overrides) {
474
+ return [
475
+ "exec",
476
+ "--json",
477
+ ...session,
478
+ "--sandbox",
479
+ "read-only",
480
+ "--skip-git-repo-check",
481
+ "--color",
482
+ "never",
483
+ ...codexOverrides(overrides),
484
+ "-"
485
+ ];
486
+ }
487
+ /**
488
+ * `claude --print` in plan mode with three read-only tools, whether it starts,
489
+ * continues or keeps no session: `session` says which.
490
+ */
491
+ function claudeArgs(session, { model, effort }) {
492
+ return [
493
+ "--print",
494
+ "--permission-mode",
495
+ "plan",
496
+ "--permission-prompts",
497
+ "none",
498
+ ...session,
499
+ "--output-format",
500
+ "stream-json",
501
+ "--verbose",
502
+ "--tools",
503
+ "Read,Glob,Grep",
504
+ "--strict-mcp-config",
505
+ ...model ? ["--model", model] : [],
506
+ ...effort ? ["--effort", effort] : []
507
+ ];
508
+ }
509
+ /**
510
+ * Every AI the planner can plan with. To add one, add an entry here, and the
511
+ * agent to `config.schema.json` and its copy in `apps/docs/public/`, which a
512
+ * test keeps in step: the CLI, `doctor`, `--help`, the config, the prompts and
513
+ * the judge all read this list. An OpenAI-compatible chat API is one
514
+ * `openAICompatibleProvider` call; an agent CLI that can run read-only is one
515
+ * `cliProvider` call.
516
+ */
517
+ const PROVIDERS = [
518
+ cliProvider({
519
+ id: "codex",
520
+ label: "Codex",
521
+ command: "codex",
522
+ args: (overrides) => codexArgs(["--ephemeral"], overrides),
523
+ sessions: {
524
+ start: (overrides) => codexArgs([], overrides),
525
+ resume: (overrides, id) => [
526
+ "exec",
527
+ "resume",
528
+ "--json",
529
+ "--skip-git-repo-check",
530
+ "-c",
531
+ "sandbox_mode=\"read-only\"",
532
+ ...codexOverrides(overrides),
533
+ id,
534
+ "-"
535
+ ]
536
+ },
537
+ effort: true,
538
+ events: codexEvent,
539
+ auth: ["login", "status"]
540
+ }),
541
+ cliProvider({
542
+ id: "claude",
543
+ label: "Claude",
544
+ command: "claude",
545
+ args: (overrides) => claudeArgs(["--no-session-persistence"], overrides),
546
+ sessions: {
547
+ start: (overrides, id) => claudeArgs(["--session-id", id], overrides),
548
+ resume: (overrides, id) => claudeArgs(["--resume", id], overrides)
549
+ },
550
+ effort: true,
551
+ events: claudeEvent,
552
+ auth: claudeAuth
553
+ }),
554
+ openAICompatibleProvider({
555
+ id: "deepseek",
556
+ label: "DeepSeek",
557
+ baseUrl: "https://api.deepseek.com",
558
+ apiKeyEnv: "DEEPSEEK_API_KEY",
559
+ model: "deepseek-chat"
560
+ }),
561
+ openAICompatibleProvider({
562
+ id: "kimi",
563
+ label: "Kimi",
564
+ baseUrl: "https://api.moonshot.ai/v1",
565
+ apiKeyEnv: "MOONSHOT_API_KEY",
566
+ model: "kimi-latest"
567
+ }),
568
+ openAICompatibleProvider({
569
+ id: "glm",
570
+ label: "GLM",
571
+ baseUrl: "https://api.z.ai/api/paas/v4",
572
+ apiKeyEnv: "ZAI_API_KEY",
573
+ model: "glm-4.6"
574
+ })
575
+ ];
576
+ /** The agents a run uses without `--agents`. */
577
+ const DEFAULT_AGENTS = ["codex", "claude"];
578
+ const IDS = PROVIDERS.map(({ id }) => id);
579
+ const PROVIDER_IDS$1 = IDS.join(", ");
580
+ /** Where the keys a config must never hold belong instead: the judge's, then every provider's. */
581
+ const secretEnv$1 = (judgeEnv) => [...judgeEnv, ...PROVIDERS.flatMap(({ secretEnv }) => secretEnv)];
582
+ const SECRET = /key|token|secret|password/i;
583
+ function object(at, value) {
584
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${at}: must be an object`);
585
+ return value;
586
+ }
587
+ function string(at, value) {
588
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${at}: must be a non-empty string`);
589
+ return value.trim();
590
+ }
591
+ function boolean(at, value) {
592
+ if (typeof value !== "boolean") throw new Error(`${at}: must be true or false`);
593
+ return value;
594
+ }
595
+ function notOneOf(at, options) {
596
+ return /* @__PURE__ */ new Error(`${at}: must be one of ${options.map((option) => JSON.stringify(option)).join(", ")}`);
597
+ }
598
+ function choice$1(at, value, options) {
599
+ if (!options.includes(value)) throw notOneOf(at, options);
600
+ return value;
601
+ }
602
+ /** A number of seconds, as the matching flag's text; zero only where `zero` allows it. */
603
+ function seconds(at, value, zero = false) {
604
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || !zero && value === 0) throw new Error(`${at}: must be ${zero ? "a number of seconds, 0 or more" : "a positive number of seconds"}`);
605
+ return String(value);
606
+ }
607
+ function unknownKey(at, expected, judgeEnv) {
608
+ const key = at.slice(at.lastIndexOf(".") + 1);
609
+ if (SECRET.test(key)) return /* @__PURE__ */ new Error(`${at}: a config never holds a secret; it is meant to be committed. Set ${secretEnv$1(judgeEnv).join(", ")} in the environment instead.`);
610
+ return /* @__PURE__ */ new Error(`${at}: unknown key. Expected one of ${expected.join(", ")}.`);
611
+ }
612
+ const AGENT_FIELDS = {
613
+ model: "model",
614
+ effort: "effort",
615
+ reviewEffort: "review-effort"
616
+ };
617
+ const NAME = /^[a-z][a-z0-9-]{0,23}$/;
618
+ /** Taken by `--finalizer` (`auto`, `none`), by the judge's verdict (`tie`), or by Windows as a file name. */
619
+ const RESERVED = /* @__PURE__ */ new Set([
620
+ "auto",
621
+ "none",
622
+ "tie",
623
+ "con",
624
+ "prn",
625
+ "aux",
626
+ "nul",
627
+ ...Array.from({ length: 9 }, (_, index) => `com${String(index + 1)}`),
628
+ ...Array.from({ length: 9 }, (_, index) => `lpt${String(index + 1)}`)
629
+ ]);
630
+ /**
631
+ * The name of an agent of `provider`, lowercased, or an error: a letter, then
632
+ * letters, digits or `-`, 24 at most, so it fits an objection id
633
+ * (`sol:terra:C1`) and a file name (`sol.md`) on every system. It cannot be a
634
+ * word the run gives its own meaning, nor another provider's id.
635
+ */
636
+ function validateAgentName(name, provider) {
637
+ const lower = name.trim().toLowerCase();
638
+ if (!NAME.test(lower)) throw new Error(`${name}: an agent name is a letter, then letters, digits or -, at most 24 characters`);
639
+ if (RESERVED.has(lower)) throw new Error(`${lower}: a reserved word, not an agent name`);
640
+ if (lower !== provider && PROVIDERS.some(({ id }) => id === lower)) throw new Error(`${lower}: the name of another provider, not an agent name`);
641
+ return lower;
642
+ }
643
+ function agents(value, { judgeEnv }) {
644
+ const entries = Object.entries(object("agents", value));
645
+ if (entries.length < 2) throw new Error("agents: needs at least two agents");
646
+ const values = {
647
+ model: [],
648
+ effort: [],
649
+ "review-effort": []
650
+ };
651
+ const specs = [];
652
+ const names = /* @__PURE__ */ new Set();
653
+ for (const [key, settings] of entries) {
654
+ const fields = object(`agents.${key}`, settings);
655
+ const own = PROVIDERS.find(({ id }) => id === key);
656
+ const named = own === void 0;
657
+ if (!named && "provider" in fields) throw new Error(`agents.${key}.provider: a provider key names its own provider; use another key for an instance`);
658
+ if (named && !("provider" in fields)) throw new Error(`agents.${key}: unknown agent. Expected one of ${PROVIDER_IDS$1}. A named agent sets "provider".`);
659
+ const provider = own ?? PROVIDERS.find(({ id }) => id === fields.provider);
660
+ if (!provider) throw notOneOf(`agents.${key}.provider`, IDS);
661
+ let name;
662
+ try {
663
+ name = validateAgentName(key, provider.id);
664
+ } catch (error) {
665
+ throw new Error(`agents.${error.message}`, { cause: error });
666
+ }
667
+ if (names.has(name)) throw new Error(`agents.${key}: ${name} is listed more than once`);
668
+ names.add(name);
669
+ specs.push(named ? `${provider.id}:${name}` : name);
670
+ for (const [field, setting] of Object.entries(fields)) {
671
+ if (named && field === "provider") continue;
672
+ const at = `agents.${key}.${field}`;
673
+ if (!Object.hasOwn(AGENT_FIELDS, field)) throw unknownKey(at, [...named ? ["provider"] : [], ...Object.keys(AGENT_FIELDS)], judgeEnv);
674
+ const flag = AGENT_FIELDS[field];
675
+ if (flag !== "model" && !provider.effort) throw new Error(`${at}: ${provider.label} does not take effort`);
676
+ values[flag].push(`${name}=${string(at, setting)}`);
677
+ }
678
+ }
679
+ return {
680
+ agents: specs.join(","),
681
+ ...values
682
+ };
683
+ }
684
+ /** Every top-level key, what it becomes, and its check. The keys follow the flags' names. */
685
+ const SETTINGS = {
686
+ agents,
687
+ mode: (value) => ({ mode: choice$1("mode", value, [
688
+ "fast",
689
+ "balanced",
690
+ "ultra"
691
+ ]) }),
692
+ reviewMode: (value) => ({ "review-mode": choice$1("reviewMode", value, ["standard", "debate"]) }),
693
+ reviewRounds: (value) => ({ "review-rounds": String(choice$1("reviewRounds", value, [
694
+ 0,
695
+ 1,
696
+ 2
697
+ ])) }),
698
+ claimChecks: (value) => ({ "claim-checks": boolean("claimChecks", value) }),
699
+ finalizer: (value) => ({ finalizer: string("finalizer", value) }),
700
+ judgeModel: (value) => ({ "judge-model": string("judgeModel", value) }),
701
+ stragglerGrace: (value) => ({ "straggler-grace": seconds("stragglerGrace", value, true) }),
702
+ timeout: (value) => ({ timeout: seconds("timeout", value) }),
703
+ resume: (value) => ({ resume: boolean("resume", value) }),
704
+ rounds: (value) => ({ rounds: boolean("rounds", value) }),
705
+ json: (value) => ({ json: boolean("json", value) }),
706
+ verbose: (value) => ({ verbose: boolean("verbose", value) }),
707
+ allowAnyTask: (value) => ({ "allow-any-task": boolean("allowAnyTask", value) }),
708
+ runsDir: (value, { dir }) => ({ runsDir: (0, node_path.resolve)(dir, string("runsDir", value)) }),
709
+ output: (value, { dir }) => ({ output: (0, node_path.resolve)(dir, string("output", value)) }),
710
+ cwd: (value, { dir }) => ({ cwd: (0, node_path.resolve)(dir, string("cwd", value)) }),
711
+ task: (value) => ({ task: string("task", value) }),
712
+ taskFile: (value, { dir }) => ({ taskFile: (0, node_path.resolve)(dir, string("taskFile", value)) })
713
+ };
714
+ /** Every key a config may hold, `$schema` included; `config.schema.json` must list the same. */
715
+ const CONFIG_KEYS = ["$schema", ...Object.keys(SETTINGS)];
716
+ /**
717
+ * Checks a config's text and turns it into flag-shaped values. Every error
718
+ * names the file and the key. Rules that span a config and the flags, such as
719
+ * a finalizer that is not one of the run's agents, are left to the CLI's
720
+ * parsers, which run on the merged values.
721
+ *
722
+ * `explicit` is whether the file was named with `--config`: only such a file
723
+ * may set `cwd`, so a file found inside a repository cannot send the agents to
724
+ * another one. `judgeEnv` joins the providers' variables in the error for a
725
+ * key that looks like a secret.
726
+ */
727
+ function parseConfig(text, path, explicit, judgeEnv = []) {
728
+ let raw;
729
+ try {
730
+ raw = JSON.parse(text.replace(/^\uFEFF/, ""));
731
+ } catch (error) {
732
+ throw new Error(`${path}: not valid JSON: ${error.message}`, { cause: error });
733
+ }
734
+ const values = {
735
+ model: [],
736
+ effort: [],
737
+ "review-effort": []
738
+ };
739
+ try {
740
+ const top = object("the top level", raw);
741
+ const dir = (0, node_path.dirname)(path);
742
+ for (const [key, value] of Object.entries(top)) {
743
+ if (key === "$schema") continue;
744
+ if (!Object.hasOwn(SETTINGS, key)) throw unknownKey(key, CONFIG_KEYS, judgeEnv);
745
+ Object.assign(values, SETTINGS[key]?.(value, {
746
+ dir,
747
+ judgeEnv
748
+ }));
749
+ }
750
+ if ("cwd" in top && !explicit) throw new Error("cwd: allowed only in a file passed with --config, so a config inside a repository cannot point the agents at another one");
751
+ if ("task" in top && "taskFile" in top) throw new Error("task and taskFile: set one, not both");
752
+ if (values.runsDir !== void 0 && values.rounds === false) throw new Error("runsDir and rounds: false: set one, not both");
753
+ } catch (error) {
754
+ throw new Error(`${path}: ${error.message}`, { cause: error });
755
+ }
756
+ return values;
757
+ }
758
+ /**
759
+ * The config for a run: the file named with `--config` (`explicit`, already
760
+ * resolved), which must exist, or else `setup.file` in `dir`, where a missing
761
+ * file just means no config.
762
+ */
763
+ async function findConfig(dir, explicit, setup) {
764
+ const path = explicit ?? (0, node_path.join)(dir, setup.file);
765
+ let text;
766
+ try {
767
+ text = await (0, node_fs_promises.readFile)(path, "utf8");
768
+ } catch (error) {
769
+ if (explicit === void 0 && error.code === "ENOENT") return void 0;
770
+ throw new Error(`Cannot read the config ${path}: ${error.message}`, { cause: error });
771
+ }
772
+ return {
773
+ path,
774
+ values: parseConfig(text, path, explicit !== void 0, setup.judgeEnv)
775
+ };
776
+ }
777
+ /** Thrown before any agent call when the task is empty or a placeholder. */
778
+ var TaskValidationError = class extends Error {
779
+ name = "TaskValidationError";
780
+ };
781
+ const MISSING_TASK_MESSAGE = "Missing coding task. Pass it as an argument, with --file, on stdin, or as task in the config file.";
782
+ /** Piped text and a configured task at once: the pipe is never silently ignored. */
783
+ const STDIN_CONFLICT_MESSAGE = "The task is piped on stdin and set in the config. Pass the task as an argument or with --file to override the config's task.";
784
+ /**
785
+ * Whole-text placeholders, compared after normalization. Kept short and fixed:
786
+ * a false rejection costs a user more than a wasted run, so widen it only after
787
+ * a real incident.
788
+ */
789
+ const PLACEHOLDERS = /* @__PURE__ */ new Set([
790
+ "describe the coding change you want to plan",
791
+ "your task here",
792
+ "todo",
793
+ "tbd",
794
+ "<coding task>"
795
+ ]);
796
+ /** Unfilled template slots: `<task>`, `{{task}}`, `[task]`, as the whole text. */
797
+ const TEMPLATE_SLOTS = [
798
+ /^<[^>]*>$/,
799
+ /^\{\{.*\}\}$/s,
800
+ /^\[[^\]]*\]$/
801
+ ];
802
+ const TRAILING_PUNCTUATION = /* @__PURE__ */ new Set(".!?,;:…");
803
+ /** Lowercase, collapse whitespace, drop trailing punctuation: for comparison only. */
804
+ function normalize(text) {
805
+ const collapsed = text.toLowerCase().replace(/\s+/g, " ");
806
+ let end = collapsed.length;
807
+ while (end > 0 && TRAILING_PUNCTUATION.has(collapsed.charAt(end - 1))) end -= 1;
808
+ return collapsed.slice(0, end);
809
+ }
810
+ function preview(text) {
811
+ const line = text.replace(/\s+/g, " ");
812
+ return line.length > 60 ? `${line.slice(0, 57)}...` : line;
813
+ }
814
+ /** The trimmed task, or a `TaskValidationError` if nothing is left. */
815
+ function requireNonEmptyTask(raw) {
816
+ const task = raw.trim();
817
+ if (!task) throw new TaskValidationError(MISSING_TASK_MESSAGE);
818
+ return task;
819
+ }
820
+ /**
821
+ * The trimmed task, or a `TaskValidationError` if it is empty or a near-certain
822
+ * placeholder: a known template phrase, an unfilled `<…>`, `{{…}}` or `[…]`
823
+ * slot, or text with no letters in it. Matches the whole text only, so a brief
824
+ * that quotes a placeholder passes, and so does any short real task.
825
+ */
826
+ function validateTask(raw) {
827
+ const task = requireNonEmptyTask(raw);
828
+ if (PLACEHOLDERS.has(normalize(task)) || TEMPLATE_SLOTS.some((slot) => slot.test(task)) || !/\p{L}/u.test(task)) throw new TaskValidationError(`The task looks like a placeholder: "${preview(task)}". Pass the change to plan as an argument, with --file, on stdin or in the config file, or use --allow-any-task to plan it anyway.`);
829
+ return task;
830
+ }
831
+ /** The file a program's CLI looks for in the repository. */
832
+ const configFile = (program) => `${program.name}.json`;
833
+ /** Where a program's runs keep their rounds by default, relative to the repository. */
834
+ const runsDir = (program) => `.${program.name}`;
835
+ /**
836
+ * Every secret the program knows of, the judge's and every provider's: what
837
+ * `createAgents` is given as `omitEnv`, so no agent subprocess inherits one.
838
+ */
839
+ const secretEnv = (program) => [...program.judgeEnv.map(({ variable }) => variable), ...PROVIDERS.flatMap(({ secretEnv }) => secretEnv)];
840
+ function agentLine(provider) {
841
+ const access = provider.kind === "cli" ? `agent CLI, reads the repository${provider.effort ? "; takes --effort" : ""}` : `chat API, gets a repository snapshot; needs ${provider.secretEnv.join(", ")}`;
842
+ return ` ${provider.id.padEnd(26)}${provider.label}: ${access}`;
843
+ }
844
+ /** `--help`'s text for `program`. */
845
+ function helpText(program) {
846
+ const { name, judge } = program;
847
+ const variables = program.judgeEnv.map(({ variable }) => variable);
848
+ const requires = variables.length > 0 ? ` ${judge} requires ${variables.join(", ")}, which the config never holds.` : "";
849
+ return `${name} — ${program.summary}
850
+
851
+ Usage:
852
+ ${name} [plan] [options] "<coding task>"
853
+ ${name} doctor [--agents <agents>] [--cwd <directory>]
854
+
855
+ Options:
856
+ -c, --config <path> Read the run's settings from this JSON file
857
+ (default: ${configFile(program)} in the repository, if there is one)
858
+ --no-config Read no config file
859
+ -C, --cwd <directory> Repository to inspect (default: current directory)
860
+ -f, --file <path> Read the coding task from a UTF-8 file
861
+ -o, --output <path> Write the final plan to a file instead of stdout
862
+ -a, --agents <provider[:name],…>
863
+ Two or more comma-separated agents (default: ${DEFAULT_AGENTS.join(",")});
864
+ a provider can appear twice under different names,
865
+ as codex:sol,codex:terra
866
+ -m, --model <name>=<model> Override one agent's model; repeatable
867
+ -e, --effort <name>=<level> Override one agent's reasoning effort; repeatable
868
+ --review-effort <name>=<level>
869
+ The effort for its cross-review and synthesis only; repeatable
870
+ --judge-model <model> Override ${judge}'s model (default: ${program.judgeModelDefault})
871
+ --finalizer <name> auto, none, or one of the agents (default: auto/${judge} decides);
872
+ none keeps a cross-reviewed plan ${judge} rates stronger, unmerged
873
+ --mode <fast|balanced|ultra>
874
+ fast: answer with the first draft ${judge} accepts alone
875
+ balanced: ${judge} skips the rounds a run does not need
876
+ ultra: always cross-review, then merge (default: balanced)
877
+ --review-rounds <0|1|2> Maximum cross-review rounds (default: 2)
878
+ --review-mode <standard|debate>
879
+ debate: agents critique each other, authors answer,
880
+ ${judge} rules on the disagreements (experimental; default: standard)
881
+ --claim-checks In a debate, have two agents that read the repository
882
+ check the disputed claims about it; implies debate
883
+ --straggler-grace <s> In balanced and fast mode, how long a round waits for the agents
884
+ still working once half have answered; 0 waits for
885
+ every agent (default: 90)
886
+ --timeout <seconds> Timeout for each agent call (default: 600)
887
+ --no-resume Start each agent call afresh, not from its draft session
888
+ --json Emit plan metadata as JSON
889
+ --verbose Stream each agent's work, then ${judge}'s verdict, to stderr
890
+ --rounds-dir <path> Write every round's plans to round1/, round2/, …, final/
891
+ (default: .${name}/<run>/ in the repository)
892
+ --no-rounds Do not write the rounds anywhere
893
+ --allow-any-task Plan the task even if it looks like a placeholder
894
+ -h, --help Show help
895
+ -v, --version Show version
896
+
897
+ Agents:
898
+ ${PROVIDERS.map(agentLine).join("\n")}
899
+
900
+ The task can also be piped on stdin. A flag given here beats the config; --json,
901
+ --verbose, --claim-checks and --allow-any-task each have a --no- form, and --resume
902
+ and --rounds turn back on what the config turned off. Agent CLIs use their existing
903
+ logins.${requires}`;
904
+ }
905
+ /** The planner's agents for `setup`: each spec's provider, built under the spec's name and label. */
906
+ function createAgents(setup, env, omitEnv) {
907
+ return setup.agents.map(({ name, label, provider }) => provider.create({
908
+ name,
909
+ label,
910
+ ...setup.models[name] === void 0 ? {} : { model: setup.models[name] },
911
+ ...setup.efforts[name] === void 0 ? {} : { effort: setup.efforts[name] },
912
+ omitEnv,
913
+ env
914
+ }));
915
+ }
916
+ async function assertDirectory(path) {
917
+ if (!(await (0, node_fs_promises.stat)(path).catch(() => void 0))?.isDirectory()) throw new Error(`Not a directory: ${path}`);
918
+ }
919
+ const PROVIDER_IDS = PROVIDERS.map(({ id }) => id).join(", ");
920
+ function provider(id, flag) {
921
+ const found = PROVIDERS.find((candidate) => candidate.id === id);
922
+ if (!found) throw new Error(`Unknown agent in ${flag}: ${id}. Expected one of ${PROVIDER_IDS}.`);
923
+ return found;
924
+ }
925
+ /** `<provider>[:<name>]`, as `--agents` and the config's `agents` list it. */
926
+ function agentSpec(entry) {
927
+ const [id = "", name, ...rest] = entry.split(":").map((part) => part.trim().toLowerCase());
928
+ if (rest.length > 0) throw new Error(`Invalid agent in --agents: ${entry}. Expected <provider>[:<name>].`);
929
+ const found = provider(id, "--agents");
930
+ const resolved = name === void 0 ? found.id : validateAgentName(name, found.id);
931
+ return {
932
+ name: resolved,
933
+ label: agentLabel(found, resolved),
934
+ provider: found
935
+ };
936
+ }
937
+ function parseAgents(value) {
938
+ const agents = (value ?? DEFAULT_AGENTS.join(",")).split(",").map((entry) => entry.trim()).filter(Boolean).map(agentSpec);
939
+ const names = agents.map(({ name }) => name);
940
+ const duplicate = names.find((name, index) => names.indexOf(name) !== index);
941
+ if (duplicate !== void 0) {
942
+ const hint = PROVIDERS.some(({ id }) => id === duplicate) ? `; name each instance: ${duplicate}:a,${duplicate}:b` : "";
943
+ throw new Error(`--agents lists ${duplicate} more than once${hint}`);
944
+ }
945
+ if (agents.length < 2) throw new Error("--agents needs at least two agents");
946
+ return agents;
947
+ }
948
+ /** The run's agent `name` sets, or an error that says what `name` is instead. */
949
+ function agentNamed(name, flag, agents) {
950
+ const found = agents.find((agent) => agent.name === name);
951
+ if (found) return found;
952
+ const instances = agents.filter((agent) => agent.provider.id === name);
953
+ const first = instances[0];
954
+ if (first) throw new Error(`${flag} ${name}: the run's ${first.provider.label} agents are ${instances.map((agent) => agent.name).join(", ")}; name one`);
955
+ if (PROVIDERS.some(({ id }) => id === name)) throw new Error(`${flag} sets ${name}, which is not one of the --agents`);
956
+ throw new Error(`Unknown agent in ${flag}: ${name}. Expected one of ${agents.map((agent) => agent.name).join(", ")}.`);
957
+ }
958
+ /** Repeated `<name>=<value>` flags, by agent name; each must be one of the run's agents. */
959
+ function parseOverrides(flag, values, agents, accepts = () => true) {
960
+ const overrides = {};
961
+ const placeholder = flag.slice(2);
962
+ for (const value of values) {
963
+ const separator = value.indexOf("=");
964
+ const name = value.slice(0, separator).trim().toLowerCase();
965
+ const setting = value.slice(separator + 1).trim();
966
+ if (separator < 0 || !name || !setting) throw new Error(`Invalid ${flag} value: ${value}. Expected <agent>=<${placeholder}>.`);
967
+ const agent = agentNamed(name, flag, agents);
968
+ if (!accepts(agent.provider)) throw new Error(`${agent.provider.label} does not take ${flag}`);
969
+ overrides[name] = setting;
970
+ }
971
+ return overrides;
972
+ }
973
+ /** `auto` gives `undefined`, `none` gives `'none'`, an agent gives its name. */
974
+ function parseFinalizer(value, agents) {
975
+ if (value === void 0 || value === "auto") return void 0;
976
+ const name = value.toLowerCase();
977
+ if (name === "none" || agents.some((agent) => agent.name === name)) return name;
978
+ throw new Error(`Invalid --finalizer value: ${value}. Expected auto, none or one of ${agents.map((a) => a.name).join(", ")}.`);
979
+ }
980
+ /** Creates `dir`, which must be new or empty so rounds from different runs never mix. */
981
+ async function prepareRoundsDir(dir) {
982
+ if ((await (0, node_fs_promises.readdir)(dir).catch((error) => {
983
+ if (error.code === "ENOENT") return [];
984
+ throw error;
985
+ })).length > 0) throw new Error(`--rounds-dir must be new or empty: ${dir}`);
986
+ await (0, node_fs_promises.mkdir)(dir, { recursive: true });
987
+ }
988
+ /**
989
+ * A new folder for this run under `home`, `<cwd>/.<name>/` unless the
990
+ * config names a `runsDir`, named by its UTC start time with no `:` so it is a valid name on Windows too. A second run in the
991
+ * same second gets `-2`, and so on: `mkdir` without `recursive` fails on an
992
+ * existing folder, so two runs can never claim the same one.
993
+ *
994
+ * The first run also writes `.gitignore` with `*` in `home`, so the output
995
+ * never shows up as untracked in the repository being planned. One that is
996
+ * already there is left alone.
997
+ */
998
+ async function newRunDir(home, now, name) {
999
+ await (0, node_fs_promises.mkdir)(home, { recursive: true });
1000
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(home, ".gitignore"), `# Written by ${name}: run output, not source.\n*\n`, { flag: "wx" }).catch((error) => {
1001
+ if (error.code !== "EEXIST") throw error;
1002
+ });
1003
+ const stamp = now.toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
1004
+ for (let attempt = 1;; attempt++) {
1005
+ const dir = (0, node_path.join)(home, attempt === 1 ? stamp : `${stamp}-${String(attempt)}`);
1006
+ try {
1007
+ await (0, node_fs_promises.mkdir)(dir);
1008
+ return dir;
1009
+ } catch (error) {
1010
+ if (error.code !== "EEXIST") throw error;
1011
+ }
1012
+ }
1013
+ }
1014
+ /** What a debate round's raw answers are saved as, beside its plans. */
1015
+ const ARTIFACT_SUFFIX = {
1016
+ critique: "critique",
1017
+ reply: "reply",
1018
+ review: "reply",
1019
+ check: "check"
1020
+ };
1021
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
1022
+ /**
1023
+ * One folder per round: `round<N>/<agent>.md` for each agent's plan, with the judge's
1024
+ * `verdict.json` beside a judged round's plans, `timings.json` in every
1025
+ * round, and `final/plan.md` last. A debate round adds each agent's raw answer
1026
+ * as `<agent>.critique.md`, `.reply.md` or `.check.md`, and what was parsed
1027
+ * from them as `objections.json`, `replies.json` and `disputes.json`; its
1028
+ * critique and check rounds leave the plans unchanged, so write none.
1029
+ */
1030
+ async function writeRound(dir, round) {
1031
+ const folder = (0, node_path.join)(dir, round.stage === "final" ? "final" : `round${String(round.round)}`);
1032
+ await (0, node_fs_promises.mkdir)(folder, { recursive: true });
1033
+ const files = round.stage === "final" ? Object.entries(round.plans).map(([agent, plan]) => ["plan.md", `<!-- ${round.selected ? "selected from" : "merged by"} ${agent} -->\n${plan.trim()}\n`]) : round.stage === "critique" || round.stage === "check" ? [] : Object.entries(round.plans).map(([agent, plan]) => [`${agent}.md`, `${plan.trim()}\n`]);
1034
+ const suffix = ARTIFACT_SUFFIX[round.stage];
1035
+ if (round.artifacts && suffix !== void 0) for (const [agent, text] of Object.entries(round.artifacts)) files.push([`${agent}.${suffix}.md`, `${text.trim()}\n`]);
1036
+ const { debate } = round;
1037
+ if (debate) {
1038
+ if (round.stage === "critique") files.push(["objections.json", json(debate.objections)]);
1039
+ if (debate.replies) files.push(["replies.json", json({
1040
+ replies: debate.replies,
1041
+ unanswered: debate.unanswered ?? []
1042
+ })]);
1043
+ if (debate.disputes) files.push(["disputes.json", json({
1044
+ disputes: debate.disputes,
1045
+ overflow: debate.overflow ?? [],
1046
+ ...debate.claimChecks ? { claimChecks: debate.claimChecks } : {}
1047
+ })]);
1048
+ }
1049
+ if (round.verdict) files.push(["verdict.json", json(round.verdict)]);
1050
+ files.push(["timings.json", `${JSON.stringify(round.timings, null, 2)}\n`]);
1051
+ await Promise.all(files.map(([name, text]) => (0, node_fs_promises.writeFile)((0, node_path.join)(folder, name), text, "utf8")));
1052
+ }
1053
+ /** `4m12s`, `51s` or `0.8s`: minutes once a minute has passed, tenths below ten seconds. */
1054
+ function formatDuration(ms) {
1055
+ const seconds = ms / 1e3;
1056
+ if (seconds < 10) return `${seconds.toFixed(1)}s`;
1057
+ const whole = Math.round(seconds);
1058
+ if (whole < 60) return `${String(whole)}s`;
1059
+ return `${String(Math.floor(whole / 60))}m${String(whole % 60).padStart(2, "0")}s`;
1060
+ }
1061
+ const STAGE_NAMES = {
1062
+ draft: "Drafts",
1063
+ critique: "Critiques",
1064
+ reply: "Replies",
1065
+ check: "Claim checks",
1066
+ review: "Review",
1067
+ final: "Final plan"
1068
+ };
1069
+ /** One line per round for `--verbose`: the round, then each agent call and the judge's. */
1070
+ function roundTimingLine(round, labels, judge) {
1071
+ const calls = [...Object.entries(round.timings.agents).map(([agent, ms]) => `${labels.get(agent) ?? agent} ${formatDuration(ms)}`), ...round.timings.judgeMs === void 0 ? [] : [`${judge} ${formatDuration(round.timings.judgeMs)}`]];
1072
+ const line = `${STAGE_NAMES[round.stage]}: ${formatDuration(round.timings.totalMs)}`;
1073
+ return calls.length > 0 ? `${line} (${calls.join(", ")})` : line;
1074
+ }
1075
+ /**
1076
+ * What the run spent, on one line, so the cost of a mode is visible without
1077
+ * `--json`. `judge` names the judge's calls: `Jev`.
1078
+ */
1079
+ function costLine(cost, judge) {
1080
+ const plural = (count, thing) => `${String(count)} ${thing}${count === 1 ? "" : "s"}`;
1081
+ const parts = [
1082
+ `${cost.mode} mode`,
1083
+ ...cost.reviewMode === "debate" ? ["debate review"] : [],
1084
+ plural(cost.agentCalls, "agent call"),
1085
+ plural(cost.judgeCalls, `${judge} call`),
1086
+ plural(cost.reviewRounds, "cross-review round"),
1087
+ cost.synthesized ? "merged" : cost.mode === "fast" ? "selected" : "adopted whole"
1088
+ ];
1089
+ if (cost.dropped.length > 0) parts.push(`not waited for: ${cost.dropped.join(", ")}`);
1090
+ return parts.join(", ");
1091
+ }
1092
+ function parseTimeout(value) {
1093
+ const seconds = Number(value ?? "600");
1094
+ if (!Number.isFinite(seconds) || seconds <= 0) throw new Error("--timeout must be a positive number of seconds");
1095
+ return Math.round(seconds * 1e3);
1096
+ }
1097
+ function parseReviewRounds(value) {
1098
+ if (value === void 0 || value === "2") return 2;
1099
+ if (value === "1") return 1;
1100
+ if (value === "0") return 0;
1101
+ throw new Error("--review-rounds must be 0, 1 or 2");
1102
+ }
1103
+ function parseMode(value) {
1104
+ if (value === void 0 || value === "balanced") return "balanced";
1105
+ if (value === "ultra" || value === "fast") return value;
1106
+ throw new Error(`Invalid --mode value: ${value}. Expected fast, balanced or ultra.`);
1107
+ }
1108
+ function parseReviewMode(value, claimChecks, reviewRounds, planMode) {
1109
+ let mode;
1110
+ if (value === void 0) mode = claimChecks ? "debate" : "standard";
1111
+ else if (value === "standard" || value === "debate") mode = value;
1112
+ else throw new Error(`Invalid --review-mode value: ${value}. Expected standard or debate.`);
1113
+ if (claimChecks && mode !== "debate") throw new Error("--claim-checks runs in the debate review; drop --review-mode standard");
1114
+ if (mode === "debate" && planMode === "fast") throw new Error(`${claimChecks ? "--claim-checks" : "--review-mode debate"} needs a review round, and --mode fast has none`);
1115
+ if (mode === "debate" && reviewRounds === 0) throw new Error("--review-mode debate is a review round; it needs --review-rounds 1 or 2");
1116
+ return mode;
1117
+ }
1118
+ function parseStragglerGrace(value, mode) {
1119
+ if (value === void 0) return void 0;
1120
+ if (mode === "ultra") throw new Error("--straggler-grace is for --mode balanced or fast; ultra never drops an agent");
1121
+ const seconds = Number(value);
1122
+ if (!Number.isFinite(seconds) || seconds < 0) throw new Error("--straggler-grace must be a number of seconds, 0 or more");
1123
+ return Math.round(seconds * 1e3);
1124
+ }
1125
+ /**
1126
+ * A boolean flag and its `--no-` form: `undefined` when neither was given, so
1127
+ * the config decides. Written out rather than with `allowNegative`, which also
1128
+ * takes `--no-cwd` and the like as `false`.
1129
+ */
1130
+ function toggle(on, off, name) {
1131
+ if (on && off) throw new Error(`Pass --${name} or --no-${name}, not both`);
1132
+ return on ? true : off ? false : void 0;
1133
+ }
1134
+ /**
1135
+ * The config's per-agent `<name>=<value>` entries for the run's agents only,
1136
+ * then the flags', which win. An entry is kept only where the run's agent of
1137
+ * that name has the provider the config gave it, so a Codex model set for
1138
+ * `sol` never reaches `--agents claude:sol`.
1139
+ */
1140
+ function withConfig(configured, given, agents, configAgents) {
1141
+ const providerOf = new Map((configAgents ?? "").split(",").map((entry) => {
1142
+ const [id = "", name = id] = entry.split(":");
1143
+ return [name, id];
1144
+ }));
1145
+ return [...configured.filter((entry) => {
1146
+ const name = entry.slice(0, entry.indexOf("="));
1147
+ return agents.some((agent) => agent.name === name && agent.provider.id === providerOf.get(name));
1148
+ }), ...given];
1149
+ }
1150
+ /**
1151
+ * A warning for agents of one provider with the same model and draft effort,
1152
+ * whose drafts may barely differ; `undefined` when every agent differs.
1153
+ */
1154
+ function identicalWarning(agents, models, efforts) {
1155
+ const groups = /* @__PURE__ */ new Map();
1156
+ for (const agent of agents) {
1157
+ const key = JSON.stringify([
1158
+ agent.provider.id,
1159
+ models[agent.name],
1160
+ efforts[agent.name]
1161
+ ]);
1162
+ groups.set(key, [...groups.get(key) ?? [], agent]);
1163
+ }
1164
+ const same = [...groups.values()].find((group) => group.length > 1);
1165
+ if (!same) return void 0;
1166
+ const names = same.map(({ name }) => name);
1167
+ const list = `${names.slice(0, -1).join(", ")} and ${String(names.at(-1))}`;
1168
+ const [first] = same;
1169
+ return `${list} are ${same.length > 2 ? "all" : "both"} ${String(first?.provider.label)} with the same model and effort; their drafts may barely differ. Vary --model or --effort.`;
1170
+ }
1171
+ const NO_CONFIG = {
1172
+ model: [],
1173
+ effort: [],
1174
+ "review-effort": []
1175
+ };
1176
+ function parse(argv) {
1177
+ return (0, node_util.parseArgs)({
1178
+ args: [...argv],
1179
+ allowPositionals: true,
1180
+ strict: true,
1181
+ options: {
1182
+ config: {
1183
+ type: "string",
1184
+ short: "c"
1185
+ },
1186
+ "no-config": { type: "boolean" },
1187
+ cwd: {
1188
+ type: "string",
1189
+ short: "C"
1190
+ },
1191
+ file: {
1192
+ type: "string",
1193
+ short: "f"
1194
+ },
1195
+ output: {
1196
+ type: "string",
1197
+ short: "o"
1198
+ },
1199
+ agents: {
1200
+ type: "string",
1201
+ short: "a"
1202
+ },
1203
+ model: {
1204
+ type: "string",
1205
+ short: "m",
1206
+ multiple: true,
1207
+ default: []
1208
+ },
1209
+ effort: {
1210
+ type: "string",
1211
+ short: "e",
1212
+ multiple: true,
1213
+ default: []
1214
+ },
1215
+ "review-effort": {
1216
+ type: "string",
1217
+ multiple: true,
1218
+ default: []
1219
+ },
1220
+ "judge-model": { type: "string" },
1221
+ finalizer: { type: "string" },
1222
+ mode: { type: "string" },
1223
+ "review-rounds": { type: "string" },
1224
+ "review-mode": { type: "string" },
1225
+ "claim-checks": { type: "boolean" },
1226
+ "no-claim-checks": { type: "boolean" },
1227
+ "straggler-grace": { type: "string" },
1228
+ timeout: { type: "string" },
1229
+ resume: { type: "boolean" },
1230
+ "no-resume": { type: "boolean" },
1231
+ json: { type: "boolean" },
1232
+ "no-json": { type: "boolean" },
1233
+ verbose: { type: "boolean" },
1234
+ "no-verbose": { type: "boolean" },
1235
+ "rounds-dir": { type: "string" },
1236
+ rounds: { type: "boolean" },
1237
+ "no-rounds": { type: "boolean" },
1238
+ "allow-any-task": { type: "boolean" },
1239
+ "no-allow-any-task": { type: "boolean" },
1240
+ help: {
1241
+ type: "boolean",
1242
+ short: "h",
1243
+ default: false
1244
+ },
1245
+ version: {
1246
+ type: "boolean",
1247
+ short: "v",
1248
+ default: false
1249
+ }
1250
+ }
1251
+ });
1252
+ }
1253
+ async function run(argv, deps) {
1254
+ const { values, positionals } = parse(argv);
1255
+ const { program } = deps;
1256
+ const say = (message) => {
1257
+ deps.stderr(`[${program.name}] ${message}\n`);
1258
+ };
1259
+ if (values.help) {
1260
+ deps.stdout(`${helpText(program)}\n`);
1261
+ return 0;
1262
+ }
1263
+ if (values.version) {
1264
+ deps.stdout(`${program.version}\n`);
1265
+ return 0;
1266
+ }
1267
+ const invoked = deps.cwd();
1268
+ if (values.config !== void 0 && values["no-config"]) throw new Error("Pass --config or --no-config, not both");
1269
+ const config = values["no-config"] ? void 0 : await findConfig((0, node_path.resolve)(invoked, values.cwd ?? "."), values.config === void 0 ? void 0 : (0, node_path.resolve)(invoked, values.config), {
1270
+ file: configFile(program),
1271
+ judgeEnv: program.judgeEnv.map(({ variable }) => variable)
1272
+ });
1273
+ if (config) say(`Using config ${config.path}`);
1274
+ const configured = config?.values ?? NO_CONFIG;
1275
+ const cwd = values.cwd === void 0 ? configured.cwd ?? invoked : (0, node_path.resolve)(invoked, values.cwd);
1276
+ await assertDirectory(cwd);
1277
+ const agents = parseAgents(values.agents ?? configured.agents);
1278
+ if (positionals[0] === "doctor") {
1279
+ if (positionals.length > 1) throw new Error("doctor does not accept a task");
1280
+ const providers = [...new Set(agents.map(({ provider }) => provider))];
1281
+ const checks = [...await deps.doctor(cwd, providers), ...program.judgeEnv.map(({ check, variable }) => envCheck(check, variable, deps.env))];
1282
+ for (const check of checks) deps.stdout(`${check.ok ? "✓" : "✗"} ${check.name}: ${check.detail}\n`);
1283
+ return checks.every((check) => check.ok) ? 0 : 1;
1284
+ }
1285
+ const taskPositionals = positionals[0] === "plan" ? positionals.slice(1) : positionals;
1286
+ if (values.file !== void 0 && taskPositionals.length > 0) throw new Error("Provide the task either as arguments or with --file, not both");
1287
+ let task = taskPositionals.join(" ").trim();
1288
+ if (values.file !== void 0) task = (await (0, node_fs_promises.readFile)((0, node_path.resolve)(invoked, values.file), "utf8")).trim();
1289
+ if (!task) {
1290
+ const piped = (await deps.readStdin())?.trim() ?? "";
1291
+ if (piped && (configured.task ?? configured.taskFile) !== void 0) throw new Error(STDIN_CONFLICT_MESSAGE);
1292
+ if (configured.task !== void 0) task = configured.task;
1293
+ else if (configured.taskFile !== void 0) task = (await (0, node_fs_promises.readFile)(configured.taskFile, "utf8")).trim();
1294
+ else task = piped;
1295
+ }
1296
+ const allowAnyTask = toggle(values["allow-any-task"], values["no-allow-any-task"], "allow-any-task") ?? configured["allow-any-task"] ?? false;
1297
+ task = allowAnyTask ? requireNonEmptyTask(task) : validateTask(task);
1298
+ const unset = program.judgeEnv.find(({ variable }) => !deps.env[variable]?.trim());
1299
+ if (unset) throw new Error(unset.missing);
1300
+ const models = parseOverrides("--model", withConfig(configured.model, values.model, agents, configured.agents), agents);
1301
+ const efforts = parseOverrides("--effort", withConfig(configured.effort, values.effort, agents, configured.agents), agents, (provider) => provider.effort);
1302
+ const reviewEfforts = parseOverrides("--review-effort", withConfig(configured["review-effort"], values["review-effort"], agents, configured.agents), agents, (provider) => provider.effort);
1303
+ const finalizer = parseFinalizer(values.finalizer ?? configured.finalizer, agents);
1304
+ const judgeModel = values["judge-model"] ?? configured["judge-model"];
1305
+ const mode = parseMode(values.mode ?? configured.mode);
1306
+ const stragglerGraceMs = parseStragglerGrace(values["straggler-grace"] ?? configured["straggler-grace"], mode);
1307
+ const maxReviewRounds = parseReviewRounds(values["review-rounds"] ?? configured["review-rounds"]);
1308
+ const claimChecks = toggle(values["claim-checks"], values["no-claim-checks"], "claim-checks") ?? configured["claim-checks"] ?? false;
1309
+ const reviewMode = parseReviewMode(values["review-mode"] ?? configured["review-mode"], claimChecks, maxReviewRounds, mode);
1310
+ const resume = toggle(values.resume, values["no-resume"], "resume") ?? configured.resume ?? true;
1311
+ const asJson = toggle(values.json, values["no-json"], "json") ?? configured.json ?? false;
1312
+ const verbose = toggle(values.verbose, values["no-verbose"], "verbose") ?? configured.verbose ?? false;
1313
+ const rounds = toggle(values.rounds, values["no-rounds"], "rounds");
1314
+ if (rounds === false && values["rounds-dir"] !== void 0) throw new Error("Pass --rounds-dir or --no-rounds, not both");
1315
+ let roundsDir;
1316
+ if (values["rounds-dir"] !== void 0) {
1317
+ roundsDir = (0, node_path.resolve)(cwd, values["rounds-dir"]);
1318
+ await prepareRoundsDir(roundsDir);
1319
+ } else if (rounds ?? configured.rounds ?? true) roundsDir = await newRunDir(configured.runsDir ?? (0, node_path.join)(cwd, runsDir(program)), deps.now(), program.name);
1320
+ if (roundsDir !== void 0) say(`Writing rounds to ${roundsDir}`);
1321
+ const warning = identicalWarning(agents, models, efforts);
1322
+ if (warning !== void 0) say(warning);
1323
+ const planner = deps.createPlanner({
1324
+ agents,
1325
+ models,
1326
+ efforts
1327
+ });
1328
+ const labels = new Map(agents.map(({ name, label }) => [name, label]));
1329
+ const result = await planner.plan({
1330
+ task,
1331
+ cwd,
1332
+ timeoutMs: parseTimeout(values.timeout ?? configured.timeout),
1333
+ mode,
1334
+ maxReviewRounds,
1335
+ ...reviewMode === "debate" ? { reviewMode } : {},
1336
+ ...claimChecks ? { claimChecks } : {},
1337
+ ...stragglerGraceMs === void 0 ? {} : { stragglerGraceMs },
1338
+ ...judgeModel ? { judgeModel } : {},
1339
+ ...finalizer === "none" ? { selectStronger: true } : finalizer ? { finalizer } : {},
1340
+ ...allowAnyTask ? { allowAnyTask } : {},
1341
+ ...Object.keys(reviewEfforts).length > 0 ? { reviewEfforts } : {},
1342
+ ...resume ? {} : { resume: false },
1343
+ onStage: say,
1344
+ ...verbose ? { onAgentProgress: (agent, progress) => {
1345
+ for (const line of progress.split("\n")) deps.stderr(`[${agent}] ${line}\n`);
1346
+ } } : {},
1347
+ ...roundsDir === void 0 && !verbose ? {} : { onRound: async (round) => {
1348
+ if (verbose) say(roundTimingLine(round, labels, program.judge));
1349
+ if (roundsDir !== void 0) await writeRound(roundsDir, round);
1350
+ } }
1351
+ });
1352
+ if (verbose) {
1353
+ say(`Total: ${formatDuration(result.timings.totalMs)}`);
1354
+ say(`${program.judge} verdict:\n${JSON.stringify(result.verdict, null, 2)}`);
1355
+ }
1356
+ say(costLine(result.cost, program.judge));
1357
+ const rendered = asJson ? `${JSON.stringify({
1358
+ plan: result.plan,
1359
+ verdict: result.verdict,
1360
+ finalizer: result.finalizer,
1361
+ ...result.selected ? { selected: true } : {},
1362
+ ...result.debate ? { debate: result.debate } : {},
1363
+ timings: result.timings,
1364
+ cost: result.cost
1365
+ }, null, 2)}\n` : `${result.plan.trim()}\n`;
1366
+ const output = values.output ?? configured.output;
1367
+ if (output === void 0) deps.stdout(rendered);
1368
+ else {
1369
+ const outputPath = (0, node_path.resolve)(cwd, output);
1370
+ await (0, node_fs_promises.writeFile)(outputPath, rendered, "utf8");
1371
+ say(`Wrote ${outputPath}`);
1372
+ }
1373
+ return 0;
1374
+ }
1375
+ /**
1376
+ * Runs the CLI and resolves to its exit code. Never rejects: every failure is
1377
+ * reported on stderr as `<name>: <message>` with exit code 1.
1378
+ */
1379
+ async function main(argv, deps) {
1380
+ try {
1381
+ return await run(argv, deps);
1382
+ } catch (error) {
1383
+ deps.stderr(`${deps.program.name}: ${error instanceof Error ? error.message : String(error)}\n`);
1384
+ return 1;
1385
+ }
1386
+ }
1387
+ const TARGET = /^[\s#>*_-]*target\s*:\s*[*_`]*\s*(.+?)\s*[*_`]*\s*$/i;
1388
+ const OBJECTION = /^\s*[-*]?\s*\**C(\d+)\**\s*(\[[^\]]+\])?\s*\**\s*[:.)]\s*\**\s*(.+)$/i;
1389
+ const REASON = /\s+(?:—|–|--|-)\s+|\s*[—–]\s*/;
1390
+ const REPLY = /^\s*[-*]?\s*(?:R\s+)?\**([\w.-]+:[\w.-]+:C\d+)\**\s*:\s*\**(accept|reject)\**\s*(?:[—–:-]+\s*)?(.*)$/i;
1391
+ const CHECK = /^\s*[-*]?\s*\**(D\d+)\**\s*:\s*\**(confirm|refute|unknown)\**\s*(?:[—–:-]+\s*)?(.*)$/i;
1392
+ const lines = (text) => text.replace(/\r\n?/g, "\n").split("\n");
1393
+ /** Splits `claim — reason` on the first dash that separates them. */
1394
+ function claimAndReason(text) {
1395
+ const match = REASON.exec(text);
1396
+ if (!match) return {
1397
+ claim: text.trim(),
1398
+ why: ""
1399
+ };
1400
+ return {
1401
+ claim: text.slice(0, match.index).trim(),
1402
+ why: text.slice(match.index + match[0].length).trim()
1403
+ };
1404
+ }
1405
+ /**
1406
+ * One critic's objections, by the agent each is aimed at. Lines that are not
1407
+ * objections, objections to an agent that is not a peer, repeated ids and
1408
+ * objections past `MAX_OBJECTIONS` per peer go to `prose`. Never throws.
1409
+ */
1410
+ function parseCritique(critic, text, peers) {
1411
+ const objections = [];
1412
+ const prose = [];
1413
+ const lookup = (value) => {
1414
+ const wanted = value.trim().toLowerCase();
1415
+ return peers.find(({ name, label }) => name.toLowerCase() === wanted || label.toLowerCase() === wanted);
1416
+ };
1417
+ let target = peers.length === 1 ? peers[0] : void 0;
1418
+ for (const line of lines(text)) {
1419
+ const heading = TARGET.exec(line);
1420
+ if (heading?.[1] !== void 0) {
1421
+ target = lookup(heading[1]);
1422
+ if (!target) prose.push(line.trim());
1423
+ continue;
1424
+ }
1425
+ const [, number, tag, body] = OBJECTION.exec(line) ?? [];
1426
+ const id = target && number !== void 0 ? `${critic}:${target.name}:C${number}` : void 0;
1427
+ if (!target || id === void 0 || body === void 0 || objections.some((objection) => objection.id === id) || objections.filter((objection) => objection.target === target?.name).length >= 5) {
1428
+ if (line.trim()) prose.push(line.trim());
1429
+ continue;
1430
+ }
1431
+ objections.push({
1432
+ id,
1433
+ critic,
1434
+ target: target.name,
1435
+ ...claimAndReason(body.replace(/\*+$/, "")),
1436
+ repo: tag?.toLowerCase() === "[repo]"
1437
+ });
1438
+ }
1439
+ return {
1440
+ objections,
1441
+ prose: prose.join("\n")
1442
+ };
1443
+ }
1444
+ /** The text between `<tag>` and `</tag>`, or to the end when it is never closed. */
1445
+ function tagged(text, tag) {
1446
+ const open = new RegExp(`<${tag}>`, "i").exec(text);
1447
+ if (!open) return void 0;
1448
+ const from = open.index + open[0].length;
1449
+ const close = new RegExp(`</${tag}>`, "i").exec(text.slice(from));
1450
+ const to = close ? from + close.index : text.length;
1451
+ return {
1452
+ inner: text.slice(from, to),
1453
+ start: open.index,
1454
+ end: close ? to + close[0].length : text.length
1455
+ };
1456
+ }
1457
+ /**
1458
+ * An author's answers to the objections `ids`, and its revised plan.
1459
+ *
1460
+ * The plan is the `<revised-plan>` block, else what follows a `## Revised plan`
1461
+ * heading, else the text outside `<replies>` without the reply lines; when all
1462
+ * of those are empty it is `previousPlan`, so reply prose never stands in for
1463
+ * a plan. A reply to an id not in `ids` is ignored, and for a repeated id the
1464
+ * first reply wins. Never throws.
1465
+ */
1466
+ function parseReply(author, text, ids, previousPlan) {
1467
+ const normalized = text.replace(/\r\n?/g, "\n");
1468
+ const planBlock = tagged(normalized, "revised-plan");
1469
+ const withoutPlan = planBlock ? normalized.slice(0, planBlock.start) + normalized.slice(planBlock.end) : normalized;
1470
+ const replyBlock = tagged(withoutPlan, "replies");
1471
+ const replies = [];
1472
+ const prose = [];
1473
+ const known = new Map(ids.map((id) => [id.toLowerCase(), id]));
1474
+ const replyLines = /* @__PURE__ */ new Set();
1475
+ for (const line of lines(replyBlock ? replyBlock.inner : withoutPlan)) {
1476
+ const [, rawId, decision, reason] = REPLY.exec(line) ?? [];
1477
+ const id = rawId === void 0 ? void 0 : known.get(rawId.toLowerCase());
1478
+ if (rawId !== void 0) replyLines.add(line);
1479
+ if (id === void 0 || decision === void 0 || reason === void 0) {
1480
+ if (replyBlock && rawId === void 0 && line.trim()) prose.push(line.trim());
1481
+ continue;
1482
+ }
1483
+ if (replies.some((reply) => reply.id === id)) continue;
1484
+ replies.push({
1485
+ id,
1486
+ author,
1487
+ decision: decision.toLowerCase() === "accept" ? "accept" : "reject",
1488
+ reason: reason.trim()
1489
+ });
1490
+ }
1491
+ let plan = planBlock?.inner.trim() ?? "";
1492
+ if (!plan) {
1493
+ const heading = /^#{1,6}\s*revised plan\s*$/im.exec(withoutPlan);
1494
+ const rest = heading ? withoutPlan.slice(heading.index + heading[0].length) : replyBlock ? withoutPlan.slice(0, replyBlock.start) + withoutPlan.slice(replyBlock.end) : withoutPlan;
1495
+ plan = lines(rest).filter((line) => !replyLines.has(line)).join("\n").trim();
1496
+ }
1497
+ return {
1498
+ replies,
1499
+ plan: plan || previousPlan,
1500
+ prose: prose.join("\n")
1501
+ };
1502
+ }
1503
+ /** Lowercase, punctuation and whitespace collapsed: two claims match only when these are equal. */
1504
+ function normalizeClaim(claim) {
1505
+ return claim.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
1506
+ }
1507
+ /**
1508
+ * The objections their authors rejected, as disputes for the judge: the same claim
1509
+ * against the same plan is merged, whoever raised it, and nothing else is.
1510
+ * Ranked by how many critics raised it, then claims about the repository,
1511
+ * then the order they were raised; the first `cap` are for the judge to rule on.
1512
+ */
1513
+ function buildDisputes(objections, replies, cap = 8) {
1514
+ const rejected = new Map(replies.filter((reply) => reply.decision === "reject").map((reply) => [reply.id, reply]));
1515
+ const groups = /* @__PURE__ */ new Map();
1516
+ for (const objection of objections) {
1517
+ const reply = rejected.get(objection.id);
1518
+ if (!reply) continue;
1519
+ const key = `${objection.target}\n${normalizeClaim(objection.claim)}`;
1520
+ const group = groups.get(key);
1521
+ if (group) {
1522
+ if (!group.critics.includes(objection.critic)) group.critics.push(objection.critic);
1523
+ group.objections.push(objection.id);
1524
+ if (objection.why) group.reasons.push(objection.why);
1525
+ if (reply.reason) group.rejections.push(reply.reason);
1526
+ group.repo ||= objection.repo;
1527
+ continue;
1528
+ }
1529
+ groups.set(key, {
1530
+ id: "",
1531
+ target: objection.target,
1532
+ critics: [objection.critic],
1533
+ objections: [objection.id],
1534
+ claim: objection.claim,
1535
+ reasons: objection.why ? [objection.why] : [],
1536
+ rejections: reply.reason ? [reply.reason] : [],
1537
+ repo: objection.repo
1538
+ });
1539
+ }
1540
+ const ranked = [...groups.values()].sort((a, b) => b.critics.length - a.critics.length || Number(b.repo) - Number(a.repo)).map((dispute, index) => ({
1541
+ ...dispute,
1542
+ id: `D${String(index + 1)}`
1543
+ }));
1544
+ return {
1545
+ disputes: ranked.slice(0, cap),
1546
+ overflow: ranked.slice(cap)
1547
+ };
1548
+ }
1549
+ /**
1550
+ * Who checks each repository claim: an agent that reads the repository and did
1551
+ * not raise it, the author's peers before the author. Spread across the
1552
+ * checkers in turn; a claim nobody may check is left out.
1553
+ */
1554
+ function assignChecks(disputes, checkers) {
1555
+ const assigned = /* @__PURE__ */ new Map();
1556
+ disputes.forEach((dispute, index) => {
1557
+ const eligible = checkers.filter((checker) => !dispute.critics.includes(checker));
1558
+ const peers = eligible.filter((checker) => checker !== dispute.target);
1559
+ const pool = peers.length > 0 ? peers : eligible;
1560
+ const checker = pool[index % Math.max(1, pool.length)];
1561
+ if (checker === void 0) return;
1562
+ assigned.set(checker, [...assigned.get(checker) ?? [], dispute]);
1563
+ });
1564
+ return assigned;
1565
+ }
1566
+ /** A checker's answer for each id in `ids`; a missing or unparseable answer is `unknown`. */
1567
+ function parseChecks(checker, text, ids) {
1568
+ const checks = /* @__PURE__ */ new Map();
1569
+ for (const line of lines(text)) {
1570
+ const [, id, result, evidence] = CHECK.exec(line) ?? [];
1571
+ if (id === void 0 || result === void 0 || evidence === void 0) continue;
1572
+ const wanted = ids.find((candidate) => candidate.toLowerCase() === id.toLowerCase());
1573
+ if (wanted === void 0 || checks.has(wanted)) continue;
1574
+ checks.set(wanted, {
1575
+ checker,
1576
+ result: result.toLowerCase(),
1577
+ evidence: evidence.trim()
1578
+ });
1579
+ }
1580
+ for (const id of ids) if (!checks.has(id)) checks.set(id, {
1581
+ checker,
1582
+ result: "unknown",
1583
+ evidence: ""
1584
+ });
1585
+ return checks;
1586
+ }
1587
+ /**
1588
+ * The rules every stage shares. How much of the repository to read differs by
1589
+ * stage: only the draft explores it; later stages open a file to settle a point.
1590
+ */
1591
+ function planContract(repository) {
1592
+ return `
1593
+ You are designing an implementation plan, not implementing code. Work read-only.
1594
+ ${repository} Make the plan specific to files and symbols that exist.
1595
+ Call out assumptions and unresolved questions. Prefer a small, verifiable sequence of changes.
1596
+ Include architecture, edge cases, tests, validation, and rollout/compatibility concerns.
1597
+ If the task is a placeholder or too vague to act on, say so and list the clarifying questions instead of inventing scope.
1598
+ Keep the response under 1,500 words. Return Markdown only.`;
1599
+ }
1600
+ const EXPLORE = "Inspect the repository before deciding.";
1601
+ const CHECK_DISPUTES = "You examined the repository while drafting. Open a file only to check a claim on which the plans disagree, or one you are unsure of.";
1602
+ const SETTLE_CONTRADICTIONS = "Work from the plans below. Open a file only to settle a contradiction between them.";
1603
+ /** "Claude", "Claude and GLM", "Claude, GLM and Kimi". */
1604
+ function listLabels(labels) {
1605
+ return labels.join(", ").replace(/, ([^,]*)$/, " and $1");
1606
+ }
1607
+ function planBlocks(plans, tag) {
1608
+ return plans.map(({ label, plan, name }) => `${label}'s plan:\n<${tag} author="${label}"${name === void 0 ? "" : ` agent="${name}"`}>\n${plan}\n</${tag}>`).join("\n\n");
1609
+ }
1610
+ function initialPlanPrompt(task, peers) {
1611
+ return `${planContract(EXPLORE)}
1612
+
1613
+ You are the first planner in a collaboration with ${listLabels(peers)}. Produce your strongest independent plan.
1614
+
1615
+ Task:
1616
+ <task>
1617
+ ${task}
1618
+ </task>`;
1619
+ }
1620
+ /** The task, unless the agent is continuing a conversation that already has it. */
1621
+ function taskBlock(task, resumed) {
1622
+ return resumed ? "" : `Task:\n<task>\n${task}\n</task>\n\n`;
1623
+ }
1624
+ /**
1625
+ * `resumed` is for an agent continuing its own conversation, which already
1626
+ * holds the task and its last plan: both are left out.
1627
+ */
1628
+ function revisionPrompt(input) {
1629
+ const peers = listLabels(input.peerPlans.map(({ label }) => label));
1630
+ const ownPlan = input.resumed ? "" : `Your earlier draft:\n<own-plan>\n${input.ownPlan}\n</own-plan>\n\n`;
1631
+ return `${planContract(CHECK_DISPUTES)}
1632
+
1633
+ You are reviewing peer plans from ${peers}. Compare them with your ${input.resumed ? "last plan in this conversation" : "own draft"}, correct weak assumptions,
1634
+ adopt useful details, and return a revised standalone plan. Do not merely write a critique.
1635
+
1636
+ ${taskBlock(input.task, input.resumed)}${ownPlan}${planBlocks(input.peerPlans, "peer-plan")}${input.feedback === void 0 ? "" : input.targeted ? `\n\nThe debate left these disagreements open. Settle each one in your revised plan, checking the repository where a claim is about it:\n<open-disagreements>\n${input.feedback}\n</open-disagreements>` : `\n\nThe judge identified remaining uncertainty. Use this typed feedback to target the revision:\n<judge-feedback>\n${input.feedback}\n</judge-feedback>`}`;
1637
+ }
1638
+ /** `resumed` is for an agent continuing its own conversation: the task is left out. */
1639
+ function finalPlanPrompt(input) {
1640
+ return `${planContract(SETTLE_CONTRADICTIONS)}
1641
+
1642
+ Act as the final editor. Merge the best concrete parts of all the revised plans, guided by the judge's typed verdict.
1643
+ Resolve contradictions explicitly. Return one self-contained execution plan—no discussion of the planning process,
1644
+ no winner announcement, and no judge commentary.
1645
+
1646
+ ${taskBlock(input.task, input.resumed)}${planBlocks(input.plans, "revised-plan")}
1647
+
1648
+ Judge verdict:
1649
+ <judge-verdict>
1650
+ ${input.verdict}
1651
+ </judge-verdict>${input.disputes === void 0 ? "" : `
1652
+
1653
+ The agents' disagreements, and the judge's ruling on each. Follow a ruling unless the plans show it wrong:
1654
+ <disputes>
1655
+ ${input.disputes}
1656
+ </disputes>`}`;
1657
+ }
1658
+ /** The format a critique answers in, shown to the critic. */
1659
+ const CRITIQUE_FORMAT = `TARGET: <agent>
1660
+ C1 [repo]: <claim about a file, symbol or export> — <why it matters>
1661
+ C2: <claim about the design> — <why it matters>`;
1662
+ /**
1663
+ * Asks an agent for its objections to every peer plan, and nothing else: no
1664
+ * revision yet. `resumed` is for an agent continuing its own conversation,
1665
+ * which already holds the task and its own plan.
1666
+ */
1667
+ function critiquePrompt(input) {
1668
+ const ownPlan = input.resumed ? "" : `Your own draft, for reference:\n<own-plan>\n${input.ownPlan}\n</own-plan>\n\n`;
1669
+ return `${planContract(CHECK_DISPUTES)}
1670
+
1671
+ Do not write a plan this time. Critique the peer plans below: find what is wrong in each, not what
1672
+ is good. For each peer, write a TARGET line with its agent name, then at most ${String(5)} numbered
1673
+ objections, most important first:
1674
+
1675
+ ${CRITIQUE_FORMAT}
1676
+
1677
+ Mark an objection [repo] only when it makes a claim about the repository that someone could check
1678
+ by opening a file. Say why each objection matters after a dash. Raise only objections you would
1679
+ defend; an author will accept or reject each one, and the judge will rule on the rejected ones.
1680
+
1681
+ ${taskBlock(input.task, input.resumed)}${ownPlan}${planBlocks(input.peerPlans, "peer-plan")}`;
1682
+ }
1683
+ /**
1684
+ * Asks an author to accept or reject every objection to its plan, then to
1685
+ * return its revised plan. `resumed` leaves out the task and its own plan.
1686
+ */
1687
+ function replyPrompt(input) {
1688
+ const ownPlan = input.resumed ? "" : `Your plan:\n<own-plan>\n${input.ownPlan}\n</own-plan>\n\n`;
1689
+ const received = input.objections.length === 0 ? "No objections were raised against your plan. Return it, improved where you see fit." : `The other agents raised these objections against your plan:
1690
+ <objections>
1691
+ ${input.objections.map(({ id, criticLabel, claim, why, repo }) => `${id} (${criticLabel}${repo ? ", about the repository" : ""}): ${claim}${why ? ` — ${why}` : ""}`).join("\n")}
1692
+ </objections>
1693
+
1694
+ Answer every objection on its own line, by its id: ACCEPT when it is right, and change your plan to
1695
+ match; REJECT when it is wrong, with the reason. Open a file before rejecting a claim about the
1696
+ repository. Then return your revised plan, complete and standalone.`;
1697
+ return `${planContract(CHECK_DISPUTES)}
1698
+
1699
+ ${received}
1700
+
1701
+ Answer in this format:
1702
+ <replies>
1703
+ <id>: ACCEPT — <what you changed>
1704
+ <id>: REJECT — <why the objection is wrong>
1705
+ </replies>
1706
+ <revised-plan>
1707
+ <your whole revised plan>
1708
+ </revised-plan>
1709
+
1710
+ ${taskBlock(input.task, input.resumed)}${ownPlan}`.trimEnd();
1711
+ }
1712
+ /**
1713
+ * Asks an agent that reads the repository to settle disputed claims about it,
1714
+ * one answer per claim. `resumed` leaves out the task.
1715
+ */
1716
+ function claimCheckPrompt(input) {
1717
+ return `You are checking disputed claims about this repository, not planning. Work read-only.
1718
+ Open the files each claim is about and answer on one line per claim, by its id:
1719
+
1720
+ D1: CONFIRM — <file:symbol that shows the claim is true>
1721
+ D2: REFUTE — <file:symbol that shows it is false>
1722
+ D3: UNKNOWN — <what you looked at>
1723
+
1724
+ Answer UNKNOWN whenever the files do not settle it. Return nothing else.
1725
+
1726
+ ${taskBlock(input.task, input.resumed)}<claims>
1727
+ ${input.claims.map(({ id, claim, criticLabels, authorLabel, rejections }) => `${id}: ${claim}\n Raised by ${listLabels(criticLabels)} against ${authorLabel}'s plan.${rejections.length > 0 ? ` ${authorLabel} rejected it: ${rejections.join(" / ")}` : ""}`).join("\n")}
1728
+ </claims>`;
1729
+ }
1730
+ const RULING_TEXT = {
1731
+ critic: (critics) => `${critics}'s objection holds`,
1732
+ author: (_critics, author) => `${author}'s position holds`,
1733
+ unclear: () => "the material does not settle it"
1734
+ };
1735
+ /** Whether a dispute is settled: the judge ruled for a side, and with at least `SETTLED` confidence. */
1736
+ function isSettled(ruling) {
1737
+ return ruling !== void 0 && ruling.choice !== "unclear" && ruling.confidence >= .65;
1738
+ }
1739
+ /** `D1 (Codex → Claude): <claim>. Judge: Claude's position holds (0.72). Check: REFUTE src/jev.ts.` */
1740
+ function disputeLine(dispute, ruling, label) {
1741
+ const critics = listLabels(dispute.critics.map(label));
1742
+ const author = label(dispute.target);
1743
+ const parts = [`${dispute.id} (${critics} → ${author}): ${dispute.claim.replace(/\.$/, "")}.`];
1744
+ parts.push(ruling ? `Judge: ${RULING_TEXT[ruling.choice](critics, author)} (${ruling.confidence.toFixed(2)}).` : "Judge: not judged.");
1745
+ if (dispute.check) {
1746
+ const evidence = dispute.check.evidence ? ` ${dispute.check.evidence.replace(/\.$/, "")}` : "";
1747
+ parts.push(`Check: ${dispute.check.result.toUpperCase()}${evidence}.`);
1748
+ }
1749
+ return parts.join(" ");
1750
+ }
1751
+ /** Every dispute and the judge's ruling on it, one per line, for the final merge. */
1752
+ function disputeSummary(input) {
1753
+ const ruling = (id) => input.rulings.find((candidate) => candidate.id === id);
1754
+ return [...input.disputes, ...input.overflow].map((dispute) => disputeLine(dispute, ruling(dispute.id), input.label)).join("\n");
1755
+ }
1756
+ const SCORE_NAMES = {
1757
+ completeness: "completeness",
1758
+ feasibility: "feasibility and grounding in the repository",
1759
+ riskCoverage: "coverage of edge cases, tests and rollout risks"
1760
+ };
1761
+ /**
1762
+ * What a second pass after a debate aims at: the disputes the judge's rulings left
1763
+ * open, and those past the cap that it never judged. With none open, a note
1764
+ * saying so and the weakest of the judge's scores, since the judge asking for another pass
1765
+ * can also mean something is missing that nobody disputed.
1766
+ */
1767
+ function disputeFeedback(input) {
1768
+ const rulings = input.verdict.disputes ?? [];
1769
+ const ruling = (id) => rulings.find((candidate) => candidate.id === id);
1770
+ const lines = [...input.disputes.filter((dispute) => !isSettled(ruling(dispute.id))).map((dispute) => disputeLine(dispute, ruling(dispute.id), input.label)), ...input.overflow.map((dispute) => disputeLine(dispute, void 0, input.label))];
1771
+ if (lines.length > 0) return lines.join("\n");
1772
+ const name = Object.keys(SCORE_NAMES).reduce((weakest, next) => input.verdict[next] < input.verdict[weakest] ? next : weakest);
1773
+ return `No disagreement is left open, but the judge still expects another pass to improve the plan. Its weakest score is ${SCORE_NAMES[name]}: ${input.verdict[name].toFixed(1)} of 3. Strengthen that.`;
1774
+ }
1775
+ /** A round never returns fewer plans than this: below it, there is no collaboration left to judge. */
1776
+ const MIN_PLANS = 2;
1777
+ /** Each draft's plan, keyed by its agent's name: how a round is reported. */
1778
+ const byName = (drafts) => Object.fromEntries(drafts.map(({ agent, plan }) => [agent.name, plan]));
1779
+ const pendingOf = (calls) => calls.map((call) => ({
1780
+ call,
1781
+ controller: new AbortController()
1782
+ }));
1783
+ /** Half of the round's calls, and at least one: once they are in, the grace starts. */
1784
+ const quorumOf = (pending) => Math.max(1, Math.ceil(pending.length / 2));
1785
+ const asError = (error) => error instanceof Error ? error : new Error(String(error));
1786
+ /**
1787
+ * Every agent's plan for one round, in parallel.
1788
+ *
1789
+ * With a grace, the round stops waiting `graceMs` after half of the agents
1790
+ * have answered and aborts the rest, so one slow agent no longer sets the pace
1791
+ * of the round. An aborted agent keeps its plan from the round before, and an
1792
+ * agent whose plan the round cannot do without — a draft nobody can stand in
1793
+ * for, when dropping it would leave fewer than two plans — is waited for
1794
+ * anyway. Rejections are the caller's, as `Promise.all` would raise them,
1795
+ * except from a call this round aborted itself.
1796
+ */
1797
+ function round(calls, generate, graceMs, onDrop) {
1798
+ if (graceMs <= 0) return Promise.all(calls.map(async ({ agent, prompt, later }) => ({
1799
+ agent,
1800
+ plan: await generate(agent, prompt, later)
1801
+ })));
1802
+ return new Promise((resolve, reject) => {
1803
+ const pending = pendingOf(calls);
1804
+ const quorum = quorumOf(pending);
1805
+ let answered = 0;
1806
+ let timer;
1807
+ let settled = false;
1808
+ const finish = (callback) => {
1809
+ if (settled) return;
1810
+ settled = true;
1811
+ if (timer !== void 0) clearTimeout(timer);
1812
+ callback();
1813
+ };
1814
+ const settleIfDone = () => {
1815
+ if (pending.some((entry) => entry.plan === void 0 && entry.dropped === void 0)) return;
1816
+ finish(() => {
1817
+ resolve(pending.flatMap(({ call, plan }) => {
1818
+ const answer = plan ?? call.fallback;
1819
+ return answer === void 0 ? [] : [{
1820
+ agent: call.agent,
1821
+ plan: answer
1822
+ }];
1823
+ }));
1824
+ });
1825
+ };
1826
+ const cutOff = () => {
1827
+ let remaining = pending.length;
1828
+ for (const entry of pending) {
1829
+ if (entry.plan !== void 0 || entry.dropped !== void 0) continue;
1830
+ if (entry.call.fallback === void 0) {
1831
+ if (remaining - 1 < MIN_PLANS) continue;
1832
+ remaining -= 1;
1833
+ }
1834
+ entry.dropped = true;
1835
+ entry.controller.abort();
1836
+ onDrop(entry.call.agent);
1837
+ }
1838
+ settleIfDone();
1839
+ };
1840
+ for (const entry of pending) generate(entry.call.agent, entry.call.prompt, entry.call.later, entry.controller.signal).then((plan) => {
1841
+ if (settled || entry.dropped !== void 0) return;
1842
+ entry.plan = plan;
1843
+ answered += 1;
1844
+ if (timer === void 0 && answered >= quorum) timer = setTimeout(cutOff, graceMs);
1845
+ settleIfDone();
1846
+ }, (error) => {
1847
+ if (entry.dropped !== void 0) return;
1848
+ finish(() => {
1849
+ reject(asError(error));
1850
+ });
1851
+ });
1852
+ });
1853
+ }
1854
+ /**
1855
+ * The draft round of `fast` mode: every agent drafts at once, and each draft
1856
+ * is handed to `judgeSolo` as it arrives, one at a time, in arrival order.
1857
+ * The first one whose verdict stands alone is accepted: the calls still
1858
+ * running are aborted and reported to `onDrop` with it, and the round
1859
+ * resolves with every draft that arrived, those still waiting to be judged
1860
+ * included. With none accepted, it resolves once every call has answered
1861
+ * and every draft has been judged.
1862
+ *
1863
+ * The grace works as in `round`: once half the agents have answered, the
1864
+ * rest get `graceMs`, and the round never cuts below two drafts. It is kept
1865
+ * apart from `round` so that `balanced` and `ultra` run exactly the code
1866
+ * they always have. An agent or the judge failing before a draft is accepted
1867
+ * aborts the calls still running and rejects the round.
1868
+ */
1869
+ function firstAccepted(calls, generate, graceMs, onDrop, judgeSolo) {
1870
+ return new Promise((resolve, reject) => {
1871
+ const pending = pendingOf(calls);
1872
+ const quorum = quorumOf(pending);
1873
+ const queue = [];
1874
+ let answered = 0;
1875
+ let judging = false;
1876
+ let timer;
1877
+ let settled = false;
1878
+ const running = () => pending.filter((entry) => entry.plan === void 0 && entry.dropped === void 0);
1879
+ const finish = (callback) => {
1880
+ if (settled) return;
1881
+ settled = true;
1882
+ if (timer !== void 0) clearTimeout(timer);
1883
+ callback();
1884
+ };
1885
+ const resolveWith = (accepted) => {
1886
+ finish(() => {
1887
+ resolve({
1888
+ drafts: pending.flatMap(({ call, plan }) => plan === void 0 ? [] : [{
1889
+ agent: call.agent,
1890
+ plan
1891
+ }]),
1892
+ ...accepted ? { accepted } : {}
1893
+ });
1894
+ });
1895
+ };
1896
+ const fail = (error) => {
1897
+ for (const entry of running()) {
1898
+ entry.dropped = true;
1899
+ entry.controller.abort();
1900
+ }
1901
+ finish(() => {
1902
+ reject(asError(error));
1903
+ });
1904
+ };
1905
+ const judgeNext = () => {
1906
+ if (settled || judging) return;
1907
+ const draft = queue.shift();
1908
+ if (!draft) {
1909
+ if (running().length === 0) resolveWith();
1910
+ return;
1911
+ }
1912
+ judging = true;
1913
+ judgeSolo(draft).then((verdict) => {
1914
+ judging = false;
1915
+ if (settled) return;
1916
+ if (verdict.standsAloneProbability < .5) {
1917
+ judgeNext();
1918
+ return;
1919
+ }
1920
+ for (const entry of running()) {
1921
+ entry.dropped = true;
1922
+ entry.controller.abort();
1923
+ onDrop(entry.call.agent, draft);
1924
+ }
1925
+ resolveWith({
1926
+ draft,
1927
+ verdict
1928
+ });
1929
+ }, fail);
1930
+ };
1931
+ const cutOff = () => {
1932
+ let remaining = pending.length;
1933
+ for (const entry of running()) {
1934
+ if (remaining - 1 < MIN_PLANS) continue;
1935
+ remaining -= 1;
1936
+ entry.dropped = true;
1937
+ entry.controller.abort();
1938
+ onDrop(entry.call.agent);
1939
+ }
1940
+ judgeNext();
1941
+ };
1942
+ for (const entry of pending) {
1943
+ const { agent, prompt, later } = entry.call;
1944
+ generate(agent, prompt, later, entry.controller.signal).then((plan) => {
1945
+ if (settled || entry.dropped !== void 0) return;
1946
+ entry.plan = plan;
1947
+ answered += 1;
1948
+ if (graceMs > 0 && timer === void 0 && answered >= quorum) timer = setTimeout(cutOff, graceMs);
1949
+ queue.push({
1950
+ agent,
1951
+ plan
1952
+ });
1953
+ judgeNext();
1954
+ }, (error) => {
1955
+ if (entry.dropped !== void 0) return;
1956
+ fail(error);
1957
+ });
1958
+ }
1959
+ });
1960
+ }
1961
+ /**
1962
+ * The debate review: each agent critiques the others, each author answers
1963
+ * the objections to its plan and revises it, the rejected objections become
1964
+ * disputes, and the judge rules on them along with its usual verdict.
1965
+ */
1966
+ async function debateReview(run, drafts) {
1967
+ const { task } = run.options;
1968
+ const peersOf = (own) => drafts.filter((draft) => draft !== own);
1969
+ run.stage(`Collecting critiques of the ${String(drafts.length)} drafts…`);
1970
+ const critiques = await run.runRound(drafts.map((own) => {
1971
+ const input = {
1972
+ task,
1973
+ ownPlan: own.plan,
1974
+ peerPlans: peersOf(own).map(({ agent, plan }) => ({
1975
+ name: agent.name,
1976
+ label: agent.label,
1977
+ plan
1978
+ }))
1979
+ };
1980
+ return {
1981
+ agent: own.agent,
1982
+ fallback: "",
1983
+ prompt: critiquePrompt(input),
1984
+ later: run.later((resumed) => critiquePrompt({
1985
+ ...input,
1986
+ resumed
1987
+ }))
1988
+ };
1989
+ }));
1990
+ const objections = critiques.flatMap(({ agent, plan }) => parseCritique(agent.name, plan, drafts.filter((draft) => draft.agent !== agent).map((draft) => draft.agent)).objections);
1991
+ await run.report("critique", byName(drafts), {
1992
+ artifacts: byName(critiques),
1993
+ debate: { objections }
1994
+ });
1995
+ run.stage("Asking each author to answer the objections to its plan…");
1996
+ const received = (own) => objections.filter((objection) => objection.target === own.agent.name).map((objection) => ({
1997
+ ...objection,
1998
+ criticLabel: run.label(objection.critic)
1999
+ }));
2000
+ const answers = await run.runRound(drafts.map((own) => {
2001
+ const input = {
2002
+ task,
2003
+ ownPlan: own.plan,
2004
+ objections: received(own)
2005
+ };
2006
+ return {
2007
+ agent: own.agent,
2008
+ fallback: "",
2009
+ prompt: replyPrompt(input),
2010
+ later: run.later((resumed) => replyPrompt({
2011
+ ...input,
2012
+ resumed
2013
+ }))
2014
+ };
2015
+ }));
2016
+ const replies = [];
2017
+ const revised = drafts.map((own) => {
2018
+ const answer = answers.find(({ agent }) => agent === own.agent)?.plan ?? "";
2019
+ const ids = received(own).map(({ id }) => id);
2020
+ const parsed = parseReply(own.agent.name, answer, ids, own.plan);
2021
+ replies.push(...parsed.replies);
2022
+ return {
2023
+ agent: own.agent,
2024
+ plan: parsed.plan
2025
+ };
2026
+ });
2027
+ const unanswered = objections.map(({ id }) => id).filter((id) => !replies.some((reply) => reply.id === id));
2028
+ const built = buildDisputes(objections, replies);
2029
+ let { disputes } = built;
2030
+ const { overflow } = built;
2031
+ let claimChecks;
2032
+ const record = () => ({
2033
+ objections,
2034
+ replies,
2035
+ unanswered,
2036
+ disputes,
2037
+ overflow,
2038
+ ...claimChecks ? { claimChecks } : {}
2039
+ });
2040
+ let checks;
2041
+ if (run.options.claimChecks) {
2042
+ const checkers = revised.filter(({ agent }) => agent.readsRepository === true).map(({ agent }) => agent.name);
2043
+ const assigned = assignChecks(disputes.filter(({ repo }) => repo), checkers);
2044
+ if (checkers.length < 2) {
2045
+ claimChecks = "skipped";
2046
+ run.stage("Claim checks skipped: needs two agents that read the repository");
2047
+ } else if (assigned.size === 0) {
2048
+ claimChecks = "skipped";
2049
+ run.stage("Claim checks skipped: no disputed claim about the repository");
2050
+ } else {
2051
+ await run.report("reply", byName(revised), {
2052
+ artifacts: byName(answers),
2053
+ debate: record()
2054
+ });
2055
+ run.stage(`Checking ${String([...assigned.values()].flat().length)} disputed claims against the repository…`);
2056
+ const toCheck = [...assigned].map(([checker, claims]) => ({
2057
+ agent: run.agent(checker),
2058
+ claims,
2059
+ input: {
2060
+ task,
2061
+ claims: claims.map((dispute) => ({
2062
+ id: dispute.id,
2063
+ claim: dispute.claim,
2064
+ criticLabels: dispute.critics.map((critic) => run.label(critic)),
2065
+ authorLabel: run.label(dispute.target),
2066
+ rejections: dispute.rejections
2067
+ }))
2068
+ }
2069
+ }));
2070
+ checks = await run.runRound(toCheck.map(({ agent, input }) => ({
2071
+ agent,
2072
+ fallback: "",
2073
+ prompt: claimCheckPrompt(input),
2074
+ later: run.later((resumed) => claimCheckPrompt({
2075
+ ...input,
2076
+ resumed
2077
+ }))
2078
+ })));
2079
+ const results = new Map(toCheck.flatMap(({ agent, claims }) => [...parseChecks(agent.name, checks?.find((check) => check.agent === agent)?.plan ?? "", claims.map(({ id }) => id))]));
2080
+ disputes = disputes.map((dispute) => {
2081
+ const check = results.get(dispute.id);
2082
+ return check ? {
2083
+ ...dispute,
2084
+ check
2085
+ } : dispute;
2086
+ });
2087
+ claimChecks = "ran";
2088
+ }
2089
+ }
2090
+ run.stage(disputes.length > 0 ? `Re-evaluating the revised plans and ${String(disputes.length)} disagreements with ${run.judgeName}…` : `Re-evaluating the revised plans with ${run.judgeName}…`);
2091
+ const verdict = await run.judge(revised, "review", disputes);
2092
+ await run.report(checks ? "check" : "review", byName(revised), {
2093
+ verdict,
2094
+ artifacts: byName(checks ?? answers),
2095
+ debate: record()
2096
+ });
2097
+ return {
2098
+ drafts: revised,
2099
+ verdict,
2100
+ record: record(),
2101
+ disputes,
2102
+ overflow
2103
+ };
2104
+ }
2105
+ /** The agent of that name, or an error naming the agents there are. */
2106
+ function findAgent(agents, name) {
2107
+ const agent = agents.find((candidate) => candidate.name === name);
2108
+ if (!agent) throw new Error(`"${name}" is not one of this planner's agents: ${agents.map((a) => a.name).join(", ")}`);
2109
+ return agent;
2110
+ }
2111
+ /**
2112
+ * One call of `Planner.plan`: what it has spent, how long each round took,
2113
+ * each agent's session, and the calls every phase of the run makes through it.
2114
+ */
2115
+ var PlanRun = class {
2116
+ agents;
2117
+ planJudge;
2118
+ options;
2119
+ settings;
2120
+ cost;
2121
+ timings = {
2122
+ totalMs: 0,
2123
+ rounds: []
2124
+ };
2125
+ /** Whether each agent continues one conversation through the run. */
2126
+ resume;
2127
+ stage;
2128
+ runStart = performance.now();
2129
+ roundStart = this.runStart;
2130
+ agentMs = {};
2131
+ judgeMs;
2132
+ rounds = 0;
2133
+ sessions;
2134
+ constructor(agents, planJudge, options, settings) {
2135
+ this.agents = agents;
2136
+ this.planJudge = planJudge;
2137
+ this.options = options;
2138
+ this.settings = settings;
2139
+ this.cost = {
2140
+ mode: settings.mode,
2141
+ reviewMode: settings.reviewMode,
2142
+ reviewRounds: 0,
2143
+ synthesized: false,
2144
+ agentCalls: 0,
2145
+ judgeCalls: 0,
2146
+ dropped: []
2147
+ };
2148
+ this.stage = options.onStage ?? (() => void 0);
2149
+ this.resume = options.resume ?? true;
2150
+ this.sessions = new Map(this.resume ? agents.map((agent) => [agent, {}]) : []);
2151
+ }
2152
+ get judgeName() {
2153
+ return this.planJudge.name;
2154
+ }
2155
+ agent(name) {
2156
+ return findAgent(this.agents, name);
2157
+ }
2158
+ label(name) {
2159
+ return this.agent(name).label;
2160
+ }
2161
+ /** The later-call marker, with the shorter prompt when the agent continues its session. */
2162
+ later(prompt) {
2163
+ return { resumePrompt: this.resume ? prompt(true) : void 0 };
2164
+ }
2165
+ /** Ends the round: records its timings, hands it to `onRound`, and starts the next one's clock. */
2166
+ async report(stageName, plans, extra = {}) {
2167
+ this.rounds += 1;
2168
+ const roundTimings = {
2169
+ totalMs: performance.now() - this.roundStart,
2170
+ agents: this.agentMs,
2171
+ ...this.judgeMs === void 0 ? {} : { judgeMs: this.judgeMs }
2172
+ };
2173
+ this.timings.rounds.push({
2174
+ round: this.rounds,
2175
+ stage: stageName,
2176
+ ...roundTimings
2177
+ });
2178
+ await this.options.onRound?.({
2179
+ round: this.rounds,
2180
+ stage: stageName,
2181
+ plans,
2182
+ ...extra.verdict ? { verdict: extra.verdict } : {},
2183
+ timings: roundTimings,
2184
+ ...extra.selected ? { selected: extra.selected } : {},
2185
+ ...extra.artifacts ? { artifacts: extra.artifacts } : {},
2186
+ ...extra.debate ? { debate: extra.debate } : {}
2187
+ });
2188
+ this.roundStart = performance.now();
2189
+ this.agentMs = {};
2190
+ this.judgeMs = void 0;
2191
+ }
2192
+ /** Stops the run's clock. */
2193
+ finish() {
2194
+ this.timings.totalMs = performance.now() - this.runStart;
2195
+ }
2196
+ generate = (agent, prompt, later, signal) => {
2197
+ this.cost.agentCalls += 1;
2198
+ return this.timed(() => agent.generate(this.request(agent, prompt, later, signal)), (ms) => {
2199
+ if (signal?.aborted !== true) this.agentMs[agent.name] = ms;
2200
+ });
2201
+ };
2202
+ runRound(calls) {
2203
+ return round(calls, this.generate, this.settings.graceMs, (agent) => {
2204
+ this.cost.dropped.push(agent.name);
2205
+ this.stage(`${agent.label} is still working; the round goes on without it…`);
2206
+ });
2207
+ }
2208
+ /** A cross-review: each agent revises its own plan against every other one. */
2209
+ revise(drafts, feedback, targeted) {
2210
+ return this.runRound(drafts.map((own) => {
2211
+ const input = {
2212
+ task: this.options.task,
2213
+ ownPlan: own.plan,
2214
+ peerPlans: drafts.filter((draft) => draft !== own).map((draft) => ({
2215
+ label: draft.agent.label,
2216
+ plan: draft.plan
2217
+ })),
2218
+ ...feedback ? { feedback } : {},
2219
+ ...targeted ? { targeted } : {}
2220
+ };
2221
+ return {
2222
+ agent: own.agent,
2223
+ fallback: own.plan,
2224
+ prompt: revisionPrompt(input),
2225
+ later: this.later((resumed) => revisionPrompt({
2226
+ ...input,
2227
+ resumed
2228
+ }))
2229
+ };
2230
+ }));
2231
+ }
2232
+ judge(drafts, judged, disputes = []) {
2233
+ this.cost.judgeCalls += 1;
2234
+ return this.timed(() => this.planJudge.judge({
2235
+ task: this.options.task,
2236
+ stage: judged,
2237
+ plans: drafts.map(({ agent, plan }) => ({
2238
+ agent: agent.name,
2239
+ label: agent.label,
2240
+ plan
2241
+ })),
2242
+ ...disputes.length > 0 ? { disputes } : {},
2243
+ ...this.options.judgeModel ? { model: this.options.judgeModel } : {}
2244
+ }), (ms) => {
2245
+ this.judgeMs = (this.judgeMs ?? 0) + ms;
2246
+ });
2247
+ }
2248
+ request(agent, prompt, later, signal) {
2249
+ const session = this.sessions.get(agent);
2250
+ const effort = later ? this.options.reviewEfforts?.[agent.name] : void 0;
2251
+ const { onAgentProgress } = this.options;
2252
+ return {
2253
+ prompt,
2254
+ ...session ? {
2255
+ session,
2256
+ ...later?.resumePrompt ? { resumePrompt: later.resumePrompt } : {}
2257
+ } : {},
2258
+ ...effort === void 0 ? {} : { effort },
2259
+ cwd: this.options.cwd,
2260
+ timeoutMs: this.options.timeoutMs,
2261
+ ...onAgentProgress ? { onProgress: (line) => {
2262
+ onAgentProgress(agent.name, line);
2263
+ } } : {},
2264
+ ...signal ? { signal } : {}
2265
+ };
2266
+ }
2267
+ async timed(work, record) {
2268
+ const start = performance.now();
2269
+ const result = await work();
2270
+ record(performance.now() - start);
2271
+ return result;
2272
+ }
2273
+ };
2274
+ /** Above this, the judge is saying the strongest plan is final as it stands: no merge needed. */
2275
+ const STANDS_ALONE = .7;
2276
+ /** The plan the judge called stronger, or the one it routed the merge to when it called them tied. */
2277
+ const strongest = (drafts, verdict) => drafts.find(({ agent }) => agent.name === verdict.strongerPlan) ?? drafts.find(({ agent }) => agent.name === verdict.finalizer);
2278
+ /**
2279
+ * The end of a run: adopt one plan as it stands when the run allows it, or
2280
+ * have the finalizer merge them all into one.
2281
+ */
2282
+ async function synthesize(run, { drafts, verdict, accepted, debated, finalizerOverride }) {
2283
+ const { options, cost } = run;
2284
+ const debateRecord = debated ? { debate: debated.record } : {};
2285
+ const reviewed = cost.reviewRounds > 0;
2286
+ const selected = reviewed && options.selectStronger ? drafts.find(({ agent }) => agent.name === verdict.strongerPlan) : void 0;
2287
+ const standsAlone = reviewed && cost.mode === "balanced" && finalizerOverride === void 0 && verdict.standsAloneProbability >= STANDS_ALONE;
2288
+ const adopted = accepted?.draft ?? selected ?? (standsAlone ? strongest(drafts, verdict) : void 0);
2289
+ if (adopted) {
2290
+ run.stage(adopted === selected ? `${run.judgeName} rated ${adopted.agent.label}'s plan stronger; using it without a synthesis…` : `Adopting ${adopted.agent.label}'s plan: ${run.judgeName} judged it final as it stands…`);
2291
+ await run.report("final", { [adopted.agent.name]: adopted.plan }, {
2292
+ verdict,
2293
+ selected: true
2294
+ });
2295
+ run.finish();
2296
+ return {
2297
+ plan: adopted.plan,
2298
+ verdict,
2299
+ finalizer: adopted.agent.name,
2300
+ selected: true,
2301
+ drafts: byName(drafts),
2302
+ ...debateRecord,
2303
+ timings: run.timings,
2304
+ cost
2305
+ };
2306
+ }
2307
+ const finalizer = finalizerOverride ?? run.agent(verdict.finalizer);
2308
+ run.stage(`Synthesizing the final plan with ${finalizer.label}…`);
2309
+ const summary = debated && debated.disputes.length + debated.overflow.length > 0 ? disputeSummary({
2310
+ disputes: debated.disputes,
2311
+ overflow: debated.overflow,
2312
+ rulings: debated.verdict.disputes ?? [],
2313
+ label: (name) => run.label(name)
2314
+ }) : void 0;
2315
+ const finalInput = {
2316
+ task: options.task,
2317
+ plans: drafts.map(({ agent, plan }) => ({
2318
+ label: agent.label,
2319
+ plan
2320
+ })),
2321
+ verdict: JSON.stringify(verdict, null, 2),
2322
+ ...summary === void 0 ? {} : { disputes: summary }
2323
+ };
2324
+ const finalPlan = await run.generate(finalizer, finalPlanPrompt(finalInput), run.later((resumed) => finalPlanPrompt({
2325
+ ...finalInput,
2326
+ resumed
2327
+ })));
2328
+ cost.synthesized = true;
2329
+ await run.report("final", { [finalizer.name]: finalPlan }, { verdict });
2330
+ run.finish();
2331
+ return {
2332
+ plan: finalPlan,
2333
+ verdict,
2334
+ finalizer: finalizer.name,
2335
+ drafts: byName(drafts),
2336
+ ...debateRecord,
2337
+ timings: run.timings,
2338
+ cost
2339
+ };
2340
+ }
2341
+ /** Above this, the judge is asking for a cross-review pass rather than merely allowing one. */
2342
+ const NEEDS_ANOTHER_PASS = .65;
2343
+ /** What a `balanced` round waits for a straggler once enough agents have answered. */
2344
+ const DEFAULT_STRAGGLER_GRACE_MS = 9e4;
2345
+ const labelsOf = (agents) => agents.map(({ label }) => label);
2346
+ var Planner = class {
2347
+ judge;
2348
+ agents;
2349
+ /** Two or more agents with distinct names; each drafts, revises, and may finalize. */
2350
+ constructor(agents, judge) {
2351
+ this.judge = judge;
2352
+ if (agents.length < 2) throw new Error("A planner needs at least two agents");
2353
+ const names = agents.map(({ name }) => name);
2354
+ const duplicate = names.find((name, index) => names.indexOf(name) !== index);
2355
+ if (duplicate !== void 0) throw new Error(`Agent "${duplicate}" is listed more than once`);
2356
+ this.agents = [...agents];
2357
+ }
2358
+ async plan(options) {
2359
+ if (!options.allowAnyTask) validateTask(options.task);
2360
+ const finalizerOverride = options.finalizer === void 0 ? void 0 : findAgent(this.agents, options.finalizer);
2361
+ const mode = options.mode ?? "balanced";
2362
+ const maxReviewRounds = options.maxReviewRounds ?? 2;
2363
+ const reviewMode = options.reviewMode ?? (options.claimChecks ? "debate" : "standard");
2364
+ if (options.claimChecks && reviewMode !== "debate") throw new Error("Claim checks run in the debate review: set reviewMode to 'debate'");
2365
+ if (mode === "fast" && reviewMode === "debate") throw new Error("The debate review is a review round, and fast mode has none");
2366
+ if (reviewMode === "debate" && maxReviewRounds < 1) throw new Error("The debate review is a review round: maxReviewRounds must be at least 1");
2367
+ const graceMs = mode === "ultra" ? 0 : options.stragglerGraceMs ?? 9e4;
2368
+ const run = new PlanRun(this.agents, this.judge, options, {
2369
+ mode,
2370
+ reviewMode,
2371
+ graceMs
2372
+ });
2373
+ const { cost, stage } = run;
2374
+ const judgeName = this.judge.name;
2375
+ const crossReview = (count) => `Cross-reviewing the ${String(count)} drafts…`;
2376
+ let debated;
2377
+ const draftCalls = this.agents.map((agent) => ({
2378
+ agent,
2379
+ prompt: initialPlanPrompt(options.task, labelsOf(this.agents.filter((peer) => peer !== agent))),
2380
+ later: void 0
2381
+ }));
2382
+ stage(`Drafting independent plans with ${listLabels(labelsOf(this.agents))}…`);
2383
+ let drafts;
2384
+ let verdict;
2385
+ let accepted;
2386
+ if (mode === "fast") {
2387
+ ({drafts, accepted} = await firstAccepted(draftCalls, run.generate, graceMs, (agent, winner) => {
2388
+ cost.dropped.push(agent.name);
2389
+ stage(winner ? `${judgeName} accepted ${winner.agent.label}'s draft; stopping ${agent.label}…` : `${agent.label} is still working; the round goes on without it…`);
2390
+ }, async (draft) => {
2391
+ stage(`${judgeName} is judging ${draft.agent.label}'s draft alone…`);
2392
+ const solo = await run.judge([draft], "solo");
2393
+ if (solo.standsAloneProbability < .5) stage(`${judgeName} judged ${draft.agent.label}'s draft not final on its own (${solo.standsAloneProbability.toFixed(2)})…`);
2394
+ return solo;
2395
+ }));
2396
+ if (accepted) verdict = accepted.verdict;
2397
+ else {
2398
+ stage(`Asking ${judgeName} for typed quality and routing decisions…`);
2399
+ verdict = await run.judge(drafts, "draft");
2400
+ }
2401
+ await run.report("draft", byName(drafts), { verdict });
2402
+ } else {
2403
+ drafts = await run.runRound(draftCalls);
2404
+ if (mode === "ultra" && maxReviewRounds > 0) {
2405
+ await run.report("draft", byName(drafts));
2406
+ if (reviewMode === "debate") {
2407
+ debated = await debateReview(run, drafts);
2408
+ ({drafts, verdict} = debated);
2409
+ } else {
2410
+ stage(crossReview(drafts.length));
2411
+ drafts = await run.revise(drafts);
2412
+ stage(`Asking ${judgeName} for typed quality and routing decisions…`);
2413
+ verdict = await run.judge(drafts, "review");
2414
+ await run.report("review", byName(drafts), { verdict });
2415
+ }
2416
+ cost.reviewRounds = 1;
2417
+ } else {
2418
+ stage(`Asking ${judgeName} for typed quality and routing decisions…`);
2419
+ verdict = await run.judge(drafts, "draft");
2420
+ await run.report("draft", byName(drafts), { verdict });
2421
+ }
2422
+ }
2423
+ while (mode !== "fast" && cost.reviewRounds < maxReviewRounds && verdict.needsAnotherPassProbability >= NEEDS_ANOTHER_PASS) {
2424
+ if (reviewMode === "debate" && debated === void 0) {
2425
+ debated = await debateReview(run, drafts);
2426
+ ({drafts, verdict} = debated);
2427
+ cost.reviewRounds += 1;
2428
+ continue;
2429
+ }
2430
+ if (debated) {
2431
+ const rulings = debated.verdict.disputes;
2432
+ stage(`${judgeName} requested another pass on the open disagreements…`);
2433
+ drafts = await run.revise(drafts, disputeFeedback({
2434
+ disputes: debated.disputes,
2435
+ overflow: debated.overflow,
2436
+ verdict: rulings ? {
2437
+ ...verdict,
2438
+ disputes: rulings
2439
+ } : verdict,
2440
+ label: (name) => run.label(name)
2441
+ }), true);
2442
+ } else {
2443
+ stage(cost.reviewRounds === 0 ? crossReview(drafts.length) : `${judgeName} requested another cross-review pass…`);
2444
+ drafts = await run.revise(drafts, JSON.stringify(verdict, null, 2));
2445
+ }
2446
+ cost.reviewRounds += 1;
2447
+ stage(`Re-evaluating the revised plans with ${judgeName}…`);
2448
+ verdict = await run.judge(drafts, "review");
2449
+ await run.report("review", byName(drafts), { verdict });
2450
+ }
2451
+ return synthesize(run, {
2452
+ drafts,
2453
+ verdict,
2454
+ ...accepted ? { accepted } : {},
2455
+ ...debated ? { debated } : {},
2456
+ ...finalizerOverride ? { finalizerOverride } : {}
2457
+ });
2458
+ }
2459
+ };
2460
+ async function readPipedStdin() {
2461
+ if (process.stdin.isTTY) return void 0;
2462
+ const chunks = [];
2463
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2464
+ return Buffer.concat(chunks).toString("utf8").trim();
2465
+ }
2466
+ /**
2467
+ * `main`'s dependencies in a real process: its stdio, environment and working
2468
+ * directory, the real agents, and a judge from `judge` for each run. What a
2469
+ * program's `bin` hands `main`.
2470
+ *
2471
+ * Every provider's credentials, and the judge's, are kept from every agent
2472
+ * subprocess: an agent only ever sees the login it uses itself.
2473
+ */
2474
+ function processDeps(program, judge) {
2475
+ const omitEnv = secretEnv(program);
2476
+ return {
2477
+ stdout: (text) => process.stdout.write(text),
2478
+ stderr: (text) => process.stderr.write(text),
2479
+ env: process.env,
2480
+ cwd: () => process.cwd(),
2481
+ readStdin: readPipedStdin,
2482
+ createPlanner: (setup) => new Planner(createAgents(setup, process.env, omitEnv), judge()),
2483
+ doctor: runDoctor,
2484
+ now: () => /* @__PURE__ */ new Date(),
2485
+ program
2486
+ };
2487
+ }
2488
+ const MAX_PLAN_CHARS = 4e4;
2489
+ /** A plan cut to what a judge is shown. */
2490
+ function bounded(text) {
2491
+ if (text.length <= MAX_PLAN_CHARS) return text;
2492
+ return `${text.slice(0, MAX_PLAN_CHARS)}\n[truncated for evaluation]`;
2493
+ }
2494
+ /** What a judge is told the plans are. */
2495
+ const STAGE_TEXT = {
2496
+ solo: "One agent's independent draft, judged alone: no other agent has seen it",
2497
+ draft: "Independent drafts: no agent has seen another agent’s plan yet",
2498
+ review: "Cross-reviewed plans: each agent has read every other plan and revised its own"
2499
+ };
2500
+ const disputeKey = (index) => `dispute_${String(index + 1)}`;
2501
+ function agentOptions(plans, describe) {
2502
+ return Object.fromEntries(plans.map(({ agent, label }) => [agent, describe(label)]));
2503
+ }
2504
+ function labelOf(plans, agent) {
2505
+ return plans.find((plan) => plan.agent === agent)?.label ?? agent;
2506
+ }
2507
+ function disputeQuestion(plans, dispute) {
2508
+ const critics = dispute.critics.map((critic) => labelOf(plans, critic)).join(" and ");
2509
+ const author = labelOf(plans, dispute.target);
2510
+ return {
2511
+ kind: "choice",
2512
+ ask: `${critics} objected to ${author}'s plan, and ${author} rejected the objection. Judging from the plans, which side is right?`,
2513
+ details: {
2514
+ claim: dispute.claim,
2515
+ why: dispute.reasons,
2516
+ rejection: dispute.rejections,
2517
+ check: dispute.check ? `${dispute.check.result.toUpperCase()}${dispute.check.evidence ? `: ${dispute.check.evidence}` : ""}` : "not checked"
2518
+ },
2519
+ options: {
2520
+ critic: `${critics}'s objection holds`,
2521
+ author: `${author}'s position holds`,
2522
+ unclear: "The material does not settle it"
2523
+ },
2524
+ fallback: "unclear"
2525
+ };
2526
+ }
2527
+ /** Whether the plan could be the final plan: the strongest plan, or a lone draft. */
2528
+ function standsAlone(stage) {
2529
+ return stage === "solo" ? {
2530
+ kind: "binary",
2531
+ ask: "Could this plan, as it stands, be handed to an implementer as the final plan, with no review or merge?",
2532
+ yes: "The plan is complete and self-contained; ready to implement",
2533
+ no: "The plan has gaps that review or another plan would need to fill",
2534
+ fallback: 0
2535
+ } : {
2536
+ kind: "binary",
2537
+ ask: "Could the strongest plan be handed to an implementer as the final plan, with no merge of the others?",
2538
+ yes: "One plan is already complete and self-contained; merging would add nothing material",
2539
+ no: "The plans hold complementary material that a final merge has to combine",
2540
+ fallback: 0
2541
+ };
2542
+ }
2543
+ /** A rubric of four levels; a judge that cannot answer claims the lowest. */
2544
+ function rubric(ask, levels) {
2545
+ return {
2546
+ kind: "score",
2547
+ ask,
2548
+ levels,
2549
+ fallback: 0
2550
+ };
2551
+ }
2552
+ /**
2553
+ * Every question of one judgement, in the order a verdict reads them. The
2554
+ * `finalizer` falls back to the first plan's agent, so it is always an agent.
2555
+ */
2556
+ function planQuestions(input) {
2557
+ const { plans, stage } = input;
2558
+ const disputes = {};
2559
+ input.disputes?.forEach((dispute, index) => {
2560
+ disputes[disputeKey(index)] = disputeQuestion(plans, dispute);
2561
+ });
2562
+ return {
2563
+ stronger_plan: {
2564
+ kind: "choice",
2565
+ ask: "Which plan is most likely to lead to a correct, efficient implementation of the task?",
2566
+ options: {
2567
+ ...agentOptions(plans, (label) => `${label}'s plan is materially stronger overall`),
2568
+ tie: "No plan is materially stronger, or their strengths are complementary"
2569
+ },
2570
+ fallback: "tie"
2571
+ },
2572
+ finalizer: {
2573
+ kind: "choice",
2574
+ ask: "Which agent's plan demonstrates the best judgment for merging all the plans into the final plan?",
2575
+ options: agentOptions(plans, (label) => `${label} should perform the final synthesis`),
2576
+ fallback: plans[0]?.agent ?? "tie"
2577
+ },
2578
+ completeness: rubric("How complete is the combined planning material for the requested task?", [
2579
+ "Major requirements or repository impacts are missing",
2580
+ "Several important details are missing",
2581
+ "Mostly complete, with minor gaps",
2582
+ "Complete and implementation-ready"
2583
+ ]),
2584
+ feasibility: rubric("How feasible and repository-grounded are the proposed implementation steps?", [
2585
+ "Mostly speculative or incompatible with the repository",
2586
+ "Partly grounded but contains risky assumptions",
2587
+ "Mostly grounded and feasible",
2588
+ "Highly concrete, minimal, and feasible"
2589
+ ]),
2590
+ risk_coverage: rubric("How well do the plans cover edge cases, tests, and rollout risks?", [
2591
+ "Risks are largely absent",
2592
+ "Only obvious risks are covered",
2593
+ "Important risks and tests are covered",
2594
+ "Risk, testing, and rollout coverage is thorough"
2595
+ ]),
2596
+ needs_another_pass: {
2597
+ kind: "binary",
2598
+ ask: "Would a cross-review round, each agent revising its plan against all the others, materially improve the final implementation plan?",
2599
+ yes: "Important contradictions, omissions, or unsupported assumptions remain",
2600
+ no: "The material is ready for final synthesis",
2601
+ fallback: 0
2602
+ },
2603
+ stands_alone: standsAlone(stage),
2604
+ ...disputes
2605
+ };
2606
+ }
2607
+ /** The plans as a judge sees them: keyed by agent name, the names the choices answer with. */
2608
+ function judgedPlans(plans) {
2609
+ return Object.fromEntries(plans.map(({ agent, label, plan }) => [agent, {
2610
+ author: label,
2611
+ plan: bounded(plan)
2612
+ }]));
2613
+ }
2614
+ //#endregion
2615
+ //#region src/jev/jev.ts
2616
+ /** A neutral question as the SDK asks it. */
2617
+ function toTypeSafe(question) {
2618
+ if (question.kind === "choice") {
2619
+ const instructions = question.details ? {
2620
+ question: question.ask,
2621
+ ...question.details
2622
+ } : question.ask;
2623
+ return (0, _typesafe_ai_sdk.choice)(instructions, question.options);
2624
+ }
2625
+ if (question.kind === "score") return (0, _typesafe_ai_sdk.score)(question.ask, question.levels);
2626
+ return (0, _typesafe_ai_sdk.noul)(question.ask, {
2627
+ true: question.yes,
2628
+ false: question.no
2629
+ });
2630
+ }
2631
+ /** TypeSafe Jev, asked the planner's questions in one `systemOne` call per judged round. */
2632
+ var TypeSafeJevJudge = class {
2633
+ client;
2634
+ name = "Jev";
2635
+ constructor(client = new _typesafe_ai_sdk.TypeSafeClient()) {
2636
+ this.client = client;
2637
+ }
2638
+ async judge(input) {
2639
+ const disputes = input.disputes ?? [];
2640
+ const questions = planQuestions({
2641
+ plans: input.plans,
2642
+ stage: input.stage,
2643
+ disputes
2644
+ });
2645
+ const response = await this.client.systemOne({
2646
+ ...input.model ? { model: input.model } : {},
2647
+ state: {
2648
+ task: input.task,
2649
+ stage: STAGE_TEXT[input.stage],
2650
+ plans: judgedPlans(input.plans)
2651
+ },
2652
+ questions: Object.fromEntries(Object.entries(questions).map(([key, question]) => [key, toTypeSafe(question)]))
2653
+ });
2654
+ const answers = response.answers;
2655
+ return {
2656
+ strongerPlan: answers.stronger_plan.choice,
2657
+ strongerPlanConfidence: answers.stronger_plan.confidence,
2658
+ finalizer: answers.finalizer.choice,
2659
+ finalizerConfidence: answers.finalizer.confidence,
2660
+ completeness: answers.completeness.score,
2661
+ completenessConfidence: answers.completeness.confidence,
2662
+ feasibility: answers.feasibility.score,
2663
+ feasibilityConfidence: answers.feasibility.confidence,
2664
+ riskCoverage: answers.risk_coverage.score,
2665
+ riskCoverageConfidence: answers.risk_coverage.confidence,
2666
+ needsAnotherPassProbability: answers.needs_another_pass.noul,
2667
+ standsAloneProbability: answers.stands_alone.noul,
2668
+ ...disputes.length > 0 ? { disputes: disputes.map((dispute, index) => {
2669
+ const answer = answers[disputeKey(index)];
2670
+ return answer ? {
2671
+ id: dispute.id,
2672
+ choice: answer.choice,
2673
+ confidence: answer.confidence
2674
+ } : {
2675
+ id: dispute.id,
2676
+ choice: "unclear",
2677
+ confidence: 0
2678
+ };
2679
+ }) } : {},
2680
+ model: response.model
2681
+ };
2682
+ }
2683
+ };
2684
+ //#endregion
2685
+ Object.defineProperty(exports, "DEFAULT_AGENTS", {
2686
+ enumerable: true,
2687
+ get: function() {
2688
+ return DEFAULT_AGENTS;
2689
+ }
2690
+ });
2691
+ Object.defineProperty(exports, "DEFAULT_STRAGGLER_GRACE_MS", {
2692
+ enumerable: true,
2693
+ get: function() {
2694
+ return DEFAULT_STRAGGLER_GRACE_MS;
2695
+ }
2696
+ });
2697
+ Object.defineProperty(exports, "PROVIDERS", {
2698
+ enumerable: true,
2699
+ get: function() {
2700
+ return PROVIDERS;
2701
+ }
2702
+ });
2703
+ Object.defineProperty(exports, "Planner", {
2704
+ enumerable: true,
2705
+ get: function() {
2706
+ return Planner;
2707
+ }
2708
+ });
2709
+ Object.defineProperty(exports, "ProcessError", {
2710
+ enumerable: true,
2711
+ get: function() {
2712
+ return ProcessError;
2713
+ }
2714
+ });
2715
+ Object.defineProperty(exports, "TaskValidationError", {
2716
+ enumerable: true,
2717
+ get: function() {
2718
+ return TaskValidationError;
2719
+ }
2720
+ });
2721
+ Object.defineProperty(exports, "TypeSafeJevJudge", {
2722
+ enumerable: true,
2723
+ get: function() {
2724
+ return TypeSafeJevJudge;
2725
+ }
2726
+ });
2727
+ Object.defineProperty(exports, "cliProvider", {
2728
+ enumerable: true,
2729
+ get: function() {
2730
+ return cliProvider;
2731
+ }
2732
+ });
2733
+ Object.defineProperty(exports, "main", {
2734
+ enumerable: true,
2735
+ get: function() {
2736
+ return main;
2737
+ }
2738
+ });
2739
+ Object.defineProperty(exports, "openAICompatibleProvider", {
2740
+ enumerable: true,
2741
+ get: function() {
2742
+ return openAICompatibleProvider;
2743
+ }
2744
+ });
2745
+ Object.defineProperty(exports, "processDeps", {
2746
+ enumerable: true,
2747
+ get: function() {
2748
+ return processDeps;
2749
+ }
2750
+ });
2751
+ Object.defineProperty(exports, "runDoctor", {
2752
+ enumerable: true,
2753
+ get: function() {
2754
+ return runDoctor;
2755
+ }
2756
+ });