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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +26 -0
- package/README.md +31 -10
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/dist/lib.mjs +367 -86
- package/dist/pitroom.mjs +981 -505
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +30 -30
- package/docs/backends.md +28 -1
- package/package.json +3 -2
- package/policies/gemini/base.toml +27 -0
- package/policies/gemini/no-web.toml +6 -0
- package/policies/gemini/shell.toml +50 -0
- package/policies/gemini/worker-settings.json +9 -0
- package/skills/using-pitroom/SKILL.md +2 -2
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
|
|
273
|
-
return typeof
|
|
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/
|
|
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
|
|
557
|
-
var
|
|
558
|
-
function
|
|
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: ${
|
|
596
|
-
if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text:
|
|
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" &&
|
|
605
|
-
if (st.status === "completed" &&
|
|
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 =
|
|
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:
|
|
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 +=
|
|
620
|
-
usage2.output +=
|
|
621
|
-
usage2.reasoning +=
|
|
622
|
-
usage2.cacheRead +=
|
|
623
|
-
usage2.cacheWrite +=
|
|
624
|
-
usage2.total +=
|
|
625
|
-
usage2.cost = (usage2.cost ?? 0) +
|
|
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
|
|
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
|
|
1016
|
+
function num4(v) {
|
|
652
1017
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
653
1018
|
}
|
|
654
|
-
function
|
|
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 ?
|
|
1026
|
+
return v ? oneLine4(String(v), 60) : "";
|
|
662
1027
|
}
|
|
663
1028
|
|
|
664
1029
|
// src/backends/opencode/profiles.ts
|
|
665
|
-
import
|
|
666
|
-
import
|
|
667
|
-
import
|
|
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 ??
|
|
1130
|
+
const data = process.env.XDG_DATA_HOME ?? path6.join(os6.homedir(), ".local", "share");
|
|
766
1131
|
const rules = {
|
|
767
1132
|
"*": "deny",
|
|
768
|
-
[
|
|
769
|
-
[
|
|
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([
|
|
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
|
|
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
|
|
1229
|
+
var binary4 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
|
|
865
1230
|
function oc(args, opts = {}) {
|
|
866
|
-
const { command, prefix } = resolveCommand(
|
|
867
|
-
const r =
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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 (${
|
|
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 ${
|
|
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 =
|
|
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:
|
|
1006
|
-
invocation:
|
|
1007
|
-
parse:
|
|
1008
|
-
failure:
|
|
1009
|
-
defaultModel:
|
|
1370
|
+
binary: binary4,
|
|
1371
|
+
invocation: invocation4,
|
|
1372
|
+
parse: parseEvents4,
|
|
1373
|
+
failure: failure4,
|
|
1374
|
+
defaultModel: defaultModel3,
|
|
1010
1375
|
resolveModel: resolveModel2,
|
|
1011
1376
|
listModels,
|
|
1012
|
-
catalog:
|
|
1013
|
-
doctor:
|
|
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 = [
|
|
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
|
|
1399
|
+
import fs8 from "node:fs";
|
|
1035
1400
|
|
|
1036
1401
|
// src/core/config.ts
|
|
1037
|
-
import
|
|
1038
|
-
import
|
|
1039
|
-
import
|
|
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
|
|
1055
|
-
const base2 = process.platform === "win32" ? process.env.APPDATA ??
|
|
1056
|
-
return
|
|
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 (
|
|
1430
|
+
if (fs7.existsSync(file)) {
|
|
1065
1431
|
let raw;
|
|
1066
1432
|
try {
|
|
1067
|
-
raw = JSON.parse(
|
|
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
|
|
1625
|
+
if (file) return fs8.readFileSync(file, "utf8");
|
|
1355
1626
|
const words = p.positional;
|
|
1356
|
-
if (words.length === 1 && words[0] === "-") return
|
|
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
|
|
1398
|
-
import
|
|
1399
|
-
import
|
|
1400
|
-
import { fileURLToPath as
|
|
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
|
|
1404
|
-
import
|
|
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 = (
|
|
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
|
|
1456
|
-
return { id, name: b.name, found: isFound(
|
|
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
|
|
1471
|
-
const chosen = opts.model ??
|
|
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:
|
|
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 (!
|
|
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
|
|
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:
|
|
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 ${
|
|
1502
|
-
|
|
1503
|
-
if (plan.exists)
|
|
1504
|
-
|
|
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
|
|
1885
|
+
import fs12 from "node:fs";
|
|
1525
1886
|
import { createRequire } from "node:module";
|
|
1526
|
-
import
|
|
1887
|
+
import path10 from "node:path";
|
|
1527
1888
|
import zlib from "node:zlib";
|
|
1528
1889
|
|
|
1529
1890
|
// src/core/receipt.ts
|
|
1530
|
-
import
|
|
1531
|
-
import
|
|
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
|
-
|
|
1568
|
-
|
|
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 (!
|
|
1573
|
-
return
|
|
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
|
|
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 (
|
|
2100
|
+
if (fs11.existsSync(marker)) continue;
|
|
1737
2101
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
1738
|
-
|
|
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) =>
|
|
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) ||
|
|
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
|
|
2136
|
+
var ANSWER_MAX2 = 2e5;
|
|
1773
2137
|
var cached2;
|
|
1774
|
-
var historyFile = () =>
|
|
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
|
-
|
|
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 (
|
|
1824
|
-
if (
|
|
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 (!
|
|
1831
|
-
|
|
1832
|
-
|
|
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 =
|
|
2200
|
+
const dir = path10.dirname(runFile(meta.id, "meta.json"));
|
|
1837
2201
|
let names;
|
|
1838
2202
|
try {
|
|
1839
|
-
names =
|
|
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(
|
|
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))
|
|
1847
|
-
else gzipFile(
|
|
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
|
|
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 :
|
|
1869
|
-
const patch = known?.has ? void 0 :
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
2028
|
-
|
|
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(
|
|
2050
|
-
const w = db.prepare(
|
|
2051
|
-
const d = db.prepare(
|
|
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
|
-
|
|
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
|
|
2462
|
+
import fs14 from "node:fs";
|
|
2067
2463
|
|
|
2068
2464
|
// src/core/plan.ts
|
|
2069
|
-
import
|
|
2070
|
-
import
|
|
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 =
|
|
2118
|
-
if (!
|
|
2119
|
-
const plan = parsePlan(
|
|
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) =>
|
|
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
|
|
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 (!
|
|
2239
|
-
const p = getBackend((meta.ran ?? meta.worker).backend).parse(
|
|
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
|
|
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
|
|
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 ?
|
|
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:
|
|
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:
|
|
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 ?
|
|
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 ?
|
|
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
|
|
2376
|
-
import
|
|
2377
|
-
import
|
|
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
|
|
2762
|
+
return path12.resolve(path12.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
2382
2763
|
}
|
|
2383
2764
|
function skillNames(root = packageRoot()) {
|
|
2384
|
-
const dir =
|
|
2385
|
-
if (!
|
|
2386
|
-
return
|
|
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 =
|
|
2390
|
-
const targets = [
|
|
2391
|
-
if (
|
|
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 = () =>
|
|
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 =
|
|
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
|
-
|
|
2421
|
-
const exists =
|
|
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
|
-
|
|
2805
|
+
fs15.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
2425
2806
|
} else if (exists) {
|
|
2426
|
-
|
|
2807
|
+
fs15.rmSync(dest, { force: true });
|
|
2427
2808
|
}
|
|
2428
|
-
|
|
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 =
|
|
2813
|
+
const st = fs15.lstatSync(link, { throwIfNoEntry: false });
|
|
2433
2814
|
if (!st?.isSymbolicLink()) return false;
|
|
2434
|
-
const target =
|
|
2435
|
-
const rel =
|
|
2436
|
-
return !rel.startsWith("..") && !
|
|
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
|
-
|
|
2440
|
-
const st =
|
|
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
|
-
|
|
2824
|
+
fs15.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
2444
2825
|
} else if (st) {
|
|
2445
|
-
|
|
2826
|
+
fs15.unlinkSync(dest);
|
|
2446
2827
|
}
|
|
2447
|
-
if (opts.copy)
|
|
2448
|
-
else
|
|
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 ${
|
|
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 =
|
|
2839
|
+
const l = path12.join(base2, legacy);
|
|
2459
2840
|
if (!skills.includes(legacy) && linksInto(l, root)) {
|
|
2460
|
-
|
|
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(
|
|
2845
|
+
for (const name of skills) out.push(place(path12.join(root, "skills", name), path12.join(base2, name), opts));
|
|
2465
2846
|
}
|
|
2466
|
-
const bundle =
|
|
2467
|
-
if (
|
|
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(
|
|
2470
|
-
if (!onPath) out.push(`! ${
|
|
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 (!
|
|
2479
|
-
for (const name of
|
|
2480
|
-
const l =
|
|
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
|
-
|
|
2863
|
+
fs15.unlinkSync(l);
|
|
2483
2864
|
out.push(`\u2714 removed ${l}`);
|
|
2484
2865
|
}
|
|
2485
2866
|
}
|
|
2486
2867
|
}
|
|
2487
2868
|
if (isOurLauncher(launcherPath(), root)) {
|
|
2488
|
-
|
|
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) =>
|
|
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
|
|
2500
|
-
import
|
|
2501
|
-
import
|
|
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 =
|
|
2504
|
-
return
|
|
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 (!
|
|
2509
|
-
return
|
|
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
|
-
|
|
2518
|
-
if (!
|
|
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
|
-
|
|
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 =
|
|
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 ?
|
|
3010
|
+
return r.code === 0 ? path14.resolve(r.stdout.trim()) : void 0;
|
|
2630
3011
|
}
|
|
2631
3012
|
function snapshotTree(root, exclude = []) {
|
|
2632
|
-
const tmp =
|
|
2633
|
-
const real =
|
|
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 (
|
|
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
|
-
|
|
2642
|
-
|
|
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 =
|
|
2663
|
-
|
|
2664
|
-
must(
|
|
2665
|
-
|
|
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 =
|
|
2672
|
-
if (!rel || rel.startsWith("..") ||
|
|
2673
|
-
|
|
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 =
|
|
2679
|
-
const dst =
|
|
2680
|
-
if (!
|
|
2681
|
-
|
|
2682
|
-
|
|
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
|
|
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 &&
|
|
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 &&
|
|
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 =
|
|
2844
|
-
|
|
2845
|
-
|
|
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
|
|
2851
|
-
import
|
|
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 =
|
|
2860
|
-
if (!
|
|
2861
|
-
return
|
|
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
|
|
2909
|
-
sources.push(`${b.id}: ${
|
|
2910
|
-
for (const m of
|
|
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
|
|
2968
|
-
import
|
|
3348
|
+
import crypto6 from "node:crypto";
|
|
3349
|
+
import fs20 from "node:fs";
|
|
2969
3350
|
import http from "node:http";
|
|
2970
|
-
import
|
|
2971
|
-
import { fileURLToPath as
|
|
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
|
|
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
|
-
|
|
3365
|
+
path25 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
2985
3366
|
}
|
|
2986
|
-
return prefix ?
|
|
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
|
|
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 ?
|
|
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-${
|
|
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 = () =>
|
|
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 =
|
|
3233
|
-
const { mtimeMs } =
|
|
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 =
|
|
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 = () =>
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3384
|
-
|
|
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(
|
|
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
|
|
3401
|
-
import
|
|
3402
|
-
import
|
|
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
|
|
3835
|
+
import fs22 from "node:fs";
|
|
3453
3836
|
|
|
3454
3837
|
// src/vcs/guard.ts
|
|
3455
|
-
import
|
|
3456
|
-
import
|
|
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
|
|
3914
|
+
return path18.join(home(), "shim", `v${VERSION}`);
|
|
3532
3915
|
}
|
|
3533
3916
|
function findRealGit(skip) {
|
|
3534
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
3535
|
-
if (
|
|
3536
|
-
const p =
|
|
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
|
-
|
|
3539
|
-
if (
|
|
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 = () =>
|
|
3935
|
+
var hooksDir = () => path18.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
3553
3936
|
function writeIfChanged(file, content) {
|
|
3554
|
-
if (
|
|
3555
|
-
|
|
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
|
-
|
|
3558
|
-
|
|
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(
|
|
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(
|
|
3589
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${
|
|
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 =
|
|
3595
|
-
const err =
|
|
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
|
-
|
|
3628
|
-
|
|
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
|
|
3654
|
-
import
|
|
3655
|
-
import
|
|
3656
|
-
var slotsDir = () =>
|
|
3657
|
-
var locksDir = () =>
|
|
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
|
-
|
|
4061
|
+
fs23.mkdirSync(path19.dirname(file), { recursive: true });
|
|
3672
4062
|
try {
|
|
3673
|
-
|
|
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 =
|
|
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
|
-
|
|
4075
|
+
fs23.rmSync(file, { force: true });
|
|
3686
4076
|
try {
|
|
3687
|
-
|
|
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 (
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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
|
|
3762
|
-
import
|
|
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) =>
|
|
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 =
|
|
3819
|
-
const lines = size > MAX_BYTES ? null :
|
|
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 (
|
|
4220
|
+
if (path20.isAbsolute(ref.file)) return [];
|
|
3831
4221
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
3832
|
-
return (byName().get(
|
|
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:
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
3889
|
-
else index.set(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 =
|
|
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
|
|
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 =
|
|
3909
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
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
|
|
3917
|
-
import
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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 || !
|
|
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 =
|
|
4018
|
-
if (!
|
|
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) =>
|
|
4025
|
-
for (const f of files) if (!
|
|
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
|
-
|
|
4457
|
+
fs26.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
4066
4458
|
throw e;
|
|
4067
4459
|
}
|
|
4068
4460
|
}
|
|
4069
|
-
|
|
4461
|
+
fs26.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
4070
4462
|
`);
|
|
4071
|
-
if (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 (
|
|
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
|
-
|
|
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 =
|
|
4597
|
+
meta.worktree = path22.join(worktreesDir(), meta.id);
|
|
4165
4598
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
4166
|
-
meta.cwd =
|
|
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) =>
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
4694
|
+
fs26.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4256
4695
|
}
|
|
4257
4696
|
function runVerify(meta) {
|
|
4258
|
-
const r =
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
4802
|
+
fs27.rmSync(packageFile, { force: true });
|
|
4364
4803
|
throw e;
|
|
4365
4804
|
}
|
|
4366
|
-
|
|
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 ?
|
|
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 ? "" :
|
|
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 =
|
|
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 =
|
|
4628
|
-
if (
|
|
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
|
-
|
|
4669
|
-
console.log(`card written to ${
|
|
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(`})`);
|
|
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()}${
|
|
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
|
-
|
|
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
|
|
4723
|
-
import
|
|
4724
|
-
import
|
|
4725
|
-
import
|
|
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(
|
|
4735
|
-
const file =
|
|
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 (!
|
|
5204
|
+
if (!fs28.statSync(file).isFile()) continue;
|
|
4738
5205
|
} catch {
|
|
4739
5206
|
continue;
|
|
4740
5207
|
}
|
|
4741
|
-
const r =
|
|
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
|
|
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) &&
|
|
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()}${
|
|
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 =
|
|
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(`${
|
|
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 (
|
|
4903
|
-
const r =
|
|
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 =
|
|
4915
|
-
const plugins = JSON.parse(
|
|
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 [
|
|
4933
|
-
const dir =
|
|
4934
|
-
if (
|
|
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 =
|
|
4941
|
-
return Object.keys(JSON.parse(
|
|
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 =
|
|
4949
|
-
return JSON.parse(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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) =>
|
|
5577
|
+
doctor: (p) => doctor5(has(p, "probe")),
|
|
5102
5578
|
config: cmdConfig,
|
|
5103
5579
|
init: cmdInit,
|
|
5104
5580
|
install: cmdInstall,
|