pitroom 0.22.0 → 0.24.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.
package/dist/lib.mjs CHANGED
@@ -6,174 +6,187 @@ var UserError = class extends Error {
6
6
  }
7
7
  };
8
8
 
9
- // src/backends/claude/index.ts
9
+ // src/backends/antigravity/index.ts
10
+ import crypto3 from "node:crypto";
10
11
  import { spawnSync } from "node:child_process";
11
- import fs2 from "node:fs";
12
- import os2 from "node:os";
13
- import path2 from "node:path";
12
+ import fs6 from "node:fs";
13
+ import os3 from "node:os";
14
+ import path5 from "node:path";
14
15
 
15
- // src/backends/exec.ts
16
+ // src/core/secrets.ts
17
+ import { execFileSync } from "node:child_process";
16
18
  import fs from "node:fs";
17
- import os from "node:os";
18
19
  import path from "node:path";
19
- function findBinary(name, envVar, known = []) {
20
- const override = process.env[envVar];
21
- if (override) return override;
22
- const exts = process.platform === "win32" ? [".exe", ".cmd", ""] : [""];
23
- for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
24
- for (const ext of exts) {
25
- const p = path.join(dir, `${name}${ext}`);
26
- if (isFile(p)) return p;
20
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
21
+ var MAX_VISITED = 2e4;
22
+ var MAX_DEPTH = 4;
23
+ var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
24
+ function looksSecret(file) {
25
+ const name = file.toLowerCase();
26
+ if (TEMPLATE.test(name)) return false;
27
+ if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
28
+ return /\.(pem|p12|pfx|key|jks|keystore)$/.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/.test(name);
29
+ }
30
+ function findSecretFiles(dir) {
31
+ const found = [];
32
+ let visited = 0;
33
+ const walk = (d, depth) => {
34
+ if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
35
+ let entries;
36
+ try {
37
+ entries = fs.readdirSync(d, { withFileTypes: true });
38
+ } catch {
39
+ return;
27
40
  }
28
- }
29
- return known.map((k) => k.replace(/^~/, os.homedir())).find(isFile) ?? name;
41
+ for (const e of entries) {
42
+ visited++;
43
+ if (e.isDirectory()) {
44
+ if (!SKIP_DIRS.has(e.name)) walk(path.join(d, e.name), depth + 1);
45
+ } else if (e.isFile() && looksSecret(e.name)) {
46
+ found.push(slash(path.relative(dir, path.join(d, e.name))));
47
+ }
48
+ }
49
+ };
50
+ walk(dir, 0);
51
+ return found.sort();
30
52
  }
31
- function isFile(p) {
53
+ function findSecretFilesInTree(root, dir) {
32
54
  try {
33
- return fs.statSync(p).isFile();
55
+ const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
56
+ return out.split("\0").filter((f) => f && looksSecret(path.basename(f)) && !path.relative(dir, path.join(root, f)).startsWith("..")).map((f) => slash(path.relative(dir, path.join(root, f)))).sort();
34
57
  } catch {
35
- return false;
58
+ return [];
36
59
  }
37
60
  }
38
- function resolveCommand(bin) {
39
- try {
40
- const real = fs.realpathSync(bin);
41
- const fd = fs.openSync(real, "r");
42
- const head = Buffer.alloc(128);
43
- const n = fs.readSync(fd, head, 0, 128, 0);
44
- fs.closeSync(fd);
45
- const firstLine = head.subarray(0, n).toString("utf8").split("\n")[0] ?? "";
46
- if (/^#!.*\bnode\b/.test(firstLine)) return { command: process.execPath, prefix: [real] };
47
- } catch {
48
- }
49
- return { command: bin, prefix: [] };
61
+ function secretWarning(files, mode) {
62
+ if (!files.length) return void 0;
63
+ const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
64
+ const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
65
+ const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
66
+ return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
50
67
  }
68
+ var slash = (p) => p.split(path.sep).join("/");
51
69
 
52
- // src/backends/types.ts
53
- var MAX_STEPS = 400;
70
+ // src/core/store.ts
71
+ import fs3 from "node:fs";
72
+ import os from "node:os";
73
+ import path2 from "node:path";
74
+ import crypto from "node:crypto";
54
75
 
55
- // src/backends/claude/events.ts
56
- var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
57
- var DENIED = /permission|not allowed|denied|blocked|disallowed/i;
58
- function parseEvents(jsonl) {
59
- const usage = {
60
- input: 0,
61
- output: 0,
62
- reasoning: 0,
63
- cacheRead: 0,
64
- cacheWrite: 0,
65
- total: 0,
66
- steps: 0,
67
- toolCalls: 0,
68
- denied: 0
69
- };
70
- const tools = {};
71
- const edits = [];
72
- const pendingEdits = /* @__PURE__ */ new Map();
73
- const timeline = [];
74
- const stepOf = /* @__PURE__ */ new Map();
75
- const steps = /* @__PURE__ */ new Set();
76
- let sessionId;
77
- let model;
78
- let lastText = "";
79
- let finalText;
80
- let lastActivity;
81
- let error;
82
- for (const line of jsonl.split("\n")) {
83
- if (!line.trim()) continue;
84
- let e;
76
+ // src/core/fs-atomic.ts
77
+ import fs2 from "node:fs";
78
+ var BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
79
+ function renameOver(tmp, file) {
80
+ for (let attempt = 0; ; attempt++) {
85
81
  try {
86
- e = JSON.parse(line);
87
- } catch {
88
- continue;
82
+ fs2.renameSync(tmp, file);
83
+ return;
84
+ } catch (e) {
85
+ if (process.platform !== "win32" || !BUSY.has(e.code ?? "") || attempt >= 40) throw e;
86
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
89
87
  }
90
- sessionId ??= e.session_id;
91
- if (e.type === "system" && e.subtype === "init") {
92
- model = e.model;
93
- } else if (e.type === "assistant") {
94
- const content = e.message?.content ?? [];
95
- if (e.is_api_error_message || e.error) {
96
- error = content.map((c) => c.text).filter(Boolean).join(" ") || String(e.error);
97
- if (e.error) error = `${error} [${e.error}]`;
98
- continue;
99
- }
100
- steps.add(String(e.message?.id ?? e.uuid ?? steps.size));
101
- for (const c of content) {
102
- if (c.type === "text" && String(c.text ?? "").trim()) {
103
- lastText = String(c.text).trim();
104
- lastActivity = `says: ${oneLine(lastText)}`;
105
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(lastText, 600) });
106
- } else if (c.type === "tool_use") {
107
- usage.toolCalls++;
108
- tools[c.name] = (tools[c.name] ?? 0) + 1;
109
- const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
110
- lastActivity = `${c.name} ${target ? oneLine(String(target), 60) : ""}`.trim();
111
- if (EDIT_TOOLS.has(c.name) && c.input?.file_path) pendingEdits.set(c.id, String(c.input.file_path));
112
- if (timeline.length < MAX_STEPS) {
113
- const step = { kind: EDIT_TOOLS.has(c.name) ? "edit" : c.name === "Bash" ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
114
- timeline.push(step);
115
- stepOf.set(String(c.id), step);
116
- }
117
- }
118
- }
119
- } else if (e.type === "user") {
120
- for (const c of e.message?.content ?? []) {
121
- if (c?.type !== "tool_result") continue;
122
- const text = typeof c.content === "string" ? c.content : JSON.stringify(c.content ?? "");
123
- const step = stepOf.get(String(c.tool_use_id));
124
- if (step) step.ok = !c.is_error;
125
- if (c.is_error) {
126
- if (DENIED.test(text)) usage.denied++;
127
- } else if (pendingEdits.has(c.tool_use_id)) {
128
- edits.push(pendingEdits.get(c.tool_use_id));
129
- }
130
- }
131
- } else if (e.type === "result") {
132
- const u = e.usage ?? {};
133
- usage.input += num(u.input_tokens);
134
- usage.cacheRead += num(u.cache_read_input_tokens);
135
- usage.cacheWrite += num(u.cache_creation_input_tokens);
136
- usage.reasoning += num(u.output_tokens_details?.thinking_tokens);
137
- usage.output += Math.max(0, num(u.output_tokens) - num(u.output_tokens_details?.thinking_tokens));
138
- usage.total += num(u.input_tokens) + num(u.cache_read_input_tokens) + num(u.cache_creation_input_tokens) + num(u.output_tokens);
139
- usage.cost = (usage.cost ?? 0) + num(e.total_cost_usd);
140
- usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
141
- if (e.is_error) {
142
- const tags = [e.terminal_reason, e.api_error_status && `HTTP ${e.api_error_status}`].filter(Boolean);
143
- error ??= `${String(e.result ?? e.subtype ?? "Claude Code error")}${tags.length ? ` [${tags.join(", ")}]` : ""}`;
144
- } else if (typeof e.result === "string") {
145
- finalText = e.result.trim();
146
- }
88
+ }
89
+ }
90
+
91
+ // src/core/store.ts
92
+ var TERMINAL = ["done", "failed", "timeout", "stopped"];
93
+ var isActive = (s) => !TERMINAL.includes(s);
94
+ function home() {
95
+ if (process.env.PITROOM_HOME) return path2.resolve(process.env.PITROOM_HOME);
96
+ if (process.platform === "win32") {
97
+ return path2.join(process.env.LOCALAPPDATA ?? path2.join(os.homedir(), "AppData", "Local"), "pitroom");
98
+ }
99
+ return path2.join(process.env.XDG_STATE_HOME ?? path2.join(os.homedir(), ".local", "state"), "pitroom");
100
+ }
101
+ var runsDir = () => path2.join(home(), "runs");
102
+ var ledgerFile = () => path2.join(home(), "ledger.jsonl");
103
+ var runDir = (id2) => path2.join(runsDir(), id2);
104
+ var runFile = (id2, name) => path2.join(runDir(id2), name);
105
+ function newRunId(now = /* @__PURE__ */ new Date()) {
106
+ const p = (n) => String(n).padStart(2, "0");
107
+ const stamp2 = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
108
+ return `${stamp2}-${crypto.randomBytes(2).toString("hex")}`;
109
+ }
110
+ var onFinished;
111
+ var archive;
112
+ function writeMeta(meta) {
113
+ fs3.mkdirSync(runDir(meta.id), { recursive: true });
114
+ const file = runFile(meta.id, "meta.json");
115
+ const tmp = `${file}.${process.pid}.tmp`;
116
+ fs3.writeFileSync(tmp, JSON.stringify(meta, null, 2));
117
+ renameOver(tmp, file);
118
+ if (TERMINAL.includes(meta.state)) {
119
+ try {
120
+ onFinished?.(meta);
121
+ } catch {
147
122
  }
148
123
  }
149
- usage.steps = steps.size;
150
- return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
151
124
  }
152
- var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
153
- function num(v) {
154
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
125
+ function readMeta(id2) {
126
+ const file = runFile(id2, "meta.json");
127
+ if (!fs3.existsSync(file)) {
128
+ const kept = archive?.meta(id2);
129
+ if (kept) return upgrade(kept);
130
+ }
131
+ return upgrade(JSON.parse(fs3.readFileSync(file, "utf8")));
155
132
  }
156
- function oneLine(s, max = 80) {
157
- const t = s.replace(/\s+/g, " ").trim();
158
- return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
133
+ function upgrade(raw) {
134
+ if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
135
+ if (!Array.isArray(raw.fallback)) {
136
+ raw.fallback = (raw.fallbackModels ?? []).map((m) => ({ backend: "opencode", model: m }));
137
+ }
138
+ raw.warnings ??= [];
139
+ raw.files ??= [];
140
+ raw.link ??= [];
141
+ return raw;
159
142
  }
160
-
161
- // src/backends/claude/index.ts
162
- var binary = () => findBinary("claude", "PITROOM_CLAUDE_BIN", ["~/.local/bin/claude", "~/.claude/local/claude"]);
163
- function cl(args, timeout = 6e4) {
164
- const { command, prefix } = resolveCommand(binary());
165
- const r = spawnSync(command, [...prefix, ...args], {
166
- encoding: "utf8",
167
- timeout,
168
- stdio: ["ignore", "pipe", "pipe"],
169
- maxBuffer: 16 * 1024 * 1024
170
- });
171
- return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
143
+ function listRunIds() {
144
+ if (!fs3.existsSync(runsDir())) return [];
145
+ return fs3.readdirSync(runsDir()).filter((d) => fs3.existsSync(runFile(d, "meta.json"))).sort();
172
146
  }
173
- var READ_TOOLS = ["Read", "Grep", "Glob"];
174
- var WRITE_TOOLS = [...READ_TOOLS, "Edit", "Write", "Bash"];
175
- var WEB_TOOLS = ["WebFetch", "WebSearch"];
176
- var DENY_BASH = [
147
+ function resolveRun(ref) {
148
+ const ids = listRunIds();
149
+ if (!ref || ref === "latest" || ref === "last") {
150
+ if (!ids.length) throw new UserError("no runs yet");
151
+ return ids[ids.length - 1];
152
+ }
153
+ if (ids.includes(ref)) return ref;
154
+ const hits = ids.filter((id2) => id2.startsWith(ref) || id2.endsWith(ref));
155
+ if (hits.length === 1) return hits[0];
156
+ if (!hits.length) {
157
+ const kept = archive?.id(ref);
158
+ if (kept) return kept;
159
+ }
160
+ throw new UserError(hits.length ? `ambiguous run "${ref}": ${hits.join(", ")}` : ids.length ? `unknown run "${ref}"` : "no runs yet");
161
+ }
162
+ function isAlive(pid) {
163
+ if (!pid) return false;
164
+ try {
165
+ process.kill(pid, 0);
166
+ return true;
167
+ } catch (e) {
168
+ return e.code === "EPERM";
169
+ }
170
+ }
171
+ var STOP_FILE = "stop-requested";
172
+ var requestStop = (id2) => fs3.writeFileSync(runFile(id2, STOP_FILE), "");
173
+ function freshMeta(id2) {
174
+ const meta = readMeta(id2);
175
+ if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
176
+ const latest = readMeta(id2);
177
+ if (TERMINAL.includes(latest.state)) return latest;
178
+ const stopped = latest.stopRequested || fs3.existsSync(runFile(id2, STOP_FILE));
179
+ latest.state = stopped ? "stopped" : "failed";
180
+ if (!stopped) latest.error ??= "worker process exited unexpectedly";
181
+ latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
182
+ writeMeta(latest);
183
+ return latest;
184
+ }
185
+ return meta;
186
+ }
187
+
188
+ // src/backends/deny.ts
189
+ var FORBIDDEN_COMMANDS = [
177
190
  "git commit",
178
191
  "git push",
179
192
  "git pull",
@@ -198,6 +211,9 @@ var DENY_BASH = [
198
211
  "git add",
199
212
  "rm -rf",
200
213
  "rm -fr",
214
+ "rm -r",
215
+ "rm -R",
216
+ "rm --recursive",
201
217
  "sudo",
202
218
  "chown",
203
219
  "kill",
@@ -209,119 +225,521 @@ var DENY_BASH = [
209
225
  "opencode",
210
226
  "claude",
211
227
  "codex",
212
- "gemini"
213
- ].map((c) => `Bash(${c}:*)`);
214
- var SECRETS = ["**/.env", "**/.env.*", "**/*.pem", "**/id_rsa*", "**/id_ed25519*"].map((p) => `Read(${p})`);
215
- function toolsFor(mode, web2) {
216
- return [...mode === "read" ? READ_TOOLS : WRITE_TOOLS, ...web2 ? WEB_TOOLS : []];
228
+ "gemini",
229
+ "qwen",
230
+ "kilo",
231
+ "kilocode",
232
+ "agy"
233
+ ];
234
+
235
+ // src/backends/exec.ts
236
+ import fs4 from "node:fs";
237
+ import os2 from "node:os";
238
+ import path3 from "node:path";
239
+ function findBinary(name, envVar, known = []) {
240
+ const override = process.env[envVar];
241
+ if (override) return override;
242
+ const exts = process.platform === "win32" ? [".exe", ".cmd", ""] : [""];
243
+ for (const dir of (process.env.PATH ?? "").split(path3.delimiter).filter(Boolean)) {
244
+ for (const ext of exts) {
245
+ const p = path3.join(dir, `${name}${ext}`);
246
+ if (isFile(p)) return p;
247
+ }
248
+ }
249
+ return known.map((k) => k.replace(/^~/, os2.homedir())).find(isFile) ?? name;
250
+ }
251
+ function isFile(p) {
252
+ try {
253
+ return fs4.statSync(p).isFile();
254
+ } catch {
255
+ return false;
256
+ }
257
+ }
258
+ function resolveCommand(bin) {
259
+ try {
260
+ const real = fs4.realpathSync(bin);
261
+ const fd = fs4.openSync(real, "r");
262
+ const head = Buffer.alloc(128);
263
+ const n = fs4.readSync(fd, head, 0, 128, 0);
264
+ fs4.closeSync(fd);
265
+ const firstLine = head.subarray(0, n).toString("utf8").split("\n")[0] ?? "";
266
+ if (/^#!.*\bnode\b/.test(firstLine)) return { command: process.execPath, prefix: [real] };
267
+ } catch {
268
+ }
269
+ return { command: bin, prefix: [] };
270
+ }
271
+
272
+ // src/backends/home.ts
273
+ import crypto2 from "node:crypto";
274
+ import fs5 from "node:fs";
275
+ import path4 from "node:path";
276
+ function writeIfChanged(file, body) {
277
+ try {
278
+ if (fs5.readFileSync(file, "utf8") === body) return;
279
+ } catch {
280
+ }
281
+ fs5.mkdirSync(path4.dirname(file), { recursive: true });
282
+ const tmp = `${file}.${process.pid}-${crypto2.randomBytes(3).toString("hex")}.tmp`;
283
+ fs5.writeFileSync(tmp, body);
284
+ for (let tries = 0; ; tries++) {
285
+ try {
286
+ fs5.renameSync(tmp, file);
287
+ return;
288
+ } catch (e) {
289
+ const busy = ["EPERM", "EBUSY", "EACCES"].includes(e.code ?? "");
290
+ if (busy && tries < 10) {
291
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
292
+ continue;
293
+ }
294
+ fs5.rmSync(tmp, { force: true });
295
+ if (!busy) throw e;
296
+ fs5.writeFileSync(file, body);
297
+ return;
298
+ }
299
+ }
300
+ }
301
+ function linkInto(dir, from) {
302
+ const to = path4.join(dir, path4.basename(from));
303
+ try {
304
+ let st;
305
+ try {
306
+ st = fs5.lstatSync(to);
307
+ } catch {
308
+ }
309
+ if (st && !st.isSymbolicLink()) return;
310
+ if (st) {
311
+ if (fs5.readlinkSync(to) === from && fs5.existsSync(from)) return;
312
+ fs5.unlinkSync(to);
313
+ }
314
+ if (fs5.existsSync(from)) fs5.symlinkSync(from, to);
315
+ } catch {
316
+ }
317
+ }
318
+
319
+ // src/backends/text.ts
320
+ var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
321
+ function num(v) {
322
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
323
+ }
324
+ function oneLine(s, max = 80) {
325
+ return clip(s.replace(/\s+/g, " ").trim(), max);
326
+ }
327
+ function splitModel(model) {
328
+ if (!model) return {};
329
+ const [m, effort] = model.split("#");
330
+ return { model: m || void 0, effort: effort || void 0 };
331
+ }
332
+
333
+ // src/backends/types.ts
334
+ var MAX_STEPS = 400;
335
+
336
+ // src/backends/antigravity/events.ts
337
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["write_to_file", "replace_file_content", "multi_replace_file_content", "edit_file", "write_file"]);
338
+ var SHELL = "run_command";
339
+ var DENIED = /soft-denied|permission|denied|not allowed|blocked|refus|requires approval/i;
340
+ var FAILED = /error|fail|denied|cancel|reject/i;
341
+ var OK = /^(success|succeeded|ok|done|completed?)$/i;
342
+ function parseEvents(jsonl) {
343
+ const usage = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0, steps: 0, toolCalls: 0, denied: 0 };
344
+ const stepUsage = [];
345
+ const usageSeen = /* @__PURE__ */ new Set();
346
+ const tools = {};
347
+ const edits = [];
348
+ const timeline = [];
349
+ const said = /* @__PURE__ */ new Map();
350
+ const stepOf = /* @__PURE__ */ new Map();
351
+ const toolSeen = /* @__PURE__ */ new Set();
352
+ const done = /* @__PURE__ */ new Set();
353
+ let sessionId;
354
+ let model;
355
+ let lastText = "";
356
+ let response;
357
+ let lastActivity;
358
+ let error;
359
+ let streamError;
360
+ let resultSeen = false;
361
+ let haveResultUsage = false;
362
+ const open = /* @__PURE__ */ new Map();
363
+ let unnamed = 0;
364
+ let turns = 0;
365
+ let inTurn = false;
366
+ const startTurn = () => {
367
+ if (!inTurn) turns++;
368
+ inTurn = true;
369
+ };
370
+ for (const line of jsonl.split("\n")) {
371
+ if (!line.trim()) continue;
372
+ let e;
373
+ try {
374
+ e = JSON.parse(line);
375
+ } catch {
376
+ continue;
377
+ }
378
+ if (!e || typeof e !== "object") continue;
379
+ const type = String(e.event ?? e.type ?? "").toLowerCase();
380
+ const body = (name) => e[name] && typeof e[name] === "object" ? e[name] : e;
381
+ if (type === "init") {
382
+ sessionId ??= id(e.conversation_id ?? e.session_id ?? body("init").conversation_id);
383
+ model ??= id(body("init").model);
384
+ } else if (type === "step_update") {
385
+ const s = body("step_update");
386
+ sessionId ??= id(s.conversation_id);
387
+ const kind2 = String(s.step_type ?? "").toLowerCase();
388
+ const state = String(s.state ?? "").toUpperCase();
389
+ const failedStep = state === "ERROR";
390
+ const finished = state === "DONE" || failedStep;
391
+ let key = s.step_index ?? s.step_id;
392
+ if (key === void 0 || key === null) {
393
+ key = open.get(kind2) ?? `${kind2}#${unnamed++}`;
394
+ if (finished) open.delete(kind2);
395
+ else open.set(kind2, key);
396
+ }
397
+ key = String(key);
398
+ if (kind2 === "agent_response") {
399
+ startTurn();
400
+ const text = (said.get(key) ?? "") + str(s.text_delta);
401
+ said.set(key, text);
402
+ if (finished && !done.has(key)) {
403
+ done.add(key);
404
+ const t = text.trim();
405
+ if (t) {
406
+ lastText = t;
407
+ lastActivity = `says: ${oneLine(t)}`;
408
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(t, 600) });
409
+ }
410
+ }
411
+ } else if (kind2 === "tool") {
412
+ const info = s.tool_info && typeof s.tool_info === "object" ? s.tool_info : {};
413
+ const name = String(info.name ?? info.tool_name ?? s.tool_name ?? "tool");
414
+ const p = info.parameters ?? info.args ?? info.arguments ?? s.parameters ?? {};
415
+ if (!toolSeen.has(key)) {
416
+ toolSeen.add(key);
417
+ startTurn();
418
+ lastText = "";
419
+ usage.toolCalls++;
420
+ tools[name] = (tools[name] ?? 0) + 1;
421
+ const target = p.CommandLine ?? p.command ?? p.TargetFile ?? p.AbsolutePath ?? p.file_path ?? p.path ?? p.SearchPath ?? p.Query ?? p.query ?? p.Url ?? p.url;
422
+ lastActivity = `${name} ${target ? oneLine(String(target), 60) : ""}`.trim();
423
+ if (timeline.length < MAX_STEPS) {
424
+ const step = { kind: EDIT_TOOLS.has(name) ? "edit" : name === SHELL ? "shell" : "tool", name, text: clip(String(target ?? ""), 240) };
425
+ timeline.push(step);
426
+ stepOf.set(key, step);
427
+ }
428
+ }
429
+ if (finished && !done.has(key)) {
430
+ done.add(key);
431
+ inTurn = false;
432
+ const problem = `${errText(info.error)} ${str(info.status ?? s.status)}`.trim();
433
+ const ok = !failedStep && (!problem || OK.test(problem) || !FAILED.test(problem));
434
+ const step = stepOf.get(key);
435
+ if (step) step.ok = ok;
436
+ if (!ok && DENIED.test(`${problem} ${str(info.output)}`)) usage.denied++;
437
+ const file = p.TargetFile ?? p.AbsolutePath ?? p.file_path ?? p.path;
438
+ if (ok && EDIT_TOOLS.has(name) && file) edits.push(String(file));
439
+ }
440
+ }
441
+ if (finished && s.usage && typeof s.usage === "object" && !usageSeen.has(`${kind2}:${key}`)) {
442
+ usageSeen.add(`${kind2}:${key}`);
443
+ stepUsage.push(s.usage);
444
+ }
445
+ } else if (type === "result") {
446
+ const r = body("result");
447
+ sessionId ??= id(r.conversation_id);
448
+ model ??= id(r.model);
449
+ if (typeof r.response === "string") response = r.response.trim();
450
+ if (r.usage && typeof r.usage === "object") {
451
+ add(usage, r.usage);
452
+ haveResultUsage = true;
453
+ }
454
+ if (Array.isArray(r.denied_actions)) usage.denied = Math.max(usage.denied, r.denied_actions.length);
455
+ resultSeen = true;
456
+ const status = str(r.status);
457
+ const message = errText(r.error);
458
+ if (status ? !OK.test(status) : !!message) {
459
+ error ??= message || (/^waiting$/i.test(status) ? "agy stopped waiting for an approval a headless run cannot give" : `agy ended with status ${status}`);
460
+ }
461
+ } else if (type === "error") {
462
+ streamError ??= typeof e.message === "string" && e.message || errText(e.error) || "Antigravity CLI error";
463
+ }
464
+ }
465
+ if (streamError && (!resultSeen || !response)) error ??= streamError;
466
+ if (!haveResultUsage) for (const u of stepUsage) add(usage, u);
467
+ usage.steps = turns;
468
+ const finalText = error ? "" : response || lastText;
469
+ return { sessionId, model, finalText, usage, tools, edits, lastActivity, timeline, error };
470
+ }
471
+ function add(u, r) {
472
+ const cached3 = num(r.cache_read_tokens ?? r.cached_tokens);
473
+ const input = num(r.input_tokens);
474
+ u.cacheRead += cached3;
475
+ u.input += Math.max(0, input - cached3);
476
+ u.output += num(r.output_tokens);
477
+ u.reasoning += num(r.thinking_tokens ?? r.reasoning_tokens);
478
+ u.total += num(r.total_tokens) || input + num(r.output_tokens) + num(r.thinking_tokens ?? r.reasoning_tokens);
479
+ }
480
+ var id = (v) => typeof v === "string" && v ? v : void 0;
481
+ function errText(v) {
482
+ if (typeof v === "string") return v;
483
+ if (!v || typeof v !== "object" || !Object.keys(v).length) return "";
484
+ const code = typeof v.code === "string" || typeof v.code === "number" ? String(v.code) : "";
485
+ const message = typeof v.message === "string" ? v.message : typeof v.reason === "string" ? v.reason : "";
486
+ return [code, message].filter(Boolean).join(": ") || JSON.stringify(v);
487
+ }
488
+ var str = (v) => typeof v === "string" ? v : v === void 0 || v === null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
489
+
490
+ // src/backends/antigravity/index.ts
491
+ var binary = () => findBinary("agy", "PITROOM_ANTIGRAVITY_BIN", ["~/.local/bin/agy", "~/.antigravity/bin/agy"]);
492
+ function agy(args, timeout = 3e4) {
493
+ const { command, prefix } = resolveCommand(binary());
494
+ const r = spawnSync(command, [...prefix, ...args], { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"] });
495
+ return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
496
+ }
497
+ var dataDir = (h) => path5.join(h, ".gemini", "antigravity-cli");
498
+ var userDir = () => dataDir(os3.homedir());
499
+ var TOKEN = "antigravity-oauth-token";
500
+ var HOME_SECRETS = [".ssh", ".aws", ".gnupg", ".netrc", ".docker", ".kube", ".config/gcloud", ".config/gh", ".gemini", "Library/Keychains"];
501
+ function denyCommandRules() {
502
+ const subcommands = FORBIDDEN_COMMANDS.filter((c) => c.startsWith("git ")).map((c) => c.slice(4));
503
+ const anywhere = (cmd, option) => [0, 1, 2, 3, 4, 5].map((n) => `${cmd} ${".* ".repeat(n)}${option}`);
504
+ return [
505
+ ...FORBIDDEN_COMMANDS.map((c) => `command(${c})`),
506
+ ...FORBIDDEN_COMMANDS.filter((c) => !c.startsWith("git ")).map((c) => `command(regex:.*/${c})`),
507
+ // git by path would bypass the git guard's PATH shim; -C, -c, --git-dir and --work-tree point it elsewhere
508
+ "command(regex:.*/git)",
509
+ "command(regex:git (-C|-c|--git-dir.*|--work-tree.*))",
510
+ `command(regex:git -.* (${subcommands.join("|")}))`,
511
+ `command(regex:git -.* -.* (${subcommands.join("|")}))`,
512
+ // a recursive delete however its options are spelled or placed (rm -Rf, rm -r -f, rm dir -rf, rm --recursive)
513
+ ...["rm", ".*/rm"].flatMap((rm) => [...anywhere(rm, "-[a-zA-Z]*[rR][a-zA-Z]*"), ...anywhere(rm, "--recursive")]).map((r) => `command(regex:${r})`),
514
+ // find runs what follows -exec itself, and agy does not take it apart; such a rule only holds when it spans the
515
+ // whole command (a prefix of it was seen not to match), so find with -exec, -ok or -delete is refused outright,
516
+ // up to six words before and after (grep -r does what find -exec grep did)
517
+ ...["find", ".*/find"].flatMap((find) => [0, 1, 2, 3, 4, 5, 6].flatMap((before) => [0, 1, 2, 3, 4, 5, 6].flatMap((after) => {
518
+ const words = (n) => " .*".repeat(n);
519
+ return [
520
+ ...after ? [`${find}${words(before)} -(exec|execdir|ok|okdir)${words(after)}`] : [],
521
+ `${find}${words(before)} -delete${words(after)}`
522
+ ];
523
+ }))).map((r) => `command(regex:${r})`),
524
+ // xargs runs its arguments as a command, and agy does not take it apart either; with a `{}` in it a rule has
525
+ // to span the whole command again, so the words after the command are spelled out too
526
+ ...["xargs", ".*/xargs"].flatMap((xargs) => [0, 1, 2, 3].flatMap((n) => [0, 1, 2, 3].flatMap((after) => [
527
+ `${xargs}${" .*".repeat(n)} (.*/)?(${FORBIDDEN_COMMANDS.filter((c) => !c.includes(" ")).join("|")})${" .*".repeat(after)}`,
528
+ `${xargs}${" .*".repeat(n)} (.*/)?git (${subcommands.join("|")})${" .*".repeat(after)}`,
529
+ `${xargs}${" .*".repeat(n)} (.*/)?rm -[a-zA-Z]*[rR][a-zA-Z]*${" .*".repeat(after)}`
530
+ ]))).map((r) => `command(regex:${r})`),
531
+ // a command line with a `{}` in it is not split into commands: a rule is then matched against the whole line,
532
+ // so these find xargs and find wherever they stand in it
533
+ `command(regex:.*git (${subcommands.join("|")}).*)`,
534
+ "command(regex:.*rm -[a-zA-Z]*[rR].*)",
535
+ `command(regex:.*xargs .*rm -[a-zA-Z]*[rR].*)`,
536
+ `command(regex:.*xargs .*git .*(${subcommands.join("|")}).*)`,
537
+ "command(regex:.*find .*-(exec|execdir|ok|okdir) .*)",
538
+ "command(regex:.*find .*-delete.*)",
539
+ // a command inside a string is not matched above
540
+ "command(regex:(ba|z|da|k)?sh -[a-zA-Z]*c[a-zA-Z]*)",
541
+ "command(regex:.*/(ba|z|da|k)?sh -[a-zA-Z]*c[a-zA-Z]*)",
542
+ "command(eval)"
543
+ ];
544
+ }
545
+ function workerSettings(workspace, workerHome3) {
546
+ const real = (p) => {
547
+ try {
548
+ return fs6.realpathSync(p);
549
+ } catch {
550
+ return p;
551
+ }
552
+ };
553
+ const both = (p) => [.../* @__PURE__ */ new Set([p, real(p)])];
554
+ const secrets = [
555
+ ...findSecretFiles(workspace).map((f) => path5.join(workspace, f)),
556
+ ...HOME_SECRETS.map((f) => path5.join(os3.homedir(), f)).filter((p) => fs6.existsSync(p)),
557
+ // the keychain link of the worker's own home
558
+ path5.join(workerHome3, "Library")
559
+ ];
560
+ return {
561
+ ...baseSettings(),
562
+ permissions: {
563
+ // plan mode writes its plan into agy's own data folder, which is outside the workspace; the user's toolchains
564
+ // under their home stay usable in the sandbox (read-only), which sees only what is granted besides the system
565
+ allow: [
566
+ "command(*)",
567
+ ...both(path5.join(dataDir(workerHome3), "brain")).map((p) => `write_file(${rulePath(p)})`),
568
+ ...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`)
569
+ ],
570
+ deny: [
571
+ ...secrets.flatMap(both).map((p) => `read_file(${rulePath(p)})`),
572
+ ...denyCommandRules(),
573
+ "mcp(*)",
574
+ // No web for agy workers: the rule is in the settings file, so --web cannot switch it on per run.
575
+ "read_url(*)",
576
+ "execute_url(*)"
577
+ ]
578
+ }
579
+ };
580
+ }
581
+ function toolchains() {
582
+ const homeDir = os3.homedir();
583
+ const inHome = (d) => {
584
+ const rel = path5.relative(homeDir, d);
585
+ return !!rel && !rel.startsWith("..") && !path5.isAbsolute(rel);
586
+ };
587
+ const dirs = (process.env.PATH ?? "").split(path5.delimiter).filter((d) => d && path5.isAbsolute(d) && inHome(d) && fs6.existsSync(d));
588
+ const install = (d) => path5.basename(d) === "bin" && /[\\/](versions|toolchains)[\\/]/.test(d) ? path5.dirname(d) : d;
589
+ return [...new Set(dirs.map(install))].filter((d) => !inHome(home()) || path5.relative(home(), d).startsWith(".."));
590
+ }
591
+ function baseSettings() {
592
+ return {
593
+ enableTerminalSandbox: true,
594
+ toolPermission: "proceed-in-sandbox",
595
+ artifactReviewPolicy: "always-proceed",
596
+ allowNonWorkspaceAccess: false
597
+ };
598
+ }
599
+ var rulePath = (p) => p.replace(/^[A-Za-z]:(?=[\\/])/, "").replace(/\\/g, "/");
600
+ var HOMES_KEPT_DAYS = 14;
601
+ function workerHome(workspace) {
602
+ const root = path5.join(home(), "antigravity-home");
603
+ const id2 = crypto3.createHash("sha256").update(path5.resolve(workspace)).digest("hex").slice(0, 16);
604
+ const dir = path5.join(root, id2);
605
+ fs6.mkdirSync(dataDir(dir), { recursive: true });
606
+ writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir), null, 2)}
607
+ `);
608
+ if (process.platform === "darwin") {
609
+ fs6.mkdirSync(path5.join(dir, "Library"), { recursive: true });
610
+ linkInto(path5.join(dir, "Library"), path5.join(os3.homedir(), "Library", "Keychains"));
611
+ } else {
612
+ linkInto(dataDir(dir), path5.join(userDir(), TOKEN));
613
+ }
614
+ const now = /* @__PURE__ */ new Date();
615
+ fs6.utimesSync(dir, now, now);
616
+ pruneHomes(root, dir);
617
+ return dir;
618
+ }
619
+ function pruneHomes(root, keep) {
620
+ const cutoff = Date.now() - HOMES_KEPT_DAYS * 864e5;
621
+ try {
622
+ for (const name of fs6.readdirSync(root)) {
623
+ const dir = path5.join(root, name);
624
+ if (dir === keep || !/^[0-9a-f]{16}$/.test(name)) continue;
625
+ if (fs6.statSync(dir).mtimeMs < cutoff) fs6.rmSync(dir, { recursive: true, force: true });
626
+ }
627
+ } catch {
628
+ }
217
629
  }
218
630
  function invocation(req) {
219
- const tools = toolsFor(req.mode, req.web).join(",");
220
631
  const args = [
632
+ // -p takes the prompt as its value; one that starts with "-" would be read as an option: a leading space keeps it a value.
221
633
  "-p",
222
- "--safe-mode",
223
- "--restricted",
224
- "--strict-mcp-config",
225
- "--tools",
226
- tools,
227
- "--allowedTools",
228
- tools
229
- ];
230
- if (req.mode !== "read") args.push("--disallowedTools", ...DENY_BASH);
231
- args.push(
232
- "--settings",
233
- JSON.stringify({ permissions: { deny: SECRETS } }),
234
- // Non-variadic options last, so no list above can swallow what follows.
235
- "--permission-mode",
236
- "dontAsk",
634
+ req.prompt.startsWith("-") ? ` ${req.prompt}` : req.prompt,
237
635
  "--output-format",
238
636
  "stream-json",
239
- "--verbose"
240
- );
241
- const [model, effort] = (req.model ?? "").split("#");
637
+ "--mode",
638
+ req.mode === "read" ? "plan" : "accept-edits",
639
+ "--sandbox",
640
+ // agy's own limit (5m by default) would end a long run with partial output and exit 0; Pitroom's timeout governs.
641
+ "--print-timeout",
642
+ "24h"
643
+ ];
644
+ if (req.sessionId) args.push("--conversation", req.sessionId);
645
+ const { model, effort } = splitModel(req.model);
242
646
  if (model) args.push("--model", model);
243
647
  if (effort) args.push("--effort", effort);
244
- if (req.sessionId) args.push("--resume", req.sessionId);
245
- args.push("--", req.prompt);
246
648
  const { command, prefix } = resolveCommand(binary());
247
- return { command, args: [...prefix, ...args], env: { DISABLE_AUTOUPDATER: "1" } };
649
+ const workerDir = workerHome(req.cwd);
650
+ const env = { HOME: workerDir };
651
+ if (process.platform === "win32") env.USERPROFILE = workerDir;
652
+ return { command, args: [...prefix, ...args], env };
248
653
  }
249
654
  function classify(message) {
250
- if (/authentication_failed|failed to authenticate|oauth|not logged in|\/login|invalid api key|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
655
+ if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
656
+ if (/not (signed|logged) in|\bsign.?in\b|\blog ?in\b|authenticat|PERMISSION_DENIED|oauth|credential|keyring|keychain|\b401\b|unauthori[sz]ed/i.test(message)) {
251
657
  return "auth";
252
658
  }
253
- if (/rate.?limit|too many requests|\b429\b|\b529\b|overloaded|usage limit|limit reached|quota|capacity/i.test(message)) return "rate-limited";
254
- if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
659
+ if (/model.{0,80}(not found|not available|no longer available|does not exist|unknown|unsupported|invalid)|unknown model|invalid model|NOT_FOUND|\b404\b/i.test(message)) {
660
+ return "model-unavailable";
661
+ }
255
662
  return "other";
256
663
  }
664
+ var TIMED_OUT = /print timeout after/i;
665
+ var BLOCKED = /^This request was blocked by \S+ filters\b/;
257
666
  function failure(run, stderr, exitCode) {
258
- if (exitCode === 0 && !run.error) return void 0;
259
- const detail = stderr.trim().split("\n").filter(Boolean).pop();
260
- const message = run.error ?? detail ?? `claude exited with code ${exitCode}`;
261
- return { kind: classify(message), message };
262
- }
263
- function defaultModel() {
667
+ const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
668
+ const envelope = lines.find((l) => l.startsWith("AGY_ERROR"));
669
+ const detail = (envelope && fromEnvelope(envelope)) ?? lines.find((l) => /RESOURCE_EXHAUSTED|error|failed|quota|exhausted|denied|not found|\bsign.?in\b/i.test(l) && !TIMED_OUT.test(l));
670
+ if (exitCode === 0 && !run.error) {
671
+ if (BLOCKED.test(run.finalText)) return { kind: "other", message: `the model's filter blocked the request: ${run.finalText.split("\n")[0]}` };
672
+ if (run.finalText) return void 0;
673
+ if (detail) return { kind: classify(detail), message: detail };
674
+ if (lines.some((l) => TIMED_OUT.test(l))) return { kind: "other", message: "agy stopped at its own --print-timeout before answering" };
675
+ if (run.usage.denied) return { kind: "other", message: "agy ended its turn without an answer when a tool call was refused" };
676
+ if (!run.sessionId && !stderr.trim()) return { kind: "other", message: "agy printed nothing (some agy versions write no output to a pipe); update agy" };
677
+ return { kind: "other", message: "agy ended without an answer (a refused tool call ends its turn with no other sign)" };
678
+ }
679
+ const message = run.error ?? detail ?? lines.at(-1) ?? `agy exited with code ${exitCode}`;
680
+ return { kind: classify(`${message} ${detail ?? ""}`), message };
681
+ }
682
+ function warnings(_run, stderr) {
683
+ const lost = /conversation "([^"]+)" not found/i.exec(stderr);
684
+ return lost ? [`agy did not find the earlier conversation (${lost[1]}) and started a new one: this answer does not know the earlier turns`] : [];
685
+ }
686
+ function fromEnvelope(line) {
264
687
  try {
265
- const settings2 = JSON.parse(fs2.readFileSync(path2.join(os2.homedir(), ".claude", "settings.json"), "utf8"));
266
- return typeof settings2.model === "string" ? settings2.model : void 0;
688
+ const e = JSON.parse(line.slice(line.indexOf("{")));
689
+ if (typeof e.short_error === "string" && e.short_error) return e.short_error;
690
+ const text = [e.status ?? e.code, e.message ?? e.error].filter((v) => typeof v === "string" && v).join(": ");
691
+ return text || void 0;
267
692
  } catch {
268
- return void 0;
693
+ return line.replace(/^AGY_ERROR:?\s*/, "") || void 0;
269
694
  }
270
695
  }
271
696
  function doctor({ models, hasFallback }) {
272
- const version = cl(["--version"]);
697
+ const version = agy(["--version"]);
273
698
  if (version.missing || !version.ok) {
274
- return [{ level: "fail", message: `claude not runnable (${binary()}). Install Claude Code, or set PITROOM_CLAUDE_BIN` }];
275
- }
276
- const checks = [{ level: "ok", message: `Claude Code ${version.out.trim()} at ${binary()}` }];
277
- let loggedIn = false;
278
- try {
279
- loggedIn = JSON.parse(cl(["auth", "status"]).out).loggedIn === true;
280
- } catch {
699
+ return [{
700
+ level: "fail",
701
+ message: `agy not runnable (${binary()}). Install the Antigravity CLI (curl -fsSL https://antigravity.google/cli/install.sh | bash), or set PITROOM_ANTIGRAVITY_BIN`
702
+ }];
703
+ }
704
+ const checks = [
705
+ { level: "ok", message: `Antigravity CLI ${version.out.trim().split("\n")[0]} at ${binary()}` },
706
+ {
707
+ level: "warn",
708
+ message: "the Antigravity worker is in beta (checked against agy 1.3.5). agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes"
709
+ }
710
+ ];
711
+ if (process.platform === "win32") {
712
+ checks.push({ level: "warn", message: "agy's permission rules and terminal sandbox are for macOS and Linux; on Windows run the Antigravity worker only with -i (an isolated copy)" });
281
713
  }
282
- checks.push(
283
- loggedIn || process.env.ANTHROPIC_API_KEY ? { level: "ok", message: `Claude Code: ${loggedIn ? "logged in" : "ANTHROPIC_API_KEY set"}` } : { level: "fail", message: "Claude Code is not logged in (or the session expired): run `claude auth login`" }
284
- );
285
- for (const m of models) {
286
- const model = m ?? defaultModel();
287
- const pricey = model && /opus|fable/i.test(model);
288
- checks.push({
289
- level: pricey ? "warn" : "ok",
290
- message: pricey ? `Claude Code model: ${model} is the most expensive tier for a worker; consider -W claude:haiku or claude:sonnet` : model ? `Claude Code model: ${model}` : "Claude Code model: Claude Code's default (often the largest model; a cheaper worker is -W claude:haiku)"
291
- });
714
+ if (process.platform === "linux" && !fs6.existsSync(path5.join(userDir(), TOKEN))) {
715
+ checks.push({ level: "warn", message: "no Antigravity sign-in file found: if a run fails to sign in, run `agy` once and sign in with your Google account" });
292
716
  }
293
- if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Claude Code runs" });
717
+ for (const m of models) checks.push({ level: "ok", message: m ? `Antigravity model: ${m}` : "Antigravity model: agy's own choice (`agy models` lists yours)" });
718
+ checks.push({ level: "ok", message: "Antigravity workers have no web access (--web does not apply to them)" });
719
+ if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Antigravity runs (its quota is per account and can run out for hours)" });
294
720
  return checks;
295
721
  }
296
- var CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
297
- function catalog() {
298
- const def = defaultModel();
299
- const models = ["haiku", "sonnet", "opus"].map((id) => ({ id, efforts: CLAUDE_EFFORTS }));
300
- if (def && !models.some((m) => m.id === def)) models.push({ id: def, efforts: CLAUDE_EFFORTS });
301
- return { models, source: "Claude Code aliases (latest of each size) and --effort levels from `claude --help`" };
302
- }
303
- var claude = {
304
- id: "claude",
305
- name: "Claude Code",
306
- capabilities: { readOnly: "tool-allowlist", resume: "by-id", reportsCost: true, attachFiles: false },
722
+ var antigravity = {
723
+ id: "antigravity",
724
+ name: "Antigravity CLI",
725
+ capabilities: { readOnly: "snapshot", resume: "by-id", reportsCost: false, attachFiles: false },
307
726
  binary,
308
- catalog,
309
727
  invocation,
310
728
  parse: parseEvents,
311
729
  failure,
312
- defaultModel,
730
+ warnings,
313
731
  doctor
314
732
  };
315
733
 
316
- // src/backends/codex/index.ts
734
+ // src/backends/claude/index.ts
317
735
  import { spawnSync as spawnSync2 } from "node:child_process";
318
- import fs3 from "node:fs";
319
- import os3 from "node:os";
320
- import path3 from "node:path";
736
+ import fs7 from "node:fs";
737
+ import os4 from "node:os";
738
+ import path6 from "node:path";
321
739
 
322
- // src/backends/codex/events.ts
323
- var WORK = /* @__PURE__ */ new Set(["agent_message", "reasoning", "command_execution", "file_change", "mcp_tool_call", "web_search", "todo_list"]);
324
- var TOOLS = /* @__PURE__ */ new Set(["command_execution", "file_change", "mcp_tool_call", "web_search"]);
740
+ // src/backends/claude/events.ts
741
+ var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
742
+ var DENIED2 = /permission|not allowed|denied|blocked|disallowed/i;
325
743
  function parseEvents2(jsonl) {
326
744
  const usage = {
327
745
  input: 0,
@@ -336,9 +754,14 @@ function parseEvents2(jsonl) {
336
754
  };
337
755
  const tools = {};
338
756
  const edits = [];
757
+ const pendingEdits = /* @__PURE__ */ new Map();
339
758
  const timeline = [];
759
+ const stepOf = /* @__PURE__ */ new Map();
760
+ const steps = /* @__PURE__ */ new Set();
340
761
  let sessionId;
341
- let finalText = "";
762
+ let model;
763
+ let lastText = "";
764
+ let finalText;
342
765
  let lastActivity;
343
766
  let error;
344
767
  for (const line of jsonl.split("\n")) {
@@ -349,45 +772,262 @@ function parseEvents2(jsonl) {
349
772
  } catch {
350
773
  continue;
351
774
  }
352
- switch (e.type) {
353
- case "thread.started":
354
- sessionId ??= e.thread_id;
355
- break;
356
- case "item.completed": {
357
- const it = e.item ?? {};
358
- if (!WORK.has(it.type)) break;
359
- usage.steps++;
360
- if (TOOLS.has(it.type)) {
361
- usage.toolCalls++;
362
- tools[it.type] = (tools[it.type] ?? 0) + 1;
363
- }
364
- if (it.type === "agent_message" && String(it.text ?? "").trim()) {
365
- finalText = String(it.text).trim();
366
- lastActivity = `says: ${oneLine2(finalText)}`;
367
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip2(finalText, 600) });
368
- } else if (it.type === "command_execution") {
369
- if (it.status === "declined" || /operation not permitted|sandbox/i.test(String(it.aggregated_output ?? ""))) usage.denied++;
370
- lastActivity = `shell ${oneLine2(String(it.command ?? ""), 60)}`;
371
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "shell", name: "shell", text: clip2(String(it.command ?? "").replace(/^\/bin\/\w+ -lc /, ""), 240), ok: it.exit_code === 0 });
372
- } else if (it.type === "file_change") {
373
- const paths = (it.changes ?? []).map((c) => String(c.path));
374
- if (it.status !== "failed" && it.status !== "declined") edits.push(...paths);
375
- else usage.denied++;
376
- lastActivity = `edit ${oneLine2(paths.join(", "), 60)}`;
377
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "edit", name: "edit", text: clip2(paths.join(", "), 240), ok: it.status !== "failed" && it.status !== "declined" });
378
- }
379
- break;
380
- }
775
+ sessionId ??= e.session_id;
776
+ if (e.type === "system" && e.subtype === "init") {
777
+ model = e.model;
778
+ } else if (e.type === "assistant") {
779
+ const content = e.message?.content ?? [];
780
+ if (e.is_api_error_message || e.error) {
781
+ error = content.map((c) => c.text).filter(Boolean).join(" ") || String(e.error);
782
+ if (e.error) error = `${error} [${e.error}]`;
783
+ continue;
784
+ }
785
+ steps.add(String(e.message?.id ?? e.uuid ?? steps.size));
786
+ for (const c of content) {
787
+ if (c.type === "text" && String(c.text ?? "").trim()) {
788
+ lastText = String(c.text).trim();
789
+ lastActivity = `says: ${oneLine(lastText)}`;
790
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(lastText, 600) });
791
+ } else if (c.type === "tool_use") {
792
+ usage.toolCalls++;
793
+ tools[c.name] = (tools[c.name] ?? 0) + 1;
794
+ const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
795
+ lastActivity = `${c.name} ${target ? oneLine(String(target), 60) : ""}`.trim();
796
+ if (EDIT_TOOLS2.has(c.name) && c.input?.file_path) pendingEdits.set(c.id, String(c.input.file_path));
797
+ if (timeline.length < MAX_STEPS) {
798
+ const step = { kind: EDIT_TOOLS2.has(c.name) ? "edit" : c.name === "Bash" ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
799
+ timeline.push(step);
800
+ stepOf.set(String(c.id), step);
801
+ }
802
+ }
803
+ }
804
+ } else if (e.type === "user") {
805
+ for (const c of e.message?.content ?? []) {
806
+ if (c?.type !== "tool_result") continue;
807
+ const text = typeof c.content === "string" ? c.content : JSON.stringify(c.content ?? "");
808
+ const step = stepOf.get(String(c.tool_use_id));
809
+ if (step) step.ok = !c.is_error;
810
+ if (c.is_error) {
811
+ if (DENIED2.test(text)) usage.denied++;
812
+ } else if (pendingEdits.has(c.tool_use_id)) {
813
+ edits.push(pendingEdits.get(c.tool_use_id));
814
+ }
815
+ }
816
+ } else if (e.type === "result") {
817
+ const u = e.usage ?? {};
818
+ usage.input += num(u.input_tokens);
819
+ usage.cacheRead += num(u.cache_read_input_tokens);
820
+ usage.cacheWrite += num(u.cache_creation_input_tokens);
821
+ usage.reasoning += num(u.output_tokens_details?.thinking_tokens);
822
+ usage.output += Math.max(0, num(u.output_tokens) - num(u.output_tokens_details?.thinking_tokens));
823
+ usage.total += num(u.input_tokens) + num(u.cache_read_input_tokens) + num(u.cache_creation_input_tokens) + num(u.output_tokens);
824
+ usage.cost = (usage.cost ?? 0) + num(e.total_cost_usd);
825
+ usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
826
+ if (e.is_error) {
827
+ const tags = [e.terminal_reason, e.api_error_status && `HTTP ${e.api_error_status}`].filter(Boolean);
828
+ error ??= `${String(e.result ?? e.subtype ?? "Claude Code error")}${tags.length ? ` [${tags.join(", ")}]` : ""}`;
829
+ } else if (typeof e.result === "string") {
830
+ finalText = e.result.trim();
831
+ }
832
+ }
833
+ }
834
+ usage.steps = steps.size;
835
+ return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
836
+ }
837
+
838
+ // src/backends/claude/index.ts
839
+ var binary2 = () => findBinary("claude", "PITROOM_CLAUDE_BIN", ["~/.local/bin/claude", "~/.claude/local/claude"]);
840
+ function cl(args, timeout = 6e4) {
841
+ const { command, prefix } = resolveCommand(binary2());
842
+ const r = spawnSync2(command, [...prefix, ...args], {
843
+ encoding: "utf8",
844
+ timeout,
845
+ stdio: ["ignore", "pipe", "pipe"],
846
+ maxBuffer: 16 * 1024 * 1024
847
+ });
848
+ return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
849
+ }
850
+ var READ_TOOLS = ["Read", "Grep", "Glob"];
851
+ var WRITE_TOOLS = [...READ_TOOLS, "Edit", "Write", "Bash"];
852
+ var WEB_TOOLS = ["WebFetch", "WebSearch"];
853
+ var DENY_BASH = FORBIDDEN_COMMANDS.map((c) => `Bash(${c}:*)`);
854
+ var SECRETS = ["**/.env", "**/.env.*", "**/*.pem", "**/id_rsa*", "**/id_ed25519*"].map((p) => `Read(${p})`);
855
+ function toolsFor(mode, web2) {
856
+ return [...mode === "read" ? READ_TOOLS : WRITE_TOOLS, ...web2 ? WEB_TOOLS : []];
857
+ }
858
+ function invocation2(req) {
859
+ const tools = toolsFor(req.mode, req.web).join(",");
860
+ const args = [
861
+ "-p",
862
+ "--safe-mode",
863
+ "--restricted",
864
+ "--strict-mcp-config",
865
+ "--tools",
866
+ tools,
867
+ "--allowedTools",
868
+ tools
869
+ ];
870
+ if (req.mode !== "read") args.push("--disallowedTools", ...DENY_BASH);
871
+ args.push(
872
+ "--settings",
873
+ JSON.stringify({ permissions: { deny: SECRETS } }),
874
+ // Non-variadic options last, so no list above can swallow what follows.
875
+ "--permission-mode",
876
+ "dontAsk",
877
+ "--output-format",
878
+ "stream-json",
879
+ "--verbose"
880
+ );
881
+ const [model, effort] = (req.model ?? "").split("#");
882
+ if (model) args.push("--model", model);
883
+ if (effort) args.push("--effort", effort);
884
+ if (req.sessionId) args.push("--resume", req.sessionId);
885
+ args.push("--", req.prompt);
886
+ const { command, prefix } = resolveCommand(binary2());
887
+ return { command, args: [...prefix, ...args], env: { DISABLE_AUTOUPDATER: "1" } };
888
+ }
889
+ function classify2(message) {
890
+ if (/authentication_failed|failed to authenticate|oauth|not logged in|\/login|invalid api key|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
891
+ return "auth";
892
+ }
893
+ if (/rate.?limit|too many requests|\b429\b|\b529\b|overloaded|usage limit|limit reached|quota|capacity/i.test(message)) return "rate-limited";
894
+ if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
895
+ return "other";
896
+ }
897
+ function failure2(run, stderr, exitCode) {
898
+ if (exitCode === 0 && !run.error) return void 0;
899
+ const detail = stderr.trim().split("\n").filter(Boolean).pop();
900
+ const message = run.error ?? detail ?? `claude exited with code ${exitCode}`;
901
+ return { kind: classify2(message), message };
902
+ }
903
+ function defaultModel() {
904
+ try {
905
+ const settings2 = JSON.parse(fs7.readFileSync(path6.join(os4.homedir(), ".claude", "settings.json"), "utf8"));
906
+ return typeof settings2.model === "string" ? settings2.model : void 0;
907
+ } catch {
908
+ return void 0;
909
+ }
910
+ }
911
+ function doctor2({ models, hasFallback }) {
912
+ const version = cl(["--version"]);
913
+ if (version.missing || !version.ok) {
914
+ return [{ level: "fail", message: `claude not runnable (${binary2()}). Install Claude Code, or set PITROOM_CLAUDE_BIN` }];
915
+ }
916
+ const checks = [{ level: "ok", message: `Claude Code ${version.out.trim()} at ${binary2()}` }];
917
+ let loggedIn = false;
918
+ try {
919
+ loggedIn = JSON.parse(cl(["auth", "status"]).out).loggedIn === true;
920
+ } catch {
921
+ }
922
+ checks.push(
923
+ loggedIn || process.env.ANTHROPIC_API_KEY ? { level: "ok", message: `Claude Code: ${loggedIn ? "logged in" : "ANTHROPIC_API_KEY set"}` } : { level: "fail", message: "Claude Code is not logged in (or the session expired): run `claude auth login`" }
924
+ );
925
+ for (const m of models) {
926
+ const model = m ?? defaultModel();
927
+ const pricey = model && /opus|fable/i.test(model);
928
+ checks.push({
929
+ level: pricey ? "warn" : "ok",
930
+ message: pricey ? `Claude Code model: ${model} is the most expensive tier for a worker; consider -W claude:haiku or claude:sonnet` : model ? `Claude Code model: ${model}` : "Claude Code model: Claude Code's default (often the largest model; a cheaper worker is -W claude:haiku)"
931
+ });
932
+ }
933
+ if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Claude Code runs" });
934
+ return checks;
935
+ }
936
+ var CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
937
+ function catalog() {
938
+ const def = defaultModel();
939
+ const models = ["haiku", "sonnet", "opus"].map((id2) => ({ id: id2, efforts: CLAUDE_EFFORTS }));
940
+ if (def && !models.some((m) => m.id === def)) models.push({ id: def, efforts: CLAUDE_EFFORTS });
941
+ return { models, source: "Claude Code aliases (latest of each size) and --effort levels from `claude --help`" };
942
+ }
943
+ var claude = {
944
+ id: "claude",
945
+ name: "Claude Code",
946
+ capabilities: { readOnly: "tool-allowlist", resume: "by-id", reportsCost: true, attachFiles: false },
947
+ binary: binary2,
948
+ catalog,
949
+ invocation: invocation2,
950
+ parse: parseEvents2,
951
+ failure: failure2,
952
+ defaultModel,
953
+ doctor: doctor2
954
+ };
955
+
956
+ // src/backends/codex/index.ts
957
+ import { spawnSync as spawnSync3 } from "node:child_process";
958
+ import fs8 from "node:fs";
959
+ import os5 from "node:os";
960
+ import path7 from "node:path";
961
+
962
+ // src/backends/codex/events.ts
963
+ var WORK = /* @__PURE__ */ new Set(["agent_message", "reasoning", "command_execution", "file_change", "mcp_tool_call", "web_search", "todo_list"]);
964
+ var TOOLS = /* @__PURE__ */ new Set(["command_execution", "file_change", "mcp_tool_call", "web_search"]);
965
+ function parseEvents3(jsonl) {
966
+ const usage = {
967
+ input: 0,
968
+ output: 0,
969
+ reasoning: 0,
970
+ cacheRead: 0,
971
+ cacheWrite: 0,
972
+ total: 0,
973
+ steps: 0,
974
+ toolCalls: 0,
975
+ denied: 0
976
+ };
977
+ const tools = {};
978
+ const edits = [];
979
+ const timeline = [];
980
+ let sessionId;
981
+ let finalText = "";
982
+ let lastActivity;
983
+ let error;
984
+ for (const line of jsonl.split("\n")) {
985
+ if (!line.trim()) continue;
986
+ let e;
987
+ try {
988
+ e = JSON.parse(line);
989
+ } catch {
990
+ continue;
991
+ }
992
+ switch (e.type) {
993
+ case "thread.started":
994
+ sessionId ??= e.thread_id;
995
+ break;
996
+ case "item.completed": {
997
+ const it = e.item ?? {};
998
+ if (!WORK.has(it.type)) break;
999
+ usage.steps++;
1000
+ if (TOOLS.has(it.type)) {
1001
+ usage.toolCalls++;
1002
+ tools[it.type] = (tools[it.type] ?? 0) + 1;
1003
+ }
1004
+ if (it.type === "agent_message" && String(it.text ?? "").trim()) {
1005
+ finalText = String(it.text).trim();
1006
+ lastActivity = `says: ${oneLine(finalText)}`;
1007
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(finalText, 600) });
1008
+ } else if (it.type === "command_execution") {
1009
+ if (it.status === "declined" || /operation not permitted|sandbox/i.test(String(it.aggregated_output ?? ""))) usage.denied++;
1010
+ lastActivity = `shell ${oneLine(String(it.command ?? ""), 60)}`;
1011
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "shell", name: "shell", text: clip(String(it.command ?? "").replace(/^\/bin\/\w+ -lc /, ""), 240), ok: it.exit_code === 0 });
1012
+ } else if (it.type === "file_change") {
1013
+ const paths = (it.changes ?? []).map((c) => String(c.path));
1014
+ if (it.status !== "failed" && it.status !== "declined") edits.push(...paths);
1015
+ else usage.denied++;
1016
+ lastActivity = `edit ${oneLine(paths.join(", "), 60)}`;
1017
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "edit", name: "edit", text: clip(paths.join(", "), 240), ok: it.status !== "failed" && it.status !== "declined" });
1018
+ }
1019
+ break;
1020
+ }
381
1021
  case "turn.completed": {
382
1022
  const u = e.usage ?? {};
383
- const cached3 = num2(u.cached_input_tokens);
384
- const reasoning = num2(u.reasoning_output_tokens);
385
- usage.input += Math.max(0, num2(u.input_tokens) - cached3);
1023
+ const cached3 = num(u.cached_input_tokens);
1024
+ const reasoning = num(u.reasoning_output_tokens);
1025
+ usage.input += Math.max(0, num(u.input_tokens) - cached3);
386
1026
  usage.cacheRead += cached3;
387
- usage.cacheWrite += num2(u.cache_write_input_tokens);
388
- usage.output += Math.max(0, num2(u.output_tokens) - reasoning);
1027
+ usage.cacheWrite += num(u.cache_write_input_tokens);
1028
+ usage.output += Math.max(0, num(u.output_tokens) - reasoning);
389
1029
  usage.reasoning += reasoning;
390
- usage.total += num2(u.input_tokens) + num2(u.output_tokens);
1030
+ usage.total += num(u.input_tokens) + num(u.output_tokens);
391
1031
  break;
392
1032
  }
393
1033
  case "error":
@@ -411,21 +1051,13 @@ function describe(raw) {
411
1051
  return s;
412
1052
  }
413
1053
  }
414
- function num2(v) {
415
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
416
- }
417
- function oneLine2(s, max = 80) {
418
- const t = s.replace(/\s+/g, " ").trim();
419
- return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
420
- }
421
- var clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
422
1054
 
423
1055
  // src/backends/codex/index.ts
424
- var binary2 = () => findBinary("codex", "PITROOM_CODEX_BIN");
425
- var codexHome = () => process.env.CODEX_HOME ?? path3.join(os3.homedir(), ".codex");
1056
+ var binary3 = () => findBinary("codex", "PITROOM_CODEX_BIN");
1057
+ var codexHome = () => process.env.CODEX_HOME ?? path7.join(os5.homedir(), ".codex");
426
1058
  function cx(args, timeout = 6e4) {
427
- const { command, prefix } = resolveCommand(binary2());
428
- const r = spawnSync2(command, [...prefix, ...args], {
1059
+ const { command, prefix } = resolveCommand(binary3());
1060
+ const r = spawnSync3(command, [...prefix, ...args], {
429
1061
  encoding: "utf8",
430
1062
  timeout,
431
1063
  stdio: ["ignore", "pipe", "pipe"],
@@ -435,12 +1067,7 @@ function cx(args, timeout = 6e4) {
435
1067
  }
436
1068
  var SANDBOX = { read: "read-only", write: "workspace-write", isolate: "workspace-write" };
437
1069
  var toml = (s) => JSON.stringify(s);
438
- function splitModel(model) {
439
- if (!model) return {};
440
- const [m, effort] = model.split("#");
441
- return { model: m || void 0, effort: effort || void 0 };
442
- }
443
- function invocation2(req) {
1070
+ function invocation3(req) {
444
1071
  const { model, effort } = splitModel(req.model);
445
1072
  const args = ["exec"];
446
1073
  if (req.sessionId) args.push("resume", req.sessionId);
@@ -457,10 +1084,10 @@ function invocation2(req) {
457
1084
  if (model) args.push("--model", model);
458
1085
  if (effort) args.push("-c", `model_reasoning_effort=${toml(effort)}`);
459
1086
  args.push(req.prompt);
460
- const { command, prefix } = resolveCommand(binary2());
1087
+ const { command, prefix } = resolveCommand(binary3());
461
1088
  return { command, args: [...prefix, ...args], env: {} };
462
1089
  }
463
- function classify2(message) {
1090
+ function classify3(message) {
464
1091
  if (/not supported|model[^.]*(not found|does not exist|unknown|unavailable)|unsupported model|no such model/i.test(message)) {
465
1092
  return "model-unavailable";
466
1093
  }
@@ -468,37 +1095,37 @@ function classify2(message) {
468
1095
  if (/unauthori[sz]ed|\b40[13]\b|not logged in|log ?in|api.?key|credits?\b|billing|subscription/i.test(message)) return "auth";
469
1096
  return "other";
470
1097
  }
471
- function failure2(run, stderr, exitCode) {
1098
+ function failure3(run, stderr, exitCode) {
472
1099
  if (exitCode === 0 && !run.error) return void 0;
473
1100
  const detail = stderr.split("\n").find((l) => /^Error[: ]|ERROR/.test(l) && !/rmcp|models cache/.test(l));
474
1101
  let message = run.error ?? (detail ? detail.trim() : `codex exited with code ${exitCode}`);
475
1102
  if (/not supported when using Codex/i.test(message)) message += " (an outdated Codex CLI says this too: npm i -g @openai/codex@latest)";
476
- return { kind: classify2(message), message };
1103
+ return { kind: classify3(message), message };
477
1104
  }
478
1105
  function resolveModel(sessionId) {
479
- const root = path3.join(codexHome(), "sessions");
480
- const day = (d) => path3.join(root, String(d.getFullYear()), String(d.getMonth() + 1).padStart(2, "0"), String(d.getDate()).padStart(2, "0"));
1106
+ const root = path7.join(codexHome(), "sessions");
1107
+ const day = (d) => path7.join(root, String(d.getFullYear()), String(d.getMonth() + 1).padStart(2, "0"), String(d.getDate()).padStart(2, "0"));
481
1108
  const now = /* @__PURE__ */ new Date();
482
1109
  for (const dir of [day(now), day(new Date(now.getTime() - 864e5))]) {
483
1110
  let files = [];
484
1111
  try {
485
- files = fs3.readdirSync(dir).filter((f) => f.includes(sessionId));
1112
+ files = fs8.readdirSync(dir).filter((f) => f.includes(sessionId));
486
1113
  } catch {
487
1114
  continue;
488
1115
  }
489
1116
  for (const f of files) {
490
- const m = /"model":"([^"]+)"/.exec(fs3.readFileSync(path3.join(dir, f), "utf8"));
1117
+ const m = /"model":"([^"]+)"/.exec(fs8.readFileSync(path7.join(dir, f), "utf8"));
491
1118
  if (m) return m[1];
492
1119
  }
493
1120
  }
494
1121
  return void 0;
495
1122
  }
496
- function doctor2({ models, hasFallback }) {
1123
+ function doctor3({ models, hasFallback }) {
497
1124
  const version = cx(["--version"]);
498
1125
  if (version.missing || !version.ok) {
499
- return [{ level: "fail", message: `codex not runnable (${binary2()}). Install: npm i -g @openai/codex, or set PITROOM_CODEX_BIN` }];
1126
+ return [{ level: "fail", message: `codex not runnable (${binary3()}). Install: npm i -g @openai/codex, or set PITROOM_CODEX_BIN` }];
500
1127
  }
501
- const checks = [{ level: "ok", message: `${version.out.trim()} at ${binary2()}` }];
1128
+ const checks = [{ level: "ok", message: `${version.out.trim()} at ${binary3()}` }];
502
1129
  const login = cx(["login", "status"]);
503
1130
  checks.push(
504
1131
  login.ok && /logged in/i.test(login.out) ? { level: "ok", message: `Codex: ${login.out.trim().split("\n")[0]}` } : { level: "fail", message: "Codex is not logged in: run `codex login`" }
@@ -514,9 +1141,9 @@ function doctor2({ models, hasFallback }) {
514
1141
  return checks;
515
1142
  }
516
1143
  function catalog2() {
517
- const file = path3.join(codexHome(), "models_cache.json");
1144
+ const file = path7.join(codexHome(), "models_cache.json");
518
1145
  try {
519
- const raw = JSON.parse(fs3.readFileSync(file, "utf8"));
1146
+ const raw = JSON.parse(fs8.readFileSync(file, "utf8"));
520
1147
  const models = (raw.models ?? []).filter((m) => m.visibility !== "hide" && typeof m.slug === "string").map((m) => ({
521
1148
  id: m.slug,
522
1149
  efforts: (m.supported_reasoning_levels ?? []).map(
@@ -534,143 +1161,25 @@ var codex = {
534
1161
  name: "Codex",
535
1162
  catalog: catalog2,
536
1163
  capabilities: { readOnly: "os-sandbox", resume: "by-id", reportsCost: false, attachFiles: false },
537
- binary: binary2,
538
- invocation: invocation2,
539
- parse: parseEvents2,
540
- failure: failure2,
1164
+ binary: binary3,
1165
+ invocation: invocation3,
1166
+ parse: parseEvents3,
1167
+ failure: failure3,
541
1168
  resolveModel,
542
- doctor: doctor2
1169
+ doctor: doctor3
543
1170
  };
544
1171
 
545
1172
  // src/backends/gemini/index.ts
546
- import { spawnSync as spawnSync3 } from "node:child_process";
547
- import fs6 from "node:fs";
548
- import os5 from "node:os";
549
- import path5 from "node:path";
1173
+ import { spawnSync as spawnSync4 } from "node:child_process";
1174
+ import fs9 from "node:fs";
1175
+ import os6 from "node:os";
1176
+ import path8 from "node:path";
550
1177
  import { fileURLToPath } from "node:url";
551
1178
 
552
- // src/core/store.ts
553
- import fs5 from "node:fs";
554
- import os4 from "node:os";
555
- import path4 from "node:path";
556
- import crypto from "node:crypto";
557
-
558
- // src/core/fs-atomic.ts
559
- import fs4 from "node:fs";
560
- var BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
561
- function renameOver(tmp, file) {
562
- for (let attempt = 0; ; attempt++) {
563
- try {
564
- fs4.renameSync(tmp, file);
565
- return;
566
- } catch (e) {
567
- if (process.platform !== "win32" || !BUSY.has(e.code ?? "") || attempt >= 40) throw e;
568
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
569
- }
570
- }
571
- }
572
-
573
- // src/core/store.ts
574
- var TERMINAL = ["done", "failed", "timeout", "stopped"];
575
- var isActive = (s) => !TERMINAL.includes(s);
576
- function home() {
577
- if (process.env.PITROOM_HOME) return path4.resolve(process.env.PITROOM_HOME);
578
- if (process.platform === "win32") {
579
- return path4.join(process.env.LOCALAPPDATA ?? path4.join(os4.homedir(), "AppData", "Local"), "pitroom");
580
- }
581
- return path4.join(process.env.XDG_STATE_HOME ?? path4.join(os4.homedir(), ".local", "state"), "pitroom");
582
- }
583
- var runsDir = () => path4.join(home(), "runs");
584
- var ledgerFile = () => path4.join(home(), "ledger.jsonl");
585
- var runDir = (id) => path4.join(runsDir(), id);
586
- var runFile = (id, name) => path4.join(runDir(id), name);
587
- function newRunId(now = /* @__PURE__ */ new Date()) {
588
- const p = (n) => String(n).padStart(2, "0");
589
- const stamp2 = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
590
- return `${stamp2}-${crypto.randomBytes(2).toString("hex")}`;
591
- }
592
- var onFinished;
593
- var archive;
594
- function writeMeta(meta) {
595
- fs5.mkdirSync(runDir(meta.id), { recursive: true });
596
- const file = runFile(meta.id, "meta.json");
597
- const tmp = `${file}.${process.pid}.tmp`;
598
- fs5.writeFileSync(tmp, JSON.stringify(meta, null, 2));
599
- renameOver(tmp, file);
600
- if (TERMINAL.includes(meta.state)) {
601
- try {
602
- onFinished?.(meta);
603
- } catch {
604
- }
605
- }
606
- }
607
- function readMeta(id) {
608
- const file = runFile(id, "meta.json");
609
- if (!fs5.existsSync(file)) {
610
- const kept = archive?.meta(id);
611
- if (kept) return upgrade(kept);
612
- }
613
- return upgrade(JSON.parse(fs5.readFileSync(file, "utf8")));
614
- }
615
- function upgrade(raw) {
616
- if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
617
- if (!Array.isArray(raw.fallback)) {
618
- raw.fallback = (raw.fallbackModels ?? []).map((m) => ({ backend: "opencode", model: m }));
619
- }
620
- raw.warnings ??= [];
621
- raw.files ??= [];
622
- raw.link ??= [];
623
- return raw;
624
- }
625
- function listRunIds() {
626
- if (!fs5.existsSync(runsDir())) return [];
627
- return fs5.readdirSync(runsDir()).filter((d) => fs5.existsSync(runFile(d, "meta.json"))).sort();
628
- }
629
- function resolveRun(ref) {
630
- const ids = listRunIds();
631
- if (!ref || ref === "latest" || ref === "last") {
632
- if (!ids.length) throw new UserError("no runs yet");
633
- return ids[ids.length - 1];
634
- }
635
- if (ids.includes(ref)) return ref;
636
- const hits = ids.filter((id) => id.startsWith(ref) || id.endsWith(ref));
637
- if (hits.length === 1) return hits[0];
638
- if (!hits.length) {
639
- const kept = archive?.id(ref);
640
- if (kept) return kept;
641
- }
642
- throw new UserError(hits.length ? `ambiguous run "${ref}": ${hits.join(", ")}` : ids.length ? `unknown run "${ref}"` : "no runs yet");
643
- }
644
- function isAlive(pid) {
645
- if (!pid) return false;
646
- try {
647
- process.kill(pid, 0);
648
- return true;
649
- } catch (e) {
650
- return e.code === "EPERM";
651
- }
652
- }
653
- var STOP_FILE = "stop-requested";
654
- var requestStop = (id) => fs5.writeFileSync(runFile(id, STOP_FILE), "");
655
- function freshMeta(id) {
656
- const meta = readMeta(id);
657
- if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
658
- const latest = readMeta(id);
659
- if (TERMINAL.includes(latest.state)) return latest;
660
- const stopped = latest.stopRequested || fs5.existsSync(runFile(id, STOP_FILE));
661
- latest.state = stopped ? "stopped" : "failed";
662
- if (!stopped) latest.error ??= "worker process exited unexpectedly";
663
- latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
664
- writeMeta(latest);
665
- return latest;
666
- }
667
- return meta;
668
- }
669
-
670
1179
  // src/backends/gemini/events.ts
671
- var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["replace", "write_file", "edit"]);
672
- var DENIED2 = /policy|permission|not allowed|denied|blocked|disallowed|refus/i;
673
- function parseEvents3(jsonl) {
1180
+ var EDIT_TOOLS3 = /* @__PURE__ */ new Set(["replace", "write_file", "edit"]);
1181
+ var DENIED3 = /policy|permission|not allowed|denied|blocked|disallowed|refus/i;
1182
+ function parseEvents4(jsonl) {
674
1183
  const usage = {
675
1184
  input: 0,
676
1185
  output: 0,
@@ -700,8 +1209,8 @@ function parseEvents3(jsonl) {
700
1209
  text = "";
701
1210
  if (!said) return;
702
1211
  lastText = said;
703
- lastActivity = `says: ${oneLine3(said)}`;
704
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip3(said, 600) });
1212
+ lastActivity = `says: ${oneLine(said)}`;
1213
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(said, 600) });
705
1214
  };
706
1215
  const startTurn = () => {
707
1216
  if (!inTurn) turns++;
@@ -733,11 +1242,11 @@ function parseEvents3(jsonl) {
733
1242
  usage.toolCalls++;
734
1243
  tools[name] = (tools[name] ?? 0) + 1;
735
1244
  const target = p.file_path ?? p.absolute_path ?? p.path ?? p.dir_path ?? p.pattern ?? p.command ?? p.query ?? p.url ?? p.prompt;
736
- lastActivity = `${name} ${target ? oneLine3(String(target), 60) : ""}`.trim();
1245
+ lastActivity = `${name} ${target ? oneLine(String(target), 60) : ""}`.trim();
737
1246
  const file = p.file_path ?? p.absolute_path ?? p.path;
738
- if (EDIT_TOOLS2.has(name) && file) pendingEdits.set(String(e.tool_id), String(file));
1247
+ if (EDIT_TOOLS3.has(name) && file) pendingEdits.set(String(e.tool_id), String(file));
739
1248
  if (timeline.length < MAX_STEPS) {
740
- const step = { kind: EDIT_TOOLS2.has(name) ? "edit" : name === "run_shell_command" ? "shell" : "tool", name, text: clip3(String(target ?? ""), 240) };
1249
+ const step = { kind: EDIT_TOOLS3.has(name) ? "edit" : name === "run_shell_command" ? "shell" : "tool", name, text: clip(String(target ?? ""), 240) };
741
1250
  timeline.push(step);
742
1251
  stepOf.set(String(e.tool_id), step);
743
1252
  }
@@ -746,18 +1255,18 @@ function parseEvents3(jsonl) {
746
1255
  const ok = e.status === "success";
747
1256
  const step = stepOf.get(String(e.tool_id));
748
1257
  if (step) step.ok = ok;
749
- if (!ok && DENIED2.test(`${e.error?.type ?? ""} ${e.error?.message ?? ""}`)) usage.denied++;
1258
+ if (!ok && DENIED3.test(`${e.error?.type ?? ""} ${e.error?.message ?? ""}`)) usage.denied++;
750
1259
  if (ok && pendingEdits.has(String(e.tool_id))) edits.push(pendingEdits.get(String(e.tool_id)));
751
1260
  } else if (e.type === "error") {
752
1261
  if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
753
1262
  } else if (e.type === "result") {
754
1263
  const s = e.stats ?? {};
755
- const cached3 = num3(s.cached);
1264
+ const cached3 = num(s.cached);
756
1265
  usage.cacheRead += cached3;
757
- usage.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached3);
758
- usage.output += num3(s.output_tokens);
759
- usage.total += num3(s.total_tokens) || num3(s.input_tokens) + num3(s.output_tokens);
760
- usage.toolCalls = Math.max(usage.toolCalls, num3(s.tool_calls));
1266
+ usage.input += s.input !== void 0 ? num(s.input) : Math.max(0, num(s.input_tokens) - cached3);
1267
+ usage.output += num(s.output_tokens);
1268
+ usage.total += num(s.total_tokens) || num(s.input_tokens) + num(s.output_tokens);
1269
+ usage.toolCalls = Math.max(usage.toolCalls, num(s.tool_calls));
761
1270
  model ??= Object.keys(s.models ?? {})[0];
762
1271
  if (e.status === "error") error ??= String(e.error?.message ?? e.error?.type ?? "Gemini CLI error");
763
1272
  }
@@ -766,20 +1275,12 @@ function parseEvents3(jsonl) {
766
1275
  usage.steps = turns;
767
1276
  return { sessionId, model, finalText: error ? "" : lastText, usage, tools, edits, lastActivity, timeline, error };
768
1277
  }
769
- var clip3 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
770
- function num3(v) {
771
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
772
- }
773
- function oneLine3(s, max = 80) {
774
- const t = s.replace(/\s+/g, " ").trim();
775
- return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
776
- }
777
1278
 
778
1279
  // src/backends/gemini/index.ts
779
- var binary3 = () => findBinary("gemini", "PITROOM_GEMINI_BIN", ["~/.local/bin/gemini"]);
1280
+ var binary4 = () => findBinary("gemini", "PITROOM_GEMINI_BIN", ["~/.local/bin/gemini"]);
780
1281
  function gem(args, timeout = 6e4) {
781
- const { command, prefix } = resolveCommand(binary3());
782
- const r = spawnSync3(command, [...prefix, ...args], {
1282
+ const { command, prefix } = resolveCommand(binary4());
1283
+ const r = spawnSync4(command, [...prefix, ...args], {
783
1284
  encoding: "utf8",
784
1285
  timeout,
785
1286
  stdio: ["ignore", "pipe", "pipe"],
@@ -787,32 +1288,19 @@ function gem(args, timeout = 6e4) {
787
1288
  });
788
1289
  return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
789
1290
  }
790
- var policyDir = () => path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "..", "policies", "gemini");
791
- function workerHome() {
792
- const root = path5.join(home(), "gemini-home");
793
- const dir = path5.join(root, ".gemini");
794
- const base = JSON.parse(fs6.readFileSync(path5.join(policyDir(), "worker-settings.json"), "utf8"));
1291
+ var policyDir = () => path8.resolve(path8.dirname(fileURLToPath(import.meta.url)), "..", "policies", "gemini");
1292
+ function workerHome2() {
1293
+ const root = path8.join(home(), "gemini-home");
1294
+ const dir = path8.join(root, ".gemini");
1295
+ const base = JSON.parse(fs9.readFileSync(path8.join(policyDir(), "worker-settings.json"), "utf8"));
795
1296
  const auth = settings().security?.auth;
796
1297
  const body = `${JSON.stringify(auth ? { ...base, security: { ...base.security, auth } } : base, null, 2)}
797
1298
  `;
798
- const file = path5.join(dir, "settings.json");
799
- try {
800
- if (fs6.readFileSync(file, "utf8") === body) return root;
801
- } catch {
802
- }
803
- fs6.mkdirSync(dir, { recursive: true });
804
- fs6.writeFileSync(file, body);
805
- for (const name of ["oauth_creds.json", "google_accounts.json"]) {
806
- const from = path5.join(geminiHome(), name);
807
- const to = path5.join(dir, name);
808
- try {
809
- if (fs6.existsSync(from) && !fs6.existsSync(to)) fs6.symlinkSync(from, to);
810
- } catch {
811
- }
812
- }
1299
+ writeIfChanged(path8.join(dir, "settings.json"), body);
1300
+ for (const name of ["oauth_creds.json", "google_accounts.json"]) linkInto(dir, path8.join(geminiHome(), name));
813
1301
  return root;
814
1302
  }
815
- function invocation3(req) {
1303
+ function invocation4(req) {
816
1304
  const dir = policyDir();
817
1305
  const policies = ["base.toml", ...req.web ? [] : ["no-web.toml"], ...req.mode === "read" ? [] : ["shell.toml"]];
818
1306
  const args = [
@@ -826,18 +1314,18 @@ function invocation3(req) {
826
1314
  "-e",
827
1315
  "none"
828
1316
  ];
829
- for (const p of policies) args.push("--policy", path5.join(dir, p));
1317
+ for (const p of policies) args.push("--policy", path8.join(dir, p));
830
1318
  if (req.sessionId) args.push("--resume", req.sessionId);
831
1319
  if (process.env.PITROOM_GEMINI_TRUST === "1") args.push("--skip-trust");
832
1320
  const [model] = (req.model ?? "").split("#");
833
1321
  if (model) args.push("--model", model);
834
- const { command, prefix } = resolveCommand(binary3());
835
- const env = { GEMINI_CLI_HOME: workerHome() };
836
- const trusted = path5.join(geminiHome(), "trustedFolders.json");
837
- if (fs6.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
1322
+ const { command, prefix } = resolveCommand(binary4());
1323
+ const env = { GEMINI_CLI_HOME: workerHome2() };
1324
+ const trusted = path8.join(geminiHome(), "trustedFolders.json");
1325
+ if (fs9.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
838
1326
  return { command, args: [...prefix, ...args], env };
839
1327
  }
840
- function classify3(message) {
1328
+ function classify4(message) {
841
1329
  if (/IneligibleTier|no longer supported for Gemini|error authenticating|not logged in|please (log ?in|sign in)|credentials|api key|UNAUTHENTICATED|PERMISSION_DENIED|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
842
1330
  return "auth";
843
1331
  }
@@ -846,20 +1334,20 @@ function classify3(message) {
846
1334
  return "other";
847
1335
  }
848
1336
  var UNTRUSTED = "Gemini CLI does not trust this folder: open `gemini` in it once and trust it, or set PITROOM_GEMINI_TRUST=1 (the folder's own Gemini settings, hooks included, then load)";
849
- function failure3(run, stderr, exitCode) {
1337
+ function failure4(run, stderr, exitCode) {
850
1338
  if (exitCode === 0 && !run.error) return void 0;
851
1339
  const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
852
1340
  const detail = lines.find((l) => /error|failed|exceeded|quota|exhausted|unsupported/i.test(l) && !/^at /.test(l)) ?? lines.at(-1);
853
1341
  const raw = run.error ?? detail ?? `gemini exited with code ${exitCode}`;
854
1342
  const message = /not running in a trusted directory/i.test(`${stderr} ${raw}`) ? UNTRUSTED : raw;
855
- return { kind: classify3(message), message };
1343
+ return { kind: classify4(message), message };
856
1344
  }
857
1345
  function geminiHome() {
858
- return path5.join(process.env.GEMINI_CLI_HOME ?? os5.homedir(), ".gemini");
1346
+ return path8.join(process.env.GEMINI_CLI_HOME ?? os6.homedir(), ".gemini");
859
1347
  }
860
1348
  function settings() {
861
1349
  try {
862
- return JSON.parse(fs6.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
1350
+ return JSON.parse(fs9.readFileSync(path8.join(geminiHome(), "settings.json"), "utf8"));
863
1351
  } catch {
864
1352
  return {};
865
1353
  }
@@ -870,15 +1358,15 @@ function defaultModel2() {
870
1358
  const name = typeof m === "string" ? m : m?.name;
871
1359
  return typeof name === "string" ? name : void 0;
872
1360
  }
873
- function doctor3({ models, hasFallback }) {
1361
+ function doctor4({ models, hasFallback }) {
874
1362
  const version = gem(["--version"]);
875
1363
  if (version.missing || !version.ok) {
876
- return [{ level: "fail", message: `gemini not runnable (${binary3()}). Install Gemini CLI (npm i -g @google/gemini-cli), or set PITROOM_GEMINI_BIN` }];
1364
+ return [{ level: "fail", message: `gemini not runnable (${binary4()}). Install Gemini CLI (npm i -g @google/gemini-cli), or set PITROOM_GEMINI_BIN` }];
877
1365
  }
878
- const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary3()}` }];
1366
+ const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary4()}` }];
879
1367
  const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
880
1368
  const vertex = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true" || process.env.GOOGLE_GENAI_USE_VERTEXAI === "1";
881
- const signedIn = fs6.existsSync(path5.join(geminiHome(), "oauth_creds.json"));
1369
+ const signedIn = fs9.existsSync(path8.join(geminiHome(), "oauth_creds.json"));
882
1370
  const type = settings().security?.auth?.selectedType;
883
1371
  if (key || vertex) {
884
1372
  checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
@@ -907,7 +1395,7 @@ function doctor3({ models, hasFallback }) {
907
1395
  }
908
1396
  function catalog3() {
909
1397
  const def = defaultModel2();
910
- const models = ["gemini-3.8-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3-flash-preview"].map((id) => ({ id }));
1398
+ const models = ["gemini-3.8-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3-flash-preview"].map((id2) => ({ id: id2 }));
911
1399
  if (def && !models.some((m) => m.id === def)) models.push({ id: def });
912
1400
  return { models, source: "Gemini CLI's built-in model names (it cannot list models); the effort suffix is not used" };
913
1401
  }
@@ -915,22 +1403,26 @@ var gemini = {
915
1403
  id: "gemini",
916
1404
  name: "Gemini CLI",
917
1405
  capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
918
- binary: binary3,
1406
+ binary: binary4,
919
1407
  catalog: catalog3,
920
- invocation: invocation3,
921
- parse: parseEvents3,
922
- failure: failure3,
1408
+ invocation: invocation4,
1409
+ parse: parseEvents4,
1410
+ failure: failure4,
923
1411
  defaultModel: defaultModel2,
924
- doctor: doctor3
1412
+ doctor: doctor4
925
1413
  };
926
1414
 
1415
+ // src/backends/kilo/index.ts
1416
+ import { spawnSync as spawnSync6 } from "node:child_process";
1417
+ import path10 from "node:path";
1418
+
927
1419
  // src/backends/opencode/index.ts
928
- import { spawnSync as spawnSync4 } from "node:child_process";
1420
+ import { spawnSync as spawnSync5 } from "node:child_process";
929
1421
 
930
1422
  // src/backends/opencode/events.ts
931
- var EDIT_TOOLS3 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit", "apply_patch"]);
932
- var DENIED3 = /rule which prevents you|permission denied|permission\.rejected/i;
933
- function parseEvents4(ndjson) {
1423
+ var EDIT_TOOLS4 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit", "apply_patch"]);
1424
+ var DENIED4 = /rule which prevents you|permission denied|permission\.rejected/i;
1425
+ function parseEvents5(ndjson) {
934
1426
  const usage = {
935
1427
  input: 0,
936
1428
  output: 0,
@@ -967,8 +1459,8 @@ function parseEvents4(ndjson) {
967
1459
  const key = String(p.messageID ?? "");
968
1460
  if (!textByMessage.has(key)) textByMessage.set(key, []);
969
1461
  textByMessage.get(key).push(text);
970
- lastActivity = `says: ${oneLine4(text)}`;
971
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip4(text.trim(), 600), at: stamp(e.timestamp) });
1462
+ lastActivity = `says: ${oneLine(text)}`;
1463
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(text.trim(), 600), at: stamp(e.timestamp) });
972
1464
  break;
973
1465
  }
974
1466
  case "tool_use": {
@@ -976,28 +1468,28 @@ function parseEvents4(ndjson) {
976
1468
  const st = p.state ?? {};
977
1469
  usage.toolCalls++;
978
1470
  tools[name] = (tools[name] ?? 0) + 1;
979
- if (st.status === "error" && DENIED3.test(String(st.error ?? ""))) usage.denied++;
980
- if (st.status === "completed" && EDIT_TOOLS3.has(name)) {
1471
+ if (st.status === "error" && DENIED4.test(String(st.error ?? ""))) usage.denied++;
1472
+ if (st.status === "completed" && EDIT_TOOLS4.has(name)) {
981
1473
  edits.push(String(st.input?.filePath ?? st.input?.path ?? name));
982
1474
  }
983
1475
  lastActivity = `${name} ${describe2(st.input)}`.trim();
984
1476
  if (timeline.length < MAX_STEPS) {
985
- const kind = EDIT_TOOLS3.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
986
- const text = kind === "shell" ? String(st.input?.command ?? "") : String(st.input?.filePath ?? st.input?.path ?? st.input?.pattern ?? st.input?.url ?? "") || describe2(st.input);
987
- timeline.push({ kind, name, text: clip4(text, 240), ok: st.status === "completed" ? true : st.status === "error" ? false : void 0, at: stamp(e.timestamp) });
1477
+ const kind2 = EDIT_TOOLS4.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
1478
+ const text = kind2 === "shell" ? String(st.input?.command ?? "") : String(st.input?.filePath ?? st.input?.path ?? st.input?.pattern ?? st.input?.url ?? "") || describe2(st.input);
1479
+ timeline.push({ kind: kind2, name, text: clip(text, 240), ok: st.status === "completed" ? true : st.status === "error" ? false : void 0, at: stamp(e.timestamp) });
988
1480
  }
989
1481
  break;
990
1482
  }
991
1483
  case "step_finish": {
992
1484
  const t = p.tokens ?? {};
993
1485
  usage.steps++;
994
- usage.input += num4(t.input);
995
- usage.output += num4(t.output);
996
- usage.reasoning += num4(t.reasoning);
997
- usage.cacheRead += num4(t.cache?.read);
998
- usage.cacheWrite += num4(t.cache?.write);
999
- usage.total += num4(t.total) || num4(t.input) + num4(t.output) + num4(t.reasoning) + num4(t.cache?.read);
1000
- usage.cost = (usage.cost ?? 0) + num4(p.cost);
1486
+ usage.input += num(t.input);
1487
+ usage.output += num(t.output);
1488
+ usage.reasoning += num(t.reasoning);
1489
+ usage.cacheRead += num(t.cache?.read);
1490
+ usage.cacheWrite += num(t.cache?.write);
1491
+ usage.total += num(t.total) || num(t.input) + num(t.output) + num(t.reasoning) + num(t.cache?.read);
1492
+ usage.cost = (usage.cost ?? 0) + num(p.cost);
1001
1493
  break;
1002
1494
  }
1003
1495
  case "error": {
@@ -1021,25 +1513,17 @@ function describeError(err) {
1021
1513
  const tags = [err.type, inner, err.status && `HTTP ${err.status}`].filter(Boolean);
1022
1514
  return tags.length ? `${message} [${tags.join(", ")}]` : message;
1023
1515
  }
1024
- var clip4 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1025
1516
  var stamp = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
1026
- function num4(v) {
1027
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
1028
- }
1029
- function oneLine4(s, max = 80) {
1030
- const t = s.replace(/\s+/g, " ").trim();
1031
- return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
1032
- }
1033
1517
  function describe2(input) {
1034
1518
  if (!input || typeof input !== "object") return "";
1035
1519
  const v = input.filePath ?? input.path ?? input.pattern ?? input.command ?? input.url ?? input.query;
1036
- return v ? oneLine4(String(v), 60) : "";
1520
+ return v ? oneLine(String(v), 60) : "";
1037
1521
  }
1038
1522
 
1039
1523
  // src/backends/opencode/profiles.ts
1040
- import fs7 from "node:fs";
1041
- import os6 from "node:os";
1042
- import path6 from "node:path";
1524
+ import fs10 from "node:fs";
1525
+ import os7 from "node:os";
1526
+ import path9 from "node:path";
1043
1527
  var AGENT = { read: "pitroom-read", write: "pitroom-write" };
1044
1528
  var READ_FILES = {
1045
1529
  "*": "allow",
@@ -1133,22 +1617,25 @@ var FORBIDDEN_BASH = [
1133
1617
  "twine upload*",
1134
1618
  // no recursive delegation
1135
1619
  "opencode*",
1620
+ "kilo *",
1621
+ "kilocode*",
1622
+ "agy *",
1136
1623
  "pitroom *",
1137
1624
  "*pitroom*"
1138
1625
  ];
1139
- function scratchDirs() {
1140
- const data = process.env.XDG_DATA_HOME ?? path6.join(os6.homedir(), ".local", "share");
1626
+ function scratchDirs(app) {
1627
+ const data = process.env.XDG_DATA_HOME ?? path9.join(os7.homedir(), ".local", "share");
1141
1628
  const rules = {
1142
1629
  "*": "deny",
1143
- [path6.join(data, "opencode", "tool-output", "*")]: "allow",
1144
- [path6.join(data, "opencode", "shell", "*", "*")]: "allow"
1630
+ [path9.join(data, app, "tool-output", "*")]: "allow",
1631
+ [path9.join(data, app, "shell", "*", "*")]: "allow"
1145
1632
  };
1146
- for (const tmp of /* @__PURE__ */ new Set([os6.tmpdir(), realpath(os6.tmpdir())])) rules[path6.join(tmp, "opencode", "*")] = "allow";
1633
+ for (const tmp of /* @__PURE__ */ new Set([os7.tmpdir(), realpath(os7.tmpdir())])) rules[path9.join(tmp, app, "*")] = "allow";
1147
1634
  return rules;
1148
1635
  }
1149
1636
  function realpath(p) {
1150
1637
  try {
1151
- return fs7.realpathSync(p);
1638
+ return fs10.realpathSync(p);
1152
1639
  } catch {
1153
1640
  return p;
1154
1641
  }
@@ -1164,7 +1651,7 @@ var COMMON = {
1164
1651
  doom_loop: "deny"
1165
1652
  };
1166
1653
  var web = (on) => ({ webfetch: on ? "allow" : "deny", websearch: on ? "allow" : "deny" });
1167
- function readProfile(allowWeb = false) {
1654
+ function readProfile(allowWeb = false, app = "opencode") {
1168
1655
  return {
1169
1656
  "*": "deny",
1170
1657
  read: READ_FILES,
@@ -1180,11 +1667,11 @@ function readProfile(allowWeb = false) {
1180
1667
  bash: READ_ONLY_BASH,
1181
1668
  shell: READ_ONLY_BASH,
1182
1669
  edit: "deny",
1183
- external_directory: scratchDirs(),
1670
+ external_directory: scratchDirs(app),
1184
1671
  ...COMMON
1185
1672
  };
1186
1673
  }
1187
- function writeProfile(allowWeb = false) {
1674
+ function writeProfile(allowWeb = false, app = "opencode") {
1188
1675
  const bash = { "*": "allow" };
1189
1676
  for (const pattern of FORBIDDEN_BASH) bash[pattern] = "deny";
1190
1677
  return {
@@ -1193,25 +1680,26 @@ function writeProfile(allowWeb = false) {
1193
1680
  ...web(allowWeb),
1194
1681
  bash,
1195
1682
  shell: bash,
1196
- external_directory: scratchDirs(),
1683
+ external_directory: scratchDirs(app),
1197
1684
  ...COMMON
1198
1685
  };
1199
1686
  }
1200
1687
  function agentFor(mode) {
1201
1688
  return mode === "read" ? AGENT.read : AGENT.write;
1202
1689
  }
1203
- function configContent(existing, allowWeb = false) {
1690
+ function configContent(existing, allowWeb = false, app = "opencode", extra = {}) {
1204
1691
  const ours = {
1692
+ ...extra,
1205
1693
  agent: {
1206
1694
  [AGENT.read]: {
1207
1695
  mode: "primary",
1208
1696
  description: "pitroom: read-only worker (research, search, review)",
1209
- permission: readProfile(allowWeb)
1697
+ permission: readProfile(allowWeb, app)
1210
1698
  },
1211
1699
  [AGENT.write]: {
1212
1700
  mode: "primary",
1213
1701
  description: "pitroom: editing worker (no git history changes, no bulk deletes)",
1214
- permission: writeProfile(allowWeb)
1702
+ permission: writeProfile(allowWeb, app)
1215
1703
  }
1216
1704
  }
1217
1705
  };
@@ -1236,10 +1724,10 @@ function isObject(v) {
1236
1724
  }
1237
1725
 
1238
1726
  // src/backends/opencode/index.ts
1239
- var binary4 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
1727
+ var binary5 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
1240
1728
  function oc(args, opts = {}) {
1241
- const { command, prefix } = resolveCommand(binary4());
1242
- const r = spawnSync4(command, [...prefix, ...args], {
1729
+ const { command, prefix } = resolveCommand(binary5());
1730
+ const r = spawnSync5(command, [...prefix, ...args], {
1243
1731
  encoding: "utf8",
1244
1732
  timeout: opts.timeout ?? 6e4,
1245
1733
  env: opts.env ?? process.env,
@@ -1249,7 +1737,7 @@ function oc(args, opts = {}) {
1249
1737
  return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
1250
1738
  }
1251
1739
  var jsonIn = (s) => JSON.parse(s.slice(s.indexOf("{")));
1252
- function invocation4(req) {
1740
+ function invocation5(req) {
1253
1741
  const args = [
1254
1742
  "run",
1255
1743
  "--standalone",
@@ -1268,7 +1756,7 @@ function invocation4(req) {
1268
1756
  if (req.sessionId) args.push("--session", req.sessionId);
1269
1757
  for (const f of req.files) args.push("--file", f);
1270
1758
  args.push(req.prompt);
1271
- const { command, prefix } = resolveCommand(binary4());
1759
+ const { command, prefix } = resolveCommand(binary5());
1272
1760
  return {
1273
1761
  command,
1274
1762
  args: [...prefix, ...args],
@@ -1283,7 +1771,7 @@ function majorVersion(output) {
1283
1771
  const m = /(\d+)\.\d+\.\d+/.exec(output);
1284
1772
  return m ? Number(m[1]) : void 0;
1285
1773
  }
1286
- function classify4(message) {
1774
+ function classify5(message) {
1287
1775
  if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
1288
1776
  if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
1289
1777
  if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
@@ -1291,17 +1779,17 @@ function classify4(message) {
1291
1779
  }
1292
1780
  return "other";
1293
1781
  }
1294
- function failure4(run, stderr, exitCode) {
1782
+ function failure5(run, stderr, exitCode) {
1295
1783
  if (exitCode === 0 && !run.error) return void 0;
1296
1784
  const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
1297
1785
  const raw = run.error ?? `opencode exited with code ${exitCode}`;
1298
1786
  const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
1299
- return { kind: classify4(message), message };
1787
+ return { kind: classify5(message), message };
1300
1788
  }
1301
1789
  function modelId(m) {
1302
1790
  if (typeof m === "string") return m;
1303
- const id = m?.model ?? m?.id;
1304
- return m?.providerID && id ? `${m.providerID}/${id}` : void 0;
1791
+ const id2 = m?.model ?? m?.id;
1792
+ return m?.providerID && id2 ? `${m.providerID}/${id2}` : void 0;
1305
1793
  }
1306
1794
  function defaultModel3() {
1307
1795
  try {
@@ -1331,25 +1819,25 @@ function resolveModel2(sessionId) {
1331
1819
  function catalog4() {
1332
1820
  const ids = listModels();
1333
1821
  return {
1334
- models: (ids.length ? ids : listModels()).map((id) => ({ id })),
1822
+ models: (ids.length ? ids : listModels()).map((id2) => ({ id: id2 })),
1335
1823
  source: "`opencode models` (reasoning variants are provider-specific: provider/model#variant)"
1336
1824
  };
1337
1825
  }
1338
1826
  function listModels() {
1339
1827
  return oc(["models"]).out.split("\n").map((s) => s.trim()).filter(Boolean);
1340
1828
  }
1341
- function doctor4({ models, hasFallback }) {
1829
+ function doctor5({ models, hasFallback }) {
1342
1830
  const checks = [];
1343
1831
  const version = oc(["--version"]);
1344
1832
  if (version.missing || !version.ok) {
1345
- return [{ level: "fail", message: `opencode not runnable (${binary4()}). Install: https://opencode.ai or set PITROOM_OPENCODE_BIN` }];
1833
+ return [{ level: "fail", message: `opencode not runnable (${binary5()}). Install: https://opencode.ai or set PITROOM_OPENCODE_BIN` }];
1346
1834
  }
1347
1835
  const v = version.out.trim().replace(/^opencode\s+/i, "");
1348
1836
  const major = majorVersion(v);
1349
1837
  if (major !== void 0 && major < 2) {
1350
1838
  return [{ level: "fail", message: `OpenCode ${v} is too old: Pitroom needs OpenCode v2 or newer (run \`opencode upgrade\`)` }];
1351
1839
  }
1352
- checks.push({ level: "ok", message: `opencode ${v} at ${binary4()} (private --standalone server per run)` });
1840
+ checks.push({ level: "ok", message: `opencode ${v} at ${binary5()} (private --standalone server per run)` });
1353
1841
  const known = new Set(listModels());
1354
1842
  const def = defaultModel3();
1355
1843
  for (const m of models) {
@@ -1377,79 +1865,371 @@ var opencode = {
1377
1865
  id: "opencode",
1378
1866
  name: "OpenCode",
1379
1867
  capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
1380
- binary: binary4,
1381
- invocation: invocation4,
1382
- parse: parseEvents4,
1383
- failure: failure4,
1868
+ binary: binary5,
1869
+ invocation: invocation5,
1870
+ parse: parseEvents5,
1871
+ failure: failure5,
1384
1872
  defaultModel: defaultModel3,
1385
1873
  resolveModel: resolveModel2,
1386
1874
  listModels,
1387
1875
  catalog: catalog4,
1388
- doctor: doctor4
1876
+ doctor: doctor5
1389
1877
  };
1390
1878
 
1391
- // src/backends/index.ts
1392
- var REGISTRY = new Map([opencode, codex, claude, gemini].map((b) => [b.id, b]));
1393
- var DEFAULT_BACKEND = opencode.id;
1394
- var PLANNED = [];
1395
- var backendIds = () => [...REGISTRY.keys()];
1396
- var allBackends = () => [...REGISTRY.values()];
1397
- var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
1398
- function getBackend(id) {
1399
- const backend = REGISTRY.get(id);
1400
- if (backend) return backend;
1401
- const available = backendIds().join(", ");
1402
- throw new UserError(
1403
- PLANNED.includes(id) ? `the "${id}" worker is not supported yet (available: ${available})` : `unknown worker "${id}" (available: ${available})`,
1404
- 3
1405
- );
1879
+ // src/backends/kilo/index.ts
1880
+ var binary6 = () => findBinary("kilo", "PITROOM_KILO_BIN", ["~/.kilo/bin/kilo"]);
1881
+ function kilo(args, timeout = 6e4) {
1882
+ const { command, prefix } = resolveCommand(binary6());
1883
+ const r = spawnSync6(command, [...prefix, ...args], { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
1884
+ return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
1406
1885
  }
1407
-
1408
- // src/core/target.ts
1409
- function parseTarget(spec, defaultBackend) {
1410
- const s = spec.trim();
1411
- if (!s) throw new UserError("empty worker target");
1412
- const i = s.indexOf(":");
1413
- if (i > 0 && isBackendId(s.slice(0, i))) {
1414
- const model = s.slice(i + 1).trim();
1415
- return model ? { backend: s.slice(0, i), model } : { backend: s.slice(0, i) };
1886
+ function invocation6(req) {
1887
+ const args = [
1888
+ "run",
1889
+ "--agent",
1890
+ agentFor(req.mode),
1891
+ "--format",
1892
+ "json",
1893
+ "--title",
1894
+ req.title,
1895
+ // Some failure causes only appear in the server logs (Kilo takes the level in capitals only).
1896
+ "--print-logs",
1897
+ "--log-level",
1898
+ "ERROR"
1899
+ ];
1900
+ if (req.model) args.push("--model", req.model);
1901
+ if (req.sessionId) args.push("--session", req.sessionId);
1902
+ for (const f of req.files) args.push("--file", f);
1903
+ args.push(req.prompt);
1904
+ const { command, prefix } = resolveCommand(binary6());
1905
+ return {
1906
+ command,
1907
+ args: [...prefix, ...args],
1908
+ env: {
1909
+ // A worker run must not upgrade Kilo underneath the user.
1910
+ KILO_CONFIG_CONTENT: configContent(process.env.KILO_CONFIG_CONTENT, req.web, "kilo", { autoupdate: false }),
1911
+ // No daemon to attach to: a private server per run (see above).
1912
+ XDG_STATE_HOME: path10.join(home(), "kilo-state")
1913
+ }
1914
+ };
1915
+ }
1916
+ function failure6(run, stderr, exitCode) {
1917
+ if (exitCode === 0 && !run.error) return void 0;
1918
+ const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
1919
+ const raw = run.error ?? `kilo exited with code ${exitCode}`;
1920
+ const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
1921
+ return { kind: classify5(message), message };
1922
+ }
1923
+ function listModels2() {
1924
+ return kilo(["models"]).out.split("\n").map((s) => s.trim()).filter((s) => /^[\w.-]+\/\S+$/.test(s));
1925
+ }
1926
+ function defaultModel4() {
1927
+ try {
1928
+ const out = kilo(["debug", "config"], 3e4).out;
1929
+ const model = JSON.parse(out.slice(out.indexOf("{"))).model;
1930
+ return typeof model === "string" ? model : void 0;
1931
+ } catch {
1932
+ return void 0;
1416
1933
  }
1417
- if (isBackendId(s)) return { backend: s };
1418
- return { backend: defaultBackend, model: s };
1419
1934
  }
1420
- var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
1421
- var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
1422
-
1423
- // src/core/refs.ts
1424
- import fs8 from "node:fs";
1425
- import path7 from "node:path";
1426
- var FILE_EXTENSIONS = new Set(
1427
- "ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
1428
- );
1429
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
1430
- var MAX_INDEXED = 6e4;
1431
- var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
1432
- var REF = new RegExp(
1433
- String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
1434
- "g"
1435
- );
1436
- var IDENT = /`([A-Za-z_$][\w$]*)(?:\([^`]*\))?`/g;
1437
- var CLAUSE = /[;|—]|\.\s|:\s/g;
1438
- var MAX_BYTES = 5 * 1024 * 1024;
1439
- var WINDOW = 5;
1440
- var DEFINITION_LOOKBACK = 80;
1441
- function looksLikeSymbol(m) {
1442
- const name = m[1];
1443
- return m[0].includes("(") || /[A-Z_$0-9]/.test(name) || name.length >= 5;
1935
+ function catalog5() {
1936
+ return { models: listModels2().map((id2) => ({ id: id2 })), source: "`kilo models` (reasoning variants are provider-specific: provider/model#variant)" };
1444
1937
  }
1445
- function extractRefs(answer) {
1446
- const seen = /* @__PURE__ */ new Map();
1447
- for (const line of answer.split("\n")) {
1448
- const matches2 = [...line.matchAll(REF)];
1449
- const seps = [...line.matchAll(CLAUSE)].map((s) => s.index ?? 0);
1450
- const sepBetween = (a, b) => seps.some((s) => s >= a && s < b);
1451
- const idents = [...line.matchAll(IDENT)].filter((i) => {
1452
- if (!looksLikeSymbol(i)) return false;
1938
+ function doctor6({ models, hasFallback }) {
1939
+ const version = kilo(["--version"]);
1940
+ if (version.missing || !version.ok) {
1941
+ return [{ level: "fail", message: `kilo not runnable (${binary6()}). Install Kilo Code's CLI (npm i -g @kilocode/cli), or set PITROOM_KILO_BIN` }];
1942
+ }
1943
+ const checks = [{ level: "ok", message: `Kilo Code ${version.out.trim().split("\n").pop()} at ${binary6()} (beta worker: checked against 7.8)` }];
1944
+ const def = defaultModel4();
1945
+ for (const m of models) {
1946
+ const model = m ?? def;
1947
+ checks.push(model ? { level: "ok", message: `Kilo Code ${m ? "model" : "default model"}: ${model}` } : { level: "warn", message: 'Kilo Code has no default model: pass -W kilo:<provider/model> or set "model" in its config' });
1948
+ }
1949
+ if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Kilo Code runs" });
1950
+ checks.push({ level: "ok", message: `permission profiles ${AGENT.read}/${AGENT.write} injected per run through KILO_CONFIG_CONTENT` });
1951
+ return checks;
1952
+ }
1953
+ var kiloBackend = {
1954
+ id: "kilo",
1955
+ name: "Kilo Code",
1956
+ capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
1957
+ binary: binary6,
1958
+ invocation: invocation6,
1959
+ parse: parseEvents5,
1960
+ failure: failure6,
1961
+ defaultModel: defaultModel4,
1962
+ listModels: listModels2,
1963
+ catalog: catalog5,
1964
+ doctor: doctor6
1965
+ };
1966
+
1967
+ // src/backends/qwen/index.ts
1968
+ import { spawnSync as spawnSync7 } from "node:child_process";
1969
+ import fs11 from "node:fs";
1970
+ import os8 from "node:os";
1971
+ import path11 from "node:path";
1972
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1973
+
1974
+ // src/backends/qwen/events.ts
1975
+ var EDIT_TOOLS5 = /* @__PURE__ */ new Set(["write_file", "edit", "notebook_edit"]);
1976
+ var SHELL2 = "run_shell_command";
1977
+ var DENIED5 = /permission was declined|denied by permission rules|requires permission|not allowed/i;
1978
+ var API_ERROR = /^\[API Error: /;
1979
+ function parseEvents6(jsonl) {
1980
+ const usage = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0, steps: 0, toolCalls: 0, denied: 0 };
1981
+ const tools = {};
1982
+ const edits = [];
1983
+ const pendingEdits = /* @__PURE__ */ new Map();
1984
+ const timeline = [];
1985
+ const stepOf = /* @__PURE__ */ new Map();
1986
+ let sessionId;
1987
+ let model;
1988
+ let lastText = "";
1989
+ let finalText;
1990
+ let lastActivity;
1991
+ let error;
1992
+ for (const line of jsonl.split("\n")) {
1993
+ if (!line.trim()) continue;
1994
+ let e;
1995
+ try {
1996
+ e = JSON.parse(line);
1997
+ } catch {
1998
+ continue;
1999
+ }
2000
+ sessionId ??= e.session_id;
2001
+ if (e.type === "system" && e.subtype === "init") {
2002
+ model = e.model;
2003
+ } else if (e.type === "system" && e.subtype === "retry") {
2004
+ lastActivity = `waiting to retry (attempt ${e.data?.attempt ?? "?"} of ${e.data?.maxRetries ?? "?"})`;
2005
+ } else if (e.type === "assistant") {
2006
+ const content = e.message?.content ?? [];
2007
+ const text = content.filter((c) => c.type === "text").map((c) => String(c.text ?? "")).join(" ").trim();
2008
+ if (API_ERROR.test(text)) {
2009
+ error = apiError(text);
2010
+ continue;
2011
+ }
2012
+ usage.steps++;
2013
+ for (const c of content) {
2014
+ if (c.type === "text" && String(c.text ?? "").trim()) {
2015
+ lastText = String(c.text).trim();
2016
+ lastActivity = `says: ${oneLine(lastText)}`;
2017
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(lastText, 600) });
2018
+ } else if (c.type === "tool_use") {
2019
+ usage.toolCalls++;
2020
+ tools[c.name] = (tools[c.name] ?? 0) + 1;
2021
+ const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
2022
+ lastActivity = `${c.name} ${target ? oneLine(String(target), 60) : ""}`.trim();
2023
+ if (EDIT_TOOLS5.has(c.name) && c.input?.file_path) pendingEdits.set(String(c.id), String(c.input.file_path));
2024
+ if (timeline.length < MAX_STEPS) {
2025
+ const step = { kind: EDIT_TOOLS5.has(c.name) ? "edit" : c.name === SHELL2 ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
2026
+ timeline.push(step);
2027
+ stepOf.set(String(c.id), step);
2028
+ }
2029
+ }
2030
+ }
2031
+ } else if (e.type === "user") {
2032
+ for (const c of e.message?.content ?? []) {
2033
+ if (c?.type !== "tool_result") continue;
2034
+ const text = typeof c.content === "string" ? c.content : JSON.stringify(c.content ?? "");
2035
+ const step = stepOf.get(String(c.tool_use_id));
2036
+ if (step) step.ok = !c.is_error;
2037
+ if (c.is_error) {
2038
+ if (DENIED5.test(text)) usage.denied++;
2039
+ } else if (pendingEdits.has(String(c.tool_use_id))) {
2040
+ edits.push(pendingEdits.get(String(c.tool_use_id)));
2041
+ }
2042
+ }
2043
+ } else if (e.type === "result") {
2044
+ const u = e.usage ?? {};
2045
+ usage.input += Math.max(0, num(u.input_tokens) - num(u.cache_read_input_tokens));
2046
+ usage.cacheRead += num(u.cache_read_input_tokens);
2047
+ usage.output += num(u.output_tokens);
2048
+ usage.total += num(u.total_tokens) || num(u.input_tokens) + num(u.output_tokens);
2049
+ usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
2050
+ if (e.is_error) {
2051
+ error ??= apiError(String(e.error?.message ?? e.result ?? e.subtype ?? "Qwen Code error"));
2052
+ } else if (typeof e.result === "string") {
2053
+ finalText = e.result.trim();
2054
+ }
2055
+ }
2056
+ }
2057
+ return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
2058
+ }
2059
+ var apiError = (text) => text.replace(API_ERROR, "").replace(/\](?=\s*(\n|$))/, "").replace(/\s+/g, " ").trim() || text;
2060
+
2061
+ // src/backends/qwen/index.ts
2062
+ var binary7 = () => findBinary("qwen", "PITROOM_QWEN_BIN", ["~/.local/bin/qwen", "~/.npm-global/bin/qwen"]);
2063
+ var policyDir2 = () => path11.resolve(path11.dirname(fileURLToPath2(import.meta.url)), "..", "policies", "qwen");
2064
+ var EXCLUDED = [
2065
+ "agent",
2066
+ "send_message",
2067
+ "skill",
2068
+ "manage_memory",
2069
+ "search_memory",
2070
+ "enter_worktree",
2071
+ "exit_worktree",
2072
+ "record_artifact",
2073
+ "cron_create",
2074
+ "cron_list",
2075
+ "cron_delete",
2076
+ "loop_wakeup",
2077
+ "task_stop",
2078
+ "update_goal",
2079
+ "get_goal",
2080
+ "list_agents",
2081
+ "report_findings",
2082
+ "tool_call",
2083
+ "tool_search",
2084
+ "read_mcp_resource"
2085
+ ];
2086
+ var WEB = ["web_fetch", "web_search"];
2087
+ var EDITS_HERE = ["Edit(./**)", "write_file(./**)", "notebook_edit(./**)"];
2088
+ var DENY_SHELL = FORBIDDEN_COMMANDS.map((c) => `run_shell_command(${c})`);
2089
+ function invocation7(req) {
2090
+ const read = req.mode === "read";
2091
+ const exclude = [...EXCLUDED, ...req.web ? [] : WEB, ...read ? [] : DENY_SHELL];
2092
+ const args = [
2093
+ // The prompt is the first word: the list options below take every word that follows them, and Qwen Code reads
2094
+ // no prompt after "--". One that starts with "-" would be read as an option: a leading space keeps it the prompt.
2095
+ req.prompt.startsWith("-") ? ` ${req.prompt}` : req.prompt,
2096
+ "--safe-mode",
2097
+ "--output-format",
2098
+ "stream-json",
2099
+ "--approval-mode",
2100
+ read ? "plan" : "default",
2101
+ "--exclude-tools",
2102
+ ...exclude
2103
+ ];
2104
+ if (!read) args.push("--allowed-tools", ...EDITS_HERE, "run_shell_command");
2105
+ const model = (req.model ?? "").split("#")[0];
2106
+ if (model) args.push("--model", model);
2107
+ if (req.sessionId) args.push("--resume", req.sessionId);
2108
+ const { command, prefix } = resolveCommand(binary7());
2109
+ return {
2110
+ command,
2111
+ args: [...prefix, ...args],
2112
+ env: {
2113
+ QWEN_CODE_SYSTEM_DEFAULTS_PATH: path11.join(policyDir2(), "worker-defaults.json"),
2114
+ QWEN_CODE_DISABLE_CRON: "1",
2115
+ QWEN_DISABLE_AUTO_TITLE: "1"
2116
+ }
2117
+ };
2118
+ }
2119
+ function classify6(message) {
2120
+ if (/\b40[13]\b|incorrect api key|invalid api key|unauthori[sz]ed|authenticat|not logged in|no auth/i.test(message)) return "auth";
2121
+ if (/\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
2122
+ if (/model[^.]*(not found|does not exist|not available|not supported)|invalid model|model_not_found|\b404\b/i.test(message)) return "model-unavailable";
2123
+ return "other";
2124
+ }
2125
+ function failure7(run, stderr, exitCode) {
2126
+ if (exitCode === 0 && !run.error) return void 0;
2127
+ const detail = stderr.trim().split("\n").filter((l) => l.trim() && !/SAFE MODE/.test(l)).pop();
2128
+ const message = run.error ?? detail ?? `qwen exited with code ${exitCode}`;
2129
+ return { kind: classify6(message), message };
2130
+ }
2131
+ function userSettings() {
2132
+ try {
2133
+ return JSON.parse(fs11.readFileSync(path11.join(process.env.QWEN_HOME ?? path11.join(os8.homedir(), ".qwen"), "settings.json"), "utf8"));
2134
+ } catch {
2135
+ return {};
2136
+ }
2137
+ }
2138
+ function defaultModel5() {
2139
+ const name = userSettings().model?.name;
2140
+ return typeof name === "string" && name ? name : void 0;
2141
+ }
2142
+ function doctor7({ models, hasFallback }) {
2143
+ const { command, prefix } = resolveCommand(binary7());
2144
+ const r = spawnSync7(command, [...prefix, "--version"], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
2145
+ if (r.error || r.status !== 0) return [{ level: "fail", message: `qwen not runnable (${binary7()}). Install Qwen Code (npm i -g @qwen-code/qwen-code), or set PITROOM_QWEN_BIN` }];
2146
+ const checks = [{ level: "ok", message: `Qwen Code ${r.stdout.trim()} at ${binary7()} (beta worker: checked against Qwen Code 0.25)` }];
2147
+ const auth = userSettings().security?.auth?.selectedType;
2148
+ const key = process.env.OPENAI_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY;
2149
+ checks.push(
2150
+ auth || key ? { level: "ok", message: `Qwen Code: sign-in ${auth ?? "from an API key in the environment"}` } : { level: "warn", message: "Qwen Code has no sign-in method set: run `qwen` once to choose one, or set OPENAI_API_KEY (and OPENAI_BASE_URL) for an OpenAI-compatible provider" }
2151
+ );
2152
+ for (const m of models) {
2153
+ const model = m ?? defaultModel5();
2154
+ checks.push({ level: model ? "ok" : "warn", message: model ? `Qwen Code model: ${model}` : "Qwen Code model: none pinned (pass -W qwen:<model> or set model.name in ~/.qwen/settings.json)" });
2155
+ }
2156
+ if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Qwen Code runs" });
2157
+ return checks;
2158
+ }
2159
+ var qwen = {
2160
+ id: "qwen",
2161
+ name: "Qwen Code",
2162
+ capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
2163
+ binary: binary7,
2164
+ invocation: invocation7,
2165
+ parse: parseEvents6,
2166
+ failure: failure7,
2167
+ defaultModel: defaultModel5,
2168
+ doctor: doctor7
2169
+ };
2170
+
2171
+ // src/backends/index.ts
2172
+ var REGISTRY = new Map([opencode, codex, claude, gemini, qwen, kiloBackend, antigravity].map((b) => [b.id, b]));
2173
+ var DEFAULT_BACKEND = opencode.id;
2174
+ var PLANNED = ["hermes"];
2175
+ var backendIds = () => [...REGISTRY.keys()];
2176
+ var allBackends = () => [...REGISTRY.values()];
2177
+ var isBackendId = (id2) => REGISTRY.has(id2) || PLANNED.includes(id2);
2178
+ function getBackend(id2) {
2179
+ const backend = REGISTRY.get(id2);
2180
+ if (backend) return backend;
2181
+ const available = backendIds().join(", ");
2182
+ throw new UserError(
2183
+ PLANNED.includes(id2) ? `the "${id2}" worker is not supported yet (available: ${available})` : `unknown worker "${id2}" (available: ${available})`,
2184
+ 3
2185
+ );
2186
+ }
2187
+
2188
+ // src/core/target.ts
2189
+ function parseTarget(spec, defaultBackend) {
2190
+ const s = spec.trim();
2191
+ if (!s) throw new UserError("empty worker target");
2192
+ const i = s.indexOf(":");
2193
+ if (i > 0 && isBackendId(s.slice(0, i))) {
2194
+ const model = s.slice(i + 1).trim();
2195
+ return model ? { backend: s.slice(0, i), model } : { backend: s.slice(0, i) };
2196
+ }
2197
+ if (isBackendId(s)) return { backend: s };
2198
+ return { backend: defaultBackend, model: s };
2199
+ }
2200
+ var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
2201
+ var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
2202
+
2203
+ // src/core/refs.ts
2204
+ import fs12 from "node:fs";
2205
+ import path12 from "node:path";
2206
+ var FILE_EXTENSIONS = new Set(
2207
+ "ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
2208
+ );
2209
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
2210
+ var MAX_INDEXED = 6e4;
2211
+ var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
2212
+ var REF = new RegExp(
2213
+ String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
2214
+ "g"
2215
+ );
2216
+ var IDENT = /`([A-Za-z_$][\w$]*)(?:\([^`]*\))?`/g;
2217
+ var CLAUSE = /[;|—]|\.\s|:\s/g;
2218
+ var MAX_BYTES = 5 * 1024 * 1024;
2219
+ var WINDOW = 5;
2220
+ var DEFINITION_LOOKBACK = 80;
2221
+ function looksLikeSymbol(m) {
2222
+ const name = m[1];
2223
+ return m[0].includes("(") || /[A-Z_$0-9]/.test(name) || name.length >= 5;
2224
+ }
2225
+ function extractRefs(answer) {
2226
+ const seen = /* @__PURE__ */ new Map();
2227
+ for (const line of answer.split("\n")) {
2228
+ const matches2 = [...line.matchAll(REF)];
2229
+ const seps = [...line.matchAll(CLAUSE)].map((s) => s.index ?? 0);
2230
+ const sepBetween = (a, b) => seps.some((s) => s >= a && s < b);
2231
+ const idents = [...line.matchAll(IDENT)].filter((i) => {
2232
+ if (!looksLikeSymbol(i)) return false;
1453
2233
  const pos = i.index ?? 0;
1454
2234
  const before = matches2.filter((m) => (m.index ?? 0) + m[0].length <= pos).at(-1);
1455
2235
  const after = matches2.find((m) => (m.index ?? 0) >= pos + i[0].length);
@@ -1473,13 +2253,13 @@ function extractRefs(answer) {
1473
2253
  return [...seen.values()];
1474
2254
  }
1475
2255
  function verifyRefs(refs, dirs) {
1476
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path7.resolve(d)))];
2256
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path12.resolve(d)))];
1477
2257
  const invalid = [];
1478
2258
  const lineCache = /* @__PURE__ */ new Map();
1479
2259
  const load = (file) => {
1480
2260
  if (!lineCache.has(file)) {
1481
- const size = fs8.statSync(file).size;
1482
- const lines = size > MAX_BYTES ? null : fs8.readFileSync(file, "utf8").split("\n");
2261
+ const size = fs12.statSync(file).size;
2262
+ const lines = size > MAX_BYTES ? null : fs12.readFileSync(file, "utf8").split("\n");
1483
2263
  if (lines && lines[lines.length - 1] === "") lines.pop();
1484
2264
  lineCache.set(file, lines);
1485
2265
  }
@@ -1490,9 +2270,9 @@ function verifyRefs(refs, dirs) {
1490
2270
  const candidates = (ref) => {
1491
2271
  const direct = resolve(ref.file, roots);
1492
2272
  if (direct) return [direct];
1493
- if (path7.isAbsolute(ref.file)) return [];
2273
+ if (path12.isAbsolute(ref.file)) return [];
1494
2274
  const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
1495
- return (byName().get(path7.basename(wanted)) ?? []).filter((f) => f.split(path7.sep).join("/").endsWith(`/${wanted}`) || path7.basename(f) === wanted).slice(0, 20);
2275
+ return (byName().get(path12.basename(wanted)) ?? []).filter((f) => f.split(path12.sep).join("/").endsWith(`/${wanted}`) || path12.basename(f) === wanted).slice(0, 20);
1496
2276
  };
1497
2277
  const check = (file, ref) => {
1498
2278
  const lines = load(file);
@@ -1507,7 +2287,7 @@ function verifyRefs(refs, dirs) {
1507
2287
  if (!files.length) {
1508
2288
  if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
1509
2289
  total++;
1510
- invalid.push({ ref: ref.text, reason: path7.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
2290
+ invalid.push({ ref: ref.text, reason: path12.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
1511
2291
  continue;
1512
2292
  }
1513
2293
  total++;
@@ -1528,7 +2308,7 @@ function enclosedBy(lines, ref) {
1528
2308
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
1529
2309
  }
1530
2310
  var isFileName = (name) => {
1531
- const base = path7.basename(name);
2311
+ const base = path12.basename(name);
1532
2312
  return !base.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base) || FILE_EXTENSIONS.has(base.split(".").pop().toLowerCase());
1533
2313
  };
1534
2314
  function indexFiles(roots) {
@@ -1537,19 +2317,19 @@ function indexFiles(roots) {
1537
2317
  const walk = (dir) => {
1538
2318
  let entries;
1539
2319
  try {
1540
- entries = fs8.readdirSync(dir, { withFileTypes: true });
2320
+ entries = fs12.readdirSync(dir, { withFileTypes: true });
1541
2321
  } catch {
1542
2322
  return;
1543
2323
  }
1544
2324
  for (const e of entries) {
1545
2325
  if (count >= MAX_INDEXED) return;
1546
2326
  if (e.isDirectory()) {
1547
- if (!SKIP_DIRS.has(e.name)) walk(path7.join(dir, e.name));
2327
+ if (!SKIP_DIRS2.has(e.name)) walk(path12.join(dir, e.name));
1548
2328
  } else if (e.isFile()) {
1549
2329
  count++;
1550
2330
  const list2 = index.get(e.name);
1551
- if (list2) list2.push(path7.join(dir, e.name));
1552
- else index.set(e.name, [path7.join(dir, e.name)]);
2331
+ if (list2) list2.push(path12.join(dir, e.name));
2332
+ else index.set(e.name, [path12.join(dir, e.name)]);
1553
2333
  }
1554
2334
  }
1555
2335
  };
@@ -1557,10 +2337,10 @@ function indexFiles(roots) {
1557
2337
  return index;
1558
2338
  }
1559
2339
  function resolve(ref, roots) {
1560
- const candidates = path7.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path7.join(r, ref));
2340
+ const candidates = path12.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path12.join(r, ref));
1561
2341
  return candidates.find((c) => {
1562
2342
  try {
1563
- return fs8.statSync(c).isFile() && inside(c, roots);
2343
+ return fs12.statSync(c).isFile() && inside(c, roots);
1564
2344
  } catch {
1565
2345
  return false;
1566
2346
  }
@@ -1568,21 +2348,21 @@ function resolve(ref, roots) {
1568
2348
  }
1569
2349
  function inside(file, roots) {
1570
2350
  return roots.some((r) => {
1571
- const rel = path7.relative(r, path7.resolve(file));
1572
- return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
2351
+ const rel = path12.relative(r, path12.resolve(file));
2352
+ return rel !== "" && !rel.startsWith("..") && !path12.isAbsolute(rel);
1573
2353
  });
1574
2354
  }
1575
2355
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1576
2356
 
1577
2357
  // src/core/templates.ts
1578
- import fs9 from "node:fs";
1579
- import path9 from "node:path";
2358
+ import fs13 from "node:fs";
2359
+ import path14 from "node:path";
1580
2360
 
1581
2361
  // src/core/install.ts
1582
- import path8 from "node:path";
1583
- import { fileURLToPath as fileURLToPath2 } from "node:url";
2362
+ import path13 from "node:path";
2363
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
1584
2364
  function packageRoot() {
1585
- return path8.resolve(path8.dirname(fileURLToPath2(import.meta.url)), "..");
2365
+ return path13.resolve(path13.dirname(fileURLToPath3(import.meta.url)), "..");
1586
2366
  }
1587
2367
 
1588
2368
  // src/core/templates.ts
@@ -1593,9 +2373,9 @@ var FILES = {
1593
2373
  "code-reviewer": "pitroom-review/code-reviewer.md"
1594
2374
  };
1595
2375
  function loadTemplate(name, root = packageRoot()) {
1596
- const file = path9.join(root, "skills", FILES[name]);
1597
- if (!fs9.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
1598
- return fs9.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
2376
+ const file = path14.join(root, "skills", FILES[name]);
2377
+ if (!fs13.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
2378
+ return fs13.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
1599
2379
  }
1600
2380
  function fill(template, values) {
1601
2381
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -1651,37 +2431,399 @@ function parseAudit(text) {
1651
2431
  return { verdict, disputed };
1652
2432
  }
1653
2433
 
1654
- // src/core/plan.ts
1655
- import fs10 from "node:fs";
1656
- import path10 from "node:path";
1657
- var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
1658
- var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
1659
- function structure(lines) {
1660
- const headings = [];
1661
- const rules = [];
1662
- const fenced = [];
1663
- let open;
1664
- lines.forEach((l, i) => {
1665
- const f = FENCE.exec(l);
1666
- if (f) {
1667
- fenced[i] = true;
1668
- const mark = f[1];
1669
- if (!open) open = mark;
1670
- else if (mark[0] === open[0] && mark.length >= open.length && !f[2].trim()) open = void 0;
1671
- return;
2434
+ // src/core/config.ts
2435
+ import fs14 from "node:fs";
2436
+ import os9 from "node:os";
2437
+ import path15 from "node:path";
2438
+ var SCHEMA = {
2439
+ worker: "string",
2440
+ fallback: "string[]",
2441
+ timeout: "string",
2442
+ primary: "string",
2443
+ price: "string",
2444
+ link: "string[]",
2445
+ web: "boolean",
2446
+ maxParallel: "number",
2447
+ models: "record",
2448
+ tiers: "record",
2449
+ costs: "numbers",
2450
+ audit: "number",
2451
+ auditFix: "boolean",
2452
+ auditFocus: "boolean",
2453
+ readIn: "string",
2454
+ cacheDays: "number",
2455
+ countRateLimits: "boolean",
2456
+ mcpDash: "boolean",
2457
+ notify: "boolean",
2458
+ notifyCommand: "string",
2459
+ notifyAfter: "number",
2460
+ workerPrices: "record",
2461
+ priceFeed: "boolean",
2462
+ priceFeedUrl: "string",
2463
+ priceFeedHours: "number",
2464
+ budgetDaily: "number",
2465
+ rankWorkers: "boolean"
2466
+ };
2467
+ function configPath() {
2468
+ if (process.env.PITROOM_CONFIG) return path15.resolve(process.env.PITROOM_CONFIG);
2469
+ const base = process.platform === "win32" ? process.env.APPDATA ?? path15.join(os9.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path15.join(os9.homedir(), ".config");
2470
+ return path15.join(base, "pitroom", "config.json");
2471
+ }
2472
+ var cached;
2473
+ function loadConfig() {
2474
+ if (cached) return cached;
2475
+ const file = configPath();
2476
+ const config = {};
2477
+ const warnings2 = [];
2478
+ if (fs14.existsSync(file)) {
2479
+ let raw;
2480
+ try {
2481
+ raw = JSON.parse(fs14.readFileSync(file, "utf8"));
2482
+ } catch (e) {
2483
+ warnings2.push(`${file}: invalid JSON (${e.message}); ignored`);
1672
2484
  }
1673
- fenced[i] = !!open;
1674
- if (open) return;
1675
- const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l);
1676
- if (h) headings.push({ line: i, level: h[1].length, text: h[2] });
1677
- else if (RULE.test(l)) rules.push(i);
1678
- });
1679
- return { headings, rules, fenced };
2485
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
2486
+ for (const [key, value] of Object.entries(raw)) {
2487
+ const type = SCHEMA[key];
2488
+ if (!type) {
2489
+ warnings2.push(`${file}: unknown key "${key}" ignored`);
2490
+ } else if (matches(value, type)) {
2491
+ config[key] = value;
2492
+ } else {
2493
+ warnings2.push(`${file}: "${key}" must be ${type}; ignored`);
2494
+ }
2495
+ }
2496
+ }
2497
+ }
2498
+ cached = { config, warnings: warnings2 };
2499
+ return cached;
1680
2500
  }
1681
- function parsePlan(text, file = "") {
1682
- const lines = text.replace(/\r\n?/g, "\n").split("\n");
1683
- const { headings, rules, fenced } = structure(lines);
1684
- const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
2501
+ function matches(v, type) {
2502
+ if (type === "string[]") return Array.isArray(v) && v.every((s) => typeof s === "string");
2503
+ if (type === "record") {
2504
+ return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((s) => typeof s === "string");
2505
+ }
2506
+ if (type === "numbers") {
2507
+ return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
2508
+ }
2509
+ if (type === "number") return typeof v === "number" && Number.isFinite(v) && v >= 0;
2510
+ return typeof v === type;
2511
+ }
2512
+ var DEFAULT_PARALLEL = 20;
2513
+ var MAX_PARALLEL_LIMIT = 30;
2514
+ var clampParallel = (s) => ({ ...s, value: Math.min(s.value, MAX_PARALLEL_LIMIT) });
2515
+ var positiveInt = (v) => {
2516
+ const n = Number(v);
2517
+ return v !== void 0 && v !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
2518
+ };
2519
+ var rate = (v) => {
2520
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
2521
+ return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
2522
+ };
2523
+ var amount = (v) => {
2524
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
2525
+ return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
2526
+ };
2527
+ var days = (v) => {
2528
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
2529
+ return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
2530
+ };
2531
+ var flag01 = (v) => {
2532
+ const t = v?.trim().toLowerCase();
2533
+ return t === void 0 || t === "" ? void 0 : ["1", "true", "on", "yes"].includes(t) ? true : ["0", "false", "off", "no"].includes(t) ? false : void 0;
2534
+ };
2535
+ var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
2536
+ var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
2537
+ function setting(flag, env, conf, fallback) {
2538
+ if (flag !== void 0) return { value: flag, source: "flag" };
2539
+ if (env !== void 0) return { value: env, source: "env" };
2540
+ if (conf !== void 0) return { value: conf, source: "config" };
2541
+ return { value: fallback, source: "default" };
2542
+ }
2543
+ function effective(flags = {}) {
2544
+ const c = loadConfig().config;
2545
+ const e = process.env;
2546
+ return {
2547
+ worker: setting(flags.worker, e.PITROOM_WORKER, c.worker, DEFAULT_BACKEND),
2548
+ /** Overrides the model of the preferred worker only. */
2549
+ model: setting(flags.model, e.PITROOM_MODEL, void 0, void 0),
2550
+ fallback: setting(void 0, list(e.PITROOM_FALLBACK), c.fallback, []),
2551
+ timeout: setting(flags.timeout, e.PITROOM_TIMEOUT, c.timeout, "30m"),
2552
+ primary: setting(void 0, e.PITROOM_PRIMARY, c.primary, "sonnet"),
2553
+ price: setting(void 0, e.PITROOM_PRICE, c.price, void 0),
2554
+ link: setting(void 0, void 0, c.link, []),
2555
+ web: setting(void 0, void 0, c.web, false),
2556
+ maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
2557
+ models: setting(void 0, void 0, c.models, {}),
2558
+ tiers: setting(void 0, void 0, c.tiers, {}),
2559
+ costs: setting(void 0, void 0, c.costs, {}),
2560
+ audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
2561
+ auditFix: setting(void 0, flag01(e.PITROOM_AUDIT_FIX), c.auditFix, false),
2562
+ auditFocus: setting(void 0, flag01(e.PITROOM_AUDIT_FOCUS), c.auditFocus, false),
2563
+ readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
2564
+ cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
2565
+ countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
2566
+ mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
2567
+ notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
2568
+ notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
2569
+ notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
2570
+ workerPrices: setting(void 0, void 0, c.workerPrices, {}),
2571
+ priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
2572
+ priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
2573
+ priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24),
2574
+ rankWorkers: setting(void 0, flag01(e.PITROOM_RANK_WORKERS), c.rankWorkers, false),
2575
+ budgetDaily: setting(void 0, amount(e.PITROOM_BUDGET_DAILY), amount(c.budgetDaily), void 0)
2576
+ };
2577
+ }
2578
+
2579
+ // src/core/audit.ts
2580
+ var ASKS_FOR_LIST = /\b(all|every|each|list|lists|enumerate|how many|count|which)\b/i;
2581
+ function focusedRate(base, task, record) {
2582
+ if (base <= 0 || base >= 1) return { rate: base, why: [] };
2583
+ let rate2 = base;
2584
+ const why = [];
2585
+ if (ASKS_FOR_LIST.test(task)) {
2586
+ rate2 *= 2;
2587
+ why.push("asks for a list or a count");
2588
+ }
2589
+ if (record && record.audited >= 3 && record.agreed / record.audited < 0.5) {
2590
+ rate2 *= 2;
2591
+ why.push(`the worker's audited answers were confirmed ${record.agreed} of ${record.audited} times`);
2592
+ } else if (record && record.audited >= 5 && record.agreed === record.audited) {
2593
+ rate2 /= 2;
2594
+ why.push(`the worker's ${record.audited} audited answers were all confirmed`);
2595
+ }
2596
+ return { rate: Math.min(rate2, 1), why };
2597
+ }
2598
+ function fixTask(disputed) {
2599
+ return [
2600
+ "Another worker audited your answer above and disputed these claims:",
2601
+ ...disputed.map((d) => `- ${d}`),
2602
+ "",
2603
+ "Check each one yourself against the files (the auditor can be wrong too). Then give your whole answer again,",
2604
+ "corrected, in the same answer format: keep what holds, fix what does not, and add what was missing.",
2605
+ "Under OPEN ISSUES, say which disputed claims you accepted and which you kept, and why."
2606
+ ].join("\n");
2607
+ }
2608
+
2609
+ // src/core/rank.ts
2610
+ var MIN_RUNS = 5;
2611
+ var MIN_AUDITS = 3;
2612
+ function scoreOf(r) {
2613
+ if (!r || r.runs < MIN_RUNS) return void 0;
2614
+ const ok = Math.min(r.ok, r.runs) / r.runs;
2615
+ return r.audited >= MIN_AUDITS ? ok * (r.agreed / r.audited) : ok;
2616
+ }
2617
+ var pct = (x) => `${Math.round(x * 100)}%`;
2618
+ function rankChain(chain, recordOf) {
2619
+ const scored = chain.map((t, i) => ({ t, i, score: scoreOf(recordOf(t)) }));
2620
+ const slots = scored.filter((s) => s.score !== void 0);
2621
+ if (slots.length < 2) return { chain };
2622
+ const ranked = [...slots].sort((a, b) => Math.round(b.score * 10) - Math.round(a.score * 10) || a.i - b.i);
2623
+ const out = [...chain];
2624
+ slots.forEach((slot, k) => out[slot.i] = ranked[k].t);
2625
+ if (out[0] === chain[0]) return { chain: out };
2626
+ const by = (t) => scored.find((s) => s.t === t).score;
2627
+ return {
2628
+ chain: out,
2629
+ note: `ranked by record ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
2630
+ };
2631
+ }
2632
+
2633
+ // src/core/receipt.ts
2634
+ import fs16 from "node:fs";
2635
+
2636
+ // src/core/prices.ts
2637
+ import fs15 from "node:fs";
2638
+ import path16 from "node:path";
2639
+ var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
2640
+ var catalogFile = () => path16.join(home(), "prices.json");
2641
+ function trim(raw) {
2642
+ const out = {};
2643
+ if (!raw || typeof raw !== "object") return out;
2644
+ for (const [provider, p] of Object.entries(raw)) {
2645
+ for (const [id2, m] of Object.entries(p?.models ?? {})) {
2646
+ const c = m?.cost;
2647
+ const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
2648
+ if (!c || !ok(c.input) || !ok(c.output)) continue;
2649
+ out[`${provider}/${id2}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
2650
+ }
2651
+ }
2652
+ return out;
2653
+ }
2654
+ var cached2;
2655
+ function readCatalog() {
2656
+ const file = catalogFile();
2657
+ try {
2658
+ const { mtimeMs } = fs15.statSync(file);
2659
+ if (cached2?.file === file && cached2.mtimeMs === mtimeMs) return cached2.catalog;
2660
+ const parsed = JSON.parse(fs15.readFileSync(file, "utf8"));
2661
+ const catalog6 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
2662
+ cached2 = { file, mtimeMs, catalog: catalog6 };
2663
+ return catalog6;
2664
+ } catch {
2665
+ return void 0;
2666
+ }
2667
+ }
2668
+ var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
2669
+ function catalogPrice(backend, ...models) {
2670
+ const catalog6 = readCatalog();
2671
+ if (!catalog6) return void 0;
2672
+ for (const raw of models) {
2673
+ const id2 = raw?.split("#")[0];
2674
+ if (!id2) continue;
2675
+ const keys = [id2, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id2}`)];
2676
+ const any = keys.find((k) => catalog6.models[k]) ?? Object.keys(catalog6.models).filter((k) => k.endsWith(`/${id2}`)).sort()[0];
2677
+ const triple = any ? catalog6.models[any] : void 0;
2678
+ if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
2679
+ const [input, output, cachedInput] = triple;
2680
+ return { input, output, cachedInput, key: any };
2681
+ }
2682
+ }
2683
+ return void 0;
2684
+ }
2685
+
2686
+ // src/core/receipt.ts
2687
+ var PRESETS = {
2688
+ sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
2689
+ opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
2690
+ haiku: { name: "Claude Haiku", input: 1, output: 5, cachedInput: 0.1 },
2691
+ "gpt-5": { name: "GPT-5", input: 1.25, output: 10, cachedInput: 0.125 }
2692
+ };
2693
+ function primaryPrice() {
2694
+ const eff = effective();
2695
+ const custom = eff.price.value?.split(",").map(Number);
2696
+ if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
2697
+ return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
2698
+ }
2699
+ const named = eff.primary.value;
2700
+ const preset = PRESETS[named.toLowerCase()];
2701
+ if (preset) return preset;
2702
+ const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
2703
+ return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
2704
+ }
2705
+ var estimateTokens = (text) => Math.ceil(text.length / 4);
2706
+ function parsePriceSpec(spec, name = "worker") {
2707
+ const parts = typeof spec === "string" ? spec.split(",").map((x) => Number(x.trim())) : [];
2708
+ if (parts.length < 2 || parts.length > 3 || !parts.every((n) => Number.isFinite(n) && n >= 0)) return void 0;
2709
+ return { name, source: "config", input: parts[0], output: parts[1], cachedInput: parts[2] ?? parts[0] / 10 };
2710
+ }
2711
+ var costAt = (usage, price) => (usage.input * price.input + usage.cacheRead * price.cachedInput + (usage.output + usage.reasoning) * price.output) / 1e6;
2712
+ var workerCostOf = (usage) => usage?.cost ?? usage?.costEstimate;
2713
+ function savedUsd(usage, returnedTokens, price = primaryPrice()) {
2714
+ const readingTheReport = returnedTokens * price.input / 1e6;
2715
+ return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
2716
+ }
2717
+ function readLedger(sinceMs) {
2718
+ if (!fs16.existsSync(ledgerFile())) return [];
2719
+ return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2720
+ try {
2721
+ return [JSON.parse(l)];
2722
+ } catch {
2723
+ return [];
2724
+ }
2725
+ }).filter((e) => !sinceMs || Date.parse(e.at) >= sinceMs);
2726
+ }
2727
+ function totals(entries) {
2728
+ const t = entries.reduce(
2729
+ (a, e) => ({
2730
+ runs: a.runs + 1,
2731
+ tokens: a.tokens + e.tokens,
2732
+ returned: a.returned + e.returned,
2733
+ workerCost: a.workerCost + e.workerCost,
2734
+ saved: a.saved + e.saved
2735
+ }),
2736
+ { runs: 0, tokens: 0, returned: 0, workerCost: 0, saved: 0 }
2737
+ );
2738
+ return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
2739
+ }
2740
+ var usd = (n) => n >= 100 ? `$${n.toFixed(0)}` : n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
2741
+ function compact(n) {
2742
+ const fmt = (v) => v.toFixed(v >= 100 ? 0 : 1).replace(/\.0$/, "");
2743
+ if (n >= 1e6) return `${fmt(n / 1e6)}M`;
2744
+ if (n >= 1e3) return `${fmt(n / 1e3)}k`;
2745
+ return String(Math.round(n));
2746
+ }
2747
+ function badgeUrl(t) {
2748
+ const msg = encodeURIComponent(`saved ${usd(t.saved)} \xB7 ${compact(t.tokens)} tokens offloaded`).replace(/-/g, "--");
2749
+ return `https://img.shields.io/badge/pitroom-${msg}-7c3aed`;
2750
+ }
2751
+ var esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
2752
+ function card(t, period) {
2753
+ const W = 720, H = 316, M = 32, GAP = 12;
2754
+ const tile = (i, value, label) => {
2755
+ const w = (W - 2 * M - 2 * GAP) / 3, x = M + i * (w + GAP);
2756
+ return `<rect x="${x}" y="196" width="${w}" height="72" rx="14" fill="#fff" fill-opacity=".035" stroke="#fff" stroke-opacity=".1"/>
2757
+ <text x="${x + 18}" y="224" class="lab">${esc(label.toUpperCase())}</text><text x="${x + 18}" y="254" class="val">${esc(value)}</text>`;
2758
+ };
2759
+ const when = period.toUpperCase();
2760
+ const pill = 22 + when.length * 7.4;
2761
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="pitroom saved ${esc(usd(t.saved))}">
2762
+ <style>
2763
+ text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Inter,Helvetica,Arial,sans-serif;fill:#f4f4f5}
2764
+ .name{font-size:20px;font-weight:650;letter-spacing:-.01em}
2765
+ .sub{font-size:12.5px;fill:#a1a1aa}
2766
+ .pill{font-size:11px;font-weight:600;letter-spacing:.1em;fill:#a1a1aa}
2767
+ .lab{font-size:11px;font-weight:500;letter-spacing:.09em;fill:#a1a1aa}
2768
+ .big{font-size:60px;font-weight:700;letter-spacing:-.03em;fill:#3ddc97}
2769
+ .val{font-size:26px;font-weight:650;letter-spacing:-.02em}
2770
+ .foot{font-size:11.5px;fill:#71717a}
2771
+ </style>
2772
+ <defs>
2773
+ <radialGradient id="o" cx="1" cy="0" r=".7"><stop offset="0" stop-color="#ff6a2b" stop-opacity=".24"/><stop offset="1" stop-color="#ff6a2b" stop-opacity="0"/></radialGradient>
2774
+ <radialGradient id="b" cx="0" cy="0" r=".7"><stop offset="0" stop-color="#38bdf8" stop-opacity=".16"/><stop offset="1" stop-color="#38bdf8" stop-opacity="0"/></radialGradient>
2775
+ <linearGradient id="m" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ff6a2b"/><stop offset="1" stop-color="#ffa86b"/></linearGradient>
2776
+ <clipPath id="c"><rect width="${W}" height="${H}" rx="20"/></clipPath>
2777
+ </defs>
2778
+ <g clip-path="url(#c)"><rect width="${W}" height="${H}" fill="#0b0b0f"/><rect width="${W}" height="${H}" fill="url(#o)"/><rect width="${W}" height="${H}" fill="url(#b)"/></g>
2779
+ <rect x=".5" y=".5" width="${W - 1}" height="${H - 1}" rx="19.5" fill="none" stroke="#fff" stroke-opacity=".1"/>
2780
+ <rect x="${M}" y="28" width="38" height="38" rx="11" fill="url(#m)"/>
2781
+ <path d="M4 4h4v4H4zm8 0h4v4h-4zM8 8h4v4H8zm8 0h4v4h-4zM4 12h4v4H4zm8 0h4v4h-4zm-4 4h4v4H8zm8 0h4v4h-4z" fill="#fff" transform="translate(${M + 7} 35) scale(1)"/>
2782
+ <text x="${M + 52}" y="45" class="name">Pitroom</text><text x="${M + 52}" y="63" class="sub">Your agent's pit crew</text>
2783
+ <rect x="${W - M - pill}" y="34" width="${pill}" height="26" rx="13" fill="#fff" fill-opacity=".04" stroke="#fff" stroke-opacity=".14"/>
2784
+ <text x="${W - M - pill / 2}" y="51" class="pill" text-anchor="middle">${esc(when)}</text>
2785
+ <text x="${M}" y="112" class="lab">SAVED ON YOUR MAIN CODING AGENT</text>
2786
+ <text x="${M}" y="168" class="big">${esc(usd(t.saved))}</text>
2787
+ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.round(t.ratio)}\xD7` : "\u2014", "Context compression")}${tile(2, String(t.runs), "Delegated tasks")}
2788
+ <text x="${M}" y="296" class="foot">Estimated against ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
2789
+ </svg>
2790
+ `;
2791
+ }
2792
+
2793
+ // src/core/ui.ts
2794
+ var WEEK_MS = 7 * 24 * 3600 * 1e3;
2795
+
2796
+ // src/core/plan.ts
2797
+ import fs17 from "node:fs";
2798
+ import path17 from "node:path";
2799
+ var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
2800
+ var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
2801
+ function structure(lines) {
2802
+ const headings = [];
2803
+ const rules = [];
2804
+ const fenced = [];
2805
+ let open;
2806
+ lines.forEach((l, i) => {
2807
+ const f = FENCE.exec(l);
2808
+ if (f) {
2809
+ fenced[i] = true;
2810
+ const mark = f[1];
2811
+ if (!open) open = mark;
2812
+ else if (mark[0] === open[0] && mark.length >= open.length && !f[2].trim()) open = void 0;
2813
+ return;
2814
+ }
2815
+ fenced[i] = !!open;
2816
+ if (open) return;
2817
+ const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l);
2818
+ if (h) headings.push({ line: i, level: h[1].length, text: h[2] });
2819
+ else if (RULE.test(l)) rules.push(i);
2820
+ });
2821
+ return { headings, rules, fenced };
2822
+ }
2823
+ function parsePlan(text, file = "") {
2824
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
2825
+ const { headings, rules, fenced } = structure(lines);
2826
+ const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
1685
2827
  const titleHeading = headings.find((h) => h.level === 1);
1686
2828
  const start = titleHeading ? titleHeading.line + 1 : 0;
1687
2829
  const header = lines.slice(start, after(start - 1)).filter((l) => !/^\s*>/.test(l)).join("\n").trim();
@@ -1700,9 +2842,9 @@ function parsePlan(text, file = "") {
1700
2842
  return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
1701
2843
  }
1702
2844
  function loadPlan(file) {
1703
- const abs = path10.resolve(file);
1704
- if (!fs10.existsSync(abs) || !fs10.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
1705
- const plan = parsePlan(fs10.readFileSync(abs, "utf8"), fs10.realpathSync(abs));
2845
+ const abs = path17.resolve(file);
2846
+ if (!fs17.existsSync(abs) || !fs17.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
2847
+ const plan = parsePlan(fs17.readFileSync(abs, "utf8"), fs17.realpathSync(abs));
1706
2848
  if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
1707
2849
  return plan;
1708
2850
  }
@@ -1721,11 +2863,157 @@ function brief(plan, task) {
1721
2863
  ].filter(Boolean).join("\n\n")}
1722
2864
  `;
1723
2865
  }
1724
- var planName = (file) => path10.basename(file).replace(/\.md$/i, "");
2866
+ var planName = (file) => path17.basename(file).replace(/\.md$/i, "");
1725
2867
 
1726
- // src/vcs/guard.ts
1727
- import fs11 from "node:fs";
1728
- import path11 from "node:path";
2868
+ // src/core/style.ts
2869
+ var enabled = () => {
2870
+ if (process.env.NO_COLOR) return false;
2871
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
2872
+ return !!process.stdout.isTTY && process.env.TERM !== "dumb";
2873
+ };
2874
+ var wrap = (open, close) => (text) => enabled() ? `\x1B[${open}m${text}\x1B[${close}m` : text;
2875
+ var bold = wrap(1, 22);
2876
+ var dim = wrap(2, 22);
2877
+ var red = wrap(31, 39);
2878
+ var green = wrap(32, 39);
2879
+ var yellow = wrap(33, 39);
2880
+ var blue = wrap(34, 39);
2881
+ var cyan = wrap(36, 39);
2882
+
2883
+ // src/core/group.ts
2884
+ var retryShell = (platform = process.platform) => platform === "win32" ? "powershell" : "sh";
2885
+ var quoteFor = (shell) => (s) => /^[\w./:@=\\-]+$/.test(s) && !(shell === "sh" && s.includes("\\")) ? s : shell === "sh" ? `'${s.replace(/'/g, `'\\''`)}'` : `'${s.replace(/'/g, "''")}'`;
2886
+ function retryCommands(metas, shell = retryShell()) {
2887
+ const quote = quoteFor(shell);
2888
+ return metas.filter((m) => ["failed", "timeout", "stopped"].includes(m.state) && !m.auditOf && !m.reviewOf && !m.parent && !m.fixOf).map((m) => {
2889
+ const where = ["-d", quote(m.dir), ...m.group ? ["-g", quote(m.group)] : []];
2890
+ if (m.plan) return ["pitroom run", m.mode === "write" ? "-w" : "-i", "--plan", quote(m.plan.file), "--step", String(m.plan.step), ...where, "--bg"].join(" ");
2891
+ const mode = m.mode === "isolate" ? ["-i"] : m.mode === "write" ? ["-w"] : [];
2892
+ const end = shell === "sh" || m.task.startsWith("-") ? ["--"] : [];
2893
+ return ["pitroom run", ...mode, ...where, "--bg", ...end, quote(m.task)].join(" ");
2894
+ });
2895
+ }
2896
+
2897
+ // src/core/eval.ts
2898
+ import { spawnSync as spawnSync8 } from "node:child_process";
2899
+ import fs18 from "node:fs";
2900
+ var KINDS = ["definition", "count", "set"];
2901
+ function loadEval(file) {
2902
+ let raw;
2903
+ try {
2904
+ raw = JSON.parse(fs18.readFileSync(file, "utf8"));
2905
+ } catch (e) {
2906
+ throw new UserError(`cannot read ${file}: ${e.message}`);
2907
+ }
2908
+ const list2 = raw?.questions;
2909
+ if (!Array.isArray(list2) || !list2.length) throw new UserError(`${file}: expected {"questions": [{"id", "task", "kind", "truth"}, \u2026]}`);
2910
+ const ids = /* @__PURE__ */ new Set();
2911
+ return list2.map((q, i) => {
2912
+ const where = `${file}: question ${i + 1}`;
2913
+ if (!q || typeof q !== "object") throw new UserError(`${where} is not an object`);
2914
+ const id2 = typeof q.id === "string" && q.id.trim() ? q.id.trim() : `q${i + 1}`;
2915
+ if (ids.has(id2)) throw new UserError(`${where}: the id "${id2}" is used twice`);
2916
+ ids.add(id2);
2917
+ if (typeof q.task !== "string" || !q.task.trim()) throw new UserError(`${where} (${id2}) has no task`);
2918
+ if (!KINDS.includes(q.kind)) throw new UserError(`${where} (${id2}): kind must be one of ${KINDS.join(", ")}`);
2919
+ const t = q.truth;
2920
+ const grep = t && typeof t === "object" && !Array.isArray(t) && typeof t.grep === "string" && t.grep;
2921
+ const literal = q.kind === "definition" && typeof t === "string" && /:\d+$/.test(t) || q.kind === "count" && typeof t === "number" && Number.isInteger(t) && t >= 0 || q.kind === "set" && Array.isArray(t) && t.every((s) => typeof s === "string");
2922
+ if (!grep && !literal) {
2923
+ const want = { definition: '"path:line"', count: "a whole number", set: "a list of paths" }[q.kind];
2924
+ throw new UserError(`${where} (${id2}): truth must be ${want} or {"grep": "pattern", \u2026}`);
2925
+ }
2926
+ return { id: id2, task: q.task.trim(), kind: q.kind, truth: t };
2927
+ });
2928
+ }
2929
+ var clean = (p) => p.replace(/\\/g, "/").replace(/^\.?\//, "");
2930
+ function gitGrep(root, flags, t) {
2931
+ const r = spawnSync8("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
2932
+ encoding: "utf8",
2933
+ maxBuffer: 64 * 1024 * 1024
2934
+ });
2935
+ if (r.status === 1) return [];
2936
+ if (r.status !== 0) throw new UserError(`git grep "${t.grep}" failed: ${(r.stderr || r.error?.message || "").trim()}`);
2937
+ const exclude = t.exclude ? new RegExp(t.exclude) : void 0;
2938
+ return r.stdout.split("\n").filter(Boolean).filter((l) => !exclude?.test(l.split(":")[0]));
2939
+ }
2940
+ function truthOf(q, root) {
2941
+ const t = q.truth;
2942
+ if (t && typeof t === "object" && !Array.isArray(t)) {
2943
+ if (!root) throw new UserError(`question ${q.id}: a "grep" truth needs a git repository`);
2944
+ if (q.kind === "definition") {
2945
+ const hit = gitGrep(root, ["-n"], t).map((l) => /^(.*?):(\d+):/.exec(l)).find(Boolean);
2946
+ if (!hit) throw new UserError(`question ${q.id}: git grep found no definition for "${t.grep}"`);
2947
+ return { path: clean(hit[1]), line: Number(hit[2]) };
2948
+ }
2949
+ const files = [...new Set(gitGrep(root, ["-l"], t).map(clean))];
2950
+ return q.kind === "count" ? { count: files.length } : { files };
2951
+ }
2952
+ if (q.kind === "definition") {
2953
+ const m = /^(.*):(\d+)$/.exec(t);
2954
+ return { path: clean(m[1]), line: Number(m[2]) };
2955
+ }
2956
+ if (q.kind === "count") return { count: t };
2957
+ return { files: [...new Set(t.map(clean))] };
2958
+ }
2959
+ function answerParts(answer) {
2960
+ const text = answer.replace(/`/g, "");
2961
+ const s = /SUMMARY:\s*([\s\S]*?)(?=^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):|$(?![\s\S]))/m.exec(text);
2962
+ const d = /^\s*DETAILS:\s*([\s\S]*?)(?=^\s*(?:FILES CHANGED|VERIFICATION|OPEN ISSUES):|$(?![\s\S]))/m.exec(text);
2963
+ const summary = s ? s[1] : text.split(/^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):/m)[0];
2964
+ return { summary: summary.trim(), details: (d?.[1] ?? "").trim() };
2965
+ }
2966
+ function pathsIn(text) {
2967
+ const found = text.replace(/\\/g, "/").match(/[A-Za-z0-9_.@\-/]+\.[A-Za-z0-9]{1,8}(?::\d+)?/g) ?? [];
2968
+ return [...new Set(found.map((p) => clean(p.replace(/:\d+$/, ""))))];
2969
+ }
2970
+ function scoreAnswer(q, truth, answer, root) {
2971
+ const prefix = root ? `${clean(root).replace(/\/$/, "")}/` : "";
2972
+ const text = prefix ? answer.replace(/\\/g, "/").split(prefix).join("") : answer;
2973
+ const { summary, details } = answerParts(text);
2974
+ if (!summary && !details) return { score: 0, got: "(no answer)" };
2975
+ if ("line" in truth) {
2976
+ const flat = summary.replace(/\s+/g, " ");
2977
+ if (new RegExp(`(^|[^\\w./-])\\.?/?${escape2(truth.path)}:${truth.line}(?!\\d)`).test(flat)) return { score: 1, got: `${truth.path}:${truth.line}` };
2978
+ const named = pathsIn(flat).includes(truth.path);
2979
+ return { score: named ? 0.5 : 0, got: flat.slice(0, 120) };
2980
+ }
2981
+ if ("count" in truth) {
2982
+ const n = /\d[\d,]*/.exec(summary);
2983
+ const got2 = n ? Number(n[0].replace(/,/g, "")) : void 0;
2984
+ return { score: got2 === truth.count ? 1 : 0, got: got2 === void 0 ? "(no number)" : String(got2) };
2985
+ }
2986
+ let got = pathsIn(summary);
2987
+ if (!got.length) got = pathsIn(details);
2988
+ const hit = got.filter((p) => truth.files.includes(p)).length;
2989
+ const precision = got.length ? hit / got.length : 0;
2990
+ const recall = truth.files.length ? hit / truth.files.length : got.length ? 0 : 1;
2991
+ const f1 = precision + recall ? 2 * precision * recall / (precision + recall) : 0;
2992
+ return { score: !truth.files.length && !got.length ? 1 : f1, got: `${hit}/${truth.files.length} found, ${got.length - hit} extra` };
2993
+ }
2994
+ var escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2995
+ var mean = (a) => a.length ? a.reduce((x, y) => x + y, 0) / a.length : 0;
2996
+ var median = (a) => {
2997
+ const s = a.filter((x) => x !== void 0).sort((x, y) => x - y);
2998
+ return s.length ? s[Math.floor(s.length / 2)] : void 0;
2999
+ };
3000
+ function summarize(rows) {
3001
+ const by = /* @__PURE__ */ new Map();
3002
+ for (const r of rows) by.set(r.worker, [...by.get(r.worker) ?? [], r]);
3003
+ return [...by.entries()].map(([worker, rs]) => ({
3004
+ worker,
3005
+ questions: rs.length,
3006
+ failed: rs.filter((r) => r.state !== "done").length,
3007
+ score: mean(rs.map((r) => r.score)),
3008
+ byKind: Object.fromEntries(KINDS.filter((k) => rs.some((r) => r.kind === k)).map((k) => [k, mean(rs.filter((r) => r.kind === k).map((r) => r.score))])),
3009
+ medianSeconds: median(rs.map((r) => r.seconds)),
3010
+ medianTokens: median(rs.map((r) => r.tokens))
3011
+ })).sort((a, b) => b.score - a.score || (a.medianSeconds ?? 1e9) - (b.medianSeconds ?? 1e9));
3012
+ }
3013
+
3014
+ // src/vcs/guard.ts
3015
+ import fs19 from "node:fs";
3016
+ import path18 from "node:path";
1729
3017
  var VERSION = 2;
1730
3018
  var WINDOWS = process.platform === "win32";
1731
3019
  var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
@@ -1802,29 +3090,29 @@ esac
1802
3090
  exec "$real" "$@"
1803
3091
  `;
1804
3092
  function shimDir() {
1805
- return path11.join(home(), "shim", `v${VERSION}`);
3093
+ return path18.join(home(), "shim", `v${VERSION}`);
1806
3094
  }
1807
3095
  function findRealGit(skip) {
1808
3096
  const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
1809
- for (const dir of (process.env.PATH ?? "").split(path11.delimiter).filter(Boolean)) {
1810
- if (same(path11.resolve(dir), skip)) continue;
1811
- const p = path11.join(dir, WINDOWS ? "git.exe" : "git");
3097
+ for (const dir of (process.env.PATH ?? "").split(path18.delimiter).filter(Boolean)) {
3098
+ if (same(path18.resolve(dir), skip)) continue;
3099
+ const p = path18.join(dir, WINDOWS ? "git.exe" : "git");
1812
3100
  try {
1813
- fs11.accessSync(p, fs11.constants.X_OK);
1814
- if (fs11.statSync(p).isFile()) return p;
3101
+ fs19.accessSync(p, fs19.constants.X_OK);
3102
+ if (fs19.statSync(p).isFile()) return p;
1815
3103
  } catch {
1816
3104
  }
1817
3105
  }
1818
3106
  return void 0;
1819
3107
  }
1820
3108
  function findGitSh(realGit) {
1821
- let dir = path11.dirname(realGit);
3109
+ let dir = path18.dirname(realGit);
1822
3110
  for (let i = 0; i < 4; i++) {
1823
3111
  for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
1824
- const p = path11.join(dir, rel);
1825
- if (fs11.existsSync(p)) return p;
3112
+ const p = path18.join(dir, rel);
3113
+ if (fs19.existsSync(p)) return p;
1826
3114
  }
1827
- const up = path11.dirname(dir);
3115
+ const up = path18.dirname(dir);
1828
3116
  if (up === dir) break;
1829
3117
  dir = up;
1830
3118
  }
@@ -1842,12 +3130,12 @@ var REF_HOOK = `#!/bin/sh
1842
3130
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
1843
3131
  exit 1
1844
3132
  `;
1845
- var hooksDir = () => path11.join(home(), "git-hooks", `v${HOOK_VERSION}`);
1846
- function writeIfChanged(file, content) {
1847
- if (fs11.existsSync(file) && fs11.readFileSync(file, "utf8") === content) return;
1848
- fs11.mkdirSync(path11.dirname(file), { recursive: true });
3133
+ var hooksDir = () => path18.join(home(), "git-hooks", `v${HOOK_VERSION}`);
3134
+ function writeIfChanged2(file, content) {
3135
+ if (fs19.existsSync(file) && fs19.readFileSync(file, "utf8") === content) return;
3136
+ fs19.mkdirSync(path18.dirname(file), { recursive: true });
1849
3137
  const tmp = `${file}.${process.pid}.tmp`;
1850
- fs11.writeFileSync(tmp, content, { mode: 493 });
3138
+ fs19.writeFileSync(tmp, content, { mode: 493 });
1851
3139
  renameOver(tmp, file);
1852
3140
  }
1853
3141
  function pathKey(env) {
@@ -1879,7 +3167,7 @@ function guardEnv(env) {
1879
3167
  GIT_PAGER: "cat",
1880
3168
  PAGER: "cat"
1881
3169
  };
1882
- writeIfChanged(path11.join(hooksDir(), "reference-transaction"), REF_HOOK);
3170
+ writeIfChanged2(path18.join(hooksDir(), "reference-transaction"), REF_HOOK);
1883
3171
  env = withGitConfig(onePath(env), [
1884
3172
  ["core.hooksPath", fwd(hooksDir())],
1885
3173
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -1891,11 +3179,11 @@ function guardEnv(env) {
1891
3179
  if (WINDOWS) {
1892
3180
  sh = findGitSh(real);
1893
3181
  if (!sh) return { ...env, ...quiet };
1894
- writeIfChanged(path11.join(dir, "git.cmd"), cmdShim(sh));
3182
+ writeIfChanged2(path18.join(dir, "git.cmd"), cmdShim(sh));
1895
3183
  }
1896
- writeIfChanged(path11.join(dir, "git"), SHIM);
3184
+ writeIfChanged2(path18.join(dir, "git"), SHIM);
1897
3185
  const key = pathKey(env);
1898
- return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path11.delimiter}${env[key] ?? ""}` };
3186
+ return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path18.delimiter}${env[key] ?? ""}` };
1899
3187
  }
1900
3188
  function shimReady() {
1901
3189
  const env = guardEnv({ ...process.env });
@@ -1903,11 +3191,11 @@ function shimReady() {
1903
3191
  }
1904
3192
 
1905
3193
  // src/core/slots.ts
1906
- import crypto2 from "node:crypto";
1907
- import fs12 from "node:fs";
1908
- import path12 from "node:path";
1909
- var slotsDir = () => path12.join(home(), "slots");
1910
- var locksDir = () => path12.join(home(), "locks");
3194
+ import crypto4 from "node:crypto";
3195
+ import fs20 from "node:fs";
3196
+ import path19 from "node:path";
3197
+ var slotsDir = () => path19.join(home(), "slots");
3198
+ var locksDir = () => path19.join(home(), "locks");
1911
3199
  var STARTUP_GRACE_MS = 3e4;
1912
3200
  function holderActive(runId, self) {
1913
3201
  if (!runId || runId === self) return false;
@@ -1921,23 +3209,23 @@ function holderActive(runId, self) {
1921
3209
  }
1922
3210
  }
1923
3211
  function tryClaim(file, runId) {
1924
- fs12.mkdirSync(path12.dirname(file), { recursive: true });
3212
+ fs20.mkdirSync(path19.dirname(file), { recursive: true });
1925
3213
  try {
1926
- fs12.writeFileSync(file, runId, { flag: "wx" });
3214
+ fs20.writeFileSync(file, runId, { flag: "wx" });
1927
3215
  return true;
1928
3216
  } catch (e) {
1929
3217
  if (e.code !== "EEXIST") throw e;
1930
3218
  }
1931
3219
  let owner = "";
1932
3220
  try {
1933
- owner = fs12.readFileSync(file, "utf8").trim();
3221
+ owner = fs20.readFileSync(file, "utf8").trim();
1934
3222
  } catch {
1935
3223
  }
1936
3224
  if (owner === runId) return true;
1937
3225
  if (holderActive(owner, runId)) return false;
1938
- fs12.rmSync(file, { force: true });
3226
+ fs20.rmSync(file, { force: true });
1939
3227
  try {
1940
- fs12.writeFileSync(file, runId, { flag: "wx" });
3228
+ fs20.writeFileSync(file, runId, { flag: "wx" });
1941
3229
  return true;
1942
3230
  } catch {
1943
3231
  return false;
@@ -1946,394 +3234,47 @@ function tryClaim(file, runId) {
1946
3234
  function releaseIfOwner(file, runId) {
1947
3235
  if (!file) return;
1948
3236
  try {
1949
- if (fs12.readFileSync(file, "utf8").trim() === runId) fs12.rmSync(file, { force: true });
3237
+ if (fs20.readFileSync(file, "utf8").trim() === runId) fs20.rmSync(file, { force: true });
1950
3238
  } catch {
1951
3239
  }
1952
3240
  }
1953
3241
  function tryAcquireSlot(runId, maxParallel) {
1954
3242
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
1955
- const file = path12.join(slotsDir(), `slot-${n}`);
3243
+ const file = path19.join(slotsDir(), `slot-${n}`);
1956
3244
  if (tryClaim(file, runId)) return file;
1957
3245
  }
1958
3246
  return void 0;
1959
3247
  }
1960
3248
  var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
1961
3249
  function slotHolders() {
1962
- if (!fs12.existsSync(slotsDir())) return [];
1963
- return fs12.readdirSync(slotsDir()).map((f) => {
3250
+ if (!fs20.existsSync(slotsDir())) return [];
3251
+ return fs20.readdirSync(slotsDir()).map((f) => {
1964
3252
  try {
1965
- return fs12.readFileSync(path12.join(slotsDir(), f), "utf8").trim();
3253
+ return fs20.readFileSync(path19.join(slotsDir(), f), "utf8").trim();
1966
3254
  } catch {
1967
3255
  return "";
1968
3256
  }
1969
- }).filter((id) => holderActive(id, ""));
3257
+ }).filter((id2) => holderActive(id2, ""));
1970
3258
  }
1971
- var lockFile = (repoRoot) => path12.join(locksDir(), `write-${crypto2.createHash("sha1").update(path12.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
3259
+ var lockFile = (repoRoot) => path19.join(locksDir(), `write-${crypto4.createHash("sha1").update(path19.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
1972
3260
  function acquireWriteLock(repoRoot, runId) {
1973
3261
  const file = lockFile(repoRoot);
1974
3262
  if (tryClaim(file, runId)) return;
1975
3263
  let owner = "";
1976
3264
  try {
1977
- owner = fs12.readFileSync(file, "utf8").trim();
3265
+ owner = fs20.readFileSync(file, "utf8").trim();
1978
3266
  } catch {
1979
3267
  }
1980
3268
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
1981
3269
  }
1982
3270
  var releaseWriteLock = (repoRoot, runId) => releaseIfOwner(lockFile(repoRoot), runId);
1983
3271
 
1984
- // src/core/secrets.ts
1985
- import { execFileSync } from "node:child_process";
1986
- import fs13 from "node:fs";
1987
- import path13 from "node:path";
1988
- var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
1989
- var MAX_VISITED = 2e4;
1990
- var MAX_DEPTH = 4;
1991
- var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
1992
- function looksSecret(file) {
1993
- const name = file.toLowerCase();
1994
- if (TEMPLATE.test(name)) return false;
1995
- if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
1996
- return /\.(pem|p12|pfx|key|jks|keystore)$/.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/.test(name);
1997
- }
1998
- function findSecretFiles(dir) {
1999
- const found = [];
2000
- let visited = 0;
2001
- const walk = (d, depth) => {
2002
- if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
2003
- let entries;
2004
- try {
2005
- entries = fs13.readdirSync(d, { withFileTypes: true });
2006
- } catch {
2007
- return;
2008
- }
2009
- for (const e of entries) {
2010
- visited++;
2011
- if (e.isDirectory()) {
2012
- if (!SKIP_DIRS2.has(e.name)) walk(path13.join(d, e.name), depth + 1);
2013
- } else if (e.isFile() && looksSecret(e.name)) {
2014
- found.push(slash(path13.relative(dir, path13.join(d, e.name))));
2015
- }
2016
- }
2017
- };
2018
- walk(dir, 0);
2019
- return found.sort();
2020
- }
2021
- function findSecretFilesInTree(root, dir) {
2022
- try {
2023
- const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
2024
- return out.split("\0").filter((f) => f && looksSecret(path13.basename(f)) && !path13.relative(dir, path13.join(root, f)).startsWith("..")).map((f) => slash(path13.relative(dir, path13.join(root, f)))).sort();
2025
- } catch {
2026
- return [];
2027
- }
2028
- }
2029
- function secretWarning(files, mode) {
2030
- if (!files.length) return void 0;
2031
- const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
2032
- const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
2033
- const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
2034
- return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
2035
- }
2036
- var slash = (p) => p.split(path13.sep).join("/");
2037
-
2038
- // src/core/receipt.ts
2039
- import fs16 from "node:fs";
2040
-
2041
- // src/core/config.ts
2042
- import fs14 from "node:fs";
2043
- import os7 from "node:os";
2044
- import path14 from "node:path";
2045
- var SCHEMA = {
2046
- worker: "string",
2047
- fallback: "string[]",
2048
- timeout: "string",
2049
- primary: "string",
2050
- price: "string",
2051
- link: "string[]",
2052
- web: "boolean",
2053
- maxParallel: "number",
2054
- models: "record",
2055
- tiers: "record",
2056
- costs: "numbers",
2057
- audit: "number",
2058
- readIn: "string",
2059
- cacheDays: "number",
2060
- countRateLimits: "boolean",
2061
- mcpDash: "boolean",
2062
- notify: "boolean",
2063
- notifyCommand: "string",
2064
- notifyAfter: "number",
2065
- workerPrices: "record",
2066
- priceFeed: "boolean",
2067
- priceFeedUrl: "string",
2068
- priceFeedHours: "number"
2069
- };
2070
- function configPath() {
2071
- if (process.env.PITROOM_CONFIG) return path14.resolve(process.env.PITROOM_CONFIG);
2072
- const base = process.platform === "win32" ? process.env.APPDATA ?? path14.join(os7.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path14.join(os7.homedir(), ".config");
2073
- return path14.join(base, "pitroom", "config.json");
2074
- }
2075
- var cached;
2076
- function loadConfig() {
2077
- if (cached) return cached;
2078
- const file = configPath();
2079
- const config = {};
2080
- const warnings = [];
2081
- if (fs14.existsSync(file)) {
2082
- let raw;
2083
- try {
2084
- raw = JSON.parse(fs14.readFileSync(file, "utf8"));
2085
- } catch (e) {
2086
- warnings.push(`${file}: invalid JSON (${e.message}); ignored`);
2087
- }
2088
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
2089
- for (const [key, value] of Object.entries(raw)) {
2090
- const type = SCHEMA[key];
2091
- if (!type) {
2092
- warnings.push(`${file}: unknown key "${key}" ignored`);
2093
- } else if (matches(value, type)) {
2094
- config[key] = value;
2095
- } else {
2096
- warnings.push(`${file}: "${key}" must be ${type}; ignored`);
2097
- }
2098
- }
2099
- }
2100
- }
2101
- cached = { config, warnings };
2102
- return cached;
2103
- }
2104
- function matches(v, type) {
2105
- if (type === "string[]") return Array.isArray(v) && v.every((s) => typeof s === "string");
2106
- if (type === "record") {
2107
- return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((s) => typeof s === "string");
2108
- }
2109
- if (type === "numbers") {
2110
- return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
2111
- }
2112
- if (type === "number") return typeof v === "number" && Number.isFinite(v) && v >= 0;
2113
- return typeof v === type;
2114
- }
2115
- var DEFAULT_PARALLEL = 20;
2116
- var MAX_PARALLEL_LIMIT = 30;
2117
- var clampParallel = (s) => ({ ...s, value: Math.min(s.value, MAX_PARALLEL_LIMIT) });
2118
- var positiveInt = (v) => {
2119
- const n = Number(v);
2120
- return v !== void 0 && v !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
2121
- };
2122
- var rate = (v) => {
2123
- const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
2124
- return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
2125
- };
2126
- var days = (v) => {
2127
- const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
2128
- return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
2129
- };
2130
- var flag01 = (v) => {
2131
- const t = v?.trim().toLowerCase();
2132
- return t === void 0 || t === "" ? void 0 : ["1", "true", "on", "yes"].includes(t) ? true : ["0", "false", "off", "no"].includes(t) ? false : void 0;
2133
- };
2134
- var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
2135
- var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
2136
- function setting(flag, env, conf, fallback) {
2137
- if (flag !== void 0) return { value: flag, source: "flag" };
2138
- if (env !== void 0) return { value: env, source: "env" };
2139
- if (conf !== void 0) return { value: conf, source: "config" };
2140
- return { value: fallback, source: "default" };
2141
- }
2142
- function effective(flags = {}) {
2143
- const c = loadConfig().config;
2144
- const e = process.env;
2145
- return {
2146
- worker: setting(flags.worker, e.PITROOM_WORKER, c.worker, DEFAULT_BACKEND),
2147
- /** Overrides the model of the preferred worker only. */
2148
- model: setting(flags.model, e.PITROOM_MODEL, void 0, void 0),
2149
- fallback: setting(void 0, list(e.PITROOM_FALLBACK), c.fallback, []),
2150
- timeout: setting(flags.timeout, e.PITROOM_TIMEOUT, c.timeout, "30m"),
2151
- primary: setting(void 0, e.PITROOM_PRIMARY, c.primary, "sonnet"),
2152
- price: setting(void 0, e.PITROOM_PRICE, c.price, void 0),
2153
- link: setting(void 0, void 0, c.link, []),
2154
- web: setting(void 0, void 0, c.web, false),
2155
- maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
2156
- models: setting(void 0, void 0, c.models, {}),
2157
- tiers: setting(void 0, void 0, c.tiers, {}),
2158
- costs: setting(void 0, void 0, c.costs, {}),
2159
- audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
2160
- readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
2161
- cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
2162
- countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
2163
- mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
2164
- notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
2165
- notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
2166
- notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
2167
- workerPrices: setting(void 0, void 0, c.workerPrices, {}),
2168
- priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
2169
- priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
2170
- priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24)
2171
- };
2172
- }
2173
-
2174
- // src/core/prices.ts
2175
- import fs15 from "node:fs";
2176
- import path15 from "node:path";
2177
- var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
2178
- var catalogFile = () => path15.join(home(), "prices.json");
2179
- function trim(raw) {
2180
- const out = {};
2181
- if (!raw || typeof raw !== "object") return out;
2182
- for (const [provider, p] of Object.entries(raw)) {
2183
- for (const [id, m] of Object.entries(p?.models ?? {})) {
2184
- const c = m?.cost;
2185
- const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
2186
- if (!c || !ok(c.input) || !ok(c.output)) continue;
2187
- out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
2188
- }
2189
- }
2190
- return out;
2191
- }
2192
- var cached2;
2193
- function readCatalog() {
2194
- const file = catalogFile();
2195
- try {
2196
- const { mtimeMs } = fs15.statSync(file);
2197
- if (cached2?.file === file && cached2.mtimeMs === mtimeMs) return cached2.catalog;
2198
- const parsed = JSON.parse(fs15.readFileSync(file, "utf8"));
2199
- const catalog5 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
2200
- cached2 = { file, mtimeMs, catalog: catalog5 };
2201
- return catalog5;
2202
- } catch {
2203
- return void 0;
2204
- }
2205
- }
2206
- var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
2207
- function catalogPrice(backend, ...models) {
2208
- const catalog5 = readCatalog();
2209
- if (!catalog5) return void 0;
2210
- for (const raw of models) {
2211
- const id = raw?.split("#")[0];
2212
- if (!id) continue;
2213
- const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
2214
- const any = keys.find((k) => catalog5.models[k]) ?? Object.keys(catalog5.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
2215
- const triple = any ? catalog5.models[any] : void 0;
2216
- if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
2217
- const [input, output, cachedInput] = triple;
2218
- return { input, output, cachedInput, key: any };
2219
- }
2220
- }
2221
- return void 0;
2222
- }
2223
-
2224
- // src/core/receipt.ts
2225
- var PRESETS = {
2226
- sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
2227
- opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
2228
- haiku: { name: "Claude Haiku", input: 1, output: 5, cachedInput: 0.1 },
2229
- "gpt-5": { name: "GPT-5", input: 1.25, output: 10, cachedInput: 0.125 }
2230
- };
2231
- function primaryPrice() {
2232
- const eff = effective();
2233
- const custom = eff.price.value?.split(",").map(Number);
2234
- if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
2235
- return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
2236
- }
2237
- const named = eff.primary.value;
2238
- const preset = PRESETS[named.toLowerCase()];
2239
- if (preset) return preset;
2240
- const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
2241
- return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
2242
- }
2243
- var estimateTokens = (text) => Math.ceil(text.length / 4);
2244
- function parsePriceSpec(spec, name = "worker") {
2245
- const parts = typeof spec === "string" ? spec.split(",").map((x) => Number(x.trim())) : [];
2246
- if (parts.length < 2 || parts.length > 3 || !parts.every((n) => Number.isFinite(n) && n >= 0)) return void 0;
2247
- return { name, source: "config", input: parts[0], output: parts[1], cachedInput: parts[2] ?? parts[0] / 10 };
2248
- }
2249
- var costAt = (usage, price) => (usage.input * price.input + usage.cacheRead * price.cachedInput + (usage.output + usage.reasoning) * price.output) / 1e6;
2250
- var workerCostOf = (usage) => usage?.cost ?? usage?.costEstimate;
2251
- function savedUsd(usage, returnedTokens, price = primaryPrice()) {
2252
- const readingTheReport = returnedTokens * price.input / 1e6;
2253
- return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
2254
- }
2255
- function readLedger(sinceMs) {
2256
- if (!fs16.existsSync(ledgerFile())) return [];
2257
- return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2258
- try {
2259
- return [JSON.parse(l)];
2260
- } catch {
2261
- return [];
2262
- }
2263
- }).filter((e) => !sinceMs || Date.parse(e.at) >= sinceMs);
2264
- }
2265
- function totals(entries) {
2266
- const t = entries.reduce(
2267
- (a, e) => ({
2268
- runs: a.runs + 1,
2269
- tokens: a.tokens + e.tokens,
2270
- returned: a.returned + e.returned,
2271
- workerCost: a.workerCost + e.workerCost,
2272
- saved: a.saved + e.saved
2273
- }),
2274
- { runs: 0, tokens: 0, returned: 0, workerCost: 0, saved: 0 }
2275
- );
2276
- return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
2277
- }
2278
- var usd = (n) => n >= 100 ? `$${n.toFixed(0)}` : n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
2279
- function compact(n) {
2280
- const fmt = (v) => v.toFixed(v >= 100 ? 0 : 1).replace(/\.0$/, "");
2281
- if (n >= 1e6) return `${fmt(n / 1e6)}M`;
2282
- if (n >= 1e3) return `${fmt(n / 1e3)}k`;
2283
- return String(Math.round(n));
2284
- }
2285
- function badgeUrl(t) {
2286
- const msg = encodeURIComponent(`saved ${usd(t.saved)} \xB7 ${compact(t.tokens)} tokens offloaded`).replace(/-/g, "--");
2287
- return `https://img.shields.io/badge/pitroom-${msg}-7c3aed`;
2288
- }
2289
- var esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
2290
- function card(t, period) {
2291
- const W = 720, H = 316, M = 32, GAP = 12;
2292
- const tile = (i, value, label) => {
2293
- const w = (W - 2 * M - 2 * GAP) / 3, x = M + i * (w + GAP);
2294
- return `<rect x="${x}" y="196" width="${w}" height="72" rx="14" fill="#fff" fill-opacity=".035" stroke="#fff" stroke-opacity=".1"/>
2295
- <text x="${x + 18}" y="224" class="lab">${esc(label.toUpperCase())}</text><text x="${x + 18}" y="254" class="val">${esc(value)}</text>`;
2296
- };
2297
- const when = period.toUpperCase();
2298
- const pill = 22 + when.length * 7.4;
2299
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="pitroom saved ${esc(usd(t.saved))}">
2300
- <style>
2301
- text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Inter,Helvetica,Arial,sans-serif;fill:#f4f4f5}
2302
- .name{font-size:20px;font-weight:650;letter-spacing:-.01em}
2303
- .sub{font-size:12.5px;fill:#a1a1aa}
2304
- .pill{font-size:11px;font-weight:600;letter-spacing:.1em;fill:#a1a1aa}
2305
- .lab{font-size:11px;font-weight:500;letter-spacing:.09em;fill:#a1a1aa}
2306
- .big{font-size:60px;font-weight:700;letter-spacing:-.03em;fill:#3ddc97}
2307
- .val{font-size:26px;font-weight:650;letter-spacing:-.02em}
2308
- .foot{font-size:11.5px;fill:#71717a}
2309
- </style>
2310
- <defs>
2311
- <radialGradient id="o" cx="1" cy="0" r=".7"><stop offset="0" stop-color="#ff6a2b" stop-opacity=".24"/><stop offset="1" stop-color="#ff6a2b" stop-opacity="0"/></radialGradient>
2312
- <radialGradient id="b" cx="0" cy="0" r=".7"><stop offset="0" stop-color="#38bdf8" stop-opacity=".16"/><stop offset="1" stop-color="#38bdf8" stop-opacity="0"/></radialGradient>
2313
- <linearGradient id="m" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ff6a2b"/><stop offset="1" stop-color="#ffa86b"/></linearGradient>
2314
- <clipPath id="c"><rect width="${W}" height="${H}" rx="20"/></clipPath>
2315
- </defs>
2316
- <g clip-path="url(#c)"><rect width="${W}" height="${H}" fill="#0b0b0f"/><rect width="${W}" height="${H}" fill="url(#o)"/><rect width="${W}" height="${H}" fill="url(#b)"/></g>
2317
- <rect x=".5" y=".5" width="${W - 1}" height="${H - 1}" rx="19.5" fill="none" stroke="#fff" stroke-opacity=".1"/>
2318
- <rect x="${M}" y="28" width="38" height="38" rx="11" fill="url(#m)"/>
2319
- <path d="M4 4h4v4H4zm8 0h4v4h-4zM8 8h4v4H8zm8 0h4v4h-4zM4 12h4v4H4zm8 0h4v4h-4zm-4 4h4v4H8zm8 0h4v4h-4z" fill="#fff" transform="translate(${M + 7} 35) scale(1)"/>
2320
- <text x="${M + 52}" y="45" class="name">Pitroom</text><text x="${M + 52}" y="63" class="sub">Your agent's pit crew</text>
2321
- <rect x="${W - M - pill}" y="34" width="${pill}" height="26" rx="13" fill="#fff" fill-opacity=".04" stroke="#fff" stroke-opacity=".14"/>
2322
- <text x="${W - M - pill / 2}" y="51" class="pill" text-anchor="middle">${esc(when)}</text>
2323
- <text x="${M}" y="112" class="lab">SAVED ON YOUR MAIN CODING AGENT</text>
2324
- <text x="${M}" y="168" class="big">${esc(usd(t.saved))}</text>
2325
- ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.round(t.ratio)}\xD7` : "\u2014", "Context compression")}${tile(2, String(t.runs), "Delegated tasks")}
2326
- <text x="${M}" y="296" class="foot">Estimated against ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
2327
- </svg>
2328
- `;
2329
- }
2330
-
2331
3272
  // src/core/process.ts
2332
3273
  import { spawn } from "node:child_process";
2333
- import fs17 from "node:fs";
3274
+ import fs21 from "node:fs";
2334
3275
  async function spawnWorker(inv, opts) {
2335
- const out = fs17.openSync(opts.stdoutFile, "w");
2336
- const err = fs17.openSync(opts.stderrFile, "w");
3276
+ const out = fs21.openSync(opts.stdoutFile, "w");
3277
+ const err = fs21.openSync(opts.stderrFile, "w");
2337
3278
  const res = { code: null, timedOut: false, stopped: false };
2338
3279
  const child = spawn(inv.command, inv.args, {
2339
3280
  cwd: opts.cwd,
@@ -2356,7 +3297,7 @@ async function spawnWorker(inv, opts) {
2356
3297
  }, opts.timeoutSec * 1e3);
2357
3298
  res.code = await new Promise((resolve2) => {
2358
3299
  child.on("error", (e) => {
2359
- res.spawnError = e.code === "ENOENT" ? fs17.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
3300
+ res.spawnError = e.code === "ENOENT" ? fs21.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
2360
3301
  resolve2(127);
2361
3302
  });
2362
3303
  child.on("close", (c) => resolve2(c));
@@ -2365,16 +3306,19 @@ async function spawnWorker(inv, opts) {
2365
3306
  if (killer) clearTimeout(killer);
2366
3307
  process.off("SIGINT", stop);
2367
3308
  process.off("SIGTERM", stop);
2368
- fs17.closeSync(out);
2369
- fs17.closeSync(err);
3309
+ fs21.closeSync(out);
3310
+ fs21.closeSync(err);
2370
3311
  return res;
2371
3312
  }
2372
3313
  export {
3314
+ ASKS_FOR_LIST,
2373
3315
  DEFAULT_BACKEND,
3316
+ FORBIDDEN_COMMANDS,
2374
3317
  STOP_FILE,
2375
3318
  TERMINAL,
2376
3319
  acquireWriteLock,
2377
3320
  allBackends,
3321
+ answerParts,
2378
3322
  backendIds,
2379
3323
  badgeUrl,
2380
3324
  brief,
@@ -2389,6 +3333,8 @@ export {
2389
3333
  fill,
2390
3334
  findSecretFiles,
2391
3335
  findSecretFilesInTree,
3336
+ fixTask,
3337
+ focusedRate,
2392
3338
  formatTarget,
2393
3339
  freshMeta,
2394
3340
  getBackend,
@@ -2396,7 +3342,9 @@ export {
2396
3342
  home,
2397
3343
  isActive,
2398
3344
  isAlive,
3345
+ linkInto,
2399
3346
  listRunIds,
3347
+ loadEval,
2400
3348
  loadPlan,
2401
3349
  loadTemplate,
2402
3350
  looksSecret,
@@ -2407,9 +3355,11 @@ export {
2407
3355
  parseStatus,
2408
3356
  parseTarget,
2409
3357
  parseVerdict,
3358
+ pathsIn,
2410
3359
  planName,
2411
3360
  planTask,
2412
3361
  primaryPrice,
3362
+ rankChain,
2413
3363
  readCatalog,
2414
3364
  readLedger,
2415
3365
  readMeta,
@@ -2418,20 +3368,26 @@ export {
2418
3368
  requestStop,
2419
3369
  resolveRun,
2420
3370
  retryAfterMs,
3371
+ retryCommands,
2421
3372
  runDir,
2422
3373
  runFile,
2423
3374
  runsDir,
2424
3375
  savedUsd,
3376
+ scoreAnswer,
3377
+ scoreOf,
2425
3378
  secretWarning,
2426
3379
  shimDir,
2427
3380
  shimReady,
2428
3381
  slotHolders,
2429
3382
  spawnWorker,
3383
+ summarize,
2430
3384
  totals,
2431
3385
  trim,
3386
+ truthOf,
2432
3387
  tryAcquireSlot,
2433
3388
  usd,
2434
3389
  verifyRefs,
2435
3390
  workerCostOf,
3391
+ writeIfChanged,
2436
3392
  writeMeta
2437
3393
  };