pitroom 0.8.1 → 0.10.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/pitroom.mjs CHANGED
@@ -216,7 +216,8 @@ var DENY_BASH = [
216
216
  "pitroom",
217
217
  "opencode",
218
218
  "claude",
219
- "codex"
219
+ "codex",
220
+ "gemini"
220
221
  ].map((c) => `Bash(${c}:*)`);
221
222
  var SECRETS = ["**/.env", "**/.env.*", "**/*.pem", "**/id_rsa*", "**/id_ed25519*"].map((p) => `Read(${p})`);
222
223
  function toolsFor(mode, web2) {
@@ -269,8 +270,8 @@ function failure(run, stderr, exitCode) {
269
270
  }
270
271
  function defaultModel() {
271
272
  try {
272
- const settings = JSON.parse(fs2.readFileSync(path2.join(os2.homedir(), ".claude", "settings.json"), "utf8"));
273
- return typeof settings.model === "string" ? settings.model : void 0;
273
+ const settings2 = JSON.parse(fs2.readFileSync(path2.join(os2.homedir(), ".claude", "settings.json"), "utf8"));
274
+ return typeof settings2.model === "string" ? settings2.model : void 0;
274
275
  } catch {
275
276
  return void 0;
276
277
  }
@@ -549,13 +550,377 @@ var codex = {
549
550
  doctor: doctor2
550
551
  };
551
552
 
552
- // src/backends/opencode/index.ts
553
+ // src/backends/gemini/index.ts
553
554
  import { spawnSync as spawnSync3 } from "node:child_process";
555
+ import fs5 from "node:fs";
556
+ import os5 from "node:os";
557
+ import path5 from "node:path";
558
+ import { fileURLToPath } from "node:url";
559
+
560
+ // src/core/store.ts
561
+ import fs4 from "node:fs";
562
+ import os4 from "node:os";
563
+ import path4 from "node:path";
564
+ import crypto from "node:crypto";
565
+ var TERMINAL = ["done", "failed", "timeout", "stopped"];
566
+ var isActive = (s) => !TERMINAL.includes(s);
567
+ function home() {
568
+ if (process.env.PITROOM_HOME) return path4.resolve(process.env.PITROOM_HOME);
569
+ if (process.platform === "win32") {
570
+ return path4.join(process.env.LOCALAPPDATA ?? path4.join(os4.homedir(), "AppData", "Local"), "pitroom");
571
+ }
572
+ return path4.join(process.env.XDG_STATE_HOME ?? path4.join(os4.homedir(), ".local", "state"), "pitroom");
573
+ }
574
+ var runsDir = () => path4.join(home(), "runs");
575
+ var worktreesDir = () => path4.join(home(), "worktrees");
576
+ var ledgerFile = () => path4.join(home(), "ledger.jsonl");
577
+ var runDir = (id) => path4.join(runsDir(), id);
578
+ var runFile = (id, name) => path4.join(runDir(id), name);
579
+ function newRunId(now = /* @__PURE__ */ new Date()) {
580
+ const p = (n) => String(n).padStart(2, "0");
581
+ const stamp2 = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
582
+ return `${stamp2}-${crypto.randomBytes(2).toString("hex")}`;
583
+ }
584
+ var onFinished;
585
+ var archive;
586
+ function useArchive(hooks) {
587
+ onFinished = hooks.onFinished;
588
+ archive = { meta: hooks.meta, id: hooks.id };
589
+ }
590
+ function writeMeta(meta) {
591
+ fs4.mkdirSync(runDir(meta.id), { recursive: true });
592
+ const file = runFile(meta.id, "meta.json");
593
+ const tmp = `${file}.${process.pid}.tmp`;
594
+ fs4.writeFileSync(tmp, JSON.stringify(meta, null, 2));
595
+ fs4.renameSync(tmp, file);
596
+ if (TERMINAL.includes(meta.state)) {
597
+ try {
598
+ onFinished?.(meta);
599
+ } catch {
600
+ }
601
+ }
602
+ }
603
+ function readMeta(id) {
604
+ const file = runFile(id, "meta.json");
605
+ if (!fs4.existsSync(file)) {
606
+ const kept = archive?.meta(id);
607
+ if (kept) return upgrade(kept);
608
+ }
609
+ return upgrade(JSON.parse(fs4.readFileSync(file, "utf8")));
610
+ }
611
+ function upgrade(raw) {
612
+ if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
613
+ if (!Array.isArray(raw.fallback)) {
614
+ raw.fallback = (raw.fallbackModels ?? []).map((m) => ({ backend: "opencode", model: m }));
615
+ }
616
+ raw.warnings ??= [];
617
+ raw.files ??= [];
618
+ raw.link ??= [];
619
+ return raw;
620
+ }
621
+ function listRunIds() {
622
+ if (!fs4.existsSync(runsDir())) return [];
623
+ return fs4.readdirSync(runsDir()).filter((d) => fs4.existsSync(runFile(d, "meta.json"))).sort();
624
+ }
625
+ function resolveRun(ref) {
626
+ const ids = listRunIds();
627
+ if (!ref || ref === "latest" || ref === "last") {
628
+ if (!ids.length) throw new UserError("no runs yet");
629
+ return ids[ids.length - 1];
630
+ }
631
+ if (ids.includes(ref)) return ref;
632
+ const hits = ids.filter((id) => id.startsWith(ref) || id.endsWith(ref));
633
+ if (hits.length === 1) return hits[0];
634
+ if (!hits.length) {
635
+ const kept = archive?.id(ref);
636
+ if (kept) return kept;
637
+ }
638
+ throw new UserError(hits.length ? `ambiguous run "${ref}": ${hits.join(", ")}` : ids.length ? `unknown run "${ref}"` : "no runs yet");
639
+ }
640
+ function isAlive(pid) {
641
+ if (!pid) return false;
642
+ try {
643
+ process.kill(pid, 0);
644
+ return true;
645
+ } catch (e) {
646
+ return e.code === "EPERM";
647
+ }
648
+ }
649
+ function freshMeta(id) {
650
+ const meta = readMeta(id);
651
+ if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
652
+ const latest = readMeta(id);
653
+ if (TERMINAL.includes(latest.state)) return latest;
654
+ latest.state = "failed";
655
+ latest.error ??= "worker process exited unexpectedly";
656
+ latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
657
+ writeMeta(latest);
658
+ return latest;
659
+ }
660
+ return meta;
661
+ }
662
+
663
+ // src/backends/gemini/events.ts
664
+ var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["replace", "write_file", "edit"]);
665
+ var DENIED2 = /policy|permission|not allowed|denied|blocked|disallowed|refus/i;
666
+ function parseEvents3(jsonl) {
667
+ const usage2 = {
668
+ input: 0,
669
+ output: 0,
670
+ reasoning: 0,
671
+ cacheRead: 0,
672
+ cacheWrite: 0,
673
+ total: 0,
674
+ steps: 0,
675
+ toolCalls: 0,
676
+ denied: 0
677
+ };
678
+ const tools = {};
679
+ const edits = [];
680
+ const pendingEdits = /* @__PURE__ */ new Map();
681
+ const timeline = [];
682
+ const stepOf = /* @__PURE__ */ new Map();
683
+ let sessionId;
684
+ let model;
685
+ let text = "";
686
+ let lastText = "";
687
+ let lastActivity;
688
+ let error;
689
+ let turns = 0;
690
+ let inTurn = false;
691
+ const flush = () => {
692
+ const said = text.trim();
693
+ text = "";
694
+ if (!said) return;
695
+ lastText = said;
696
+ lastActivity = `says: ${oneLine3(said)}`;
697
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip3(said, 600) });
698
+ };
699
+ const startTurn = () => {
700
+ if (!inTurn) turns++;
701
+ inTurn = true;
702
+ };
703
+ for (const line of jsonl.split("\n")) {
704
+ if (!line.trim()) continue;
705
+ let e;
706
+ try {
707
+ e = JSON.parse(line);
708
+ } catch {
709
+ continue;
710
+ }
711
+ if (!e || typeof e !== "object") continue;
712
+ if (e.type === "init") {
713
+ sessionId ??= e.session_id;
714
+ model ??= e.model;
715
+ } else if (e.type === "message") {
716
+ if (e.role !== "assistant") continue;
717
+ startTurn();
718
+ if (!e.delta && text) flush();
719
+ text += String(e.content ?? "");
720
+ } else if (e.type === "tool_use") {
721
+ flush();
722
+ lastText = "";
723
+ startTurn();
724
+ const name = String(e.tool_name ?? "tool");
725
+ const p = e.parameters ?? {};
726
+ usage2.toolCalls++;
727
+ tools[name] = (tools[name] ?? 0) + 1;
728
+ const target = p.file_path ?? p.absolute_path ?? p.path ?? p.dir_path ?? p.pattern ?? p.command ?? p.query ?? p.url ?? p.prompt;
729
+ lastActivity = `${name} ${target ? oneLine3(String(target), 60) : ""}`.trim();
730
+ const file = p.file_path ?? p.absolute_path ?? p.path;
731
+ if (EDIT_TOOLS2.has(name) && file) pendingEdits.set(String(e.tool_id), String(file));
732
+ if (timeline.length < MAX_STEPS) {
733
+ const step = { kind: EDIT_TOOLS2.has(name) ? "edit" : name === "run_shell_command" ? "shell" : "tool", name, text: clip3(String(target ?? ""), 240) };
734
+ timeline.push(step);
735
+ stepOf.set(String(e.tool_id), step);
736
+ }
737
+ } else if (e.type === "tool_result") {
738
+ inTurn = false;
739
+ const ok = e.status === "success";
740
+ const step = stepOf.get(String(e.tool_id));
741
+ if (step) step.ok = ok;
742
+ if (!ok && DENIED2.test(`${e.error?.type ?? ""} ${e.error?.message ?? ""}`)) usage2.denied++;
743
+ if (ok && pendingEdits.has(String(e.tool_id))) edits.push(pendingEdits.get(String(e.tool_id)));
744
+ } else if (e.type === "error") {
745
+ if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
746
+ } else if (e.type === "result") {
747
+ const s = e.stats ?? {};
748
+ const cached3 = num3(s.cached);
749
+ usage2.cacheRead += cached3;
750
+ usage2.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached3);
751
+ usage2.output += num3(s.output_tokens);
752
+ usage2.total += num3(s.total_tokens) || num3(s.input_tokens) + num3(s.output_tokens);
753
+ usage2.toolCalls = Math.max(usage2.toolCalls, num3(s.tool_calls));
754
+ model ??= Object.keys(s.models ?? {})[0];
755
+ if (e.status === "error") error ??= String(e.error?.message ?? e.error?.type ?? "Gemini CLI error");
756
+ }
757
+ }
758
+ flush();
759
+ usage2.steps = turns;
760
+ return { sessionId, model, finalText: error ? "" : lastText, usage: usage2, tools, edits, lastActivity, timeline, error };
761
+ }
762
+ var clip3 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
763
+ function num3(v) {
764
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
765
+ }
766
+ function oneLine3(s, max = 80) {
767
+ const t = s.replace(/\s+/g, " ").trim();
768
+ return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
769
+ }
770
+
771
+ // src/backends/gemini/index.ts
772
+ var binary3 = () => findBinary("gemini", "PITROOM_GEMINI_BIN", ["~/.local/bin/gemini"]);
773
+ function gem(args, timeout = 6e4) {
774
+ const { command, prefix } = resolveCommand(binary3());
775
+ const r = spawnSync3(command, [...prefix, ...args], {
776
+ encoding: "utf8",
777
+ timeout,
778
+ stdio: ["ignore", "pipe", "pipe"],
779
+ maxBuffer: 16 * 1024 * 1024
780
+ });
781
+ return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
782
+ }
783
+ var policyDir = () => path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "..", "policies", "gemini");
784
+ function workerHome() {
785
+ const root = path5.join(home(), "gemini-home");
786
+ const dir = path5.join(root, ".gemini");
787
+ const base2 = JSON.parse(fs5.readFileSync(path5.join(policyDir(), "worker-settings.json"), "utf8"));
788
+ const auth = settings().security?.auth;
789
+ const body = `${JSON.stringify(auth ? { ...base2, security: { ...base2.security, auth } } : base2, null, 2)}
790
+ `;
791
+ const file = path5.join(dir, "settings.json");
792
+ try {
793
+ if (fs5.readFileSync(file, "utf8") === body) return root;
794
+ } catch {
795
+ }
796
+ fs5.mkdirSync(dir, { recursive: true });
797
+ fs5.writeFileSync(file, body);
798
+ for (const name of ["oauth_creds.json", "google_accounts.json"]) {
799
+ const from = path5.join(geminiHome(), name);
800
+ const to = path5.join(dir, name);
801
+ try {
802
+ if (fs5.existsSync(from) && !fs5.existsSync(to)) fs5.symlinkSync(from, to);
803
+ } catch {
804
+ }
805
+ }
806
+ return root;
807
+ }
808
+ function invocation3(req) {
809
+ const dir = policyDir();
810
+ const policies = ["base.toml", ...req.web ? [] : ["no-web.toml"], ...req.mode === "read" ? [] : ["shell.toml"]];
811
+ const args = [
812
+ // A prompt that starts with "-" would be read as an option: a leading space keeps it a value.
813
+ "--prompt",
814
+ req.prompt.startsWith("-") ? ` ${req.prompt}` : req.prompt,
815
+ "--output-format",
816
+ "stream-json",
817
+ "--approval-mode",
818
+ req.mode === "read" ? "plan" : "auto_edit",
819
+ "-e",
820
+ "none"
821
+ ];
822
+ for (const p of policies) args.push("--policy", path5.join(dir, p));
823
+ if (process.env.PITROOM_GEMINI_TRUST === "1") args.push("--skip-trust");
824
+ const [model] = (req.model ?? "").split("#");
825
+ if (model) args.push("--model", model);
826
+ const { command, prefix } = resolveCommand(binary3());
827
+ const env = { GEMINI_CLI_HOME: workerHome() };
828
+ const trusted = path5.join(geminiHome(), "trustedFolders.json");
829
+ if (fs5.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
830
+ return { command, args: [...prefix, ...args], env };
831
+ }
832
+ function classify3(message) {
833
+ 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)) {
834
+ return "auth";
835
+ }
836
+ if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
837
+ if (/model.{0,80}(not found|not available|no longer available|does not exist|unsupported|invalid)|\b404\b|NOT_FOUND|invalid model|is not supported/i.test(message)) return "model-unavailable";
838
+ return "other";
839
+ }
840
+ 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)";
841
+ function failure3(run, stderr, exitCode) {
842
+ if (exitCode === 0 && !run.error) return void 0;
843
+ const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
844
+ const detail = lines.find((l) => /error|failed|exceeded|quota|exhausted|unsupported/i.test(l) && !/^at /.test(l)) ?? lines.at(-1);
845
+ const raw = run.error ?? detail ?? `gemini exited with code ${exitCode}`;
846
+ const message = /not running in a trusted directory/i.test(`${stderr} ${raw}`) ? UNTRUSTED : raw;
847
+ return { kind: classify3(message), message };
848
+ }
849
+ function geminiHome() {
850
+ return path5.join(process.env.GEMINI_CLI_HOME ?? os5.homedir(), ".gemini");
851
+ }
852
+ function settings() {
853
+ try {
854
+ return JSON.parse(fs5.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
855
+ } catch {
856
+ return {};
857
+ }
858
+ }
859
+ function defaultModel2() {
860
+ if (process.env.GEMINI_MODEL) return process.env.GEMINI_MODEL;
861
+ const m = settings().model;
862
+ const name = typeof m === "string" ? m : m?.name;
863
+ return typeof name === "string" ? name : void 0;
864
+ }
865
+ function doctor3({ models, hasFallback }) {
866
+ const version = gem(["--version"]);
867
+ if (version.missing || !version.ok) {
868
+ return [{ level: "fail", message: `gemini not runnable (${binary3()}). Install Gemini CLI (npm i -g @google/gemini-cli), or set PITROOM_GEMINI_BIN` }];
869
+ }
870
+ const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary3()}` }];
871
+ const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
872
+ const vertex = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true" || process.env.GOOGLE_GENAI_USE_VERTEXAI === "1";
873
+ const signedIn = fs5.existsSync(path5.join(geminiHome(), "oauth_creds.json"));
874
+ const type = settings().security?.auth?.selectedType;
875
+ if (key || vertex) {
876
+ checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
877
+ } else if (signedIn && type === "oauth-personal") {
878
+ checks.push({
879
+ level: "warn",
880
+ message: "Gemini CLI is signed in with a Google account (oauth-personal). If a run fails with IneligibleTierError, Google no longer serves that sign-in to this CLI: set GEMINI_API_KEY (Google AI Studio) instead"
881
+ });
882
+ } else if (signedIn) {
883
+ checks.push({ level: "ok", message: "Gemini CLI: signed in" });
884
+ } else {
885
+ checks.push({ level: "fail", message: "Gemini CLI is not signed in: run `gemini` once to sign in, or set GEMINI_API_KEY (Google AI Studio)" });
886
+ }
887
+ for (const m of models) {
888
+ const model = m ?? defaultModel2();
889
+ const pricey = model && /pro/i.test(model);
890
+ checks.push({
891
+ level: pricey ? "warn" : "ok",
892
+ message: pricey ? `Gemini model: ${model} is the largest tier for a worker; consider -W gemini:gemini-3.8-flash` : model ? `Gemini model: ${model}` : "Gemini model: Gemini CLI's own choice (it may pick a Pro model; a cheaper worker is -W gemini:gemini-3.8-flash)"
893
+ });
894
+ }
895
+ if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Gemini runs" });
896
+ return checks;
897
+ }
898
+ function catalog3() {
899
+ const def = defaultModel2();
900
+ 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 }));
901
+ if (def && !models.some((m) => m.id === def)) models.push({ id: def });
902
+ return { models, source: "Gemini CLI's built-in model names (it cannot list models); the effort suffix is not used" };
903
+ }
904
+ var gemini = {
905
+ id: "gemini",
906
+ name: "Gemini CLI",
907
+ capabilities: { readOnly: "approval-mode", resume: "none", reportsCost: false, attachFiles: false },
908
+ binary: binary3,
909
+ catalog: catalog3,
910
+ invocation: invocation3,
911
+ parse: parseEvents3,
912
+ failure: failure3,
913
+ defaultModel: defaultModel2,
914
+ doctor: doctor3
915
+ };
916
+
917
+ // src/backends/opencode/index.ts
918
+ import { spawnSync as spawnSync4 } from "node:child_process";
554
919
 
555
920
  // src/backends/opencode/events.ts
556
- var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit", "apply_patch"]);
557
- var DENIED2 = /rule which prevents you|permission denied|permission\.rejected/i;
558
- function parseEvents3(ndjson) {
921
+ var EDIT_TOOLS3 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit", "apply_patch"]);
922
+ var DENIED3 = /rule which prevents you|permission denied|permission\.rejected/i;
923
+ function parseEvents4(ndjson) {
559
924
  const usage2 = {
560
925
  input: 0,
561
926
  output: 0,
@@ -592,8 +957,8 @@ function parseEvents3(ndjson) {
592
957
  const key = String(p.messageID ?? "");
593
958
  if (!textByMessage.has(key)) textByMessage.set(key, []);
594
959
  textByMessage.get(key).push(text);
595
- lastActivity = `says: ${oneLine3(text)}`;
596
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip3(text.trim(), 600), at: stamp(e.timestamp) });
960
+ lastActivity = `says: ${oneLine4(text)}`;
961
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip4(text.trim(), 600), at: stamp(e.timestamp) });
597
962
  break;
598
963
  }
599
964
  case "tool_use": {
@@ -601,28 +966,28 @@ function parseEvents3(ndjson) {
601
966
  const st = p.state ?? {};
602
967
  usage2.toolCalls++;
603
968
  tools[name] = (tools[name] ?? 0) + 1;
604
- if (st.status === "error" && DENIED2.test(String(st.error ?? ""))) usage2.denied++;
605
- if (st.status === "completed" && EDIT_TOOLS2.has(name)) {
969
+ if (st.status === "error" && DENIED3.test(String(st.error ?? ""))) usage2.denied++;
970
+ if (st.status === "completed" && EDIT_TOOLS3.has(name)) {
606
971
  edits.push(String(st.input?.filePath ?? st.input?.path ?? name));
607
972
  }
608
973
  lastActivity = `${name} ${describe2(st.input)}`.trim();
609
974
  if (timeline.length < MAX_STEPS) {
610
- const kind2 = EDIT_TOOLS2.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
975
+ const kind2 = EDIT_TOOLS3.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
611
976
  const text = kind2 === "shell" ? String(st.input?.command ?? "") : String(st.input?.filePath ?? st.input?.path ?? st.input?.pattern ?? st.input?.url ?? "") || describe2(st.input);
612
- timeline.push({ kind: kind2, name, text: clip3(text, 240), ok: st.status === "completed" ? true : st.status === "error" ? false : void 0, at: stamp(e.timestamp) });
977
+ timeline.push({ kind: kind2, name, text: clip4(text, 240), ok: st.status === "completed" ? true : st.status === "error" ? false : void 0, at: stamp(e.timestamp) });
613
978
  }
614
979
  break;
615
980
  }
616
981
  case "step_finish": {
617
982
  const t = p.tokens ?? {};
618
983
  usage2.steps++;
619
- usage2.input += num3(t.input);
620
- usage2.output += num3(t.output);
621
- usage2.reasoning += num3(t.reasoning);
622
- usage2.cacheRead += num3(t.cache?.read);
623
- usage2.cacheWrite += num3(t.cache?.write);
624
- usage2.total += num3(t.total) || num3(t.input) + num3(t.output) + num3(t.reasoning) + num3(t.cache?.read);
625
- usage2.cost = (usage2.cost ?? 0) + num3(p.cost);
984
+ usage2.input += num4(t.input);
985
+ usage2.output += num4(t.output);
986
+ usage2.reasoning += num4(t.reasoning);
987
+ usage2.cacheRead += num4(t.cache?.read);
988
+ usage2.cacheWrite += num4(t.cache?.write);
989
+ usage2.total += num4(t.total) || num4(t.input) + num4(t.output) + num4(t.reasoning) + num4(t.cache?.read);
990
+ usage2.cost = (usage2.cost ?? 0) + num4(p.cost);
626
991
  break;
627
992
  }
628
993
  case "error": {
@@ -646,25 +1011,25 @@ function describeError(err) {
646
1011
  const tags = [err.type, inner, err.status && `HTTP ${err.status}`].filter(Boolean);
647
1012
  return tags.length ? `${message} [${tags.join(", ")}]` : message;
648
1013
  }
649
- var clip3 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1014
+ var clip4 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
650
1015
  var stamp = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
651
- function num3(v) {
1016
+ function num4(v) {
652
1017
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
653
1018
  }
654
- function oneLine3(s, max = 80) {
1019
+ function oneLine4(s, max = 80) {
655
1020
  const t = s.replace(/\s+/g, " ").trim();
656
1021
  return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
657
1022
  }
658
1023
  function describe2(input) {
659
1024
  if (!input || typeof input !== "object") return "";
660
1025
  const v = input.filePath ?? input.path ?? input.pattern ?? input.command ?? input.url ?? input.query;
661
- return v ? oneLine3(String(v), 60) : "";
1026
+ return v ? oneLine4(String(v), 60) : "";
662
1027
  }
663
1028
 
664
1029
  // src/backends/opencode/profiles.ts
665
- import fs4 from "node:fs";
666
- import os4 from "node:os";
667
- import path4 from "node:path";
1030
+ import fs6 from "node:fs";
1031
+ import os6 from "node:os";
1032
+ import path6 from "node:path";
668
1033
  var AGENT = { read: "pitroom-read", write: "pitroom-write" };
669
1034
  var READ_FILES = {
670
1035
  "*": "allow",
@@ -762,18 +1127,18 @@ var FORBIDDEN_BASH = [
762
1127
  "*pitroom*"
763
1128
  ];
764
1129
  function scratchDirs() {
765
- const data = process.env.XDG_DATA_HOME ?? path4.join(os4.homedir(), ".local", "share");
1130
+ const data = process.env.XDG_DATA_HOME ?? path6.join(os6.homedir(), ".local", "share");
766
1131
  const rules = {
767
1132
  "*": "deny",
768
- [path4.join(data, "opencode", "tool-output", "*")]: "allow",
769
- [path4.join(data, "opencode", "shell", "*", "*")]: "allow"
1133
+ [path6.join(data, "opencode", "tool-output", "*")]: "allow",
1134
+ [path6.join(data, "opencode", "shell", "*", "*")]: "allow"
770
1135
  };
771
- for (const tmp of /* @__PURE__ */ new Set([os4.tmpdir(), realpath(os4.tmpdir())])) rules[path4.join(tmp, "opencode", "*")] = "allow";
1136
+ for (const tmp of /* @__PURE__ */ new Set([os6.tmpdir(), realpath(os6.tmpdir())])) rules[path6.join(tmp, "opencode", "*")] = "allow";
772
1137
  return rules;
773
1138
  }
774
1139
  function realpath(p) {
775
1140
  try {
776
- return fs4.realpathSync(p);
1141
+ return fs6.realpathSync(p);
777
1142
  } catch {
778
1143
  return p;
779
1144
  }
@@ -861,10 +1226,10 @@ function isObject(v) {
861
1226
  }
862
1227
 
863
1228
  // src/backends/opencode/index.ts
864
- var binary3 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
1229
+ var binary4 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
865
1230
  function oc(args, opts = {}) {
866
- const { command, prefix } = resolveCommand(binary3());
867
- const r = spawnSync3(command, [...prefix, ...args], {
1231
+ const { command, prefix } = resolveCommand(binary4());
1232
+ const r = spawnSync4(command, [...prefix, ...args], {
868
1233
  encoding: "utf8",
869
1234
  timeout: opts.timeout ?? 6e4,
870
1235
  env: opts.env ?? process.env,
@@ -874,7 +1239,7 @@ function oc(args, opts = {}) {
874
1239
  return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
875
1240
  }
876
1241
  var jsonIn = (s) => JSON.parse(s.slice(s.indexOf("{")));
877
- function invocation3(req) {
1242
+ function invocation4(req) {
878
1243
  const args = [
879
1244
  "run",
880
1245
  "--standalone",
@@ -893,7 +1258,7 @@ function invocation3(req) {
893
1258
  if (req.sessionId) args.push("--session", req.sessionId);
894
1259
  for (const f of req.files) args.push("--file", f);
895
1260
  args.push(req.prompt);
896
- const { command, prefix } = resolveCommand(binary3());
1261
+ const { command, prefix } = resolveCommand(binary4());
897
1262
  return {
898
1263
  command,
899
1264
  args: [...prefix, ...args],
@@ -908,7 +1273,7 @@ function majorVersion(output) {
908
1273
  const m = /(\d+)\.\d+\.\d+/.exec(output);
909
1274
  return m ? Number(m[1]) : void 0;
910
1275
  }
911
- function classify3(message) {
1276
+ function classify4(message) {
912
1277
  if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
913
1278
  if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
914
1279
  if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
@@ -916,19 +1281,19 @@ function classify3(message) {
916
1281
  }
917
1282
  return "other";
918
1283
  }
919
- function failure3(run, stderr, exitCode) {
1284
+ function failure4(run, stderr, exitCode) {
920
1285
  if (exitCode === 0 && !run.error) return void 0;
921
1286
  const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
922
1287
  const raw = run.error ?? `opencode exited with code ${exitCode}`;
923
1288
  const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
924
- return { kind: classify3(message), message };
1289
+ return { kind: classify4(message), message };
925
1290
  }
926
1291
  function modelId(m) {
927
1292
  if (typeof m === "string") return m;
928
1293
  const id = m?.model ?? m?.id;
929
1294
  return m?.providerID && id ? `${m.providerID}/${id}` : void 0;
930
1295
  }
931
- function defaultModel2() {
1296
+ function defaultModel3() {
932
1297
  try {
933
1298
  const out = oc(["debug", "config"], { timeout: 3e4 }).out;
934
1299
  const json = JSON.parse(out.slice(out.search(/[[{]/)));
@@ -953,7 +1318,7 @@ function resolveModel2(sessionId) {
953
1318
  return void 0;
954
1319
  }
955
1320
  }
956
- function catalog3() {
1321
+ function catalog4() {
957
1322
  const ids = listModels();
958
1323
  return {
959
1324
  models: (ids.length ? ids : listModels()).map((id) => ({ id })),
@@ -963,20 +1328,20 @@ function catalog3() {
963
1328
  function listModels() {
964
1329
  return oc(["models"]).out.split("\n").map((s) => s.trim()).filter(Boolean);
965
1330
  }
966
- function doctor3({ models, hasFallback }) {
1331
+ function doctor4({ models, hasFallback }) {
967
1332
  const checks = [];
968
1333
  const version = oc(["--version"]);
969
1334
  if (version.missing || !version.ok) {
970
- return [{ level: "fail", message: `opencode not runnable (${binary3()}). Install: https://opencode.ai or set PITROOM_OPENCODE_BIN` }];
1335
+ return [{ level: "fail", message: `opencode not runnable (${binary4()}). Install: https://opencode.ai or set PITROOM_OPENCODE_BIN` }];
971
1336
  }
972
1337
  const v = version.out.trim().replace(/^opencode\s+/i, "");
973
1338
  const major = majorVersion(v);
974
1339
  if (major !== void 0 && major < 2) {
975
1340
  return [{ level: "fail", message: `OpenCode ${v} is too old: Pitroom needs OpenCode v2 or newer (run \`opencode upgrade\`)` }];
976
1341
  }
977
- checks.push({ level: "ok", message: `opencode ${v} at ${binary3()} (private --standalone server per run)` });
1342
+ checks.push({ level: "ok", message: `opencode ${v} at ${binary4()} (private --standalone server per run)` });
978
1343
  const known = new Set(listModels());
979
- const def = defaultModel2();
1344
+ const def = defaultModel3();
980
1345
  for (const m of models) {
981
1346
  const model = m ?? def;
982
1347
  const label = m ? "model" : "default model";
@@ -1002,21 +1367,21 @@ var opencode = {
1002
1367
  id: "opencode",
1003
1368
  name: "OpenCode",
1004
1369
  capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
1005
- binary: binary3,
1006
- invocation: invocation3,
1007
- parse: parseEvents3,
1008
- failure: failure3,
1009
- defaultModel: defaultModel2,
1370
+ binary: binary4,
1371
+ invocation: invocation4,
1372
+ parse: parseEvents4,
1373
+ failure: failure4,
1374
+ defaultModel: defaultModel3,
1010
1375
  resolveModel: resolveModel2,
1011
1376
  listModels,
1012
- catalog: catalog3,
1013
- doctor: doctor3
1377
+ catalog: catalog4,
1378
+ doctor: doctor4
1014
1379
  };
1015
1380
 
1016
1381
  // src/backends/index.ts
1017
- var REGISTRY = new Map([opencode, codex, claude].map((b) => [b.id, b]));
1382
+ var REGISTRY = new Map([opencode, codex, claude, gemini].map((b) => [b.id, b]));
1018
1383
  var DEFAULT_BACKEND = opencode.id;
1019
- var PLANNED = ["gemini"];
1384
+ var PLANNED = [];
1020
1385
  var backendIds = () => [...REGISTRY.keys()];
1021
1386
  var allBackends = () => [...REGISTRY.values()];
1022
1387
  var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
@@ -1031,12 +1396,12 @@ function getBackend(id) {
1031
1396
  }
1032
1397
 
1033
1398
  // src/cli/args.ts
1034
- import fs7 from "node:fs";
1399
+ import fs8 from "node:fs";
1035
1400
 
1036
1401
  // src/core/config.ts
1037
- import fs5 from "node:fs";
1038
- import os5 from "node:os";
1039
- import path5 from "node:path";
1402
+ import fs7 from "node:fs";
1403
+ import os7 from "node:os";
1404
+ import path7 from "node:path";
1040
1405
  var SCHEMA = {
1041
1406
  worker: "string",
1042
1407
  fallback: "string[]",
@@ -1048,12 +1413,13 @@ var SCHEMA = {
1048
1413
  maxParallel: "number",
1049
1414
  models: "record",
1050
1415
  tiers: "record",
1051
- costs: "numbers"
1416
+ costs: "numbers",
1417
+ audit: "number"
1052
1418
  };
1053
1419
  function configPath() {
1054
- if (process.env.PITROOM_CONFIG) return path5.resolve(process.env.PITROOM_CONFIG);
1055
- const base2 = process.platform === "win32" ? process.env.APPDATA ?? path5.join(os5.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path5.join(os5.homedir(), ".config");
1056
- return path5.join(base2, "pitroom", "config.json");
1420
+ if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
1421
+ const base2 = process.platform === "win32" ? process.env.APPDATA ?? path7.join(os7.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path7.join(os7.homedir(), ".config");
1422
+ return path7.join(base2, "pitroom", "config.json");
1057
1423
  }
1058
1424
  var cached;
1059
1425
  function loadConfig() {
@@ -1061,10 +1427,10 @@ function loadConfig() {
1061
1427
  const file = configPath();
1062
1428
  const config = {};
1063
1429
  const warnings = [];
1064
- if (fs5.existsSync(file)) {
1430
+ if (fs7.existsSync(file)) {
1065
1431
  let raw;
1066
1432
  try {
1067
- raw = JSON.parse(fs5.readFileSync(file, "utf8"));
1433
+ raw = JSON.parse(fs7.readFileSync(file, "utf8"));
1068
1434
  } catch (e) {
1069
1435
  warnings.push(`${file}: invalid JSON (${e.message}); ignored`);
1070
1436
  }
@@ -1092,6 +1458,7 @@ function matches(v, type) {
1092
1458
  if (type === "numbers") {
1093
1459
  return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
1094
1460
  }
1461
+ if (type === "number") return typeof v === "number" && Number.isFinite(v) && v >= 0;
1095
1462
  return typeof v === type;
1096
1463
  }
1097
1464
  var DEFAULT_PARALLEL = 20;
@@ -1101,6 +1468,10 @@ var positiveInt = (v) => {
1101
1468
  const n = Number(v);
1102
1469
  return v !== void 0 && v !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
1103
1470
  };
1471
+ var rate = (v) => {
1472
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
1473
+ return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
1474
+ };
1104
1475
  var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
1105
1476
  function setting(flag2, env, conf, fallback) {
1106
1477
  if (flag2 !== void 0) return { value: flag2, source: "flag" };
@@ -1124,113 +1495,11 @@ function effective(flags = {}) {
1124
1495
  maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
1125
1496
  models: setting(void 0, void 0, c.models, {}),
1126
1497
  tiers: setting(void 0, void 0, c.tiers, {}),
1127
- costs: setting(void 0, void 0, c.costs, {})
1498
+ costs: setting(void 0, void 0, c.costs, {}),
1499
+ audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0)
1128
1500
  };
1129
1501
  }
1130
1502
 
1131
- // src/core/store.ts
1132
- import fs6 from "node:fs";
1133
- import os6 from "node:os";
1134
- import path6 from "node:path";
1135
- import crypto from "node:crypto";
1136
- var TERMINAL = ["done", "failed", "timeout", "stopped"];
1137
- var isActive = (s) => !TERMINAL.includes(s);
1138
- function home() {
1139
- if (process.env.PITROOM_HOME) return path6.resolve(process.env.PITROOM_HOME);
1140
- if (process.platform === "win32") {
1141
- return path6.join(process.env.LOCALAPPDATA ?? path6.join(os6.homedir(), "AppData", "Local"), "pitroom");
1142
- }
1143
- return path6.join(process.env.XDG_STATE_HOME ?? path6.join(os6.homedir(), ".local", "state"), "pitroom");
1144
- }
1145
- var runsDir = () => path6.join(home(), "runs");
1146
- var worktreesDir = () => path6.join(home(), "worktrees");
1147
- var ledgerFile = () => path6.join(home(), "ledger.jsonl");
1148
- var runDir = (id) => path6.join(runsDir(), id);
1149
- var runFile = (id, name) => path6.join(runDir(id), name);
1150
- function newRunId(now = /* @__PURE__ */ new Date()) {
1151
- const p = (n) => String(n).padStart(2, "0");
1152
- const stamp2 = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
1153
- return `${stamp2}-${crypto.randomBytes(2).toString("hex")}`;
1154
- }
1155
- var onFinished;
1156
- var archive;
1157
- function useArchive(hooks) {
1158
- onFinished = hooks.onFinished;
1159
- archive = { meta: hooks.meta, id: hooks.id };
1160
- }
1161
- function writeMeta(meta) {
1162
- fs6.mkdirSync(runDir(meta.id), { recursive: true });
1163
- const file = runFile(meta.id, "meta.json");
1164
- const tmp = `${file}.${process.pid}.tmp`;
1165
- fs6.writeFileSync(tmp, JSON.stringify(meta, null, 2));
1166
- fs6.renameSync(tmp, file);
1167
- if (TERMINAL.includes(meta.state)) {
1168
- try {
1169
- onFinished?.(meta);
1170
- } catch {
1171
- }
1172
- }
1173
- }
1174
- function readMeta(id) {
1175
- const file = runFile(id, "meta.json");
1176
- if (!fs6.existsSync(file)) {
1177
- const kept = archive?.meta(id);
1178
- if (kept) return upgrade(kept);
1179
- }
1180
- return upgrade(JSON.parse(fs6.readFileSync(file, "utf8")));
1181
- }
1182
- function upgrade(raw) {
1183
- if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
1184
- if (!Array.isArray(raw.fallback)) {
1185
- raw.fallback = (raw.fallbackModels ?? []).map((m) => ({ backend: "opencode", model: m }));
1186
- }
1187
- raw.warnings ??= [];
1188
- raw.files ??= [];
1189
- raw.link ??= [];
1190
- return raw;
1191
- }
1192
- function listRunIds() {
1193
- if (!fs6.existsSync(runsDir())) return [];
1194
- return fs6.readdirSync(runsDir()).filter((d) => fs6.existsSync(runFile(d, "meta.json"))).sort();
1195
- }
1196
- function resolveRun(ref) {
1197
- const ids = listRunIds();
1198
- if (!ref || ref === "latest" || ref === "last") {
1199
- if (!ids.length) throw new UserError("no runs yet");
1200
- return ids[ids.length - 1];
1201
- }
1202
- if (ids.includes(ref)) return ref;
1203
- const hits = ids.filter((id) => id.startsWith(ref) || id.endsWith(ref));
1204
- if (hits.length === 1) return hits[0];
1205
- if (!hits.length) {
1206
- const kept = archive?.id(ref);
1207
- if (kept) return kept;
1208
- }
1209
- throw new UserError(hits.length ? `ambiguous run "${ref}": ${hits.join(", ")}` : ids.length ? `unknown run "${ref}"` : "no runs yet");
1210
- }
1211
- function isAlive(pid) {
1212
- if (!pid) return false;
1213
- try {
1214
- process.kill(pid, 0);
1215
- return true;
1216
- } catch (e) {
1217
- return e.code === "EPERM";
1218
- }
1219
- }
1220
- function freshMeta(id) {
1221
- const meta = readMeta(id);
1222
- if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
1223
- const latest = readMeta(id);
1224
- if (TERMINAL.includes(latest.state)) return latest;
1225
- latest.state = "failed";
1226
- latest.error ??= "worker process exited unexpectedly";
1227
- latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
1228
- writeMeta(latest);
1229
- return latest;
1230
- }
1231
- return meta;
1232
- }
1233
-
1234
1503
  // src/cli/args.ts
1235
1504
  var VALUE_FLAGS = {
1236
1505
  "-d": "dir",
@@ -1274,6 +1543,8 @@ var BOOL_FLAGS = {
1274
1543
  "-i": "isolate",
1275
1544
  "--isolate": "isolate",
1276
1545
  "--bg": "bg",
1546
+ "--audit": "audit",
1547
+ "--no-audit": "no-audit",
1277
1548
  "--web": "web",
1278
1549
  "--no-fallback": "no-fallback",
1279
1550
  "--json": "json",
@@ -1351,17 +1622,20 @@ function exitCodeFor(meta) {
1351
1622
  }
1352
1623
  function readTask(p) {
1353
1624
  const file = flag(p, "task-file");
1354
- if (file) return fs7.readFileSync(file, "utf8");
1625
+ if (file) return fs8.readFileSync(file, "utf8");
1355
1626
  const words = p.positional;
1356
- if (words.length === 1 && words[0] === "-") return fs7.readFileSync(0, "utf8");
1627
+ if (words.length === 1 && words[0] === "-") return fs8.readFileSync(0, "utf8");
1357
1628
  return words.join(" ");
1358
1629
  }
1359
1630
  function runOptions(p, task) {
1360
1631
  const modes = ["read", "write", "isolate"].filter((m) => has(p, m));
1361
1632
  if (modes.length > 1) throw new UserError("choose one of --read, --write, --isolate");
1362
1633
  const cont = flag(p, "continue");
1634
+ if (has(p, "audit") && has(p, "no-audit")) throw new UserError("choose one of --audit, --no-audit");
1635
+ if (has(p, "audit") && (has(p, "write") || has(p, "isolate"))) throw new UserError("audits re-check the answer of a read run: drop -w/-i");
1363
1636
  return {
1364
1637
  mode: modes[0] ?? "read",
1638
+ auditRate: has(p, "no-audit") ? 0 : has(p, "audit") ? 1 : void 0,
1365
1639
  task,
1366
1640
  dir: flag(p, "dir") ?? process.cwd(),
1367
1641
  files: p.flags.get("file") ?? [],
@@ -1394,14 +1668,14 @@ function effortFlag(p) {
1394
1668
  }
1395
1669
 
1396
1670
  // src/cli/commands.ts
1397
- import { spawnSync as spawnSync6 } from "node:child_process";
1398
- import fs26 from "node:fs";
1399
- import path22 from "node:path";
1400
- import { fileURLToPath as fileURLToPath3 } from "node:url";
1671
+ import { spawnSync as spawnSync7 } from "node:child_process";
1672
+ import fs27 from "node:fs";
1673
+ import path23 from "node:path";
1674
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
1401
1675
 
1402
1676
  // src/core/init.ts
1403
- import fs8 from "node:fs";
1404
- import path7 from "node:path";
1677
+ import fs9 from "node:fs";
1678
+ import path8 from "node:path";
1405
1679
 
1406
1680
  // src/core/style.ts
1407
1681
  var enabled = () => {
@@ -1447,13 +1721,13 @@ ${" ".repeat(indent)}`);
1447
1721
  // src/core/init.ts
1448
1722
  var FREE = /-free$/;
1449
1723
  var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
1450
- var isFound = (binary4) => path7.isAbsolute(binary4) && fs8.existsSync(binary4);
1724
+ var isFound = (binary5) => path8.isAbsolute(binary5) && fs9.existsSync(binary5);
1451
1725
  function planInit(opts = {}) {
1452
1726
  const file = configPath();
1453
1727
  const workers = backendIds().map((id) => {
1454
1728
  const b = getBackend(id);
1455
- const binary4 = b.binary();
1456
- return { id, name: b.name, found: isFound(binary4), binary: binary4 };
1729
+ const binary5 = b.binary();
1730
+ return { id, name: b.name, found: isFound(binary5), binary: binary5 };
1457
1731
  });
1458
1732
  const notes = [];
1459
1733
  const config = {};
@@ -1467,15 +1741,15 @@ function planInit(opts = {}) {
1467
1741
  } catch {
1468
1742
  notes.push("could not list OpenCode models (is it logged in?)");
1469
1743
  }
1470
- const defaultModel3 = b.defaultModel?.();
1471
- const chosen = opts.model ?? defaultModel3;
1744
+ const defaultModel4 = b.defaultModel?.();
1745
+ const chosen = opts.model ?? defaultModel4;
1472
1746
  const free = models.filter((m) => FREE.test(m) && m !== chosen);
1473
- opencode2 = { defaultModel: defaultModel3, models: models.length, free: free.slice(0, 6) };
1747
+ opencode2 = { defaultModel: defaultModel4, models: models.length, free: free.slice(0, 6) };
1474
1748
  if (opts.model) {
1475
1749
  if (models.length && !models.includes(opts.model)) throw new UserError(`"${opts.model}" is not in \`opencode models\``);
1476
1750
  if (!models.length) notes.push(`could not check "${opts.model}": \`opencode models\` listed nothing`);
1477
1751
  config.models = { opencode: opts.model };
1478
- } else if (!defaultModel3) {
1752
+ } else if (!defaultModel4) {
1479
1753
  blocked = `OpenCode has no default model: choose one with --model <id>${free.length ? ` (free models you have: ${free.slice(0, 4).join(", ")})` : ""}`;
1480
1754
  }
1481
1755
  const fallback = opts.fallback ?? free.slice(0, 2);
@@ -1491,17 +1765,17 @@ function planInit(opts = {}) {
1491
1765
  }
1492
1766
  const tiers = Object.fromEntries(TIER_ORDER.filter(([, id]) => workers.find((w) => w.id === id)?.found));
1493
1767
  if (Object.keys(tiers).length > 1) config.tiers = tiers;
1494
- if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI or Claude Code first";
1768
+ if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
1495
1769
  else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
1496
- return { path: file, exists: fs8.existsSync(file), workers, opencode: opencode2, config, blocked, notes };
1770
+ return { path: file, exists: fs9.existsSync(file), workers, opencode: opencode2, config, blocked, notes };
1497
1771
  }
1498
1772
  function writeInit(plan, force) {
1499
1773
  if (plan.blocked) throw new UserError(plan.blocked);
1500
1774
  if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
1501
- if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path7.basename(plan.path)}.bak)`);
1502
- fs8.mkdirSync(path7.dirname(plan.path), { recursive: true });
1503
- if (plan.exists) fs8.copyFileSync(plan.path, `${plan.path}.bak`);
1504
- fs8.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
1775
+ if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path8.basename(plan.path)}.bak)`);
1776
+ fs9.mkdirSync(path8.dirname(plan.path), { recursive: true });
1777
+ if (plan.exists) fs9.copyFileSync(plan.path, `${plan.path}.bak`);
1778
+ fs9.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
1505
1779
  `);
1506
1780
  return plan.path;
1507
1781
  }
@@ -1520,15 +1794,102 @@ function formatInit(plan, written) {
1520
1794
  return out.join("\n");
1521
1795
  }
1522
1796
 
1797
+ // src/core/audit.ts
1798
+ import crypto2 from "node:crypto";
1799
+
1800
+ // src/core/target.ts
1801
+ function parseTarget(spec, defaultBackend) {
1802
+ const s = spec.trim();
1803
+ if (!s) throw new UserError("empty worker target");
1804
+ const i = s.indexOf(":");
1805
+ if (i > 0 && isBackendId(s.slice(0, i))) {
1806
+ const model = s.slice(i + 1).trim();
1807
+ return model ? { backend: s.slice(0, i), model } : { backend: s.slice(0, i) };
1808
+ }
1809
+ if (isBackendId(s)) return { backend: s };
1810
+ return { backend: defaultBackend, model: s };
1811
+ }
1812
+ var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
1813
+ var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
1814
+ var sameTarget = (a, b) => a.backend === b.backend && a.model === b.model;
1815
+
1816
+ // src/core/audit.ts
1817
+ var ANSWER_MAX = 8e3;
1818
+ function auditRate(meta) {
1819
+ return meta.auditRate ?? effective().audit.value;
1820
+ }
1821
+ function draw(id) {
1822
+ return crypto2.createHash("sha256").update(id).digest().readUInt32BE(0) / 4294967296;
1823
+ }
1824
+ function auditable(meta, answer) {
1825
+ return meta.mode === "read" && meta.state === "done" && !meta.reviewOf && !meta.auditOf && !meta.audit && !!answer.trim();
1826
+ }
1827
+ function sampled(meta) {
1828
+ const rate2 = auditRate(meta);
1829
+ return rate2 > 0 && (rate2 >= 1 || draw(meta.id) < rate2);
1830
+ }
1831
+ function pickAuditor(meta) {
1832
+ const eff = effective();
1833
+ const tiers = eff.tiers.value;
1834
+ const def = parseTarget(eff.worker.value, DEFAULT_BACKEND).backend;
1835
+ const ran = meta.ran ?? meta.worker;
1836
+ const candidates = [tiers.audit, tiers.cheap, ...eff.fallback.value, eff.worker.value].filter((s) => !!s);
1837
+ return candidates.find((c) => !sameTarget(parseTarget(c, def), ran));
1838
+ }
1839
+ function auditTask(meta, answer) {
1840
+ const text = answer.trim();
1841
+ const shown = text.length > ANSWER_MAX ? `${text.slice(0, ANSWER_MAX)}
1842
+ \u2026 (cut: ${text.length - ANSWER_MAX} more characters)` : text;
1843
+ return [
1844
+ "You are auditing another worker's answer to a read-only question about this project. Do not take the answer on trust:",
1845
+ "check its key claims yourself against the files here (open the cited files and lines, search for what it says exists or is missing).",
1846
+ "Do not edit anything and do not run anything that changes files.",
1847
+ "",
1848
+ "QUESTION:",
1849
+ meta.task.trim(),
1850
+ "",
1851
+ "ANSWER TO AUDIT:",
1852
+ shown,
1853
+ "",
1854
+ "Reply in exactly this form and nothing else:",
1855
+ "AUDIT: AGREE | PARTIAL | DISAGREE",
1856
+ "CHECKED: <how many key claims you checked>",
1857
+ "DISPUTED:",
1858
+ "- <the claim> \u2014 <what is actually true, with path:line>",
1859
+ "",
1860
+ "AGREE: every key claim you checked holds. PARTIAL: some are wrong or could not be verified. DISAGREE: the main conclusion is wrong.",
1861
+ 'Under DISPUTED list only claims you checked and found wrong or unsupported; write "- (none)" when there are none.'
1862
+ ].join("\n");
1863
+ }
1864
+ function auditLines(meta) {
1865
+ if (meta.auditOf) {
1866
+ return meta.state === "done" ? [`\u2500\u2500 audit of ${meta.auditOf}: ${(meta.auditVerdict ?? "unclear").toUpperCase()}${meta.auditDisputed?.length ? ` \xB7 ${meta.auditDisputed.length} disputed` : ""}`] : [];
1867
+ }
1868
+ const a = meta.audit;
1869
+ if (!a) return [];
1870
+ if (a.state === "done") {
1871
+ const head = `\u2500\u2500 audit (run ${a.id}): ${(a.verdict ?? "unclear").toUpperCase()}${a.verdict === "agree" ? "" : a.verdict === "unclear" ? " (the auditor did not answer in the expected form)" : ""}`;
1872
+ return [head, ...(a.disputed ?? []).map((d) => ` disputed: ${d}`)];
1873
+ }
1874
+ if (a.state === "running" || a.state === "queued") return [`\u2500\u2500 audit: another worker is re-checking this answer in the background (pitroom show ${a.id})`];
1875
+ return [`\u2500\u2500 audit (run ${a.id}) ${a.state}: no verdict`];
1876
+ }
1877
+ function auditBadge(meta) {
1878
+ const a = meta.audit;
1879
+ if (!a) return void 0;
1880
+ if (a.state === "done") return (a.verdict ?? "unclear").toUpperCase();
1881
+ return a.state === "running" || a.state === "queued" ? "PENDING" : "FAILED";
1882
+ }
1883
+
1523
1884
  // src/core/history.ts
1524
- import fs11 from "node:fs";
1885
+ import fs12 from "node:fs";
1525
1886
  import { createRequire } from "node:module";
1526
- import path9 from "node:path";
1887
+ import path10 from "node:path";
1527
1888
  import zlib from "node:zlib";
1528
1889
 
1529
1890
  // src/core/receipt.ts
1530
- import fs9 from "node:fs";
1531
- import path8 from "node:path";
1891
+ import fs10 from "node:fs";
1892
+ import path9 from "node:path";
1532
1893
  var PRESETS = {
1533
1894
  sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
1534
1895
  opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
@@ -1550,7 +1911,7 @@ function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
1550
1911
  return Math.max(0, wouldCost - (usage2.cost ?? 0) - readingTheReport);
1551
1912
  }
1552
1913
  function record(meta) {
1553
- if (!meta.usage?.steps) return;
1914
+ if (!meta.usage?.steps || meta.auditOf) return;
1554
1915
  const entry = {
1555
1916
  id: meta.id,
1556
1917
  at: meta.endedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -1564,13 +1925,13 @@ function record(meta) {
1564
1925
  saved: meta.savedUsd ?? 0,
1565
1926
  price: primaryPrice().name
1566
1927
  };
1567
- fs9.mkdirSync(path8.dirname(ledgerFile()), { recursive: true });
1568
- fs9.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
1928
+ fs10.mkdirSync(path9.dirname(ledgerFile()), { recursive: true });
1929
+ fs10.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
1569
1930
  `);
1570
1931
  }
1571
1932
  function readLedger(sinceMs2) {
1572
- if (!fs9.existsSync(ledgerFile())) return [];
1573
- return fs9.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
1933
+ if (!fs10.existsSync(ledgerFile())) return [];
1934
+ return fs10.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
1574
1935
  try {
1575
1936
  return [JSON.parse(l)];
1576
1937
  } catch {
@@ -1645,7 +2006,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
1645
2006
  }
1646
2007
 
1647
2008
  // src/core/ui.ts
1648
- import fs10 from "node:fs";
2009
+ import fs11 from "node:fs";
1649
2010
  var WEEK_MS = 7 * 24 * 3600 * 1e3;
1650
2011
  var RECENT = 40;
1651
2012
  var RECENT_HOURS = 1;
@@ -1677,6 +2038,7 @@ var RUN_ID = /\b\d{8}-\d{6}-[0-9a-f]{4}\b/g;
1677
2038
  var PITROOM_CALL = /(^|[\s;&|(`])(\S*\/)?pitroom(\.mjs)?(\s|$)/;
1678
2039
  var ICON = { done: "\u2714", failed: "\u2718", timeout: "\u23F1", stopped: "\u25A0" };
1679
2040
  function kind(m) {
2041
+ if (m.auditOf) return "audit";
1680
2042
  if (m.reviewOf) return m.reviewKind === "range" ? "branch review" : m.reviewKind === "fix" ? "re-review" : "review";
1681
2043
  return m.mode === "read" ? "research" : m.mode === "isolate" ? "change (isolated copy)" : "change";
1682
2044
  }
@@ -1700,6 +2062,7 @@ function ranTarget(m) {
1700
2062
  }
1701
2063
  function what(m) {
1702
2064
  if (m.plan) return short(`Task ${m.plan.step}: ${m.plan.title}`);
2065
+ if (m.auditOf) return `of ${m.auditOf}`;
1703
2066
  if (m.reviewOf) return `of ${m.reviewOf.replace(/\b([0-9a-f]{9})[0-9a-f]{31}\b/g, "$1")}`;
1704
2067
  const line = m.task.split("\n").find((l) => l.trim()) ?? "";
1705
2068
  const brief2 = /^You are implementing Task (\d+)\b.*?\bplan\s+(\S+)/i.exec(line);
@@ -1719,6 +2082,7 @@ function endedCard(m) {
1719
2082
  what(m),
1720
2083
  elapsed(m),
1721
2084
  v && `SPEC ${v.spec.toUpperCase()} \xB7 QUALITY ${v.quality.toUpperCase()}`,
2085
+ m.auditVerdict && `${m.auditVerdict.toUpperCase()}${m.auditDisputed?.length ? ` \xB7 ${m.auditDisputed.length} disputed` : ""}`,
1722
2086
  m.changes?.length ? `${m.changes.length} file${m.changes.length === 1 ? "" : "s"} changed` : "",
1723
2087
  m.savedUsd ? `~${usd(m.savedUsd)} saved` : ""
1724
2088
  ];
@@ -1733,9 +2097,9 @@ function cardsFor(m) {
1733
2097
  const out = [];
1734
2098
  for (const [phase, text] of phases) {
1735
2099
  const marker = runFile(m.id, `card-${phase}`);
1736
- if (fs10.existsSync(marker)) continue;
2100
+ if (fs11.existsSync(marker)) continue;
1737
2101
  if (!(phase === "started" && !isActive(m.state))) out.push(text());
1738
- fs10.writeFileSync(marker, "");
2102
+ fs11.writeFileSync(marker, "");
1739
2103
  }
1740
2104
  return out;
1741
2105
  }
@@ -1746,10 +2110,10 @@ function hookCards(input) {
1746
2110
  const response = event.tool_response;
1747
2111
  const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
1748
2112
  const ids = [...new Set(`${command}
1749
- ${output}`.match(RUN_ID) ?? [])].filter((id) => fs10.existsSync(`${runsDir()}/${id}`));
2113
+ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs11.existsSync(`${runsDir()}/${id}`));
1750
2114
  const since = Date.now() - RECENT_HOURS * 36e5;
1751
2115
  for (const id of listRunIds().slice(-RECENT)) {
1752
- if (ids.includes(id) || fs10.existsSync(runFile(id, "card-ended"))) continue;
2116
+ if (ids.includes(id) || fs11.existsSync(runFile(id, "card-ended"))) continue;
1753
2117
  try {
1754
2118
  const m = readMeta(id);
1755
2119
  if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since) ids.push(id);
@@ -1769,9 +2133,9 @@ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs10.existsSync(`${runsDir()}/${
1769
2133
  // src/core/history.ts
1770
2134
  var SCHEMA_VERSION = 1;
1771
2135
  var PATCH_MAX = 1e6;
1772
- var ANSWER_MAX = 2e5;
2136
+ var ANSWER_MAX2 = 2e5;
1773
2137
  var cached2;
1774
- var historyFile = () => path9.join(home(), "history.db");
2138
+ var historyFile = () => path10.join(home(), "history.db");
1775
2139
  function openDb() {
1776
2140
  const file = historyFile();
1777
2141
  if (cached2?.file === file) return cached2.db;
@@ -1785,7 +2149,7 @@ function openDb() {
1785
2149
  } finally {
1786
2150
  process.emitWarning = emit;
1787
2151
  }
1788
- fs11.mkdirSync(path9.dirname(file), { recursive: true });
2152
+ fs12.mkdirSync(path10.dirname(file), { recursive: true });
1789
2153
  db = new DatabaseSync(file);
1790
2154
  db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
1791
2155
  migrate(db);
@@ -1820,35 +2184,35 @@ function migrate(db) {
1820
2184
  function readRunFile(id, name) {
1821
2185
  const f = runFile(id, name);
1822
2186
  try {
1823
- if (fs11.existsSync(f)) return fs11.readFileSync(f, "utf8");
1824
- if (fs11.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs11.readFileSync(`${f}.gz`)).toString("utf8");
2187
+ if (fs12.existsSync(f)) return fs12.readFileSync(f, "utf8");
2188
+ if (fs12.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs12.readFileSync(`${f}.gz`)).toString("utf8");
1825
2189
  } catch {
1826
2190
  }
1827
2191
  return void 0;
1828
2192
  }
1829
2193
  function gzipFile(f) {
1830
- if (!fs11.existsSync(f)) return;
1831
- fs11.writeFileSync(`${f}.gz`, zlib.gzipSync(fs11.readFileSync(f)));
1832
- fs11.rmSync(f);
2194
+ if (!fs12.existsSync(f)) return;
2195
+ fs12.writeFileSync(`${f}.gz`, zlib.gzipSync(fs12.readFileSync(f)));
2196
+ fs12.rmSync(f);
1833
2197
  }
1834
2198
  function compactRun(meta) {
1835
2199
  if (!TERMINAL.includes(meta.state)) return;
1836
- const dir = path9.dirname(runFile(meta.id, "meta.json"));
2200
+ const dir = path10.dirname(runFile(meta.id, "meta.json"));
1837
2201
  let names;
1838
2202
  try {
1839
- names = fs11.readdirSync(dir);
2203
+ names = fs12.readdirSync(dir);
1840
2204
  } catch {
1841
2205
  return;
1842
2206
  }
1843
2207
  for (const n of names) {
1844
- if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path9.join(dir, n));
2208
+ if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path10.join(dir, n));
1845
2209
  else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
1846
- if (meta.state === "done" && !/attempt/.test(n)) fs11.rmSync(path9.join(dir, n), { force: true });
1847
- else gzipFile(path9.join(dir, n));
2210
+ if (meta.state === "done" && !/attempt/.test(n)) fs12.rmSync(path10.join(dir, n), { force: true });
2211
+ else gzipFile(path10.join(dir, n));
1848
2212
  }
1849
2213
  }
1850
2214
  }
1851
- var clip4 = (s, n) => s && s.length > n ? s.slice(0, n) : s;
2215
+ var clip5 = (s, n) => s && s.length > n ? s.slice(0, n) : s;
1852
2216
  function timelineOf(meta) {
1853
2217
  const raw = readRunFile(meta.id, "events.jsonl");
1854
2218
  if (!raw) return [];
@@ -1865,8 +2229,8 @@ function recordRun(meta) {
1865
2229
  try {
1866
2230
  const u = meta.usage;
1867
2231
  const known = db.prepare("SELECT answer IS NOT NULL AS has FROM runs WHERE id = ?").get(meta.id);
1868
- const answer = known?.has ? void 0 : clip4(readRunFile(meta.id, "summary.md")?.trim(), ANSWER_MAX);
1869
- const patch = known?.has ? void 0 : clip4(readRunFile(meta.id, "changes.patch"), PATCH_MAX);
2232
+ const answer = known?.has ? void 0 : clip5(readRunFile(meta.id, "summary.md")?.trim(), ANSWER_MAX2);
2233
+ const patch = known?.has ? void 0 : clip5(readRunFile(meta.id, "changes.patch"), PATCH_MAX);
1870
2234
  const seconds = meta.endedAt ? Math.max(0, Math.round((Date.parse(meta.endedAt) - Date.parse(meta.startedAt)) / 1e3)) : null;
1871
2235
  const t = meta.ran ?? meta.worker;
1872
2236
  const fields = {
@@ -1878,11 +2242,11 @@ function recordRun(meta) {
1878
2242
  kind: kind(meta),
1879
2243
  backend: t.backend,
1880
2244
  model: meta.resolvedModel ?? t.model ?? null,
1881
- task: clip4(meta.task, 2e4) ?? "",
2245
+ task: clip5(meta.task, 2e4) ?? "",
1882
2246
  grp: meta.group ?? null,
1883
2247
  dir: meta.dir,
1884
- review_of: meta.reviewOf ?? null,
1885
- verdict: meta.verdict ? `SPEC ${meta.verdict.spec.toUpperCase()} \xB7 QUALITY ${meta.verdict.quality.toUpperCase()}` : null,
2248
+ review_of: meta.reviewOf ?? meta.auditOf ?? null,
2249
+ verdict: meta.verdict ? `SPEC ${meta.verdict.spec.toUpperCase()} \xB7 QUALITY ${meta.verdict.quality.toUpperCase()}` : meta.auditVerdict ? `AUDIT ${meta.auditVerdict.toUpperCase()}` : null,
1886
2250
  seconds,
1887
2251
  steps: u?.steps ?? null,
1888
2252
  tool_calls: u?.toolCalls ?? null,
@@ -1892,7 +2256,7 @@ function recordRun(meta) {
1892
2256
  saved: meta.savedUsd ?? null,
1893
2257
  files_changed: meta.changes?.length ?? 0,
1894
2258
  applied: meta.applied ? 1 : 0,
1895
- error: clip4(meta.error, 2e3) ?? null,
2259
+ error: clip5(meta.error, 2e3) ?? null,
1896
2260
  meta_json: JSON.stringify(meta)
1897
2261
  };
1898
2262
  db.exec("BEGIN IMMEDIATE");
@@ -1999,7 +2363,7 @@ function listHistory(q = {}) {
1999
2363
  try {
2000
2364
  const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
2001
2365
  const page = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
2002
- const rows = db.prepare(`SELECT r.* FROM runs r ${page} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
2366
+ const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict FROM runs r ${page} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
2003
2367
  return {
2004
2368
  total,
2005
2369
  rows: rows.map((r) => ({
@@ -2012,6 +2376,7 @@ function listHistory(q = {}) {
2012
2376
  task: r.review_of ? `of ${r.review_of.replace(/\b([0-9a-f]{9})[0-9a-f]{31}\b/g, "$1")}` : r.task.split("\n").find((l) => l.trim()) ?? "",
2013
2377
  group: r.grp ?? void 0,
2014
2378
  verdict: r.verdict ?? void 0,
2379
+ audit: auditBadge({ audit: r.audit_state ? { id: "", state: r.audit_state, verdict: r.audit_verdict ?? void 0 } : void 0 }),
2015
2380
  seconds: r.seconds ?? void 0,
2016
2381
  steps: r.steps ?? void 0,
2017
2382
  tokens: r.tokens ?? void 0,
@@ -2024,37 +2389,68 @@ function listHistory(q = {}) {
2024
2389
  return { rows: [], total: 0 };
2025
2390
  }
2026
2391
  }
2027
- function workerRows(rows, ledger) {
2028
- const out = rows.map((r) => ({ backend: r.backend, model: r.model ?? void 0, runs: r.runs, ok: r.ok ?? 0, avgSeconds: r.avg_s, avgTokens: r.avg_t, saved: 0 }));
2392
+ var NOT_AUDIT = "json_extract(meta_json, '$.auditOf') IS NULL";
2393
+ function auditStats(db, since) {
2394
+ const total = { runs: 0, agree: 0, partial: 0, disagree: 0, unclear: 0, tokens: 0 };
2395
+ const byWorker = /* @__PURE__ */ new Map();
2396
+ try {
2397
+ const t = db.prepare("SELECT COUNT(*) runs, COALESCE(SUM(tokens),0) tokens FROM runs WHERE started_at >= ? AND json_extract(meta_json, '$.auditOf') IS NOT NULL").get(since);
2398
+ total.runs = t.runs;
2399
+ total.tokens = t.tokens;
2400
+ const rows = db.prepare(`SELECT r.backend, r.model, json_extract(a.meta_json, '$.auditVerdict') v, COUNT(*) n
2401
+ FROM runs a JOIN runs r ON r.id = json_extract(a.meta_json, '$.auditOf')
2402
+ WHERE a.started_at >= ? AND a.state = 'done' AND json_extract(a.meta_json, '$.auditOf') IS NOT NULL
2403
+ GROUP BY r.backend, r.model, v`).all(since);
2404
+ for (const r of rows) {
2405
+ const v = r.v ?? "unclear";
2406
+ if (v in total && v !== "runs" && v !== "tokens") total[v] += r.n;
2407
+ const k = `${r.backend ?? ""}\0${r.model ?? ""}`;
2408
+ const w = byWorker.get(k) ?? { audited: 0, agreed: 0 };
2409
+ w.audited += r.n;
2410
+ if (r.v === "agree") w.agreed += r.n;
2411
+ byWorker.set(k, w);
2412
+ }
2413
+ } catch {
2414
+ }
2415
+ return { total, byWorker };
2416
+ }
2417
+ function workerRows(rows, ledger, audited) {
2418
+ const out = rows.map((r) => ({ backend: r.backend, model: r.model ?? void 0, runs: r.runs, ok: r.ok ?? 0, avgSeconds: r.avg_s, avgTokens: r.avg_t, saved: 0, audited: 0, agreed: 0 }));
2029
2419
  const key = (backend, model) => `${backend ?? ""}\0${model ?? ""}`;
2030
2420
  const index = new Map(out.map((r) => [key(r.backend, r.model), r]));
2031
2421
  for (const e of ledger) {
2032
2422
  const backend = e.backend ?? "opencode";
2033
2423
  let row = index.get(key(backend, e.model)) ?? (e.model ? out.find((r) => r.backend === backend && r.model?.endsWith(`/${e.model}`)) : void 0);
2034
2424
  if (!row) {
2035
- row = { backend, model: e.model, runs: 0, ok: 0, avgSeconds: null, avgTokens: null, saved: 0 };
2425
+ row = { backend, model: e.model, runs: 0, ok: 0, avgSeconds: null, avgTokens: null, saved: 0, audited: 0, agreed: 0 };
2036
2426
  index.set(key(backend, e.model), row);
2037
2427
  out.push(row);
2038
2428
  }
2039
2429
  row.saved += e.saved;
2040
2430
  }
2431
+ for (const r of out) {
2432
+ const a = audited.get(key(r.backend, r.model));
2433
+ if (a) Object.assign(r, a);
2434
+ }
2041
2435
  return out;
2042
2436
  }
2043
2437
  function historyStats(sinceMs2) {
2044
- const empty = { totals: { runs: 0, ok: 0, failed: 0, seconds: 0, tokens: 0, saved: 0 }, byWorker: [], byDay: [] };
2438
+ const empty = { totals: { runs: 0, ok: 0, failed: 0, seconds: 0, tokens: 0, saved: 0 }, audits: { runs: 0, agree: 0, partial: 0, disagree: 0, unclear: 0, tokens: 0 }, byWorker: [], byDay: [] };
2045
2439
  const db = openDb();
2046
2440
  if (!db) return empty;
2047
2441
  const since = sinceMs2 ? new Date(sinceMs2).toISOString() : "";
2048
2442
  try {
2049
- const t = db.prepare("SELECT COUNT(*) runs, COALESCE(SUM(state='done'),0) ok, COALESCE(SUM(seconds),0) seconds, COALESCE(SUM(tokens),0) tokens, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ?").get(since);
2050
- const w = db.prepare("SELECT backend, model, COUNT(*) runs, SUM(state='done') ok, AVG(seconds) avg_s, AVG(tokens) avg_t, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ? GROUP BY backend, model ORDER BY runs DESC LIMIT 40").all(since);
2051
- const d = db.prepare("SELECT substr(started_at,1,10) day, COUNT(*) runs, SUM(state='done') ok, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ? GROUP BY day ORDER BY day DESC LIMIT 60").all(since);
2443
+ const t = db.prepare(`SELECT COUNT(*) runs, COALESCE(SUM(state='done'),0) ok, COALESCE(SUM(seconds),0) seconds, COALESCE(SUM(tokens),0) tokens, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ? AND ${NOT_AUDIT}`).get(since);
2444
+ const w = db.prepare(`SELECT backend, model, COUNT(*) runs, SUM(state='done') ok, AVG(seconds) avg_s, AVG(tokens) avg_t, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ? AND ${NOT_AUDIT} GROUP BY backend, model ORDER BY runs DESC LIMIT 40`).all(since);
2445
+ const d = db.prepare(`SELECT substr(started_at,1,10) day, COUNT(*) runs, SUM(state='done') ok, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ? AND ${NOT_AUDIT} GROUP BY day ORDER BY day DESC LIMIT 60`).all(since);
2446
+ const audits = auditStats(db, since);
2052
2447
  const ledger = readLedger(sinceMs2);
2053
2448
  const byDay = /* @__PURE__ */ new Map();
2054
2449
  for (const e of ledger) byDay.set(e.at.slice(0, 10), (byDay.get(e.at.slice(0, 10)) ?? 0) + e.saved);
2055
2450
  return {
2056
2451
  totals: { runs: t.runs, ok: t.ok, failed: t.runs - t.ok, seconds: t.seconds, tokens: t.tokens, saved: totals(ledger).saved },
2057
- byWorker: workerRows(w, ledger),
2452
+ audits: audits.total,
2453
+ byWorker: workerRows(w, ledger, audits.byWorker),
2058
2454
  byDay: d.reverse().map((r) => ({ day: r.day, runs: r.runs, ok: r.ok ?? 0, saved: byDay.get(r.day) ?? 0 }))
2059
2455
  };
2060
2456
  } catch {
@@ -2063,11 +2459,11 @@ function historyStats(sinceMs2) {
2063
2459
  }
2064
2460
 
2065
2461
  // src/core/report.ts
2066
- import fs13 from "node:fs";
2462
+ import fs14 from "node:fs";
2067
2463
 
2068
2464
  // src/core/plan.ts
2069
- import fs12 from "node:fs";
2070
- import path10 from "node:path";
2465
+ import fs13 from "node:fs";
2466
+ import path11 from "node:path";
2071
2467
  var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
2072
2468
  var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
2073
2469
  function structure(lines) {
@@ -2114,9 +2510,9 @@ function parsePlan(text, file = "") {
2114
2510
  return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
2115
2511
  }
2116
2512
  function loadPlan(file) {
2117
- const abs = path10.resolve(file);
2118
- if (!fs12.existsSync(abs) || !fs12.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
2119
- const plan = parsePlan(fs12.readFileSync(abs, "utf8"), fs12.realpathSync(abs));
2513
+ const abs = path11.resolve(file);
2514
+ if (!fs13.existsSync(abs) || !fs13.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
2515
+ const plan = parsePlan(fs13.readFileSync(abs, "utf8"), fs13.realpathSync(abs));
2120
2516
  if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
2121
2517
  return plan;
2122
2518
  }
@@ -2135,7 +2531,7 @@ function brief(plan, task) {
2135
2531
  ].filter(Boolean).join("\n\n")}
2136
2532
  `;
2137
2533
  }
2138
- var planName = (file) => path10.basename(file).replace(/\.md$/i, "");
2534
+ var planName = (file) => path11.basename(file).replace(/\.md$/i, "");
2139
2535
 
2140
2536
  // src/core/report.ts
2141
2537
  var ICON2 = {
@@ -2154,7 +2550,7 @@ function duration(meta) {
2154
2550
  }
2155
2551
  function readSummary(meta) {
2156
2552
  const f = runFile(meta.id, "summary.md");
2157
- return fs13.existsSync(f) ? fs13.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
2553
+ return fs14.existsSync(f) ? fs14.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
2158
2554
  }
2159
2555
  function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
2160
2556
  const out = [];
@@ -2185,6 +2581,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
2185
2581
  if (lines.length > maxLines) out.push(`\u2026 (${lines.length - maxLines} more lines: pitroom show ${meta.id} --full)`);
2186
2582
  }
2187
2583
  out.push("");
2584
+ out.push(...auditLines(meta));
2188
2585
  if (meta.refs) {
2189
2586
  const r = meta.refs;
2190
2587
  const bad = r.invalid.slice(0, 8).map((i) => `${i.ref} (${i.reason})`).join(", ");
@@ -2235,8 +2632,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
2235
2632
  }
2236
2633
  function live(meta) {
2237
2634
  const f = runFile(meta.id, "events.jsonl");
2238
- if (!fs13.existsSync(f)) return { steps: 0, toolCalls: 0 };
2239
- const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs13.readFileSync(f, "utf8"));
2635
+ if (!fs14.existsSync(f)) return { steps: 0, toolCalls: 0 };
2636
+ const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs14.readFileSync(f, "utf8"));
2240
2637
  return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
2241
2638
  }
2242
2639
  function progress(meta) {
@@ -2246,22 +2643,6 @@ function progress(meta) {
2246
2643
  return `pitroom \u2026 running \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 ${l.steps} steps, ${l.toolCalls} tool calls${last} \xB7 run ${meta.id}`;
2247
2644
  }
2248
2645
 
2249
- // src/core/target.ts
2250
- function parseTarget(spec, defaultBackend) {
2251
- const s = spec.trim();
2252
- if (!s) throw new UserError("empty worker target");
2253
- const i = s.indexOf(":");
2254
- if (i > 0 && isBackendId(s.slice(0, i))) {
2255
- const model = s.slice(i + 1).trim();
2256
- return model ? { backend: s.slice(0, i), model } : { backend: s.slice(0, i) };
2257
- }
2258
- if (isBackendId(s)) return { backend: s };
2259
- return { backend: defaultBackend, model: s };
2260
- }
2261
- var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
2262
- var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
2263
- var sameTarget = (a, b) => a.backend === b.backend && a.model === b.model;
2264
-
2265
2646
  // src/core/group.ts
2266
2647
  function groupIds(group) {
2267
2648
  return listRunIds().filter((id) => {
@@ -2272,21 +2653,21 @@ function groupIds(group) {
2272
2653
  }
2273
2654
  });
2274
2655
  }
2275
- var oneLine4 = (s, max) => {
2656
+ var oneLine5 = (s, max) => {
2276
2657
  const t = s.replace(/\s+/g, " ").trim();
2277
2658
  return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
2278
2659
  };
2279
2660
  function headline(meta, max = 160) {
2280
2661
  const text = readSummary(meta);
2281
2662
  const line = text.split("\n").find((l) => /^SUMMARY:/i.test(l.trim())) ?? text.split("\n").find((l) => l.trim()) ?? "";
2282
- return oneLine4(line.replace(/^SUMMARY:\s*/i, ""), max);
2663
+ return oneLine5(line.replace(/^SUMMARY:\s*/i, ""), max);
2283
2664
  }
2284
2665
  function table(metas) {
2285
2666
  if (!metas.length) return "no runs";
2286
2667
  const rows = metas.map((m) => {
2287
2668
  const l = m.state === "running" ? live(m) : void 0;
2288
2669
  const steps = l ? String(l.steps) : m.usage ? String(m.usage.steps) : "-";
2289
- const note = l?.last ? oneLine4(l.last, 48) : isActive(m.state) ? oneLine4(m.task, 48) : headline(m, 48) || oneLine4(m.error ?? m.task, 48);
2670
+ const note = l?.last ? oneLine5(l.last, 48) : isActive(m.state) ? oneLine5(m.task, 48) : headline(m, 48) || oneLine5(m.error ?? m.task, 48);
2290
2671
  return [m.id, m.state, m.mode, duration(m), steps, describeTarget(m.ran ?? m.worker), note];
2291
2672
  });
2292
2673
  const head = ["RUN", "STATE", "MODE", "TIME", "STEPS", "WORKER", "NOW / RESULT"];
@@ -2328,15 +2709,15 @@ async function watch(select, opts) {
2328
2709
  const prev = seen.get(m.id);
2329
2710
  const l = m.state === "running" ? live(m) : { steps: m.usage?.steps ?? 0, toolCalls: m.usage?.toolCalls ?? 0 };
2330
2711
  const attempts = m.attempts?.length ?? 0;
2331
- const base2 = { run: m.id, mode: m.mode, task: oneLine4(m.task, 60) };
2712
+ const base2 = { run: m.id, mode: m.mode, task: oneLine5(m.task, 60) };
2332
2713
  if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 }, m);
2333
2714
  else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 }, m);
2334
2715
  if (attempts > (prev?.attempts ?? 0)) {
2335
2716
  const a = m.attempts[attempts - 1];
2336
- emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine4(a.error, 120) }, m);
2717
+ emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine5(a.error, 120) }, m);
2337
2718
  }
2338
2719
  if (m.state === "running" && l.steps > (prev?.steps ?? 0)) {
2339
- emit({ event: "progress", run: m.id, steps: l.steps, tools: l.toolCalls, last: "last" in l && l.last ? oneLine4(String(l.last), 80) : void 0 });
2720
+ emit({ event: "progress", run: m.id, steps: l.steps, tools: l.toolCalls, last: "last" in l && l.last ? oneLine5(String(l.last), 80) : void 0 });
2340
2721
  }
2341
2722
  if (TERMINAL.includes(m.state) && prev?.state !== m.state) {
2342
2723
  emit({
@@ -2345,7 +2726,7 @@ async function watch(select, opts) {
2345
2726
  time: duration(m),
2346
2727
  worker: describeTarget(m.ran ?? m.worker),
2347
2728
  summary: headline(m) || void 0,
2348
- error: m.error ? oneLine4(m.error, 160) : void 0,
2729
+ error: m.error ? oneLine5(m.error, 160) : void 0,
2349
2730
  refs: m.refs ? `${m.refs.valid}/${m.refs.total}` : void 0,
2350
2731
  changes: m.changes?.length || void 0
2351
2732
  }, m);
@@ -2372,26 +2753,26 @@ ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 Ctrl-C to stop watchin
2372
2753
  }
2373
2754
 
2374
2755
  // src/core/install.ts
2375
- import fs14 from "node:fs";
2376
- import os7 from "node:os";
2377
- import path11 from "node:path";
2378
- import { fileURLToPath } from "node:url";
2756
+ import fs15 from "node:fs";
2757
+ import os8 from "node:os";
2758
+ import path12 from "node:path";
2759
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2379
2760
  var LEGACY = ["pitroom", "opencode-worker"];
2380
2761
  function packageRoot() {
2381
- return path11.resolve(path11.dirname(fileURLToPath(import.meta.url)), "..");
2762
+ return path12.resolve(path12.dirname(fileURLToPath2(import.meta.url)), "..");
2382
2763
  }
2383
2764
  function skillNames(root = packageRoot()) {
2384
- const dir = path11.join(root, "skills");
2385
- if (!fs14.existsSync(dir)) return [];
2386
- return fs14.readdirSync(dir).filter((d) => fs14.existsSync(path11.join(dir, d, "SKILL.md"))).sort();
2765
+ const dir = path12.join(root, "skills");
2766
+ if (!fs15.existsSync(dir)) return [];
2767
+ return fs15.readdirSync(dir).filter((d) => fs15.existsSync(path12.join(dir, d, "SKILL.md"))).sort();
2387
2768
  }
2388
2769
  function skillTargets() {
2389
- const home2 = os7.homedir();
2390
- const targets = [path11.join(home2, ".agents", "skills")];
2391
- if (fs14.existsSync(path11.join(home2, ".claude"))) targets.push(path11.join(home2, ".claude", "skills"));
2770
+ const home2 = os8.homedir();
2771
+ const targets = [path12.join(home2, ".agents", "skills")];
2772
+ if (fs15.existsSync(path12.join(home2, ".claude"))) targets.push(path12.join(home2, ".claude", "skills"));
2392
2773
  return targets;
2393
2774
  }
2394
- var launcherPath = () => path11.join(os7.homedir(), ".local", "bin", "pitroom");
2775
+ var launcherPath = () => path12.join(os8.homedir(), ".local", "bin", "pitroom");
2395
2776
  var LAUNCHER_MARK = "# pitroom launcher";
2396
2777
  function launcherScript(bundle) {
2397
2778
  const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
@@ -2409,7 +2790,7 @@ exit 127
2409
2790
  function isOurLauncher(file, root) {
2410
2791
  if (linksInto(file, root)) return true;
2411
2792
  try {
2412
- const s = fs14.readFileSync(file, "utf8");
2793
+ const s = fs15.readFileSync(file, "utf8");
2413
2794
  return s.includes(LAUNCHER_MARK) && s.includes(root);
2414
2795
  } catch {
2415
2796
  return false;
@@ -2417,57 +2798,57 @@ function isOurLauncher(file, root) {
2417
2798
  }
2418
2799
  function placeLauncher(bundle, root, force) {
2419
2800
  const dest = launcherPath();
2420
- fs14.mkdirSync(path11.dirname(dest), { recursive: true });
2421
- const exists = fs14.lstatSync(dest, { throwIfNoEntry: false });
2801
+ fs15.mkdirSync(path12.dirname(dest), { recursive: true });
2802
+ const exists = fs15.lstatSync(dest, { throwIfNoEntry: false });
2422
2803
  if (exists && !isOurLauncher(dest, root)) {
2423
2804
  if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
2424
- fs14.renameSync(dest, `${dest}.bak-${Date.now()}`);
2805
+ fs15.renameSync(dest, `${dest}.bak-${Date.now()}`);
2425
2806
  } else if (exists) {
2426
- fs14.rmSync(dest, { force: true });
2807
+ fs15.rmSync(dest, { force: true });
2427
2808
  }
2428
- fs14.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
2809
+ fs15.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
2429
2810
  return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
2430
2811
  }
2431
2812
  function linksInto(link, dir) {
2432
- const st = fs14.lstatSync(link, { throwIfNoEntry: false });
2813
+ const st = fs15.lstatSync(link, { throwIfNoEntry: false });
2433
2814
  if (!st?.isSymbolicLink()) return false;
2434
- const target = path11.resolve(path11.dirname(link), fs14.readlinkSync(link));
2435
- const rel = path11.relative(dir, target);
2436
- return !rel.startsWith("..") && !path11.isAbsolute(rel);
2815
+ const target = path12.resolve(path12.dirname(link), fs15.readlinkSync(link));
2816
+ const rel = path12.relative(dir, target);
2817
+ return !rel.startsWith("..") && !path12.isAbsolute(rel);
2437
2818
  }
2438
2819
  function place(src, dest, opts) {
2439
- fs14.mkdirSync(path11.dirname(dest), { recursive: true });
2440
- const st = fs14.lstatSync(dest, { throwIfNoEntry: false });
2820
+ fs15.mkdirSync(path12.dirname(dest), { recursive: true });
2821
+ const st = fs15.lstatSync(dest, { throwIfNoEntry: false });
2441
2822
  if (st && !st.isSymbolicLink()) {
2442
2823
  if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
2443
- fs14.renameSync(dest, `${dest}.bak-${Date.now()}`);
2824
+ fs15.renameSync(dest, `${dest}.bak-${Date.now()}`);
2444
2825
  } else if (st) {
2445
- fs14.unlinkSync(dest);
2826
+ fs15.unlinkSync(dest);
2446
2827
  }
2447
- if (opts.copy) fs14.cpSync(src, dest, { recursive: true });
2448
- else fs14.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs14.statSync(src).isDirectory() ? "dir" : "file");
2828
+ if (opts.copy) fs15.cpSync(src, dest, { recursive: true });
2829
+ else fs15.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs15.statSync(src).isDirectory() ? "dir" : "file");
2449
2830
  return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
2450
2831
  }
2451
2832
  function install(opts) {
2452
2833
  const root = packageRoot();
2453
2834
  const skills = skillNames(root);
2454
- if (!skills.length) throw new Error(`no skills found under ${path11.join(root, "skills")}`);
2835
+ if (!skills.length) throw new Error(`no skills found under ${path12.join(root, "skills")}`);
2455
2836
  const out = [];
2456
2837
  for (const base2 of skillTargets()) {
2457
2838
  for (const legacy of LEGACY) {
2458
- const l = path11.join(base2, legacy);
2839
+ const l = path12.join(base2, legacy);
2459
2840
  if (!skills.includes(legacy) && linksInto(l, root)) {
2460
- fs14.unlinkSync(l);
2841
+ fs15.unlinkSync(l);
2461
2842
  out.push(`\u2714 removed old link ${l}`);
2462
2843
  }
2463
2844
  }
2464
- for (const name of skills) out.push(place(path11.join(root, "skills", name), path11.join(base2, name), opts));
2845
+ for (const name of skills) out.push(place(path12.join(root, "skills", name), path12.join(base2, name), opts));
2465
2846
  }
2466
- const bundle = path11.join(root, "dist", "pitroom.mjs");
2467
- if (fs14.existsSync(bundle)) {
2847
+ const bundle = path12.join(root, "dist", "pitroom.mjs");
2848
+ if (fs15.existsSync(bundle)) {
2468
2849
  out.push(placeLauncher(bundle, root, opts.force));
2469
- const onPath = (process.env.PATH ?? "").split(path11.delimiter).some((d) => path11.resolve(d) === path11.dirname(launcherPath()));
2470
- if (!onPath) out.push(`! ${path11.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
2850
+ const onPath = (process.env.PATH ?? "").split(path12.delimiter).some((d) => path12.resolve(d) === path12.dirname(launcherPath()));
2851
+ if (!onPath) out.push(`! ${path12.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
2471
2852
  }
2472
2853
  return out;
2473
2854
  }
@@ -2475,38 +2856,38 @@ function uninstall() {
2475
2856
  const root = packageRoot();
2476
2857
  const out = [];
2477
2858
  for (const base2 of skillTargets()) {
2478
- if (!fs14.existsSync(base2)) continue;
2479
- for (const name of fs14.readdirSync(base2)) {
2480
- const l = path11.join(base2, name);
2859
+ if (!fs15.existsSync(base2)) continue;
2860
+ for (const name of fs15.readdirSync(base2)) {
2861
+ const l = path12.join(base2, name);
2481
2862
  if (linksInto(l, root)) {
2482
- fs14.unlinkSync(l);
2863
+ fs15.unlinkSync(l);
2483
2864
  out.push(`\u2714 removed ${l}`);
2484
2865
  }
2485
2866
  }
2486
2867
  }
2487
2868
  if (isOurLauncher(launcherPath(), root)) {
2488
- fs14.rmSync(launcherPath(), { force: true });
2869
+ fs15.rmSync(launcherPath(), { force: true });
2489
2870
  out.push(`\u2714 removed ${launcherPath()}`);
2490
2871
  }
2491
2872
  return out.length ? out : ["nothing to remove"];
2492
2873
  }
2493
2874
  function installedSkills() {
2494
2875
  const names = skillNames();
2495
- return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs14.existsSync(path11.join(base2, n, "SKILL.md"))) }));
2876
+ return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs15.existsSync(path12.join(base2, n, "SKILL.md"))) }));
2496
2877
  }
2497
2878
 
2498
2879
  // src/core/plan-status.ts
2499
- import crypto2 from "node:crypto";
2500
- import fs15 from "node:fs";
2501
- import path12 from "node:path";
2880
+ import crypto3 from "node:crypto";
2881
+ import fs16 from "node:fs";
2882
+ import path13 from "node:path";
2502
2883
  function notesFile(plan) {
2503
- const id = crypto2.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
2504
- return path12.join(home(), "plans", id, "notes.md");
2884
+ const id = crypto3.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
2885
+ return path13.join(home(), "plans", id, "notes.md");
2505
2886
  }
2506
2887
  function readNotes(plan) {
2507
2888
  const f = notesFile(plan);
2508
- if (!fs15.existsSync(f)) return [];
2509
- return fs15.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
2889
+ if (!fs16.existsSync(f)) return [];
2890
+ return fs16.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
2510
2891
  const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
2511
2892
  return m ? { at: m[1], text: m[2] } : { at: "", text: l };
2512
2893
  });
@@ -2514,11 +2895,11 @@ function readNotes(plan) {
2514
2895
  function addNote(planFile, text) {
2515
2896
  const plan = loadPlan(planFile);
2516
2897
  const f = notesFile(plan);
2517
- fs15.mkdirSync(path12.dirname(f), { recursive: true });
2518
- if (!fs15.existsSync(f)) fs15.writeFileSync(f, `# plan: ${plan.file}
2898
+ fs16.mkdirSync(path13.dirname(f), { recursive: true });
2899
+ if (!fs16.existsSync(f)) fs16.writeFileSync(f, `# plan: ${plan.file}
2519
2900
  `);
2520
2901
  const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
2521
- fs15.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
2902
+ fs16.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
2522
2903
  `);
2523
2904
  return f;
2524
2905
  }
@@ -2585,16 +2966,16 @@ ${rulings}`,
2585
2966
  }
2586
2967
 
2587
2968
  // src/core/review.ts
2969
+ import crypto5 from "node:crypto";
2970
+ import fs18 from "node:fs";
2971
+ import path15 from "node:path";
2972
+
2973
+ // src/vcs/git.ts
2974
+ import { spawnSync as spawnSync5 } from "node:child_process";
2588
2975
  import crypto4 from "node:crypto";
2589
2976
  import fs17 from "node:fs";
2977
+ import os9 from "node:os";
2590
2978
  import path14 from "node:path";
2591
-
2592
- // src/vcs/git.ts
2593
- import { spawnSync as spawnSync4 } from "node:child_process";
2594
- import crypto3 from "node:crypto";
2595
- import fs16 from "node:fs";
2596
- import os8 from "node:os";
2597
- import path13 from "node:path";
2598
2979
  var SAFE = [
2599
2980
  "-c",
2600
2981
  "color.ui=false",
@@ -2606,7 +2987,7 @@ var SAFE = [
2606
2987
  "diff.mnemonicPrefix=false"
2607
2988
  ];
2608
2989
  function git(cwd, args, env) {
2609
- const r = spawnSync4("git", [...SAFE, ...args], {
2990
+ const r = spawnSync5("git", [...SAFE, ...args], {
2610
2991
  cwd,
2611
2992
  env: env ?? process.env,
2612
2993
  encoding: "utf8",
@@ -2626,20 +3007,20 @@ function gitAvailable() {
2626
3007
  }
2627
3008
  function repoRoot(dir) {
2628
3009
  const r = git(dir, ["rev-parse", "--show-toplevel"]);
2629
- return r.code === 0 ? path13.resolve(r.stdout.trim()) : void 0;
3010
+ return r.code === 0 ? path14.resolve(r.stdout.trim()) : void 0;
2630
3011
  }
2631
3012
  function snapshotTree(root, exclude = []) {
2632
- const tmp = path13.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto3.randomBytes(4).toString("hex")}`);
2633
- const real = path13.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
3013
+ const tmp = path14.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
3014
+ const real = path14.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
2634
3015
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
2635
3016
  try {
2636
- if (fs16.existsSync(real)) fs16.copyFileSync(real, tmp);
3017
+ if (fs17.existsSync(real)) fs17.copyFileSync(real, tmp);
2637
3018
  else must(root, ["read-tree", "--empty"], env);
2638
3019
  must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
2639
3020
  return must(root, ["write-tree"], env).trim();
2640
3021
  } finally {
2641
- fs16.rmSync(tmp, { force: true });
2642
- fs16.rmSync(`${tmp}.lock`, { force: true });
3022
+ fs17.rmSync(tmp, { force: true });
3023
+ fs17.rmSync(`${tmp}.lock`, { force: true });
2643
3024
  }
2644
3025
  }
2645
3026
  var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
@@ -2659,27 +3040,27 @@ function diffTrees(root, a, b) {
2659
3040
  return { changes, stats: { files: changes.length, insertions, deletions }, patch };
2660
3041
  }
2661
3042
  function createIsolatedCopy(root, tree, dest) {
2662
- const objects = path13.join(path13.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
2663
- fs16.mkdirSync(path13.dirname(dest), { recursive: true });
2664
- must(path13.dirname(dest), ["init", "--quiet", dest]);
2665
- fs16.writeFileSync(path13.join(dest, ".git", "objects", "info", "alternates"), `${objects}
3043
+ const objects = path14.join(path14.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
3044
+ fs17.mkdirSync(path14.dirname(dest), { recursive: true });
3045
+ must(path14.dirname(dest), ["init", "--quiet", dest]);
3046
+ fs17.writeFileSync(path14.join(dest, ".git", "objects", "info", "alternates"), `${objects}
2666
3047
  `);
2667
3048
  must(dest, ["read-tree", tree]);
2668
3049
  must(dest, ["checkout-index", "--all", "--force"]);
2669
3050
  }
2670
3051
  function removeIsolatedCopy(dest, ownedBy) {
2671
- const rel = path13.relative(ownedBy, dest);
2672
- if (!rel || rel.startsWith("..") || path13.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
2673
- fs16.rmSync(dest, { recursive: true, force: true });
3052
+ const rel = path14.relative(ownedBy, dest);
3053
+ if (!rel || rel.startsWith("..") || path14.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
3054
+ fs17.rmSync(dest, { recursive: true, force: true });
2674
3055
  }
2675
3056
  function linkIntoWorktree(root, worktree, rels) {
2676
3057
  const linked = [];
2677
3058
  for (const rel of rels) {
2678
- const src = path13.join(root, rel);
2679
- const dst = path13.join(worktree, rel);
2680
- if (!fs16.existsSync(src) || fs16.existsSync(dst)) continue;
2681
- fs16.mkdirSync(path13.dirname(dst), { recursive: true });
2682
- fs16.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
3059
+ const src = path14.join(root, rel);
3060
+ const dst = path14.join(worktree, rel);
3061
+ if (!fs17.existsSync(src) || fs17.existsSync(dst)) continue;
3062
+ fs17.mkdirSync(path14.dirname(dst), { recursive: true });
3063
+ fs17.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
2683
3064
  linked.push(rel);
2684
3065
  }
2685
3066
  return linked;
@@ -2738,7 +3119,7 @@ function ancestors(m) {
2738
3119
  function briefOf(m) {
2739
3120
  const root = [m, ...ancestors(m)].at(-1);
2740
3121
  const f = runFile(root.id, "brief.md");
2741
- return fs17.existsSync(f) ? fs17.readFileSync(f, "utf8") : root.task;
3122
+ return fs18.existsSync(f) ? fs18.readFileSync(f, "utf8") : root.task;
2742
3123
  }
2743
3124
  function latestReview(ids) {
2744
3125
  for (const id of listRunIds().reverse()) {
@@ -2752,7 +3133,7 @@ function latestReview(ids) {
2752
3133
  }
2753
3134
  function diffOf(m, from = m.baseTree) {
2754
3135
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
2755
- if (where && fs17.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
3136
+ if (where && fs18.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
2756
3137
  if (from !== m.baseTree) {
2757
3138
  throw new UserError(`the isolated copy of ${m.id} is gone, so its fix round cannot be shown on its own; review a run that is not applied yet`, 3);
2758
3139
  }
@@ -2766,7 +3147,7 @@ function runReview(id) {
2766
3147
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
2767
3148
  const chain = ancestors(m);
2768
3149
  const previous = latestReview(chain.map((a) => a.id));
2769
- const dir = m.mode === "isolate" && m.worktree && fs17.existsSync(m.worktree) ? m.cwd : m.dir;
3150
+ const dir = m.mode === "isolate" && m.worktree && fs18.existsSync(m.worktree) ? m.cwd : m.dir;
2770
3151
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
2771
3152
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
2772
3153
  if (!previous) {
@@ -2840,15 +3221,15 @@ function pickReviewer(job) {
2840
3221
  function writePackage(job) {
2841
3222
  const g = gitDir(job.dir);
2842
3223
  if (!g) throw new UserError(`not a git repository: ${job.dir}`);
2843
- const file = path14.join(g, "pitroom", `review-${crypto4.randomBytes(4).toString("hex")}.md`);
2844
- fs17.mkdirSync(path14.dirname(file), { recursive: true });
2845
- fs17.writeFileSync(file, job.package);
3224
+ const file = path15.join(g, "pitroom", `review-${crypto5.randomBytes(4).toString("hex")}.md`);
3225
+ fs18.mkdirSync(path15.dirname(file), { recursive: true });
3226
+ fs18.writeFileSync(file, job.package);
2846
3227
  return file;
2847
3228
  }
2848
3229
 
2849
3230
  // src/core/templates.ts
2850
- import fs18 from "node:fs";
2851
- import path15 from "node:path";
3231
+ import fs19 from "node:fs";
3232
+ import path16 from "node:path";
2852
3233
  var FILES = {
2853
3234
  implementer: "pitroom-driven-development/implementer-prompt.md",
2854
3235
  "task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
@@ -2856,9 +3237,9 @@ var FILES = {
2856
3237
  "code-reviewer": "pitroom-review/code-reviewer.md"
2857
3238
  };
2858
3239
  function loadTemplate(name, root = packageRoot()) {
2859
- const file = path15.join(root, "skills", FILES[name]);
2860
- if (!fs18.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
2861
- return fs18.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
3240
+ const file = path16.join(root, "skills", FILES[name]);
3241
+ if (!fs19.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
3242
+ return fs19.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
2862
3243
  }
2863
3244
  function fill(template, values) {
2864
3245
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -2905,9 +3286,9 @@ function modelTable(opts = {}) {
2905
3286
  const known = /* @__PURE__ */ new Set();
2906
3287
  for (const b of allBackends()) {
2907
3288
  if (opts.backend && b.id !== opts.backend) continue;
2908
- const catalog4 = b.catalog?.() ?? { models: [], source: "no model list for this worker" };
2909
- sources.push(`${b.id}: ${catalog4.source}`);
2910
- for (const m of catalog4.models) {
3289
+ const catalog5 = b.catalog?.() ?? { models: [], source: "no model list for this worker" };
3290
+ sources.push(`${b.id}: ${catalog5.source}`);
3291
+ for (const m of catalog5.models) {
2911
3292
  const key = `${b.id}:${m.id}`;
2912
3293
  known.add(key);
2913
3294
  const s = seen.get(key);
@@ -2964,16 +3345,16 @@ function formatModels(t) {
2964
3345
 
2965
3346
  // src/core/dash.ts
2966
3347
  import { spawn } from "node:child_process";
2967
- import crypto5 from "node:crypto";
2968
- import fs19 from "node:fs";
3348
+ import crypto6 from "node:crypto";
3349
+ import fs20 from "node:fs";
2969
3350
  import http from "node:http";
2970
- import path16 from "node:path";
2971
- import { fileURLToPath as fileURLToPath2 } from "node:url";
3351
+ import path17 from "node:path";
3352
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2972
3353
 
2973
3354
  // src/core/file-diff.ts
2974
3355
  var PREVIEW_LINES = 300;
2975
3356
  function gitPath(value, prefix = true) {
2976
- let path24 = value;
3357
+ let path25 = value;
2977
3358
  if (value.startsWith('"') && value.endsWith('"')) {
2978
3359
  const bytes = [];
2979
3360
  const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
@@ -2981,9 +3362,9 @@ function gitPath(value, prefix = true) {
2981
3362
  if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
2982
3363
  else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
2983
3364
  }
2984
- path24 = new TextDecoder().decode(new Uint8Array(bytes));
3365
+ path25 = new TextDecoder().decode(new Uint8Array(bytes));
2985
3366
  }
2986
- return prefix ? path24.replace(/^[ab]\//, "") : path24;
3367
+ return prefix ? path25.replace(/^[ab]\//, "") : path25;
2987
3368
  }
2988
3369
  function headerPath(header) {
2989
3370
  const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
@@ -3098,7 +3479,7 @@ var WEEK_MS2 = 7 * 24 * 3600 * 1e3;
3098
3479
  var SCAN = 400;
3099
3480
  var RUN_ID2 = /^\d{8}-\d{6}-[0-9a-f]{4}$/;
3100
3481
  var LOCAL_HOST = /^(127\.0\.0\.1|localhost|\[::1\]):\d+$/i;
3101
- var oneLine5 = (s, max) => {
3482
+ var oneLine6 = (s, max) => {
3102
3483
  const t = s.replace(/\s+/g, " ").trim();
3103
3484
  return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
3104
3485
  };
@@ -3123,10 +3504,11 @@ function toRun(m) {
3123
3504
  steps: l ? l.steps : m.usage?.steps ?? 0,
3124
3505
  tokens: m.usage?.total,
3125
3506
  saved: m.savedUsd || void 0,
3126
- verdict: m.verdict ? `SPEC ${m.verdict.spec.toUpperCase()} \xB7 QUALITY ${m.verdict.quality.toUpperCase()}` : void 0,
3507
+ verdict: m.verdict ? `SPEC ${m.verdict.spec.toUpperCase()} \xB7 QUALITY ${m.verdict.quality.toUpperCase()}` : m.auditVerdict ? `AUDIT ${m.auditVerdict.toUpperCase()}` : void 0,
3508
+ audit: auditBadge(m),
3127
3509
  changes: m.changes?.length || void 0,
3128
3510
  applied: m.applied || void 0,
3129
- note: l?.last ? oneLine5(String(l.last), 140) : isActive(m.state) ? "" : m.verdict ? findings(m) : headline(m, 200) || oneLine5(m.error ?? "", 200)
3511
+ note: l?.last ? oneLine6(String(l.last), 140) : isActive(m.state) ? "" : m.verdict ? findings(m) : m.auditOf && m.state === "done" ? m.auditDisputed?.length ? `${m.auditDisputed.length} disputed` : "nothing disputed" : headline(m, 200) || oneLine6(m.error ?? "", 200)
3130
3512
  };
3131
3513
  }
3132
3514
  function dashState(opts = {}) {
@@ -3186,7 +3568,7 @@ function runDetail(id) {
3186
3568
  id,
3187
3569
  state: m.state,
3188
3570
  card: toRun(m),
3189
- task: (m.reviewOf ? `Review of ${m.reviewOf.replace(/\b([0-9a-f]{9})[0-9a-f]{31}\b/g, "$1")}` : m.task).slice(0, TASK_MAX),
3571
+ task: (m.auditOf ? `Audit of ${m.auditOf}` : m.reviewOf ? `Review of ${m.reviewOf.replace(/\b([0-9a-f]{9})[0-9a-f]{31}\b/g, "$1")}` : m.task).slice(0, TASK_MAX),
3190
3572
  steps,
3191
3573
  answer,
3192
3574
  changes: m.changes ?? [],
@@ -3214,6 +3596,7 @@ function runDetail(id) {
3214
3596
  refs: m.refs ? { valid: m.refs.valid, total: m.refs.total, invalid: m.refs.invalid.map((r) => `${r.ref} (${r.reason})`) } : void 0,
3215
3597
  verify: m.verifyResult && m.verify ? { command: m.verify, ok: m.verifyResult.ok, tail: m.verifyResult.tail } : void 0,
3216
3598
  error: m.error,
3599
+ audit: m.auditOf ? m.state === "done" ? { state: m.state, verdict: m.auditVerdict, disputed: m.auditDisputed ?? [] } : void 0 : m.audit && { id: m.audit.id, state: m.audit.state, verdict: m.audit.verdict, disputed: m.audit.disputed ?? [] },
3217
3600
  report: isActive(m.state) ? progress(m) : formatReport(m, void 0, 120)
3218
3601
  };
3219
3602
  }
@@ -3222,18 +3605,18 @@ var HEADERS = {
3222
3605
  "x-content-type-options": "nosniff",
3223
3606
  "referrer-policy": "no-referrer",
3224
3607
  // Own files only, plus the hash of the one inline theme script; styles may be inline (the components position popups with them).
3225
- "content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto5.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
3608
+ "content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto6.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
3226
3609
  };
3227
3610
  var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
3228
- var assetsDir = () => fileURLToPath2(new URL("./ui/", import.meta.url));
3611
+ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
3229
3612
  var assetCache = /* @__PURE__ */ new Map();
3230
3613
  function readAsset(route) {
3231
3614
  try {
3232
- const file = path16.join(assetsDir(), path16.basename(route));
3233
- const { mtimeMs } = fs19.statSync(file);
3615
+ const file = path17.join(assetsDir(), path17.basename(route));
3616
+ const { mtimeMs } = fs20.statSync(file);
3234
3617
  const hit = assetCache.get(route);
3235
3618
  if (hit && hit.mtimeMs === mtimeMs) return hit.data;
3236
- const data = fs19.readFileSync(file);
3619
+ const data = fs20.readFileSync(file);
3237
3620
  assetCache.set(route, { mtimeMs, data });
3238
3621
  return data;
3239
3622
  } catch {
@@ -3313,7 +3696,7 @@ function startDash(opts) {
3313
3696
  });
3314
3697
  });
3315
3698
  }
3316
- var registryFile = () => path16.join(home(), "dash.json");
3699
+ var registryFile = () => path17.join(home(), "dash.json");
3317
3700
  async function ping(port) {
3318
3701
  try {
3319
3702
  const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
@@ -3324,11 +3707,11 @@ async function ping(port) {
3324
3707
  }
3325
3708
  async function runningDash() {
3326
3709
  try {
3327
- const r = JSON.parse(fs19.readFileSync(registryFile(), "utf8"));
3710
+ const r = JSON.parse(fs20.readFileSync(registryFile(), "utf8"));
3328
3711
  if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
3329
3712
  } catch {
3330
3713
  }
3331
- fs19.rmSync(registryFile(), { force: true });
3714
+ fs20.rmSync(registryFile(), { force: true });
3332
3715
  return void 0;
3333
3716
  }
3334
3717
  function openBrowser(url) {
@@ -3343,7 +3726,7 @@ async function dashCommand(o) {
3343
3726
  if (o.stop) {
3344
3727
  if (!existing) throw new UserError("pitroom dash is not running");
3345
3728
  process.kill(existing.pid, "SIGTERM");
3346
- fs19.rmSync(registryFile(), { force: true });
3729
+ fs20.rmSync(registryFile(), { force: true });
3347
3730
  o.log(`stopped pitroom dash (${existing.url})`);
3348
3731
  return 0;
3349
3732
  }
@@ -3380,8 +3763,8 @@ async function dashCommand(o) {
3380
3763
  if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
3381
3764
  dash = await startDash({ port: 0, idleMs: o.idleMs });
3382
3765
  }
3383
- fs19.mkdirSync(home(), { recursive: true });
3384
- fs19.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
3766
+ fs20.mkdirSync(home(), { recursive: true });
3767
+ fs20.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
3385
3768
  const stop = () => void dash.close();
3386
3769
  process.once("SIGINT", stop);
3387
3770
  process.once("SIGTERM", stop);
@@ -3390,16 +3773,16 @@ async function dashCommand(o) {
3390
3773
  if (o.open) openBrowser(dash.url);
3391
3774
  await dash.closed;
3392
3775
  try {
3393
- if (JSON.parse(fs19.readFileSync(registryFile(), "utf8")).pid === process.pid) fs19.rmSync(registryFile(), { force: true });
3776
+ if (JSON.parse(fs20.readFileSync(registryFile(), "utf8")).pid === process.pid) fs20.rmSync(registryFile(), { force: true });
3394
3777
  } catch {
3395
3778
  }
3396
3779
  return 0;
3397
3780
  }
3398
3781
 
3399
3782
  // src/core/run.ts
3400
- import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
3401
- import fs25 from "node:fs";
3402
- import path21 from "node:path";
3783
+ import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
3784
+ import fs26 from "node:fs";
3785
+ import path22 from "node:path";
3403
3786
 
3404
3787
  // src/core/chain.ts
3405
3788
  function resolveChain(flags = {}) {
@@ -3427,7 +3810,7 @@ function resolveChain(flags = {}) {
3427
3810
  worker = { ...worker, model: `${base2}#${flags.effort}` };
3428
3811
  }
3429
3812
  }
3430
- if ((worker.backend === "codex" || worker.backend === "claude") && !(worker.model ?? "").replace(/#.*$/, "")) {
3813
+ if ((worker.backend === "codex" || worker.backend === "claude" || worker.backend === "gemini") && !(worker.model ?? "").replace(/#.*$/, "")) {
3431
3814
  warnings.push(
3432
3815
  `${worker.backend} has no pinned model, so it runs its own default (which can change and cost more): set "models": {"${worker.backend}": "<model>"} in the pitroom config, or pass -W ${worker.backend}:<model>`
3433
3816
  );
@@ -3449,11 +3832,11 @@ function resolveChain(flags = {}) {
3449
3832
 
3450
3833
  // src/core/process.ts
3451
3834
  import { spawn as spawn2 } from "node:child_process";
3452
- import fs21 from "node:fs";
3835
+ import fs22 from "node:fs";
3453
3836
 
3454
3837
  // src/vcs/guard.ts
3455
- import fs20 from "node:fs";
3456
- import path17 from "node:path";
3838
+ import fs21 from "node:fs";
3839
+ import path18 from "node:path";
3457
3840
  var VERSION = 1;
3458
3841
  var SHIM = `#!/bin/sh
3459
3842
  # pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
@@ -3528,15 +3911,15 @@ esac
3528
3911
  exec "$real" "$@"
3529
3912
  `;
3530
3913
  function shimDir() {
3531
- return path17.join(home(), "shim", `v${VERSION}`);
3914
+ return path18.join(home(), "shim", `v${VERSION}`);
3532
3915
  }
3533
3916
  function findRealGit(skip) {
3534
- for (const dir of (process.env.PATH ?? "").split(path17.delimiter).filter(Boolean)) {
3535
- if (path17.resolve(dir) === skip) continue;
3536
- const p = path17.join(dir, "git");
3917
+ for (const dir of (process.env.PATH ?? "").split(path18.delimiter).filter(Boolean)) {
3918
+ if (path18.resolve(dir) === skip) continue;
3919
+ const p = path18.join(dir, "git");
3537
3920
  try {
3538
- fs20.accessSync(p, fs20.constants.X_OK);
3539
- if (fs20.statSync(p).isFile()) return p;
3921
+ fs21.accessSync(p, fs21.constants.X_OK);
3922
+ if (fs21.statSync(p).isFile()) return p;
3540
3923
  } catch {
3541
3924
  }
3542
3925
  }
@@ -3549,13 +3932,13 @@ var REF_HOOK = `#!/bin/sh
3549
3932
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
3550
3933
  exit 1
3551
3934
  `;
3552
- var hooksDir = () => path17.join(home(), "git-hooks", `v${HOOK_VERSION}`);
3935
+ var hooksDir = () => path18.join(home(), "git-hooks", `v${HOOK_VERSION}`);
3553
3936
  function writeIfChanged(file, content) {
3554
- if (fs20.existsSync(file) && fs20.readFileSync(file, "utf8") === content) return;
3555
- fs20.mkdirSync(path17.dirname(file), { recursive: true });
3937
+ if (fs21.existsSync(file) && fs21.readFileSync(file, "utf8") === content) return;
3938
+ fs21.mkdirSync(path18.dirname(file), { recursive: true });
3556
3939
  const tmp = `${file}.${process.pid}.tmp`;
3557
- fs20.writeFileSync(tmp, content, { mode: 493 });
3558
- fs20.renameSync(tmp, file);
3940
+ fs21.writeFileSync(tmp, content, { mode: 493 });
3941
+ fs21.renameSync(tmp, file);
3559
3942
  }
3560
3943
  function withGitConfig(env, entries) {
3561
3944
  const out = { ...env };
@@ -3577,7 +3960,7 @@ function guardEnv(env) {
3577
3960
  PAGER: "cat"
3578
3961
  };
3579
3962
  if (process.platform === "win32") return { ...env, ...quiet };
3580
- writeIfChanged(path17.join(hooksDir(), "reference-transaction"), REF_HOOK);
3963
+ writeIfChanged(path18.join(hooksDir(), "reference-transaction"), REF_HOOK);
3581
3964
  env = withGitConfig(env, [
3582
3965
  ["core.hooksPath", hooksDir()],
3583
3966
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -3585,14 +3968,14 @@ function guardEnv(env) {
3585
3968
  const dir = shimDir();
3586
3969
  const real = findRealGit(dir);
3587
3970
  if (!real) return { ...env, ...quiet };
3588
- writeIfChanged(path17.join(dir, "git"), SHIM);
3589
- return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path17.delimiter}${env.PATH ?? ""}` };
3971
+ writeIfChanged(path18.join(dir, "git"), SHIM);
3972
+ return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path18.delimiter}${env.PATH ?? ""}` };
3590
3973
  }
3591
3974
 
3592
3975
  // src/core/process.ts
3593
3976
  async function spawnWorker(inv, opts) {
3594
- const out = fs21.openSync(opts.stdoutFile, "w");
3595
- const err = fs21.openSync(opts.stderrFile, "w");
3977
+ const out = fs22.openSync(opts.stdoutFile, "w");
3978
+ const err = fs22.openSync(opts.stderrFile, "w");
3596
3979
  const res = { code: null, timedOut: false, stopped: false };
3597
3980
  const child = spawn2(inv.command, inv.args, {
3598
3981
  cwd: opts.cwd,
@@ -3624,8 +4007,8 @@ async function spawnWorker(inv, opts) {
3624
4007
  if (killer) clearTimeout(killer);
3625
4008
  process.off("SIGINT", stop);
3626
4009
  process.off("SIGTERM", stop);
3627
- fs21.closeSync(out);
3628
- fs21.closeSync(err);
4010
+ fs22.closeSync(out);
4011
+ fs22.closeSync(err);
3629
4012
  return res;
3630
4013
  }
3631
4014
 
@@ -3648,13 +4031,20 @@ function parseVerdict(text) {
3648
4031
  minor: count("minor")
3649
4032
  };
3650
4033
  }
4034
+ function parseAudit(text) {
4035
+ const word = /^\s*AUDIT:\s*(AGREE|PARTIAL|DISAGREE)\b/im.exec(text)?.[1]?.toLowerCase();
4036
+ const verdict = word === "agree" || word === "partial" || word === "disagree" ? word : "unclear";
4037
+ const after = /^\s*DISPUTED:\s*$/im.exec(text);
4038
+ const disputed = after ? text.slice(after.index + after[0].length).split("\n").map((l) => l.trim()).filter((l) => /^[-*•]\s+\S/.test(l)).map((l) => l.replace(/^[-*•]\s+/, "")).filter((l) => !/^\(?none\)?\.?$/i.test(l)).slice(0, 8) : [];
4039
+ return { verdict, disputed };
4040
+ }
3651
4041
 
3652
4042
  // src/core/slots.ts
3653
- import crypto6 from "node:crypto";
3654
- import fs22 from "node:fs";
3655
- import path18 from "node:path";
3656
- var slotsDir = () => path18.join(home(), "slots");
3657
- var locksDir = () => path18.join(home(), "locks");
4043
+ import crypto7 from "node:crypto";
4044
+ import fs23 from "node:fs";
4045
+ import path19 from "node:path";
4046
+ var slotsDir = () => path19.join(home(), "slots");
4047
+ var locksDir = () => path19.join(home(), "locks");
3658
4048
  var STARTUP_GRACE_MS = 3e4;
3659
4049
  function holderActive(runId, self) {
3660
4050
  if (!runId || runId === self) return false;
@@ -3668,23 +4058,23 @@ function holderActive(runId, self) {
3668
4058
  }
3669
4059
  }
3670
4060
  function tryClaim(file, runId) {
3671
- fs22.mkdirSync(path18.dirname(file), { recursive: true });
4061
+ fs23.mkdirSync(path19.dirname(file), { recursive: true });
3672
4062
  try {
3673
- fs22.writeFileSync(file, runId, { flag: "wx" });
4063
+ fs23.writeFileSync(file, runId, { flag: "wx" });
3674
4064
  return true;
3675
4065
  } catch (e) {
3676
4066
  if (e.code !== "EEXIST") throw e;
3677
4067
  }
3678
4068
  let owner = "";
3679
4069
  try {
3680
- owner = fs22.readFileSync(file, "utf8").trim();
4070
+ owner = fs23.readFileSync(file, "utf8").trim();
3681
4071
  } catch {
3682
4072
  }
3683
4073
  if (owner === runId) return true;
3684
4074
  if (holderActive(owner, runId)) return false;
3685
- fs22.rmSync(file, { force: true });
4075
+ fs23.rmSync(file, { force: true });
3686
4076
  try {
3687
- fs22.writeFileSync(file, runId, { flag: "wx" });
4077
+ fs23.writeFileSync(file, runId, { flag: "wx" });
3688
4078
  return true;
3689
4079
  } catch {
3690
4080
  return false;
@@ -3693,25 +4083,25 @@ function tryClaim(file, runId) {
3693
4083
  function releaseIfOwner(file, runId) {
3694
4084
  if (!file) return;
3695
4085
  try {
3696
- if (fs22.readFileSync(file, "utf8").trim() === runId) fs22.rmSync(file, { force: true });
4086
+ if (fs23.readFileSync(file, "utf8").trim() === runId) fs23.rmSync(file, { force: true });
3697
4087
  } catch {
3698
4088
  }
3699
4089
  }
3700
4090
  function tryAcquireSlot(runId, maxParallel) {
3701
4091
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
3702
- const file = path18.join(slotsDir(), `slot-${n}`);
4092
+ const file = path19.join(slotsDir(), `slot-${n}`);
3703
4093
  if (tryClaim(file, runId)) return file;
3704
4094
  }
3705
4095
  return void 0;
3706
4096
  }
3707
4097
  var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
3708
- var lockFile = (repoRoot2) => path18.join(locksDir(), `write-${crypto6.createHash("sha1").update(path18.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
4098
+ var lockFile = (repoRoot2) => path19.join(locksDir(), `write-${crypto7.createHash("sha1").update(path19.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
3709
4099
  function acquireWriteLock(repoRoot2, runId) {
3710
4100
  const file = lockFile(repoRoot2);
3711
4101
  if (tryClaim(file, runId)) return;
3712
4102
  let owner = "";
3713
4103
  try {
3714
- owner = fs22.readFileSync(file, "utf8").trim();
4104
+ owner = fs23.readFileSync(file, "utf8").trim();
3715
4105
  } catch {
3716
4106
  }
3717
4107
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -3758,8 +4148,8 @@ ${task}`;
3758
4148
  }
3759
4149
 
3760
4150
  // src/core/refs.ts
3761
- import fs23 from "node:fs";
3762
- import path19 from "node:path";
4151
+ import fs24 from "node:fs";
4152
+ import path20 from "node:path";
3763
4153
  var FILE_EXTENSIONS = new Set(
3764
4154
  "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(" ")
3765
4155
  );
@@ -3810,13 +4200,13 @@ function extractRefs(answer) {
3810
4200
  return [...seen.values()];
3811
4201
  }
3812
4202
  function verifyRefs(refs, dirs) {
3813
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path19.resolve(d)))];
4203
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path20.resolve(d)))];
3814
4204
  const invalid = [];
3815
4205
  const lineCache = /* @__PURE__ */ new Map();
3816
4206
  const load = (file) => {
3817
4207
  if (!lineCache.has(file)) {
3818
- const size = fs23.statSync(file).size;
3819
- const lines = size > MAX_BYTES ? null : fs23.readFileSync(file, "utf8").split("\n");
4208
+ const size = fs24.statSync(file).size;
4209
+ const lines = size > MAX_BYTES ? null : fs24.readFileSync(file, "utf8").split("\n");
3820
4210
  if (lines && lines[lines.length - 1] === "") lines.pop();
3821
4211
  lineCache.set(file, lines);
3822
4212
  }
@@ -3827,9 +4217,9 @@ function verifyRefs(refs, dirs) {
3827
4217
  const candidates = (ref) => {
3828
4218
  const direct = resolve(ref.file, roots);
3829
4219
  if (direct) return [direct];
3830
- if (path19.isAbsolute(ref.file)) return [];
4220
+ if (path20.isAbsolute(ref.file)) return [];
3831
4221
  const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
3832
- return (byName().get(path19.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path19.basename(f) === wanted).slice(0, 20);
4222
+ return (byName().get(path20.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path20.basename(f) === wanted).slice(0, 20);
3833
4223
  };
3834
4224
  const check = (file, ref) => {
3835
4225
  const lines = load(file);
@@ -3844,7 +4234,7 @@ function verifyRefs(refs, dirs) {
3844
4234
  if (!files.length) {
3845
4235
  if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
3846
4236
  total++;
3847
- invalid.push({ ref: ref.text, reason: path19.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
4237
+ invalid.push({ ref: ref.text, reason: path20.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
3848
4238
  continue;
3849
4239
  }
3850
4240
  total++;
@@ -3865,7 +4255,7 @@ function enclosedBy(lines, ref) {
3865
4255
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
3866
4256
  }
3867
4257
  var isFileName = (name) => {
3868
- const base2 = path19.basename(name);
4258
+ const base2 = path20.basename(name);
3869
4259
  return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
3870
4260
  };
3871
4261
  function indexFiles(roots) {
@@ -3874,19 +4264,19 @@ function indexFiles(roots) {
3874
4264
  const walk = (dir) => {
3875
4265
  let entries;
3876
4266
  try {
3877
- entries = fs23.readdirSync(dir, { withFileTypes: true });
4267
+ entries = fs24.readdirSync(dir, { withFileTypes: true });
3878
4268
  } catch {
3879
4269
  return;
3880
4270
  }
3881
4271
  for (const e of entries) {
3882
4272
  if (count >= MAX_INDEXED) return;
3883
4273
  if (e.isDirectory()) {
3884
- if (!SKIP_DIRS.has(e.name)) walk(path19.join(dir, e.name));
4274
+ if (!SKIP_DIRS.has(e.name)) walk(path20.join(dir, e.name));
3885
4275
  } else if (e.isFile()) {
3886
4276
  count++;
3887
4277
  const list2 = index.get(e.name);
3888
- if (list2) list2.push(path19.join(dir, e.name));
3889
- else index.set(e.name, [path19.join(dir, e.name)]);
4278
+ if (list2) list2.push(path20.join(dir, e.name));
4279
+ else index.set(e.name, [path20.join(dir, e.name)]);
3890
4280
  }
3891
4281
  }
3892
4282
  };
@@ -3894,10 +4284,10 @@ function indexFiles(roots) {
3894
4284
  return index;
3895
4285
  }
3896
4286
  function resolve(ref, roots) {
3897
- const candidates = path19.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path19.join(r, ref));
4287
+ const candidates = path20.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path20.join(r, ref));
3898
4288
  return candidates.find((c) => {
3899
4289
  try {
3900
- return fs23.statSync(c).isFile() && inside(c, roots);
4290
+ return fs24.statSync(c).isFile() && inside(c, roots);
3901
4291
  } catch {
3902
4292
  return false;
3903
4293
  }
@@ -3905,16 +4295,16 @@ function resolve(ref, roots) {
3905
4295
  }
3906
4296
  function inside(file, roots) {
3907
4297
  return roots.some((r) => {
3908
- const rel = path19.relative(r, path19.resolve(file));
3909
- return rel !== "" && !rel.startsWith("..") && !path19.isAbsolute(rel);
4298
+ const rel = path20.relative(r, path20.resolve(file));
4299
+ return rel !== "" && !rel.startsWith("..") && !path20.isAbsolute(rel);
3910
4300
  });
3911
4301
  }
3912
4302
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3913
4303
 
3914
4304
  // src/core/secrets.ts
3915
4305
  import { execFileSync } from "node:child_process";
3916
- import fs24 from "node:fs";
3917
- import path20 from "node:path";
4306
+ import fs25 from "node:fs";
4307
+ import path21 from "node:path";
3918
4308
  var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
3919
4309
  var MAX_VISITED = 2e4;
3920
4310
  var MAX_DEPTH = 4;
@@ -3930,16 +4320,16 @@ function findSecretFiles(dir) {
3930
4320
  if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
3931
4321
  let entries;
3932
4322
  try {
3933
- entries = fs24.readdirSync(d, { withFileTypes: true });
4323
+ entries = fs25.readdirSync(d, { withFileTypes: true });
3934
4324
  } catch {
3935
4325
  return;
3936
4326
  }
3937
4327
  for (const e of entries) {
3938
4328
  visited++;
3939
4329
  if (e.isDirectory()) {
3940
- if (!SKIP_DIRS2.has(e.name)) walk(path20.join(d, e.name), depth + 1);
4330
+ if (!SKIP_DIRS2.has(e.name)) walk(path21.join(d, e.name), depth + 1);
3941
4331
  } else if (e.isFile() && looksSecret(e.name)) {
3942
- found.push(path20.relative(dir, path20.join(d, e.name)));
4332
+ found.push(path21.relative(dir, path21.join(d, e.name)));
3943
4333
  }
3944
4334
  }
3945
4335
  };
@@ -3949,7 +4339,7 @@ function findSecretFiles(dir) {
3949
4339
  function findSecretFilesInTree(root, dir) {
3950
4340
  try {
3951
4341
  const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
3952
- return out.split("\0").filter((f) => f && looksSecret(path20.basename(f)) && !path20.relative(dir, path20.join(root, f)).startsWith("..")).map((f) => path20.relative(dir, path20.join(root, f))).sort();
4342
+ return out.split("\0").filter((f) => f && looksSecret(path21.basename(f)) && !path21.relative(dir, path21.join(root, f)).startsWith("..")).map((f) => path21.relative(dir, path21.join(root, f))).sort();
3953
4343
  } catch {
3954
4344
  return [];
3955
4345
  }
@@ -3963,7 +4353,7 @@ function secretWarning(files, mode) {
3963
4353
  }
3964
4354
 
3965
4355
  // src/core/run.ts
3966
- var VERSION2 = true ? "0.8.1" : "0.0.0-dev";
4356
+ var VERSION2 = true ? "0.10.0" : "0.0.0-dev";
3967
4357
  function planWork(o) {
3968
4358
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
3969
4359
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -3995,7 +4385,7 @@ function prepareRun(o) {
3995
4385
  parent = readMeta(o.continueFrom);
3996
4386
  if (o.worker || o.tier || o.effort) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier/--effort");
3997
4387
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
3998
- if (parent.mode === "isolate" && (!parent.worktree || !fs25.existsSync(parent.worktree))) {
4388
+ if (parent.mode === "isolate" && (!parent.worktree || !fs26.existsSync(parent.worktree))) {
3999
4389
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
4000
4390
  }
4001
4391
  const ran = parent.ran ?? parent.worker;
@@ -4014,15 +4404,15 @@ function prepareRun(o) {
4014
4404
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
4015
4405
  }
4016
4406
  const mode = parent?.mode ?? o.mode;
4017
- const dir = path21.resolve(parent?.dir ?? o.dir);
4018
- if (!fs25.existsSync(dir) || !fs25.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
4407
+ const dir = path22.resolve(parent?.dir ?? o.dir);
4408
+ if (!fs26.existsSync(dir) || !fs26.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
4019
4409
  const root = repoRoot(dir);
4020
4410
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
4021
4411
  if (mode === "write" && !root && !o.allowNonGit) {
4022
4412
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
4023
4413
  }
4024
- const files = o.files.map((f) => path21.resolve(f));
4025
- for (const f of files) if (!fs25.existsSync(f)) throw new UserError(`file not found: ${f}`);
4414
+ const files = o.files.map((f) => path22.resolve(f));
4415
+ for (const f of files) if (!fs26.existsSync(f)) throw new UserError(`file not found: ${f}`);
4026
4416
  if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
4027
4417
  const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
4028
4418
  const note = secretWarning(secrets, mode);
@@ -4053,6 +4443,8 @@ function prepareRun(o) {
4053
4443
  baseTree: parent?.mode === "isolate" ? parent.baseTree : void 0,
4054
4444
  worktree: parent?.mode === "isolate" ? parent.worktree : void 0,
4055
4445
  reviewOf: o.review?.of,
4446
+ auditOf: o.audit?.of,
4447
+ auditRate: o.auditRate,
4056
4448
  plan: work.plan ?? o.review?.plan ?? parent?.plan,
4057
4449
  reviewKind: o.review?.kind,
4058
4450
  packageFile: o.review?.packageFile
@@ -4062,13 +4454,13 @@ function prepareRun(o) {
4062
4454
  try {
4063
4455
  acquireWriteLock(root, meta.id);
4064
4456
  } catch (e) {
4065
- fs25.rmSync(runDir(meta.id), { recursive: true, force: true });
4457
+ fs26.rmSync(runDir(meta.id), { recursive: true, force: true });
4066
4458
  throw e;
4067
4459
  }
4068
4460
  }
4069
- fs25.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4461
+ fs26.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4070
4462
  `);
4071
- if (work.brief) fs25.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
4463
+ if (work.brief) fs26.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
4072
4464
  return meta;
4073
4465
  }
4074
4466
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -4141,7 +4533,7 @@ async function execute(meta) {
4141
4533
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
4142
4534
  for (const f of ["events.jsonl", "stderr.log"]) {
4143
4535
  const from = runFile(meta.id, f);
4144
- if (fs25.existsSync(from)) fs25.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
4536
+ if (fs26.existsSync(from)) fs26.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
4145
4537
  }
4146
4538
  writeMeta(meta);
4147
4539
  }
@@ -4154,16 +4546,57 @@ async function execute(meta) {
4154
4546
  } finally {
4155
4547
  releaseSlot(slot, meta.id);
4156
4548
  }
4157
- return finalize(meta, result);
4549
+ const done = finalize(meta, result);
4550
+ autoAudit(done);
4551
+ return done;
4552
+ }
4553
+ function autoAudit(meta) {
4554
+ try {
4555
+ if (!auditable(meta, read(runFile(meta.id, "summary.md"))) || !sampled(meta)) return;
4556
+ startAudit(meta);
4557
+ } catch {
4558
+ }
4559
+ }
4560
+ function startAudit(meta, worker) {
4561
+ const answer = read(runFile(meta.id, "summary.md"));
4562
+ const auditor = worker ?? pickAuditor(meta);
4563
+ if (!auditor) return void 0;
4564
+ const a = prepareRun({
4565
+ mode: "read",
4566
+ task: auditTask(meta, answer),
4567
+ dir: meta.dir,
4568
+ files: [],
4569
+ link: [],
4570
+ worker: auditor,
4571
+ timeoutSec: Math.min(meta.timeoutSec, 15 * 60),
4572
+ allowNonGit: true,
4573
+ web: false,
4574
+ // never fall back to the worker being audited
4575
+ noFallback: true,
4576
+ group: meta.group,
4577
+ audit: { of: meta.id }
4578
+ });
4579
+ meta.audit = { id: a.id, state: "running" };
4580
+ writeMeta(meta);
4581
+ startInBackground(a);
4582
+ return a;
4583
+ }
4584
+ function settleAudit(a) {
4585
+ try {
4586
+ const target = readMeta(a.auditOf);
4587
+ target.audit = { id: a.id, state: a.state, verdict: a.state === "done" ? a.auditVerdict : void 0, disputed: a.state === "done" ? a.auditDisputed : void 0 };
4588
+ writeMeta(target);
4589
+ } catch {
4590
+ }
4158
4591
  }
4159
4592
  function prepareTree(meta) {
4160
4593
  const root = meta.repoRoot;
4161
4594
  if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
4162
4595
  if (root && meta.mode === "isolate" && !meta.worktree) {
4163
4596
  meta.baseTree = snapshotTree(root);
4164
- meta.worktree = path21.join(worktreesDir(), meta.id);
4597
+ meta.worktree = path22.join(worktreesDir(), meta.id);
4165
4598
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
4166
- meta.cwd = path21.join(meta.worktree, path21.relative(root, meta.dir));
4599
+ meta.cwd = path22.join(meta.worktree, path22.relative(root, meta.dir));
4167
4600
  const linked = linkIntoWorktree(root, meta.worktree, meta.link);
4168
4601
  if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
4169
4602
  }
@@ -4186,7 +4619,7 @@ function attempt(meta, backend, target) {
4186
4619
  timeoutSec: meta.timeoutSec
4187
4620
  });
4188
4621
  }
4189
- var read = (f) => fs25.existsSync(f) ? fs25.readFileSync(f, "utf8") : "";
4622
+ var read = (f) => fs26.existsSync(f) ? fs26.readFileSync(f, "utf8") : "";
4190
4623
  function retryableFailure(meta, backend, res) {
4191
4624
  const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
4192
4625
  if (run.usage.steps > 0) return void 0;
@@ -4205,12 +4638,12 @@ function finalize(meta, res) {
4205
4638
  const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
4206
4639
  meta.sessionId = run.sessionId ?? meta.sessionId;
4207
4640
  meta.usage = run.usage;
4208
- fs25.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
4641
+ fs26.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
4209
4642
  `);
4210
4643
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
4211
4644
  if (meta.reviewOf) {
4212
4645
  meta.verdict = parseVerdict(run.finalText);
4213
- if (meta.packageFile) fs25.rmSync(meta.packageFile, { force: true });
4646
+ if (meta.packageFile) fs26.rmSync(meta.packageFile, { force: true });
4214
4647
  }
4215
4648
  if (!res.timedOut && !res.stopped && !meta.error) {
4216
4649
  const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
@@ -4234,14 +4667,20 @@ function finalize(meta, res) {
4234
4667
  }
4235
4668
  }
4236
4669
  meta.state = res.timedOut ? "timeout" : res.stopped ? "stopped" : meta.error ? "failed" : "done";
4670
+ if (meta.auditOf && meta.state === "done") {
4671
+ const found = parseAudit(run.finalText);
4672
+ meta.auditVerdict = found.verdict;
4673
+ meta.auditDisputed = found.disputed;
4674
+ }
4237
4675
  if (meta.state === "done" && !run.finalText) meta.warnings.push("worker finished without a written answer");
4238
4676
  if (meta.state === "done" && meta.verify) meta.verifyResult = runVerify(meta);
4239
4677
  meta.endedAt = (/* @__PURE__ */ new Date()).toISOString();
4240
4678
  meta.returnedTokens = estimateTokens(formatReport(meta, run.finalText));
4241
- meta.savedUsd = savedUsd(meta.usage, meta.returnedTokens);
4679
+ meta.savedUsd = meta.auditOf ? 0 : savedUsd(meta.usage, meta.returnedTokens);
4242
4680
  writeMeta(meta);
4243
4681
  if (meta.mode === "write" && meta.repoRoot) releaseWriteLock(meta.repoRoot, meta.id);
4244
4682
  record(meta);
4683
+ if (meta.auditOf) settleAudit(meta);
4245
4684
  return meta;
4246
4685
  }
4247
4686
  function captureChanges(meta) {
@@ -4252,10 +4691,10 @@ function captureChanges(meta) {
4252
4691
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
4253
4692
  meta.changes = d.changes;
4254
4693
  meta.stats = d.stats;
4255
- fs25.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4694
+ fs26.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4256
4695
  }
4257
4696
  function runVerify(meta) {
4258
- const r = spawnSync5(meta.verify, {
4697
+ const r = spawnSync6(meta.verify, {
4259
4698
  cwd: meta.cwd,
4260
4699
  shell: true,
4261
4700
  encoding: "utf8",
@@ -4265,7 +4704,7 @@ function runVerify(meta) {
4265
4704
  env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4266
4705
  });
4267
4706
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
4268
- fs25.writeFileSync(runFile(meta.id, "verify.log"), output);
4707
+ fs26.writeFileSync(runFile(meta.id, "verify.log"), output);
4269
4708
  return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
4270
4709
  }
4271
4710
  function applyRun(meta, allowDelete = false) {
@@ -4310,7 +4749,7 @@ function discardRun(meta) {
4310
4749
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
4311
4750
  }
4312
4751
  function cleanupWorktree(meta) {
4313
- if (meta.worktree && fs25.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4752
+ if (meta.worktree && fs26.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4314
4753
  }
4315
4754
 
4316
4755
  // src/cli/commands.ts
@@ -4360,12 +4799,39 @@ async function cmdReview(p) {
4360
4799
  writeMeta(meta);
4361
4800
  }
4362
4801
  } catch (e) {
4363
- fs26.rmSync(packageFile, { force: true });
4802
+ fs27.rmSync(packageFile, { force: true });
4364
4803
  throw e;
4365
4804
  }
4366
- fs26.writeFileSync(runFile(meta.id, "package.md"), job.package);
4805
+ fs27.writeFileSync(runFile(meta.id, "package.md"), job.package);
4367
4806
  return launch(p, meta);
4368
4807
  }
4808
+ async function cmdAudit(p) {
4809
+ if (!p.positional[0]) throw new UserError("pitroom audit RUN: which run?");
4810
+ if (has(p, "write") || has(p, "isolate")) throw new UserError("audits are read-only; drop -w/-i");
4811
+ const m = freshMeta(resolveRun(p.positional[0]));
4812
+ if (isActive(m.state)) throw new UserError(`run ${m.id} is still ${m.state}; pitroom wait ${m.id} first`, 3);
4813
+ if (m.reviewOf || m.auditOf) throw new UserError(`run ${m.id} is itself a ${m.reviewOf ? "review" : "audit"}`);
4814
+ if (m.mode !== "read") throw new UserError(`run ${m.id} changed files; an audit re-checks a read run's answer: pitroom review ${m.id} judges a change`);
4815
+ const answer = readSummary(m);
4816
+ if (!answer.trim()) throw new UserError(`run ${m.id} gave no answer to audit`);
4817
+ const worker = flag(p, "worker") ?? (flag(p, "tier") ? void 0 : pickAuditor(m));
4818
+ if (!worker && !flag(p, "tier")) {
4819
+ throw new UserError('no other worker to audit with: name one with -W, or configure tiers "audit" or "cheap", or fallback workers, that differ from the one that answered', 3);
4820
+ }
4821
+ const a = prepareRun({
4822
+ ...runOptions(p, auditTask(m, answer)),
4823
+ mode: "read",
4824
+ dir: m.dir,
4825
+ worker,
4826
+ group: flag(p, "group") ?? m.group,
4827
+ noFallback: true,
4828
+ audit: { of: m.id },
4829
+ auditRate: void 0
4830
+ });
4831
+ m.audit = { id: a.id, state: "running" };
4832
+ writeMeta(m);
4833
+ return launch(p, a);
4834
+ }
4369
4835
  function cmdPlan(p) {
4370
4836
  const [sub, file, ...rest] = p.positional;
4371
4837
  if (sub === "status" && file) {
@@ -4383,7 +4849,7 @@ function cmdPlan(p) {
4383
4849
  }
4384
4850
  function cmdCrew(p) {
4385
4851
  const file = flag(p, "task-file");
4386
- const tasks = (file ? fs26.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
4852
+ const tasks = (file ? fs27.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
4387
4853
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
4388
4854
  if (has(p, "write") && tasks.length > 1) {
4389
4855
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -4584,13 +5050,13 @@ function sinceMs(s) {
4584
5050
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
4585
5051
  return Date.now() - Number(m[1]) * 864e5;
4586
5052
  }
4587
- var readStdin = () => process.stdin.isTTY ? "" : fs26.readFileSync(0, "utf8");
5053
+ var readStdin = () => process.stdin.isTTY ? "" : fs27.readFileSync(0, "utf8");
4588
5054
  function cmdStatusline(p) {
4589
5055
  const input = readStdin();
4590
5056
  const lines = [];
4591
5057
  const then = flag(p, "then");
4592
5058
  if (then) {
4593
- const r = spawnSync6("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
5059
+ const r = spawnSync7("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
4594
5060
  if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
4595
5061
  }
4596
5062
  try {
@@ -4624,8 +5090,8 @@ function cmdModels(p) {
4624
5090
  return 0;
4625
5091
  }
4626
5092
  function cmdHookStart() {
4627
- const script = path22.join(path22.dirname(path22.dirname(fileURLToPath3(import.meta.url))), "hooks", "session-start.mjs");
4628
- if (fs26.existsSync(script)) spawnSync6(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
5093
+ const script = path23.join(path23.dirname(path23.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
5094
+ if (fs27.existsSync(script)) spawnSync7(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
4629
5095
  return 0;
4630
5096
  }
4631
5097
  function cmdInit(p) {
@@ -4665,8 +5131,8 @@ function cmdSavings(p) {
4665
5131
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
4666
5132
  const out = flag(p, "card");
4667
5133
  if (out) {
4668
- fs26.writeFileSync(out, card(t, period));
4669
- console.log(`card written to ${path22.resolve(out)}`);
5134
+ fs27.writeFileSync(out, card(t, period));
5135
+ console.log(`card written to ${path23.resolve(out)}`);
4670
5136
  }
4671
5137
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
4672
5138
  return 0;
@@ -4686,7 +5152,7 @@ function cmdConfig(p) {
4686
5152
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
4687
5153
  return 0;
4688
5154
  }
4689
- console.log(`config file: ${configPath()}${fs26.existsSync(configPath()) ? "" : " (not present)"}`);
5155
+ console.log(`config file: ${configPath()}${fs27.existsSync(configPath()) ? "" : " (not present)"}`);
4690
5156
  for (const [key, s] of Object.entries(eff)) {
4691
5157
  const v = Array.isArray(s.value) ? s.value.join(", ") || "\u2014" : s.value && typeof s.value === "object" ? Object.entries(s.value).map(([k, m]) => `${k}=${m}`).join(", ") || "\u2014" : s.value === void 0 ? key === "model" ? "the worker's default" : "\u2014" : String(s.value);
4692
5158
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -4710,7 +5176,7 @@ function cmdClean(p) {
4710
5176
  const m = readMeta(id);
4711
5177
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
4712
5178
  recordRun(m);
4713
- fs26.rmSync(runDir(id), { recursive: true, force: true });
5179
+ fs27.rmSync(runDir(id), { recursive: true, force: true });
4714
5180
  }
4715
5181
  console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
4716
5182
  return 0;
@@ -4719,26 +5185,27 @@ var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[
4719
5185
  var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positional[0])))), 0);
4720
5186
 
4721
5187
  // src/core/doctor.ts
4722
- import { spawnSync as spawnSync7 } from "node:child_process";
4723
- import fs27 from "node:fs";
4724
- import os9 from "node:os";
4725
- import path23 from "node:path";
5188
+ import { spawnSync as spawnSync8 } from "node:child_process";
5189
+ import fs28 from "node:fs";
5190
+ import os10 from "node:os";
5191
+ import path24 from "node:path";
4726
5192
  var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
4727
5193
  var NEXT = [
4728
5194
  { when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
4729
5195
  { when: /pitroom install|no Pitroom skills|launcher on PATH/, command: "pitroom install", why: "link the skills and the pitroom command" },
4730
5196
  { when: /first `node` on PATH/, command: "nvm alias default 24", why: "a current Node first in every new shell" },
4731
- { when: /not logged in/, command: "claude auth login", why: "sign in the Claude Code worker (Codex: codex login)" }
5197
+ { when: /not logged in/, command: "claude auth login", why: "sign in the Claude Code worker (Codex: codex login)" },
5198
+ { when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
4732
5199
  ];
4733
5200
  function firstNodeOnPath() {
4734
- for (const dir of (process.env.PATH ?? "").split(path23.delimiter).filter(Boolean)) {
4735
- const file = path23.join(dir, process.platform === "win32" ? "node.exe" : "node");
5201
+ for (const dir of (process.env.PATH ?? "").split(path24.delimiter).filter(Boolean)) {
5202
+ const file = path24.join(dir, process.platform === "win32" ? "node.exe" : "node");
4736
5203
  try {
4737
- if (!fs27.statSync(file).isFile()) continue;
5204
+ if (!fs28.statSync(file).isFile()) continue;
4738
5205
  } catch {
4739
5206
  continue;
4740
5207
  }
4741
- const r = spawnSync7(file, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
5208
+ const r = spawnSync8(file, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
4742
5209
  return r.status === 0 ? { path: file, version: r.stdout.trim() } : { path: file, version: "" };
4743
5210
  }
4744
5211
  return void 0;
@@ -4753,7 +5220,7 @@ var READ_ONLY_HOW = {
4753
5220
  "tool-allowlist": "a tool allowlist",
4754
5221
  "approval-mode": "the CLI's read-only approval mode"
4755
5222
  };
4756
- function doctor4(probe) {
5223
+ function doctor5(probe) {
4757
5224
  const checks = [];
4758
5225
  let current = "Setup";
4759
5226
  const section2 = (name) => void (current = name);
@@ -4761,12 +5228,12 @@ function doctor4(probe) {
4761
5228
  const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
4762
5229
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
4763
5230
  const onPath = firstNodeOnPath();
4764
- if (onPath && onPath.version && tooOld(onPath.version) && path23.resolve(onPath.path) !== path23.resolve(process.execPath)) {
5231
+ if (onPath && onPath.version && tooOld(onPath.version) && path24.resolve(onPath.path) !== path24.resolve(process.execPath)) {
4765
5232
  add("warn", `the first \`node\` on PATH is v${onPath.version} (${onPath.path}), older than the 22.13 Pitroom needs: shells that agent apps start may use it (the pitroom command finds a newer Node itself; other tools may not)`);
4766
5233
  }
4767
5234
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
4768
5235
  const cfg = loadConfig();
4769
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs27.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
5236
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs28.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
4770
5237
  for (const w of cfg.warnings) add("warn", w);
4771
5238
  if (process.platform !== "win32") {
4772
5239
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
@@ -4782,6 +5249,11 @@ function doctor4(probe) {
4782
5249
  add("fail", e.message);
4783
5250
  }
4784
5251
  if (chain.length) add("ok", `worker chain: ${chain.map(describeTarget).join(" \u2192 ")}`);
5252
+ const rate2 = effective().audit.value;
5253
+ if (rate2 > 0 && chain.length) {
5254
+ const auditor = pickAuditor({ worker: chain[0], fallback: chain.slice(1) });
5255
+ add(auditor ? "ok" : "warn", auditor ? `audit: ${Math.round(rate2 * 100)}% of read runs are re-checked by ${auditor}` : `audit is on (${Math.round(rate2 * 100)}%) but no other worker could do it: add tiers "audit" or "cheap", or a fallback, that differs from ${describeTarget(chain[0])}`);
5256
+ }
4785
5257
  const tierTargets = [];
4786
5258
  const tierNames = [];
4787
5259
  for (const name of Object.keys(effective().tiers.value)) {
@@ -4863,7 +5335,7 @@ function liveProbe(target) {
4863
5335
  title: "pitroom doctor probe"
4864
5336
  });
4865
5337
  const t0 = Date.now();
4866
- const r = spawnSync7(inv.command, inv.args, {
5338
+ const r = spawnSync8(inv.command, inv.args, {
4867
5339
  encoding: "utf8",
4868
5340
  timeout: 3e5,
4869
5341
  stdio: ["ignore", "pipe", "pipe"],
@@ -4893,14 +5365,14 @@ function skillChecks() {
4893
5365
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
4894
5366
  }
4895
5367
  if (viaPlugin) {
4896
- const linked = installedSkills().some((i) => i.base.includes(`${path23.sep}.claude${path23.sep}`) && i.names.length);
5368
+ const linked = installedSkills().some((i) => i.base.includes(`${path24.sep}.claude${path24.sep}`) && i.names.length);
4897
5369
  checks.push(
4898
5370
  linked ? { level: "warn", message: "Pitroom is installed as a Claude Code plugin and linked into ~/.claude/skills: skills load twice; run `pitroom uninstall` or remove the plugin" } : { level: "ok", message: "Claude Code plugin installed (skills + session-start hook)" }
4899
5371
  );
4900
5372
  }
4901
5373
  const launcher = launcherPath();
4902
- if (fs27.existsSync(launcher)) {
4903
- const r = spawnSync7(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
5374
+ if (fs28.existsSync(launcher)) {
5375
+ const r = spawnSync8(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
4904
5376
  checks.push(
4905
5377
  r.status === 0 ? { level: "ok", message: `launcher ${launcher} \u2192 pitroom ${r.stdout.trim()}` } : { level: "fail", message: `launcher ${launcher} does not start: ${(r.stderr || r.stdout).trim().slice(0, 200)}` }
4906
5378
  );
@@ -4911,8 +5383,8 @@ function skillChecks() {
4911
5383
  }
4912
5384
  function pluginInstalled() {
4913
5385
  try {
4914
- const f = path23.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4915
- const plugins = JSON.parse(fs27.readFileSync(f, "utf8")).plugins ?? {};
5386
+ const f = path24.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5387
+ const plugins = JSON.parse(fs28.readFileSync(f, "utf8")).plugins ?? {};
4916
5388
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
4917
5389
  } catch {
4918
5390
  return false;
@@ -4929,24 +5401,24 @@ function superpowersActive() {
4929
5401
  for (const p of openCodePlugins()) {
4930
5402
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
4931
5403
  }
4932
- for (const base2 of [path23.join(os9.homedir(), ".agents", "skills"), path23.join(os9.homedir(), ".claude", "skills")]) {
4933
- const dir = path23.join(base2, "using-superpowers");
4934
- if (fs27.existsSync(path23.join(dir, "SKILL.md"))) found.push(dir);
5404
+ for (const base2 of [path24.join(os10.homedir(), ".agents", "skills"), path24.join(os10.homedir(), ".claude", "skills")]) {
5405
+ const dir = path24.join(base2, "using-superpowers");
5406
+ if (fs28.existsSync(path24.join(dir, "SKILL.md"))) found.push(dir);
4935
5407
  }
4936
5408
  return found;
4937
5409
  }
4938
5410
  function claudePlugins() {
4939
5411
  try {
4940
- const f = path23.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4941
- return Object.keys(JSON.parse(fs27.readFileSync(f, "utf8")).plugins ?? {});
5412
+ const f = path24.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5413
+ return Object.keys(JSON.parse(fs28.readFileSync(f, "utf8")).plugins ?? {});
4942
5414
  } catch {
4943
5415
  return [];
4944
5416
  }
4945
5417
  }
4946
5418
  function claudePluginEnabled(key) {
4947
5419
  try {
4948
- const f = path23.join(os9.homedir(), ".claude", "settings.json");
4949
- return JSON.parse(fs27.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
5420
+ const f = path24.join(os10.homedir(), ".claude", "settings.json");
5421
+ return JSON.parse(fs28.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
4950
5422
  } catch {
4951
5423
  return true;
4952
5424
  }
@@ -4955,7 +5427,7 @@ function codexPlugins() {
4955
5427
  const plugins = /* @__PURE__ */ new Map();
4956
5428
  let text;
4957
5429
  try {
4958
- text = fs27.readFileSync(path23.join(process.env.CODEX_HOME ?? path23.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
5430
+ text = fs28.readFileSync(path24.join(process.env.CODEX_HOME ?? path24.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
4959
5431
  } catch {
4960
5432
  return plugins;
4961
5433
  }
@@ -4973,11 +5445,11 @@ function codexPlugins() {
4973
5445
  return plugins;
4974
5446
  }
4975
5447
  function openCodePlugins() {
4976
- const dir = path23.join(process.env.XDG_CONFIG_HOME ?? path23.join(os9.homedir(), ".config"), "opencode");
5448
+ const dir = path24.join(process.env.XDG_CONFIG_HOME ?? path24.join(os10.homedir(), ".config"), "opencode");
4977
5449
  const plugins = [];
4978
5450
  for (const name of ["opencode.json", "opencode.jsonc"]) {
4979
5451
  try {
4980
- const text = fs27.readFileSync(path23.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
5452
+ const text = fs28.readFileSync(path24.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
4981
5453
  const list2 = JSON.parse(text).plugin;
4982
5454
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
4983
5455
  } catch {
@@ -4985,7 +5457,7 @@ function openCodePlugins() {
4985
5457
  }
4986
5458
  for (const folder of ["plugin", "plugins"]) {
4987
5459
  try {
4988
- for (const f of fs27.readdirSync(path23.join(dir, folder))) plugins.push(path23.join(dir, folder, f));
5460
+ for (const f of fs28.readdirSync(path24.join(dir, folder))) plugins.push(path24.join(dir, folder, f));
4989
5461
  } catch {
4990
5462
  }
4991
5463
  }
@@ -5005,6 +5477,8 @@ Usage
5005
5477
  pitroom review [run | --range A..B [--plan PLAN]] [--tier T | -W T] [--bg]
5006
5478
  read-only review of a run's change (a follow-up: only its
5007
5479
  fix round) or of a commit range; by default on another worker
5480
+ pitroom audit RUN [-W worker] another worker re-checks a read run's answer (AGREE / PARTIAL / DISAGREE);
5481
+ "audit" in the config (0 to 1) does it for a share of read runs
5008
5482
  pitroom plan status PLAN [--json] a plan's progress: runs, STATUS, review, fix rounds, applied
5009
5483
  pitroom plan note PLAN "Task N: \u2026" record a completion, deferred finding or ruling (outside the repo)
5010
5484
  pitroom status [run | -g NAME] state / live progress (default: latest run)
@@ -5052,6 +5526,7 @@ Run options
5052
5526
  -W, --worker T worker target "backend[:model]" (default: config "worker", else opencode)
5053
5527
  -m, --model M model for that worker (default: the worker CLI's own default)
5054
5528
  --tier NAME a worker from the config's "tiers" (e.g. cheap, standard, capable); -W wins
5529
+ --audit have another worker re-check this read run's answer; --no-audit skips it (default: the config's "audit")
5055
5530
  --effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
5056
5531
  --plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
5057
5532
  --step N the plan task for --plan
@@ -5080,6 +5555,7 @@ var COMMANDS = {
5080
5555
  run: cmdRun,
5081
5556
  crew: cmdCrew,
5082
5557
  review: cmdReview,
5558
+ audit: cmdAudit,
5083
5559
  plan: cmdPlan,
5084
5560
  status: cmdStatus,
5085
5561
  wait: cmdWait,
@@ -5098,7 +5574,7 @@ var COMMANDS = {
5098
5574
  statusline: cmdStatusline,
5099
5575
  "hook-card": cmdHookCard,
5100
5576
  "hook-start": cmdHookStart,
5101
- doctor: (p) => doctor4(has(p, "probe")),
5577
+ doctor: (p) => doctor5(has(p, "probe")),
5102
5578
  config: cmdConfig,
5103
5579
  init: cmdInit,
5104
5580
  install: cmdInstall,