pitroom 0.12.0 → 0.13.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/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +4 -2
- package/dist/pitroom.mjs +450 -287
- package/docs/backends.md +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
package/dist/pitroom.mjs
CHANGED
|
@@ -1416,7 +1416,8 @@ var SCHEMA = {
|
|
|
1416
1416
|
models: "record",
|
|
1417
1417
|
tiers: "record",
|
|
1418
1418
|
costs: "numbers",
|
|
1419
|
-
audit: "number"
|
|
1419
|
+
audit: "number",
|
|
1420
|
+
readIn: "string"
|
|
1420
1421
|
};
|
|
1421
1422
|
function configPath() {
|
|
1422
1423
|
if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -1474,6 +1475,7 @@ var rate = (v) => {
|
|
|
1474
1475
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1475
1476
|
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
|
|
1476
1477
|
};
|
|
1478
|
+
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
1477
1479
|
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
1478
1480
|
function setting(flag2, env, conf, fallback) {
|
|
1479
1481
|
if (flag2 !== void 0) return { value: flag2, source: "flag" };
|
|
@@ -1498,7 +1500,8 @@ function effective(flags = {}) {
|
|
|
1498
1500
|
models: setting(void 0, void 0, c.models, {}),
|
|
1499
1501
|
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
1500
1502
|
costs: setting(void 0, void 0, c.costs, {}),
|
|
1501
|
-
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0)
|
|
1503
|
+
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
1504
|
+
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto")
|
|
1502
1505
|
};
|
|
1503
1506
|
}
|
|
1504
1507
|
|
|
@@ -1545,6 +1548,7 @@ var BOOL_FLAGS = {
|
|
|
1545
1548
|
"-i": "isolate",
|
|
1546
1549
|
"--isolate": "isolate",
|
|
1547
1550
|
"--bg": "bg",
|
|
1551
|
+
"--in-place": "in-place",
|
|
1548
1552
|
"--audit": "audit",
|
|
1549
1553
|
"--no-audit": "no-audit",
|
|
1550
1554
|
"--web": "web",
|
|
@@ -1639,6 +1643,7 @@ function runOptions(p, task) {
|
|
|
1639
1643
|
return {
|
|
1640
1644
|
mode: modes[0] ?? "read",
|
|
1641
1645
|
auditRate: has(p, "no-audit") ? 0 : has(p, "audit") ? 1 : void 0,
|
|
1646
|
+
inPlace: has(p, "in-place"),
|
|
1642
1647
|
task,
|
|
1643
1648
|
dir: flag(p, "dir") ?? process.cwd(),
|
|
1644
1649
|
files: p.flags.get("file") ?? [],
|
|
@@ -1672,8 +1677,8 @@ function effortFlag(p) {
|
|
|
1672
1677
|
|
|
1673
1678
|
// src/cli/commands.ts
|
|
1674
1679
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
1675
|
-
import
|
|
1676
|
-
import
|
|
1680
|
+
import fs29 from "node:fs";
|
|
1681
|
+
import path25 from "node:path";
|
|
1677
1682
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1678
1683
|
|
|
1679
1684
|
// src/core/init.ts
|
|
@@ -2968,84 +2973,17 @@ ${rulings}`,
|
|
|
2968
2973
|
].join("\n");
|
|
2969
2974
|
}
|
|
2970
2975
|
|
|
2971
|
-
// src/core/
|
|
2972
|
-
import fs17 from "node:fs";
|
|
2973
|
-
import path14 from "node:path";
|
|
2974
|
-
var MAX_MS = 24 * 36e5;
|
|
2975
|
-
var OVERLOAD_MS = 20 * 6e4;
|
|
2976
|
-
var DAILY_MS = 4 * 36e5;
|
|
2977
|
-
var file = () => path14.join(home(), "cooldowns.json");
|
|
2978
|
-
function cooldownKey(target, backend) {
|
|
2979
|
-
const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
|
|
2980
|
-
return `${target.backend}:${model}`;
|
|
2981
|
-
}
|
|
2982
|
-
function retryAfterMs(message) {
|
|
2983
|
-
const compact3 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
|
|
2984
|
-
const words = /(?:retry|try again|resets?)[^0-9]{0,24}(\d+(?:\.\d+)?)\s*(second|minute|hour)s?/i.exec(message);
|
|
2985
|
-
let ms = 0;
|
|
2986
|
-
if (compact3) {
|
|
2987
|
-
for (const [, n, unit] of compact3.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) ms += Number(n) * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit.toLowerCase()];
|
|
2988
|
-
} else if (words) {
|
|
2989
|
-
ms = Number(words[1]) * { second: 1e3, minute: 6e4, hour: 36e5 }[words[2].toLowerCase()];
|
|
2990
|
-
}
|
|
2991
|
-
return ms > 0 ? ms : void 0;
|
|
2992
|
-
}
|
|
2993
|
-
function cooldownMs(message) {
|
|
2994
|
-
const told = retryAfterMs(message);
|
|
2995
|
-
if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
|
|
2996
|
-
return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
|
|
2997
|
-
}
|
|
2998
|
-
function read() {
|
|
2999
|
-
try {
|
|
3000
|
-
const raw = JSON.parse(fs17.readFileSync(file(), "utf8"));
|
|
3001
|
-
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
3002
|
-
} catch {
|
|
3003
|
-
return {};
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
function write(all) {
|
|
3007
|
-
fs17.mkdirSync(path14.dirname(file()), { recursive: true });
|
|
3008
|
-
const tmp = `${file()}.${process.pid}.tmp`;
|
|
3009
|
-
fs17.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
|
|
3010
|
-
`);
|
|
3011
|
-
fs17.renameSync(tmp, file());
|
|
3012
|
-
}
|
|
3013
|
-
function activeCooldowns(now = Date.now()) {
|
|
3014
|
-
return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
|
|
3015
|
-
}
|
|
3016
|
-
var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
|
|
3017
|
-
function recordCooldown(key, targetName, message, now = Date.now()) {
|
|
3018
|
-
try {
|
|
3019
|
-
const all = activeCooldowns(now);
|
|
3020
|
-
all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
|
|
3021
|
-
write(all);
|
|
3022
|
-
} catch {
|
|
3023
|
-
}
|
|
3024
|
-
}
|
|
3025
|
-
function clearCooldowns(key) {
|
|
3026
|
-
const all = read();
|
|
3027
|
-
const keys = key ? Object.keys(all).filter((k) => k === key) : Object.keys(all);
|
|
3028
|
-
for (const k of keys) delete all[k];
|
|
3029
|
-
if (keys.length) write(all);
|
|
3030
|
-
return keys.length;
|
|
3031
|
-
}
|
|
3032
|
-
function untilText(c, now = Date.now()) {
|
|
3033
|
-
const d = new Date(c.until);
|
|
3034
|
-
const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
3035
|
-
return d.toDateString() === new Date(now).toDateString() ? `until ${time}` : `until ${d.toLocaleDateString()} ${time}`;
|
|
3036
|
-
}
|
|
3037
|
-
|
|
3038
|
-
// src/core/review.ts
|
|
3039
|
-
import crypto5 from "node:crypto";
|
|
2976
|
+
// src/core/snapshot.ts
|
|
3040
2977
|
import fs19 from "node:fs";
|
|
3041
2978
|
import path16 from "node:path";
|
|
2979
|
+
import crypto5 from "node:crypto";
|
|
3042
2980
|
|
|
3043
2981
|
// src/vcs/git.ts
|
|
3044
2982
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
3045
2983
|
import crypto4 from "node:crypto";
|
|
3046
|
-
import
|
|
2984
|
+
import fs17 from "node:fs";
|
|
3047
2985
|
import os9 from "node:os";
|
|
3048
|
-
import
|
|
2986
|
+
import path14 from "node:path";
|
|
3049
2987
|
var SAFE = [
|
|
3050
2988
|
"-c",
|
|
3051
2989
|
"color.ui=false",
|
|
@@ -3077,20 +3015,24 @@ function gitAvailable() {
|
|
|
3077
3015
|
}
|
|
3078
3016
|
function repoRoot(dir) {
|
|
3079
3017
|
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
3080
|
-
return r.code === 0 ?
|
|
3018
|
+
return r.code === 0 ? path14.resolve(r.stdout.trim()) : void 0;
|
|
3081
3019
|
}
|
|
3082
|
-
function snapshotTree(root, exclude = []) {
|
|
3083
|
-
const tmp =
|
|
3084
|
-
const real =
|
|
3020
|
+
function snapshotTree(root, exclude = [], drop = []) {
|
|
3021
|
+
const tmp = path14.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
|
|
3022
|
+
const real = path14.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
|
|
3085
3023
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
3086
3024
|
try {
|
|
3087
|
-
if (
|
|
3025
|
+
if (fs17.existsSync(real)) fs17.copyFileSync(real, tmp);
|
|
3088
3026
|
else must(root, ["read-tree", "--empty"], env);
|
|
3089
|
-
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
|
|
3027
|
+
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop.map((p) => `:(top,literal,exclude)${p}`)], env);
|
|
3028
|
+
if (drop.length) {
|
|
3029
|
+
const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop.join("\0")}\0`, encoding: "utf8" });
|
|
3030
|
+
if (r.status !== 0) throw new Error(`git update-index failed: ${r.stderr.trim()}`);
|
|
3031
|
+
}
|
|
3090
3032
|
return must(root, ["write-tree"], env).trim();
|
|
3091
3033
|
} finally {
|
|
3092
|
-
|
|
3093
|
-
|
|
3034
|
+
fs17.rmSync(tmp, { force: true });
|
|
3035
|
+
fs17.rmSync(`${tmp}.lock`, { force: true });
|
|
3094
3036
|
}
|
|
3095
3037
|
}
|
|
3096
3038
|
var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
|
|
@@ -3110,27 +3052,27 @@ function diffTrees(root, a, b) {
|
|
|
3110
3052
|
return { changes, stats: { files: changes.length, insertions, deletions }, patch };
|
|
3111
3053
|
}
|
|
3112
3054
|
function createIsolatedCopy(root, tree, dest) {
|
|
3113
|
-
const objects =
|
|
3114
|
-
|
|
3115
|
-
must(
|
|
3116
|
-
|
|
3055
|
+
const objects = path14.join(path14.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
|
|
3056
|
+
fs17.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
3057
|
+
must(path14.dirname(dest), ["init", "--quiet", dest]);
|
|
3058
|
+
fs17.writeFileSync(path14.join(dest, ".git", "objects", "info", "alternates"), `${objects}
|
|
3117
3059
|
`);
|
|
3118
3060
|
must(dest, ["read-tree", tree]);
|
|
3119
3061
|
must(dest, ["checkout-index", "--all", "--force"]);
|
|
3120
3062
|
}
|
|
3121
3063
|
function removeIsolatedCopy(dest, ownedBy) {
|
|
3122
|
-
const rel =
|
|
3123
|
-
if (!rel || rel.startsWith("..") ||
|
|
3124
|
-
|
|
3064
|
+
const rel = path14.relative(ownedBy, dest);
|
|
3065
|
+
if (!rel || rel.startsWith("..") || path14.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
|
|
3066
|
+
fs17.rmSync(dest, { recursive: true, force: true });
|
|
3125
3067
|
}
|
|
3126
3068
|
function linkIntoWorktree(root, worktree, rels) {
|
|
3127
3069
|
const linked = [];
|
|
3128
3070
|
for (const rel of rels) {
|
|
3129
|
-
const src =
|
|
3130
|
-
const dst =
|
|
3131
|
-
if (!
|
|
3132
|
-
|
|
3133
|
-
|
|
3071
|
+
const src = path14.join(root, rel);
|
|
3072
|
+
const dst = path14.join(worktree, rel);
|
|
3073
|
+
if (!fs17.existsSync(src) || fs17.existsSync(dst)) continue;
|
|
3074
|
+
fs17.mkdirSync(path14.dirname(dst), { recursive: true });
|
|
3075
|
+
fs17.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
|
|
3134
3076
|
linked.push(rel);
|
|
3135
3077
|
}
|
|
3136
3078
|
return linked;
|
|
@@ -3171,8 +3113,255 @@ function gitDir(dir) {
|
|
|
3171
3113
|
return r.code === 0 ? r.stdout.trim() : void 0;
|
|
3172
3114
|
}
|
|
3173
3115
|
|
|
3116
|
+
// src/core/secrets.ts
|
|
3117
|
+
import { execFileSync } from "node:child_process";
|
|
3118
|
+
import fs18 from "node:fs";
|
|
3119
|
+
import path15 from "node:path";
|
|
3120
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
3121
|
+
var MAX_VISITED = 2e4;
|
|
3122
|
+
var MAX_DEPTH = 4;
|
|
3123
|
+
var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
|
|
3124
|
+
function looksSecret(name) {
|
|
3125
|
+
if (TEMPLATE.test(name)) return false;
|
|
3126
|
+
if (name === ".env" || name.startsWith(".env.") || /\.env$/i.test(name)) return true;
|
|
3127
|
+
return /\.(pem|p12|pfx|key|jks|keystore)$/i.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/i.test(name);
|
|
3128
|
+
}
|
|
3129
|
+
function findSecretFiles(dir) {
|
|
3130
|
+
const found = [];
|
|
3131
|
+
let visited = 0;
|
|
3132
|
+
const walk = (d, depth) => {
|
|
3133
|
+
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
3134
|
+
let entries;
|
|
3135
|
+
try {
|
|
3136
|
+
entries = fs18.readdirSync(d, { withFileTypes: true });
|
|
3137
|
+
} catch {
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
for (const e of entries) {
|
|
3141
|
+
visited++;
|
|
3142
|
+
if (e.isDirectory()) {
|
|
3143
|
+
if (!SKIP_DIRS.has(e.name)) walk(path15.join(d, e.name), depth + 1);
|
|
3144
|
+
} else if (e.isFile() && looksSecret(e.name)) {
|
|
3145
|
+
found.push(path15.relative(dir, path15.join(d, e.name)));
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
};
|
|
3149
|
+
walk(dir, 0);
|
|
3150
|
+
return found.sort();
|
|
3151
|
+
}
|
|
3152
|
+
function findSecretFilesInTree(root, dir) {
|
|
3153
|
+
try {
|
|
3154
|
+
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
3155
|
+
return out.split("\0").filter((f) => f && looksSecret(path15.basename(f)) && !path15.relative(dir, path15.join(root, f)).startsWith("..")).map((f) => path15.relative(dir, path15.join(root, f))).sort();
|
|
3156
|
+
} catch {
|
|
3157
|
+
return [];
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
function secretWarning(files, mode) {
|
|
3161
|
+
if (!files.length) return void 0;
|
|
3162
|
+
const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
|
|
3163
|
+
const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
|
|
3164
|
+
const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
|
|
3165
|
+
return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
// src/core/snapshot.ts
|
|
3169
|
+
var READ_IN = ["auto", "snapshot", "project"];
|
|
3170
|
+
var KEEP_MS = 24 * 36e5;
|
|
3171
|
+
var PRUNE_AGE_MS = 3 * 24 * 36e5;
|
|
3172
|
+
var KEEP_NEWEST = 6;
|
|
3173
|
+
var JUST_USED_MS = 10 * 6e4;
|
|
3174
|
+
var snapshotsDir = () => path16.join(home(), "snapshots");
|
|
3175
|
+
function wantSnapshot(readIn2, root, dir) {
|
|
3176
|
+
if (!root || readIn2 === "project") return void 0;
|
|
3177
|
+
const secrets = findSecretFiles(dir);
|
|
3178
|
+
return readIn2 === "snapshot" || secrets.length ? { secrets } : void 0;
|
|
3179
|
+
}
|
|
3180
|
+
function readSnapshot(root) {
|
|
3181
|
+
const drop = findSecretFilesInTree(root, root);
|
|
3182
|
+
const tree = snapshotTree(root, [], drop);
|
|
3183
|
+
const dest = path16.join(snapshotsDir(), tree);
|
|
3184
|
+
if (fs19.existsSync(path16.join(dest, ".git"))) {
|
|
3185
|
+
touch(dest);
|
|
3186
|
+
return { dir: dest, tree, created: false };
|
|
3187
|
+
}
|
|
3188
|
+
const tmp = path16.join(snapshotsDir(), `.tmp-${process.pid}-${crypto5.randomBytes(3).toString("hex")}`);
|
|
3189
|
+
try {
|
|
3190
|
+
createIsolatedCopy(root, tree, tmp);
|
|
3191
|
+
readOnly(tmp);
|
|
3192
|
+
try {
|
|
3193
|
+
fs19.renameSync(tmp, dest);
|
|
3194
|
+
} catch (e) {
|
|
3195
|
+
if (!fs19.existsSync(path16.join(dest, ".git"))) throw e;
|
|
3196
|
+
removeIsolatedCopy(tmp, snapshotsDir());
|
|
3197
|
+
}
|
|
3198
|
+
} catch (e) {
|
|
3199
|
+
try {
|
|
3200
|
+
removeIsolatedCopy(tmp, snapshotsDir());
|
|
3201
|
+
} catch {
|
|
3202
|
+
}
|
|
3203
|
+
throw e;
|
|
3204
|
+
}
|
|
3205
|
+
prune();
|
|
3206
|
+
return { dir: dest, tree, created: true };
|
|
3207
|
+
}
|
|
3208
|
+
function touch(dir) {
|
|
3209
|
+
try {
|
|
3210
|
+
const now = /* @__PURE__ */ new Date();
|
|
3211
|
+
fs19.utimesSync(dir, now, now);
|
|
3212
|
+
} catch {
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
function readOnly(dir) {
|
|
3216
|
+
const walk = (d) => {
|
|
3217
|
+
for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
|
|
3218
|
+
if (e.name === ".git") continue;
|
|
3219
|
+
const p = path16.join(d, e.name);
|
|
3220
|
+
if (e.isDirectory()) walk(p);
|
|
3221
|
+
else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode & ~146);
|
|
3222
|
+
}
|
|
3223
|
+
};
|
|
3224
|
+
try {
|
|
3225
|
+
walk(dir);
|
|
3226
|
+
} catch {
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
|
|
3230
|
+
let names;
|
|
3231
|
+
try {
|
|
3232
|
+
names = fs19.readdirSync(snapshotsDir());
|
|
3233
|
+
} catch {
|
|
3234
|
+
return 0;
|
|
3235
|
+
}
|
|
3236
|
+
const inUse = /* @__PURE__ */ new Set();
|
|
3237
|
+
for (const id of listRunIds()) {
|
|
3238
|
+
try {
|
|
3239
|
+
const m = readMeta(id);
|
|
3240
|
+
if (m.snapshot?.dir && isActive(m.state)) inUse.add(path16.resolve(m.snapshot.dir));
|
|
3241
|
+
} catch {
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
const entries = names.map((n) => {
|
|
3245
|
+
const dir = path16.join(snapshotsDir(), n);
|
|
3246
|
+
try {
|
|
3247
|
+
return { n, dir, age: now - fs19.statSync(dir).mtimeMs };
|
|
3248
|
+
} catch {
|
|
3249
|
+
return void 0;
|
|
3250
|
+
}
|
|
3251
|
+
}).filter((e) => !!e).sort((x, y) => x.age - y.age);
|
|
3252
|
+
let removed = 0;
|
|
3253
|
+
let rank = 0;
|
|
3254
|
+
for (const e of entries) {
|
|
3255
|
+
if (inUse.has(path16.resolve(e.dir))) continue;
|
|
3256
|
+
const half = e.n.startsWith(".tmp-");
|
|
3257
|
+
const old = e.age >= (half ? KEEP_MS : maxAgeMs);
|
|
3258
|
+
const surplus = !half && ++rank > keepNewest && e.age >= JUST_USED_MS;
|
|
3259
|
+
if (!old && !surplus) continue;
|
|
3260
|
+
try {
|
|
3261
|
+
makeWritable(e.dir);
|
|
3262
|
+
removeIsolatedCopy(e.dir, snapshotsDir());
|
|
3263
|
+
removed++;
|
|
3264
|
+
} catch {
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
return removed;
|
|
3268
|
+
}
|
|
3269
|
+
function makeWritable(dir) {
|
|
3270
|
+
const walk = (d) => {
|
|
3271
|
+
for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
|
|
3272
|
+
const p = path16.join(d, e.name);
|
|
3273
|
+
if (e.isDirectory()) walk(p);
|
|
3274
|
+
else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode | 128);
|
|
3275
|
+
}
|
|
3276
|
+
};
|
|
3277
|
+
try {
|
|
3278
|
+
walk(dir);
|
|
3279
|
+
} catch {
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
function backToProject(text, snapshotDir, root) {
|
|
3283
|
+
let real = snapshotDir;
|
|
3284
|
+
try {
|
|
3285
|
+
real = fs19.realpathSync(snapshotDir);
|
|
3286
|
+
} catch {
|
|
3287
|
+
}
|
|
3288
|
+
let out = text;
|
|
3289
|
+
for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${path16.sep}`).join(`${root}${path16.sep}`).split(dir).join(root);
|
|
3290
|
+
return out;
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// src/core/cooldown.ts
|
|
3294
|
+
import fs20 from "node:fs";
|
|
3295
|
+
import path17 from "node:path";
|
|
3296
|
+
var MAX_MS = 24 * 36e5;
|
|
3297
|
+
var OVERLOAD_MS = 20 * 6e4;
|
|
3298
|
+
var DAILY_MS = 4 * 36e5;
|
|
3299
|
+
var file = () => path17.join(home(), "cooldowns.json");
|
|
3300
|
+
function cooldownKey(target, backend) {
|
|
3301
|
+
const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
|
|
3302
|
+
return `${target.backend}:${model}`;
|
|
3303
|
+
}
|
|
3304
|
+
function retryAfterMs(message) {
|
|
3305
|
+
const compact3 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
|
|
3306
|
+
const words = /(?:retry|try again|resets?)[^0-9]{0,24}(\d+(?:\.\d+)?)\s*(second|minute|hour)s?/i.exec(message);
|
|
3307
|
+
let ms = 0;
|
|
3308
|
+
if (compact3) {
|
|
3309
|
+
for (const [, n, unit] of compact3.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) ms += Number(n) * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit.toLowerCase()];
|
|
3310
|
+
} else if (words) {
|
|
3311
|
+
ms = Number(words[1]) * { second: 1e3, minute: 6e4, hour: 36e5 }[words[2].toLowerCase()];
|
|
3312
|
+
}
|
|
3313
|
+
return ms > 0 ? ms : void 0;
|
|
3314
|
+
}
|
|
3315
|
+
function cooldownMs(message) {
|
|
3316
|
+
const told = retryAfterMs(message);
|
|
3317
|
+
if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
|
|
3318
|
+
return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
|
|
3319
|
+
}
|
|
3320
|
+
function read() {
|
|
3321
|
+
try {
|
|
3322
|
+
const raw = JSON.parse(fs20.readFileSync(file(), "utf8"));
|
|
3323
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
3324
|
+
} catch {
|
|
3325
|
+
return {};
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
function write(all) {
|
|
3329
|
+
fs20.mkdirSync(path17.dirname(file()), { recursive: true });
|
|
3330
|
+
const tmp = `${file()}.${process.pid}.tmp`;
|
|
3331
|
+
fs20.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
|
|
3332
|
+
`);
|
|
3333
|
+
fs20.renameSync(tmp, file());
|
|
3334
|
+
}
|
|
3335
|
+
function activeCooldowns(now = Date.now()) {
|
|
3336
|
+
return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
|
|
3337
|
+
}
|
|
3338
|
+
var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
|
|
3339
|
+
function recordCooldown(key, targetName, message, now = Date.now()) {
|
|
3340
|
+
try {
|
|
3341
|
+
const all = activeCooldowns(now);
|
|
3342
|
+
all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
|
|
3343
|
+
write(all);
|
|
3344
|
+
} catch {
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
function clearCooldowns(key) {
|
|
3348
|
+
const all = read();
|
|
3349
|
+
const keys = key ? Object.keys(all).filter((k) => k === key) : Object.keys(all);
|
|
3350
|
+
for (const k of keys) delete all[k];
|
|
3351
|
+
if (keys.length) write(all);
|
|
3352
|
+
return keys.length;
|
|
3353
|
+
}
|
|
3354
|
+
function untilText(c, now = Date.now()) {
|
|
3355
|
+
const d = new Date(c.until);
|
|
3356
|
+
const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
3357
|
+
return d.toDateString() === new Date(now).toDateString() ? `until ${time}` : `until ${d.toLocaleDateString()} ${time}`;
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3174
3360
|
// src/core/review.ts
|
|
3175
|
-
|
|
3361
|
+
import crypto6 from "node:crypto";
|
|
3362
|
+
import fs21 from "node:fs";
|
|
3363
|
+
import path18 from "node:path";
|
|
3364
|
+
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
3176
3365
|
var section = (title, body) => `## ${title}
|
|
3177
3366
|
|
|
3178
3367
|
${body.trim() || "(empty)"}
|
|
@@ -3189,7 +3378,7 @@ function ancestors(m) {
|
|
|
3189
3378
|
function briefOf(m) {
|
|
3190
3379
|
const root = [m, ...ancestors(m)].at(-1);
|
|
3191
3380
|
const f = runFile(root.id, "brief.md");
|
|
3192
|
-
return
|
|
3381
|
+
return fs21.existsSync(f) ? fs21.readFileSync(f, "utf8") : root.task;
|
|
3193
3382
|
}
|
|
3194
3383
|
function latestReview(ids) {
|
|
3195
3384
|
for (const id of listRunIds().reverse()) {
|
|
@@ -3203,7 +3392,7 @@ function latestReview(ids) {
|
|
|
3203
3392
|
}
|
|
3204
3393
|
function diffOf(m, from = m.baseTree) {
|
|
3205
3394
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
3206
|
-
if (where &&
|
|
3395
|
+
if (where && fs21.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
3207
3396
|
if (from !== m.baseTree) {
|
|
3208
3397
|
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);
|
|
3209
3398
|
}
|
|
@@ -3217,7 +3406,7 @@ function runReview(id) {
|
|
|
3217
3406
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
3218
3407
|
const chain = ancestors(m);
|
|
3219
3408
|
const previous = latestReview(chain.map((a) => a.id));
|
|
3220
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
3409
|
+
const dir = m.mode === "isolate" && m.worktree && fs21.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
3221
3410
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
3222
3411
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
3223
3412
|
if (!previous) {
|
|
@@ -3291,15 +3480,15 @@ function pickReviewer(job) {
|
|
|
3291
3480
|
function writePackage(job) {
|
|
3292
3481
|
const g = gitDir(job.dir);
|
|
3293
3482
|
if (!g) throw new UserError(`not a git repository: ${job.dir}`);
|
|
3294
|
-
const file2 =
|
|
3295
|
-
|
|
3296
|
-
|
|
3483
|
+
const file2 = path18.join(g, "pitroom", `review-${crypto6.randomBytes(4).toString("hex")}.md`);
|
|
3484
|
+
fs21.mkdirSync(path18.dirname(file2), { recursive: true });
|
|
3485
|
+
fs21.writeFileSync(file2, job.package);
|
|
3297
3486
|
return file2;
|
|
3298
3487
|
}
|
|
3299
3488
|
|
|
3300
3489
|
// src/core/templates.ts
|
|
3301
|
-
import
|
|
3302
|
-
import
|
|
3490
|
+
import fs22 from "node:fs";
|
|
3491
|
+
import path19 from "node:path";
|
|
3303
3492
|
var FILES = {
|
|
3304
3493
|
implementer: "pitroom-driven-development/implementer-prompt.md",
|
|
3305
3494
|
"task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
|
|
@@ -3307,9 +3496,9 @@ var FILES = {
|
|
|
3307
3496
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
3308
3497
|
};
|
|
3309
3498
|
function loadTemplate(name, root = packageRoot()) {
|
|
3310
|
-
const file2 =
|
|
3311
|
-
if (!
|
|
3312
|
-
return
|
|
3499
|
+
const file2 = path19.join(root, "skills", FILES[name]);
|
|
3500
|
+
if (!fs22.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
3501
|
+
return fs22.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
3313
3502
|
}
|
|
3314
3503
|
function fill(template, values) {
|
|
3315
3504
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -3415,16 +3604,16 @@ function formatModels(t) {
|
|
|
3415
3604
|
|
|
3416
3605
|
// src/core/dash.ts
|
|
3417
3606
|
import { spawn } from "node:child_process";
|
|
3418
|
-
import
|
|
3419
|
-
import
|
|
3607
|
+
import crypto7 from "node:crypto";
|
|
3608
|
+
import fs23 from "node:fs";
|
|
3420
3609
|
import http from "node:http";
|
|
3421
|
-
import
|
|
3610
|
+
import path20 from "node:path";
|
|
3422
3611
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3423
3612
|
|
|
3424
3613
|
// src/core/file-diff.ts
|
|
3425
3614
|
var PREVIEW_LINES = 300;
|
|
3426
3615
|
function gitPath(value, prefix = true) {
|
|
3427
|
-
let
|
|
3616
|
+
let path27 = value;
|
|
3428
3617
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
3429
3618
|
const bytes = [];
|
|
3430
3619
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -3432,9 +3621,9 @@ function gitPath(value, prefix = true) {
|
|
|
3432
3621
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
3433
3622
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
3434
3623
|
}
|
|
3435
|
-
|
|
3624
|
+
path27 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
3436
3625
|
}
|
|
3437
|
-
return prefix ?
|
|
3626
|
+
return prefix ? path27.replace(/^[ab]\//, "") : path27;
|
|
3438
3627
|
}
|
|
3439
3628
|
function headerPath(header) {
|
|
3440
3629
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -3675,32 +3864,32 @@ var HEADERS = {
|
|
|
3675
3864
|
"x-content-type-options": "nosniff",
|
|
3676
3865
|
"referrer-policy": "no-referrer",
|
|
3677
3866
|
// Own files only, plus the hash of the one inline theme script; styles may be inline (the components position popups with them).
|
|
3678
|
-
"content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${
|
|
3867
|
+
"content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto7.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
|
|
3679
3868
|
};
|
|
3680
3869
|
var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
|
|
3681
3870
|
var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
3682
3871
|
var assetCache = /* @__PURE__ */ new Map();
|
|
3683
3872
|
function readAsset(route) {
|
|
3684
3873
|
try {
|
|
3685
|
-
const file2 =
|
|
3686
|
-
const { mtimeMs } =
|
|
3874
|
+
const file2 = path20.join(assetsDir(), path20.basename(route));
|
|
3875
|
+
const { mtimeMs } = fs23.statSync(file2);
|
|
3687
3876
|
const hit = assetCache.get(route);
|
|
3688
3877
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
3689
|
-
const data =
|
|
3878
|
+
const data = fs23.readFileSync(file2);
|
|
3690
3879
|
assetCache.set(route, { mtimeMs, data });
|
|
3691
3880
|
return data;
|
|
3692
3881
|
} catch {
|
|
3693
3882
|
return void 0;
|
|
3694
3883
|
}
|
|
3695
3884
|
}
|
|
3696
|
-
function handler(
|
|
3885
|
+
function handler(touch2) {
|
|
3697
3886
|
const send = (res, code, type, body) => {
|
|
3698
3887
|
res.writeHead(code, { ...HEADERS, "content-type": type });
|
|
3699
3888
|
res.end(body);
|
|
3700
3889
|
};
|
|
3701
3890
|
const json = (res, code, value) => send(res, code, "application/json; charset=utf-8", JSON.stringify(value));
|
|
3702
3891
|
return (req, res) => {
|
|
3703
|
-
|
|
3892
|
+
touch2();
|
|
3704
3893
|
if (!LOCAL_HOST.test(req.headers.host ?? "")) return json(res, 403, { error: "forbidden host" });
|
|
3705
3894
|
if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
|
|
3706
3895
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -3766,7 +3955,7 @@ function startDash(opts) {
|
|
|
3766
3955
|
});
|
|
3767
3956
|
});
|
|
3768
3957
|
}
|
|
3769
|
-
var registryFile = () =>
|
|
3958
|
+
var registryFile = () => path20.join(home(), "dash.json");
|
|
3770
3959
|
async function ping(port) {
|
|
3771
3960
|
try {
|
|
3772
3961
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -3777,11 +3966,11 @@ async function ping(port) {
|
|
|
3777
3966
|
}
|
|
3778
3967
|
async function runningDash() {
|
|
3779
3968
|
try {
|
|
3780
|
-
const r = JSON.parse(
|
|
3969
|
+
const r = JSON.parse(fs23.readFileSync(registryFile(), "utf8"));
|
|
3781
3970
|
if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
|
|
3782
3971
|
} catch {
|
|
3783
3972
|
}
|
|
3784
|
-
|
|
3973
|
+
fs23.rmSync(registryFile(), { force: true });
|
|
3785
3974
|
return void 0;
|
|
3786
3975
|
}
|
|
3787
3976
|
function openBrowser(url) {
|
|
@@ -3796,7 +3985,7 @@ async function dashCommand(o) {
|
|
|
3796
3985
|
if (o.stop) {
|
|
3797
3986
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
3798
3987
|
process.kill(existing.pid, "SIGTERM");
|
|
3799
|
-
|
|
3988
|
+
fs23.rmSync(registryFile(), { force: true });
|
|
3800
3989
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
3801
3990
|
return 0;
|
|
3802
3991
|
}
|
|
@@ -3833,8 +4022,8 @@ async function dashCommand(o) {
|
|
|
3833
4022
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
3834
4023
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
3835
4024
|
}
|
|
3836
|
-
|
|
3837
|
-
|
|
4025
|
+
fs23.mkdirSync(home(), { recursive: true });
|
|
4026
|
+
fs23.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
3838
4027
|
const stop = () => void dash.close();
|
|
3839
4028
|
process.once("SIGINT", stop);
|
|
3840
4029
|
process.once("SIGTERM", stop);
|
|
@@ -3843,7 +4032,7 @@ async function dashCommand(o) {
|
|
|
3843
4032
|
if (o.open) openBrowser(dash.url);
|
|
3844
4033
|
await dash.closed;
|
|
3845
4034
|
try {
|
|
3846
|
-
if (JSON.parse(
|
|
4035
|
+
if (JSON.parse(fs23.readFileSync(registryFile(), "utf8")).pid === process.pid) fs23.rmSync(registryFile(), { force: true });
|
|
3847
4036
|
} catch {
|
|
3848
4037
|
}
|
|
3849
4038
|
return 0;
|
|
@@ -3851,8 +4040,8 @@ async function dashCommand(o) {
|
|
|
3851
4040
|
|
|
3852
4041
|
// src/core/run.ts
|
|
3853
4042
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
3854
|
-
import
|
|
3855
|
-
import
|
|
4043
|
+
import fs28 from "node:fs";
|
|
4044
|
+
import path24 from "node:path";
|
|
3856
4045
|
|
|
3857
4046
|
// src/core/chain.ts
|
|
3858
4047
|
function resolveChain(flags = {}) {
|
|
@@ -3902,11 +4091,11 @@ function resolveChain(flags = {}) {
|
|
|
3902
4091
|
|
|
3903
4092
|
// src/core/process.ts
|
|
3904
4093
|
import { spawn as spawn2 } from "node:child_process";
|
|
3905
|
-
import
|
|
4094
|
+
import fs25 from "node:fs";
|
|
3906
4095
|
|
|
3907
4096
|
// src/vcs/guard.ts
|
|
3908
|
-
import
|
|
3909
|
-
import
|
|
4097
|
+
import fs24 from "node:fs";
|
|
4098
|
+
import path21 from "node:path";
|
|
3910
4099
|
var VERSION = 1;
|
|
3911
4100
|
var SHIM = `#!/bin/sh
|
|
3912
4101
|
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
@@ -3981,15 +4170,15 @@ esac
|
|
|
3981
4170
|
exec "$real" "$@"
|
|
3982
4171
|
`;
|
|
3983
4172
|
function shimDir() {
|
|
3984
|
-
return
|
|
4173
|
+
return path21.join(home(), "shim", `v${VERSION}`);
|
|
3985
4174
|
}
|
|
3986
4175
|
function findRealGit(skip) {
|
|
3987
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
3988
|
-
if (
|
|
3989
|
-
const p =
|
|
4176
|
+
for (const dir of (process.env.PATH ?? "").split(path21.delimiter).filter(Boolean)) {
|
|
4177
|
+
if (path21.resolve(dir) === skip) continue;
|
|
4178
|
+
const p = path21.join(dir, "git");
|
|
3990
4179
|
try {
|
|
3991
|
-
|
|
3992
|
-
if (
|
|
4180
|
+
fs24.accessSync(p, fs24.constants.X_OK);
|
|
4181
|
+
if (fs24.statSync(p).isFile()) return p;
|
|
3993
4182
|
} catch {
|
|
3994
4183
|
}
|
|
3995
4184
|
}
|
|
@@ -4002,13 +4191,13 @@ var REF_HOOK = `#!/bin/sh
|
|
|
4002
4191
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
4003
4192
|
exit 1
|
|
4004
4193
|
`;
|
|
4005
|
-
var hooksDir = () =>
|
|
4194
|
+
var hooksDir = () => path21.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
4006
4195
|
function writeIfChanged(file2, content) {
|
|
4007
|
-
if (
|
|
4008
|
-
|
|
4196
|
+
if (fs24.existsSync(file2) && fs24.readFileSync(file2, "utf8") === content) return;
|
|
4197
|
+
fs24.mkdirSync(path21.dirname(file2), { recursive: true });
|
|
4009
4198
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
4010
|
-
|
|
4011
|
-
|
|
4199
|
+
fs24.writeFileSync(tmp, content, { mode: 493 });
|
|
4200
|
+
fs24.renameSync(tmp, file2);
|
|
4012
4201
|
}
|
|
4013
4202
|
function withGitConfig(env, entries) {
|
|
4014
4203
|
const out = { ...env };
|
|
@@ -4030,7 +4219,7 @@ function guardEnv(env) {
|
|
|
4030
4219
|
PAGER: "cat"
|
|
4031
4220
|
};
|
|
4032
4221
|
if (process.platform === "win32") return { ...env, ...quiet };
|
|
4033
|
-
writeIfChanged(
|
|
4222
|
+
writeIfChanged(path21.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
4034
4223
|
env = withGitConfig(env, [
|
|
4035
4224
|
["core.hooksPath", hooksDir()],
|
|
4036
4225
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -4038,14 +4227,14 @@ function guardEnv(env) {
|
|
|
4038
4227
|
const dir = shimDir();
|
|
4039
4228
|
const real = findRealGit(dir);
|
|
4040
4229
|
if (!real) return { ...env, ...quiet };
|
|
4041
|
-
writeIfChanged(
|
|
4042
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${
|
|
4230
|
+
writeIfChanged(path21.join(dir, "git"), SHIM);
|
|
4231
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path21.delimiter}${env.PATH ?? ""}` };
|
|
4043
4232
|
}
|
|
4044
4233
|
|
|
4045
4234
|
// src/core/process.ts
|
|
4046
4235
|
async function spawnWorker(inv, opts) {
|
|
4047
|
-
const out =
|
|
4048
|
-
const err =
|
|
4236
|
+
const out = fs25.openSync(opts.stdoutFile, "w");
|
|
4237
|
+
const err = fs25.openSync(opts.stderrFile, "w");
|
|
4049
4238
|
const res = { code: null, timedOut: false, stopped: false };
|
|
4050
4239
|
const child = spawn2(inv.command, inv.args, {
|
|
4051
4240
|
cwd: opts.cwd,
|
|
@@ -4077,8 +4266,8 @@ async function spawnWorker(inv, opts) {
|
|
|
4077
4266
|
if (killer) clearTimeout(killer);
|
|
4078
4267
|
process.off("SIGINT", stop);
|
|
4079
4268
|
process.off("SIGTERM", stop);
|
|
4080
|
-
|
|
4081
|
-
|
|
4269
|
+
fs25.closeSync(out);
|
|
4270
|
+
fs25.closeSync(err);
|
|
4082
4271
|
return res;
|
|
4083
4272
|
}
|
|
4084
4273
|
|
|
@@ -4110,11 +4299,11 @@ function parseAudit(text) {
|
|
|
4110
4299
|
}
|
|
4111
4300
|
|
|
4112
4301
|
// src/core/slots.ts
|
|
4113
|
-
import
|
|
4114
|
-
import
|
|
4115
|
-
import
|
|
4116
|
-
var slotsDir = () =>
|
|
4117
|
-
var locksDir = () =>
|
|
4302
|
+
import crypto8 from "node:crypto";
|
|
4303
|
+
import fs26 from "node:fs";
|
|
4304
|
+
import path22 from "node:path";
|
|
4305
|
+
var slotsDir = () => path22.join(home(), "slots");
|
|
4306
|
+
var locksDir = () => path22.join(home(), "locks");
|
|
4118
4307
|
var STARTUP_GRACE_MS = 3e4;
|
|
4119
4308
|
function holderActive(runId, self) {
|
|
4120
4309
|
if (!runId || runId === self) return false;
|
|
@@ -4128,23 +4317,23 @@ function holderActive(runId, self) {
|
|
|
4128
4317
|
}
|
|
4129
4318
|
}
|
|
4130
4319
|
function tryClaim(file2, runId) {
|
|
4131
|
-
|
|
4320
|
+
fs26.mkdirSync(path22.dirname(file2), { recursive: true });
|
|
4132
4321
|
try {
|
|
4133
|
-
|
|
4322
|
+
fs26.writeFileSync(file2, runId, { flag: "wx" });
|
|
4134
4323
|
return true;
|
|
4135
4324
|
} catch (e) {
|
|
4136
4325
|
if (e.code !== "EEXIST") throw e;
|
|
4137
4326
|
}
|
|
4138
4327
|
let owner = "";
|
|
4139
4328
|
try {
|
|
4140
|
-
owner =
|
|
4329
|
+
owner = fs26.readFileSync(file2, "utf8").trim();
|
|
4141
4330
|
} catch {
|
|
4142
4331
|
}
|
|
4143
4332
|
if (owner === runId) return true;
|
|
4144
4333
|
if (holderActive(owner, runId)) return false;
|
|
4145
|
-
|
|
4334
|
+
fs26.rmSync(file2, { force: true });
|
|
4146
4335
|
try {
|
|
4147
|
-
|
|
4336
|
+
fs26.writeFileSync(file2, runId, { flag: "wx" });
|
|
4148
4337
|
return true;
|
|
4149
4338
|
} catch {
|
|
4150
4339
|
return false;
|
|
@@ -4153,25 +4342,25 @@ function tryClaim(file2, runId) {
|
|
|
4153
4342
|
function releaseIfOwner(file2, runId) {
|
|
4154
4343
|
if (!file2) return;
|
|
4155
4344
|
try {
|
|
4156
|
-
if (
|
|
4345
|
+
if (fs26.readFileSync(file2, "utf8").trim() === runId) fs26.rmSync(file2, { force: true });
|
|
4157
4346
|
} catch {
|
|
4158
4347
|
}
|
|
4159
4348
|
}
|
|
4160
4349
|
function tryAcquireSlot(runId, maxParallel) {
|
|
4161
4350
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
4162
|
-
const file2 =
|
|
4351
|
+
const file2 = path22.join(slotsDir(), `slot-${n}`);
|
|
4163
4352
|
if (tryClaim(file2, runId)) return file2;
|
|
4164
4353
|
}
|
|
4165
4354
|
return void 0;
|
|
4166
4355
|
}
|
|
4167
4356
|
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
4168
|
-
var lockFile = (repoRoot2) =>
|
|
4357
|
+
var lockFile = (repoRoot2) => path22.join(locksDir(), `write-${crypto8.createHash("sha1").update(path22.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
4169
4358
|
function acquireWriteLock(repoRoot2, runId) {
|
|
4170
4359
|
const file2 = lockFile(repoRoot2);
|
|
4171
4360
|
if (tryClaim(file2, runId)) return;
|
|
4172
4361
|
let owner = "";
|
|
4173
4362
|
try {
|
|
4174
|
-
owner =
|
|
4363
|
+
owner = fs26.readFileSync(file2, "utf8").trim();
|
|
4175
4364
|
} catch {
|
|
4176
4365
|
}
|
|
4177
4366
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -4218,12 +4407,12 @@ ${task}`;
|
|
|
4218
4407
|
}
|
|
4219
4408
|
|
|
4220
4409
|
// src/core/refs.ts
|
|
4221
|
-
import
|
|
4222
|
-
import
|
|
4410
|
+
import fs27 from "node:fs";
|
|
4411
|
+
import path23 from "node:path";
|
|
4223
4412
|
var FILE_EXTENSIONS = new Set(
|
|
4224
4413
|
"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(" ")
|
|
4225
4414
|
);
|
|
4226
|
-
var
|
|
4415
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
4227
4416
|
var MAX_INDEXED = 6e4;
|
|
4228
4417
|
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
4229
4418
|
var REF = new RegExp(
|
|
@@ -4270,13 +4459,13 @@ function extractRefs(answer) {
|
|
|
4270
4459
|
return [...seen.values()];
|
|
4271
4460
|
}
|
|
4272
4461
|
function verifyRefs(refs, dirs) {
|
|
4273
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
4462
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path23.resolve(d)))];
|
|
4274
4463
|
const invalid = [];
|
|
4275
4464
|
const lineCache = /* @__PURE__ */ new Map();
|
|
4276
4465
|
const load = (file2) => {
|
|
4277
4466
|
if (!lineCache.has(file2)) {
|
|
4278
|
-
const size =
|
|
4279
|
-
const lines = size > MAX_BYTES ? null :
|
|
4467
|
+
const size = fs27.statSync(file2).size;
|
|
4468
|
+
const lines = size > MAX_BYTES ? null : fs27.readFileSync(file2, "utf8").split("\n");
|
|
4280
4469
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
4281
4470
|
lineCache.set(file2, lines);
|
|
4282
4471
|
}
|
|
@@ -4287,9 +4476,9 @@ function verifyRefs(refs, dirs) {
|
|
|
4287
4476
|
const candidates = (ref) => {
|
|
4288
4477
|
const direct = resolve(ref.file, roots);
|
|
4289
4478
|
if (direct) return [direct];
|
|
4290
|
-
if (
|
|
4479
|
+
if (path23.isAbsolute(ref.file)) return [];
|
|
4291
4480
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
4292
|
-
return (byName().get(
|
|
4481
|
+
return (byName().get(path23.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path23.basename(f) === wanted).slice(0, 20);
|
|
4293
4482
|
};
|
|
4294
4483
|
const check = (file2, ref) => {
|
|
4295
4484
|
const lines = load(file2);
|
|
@@ -4304,7 +4493,7 @@ function verifyRefs(refs, dirs) {
|
|
|
4304
4493
|
if (!files.length) {
|
|
4305
4494
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
4306
4495
|
total++;
|
|
4307
|
-
invalid.push({ ref: ref.text, reason:
|
|
4496
|
+
invalid.push({ ref: ref.text, reason: path23.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
4308
4497
|
continue;
|
|
4309
4498
|
}
|
|
4310
4499
|
total++;
|
|
@@ -4325,7 +4514,7 @@ function enclosedBy(lines, ref) {
|
|
|
4325
4514
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
4326
4515
|
}
|
|
4327
4516
|
var isFileName = (name) => {
|
|
4328
|
-
const base2 =
|
|
4517
|
+
const base2 = path23.basename(name);
|
|
4329
4518
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
4330
4519
|
};
|
|
4331
4520
|
function indexFiles(roots) {
|
|
@@ -4334,19 +4523,19 @@ function indexFiles(roots) {
|
|
|
4334
4523
|
const walk = (dir) => {
|
|
4335
4524
|
let entries;
|
|
4336
4525
|
try {
|
|
4337
|
-
entries =
|
|
4526
|
+
entries = fs27.readdirSync(dir, { withFileTypes: true });
|
|
4338
4527
|
} catch {
|
|
4339
4528
|
return;
|
|
4340
4529
|
}
|
|
4341
4530
|
for (const e of entries) {
|
|
4342
4531
|
if (count >= MAX_INDEXED) return;
|
|
4343
4532
|
if (e.isDirectory()) {
|
|
4344
|
-
if (!
|
|
4533
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path23.join(dir, e.name));
|
|
4345
4534
|
} else if (e.isFile()) {
|
|
4346
4535
|
count++;
|
|
4347
4536
|
const list2 = index.get(e.name);
|
|
4348
|
-
if (list2) list2.push(
|
|
4349
|
-
else index.set(e.name, [
|
|
4537
|
+
if (list2) list2.push(path23.join(dir, e.name));
|
|
4538
|
+
else index.set(e.name, [path23.join(dir, e.name)]);
|
|
4350
4539
|
}
|
|
4351
4540
|
}
|
|
4352
4541
|
};
|
|
@@ -4354,10 +4543,10 @@ function indexFiles(roots) {
|
|
|
4354
4543
|
return index;
|
|
4355
4544
|
}
|
|
4356
4545
|
function resolve(ref, roots) {
|
|
4357
|
-
const candidates =
|
|
4546
|
+
const candidates = path23.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path23.join(r, ref));
|
|
4358
4547
|
return candidates.find((c) => {
|
|
4359
4548
|
try {
|
|
4360
|
-
return
|
|
4549
|
+
return fs27.statSync(c).isFile() && inside(c, roots);
|
|
4361
4550
|
} catch {
|
|
4362
4551
|
return false;
|
|
4363
4552
|
}
|
|
@@ -4365,65 +4554,14 @@ function resolve(ref, roots) {
|
|
|
4365
4554
|
}
|
|
4366
4555
|
function inside(file2, roots) {
|
|
4367
4556
|
return roots.some((r) => {
|
|
4368
|
-
const rel =
|
|
4369
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
4557
|
+
const rel = path23.relative(r, path23.resolve(file2));
|
|
4558
|
+
return rel !== "" && !rel.startsWith("..") && !path23.isAbsolute(rel);
|
|
4370
4559
|
});
|
|
4371
4560
|
}
|
|
4372
4561
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4373
4562
|
|
|
4374
|
-
// src/core/secrets.ts
|
|
4375
|
-
import { execFileSync } from "node:child_process";
|
|
4376
|
-
import fs26 from "node:fs";
|
|
4377
|
-
import path22 from "node:path";
|
|
4378
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
4379
|
-
var MAX_VISITED = 2e4;
|
|
4380
|
-
var MAX_DEPTH = 4;
|
|
4381
|
-
var TEMPLATE2 = /\.(example|sample|template|dist|defaults?|tpl)$/i;
|
|
4382
|
-
function looksSecret(name) {
|
|
4383
|
-
if (name === ".env" || name.startsWith(".env.") && !TEMPLATE2.test(name)) return true;
|
|
4384
|
-
return /\.(pem|p12|pfx)$/i.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json";
|
|
4385
|
-
}
|
|
4386
|
-
function findSecretFiles(dir) {
|
|
4387
|
-
const found = [];
|
|
4388
|
-
let visited = 0;
|
|
4389
|
-
const walk = (d, depth) => {
|
|
4390
|
-
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
4391
|
-
let entries;
|
|
4392
|
-
try {
|
|
4393
|
-
entries = fs26.readdirSync(d, { withFileTypes: true });
|
|
4394
|
-
} catch {
|
|
4395
|
-
return;
|
|
4396
|
-
}
|
|
4397
|
-
for (const e of entries) {
|
|
4398
|
-
visited++;
|
|
4399
|
-
if (e.isDirectory()) {
|
|
4400
|
-
if (!SKIP_DIRS2.has(e.name)) walk(path22.join(d, e.name), depth + 1);
|
|
4401
|
-
} else if (e.isFile() && looksSecret(e.name)) {
|
|
4402
|
-
found.push(path22.relative(dir, path22.join(d, e.name)));
|
|
4403
|
-
}
|
|
4404
|
-
}
|
|
4405
|
-
};
|
|
4406
|
-
walk(dir, 0);
|
|
4407
|
-
return found.sort();
|
|
4408
|
-
}
|
|
4409
|
-
function findSecretFilesInTree(root, dir) {
|
|
4410
|
-
try {
|
|
4411
|
-
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
4412
|
-
return out.split("\0").filter((f) => f && looksSecret(path22.basename(f)) && !path22.relative(dir, path22.join(root, f)).startsWith("..")).map((f) => path22.relative(dir, path22.join(root, f))).sort();
|
|
4413
|
-
} catch {
|
|
4414
|
-
return [];
|
|
4415
|
-
}
|
|
4416
|
-
}
|
|
4417
|
-
function secretWarning(files, mode) {
|
|
4418
|
-
if (!files.length) return void 0;
|
|
4419
|
-
const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
|
|
4420
|
-
const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
|
|
4421
|
-
const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
|
|
4422
|
-
return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
|
|
4423
|
-
}
|
|
4424
|
-
|
|
4425
4563
|
// src/core/run.ts
|
|
4426
|
-
var VERSION2 = true ? "0.
|
|
4564
|
+
var VERSION2 = true ? "0.13.0" : "0.0.0-dev";
|
|
4427
4565
|
function planWork(o) {
|
|
4428
4566
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
4429
4567
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -4455,7 +4593,7 @@ function prepareRun(o) {
|
|
|
4455
4593
|
parent = readMeta(o.continueFrom);
|
|
4456
4594
|
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");
|
|
4457
4595
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
4458
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
4596
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs28.existsSync(parent.worktree))) {
|
|
4459
4597
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
4460
4598
|
}
|
|
4461
4599
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -4474,16 +4612,21 @@ function prepareRun(o) {
|
|
|
4474
4612
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
4475
4613
|
}
|
|
4476
4614
|
const mode = parent?.mode ?? o.mode;
|
|
4477
|
-
const dir =
|
|
4478
|
-
if (!
|
|
4615
|
+
const dir = path24.resolve(parent?.dir ?? o.dir);
|
|
4616
|
+
if (!fs28.existsSync(dir) || !fs28.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
4479
4617
|
const root = repoRoot(dir);
|
|
4480
4618
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
4481
4619
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
4482
4620
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
4483
4621
|
}
|
|
4484
|
-
const files = o.files.map((f) =>
|
|
4485
|
-
for (const f of files) if (!
|
|
4486
|
-
|
|
4622
|
+
const files = o.files.map((f) => path24.resolve(f));
|
|
4623
|
+
for (const f of files) if (!fs28.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
4624
|
+
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
4625
|
+
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
4626
|
+
if (parent?.snapshot?.dir && !fs28.existsSync(parent.snapshot.dir)) {
|
|
4627
|
+
throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
|
|
4628
|
+
}
|
|
4629
|
+
if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
4487
4630
|
const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
|
|
4488
4631
|
const note = secretWarning(secrets, mode);
|
|
4489
4632
|
if (note) warnings.push(note);
|
|
@@ -4515,6 +4658,8 @@ function prepareRun(o) {
|
|
|
4515
4658
|
reviewOf: o.review?.of,
|
|
4516
4659
|
auditOf: o.audit?.of,
|
|
4517
4660
|
auditRate: o.auditRate,
|
|
4661
|
+
inPlace: o.inPlace || void 0,
|
|
4662
|
+
snapshot: parent?.snapshot ?? (snap ? { dir: "", tree: "", left: snap.secrets } : void 0),
|
|
4518
4663
|
plan: work.plan ?? o.review?.plan ?? parent?.plan,
|
|
4519
4664
|
reviewKind: o.review?.kind,
|
|
4520
4665
|
packageFile: o.review?.packageFile
|
|
@@ -4524,13 +4669,13 @@ function prepareRun(o) {
|
|
|
4524
4669
|
try {
|
|
4525
4670
|
acquireWriteLock(root, meta.id);
|
|
4526
4671
|
} catch (e) {
|
|
4527
|
-
|
|
4672
|
+
fs28.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
4528
4673
|
throw e;
|
|
4529
4674
|
}
|
|
4530
4675
|
}
|
|
4531
|
-
|
|
4676
|
+
fs28.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
4532
4677
|
`);
|
|
4533
|
-
if (work.brief)
|
|
4678
|
+
if (work.brief) fs28.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
4534
4679
|
return meta;
|
|
4535
4680
|
}
|
|
4536
4681
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -4610,7 +4755,7 @@ async function execute(meta) {
|
|
|
4610
4755
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
4611
4756
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
4612
4757
|
const from = runFile(meta.id, f);
|
|
4613
|
-
if (
|
|
4758
|
+
if (fs28.existsSync(from)) fs28.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
4614
4759
|
}
|
|
4615
4760
|
writeMeta(meta);
|
|
4616
4761
|
}
|
|
@@ -4668,12 +4813,26 @@ function settleAudit(a) {
|
|
|
4668
4813
|
}
|
|
4669
4814
|
function prepareTree(meta) {
|
|
4670
4815
|
const root = meta.repoRoot;
|
|
4816
|
+
if (root && meta.mode === "read" && meta.snapshot && !meta.snapshot.dir) {
|
|
4817
|
+
const shown = meta.snapshot.left.slice(0, 3).join(", ") + (meta.snapshot.left.length > 3 ? `, +${meta.snapshot.left.length - 3} more` : "");
|
|
4818
|
+
try {
|
|
4819
|
+
const s = readSnapshot(root);
|
|
4820
|
+
meta.snapshot.dir = s.dir;
|
|
4821
|
+
meta.snapshot.tree = s.tree;
|
|
4822
|
+
meta.cwd = path24.join(s.dir, path24.relative(root, meta.dir));
|
|
4823
|
+
if (!fs28.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
4824
|
+
if (shown) meta.warnings.push(`read in a clean snapshot of your project: the secret-looking files (${shown}) and git-ignored files are not in it; --in-place reads the directory itself`);
|
|
4825
|
+
} catch (e) {
|
|
4826
|
+
meta.snapshot = void 0;
|
|
4827
|
+
meta.warnings.push(`could not make a clean snapshot (${e.message.slice(0, 120)}): the worker reads the directory itself${shown ? `, where secret-looking files sit (${shown})` : ""}`);
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4671
4830
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
4672
4831
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
4673
4832
|
meta.baseTree = snapshotTree(root);
|
|
4674
|
-
meta.worktree =
|
|
4833
|
+
meta.worktree = path24.join(worktreesDir(), meta.id);
|
|
4675
4834
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
4676
|
-
meta.cwd =
|
|
4835
|
+
meta.cwd = path24.join(meta.worktree, path24.relative(root, meta.dir));
|
|
4677
4836
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
4678
4837
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
4679
4838
|
}
|
|
@@ -4696,7 +4855,7 @@ function attempt(meta, backend, target) {
|
|
|
4696
4855
|
timeoutSec: meta.timeoutSec
|
|
4697
4856
|
});
|
|
4698
4857
|
}
|
|
4699
|
-
var read2 = (f) =>
|
|
4858
|
+
var read2 = (f) => fs28.existsSync(f) ? fs28.readFileSync(f, "utf8") : "";
|
|
4700
4859
|
function retryableFailure(meta, backend, res) {
|
|
4701
4860
|
const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4702
4861
|
if (run.usage.steps > 0) return void 0;
|
|
@@ -4713,14 +4872,15 @@ function finalize(meta, res) {
|
|
|
4713
4872
|
const ran = meta.ran ?? meta.worker;
|
|
4714
4873
|
const backend = getBackend(ran.backend);
|
|
4715
4874
|
const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4875
|
+
if (meta.snapshot?.dir && meta.repoRoot) run.finalText = backToProject(run.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
4716
4876
|
meta.sessionId = run.sessionId ?? meta.sessionId;
|
|
4717
4877
|
meta.usage = run.usage;
|
|
4718
|
-
|
|
4878
|
+
fs28.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
|
|
4719
4879
|
`);
|
|
4720
4880
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
|
|
4721
4881
|
if (meta.reviewOf) {
|
|
4722
4882
|
meta.verdict = parseVerdict(run.finalText);
|
|
4723
|
-
if (meta.packageFile)
|
|
4883
|
+
if (meta.packageFile) fs28.rmSync(meta.packageFile, { force: true });
|
|
4724
4884
|
}
|
|
4725
4885
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
4726
4886
|
const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
@@ -4768,7 +4928,7 @@ function captureChanges(meta) {
|
|
|
4768
4928
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4769
4929
|
meta.changes = d.changes;
|
|
4770
4930
|
meta.stats = d.stats;
|
|
4771
|
-
|
|
4931
|
+
fs28.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4772
4932
|
}
|
|
4773
4933
|
function runVerify(meta) {
|
|
4774
4934
|
const r = spawnSync6(meta.verify, {
|
|
@@ -4781,7 +4941,7 @@ function runVerify(meta) {
|
|
|
4781
4941
|
env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4782
4942
|
});
|
|
4783
4943
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4784
|
-
|
|
4944
|
+
fs28.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4785
4945
|
return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4786
4946
|
}
|
|
4787
4947
|
function applyRun(meta, allowDelete = false) {
|
|
@@ -4826,7 +4986,7 @@ function discardRun(meta) {
|
|
|
4826
4986
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4827
4987
|
}
|
|
4828
4988
|
function cleanupWorktree(meta) {
|
|
4829
|
-
if (meta.worktree &&
|
|
4989
|
+
if (meta.worktree && fs28.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4830
4990
|
}
|
|
4831
4991
|
|
|
4832
4992
|
// src/cli/commands.ts
|
|
@@ -4861,7 +5021,7 @@ async function cmdReview(p) {
|
|
|
4861
5021
|
let meta;
|
|
4862
5022
|
try {
|
|
4863
5023
|
meta = prepareRun({
|
|
4864
|
-
...runOptions(p, fill(loadTemplate(
|
|
5024
|
+
...runOptions(p, fill(loadTemplate(TEMPLATE2[job.kind]), { PACKAGE_FILE: packageFile })),
|
|
4865
5025
|
mode: "read",
|
|
4866
5026
|
dir: job.dir,
|
|
4867
5027
|
worker: flag(p, "worker") ?? (flag(p, "tier") ? void 0 : pickReviewer(job)),
|
|
@@ -4876,10 +5036,10 @@ async function cmdReview(p) {
|
|
|
4876
5036
|
writeMeta(meta);
|
|
4877
5037
|
}
|
|
4878
5038
|
} catch (e) {
|
|
4879
|
-
|
|
5039
|
+
fs29.rmSync(packageFile, { force: true });
|
|
4880
5040
|
throw e;
|
|
4881
5041
|
}
|
|
4882
|
-
|
|
5042
|
+
fs29.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
4883
5043
|
return launch(p, meta);
|
|
4884
5044
|
}
|
|
4885
5045
|
async function cmdAudit(p) {
|
|
@@ -4947,7 +5107,7 @@ function cmdPlan(p) {
|
|
|
4947
5107
|
}
|
|
4948
5108
|
function cmdCrew(p) {
|
|
4949
5109
|
const file2 = flag(p, "task-file");
|
|
4950
|
-
const tasks = (file2 ?
|
|
5110
|
+
const tasks = (file2 ? fs29.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
4951
5111
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
4952
5112
|
if (has(p, "write") && tasks.length > 1) {
|
|
4953
5113
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -5148,7 +5308,7 @@ function sinceMs(s) {
|
|
|
5148
5308
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
5149
5309
|
return Date.now() - Number(m[1]) * 864e5;
|
|
5150
5310
|
}
|
|
5151
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
5311
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs29.readFileSync(0, "utf8");
|
|
5152
5312
|
function cmdStatusline(p) {
|
|
5153
5313
|
const input = readStdin();
|
|
5154
5314
|
const lines = [];
|
|
@@ -5188,8 +5348,8 @@ function cmdModels(p) {
|
|
|
5188
5348
|
return 0;
|
|
5189
5349
|
}
|
|
5190
5350
|
function cmdHookStart() {
|
|
5191
|
-
const script =
|
|
5192
|
-
if (
|
|
5351
|
+
const script = path25.join(path25.dirname(path25.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
|
|
5352
|
+
if (fs29.existsSync(script)) spawnSync7(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
5193
5353
|
return 0;
|
|
5194
5354
|
}
|
|
5195
5355
|
function cmdInit(p) {
|
|
@@ -5229,8 +5389,8 @@ function cmdSavings(p) {
|
|
|
5229
5389
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
5230
5390
|
const out = flag(p, "card");
|
|
5231
5391
|
if (out) {
|
|
5232
|
-
|
|
5233
|
-
console.log(`card written to ${
|
|
5392
|
+
fs29.writeFileSync(out, card(t, period));
|
|
5393
|
+
console.log(`card written to ${path25.resolve(out)}`);
|
|
5234
5394
|
}
|
|
5235
5395
|
if (has(p, "badge")) console.log(`})`);
|
|
5236
5396
|
return 0;
|
|
@@ -5250,7 +5410,7 @@ function cmdConfig(p) {
|
|
|
5250
5410
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
5251
5411
|
return 0;
|
|
5252
5412
|
}
|
|
5253
|
-
console.log(`config file: ${configPath()}${
|
|
5413
|
+
console.log(`config file: ${configPath()}${fs29.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
5254
5414
|
for (const [key, s] of Object.entries(eff)) {
|
|
5255
5415
|
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);
|
|
5256
5416
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -5274,9 +5434,10 @@ function cmdClean(p) {
|
|
|
5274
5434
|
const m = readMeta(id);
|
|
5275
5435
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
5276
5436
|
recordRun(m);
|
|
5277
|
-
|
|
5437
|
+
fs29.rmSync(runDir(id), { recursive: true, force: true });
|
|
5278
5438
|
}
|
|
5279
|
-
|
|
5439
|
+
const snapshots = prune(Date.now(), 0);
|
|
5440
|
+
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
5280
5441
|
return 0;
|
|
5281
5442
|
}
|
|
5282
5443
|
var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[0])))), 0);
|
|
@@ -5284,9 +5445,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
5284
5445
|
|
|
5285
5446
|
// src/core/doctor.ts
|
|
5286
5447
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5287
|
-
import
|
|
5448
|
+
import fs30 from "node:fs";
|
|
5288
5449
|
import os10 from "node:os";
|
|
5289
|
-
import
|
|
5450
|
+
import path26 from "node:path";
|
|
5290
5451
|
var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
5291
5452
|
var NEXT = [
|
|
5292
5453
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -5296,10 +5457,10 @@ var NEXT = [
|
|
|
5296
5457
|
{ when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
|
|
5297
5458
|
];
|
|
5298
5459
|
function firstNodeOnPath() {
|
|
5299
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
5300
|
-
const file2 =
|
|
5460
|
+
for (const dir of (process.env.PATH ?? "").split(path26.delimiter).filter(Boolean)) {
|
|
5461
|
+
const file2 = path26.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
5301
5462
|
try {
|
|
5302
|
-
if (!
|
|
5463
|
+
if (!fs30.statSync(file2).isFile()) continue;
|
|
5303
5464
|
} catch {
|
|
5304
5465
|
continue;
|
|
5305
5466
|
}
|
|
@@ -5326,12 +5487,12 @@ function doctor5(probe) {
|
|
|
5326
5487
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
5327
5488
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
5328
5489
|
const onPath = firstNodeOnPath();
|
|
5329
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
5490
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path26.resolve(onPath.path) !== path26.resolve(process.execPath)) {
|
|
5330
5491
|
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)`);
|
|
5331
5492
|
}
|
|
5332
5493
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
5333
5494
|
const cfg = loadConfig();
|
|
5334
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
5495
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs30.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
5335
5496
|
for (const w of cfg.warnings) add("warn", w);
|
|
5336
5497
|
if (process.platform !== "win32") {
|
|
5337
5498
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
@@ -5479,19 +5640,19 @@ function skillChecks() {
|
|
|
5479
5640
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
5480
5641
|
}
|
|
5481
5642
|
if (geminiExtensionInstalled()) {
|
|
5482
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
5643
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path26.sep}.agents${path26.sep}`) && i.names.length);
|
|
5483
5644
|
checks.push(
|
|
5484
5645
|
linked ? { level: "warn", message: "Pitroom is installed as a Gemini CLI extension and linked into ~/.agents/skills: Gemini loads the skills twice; run `pitroom uninstall` or `gemini extensions uninstall pitroom`" } : { level: "ok", message: "Gemini CLI extension installed (skills + context)" }
|
|
5485
5646
|
);
|
|
5486
5647
|
}
|
|
5487
5648
|
if (viaPlugin) {
|
|
5488
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
5649
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path26.sep}.claude${path26.sep}`) && i.names.length);
|
|
5489
5650
|
checks.push(
|
|
5490
5651
|
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)" }
|
|
5491
5652
|
);
|
|
5492
5653
|
}
|
|
5493
5654
|
const launcher = launcherPath();
|
|
5494
|
-
if (
|
|
5655
|
+
if (fs30.existsSync(launcher)) {
|
|
5495
5656
|
const r = spawnSync8(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
5496
5657
|
checks.push(
|
|
5497
5658
|
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)}` }
|
|
@@ -5503,12 +5664,12 @@ function skillChecks() {
|
|
|
5503
5664
|
}
|
|
5504
5665
|
function geminiExtensionInstalled() {
|
|
5505
5666
|
const home2 = process.env.GEMINI_CLI_HOME ?? os10.homedir();
|
|
5506
|
-
return
|
|
5667
|
+
return fs30.existsSync(path26.join(home2, ".gemini", "extensions", "pitroom"));
|
|
5507
5668
|
}
|
|
5508
5669
|
function pluginInstalled() {
|
|
5509
5670
|
try {
|
|
5510
|
-
const f =
|
|
5511
|
-
const plugins = JSON.parse(
|
|
5671
|
+
const f = path26.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
5672
|
+
const plugins = JSON.parse(fs30.readFileSync(f, "utf8")).plugins ?? {};
|
|
5512
5673
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
5513
5674
|
} catch {
|
|
5514
5675
|
return false;
|
|
@@ -5525,24 +5686,24 @@ function superpowersActive() {
|
|
|
5525
5686
|
for (const p of openCodePlugins()) {
|
|
5526
5687
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
5527
5688
|
}
|
|
5528
|
-
for (const base2 of [
|
|
5529
|
-
const dir =
|
|
5530
|
-
if (
|
|
5689
|
+
for (const base2 of [path26.join(os10.homedir(), ".agents", "skills"), path26.join(os10.homedir(), ".claude", "skills")]) {
|
|
5690
|
+
const dir = path26.join(base2, "using-superpowers");
|
|
5691
|
+
if (fs30.existsSync(path26.join(dir, "SKILL.md"))) found.push(dir);
|
|
5531
5692
|
}
|
|
5532
5693
|
return found;
|
|
5533
5694
|
}
|
|
5534
5695
|
function claudePlugins() {
|
|
5535
5696
|
try {
|
|
5536
|
-
const f =
|
|
5537
|
-
return Object.keys(JSON.parse(
|
|
5697
|
+
const f = path26.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
5698
|
+
return Object.keys(JSON.parse(fs30.readFileSync(f, "utf8")).plugins ?? {});
|
|
5538
5699
|
} catch {
|
|
5539
5700
|
return [];
|
|
5540
5701
|
}
|
|
5541
5702
|
}
|
|
5542
5703
|
function claudePluginEnabled(key) {
|
|
5543
5704
|
try {
|
|
5544
|
-
const f =
|
|
5545
|
-
return JSON.parse(
|
|
5705
|
+
const f = path26.join(os10.homedir(), ".claude", "settings.json");
|
|
5706
|
+
return JSON.parse(fs30.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
5546
5707
|
} catch {
|
|
5547
5708
|
return true;
|
|
5548
5709
|
}
|
|
@@ -5551,7 +5712,7 @@ function codexPlugins() {
|
|
|
5551
5712
|
const plugins = /* @__PURE__ */ new Map();
|
|
5552
5713
|
let text;
|
|
5553
5714
|
try {
|
|
5554
|
-
text =
|
|
5715
|
+
text = fs30.readFileSync(path26.join(process.env.CODEX_HOME ?? path26.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
|
|
5555
5716
|
} catch {
|
|
5556
5717
|
return plugins;
|
|
5557
5718
|
}
|
|
@@ -5569,11 +5730,11 @@ function codexPlugins() {
|
|
|
5569
5730
|
return plugins;
|
|
5570
5731
|
}
|
|
5571
5732
|
function openCodePlugins() {
|
|
5572
|
-
const dir =
|
|
5733
|
+
const dir = path26.join(process.env.XDG_CONFIG_HOME ?? path26.join(os10.homedir(), ".config"), "opencode");
|
|
5573
5734
|
const plugins = [];
|
|
5574
5735
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
5575
5736
|
try {
|
|
5576
|
-
const text =
|
|
5737
|
+
const text = fs30.readFileSync(path26.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
5577
5738
|
const list2 = JSON.parse(text).plugin;
|
|
5578
5739
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
5579
5740
|
} catch {
|
|
@@ -5581,7 +5742,7 @@ function openCodePlugins() {
|
|
|
5581
5742
|
}
|
|
5582
5743
|
for (const folder of ["plugin", "plugins"]) {
|
|
5583
5744
|
try {
|
|
5584
|
-
for (const f of
|
|
5745
|
+
for (const f of fs30.readdirSync(path26.join(dir, folder))) plugins.push(path26.join(dir, folder, f));
|
|
5585
5746
|
} catch {
|
|
5586
5747
|
}
|
|
5587
5748
|
}
|
|
@@ -5652,6 +5813,8 @@ Run options
|
|
|
5652
5813
|
-W, --worker T worker target "backend[:model]" (default: config "worker", else opencode)
|
|
5653
5814
|
-m, --model M model for that worker (default: the worker CLI's own default)
|
|
5654
5815
|
--tier NAME a worker from the config's "tiers" (e.g. cheap, standard, capable); -W wins
|
|
5816
|
+
--in-place a read run reads the directory itself, not a clean snapshot (default: a snapshot without secret-looking
|
|
5817
|
+
files when the directory has any; config "readIn": auto | snapshot | project)
|
|
5655
5818
|
--audit have another worker re-check this read run's answer; --no-audit skips it (default: the config's "audit")
|
|
5656
5819
|
--effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
|
|
5657
5820
|
--plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
|