pitroom 0.9.0 → 0.10.1
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 +17 -0
- package/README.md +27 -6
- package/dist/lib.mjs +8 -0
- package/dist/pitroom.mjs +261 -48
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +30 -30
- package/package.json +1 -1
- package/skills/using-pitroom/SKILL.md +2 -2
package/dist/pitroom.mjs
CHANGED
|
@@ -1413,7 +1413,8 @@ var SCHEMA = {
|
|
|
1413
1413
|
maxParallel: "number",
|
|
1414
1414
|
models: "record",
|
|
1415
1415
|
tiers: "record",
|
|
1416
|
-
costs: "numbers"
|
|
1416
|
+
costs: "numbers",
|
|
1417
|
+
audit: "number"
|
|
1417
1418
|
};
|
|
1418
1419
|
function configPath() {
|
|
1419
1420
|
if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -1457,6 +1458,7 @@ function matches(v, type) {
|
|
|
1457
1458
|
if (type === "numbers") {
|
|
1458
1459
|
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
|
|
1459
1460
|
}
|
|
1461
|
+
if (type === "number") return typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
1460
1462
|
return typeof v === type;
|
|
1461
1463
|
}
|
|
1462
1464
|
var DEFAULT_PARALLEL = 20;
|
|
@@ -1466,6 +1468,10 @@ var positiveInt = (v) => {
|
|
|
1466
1468
|
const n = Number(v);
|
|
1467
1469
|
return v !== void 0 && v !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
|
|
1468
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
|
+
};
|
|
1469
1475
|
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
1470
1476
|
function setting(flag2, env, conf, fallback) {
|
|
1471
1477
|
if (flag2 !== void 0) return { value: flag2, source: "flag" };
|
|
@@ -1489,7 +1495,8 @@ function effective(flags = {}) {
|
|
|
1489
1495
|
maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
|
|
1490
1496
|
models: setting(void 0, void 0, c.models, {}),
|
|
1491
1497
|
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
1492
|
-
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)
|
|
1493
1500
|
};
|
|
1494
1501
|
}
|
|
1495
1502
|
|
|
@@ -1536,6 +1543,8 @@ var BOOL_FLAGS = {
|
|
|
1536
1543
|
"-i": "isolate",
|
|
1537
1544
|
"--isolate": "isolate",
|
|
1538
1545
|
"--bg": "bg",
|
|
1546
|
+
"--audit": "audit",
|
|
1547
|
+
"--no-audit": "no-audit",
|
|
1539
1548
|
"--web": "web",
|
|
1540
1549
|
"--no-fallback": "no-fallback",
|
|
1541
1550
|
"--json": "json",
|
|
@@ -1622,8 +1631,11 @@ function runOptions(p, task) {
|
|
|
1622
1631
|
const modes = ["read", "write", "isolate"].filter((m) => has(p, m));
|
|
1623
1632
|
if (modes.length > 1) throw new UserError("choose one of --read, --write, --isolate");
|
|
1624
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");
|
|
1625
1636
|
return {
|
|
1626
1637
|
mode: modes[0] ?? "read",
|
|
1638
|
+
auditRate: has(p, "no-audit") ? 0 : has(p, "audit") ? 1 : void 0,
|
|
1627
1639
|
task,
|
|
1628
1640
|
dir: flag(p, "dir") ?? process.cwd(),
|
|
1629
1641
|
files: p.flags.get("file") ?? [],
|
|
@@ -1782,6 +1794,93 @@ function formatInit(plan, written) {
|
|
|
1782
1794
|
return out.join("\n");
|
|
1783
1795
|
}
|
|
1784
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
|
+
|
|
1785
1884
|
// src/core/history.ts
|
|
1786
1885
|
import fs12 from "node:fs";
|
|
1787
1886
|
import { createRequire } from "node:module";
|
|
@@ -1812,7 +1911,7 @@ function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
|
|
|
1812
1911
|
return Math.max(0, wouldCost - (usage2.cost ?? 0) - readingTheReport);
|
|
1813
1912
|
}
|
|
1814
1913
|
function record(meta) {
|
|
1815
|
-
if (!meta.usage?.steps) return;
|
|
1914
|
+
if (!meta.usage?.steps || meta.auditOf) return;
|
|
1816
1915
|
const entry = {
|
|
1817
1916
|
id: meta.id,
|
|
1818
1917
|
at: meta.endedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1939,6 +2038,7 @@ var RUN_ID = /\b\d{8}-\d{6}-[0-9a-f]{4}\b/g;
|
|
|
1939
2038
|
var PITROOM_CALL = /(^|[\s;&|(`])(\S*\/)?pitroom(\.mjs)?(\s|$)/;
|
|
1940
2039
|
var ICON = { done: "\u2714", failed: "\u2718", timeout: "\u23F1", stopped: "\u25A0" };
|
|
1941
2040
|
function kind(m) {
|
|
2041
|
+
if (m.auditOf) return "audit";
|
|
1942
2042
|
if (m.reviewOf) return m.reviewKind === "range" ? "branch review" : m.reviewKind === "fix" ? "re-review" : "review";
|
|
1943
2043
|
return m.mode === "read" ? "research" : m.mode === "isolate" ? "change (isolated copy)" : "change";
|
|
1944
2044
|
}
|
|
@@ -1962,6 +2062,7 @@ function ranTarget(m) {
|
|
|
1962
2062
|
}
|
|
1963
2063
|
function what(m) {
|
|
1964
2064
|
if (m.plan) return short(`Task ${m.plan.step}: ${m.plan.title}`);
|
|
2065
|
+
if (m.auditOf) return `of ${m.auditOf}`;
|
|
1965
2066
|
if (m.reviewOf) return `of ${m.reviewOf.replace(/\b([0-9a-f]{9})[0-9a-f]{31}\b/g, "$1")}`;
|
|
1966
2067
|
const line = m.task.split("\n").find((l) => l.trim()) ?? "";
|
|
1967
2068
|
const brief2 = /^You are implementing Task (\d+)\b.*?\bplan\s+(\S+)/i.exec(line);
|
|
@@ -1981,6 +2082,7 @@ function endedCard(m) {
|
|
|
1981
2082
|
what(m),
|
|
1982
2083
|
elapsed(m),
|
|
1983
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` : ""}`,
|
|
1984
2086
|
m.changes?.length ? `${m.changes.length} file${m.changes.length === 1 ? "" : "s"} changed` : "",
|
|
1985
2087
|
m.savedUsd ? `~${usd(m.savedUsd)} saved` : ""
|
|
1986
2088
|
];
|
|
@@ -2031,7 +2133,7 @@ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs11.existsSync(`${runsDir()}/${
|
|
|
2031
2133
|
// src/core/history.ts
|
|
2032
2134
|
var SCHEMA_VERSION = 1;
|
|
2033
2135
|
var PATCH_MAX = 1e6;
|
|
2034
|
-
var
|
|
2136
|
+
var ANSWER_MAX2 = 2e5;
|
|
2035
2137
|
var cached2;
|
|
2036
2138
|
var historyFile = () => path10.join(home(), "history.db");
|
|
2037
2139
|
function openDb() {
|
|
@@ -2127,7 +2229,7 @@ function recordRun(meta) {
|
|
|
2127
2229
|
try {
|
|
2128
2230
|
const u = meta.usage;
|
|
2129
2231
|
const known = db.prepare("SELECT answer IS NOT NULL AS has FROM runs WHERE id = ?").get(meta.id);
|
|
2130
|
-
const answer = known?.has ? void 0 : clip5(readRunFile(meta.id, "summary.md")?.trim(),
|
|
2232
|
+
const answer = known?.has ? void 0 : clip5(readRunFile(meta.id, "summary.md")?.trim(), ANSWER_MAX2);
|
|
2131
2233
|
const patch = known?.has ? void 0 : clip5(readRunFile(meta.id, "changes.patch"), PATCH_MAX);
|
|
2132
2234
|
const seconds = meta.endedAt ? Math.max(0, Math.round((Date.parse(meta.endedAt) - Date.parse(meta.startedAt)) / 1e3)) : null;
|
|
2133
2235
|
const t = meta.ran ?? meta.worker;
|
|
@@ -2143,8 +2245,8 @@ function recordRun(meta) {
|
|
|
2143
2245
|
task: clip5(meta.task, 2e4) ?? "",
|
|
2144
2246
|
grp: meta.group ?? null,
|
|
2145
2247
|
dir: meta.dir,
|
|
2146
|
-
review_of: meta.reviewOf ?? null,
|
|
2147
|
-
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,
|
|
2148
2250
|
seconds,
|
|
2149
2251
|
steps: u?.steps ?? null,
|
|
2150
2252
|
tool_calls: u?.toolCalls ?? null,
|
|
@@ -2261,7 +2363,7 @@ function listHistory(q = {}) {
|
|
|
2261
2363
|
try {
|
|
2262
2364
|
const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
|
|
2263
2365
|
const page = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
|
|
2264
|
-
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));
|
|
2265
2367
|
return {
|
|
2266
2368
|
total,
|
|
2267
2369
|
rows: rows.map((r) => ({
|
|
@@ -2274,6 +2376,7 @@ function listHistory(q = {}) {
|
|
|
2274
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()) ?? "",
|
|
2275
2377
|
group: r.grp ?? void 0,
|
|
2276
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 }),
|
|
2277
2380
|
seconds: r.seconds ?? void 0,
|
|
2278
2381
|
steps: r.steps ?? void 0,
|
|
2279
2382
|
tokens: r.tokens ?? void 0,
|
|
@@ -2286,37 +2389,68 @@ function listHistory(q = {}) {
|
|
|
2286
2389
|
return { rows: [], total: 0 };
|
|
2287
2390
|
}
|
|
2288
2391
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
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 }));
|
|
2291
2419
|
const key = (backend, model) => `${backend ?? ""}\0${model ?? ""}`;
|
|
2292
2420
|
const index = new Map(out.map((r) => [key(r.backend, r.model), r]));
|
|
2293
2421
|
for (const e of ledger) {
|
|
2294
2422
|
const backend = e.backend ?? "opencode";
|
|
2295
2423
|
let row = index.get(key(backend, e.model)) ?? (e.model ? out.find((r) => r.backend === backend && r.model?.endsWith(`/${e.model}`)) : void 0);
|
|
2296
2424
|
if (!row) {
|
|
2297
|
-
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 };
|
|
2298
2426
|
index.set(key(backend, e.model), row);
|
|
2299
2427
|
out.push(row);
|
|
2300
2428
|
}
|
|
2301
2429
|
row.saved += e.saved;
|
|
2302
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
|
+
}
|
|
2303
2435
|
return out;
|
|
2304
2436
|
}
|
|
2305
2437
|
function historyStats(sinceMs2) {
|
|
2306
|
-
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: [] };
|
|
2307
2439
|
const db = openDb();
|
|
2308
2440
|
if (!db) return empty;
|
|
2309
2441
|
const since = sinceMs2 ? new Date(sinceMs2).toISOString() : "";
|
|
2310
2442
|
try {
|
|
2311
|
-
const t = db.prepare(
|
|
2312
|
-
const w = db.prepare(
|
|
2313
|
-
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);
|
|
2314
2447
|
const ledger = readLedger(sinceMs2);
|
|
2315
2448
|
const byDay = /* @__PURE__ */ new Map();
|
|
2316
2449
|
for (const e of ledger) byDay.set(e.at.slice(0, 10), (byDay.get(e.at.slice(0, 10)) ?? 0) + e.saved);
|
|
2317
2450
|
return {
|
|
2318
2451
|
totals: { runs: t.runs, ok: t.ok, failed: t.runs - t.ok, seconds: t.seconds, tokens: t.tokens, saved: totals(ledger).saved },
|
|
2319
|
-
|
|
2452
|
+
audits: audits.total,
|
|
2453
|
+
byWorker: workerRows(w, ledger, audits.byWorker),
|
|
2320
2454
|
byDay: d.reverse().map((r) => ({ day: r.day, runs: r.runs, ok: r.ok ?? 0, saved: byDay.get(r.day) ?? 0 }))
|
|
2321
2455
|
};
|
|
2322
2456
|
} catch {
|
|
@@ -2447,6 +2581,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
2447
2581
|
if (lines.length > maxLines) out.push(`\u2026 (${lines.length - maxLines} more lines: pitroom show ${meta.id} --full)`);
|
|
2448
2582
|
}
|
|
2449
2583
|
out.push("");
|
|
2584
|
+
out.push(...auditLines(meta));
|
|
2450
2585
|
if (meta.refs) {
|
|
2451
2586
|
const r = meta.refs;
|
|
2452
2587
|
const bad = r.invalid.slice(0, 8).map((i) => `${i.ref} (${i.reason})`).join(", ");
|
|
@@ -2508,22 +2643,6 @@ function progress(meta) {
|
|
|
2508
2643
|
return `pitroom \u2026 running \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 ${l.steps} steps, ${l.toolCalls} tool calls${last} \xB7 run ${meta.id}`;
|
|
2509
2644
|
}
|
|
2510
2645
|
|
|
2511
|
-
// src/core/target.ts
|
|
2512
|
-
function parseTarget(spec, defaultBackend) {
|
|
2513
|
-
const s = spec.trim();
|
|
2514
|
-
if (!s) throw new UserError("empty worker target");
|
|
2515
|
-
const i = s.indexOf(":");
|
|
2516
|
-
if (i > 0 && isBackendId(s.slice(0, i))) {
|
|
2517
|
-
const model = s.slice(i + 1).trim();
|
|
2518
|
-
return model ? { backend: s.slice(0, i), model } : { backend: s.slice(0, i) };
|
|
2519
|
-
}
|
|
2520
|
-
if (isBackendId(s)) return { backend: s };
|
|
2521
|
-
return { backend: defaultBackend, model: s };
|
|
2522
|
-
}
|
|
2523
|
-
var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
|
|
2524
|
-
var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
|
|
2525
|
-
var sameTarget = (a, b) => a.backend === b.backend && a.model === b.model;
|
|
2526
|
-
|
|
2527
2646
|
// src/core/group.ts
|
|
2528
2647
|
function groupIds(group) {
|
|
2529
2648
|
return listRunIds().filter((id) => {
|
|
@@ -2758,11 +2877,11 @@ function installedSkills() {
|
|
|
2758
2877
|
}
|
|
2759
2878
|
|
|
2760
2879
|
// src/core/plan-status.ts
|
|
2761
|
-
import
|
|
2880
|
+
import crypto3 from "node:crypto";
|
|
2762
2881
|
import fs16 from "node:fs";
|
|
2763
2882
|
import path13 from "node:path";
|
|
2764
2883
|
function notesFile(plan) {
|
|
2765
|
-
const id =
|
|
2884
|
+
const id = crypto3.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
2766
2885
|
return path13.join(home(), "plans", id, "notes.md");
|
|
2767
2886
|
}
|
|
2768
2887
|
function readNotes(plan) {
|
|
@@ -2847,13 +2966,13 @@ ${rulings}`,
|
|
|
2847
2966
|
}
|
|
2848
2967
|
|
|
2849
2968
|
// src/core/review.ts
|
|
2850
|
-
import
|
|
2969
|
+
import crypto5 from "node:crypto";
|
|
2851
2970
|
import fs18 from "node:fs";
|
|
2852
2971
|
import path15 from "node:path";
|
|
2853
2972
|
|
|
2854
2973
|
// src/vcs/git.ts
|
|
2855
2974
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2856
|
-
import
|
|
2975
|
+
import crypto4 from "node:crypto";
|
|
2857
2976
|
import fs17 from "node:fs";
|
|
2858
2977
|
import os9 from "node:os";
|
|
2859
2978
|
import path14 from "node:path";
|
|
@@ -2891,7 +3010,7 @@ function repoRoot(dir) {
|
|
|
2891
3010
|
return r.code === 0 ? path14.resolve(r.stdout.trim()) : void 0;
|
|
2892
3011
|
}
|
|
2893
3012
|
function snapshotTree(root, exclude = []) {
|
|
2894
|
-
const tmp = path14.join(os9.tmpdir(), `pitroom-index-${process.pid}-${
|
|
3013
|
+
const tmp = path14.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
|
|
2895
3014
|
const real = path14.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
|
|
2896
3015
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
2897
3016
|
try {
|
|
@@ -3102,7 +3221,7 @@ function pickReviewer(job) {
|
|
|
3102
3221
|
function writePackage(job) {
|
|
3103
3222
|
const g = gitDir(job.dir);
|
|
3104
3223
|
if (!g) throw new UserError(`not a git repository: ${job.dir}`);
|
|
3105
|
-
const file = path15.join(g, "pitroom", `review-${
|
|
3224
|
+
const file = path15.join(g, "pitroom", `review-${crypto5.randomBytes(4).toString("hex")}.md`);
|
|
3106
3225
|
fs18.mkdirSync(path15.dirname(file), { recursive: true });
|
|
3107
3226
|
fs18.writeFileSync(file, job.package);
|
|
3108
3227
|
return file;
|
|
@@ -3226,7 +3345,7 @@ function formatModels(t) {
|
|
|
3226
3345
|
|
|
3227
3346
|
// src/core/dash.ts
|
|
3228
3347
|
import { spawn } from "node:child_process";
|
|
3229
|
-
import
|
|
3348
|
+
import crypto6 from "node:crypto";
|
|
3230
3349
|
import fs20 from "node:fs";
|
|
3231
3350
|
import http from "node:http";
|
|
3232
3351
|
import path17 from "node:path";
|
|
@@ -3385,10 +3504,11 @@ function toRun(m) {
|
|
|
3385
3504
|
steps: l ? l.steps : m.usage?.steps ?? 0,
|
|
3386
3505
|
tokens: m.usage?.total,
|
|
3387
3506
|
saved: m.savedUsd || void 0,
|
|
3388
|
-
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),
|
|
3389
3509
|
changes: m.changes?.length || void 0,
|
|
3390
3510
|
applied: m.applied || void 0,
|
|
3391
|
-
note: l?.last ? oneLine6(String(l.last), 140) : isActive(m.state) ? "" : m.verdict ? findings(m) : headline(m, 200) || oneLine6(m.error ?? "", 200)
|
|
3511
|
+
note: l?.last ? oneLine6(String(l.last), 140) : isActive(m.state) ? "" : m.verdict ? findings(m) : m.auditOf && m.state === "done" ? m.auditDisputed?.length ? `${m.auditDisputed.length} disputed` : "nothing disputed" : headline(m, 200) || oneLine6(m.error ?? "", 200)
|
|
3392
3512
|
};
|
|
3393
3513
|
}
|
|
3394
3514
|
function dashState(opts = {}) {
|
|
@@ -3448,7 +3568,7 @@ function runDetail(id) {
|
|
|
3448
3568
|
id,
|
|
3449
3569
|
state: m.state,
|
|
3450
3570
|
card: toRun(m),
|
|
3451
|
-
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),
|
|
3452
3572
|
steps,
|
|
3453
3573
|
answer,
|
|
3454
3574
|
changes: m.changes ?? [],
|
|
@@ -3476,6 +3596,7 @@ function runDetail(id) {
|
|
|
3476
3596
|
refs: m.refs ? { valid: m.refs.valid, total: m.refs.total, invalid: m.refs.invalid.map((r) => `${r.ref} (${r.reason})`) } : void 0,
|
|
3477
3597
|
verify: m.verifyResult && m.verify ? { command: m.verify, ok: m.verifyResult.ok, tail: m.verifyResult.tail } : void 0,
|
|
3478
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 ?? [] },
|
|
3479
3600
|
report: isActive(m.state) ? progress(m) : formatReport(m, void 0, 120)
|
|
3480
3601
|
};
|
|
3481
3602
|
}
|
|
@@ -3484,7 +3605,7 @@ var HEADERS = {
|
|
|
3484
3605
|
"x-content-type-options": "nosniff",
|
|
3485
3606
|
"referrer-policy": "no-referrer",
|
|
3486
3607
|
// Own files only, plus the hash of the one inline theme script; styles may be inline (the components position popups with them).
|
|
3487
|
-
"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'`
|
|
3488
3609
|
};
|
|
3489
3610
|
var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
|
|
3490
3611
|
var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
@@ -3910,9 +4031,16 @@ function parseVerdict(text) {
|
|
|
3910
4031
|
minor: count("minor")
|
|
3911
4032
|
};
|
|
3912
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
|
+
}
|
|
3913
4041
|
|
|
3914
4042
|
// src/core/slots.ts
|
|
3915
|
-
import
|
|
4043
|
+
import crypto7 from "node:crypto";
|
|
3916
4044
|
import fs23 from "node:fs";
|
|
3917
4045
|
import path19 from "node:path";
|
|
3918
4046
|
var slotsDir = () => path19.join(home(), "slots");
|
|
@@ -3967,7 +4095,7 @@ function tryAcquireSlot(runId, maxParallel) {
|
|
|
3967
4095
|
return void 0;
|
|
3968
4096
|
}
|
|
3969
4097
|
var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
|
|
3970
|
-
var lockFile = (repoRoot2) => path19.join(locksDir(), `write-${
|
|
4098
|
+
var lockFile = (repoRoot2) => path19.join(locksDir(), `write-${crypto7.createHash("sha1").update(path19.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
3971
4099
|
function acquireWriteLock(repoRoot2, runId) {
|
|
3972
4100
|
const file = lockFile(repoRoot2);
|
|
3973
4101
|
if (tryClaim(file, runId)) return;
|
|
@@ -4225,7 +4353,7 @@ function secretWarning(files, mode) {
|
|
|
4225
4353
|
}
|
|
4226
4354
|
|
|
4227
4355
|
// src/core/run.ts
|
|
4228
|
-
var VERSION2 = true ? "0.
|
|
4356
|
+
var VERSION2 = true ? "0.10.1" : "0.0.0-dev";
|
|
4229
4357
|
function planWork(o) {
|
|
4230
4358
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
4231
4359
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -4315,6 +4443,8 @@ function prepareRun(o) {
|
|
|
4315
4443
|
baseTree: parent?.mode === "isolate" ? parent.baseTree : void 0,
|
|
4316
4444
|
worktree: parent?.mode === "isolate" ? parent.worktree : void 0,
|
|
4317
4445
|
reviewOf: o.review?.of,
|
|
4446
|
+
auditOf: o.audit?.of,
|
|
4447
|
+
auditRate: o.auditRate,
|
|
4318
4448
|
plan: work.plan ?? o.review?.plan ?? parent?.plan,
|
|
4319
4449
|
reviewKind: o.review?.kind,
|
|
4320
4450
|
packageFile: o.review?.packageFile
|
|
@@ -4416,7 +4546,48 @@ async function execute(meta) {
|
|
|
4416
4546
|
} finally {
|
|
4417
4547
|
releaseSlot(slot, meta.id);
|
|
4418
4548
|
}
|
|
4419
|
-
|
|
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
|
+
}
|
|
4420
4591
|
}
|
|
4421
4592
|
function prepareTree(meta) {
|
|
4422
4593
|
const root = meta.repoRoot;
|
|
@@ -4496,14 +4667,20 @@ function finalize(meta, res) {
|
|
|
4496
4667
|
}
|
|
4497
4668
|
}
|
|
4498
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
|
+
}
|
|
4499
4675
|
if (meta.state === "done" && !run.finalText) meta.warnings.push("worker finished without a written answer");
|
|
4500
4676
|
if (meta.state === "done" && meta.verify) meta.verifyResult = runVerify(meta);
|
|
4501
4677
|
meta.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4502
4678
|
meta.returnedTokens = estimateTokens(formatReport(meta, run.finalText));
|
|
4503
|
-
meta.savedUsd = savedUsd(meta.usage, meta.returnedTokens);
|
|
4679
|
+
meta.savedUsd = meta.auditOf ? 0 : savedUsd(meta.usage, meta.returnedTokens);
|
|
4504
4680
|
writeMeta(meta);
|
|
4505
4681
|
if (meta.mode === "write" && meta.repoRoot) releaseWriteLock(meta.repoRoot, meta.id);
|
|
4506
4682
|
record(meta);
|
|
4683
|
+
if (meta.auditOf) settleAudit(meta);
|
|
4507
4684
|
return meta;
|
|
4508
4685
|
}
|
|
4509
4686
|
function captureChanges(meta) {
|
|
@@ -4628,6 +4805,33 @@ async function cmdReview(p) {
|
|
|
4628
4805
|
fs27.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
4629
4806
|
return launch(p, meta);
|
|
4630
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
|
+
}
|
|
4631
4835
|
function cmdPlan(p) {
|
|
4632
4836
|
const [sub, file, ...rest] = p.positional;
|
|
4633
4837
|
if (sub === "status" && file) {
|
|
@@ -5045,6 +5249,11 @@ function doctor5(probe) {
|
|
|
5045
5249
|
add("fail", e.message);
|
|
5046
5250
|
}
|
|
5047
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
|
+
}
|
|
5048
5257
|
const tierTargets = [];
|
|
5049
5258
|
const tierNames = [];
|
|
5050
5259
|
for (const name of Object.keys(effective().tiers.value)) {
|
|
@@ -5268,6 +5477,8 @@ Usage
|
|
|
5268
5477
|
pitroom review [run | --range A..B [--plan PLAN]] [--tier T | -W T] [--bg]
|
|
5269
5478
|
read-only review of a run's change (a follow-up: only its
|
|
5270
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
|
|
5271
5482
|
pitroom plan status PLAN [--json] a plan's progress: runs, STATUS, review, fix rounds, applied
|
|
5272
5483
|
pitroom plan note PLAN "Task N: \u2026" record a completion, deferred finding or ruling (outside the repo)
|
|
5273
5484
|
pitroom status [run | -g NAME] state / live progress (default: latest run)
|
|
@@ -5315,6 +5526,7 @@ Run options
|
|
|
5315
5526
|
-W, --worker T worker target "backend[:model]" (default: config "worker", else opencode)
|
|
5316
5527
|
-m, --model M model for that worker (default: the worker CLI's own default)
|
|
5317
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")
|
|
5318
5530
|
--effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
|
|
5319
5531
|
--plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
|
|
5320
5532
|
--step N the plan task for --plan
|
|
@@ -5343,6 +5555,7 @@ var COMMANDS = {
|
|
|
5343
5555
|
run: cmdRun,
|
|
5344
5556
|
crew: cmdCrew,
|
|
5345
5557
|
review: cmdReview,
|
|
5558
|
+
audit: cmdAudit,
|
|
5346
5559
|
plan: cmdPlan,
|
|
5347
5560
|
status: cmdStatus,
|
|
5348
5561
|
wait: cmdWait,
|