pitroom 0.6.9 → 0.6.10
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 +14 -0
- package/README.md +6 -3
- package/dist/lib.mjs +59 -12
- package/dist/pitroom.mjs +167 -65
- package/package.json +1 -1
- package/skills/pitroom-crew/SKILL.md +1 -1
- package/skills/using-pitroom/SKILL.md +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pitroom",
|
|
3
3
|
"description": "A free pit crew for your expensive coding agent: the superpowers development workflow, run by cheap workers. Delegate research, fixes, tests, reviews and whole plans from Claude Code, Codex or any agent to OpenCode, Codex or Claude Code workers, with receipts that prove the savings.",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.10",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TahaAtas"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pitroom",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "A free pit crew for your expensive coding agent: delegate research, fixes, tests, reviews and parallel investigations to cheap worker agents running your own models, with verified answers, exact diffs and cost receipts.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TahaAtas"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.10
|
|
4
|
+
|
|
5
|
+
### Verified answers
|
|
6
|
+
- A reference written as a bare file name or a partial path (`ipc.ts:218`, `src/main/ipc.ts:218`) is now looked up in the project, so it no longer counts as "file not found" just because the worker left out the directory. If several files match, one that holds up is enough.
|
|
7
|
+
- A call written like a reference (`Schema.parse:432`, `orchestrator.start:638`) is not a file reference and is no longer counted. On a real run this turned "7/18 verified" into "13/13"; the references that really did not match are still reported.
|
|
8
|
+
|
|
9
|
+
### Safety
|
|
10
|
+
- A run warns when the directory its worker runs in holds `.env` files, private keys (`*.pem`, `id_rsa`…) or `credentials.json`: the worker is told not to read them, but nothing stops it. The warning is on the report and when a run starts with `--bg`. For `-i` it names only the files the isolated copy would contain (a git-ignored `.env` is not copied). `PITROOM_NO_SECRET_WARNING=1` silences it.
|
|
11
|
+
|
|
12
|
+
### Smaller
|
|
13
|
+
- OpenCode with no pinned model no longer warns about `--effort` on every run: there is nothing to attach the level to, so it is left out quietly.
|
|
14
|
+
- A detached dashboard stops after an hour without a request (it was four hours).
|
|
15
|
+
- Every run card shows its worker's logo, and its animated mascot while it runs; the skills tell the agent to open the dashboard in the app's own browser pane (not with `--open`, which launches the system browser).
|
|
16
|
+
|
|
3
17
|
## 0.6.9
|
|
4
18
|
|
|
5
19
|
### Dashboard
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Model-agnostic · Verified answers · Receipts, not vibes · OpenCode / Codex /
|
|
|
11
11
|
<br />
|
|
12
12
|
|
|
13
13
|
[](https://github.com/ATASTECH/pitroom/releases/latest)
|
|
14
|
-
[](https://www.npmjs.com/package/pitroom)
|
|
15
15
|
[](https://www.npmjs.com/package/pitroom)
|
|
16
16
|
[](https://github.com/ATASTECH/pitroom/stargazers)
|
|
17
17
|
[](https://github.com/ATASTECH/pitroom/actions/workflows/ci.yml)
|
|
@@ -82,7 +82,7 @@ One group can mix them, and a fallback chain can cross them ([how adapters work]
|
|
|
82
82
|
|
|
83
83
|
### Verified answers
|
|
84
84
|
|
|
85
|
-
Every `path:line` a worker cites is checked on disk.
|
|
85
|
+
Every `path:line` a worker cites is checked on disk. A cite without its directory (`ipc.ts:218`, `src/main/ipc.ts:218`) is found in the project, and a call such as `Schema.parse:432` is not mistaken for a file.
|
|
86
86
|
|
|
87
87
|
Your agent trusts what checks out and skips re-reading it.
|
|
88
88
|
|
|
@@ -476,7 +476,7 @@ Two settings make every delegation visible, whether or not the agent mentions it
|
|
|
476
476
|
|
|
477
477
|
The Claude Code and Codex **apps** show neither hook messages nor a status line. For them there are two things that need no setup:
|
|
478
478
|
|
|
479
|
-
- `pitroom dash --detach` prints the address of a live dashboard with three tabs: **Live** (every running and recent run as an animated card; click one for the task, what the worker did step by step, its result, the diff and the details), **History** (search and filter everything Pitroom ever ran; every run opens as the same card) and **Stats** (success rate, time, tokens and savings per worker and model). It is read-only, listens on `127.0.0.1` only and stops itself after
|
|
479
|
+
- `pitroom dash --detach` prints the address of a live dashboard with three tabs: **Live** (every running and recent run as an animated card; click one for the task, what the worker did step by step, its result, the diff and the details), **History** (search and filter everything Pitroom ever ran; every run opens as the same card) and **Stats** (success rate, time, tokens and savings per worker and model). It is read-only, listens on `127.0.0.1` only and stops itself after an hour without a request (`pitroom dash --stop` ends it sooner). Open it in a browser or in the app's own browser pane. The skills tell the agent to start it and give you the address when it runs workers in the background.
|
|
480
480
|
- `pitroom watch -g NAME --brief` prints one card line when a worker starts and one when it ends. In Claude Code, the agent runs it through the Monitor tool and the lines appear in the app; in Codex the command's output block fills as it goes.
|
|
481
481
|
|
|
482
482
|
---
|
|
@@ -546,6 +546,7 @@ Fallbacks cross backends (e.g. `"fallback": ["codex:#low", "opencode"]`): a work
|
|
|
546
546
|
| `PITROOM_PRIMARY` | `sonnet` | Pricing preset for the savings estimate: `sonnet`, `opus`, `haiku`, `gpt-5` |
|
|
547
547
|
| `PITROOM_PRICE` | | Custom primary price, USD per 1M tokens: `"in,out[,cachedIn]"` |
|
|
548
548
|
| `PITROOM_HOME` | `~/.local/state/pitroom` | Where run records and the ledger live |
|
|
549
|
+
| `PITROOM_NO_SECRET_WARNING` | | Set to `1` to stop the warning about `.env` files and private keys in the worker's directory |
|
|
549
550
|
| `PITROOM_<WORKER>_BIN` | on PATH | Path to a worker CLI, e.g. `PITROOM_OPENCODE_BIN` |
|
|
550
551
|
|
|
551
552
|
</details>
|
|
@@ -581,6 +582,8 @@ Or put defaults in `~/.config/pitroom/config.json` (flags and env still win); `p
|
|
|
581
582
|
|
|
582
583
|
**Can the worker break my repo?** It can't touch git history, refs or your index (permission profile + git guard), can't run bulk deletes, and write-mode edits are revertible with one command. The guard is not an OS sandbox: git called by absolute path from inside a script, or non-git tools, are outside its reach, which is why `--isolate` exists: nothing reaches your tree until you apply it.
|
|
583
584
|
|
|
585
|
+
**Can the worker read my `.env`?** In the directory it runs in, yes: its prompt says not to, and nothing stops it, and a free model may be hosted by a third party. Pitroom warns when it sees `.env` files, private keys or `credentials.json` there (`PITROOM_NO_SECRET_WARNING=1` silences it). An isolated copy (`-i`) leaves out git-ignored files, so a git-ignored `.env` is not in it; a clean checkout is the safest place for a first try.
|
|
586
|
+
|
|
584
587
|
**What if the worker fails?** Pitroom exits non-zero with the real cause (for example a default model that no longer exists) and your agent simply continues on its own. `pitroom doctor` diagnoses setup problems.
|
|
585
588
|
|
|
586
589
|
---
|
package/dist/lib.mjs
CHANGED
|
@@ -1040,6 +1040,11 @@ var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default m
|
|
|
1040
1040
|
// src/core/refs.ts
|
|
1041
1041
|
import fs5 from "node:fs";
|
|
1042
1042
|
import path5 from "node:path";
|
|
1043
|
+
var FILE_EXTENSIONS = new Set(
|
|
1044
|
+
"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(" ")
|
|
1045
|
+
);
|
|
1046
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
1047
|
+
var MAX_INDEXED = 6e4;
|
|
1043
1048
|
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
1044
1049
|
var REF = new RegExp(
|
|
1045
1050
|
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
@@ -1097,23 +1102,36 @@ function verifyRefs(refs, dirs) {
|
|
|
1097
1102
|
}
|
|
1098
1103
|
return lineCache.get(file);
|
|
1099
1104
|
};
|
|
1105
|
+
let index;
|
|
1106
|
+
const byName = () => index ??= indexFiles(roots);
|
|
1107
|
+
const candidates = (ref) => {
|
|
1108
|
+
const direct = resolve(ref.file, roots);
|
|
1109
|
+
if (direct) return [direct];
|
|
1110
|
+
if (path5.isAbsolute(ref.file)) return [];
|
|
1111
|
+
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
1112
|
+
return (byName().get(path5.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path5.basename(f) === wanted).slice(0, 20);
|
|
1113
|
+
};
|
|
1114
|
+
const check = (file, ref) => {
|
|
1115
|
+
const lines = load(file);
|
|
1116
|
+
if (!lines) return void 0;
|
|
1117
|
+
if (ref.end > lines.length) return `file has ${lines.length} lines`;
|
|
1118
|
+
if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
|
|
1119
|
+
return void 0;
|
|
1120
|
+
};
|
|
1121
|
+
let total = 0;
|
|
1100
1122
|
for (const ref of refs) {
|
|
1101
|
-
const
|
|
1102
|
-
if (!
|
|
1123
|
+
const files = candidates(ref);
|
|
1124
|
+
if (!files.length) {
|
|
1125
|
+
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
1126
|
+
total++;
|
|
1103
1127
|
invalid.push({ ref: ref.text, reason: path5.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
1104
1128
|
continue;
|
|
1105
1129
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
if (ref.
|
|
1109
|
-
invalid.push({ ref: ref.text, reason: `file has ${lines.length} lines` });
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
1112
|
-
if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) {
|
|
1113
|
-
invalid.push({ ref: ref.text, reason: `\`${ref.symbol}\` not near line ${ref.start}` });
|
|
1114
|
-
}
|
|
1130
|
+
total++;
|
|
1131
|
+
const reasons = files.map((f) => check(f, ref));
|
|
1132
|
+
if (!reasons.some((r) => r === void 0)) invalid.push({ ref: ref.text, reason: files.length > 1 ? `${reasons[0]} (${files.length} files match)` : reasons[0] });
|
|
1115
1133
|
}
|
|
1116
|
-
return { total
|
|
1134
|
+
return { total, valid: total - invalid.length, invalid };
|
|
1117
1135
|
}
|
|
1118
1136
|
function mentions(lines, ref) {
|
|
1119
1137
|
const near = lines.slice(Math.max(0, ref.start - 1 - WINDOW), ref.end + WINDOW).join("\n");
|
|
@@ -1126,6 +1144,35 @@ function enclosedBy(lines, ref) {
|
|
|
1126
1144
|
);
|
|
1127
1145
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
1128
1146
|
}
|
|
1147
|
+
var isFileName = (name) => {
|
|
1148
|
+
const base = path5.basename(name);
|
|
1149
|
+
return !base.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base) || FILE_EXTENSIONS.has(base.split(".").pop().toLowerCase());
|
|
1150
|
+
};
|
|
1151
|
+
function indexFiles(roots) {
|
|
1152
|
+
const index = /* @__PURE__ */ new Map();
|
|
1153
|
+
let count = 0;
|
|
1154
|
+
const walk = (dir) => {
|
|
1155
|
+
let entries;
|
|
1156
|
+
try {
|
|
1157
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
1158
|
+
} catch {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
for (const e of entries) {
|
|
1162
|
+
if (count >= MAX_INDEXED) return;
|
|
1163
|
+
if (e.isDirectory()) {
|
|
1164
|
+
if (!SKIP_DIRS.has(e.name)) walk(path5.join(dir, e.name));
|
|
1165
|
+
} else if (e.isFile()) {
|
|
1166
|
+
count++;
|
|
1167
|
+
const list = index.get(e.name);
|
|
1168
|
+
if (list) list.push(path5.join(dir, e.name));
|
|
1169
|
+
else index.set(e.name, [path5.join(dir, e.name)]);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
for (const r of roots) walk(r);
|
|
1174
|
+
return index;
|
|
1175
|
+
}
|
|
1129
1176
|
function resolve(ref, roots) {
|
|
1130
1177
|
const candidates = path5.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path5.join(r, ref));
|
|
1131
1178
|
return candidates.find((c) => {
|
package/dist/pitroom.mjs
CHANGED
|
@@ -1391,8 +1391,8 @@ function effortFlag(p) {
|
|
|
1391
1391
|
|
|
1392
1392
|
// src/cli/commands.ts
|
|
1393
1393
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
1394
|
-
import
|
|
1395
|
-
import
|
|
1394
|
+
import fs25 from "node:fs";
|
|
1395
|
+
import path21 from "node:path";
|
|
1396
1396
|
|
|
1397
1397
|
// src/core/history.ts
|
|
1398
1398
|
import fs10 from "node:fs";
|
|
@@ -3085,7 +3085,7 @@ async function dashCommand(o) {
|
|
|
3085
3085
|
return 0;
|
|
3086
3086
|
}
|
|
3087
3087
|
if (o.detach) {
|
|
3088
|
-
const child = spawn(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "
|
|
3088
|
+
const child = spawn(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "1h"], {
|
|
3089
3089
|
detached: true,
|
|
3090
3090
|
stdio: "ignore",
|
|
3091
3091
|
env: process.env
|
|
@@ -3130,8 +3130,8 @@ async function dashCommand(o) {
|
|
|
3130
3130
|
|
|
3131
3131
|
// src/core/run.ts
|
|
3132
3132
|
import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
3133
|
-
import
|
|
3134
|
-
import
|
|
3133
|
+
import fs24 from "node:fs";
|
|
3134
|
+
import path20 from "node:path";
|
|
3135
3135
|
|
|
3136
3136
|
// src/core/chain.ts
|
|
3137
3137
|
function resolveChain(flags = {}) {
|
|
@@ -3155,9 +3155,7 @@ function resolveChain(flags = {}) {
|
|
|
3155
3155
|
worker = withDefault(worker);
|
|
3156
3156
|
if (flags.effort) {
|
|
3157
3157
|
const base2 = (worker.model ?? "").split("#")[0];
|
|
3158
|
-
if (worker.backend === "opencode" && !base2) {
|
|
3159
|
-
warnings.push("--effort needs a model for OpenCode (provider/model#variant): set one in the config or pass -m; ignored");
|
|
3160
|
-
} else {
|
|
3158
|
+
if (!(worker.backend === "opencode" && !base2)) {
|
|
3161
3159
|
worker = { ...worker, model: `${base2}#${flags.effort}` };
|
|
3162
3160
|
}
|
|
3163
3161
|
}
|
|
@@ -3494,6 +3492,11 @@ ${task}`;
|
|
|
3494
3492
|
// src/core/refs.ts
|
|
3495
3493
|
import fs22 from "node:fs";
|
|
3496
3494
|
import path18 from "node:path";
|
|
3495
|
+
var FILE_EXTENSIONS = new Set(
|
|
3496
|
+
"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(" ")
|
|
3497
|
+
);
|
|
3498
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
3499
|
+
var MAX_INDEXED = 6e4;
|
|
3497
3500
|
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
3498
3501
|
var REF = new RegExp(
|
|
3499
3502
|
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
@@ -3551,23 +3554,36 @@ function verifyRefs(refs, dirs) {
|
|
|
3551
3554
|
}
|
|
3552
3555
|
return lineCache.get(file);
|
|
3553
3556
|
};
|
|
3557
|
+
let index;
|
|
3558
|
+
const byName = () => index ??= indexFiles(roots);
|
|
3559
|
+
const candidates = (ref) => {
|
|
3560
|
+
const direct = resolve(ref.file, roots);
|
|
3561
|
+
if (direct) return [direct];
|
|
3562
|
+
if (path18.isAbsolute(ref.file)) return [];
|
|
3563
|
+
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
3564
|
+
return (byName().get(path18.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path18.basename(f) === wanted).slice(0, 20);
|
|
3565
|
+
};
|
|
3566
|
+
const check = (file, ref) => {
|
|
3567
|
+
const lines = load(file);
|
|
3568
|
+
if (!lines) return void 0;
|
|
3569
|
+
if (ref.end > lines.length) return `file has ${lines.length} lines`;
|
|
3570
|
+
if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
|
|
3571
|
+
return void 0;
|
|
3572
|
+
};
|
|
3573
|
+
let total = 0;
|
|
3554
3574
|
for (const ref of refs) {
|
|
3555
|
-
const
|
|
3556
|
-
if (!
|
|
3575
|
+
const files = candidates(ref);
|
|
3576
|
+
if (!files.length) {
|
|
3577
|
+
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
3578
|
+
total++;
|
|
3557
3579
|
invalid.push({ ref: ref.text, reason: path18.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
3558
3580
|
continue;
|
|
3559
3581
|
}
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
if (ref.
|
|
3563
|
-
invalid.push({ ref: ref.text, reason: `file has ${lines.length} lines` });
|
|
3564
|
-
continue;
|
|
3565
|
-
}
|
|
3566
|
-
if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) {
|
|
3567
|
-
invalid.push({ ref: ref.text, reason: `\`${ref.symbol}\` not near line ${ref.start}` });
|
|
3568
|
-
}
|
|
3582
|
+
total++;
|
|
3583
|
+
const reasons = files.map((f) => check(f, ref));
|
|
3584
|
+
if (!reasons.some((r) => r === void 0)) invalid.push({ ref: ref.text, reason: files.length > 1 ? `${reasons[0]} (${files.length} files match)` : reasons[0] });
|
|
3569
3585
|
}
|
|
3570
|
-
return { total
|
|
3586
|
+
return { total, valid: total - invalid.length, invalid };
|
|
3571
3587
|
}
|
|
3572
3588
|
function mentions(lines, ref) {
|
|
3573
3589
|
const near = lines.slice(Math.max(0, ref.start - 1 - WINDOW), ref.end + WINDOW).join("\n");
|
|
@@ -3580,6 +3596,35 @@ function enclosedBy(lines, ref) {
|
|
|
3580
3596
|
);
|
|
3581
3597
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
3582
3598
|
}
|
|
3599
|
+
var isFileName = (name) => {
|
|
3600
|
+
const base2 = path18.basename(name);
|
|
3601
|
+
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
3602
|
+
};
|
|
3603
|
+
function indexFiles(roots) {
|
|
3604
|
+
const index = /* @__PURE__ */ new Map();
|
|
3605
|
+
let count = 0;
|
|
3606
|
+
const walk = (dir) => {
|
|
3607
|
+
let entries;
|
|
3608
|
+
try {
|
|
3609
|
+
entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
3610
|
+
} catch {
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
for (const e of entries) {
|
|
3614
|
+
if (count >= MAX_INDEXED) return;
|
|
3615
|
+
if (e.isDirectory()) {
|
|
3616
|
+
if (!SKIP_DIRS.has(e.name)) walk(path18.join(dir, e.name));
|
|
3617
|
+
} else if (e.isFile()) {
|
|
3618
|
+
count++;
|
|
3619
|
+
const list2 = index.get(e.name);
|
|
3620
|
+
if (list2) list2.push(path18.join(dir, e.name));
|
|
3621
|
+
else index.set(e.name, [path18.join(dir, e.name)]);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
};
|
|
3625
|
+
for (const r of roots) walk(r);
|
|
3626
|
+
return index;
|
|
3627
|
+
}
|
|
3583
3628
|
function resolve(ref, roots) {
|
|
3584
3629
|
const candidates = path18.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path18.join(r, ref));
|
|
3585
3630
|
return candidates.find((c) => {
|
|
@@ -3598,8 +3643,59 @@ function inside(file, roots) {
|
|
|
3598
3643
|
}
|
|
3599
3644
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3600
3645
|
|
|
3646
|
+
// src/core/secrets.ts
|
|
3647
|
+
import { execFileSync } from "node:child_process";
|
|
3648
|
+
import fs23 from "node:fs";
|
|
3649
|
+
import path19 from "node:path";
|
|
3650
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
3651
|
+
var MAX_VISITED = 2e4;
|
|
3652
|
+
var MAX_DEPTH = 4;
|
|
3653
|
+
var TEMPLATE2 = /\.(example|sample|template|dist|defaults?|tpl)$/i;
|
|
3654
|
+
function looksSecret(name) {
|
|
3655
|
+
if (name === ".env" || name.startsWith(".env.") && !TEMPLATE2.test(name)) return true;
|
|
3656
|
+
return /\.(pem|p12|pfx)$/i.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json";
|
|
3657
|
+
}
|
|
3658
|
+
function findSecretFiles(dir) {
|
|
3659
|
+
const found = [];
|
|
3660
|
+
let visited = 0;
|
|
3661
|
+
const walk = (d, depth) => {
|
|
3662
|
+
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
3663
|
+
let entries;
|
|
3664
|
+
try {
|
|
3665
|
+
entries = fs23.readdirSync(d, { withFileTypes: true });
|
|
3666
|
+
} catch {
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
for (const e of entries) {
|
|
3670
|
+
visited++;
|
|
3671
|
+
if (e.isDirectory()) {
|
|
3672
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path19.join(d, e.name), depth + 1);
|
|
3673
|
+
} else if (e.isFile() && looksSecret(e.name)) {
|
|
3674
|
+
found.push(path19.relative(dir, path19.join(d, e.name)));
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
};
|
|
3678
|
+
walk(dir, 0);
|
|
3679
|
+
return found.sort();
|
|
3680
|
+
}
|
|
3681
|
+
function findSecretFilesInTree(root, dir) {
|
|
3682
|
+
try {
|
|
3683
|
+
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
3684
|
+
return out.split("\0").filter((f) => f && looksSecret(path19.basename(f)) && !path19.relative(dir, path19.join(root, f)).startsWith("..")).map((f) => path19.relative(dir, path19.join(root, f))).sort();
|
|
3685
|
+
} catch {
|
|
3686
|
+
return [];
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
function secretWarning(files, mode) {
|
|
3690
|
+
if (!files.length) return void 0;
|
|
3691
|
+
const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
|
|
3692
|
+
const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
|
|
3693
|
+
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";
|
|
3694
|
+
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`;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3601
3697
|
// src/core/run.ts
|
|
3602
|
-
var VERSION2 = true ? "0.6.
|
|
3698
|
+
var VERSION2 = true ? "0.6.10" : "0.0.0-dev";
|
|
3603
3699
|
function planWork(o) {
|
|
3604
3700
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
3605
3701
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -3631,7 +3727,7 @@ function prepareRun(o) {
|
|
|
3631
3727
|
parent = readMeta(o.continueFrom);
|
|
3632
3728
|
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");
|
|
3633
3729
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
3634
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
3730
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs24.existsSync(parent.worktree))) {
|
|
3635
3731
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
3636
3732
|
}
|
|
3637
3733
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -3650,15 +3746,20 @@ function prepareRun(o) {
|
|
|
3650
3746
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
3651
3747
|
}
|
|
3652
3748
|
const mode = parent?.mode ?? o.mode;
|
|
3653
|
-
const dir =
|
|
3654
|
-
if (!
|
|
3749
|
+
const dir = path20.resolve(parent?.dir ?? o.dir);
|
|
3750
|
+
if (!fs24.existsSync(dir) || !fs24.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
3655
3751
|
const root = repoRoot(dir);
|
|
3656
3752
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
3657
3753
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
3658
3754
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
3659
3755
|
}
|
|
3660
|
-
const files = o.files.map((f) =>
|
|
3661
|
-
for (const f of files) if (!
|
|
3756
|
+
const files = o.files.map((f) => path20.resolve(f));
|
|
3757
|
+
for (const f of files) if (!fs24.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
3758
|
+
if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
3759
|
+
const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
|
|
3760
|
+
const note = secretWarning(secrets, mode);
|
|
3761
|
+
if (note) warnings.push(note);
|
|
3762
|
+
}
|
|
3662
3763
|
const meta = {
|
|
3663
3764
|
id: newRunId(),
|
|
3664
3765
|
version: VERSION2,
|
|
@@ -3693,13 +3794,13 @@ function prepareRun(o) {
|
|
|
3693
3794
|
try {
|
|
3694
3795
|
acquireWriteLock(root, meta.id);
|
|
3695
3796
|
} catch (e) {
|
|
3696
|
-
|
|
3797
|
+
fs24.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
3697
3798
|
throw e;
|
|
3698
3799
|
}
|
|
3699
3800
|
}
|
|
3700
|
-
|
|
3801
|
+
fs24.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
3701
3802
|
`);
|
|
3702
|
-
if (work.brief)
|
|
3803
|
+
if (work.brief) fs24.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
3703
3804
|
return meta;
|
|
3704
3805
|
}
|
|
3705
3806
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -3772,7 +3873,7 @@ async function execute(meta) {
|
|
|
3772
3873
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
3773
3874
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
3774
3875
|
const from = runFile(meta.id, f);
|
|
3775
|
-
if (
|
|
3876
|
+
if (fs24.existsSync(from)) fs24.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
3776
3877
|
}
|
|
3777
3878
|
writeMeta(meta);
|
|
3778
3879
|
}
|
|
@@ -3792,9 +3893,9 @@ function prepareTree(meta) {
|
|
|
3792
3893
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
3793
3894
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
3794
3895
|
meta.baseTree = snapshotTree(root);
|
|
3795
|
-
meta.worktree =
|
|
3896
|
+
meta.worktree = path20.join(worktreesDir(), meta.id);
|
|
3796
3897
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
3797
|
-
meta.cwd =
|
|
3898
|
+
meta.cwd = path20.join(meta.worktree, path20.relative(root, meta.dir));
|
|
3798
3899
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
3799
3900
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
3800
3901
|
}
|
|
@@ -3817,7 +3918,7 @@ function attempt(meta, backend, target) {
|
|
|
3817
3918
|
timeoutSec: meta.timeoutSec
|
|
3818
3919
|
});
|
|
3819
3920
|
}
|
|
3820
|
-
var read = (f) =>
|
|
3921
|
+
var read = (f) => fs24.existsSync(f) ? fs24.readFileSync(f, "utf8") : "";
|
|
3821
3922
|
function retryableFailure(meta, backend, res) {
|
|
3822
3923
|
const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
|
|
3823
3924
|
if (run.usage.steps > 0) return void 0;
|
|
@@ -3836,12 +3937,12 @@ function finalize(meta, res) {
|
|
|
3836
3937
|
const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
|
|
3837
3938
|
meta.sessionId = run.sessionId ?? meta.sessionId;
|
|
3838
3939
|
meta.usage = run.usage;
|
|
3839
|
-
|
|
3940
|
+
fs24.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
|
|
3840
3941
|
`);
|
|
3841
3942
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
|
|
3842
3943
|
if (meta.reviewOf) {
|
|
3843
3944
|
meta.verdict = parseVerdict(run.finalText);
|
|
3844
|
-
if (meta.packageFile)
|
|
3945
|
+
if (meta.packageFile) fs24.rmSync(meta.packageFile, { force: true });
|
|
3845
3946
|
}
|
|
3846
3947
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
3847
3948
|
const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
|
|
@@ -3883,7 +3984,7 @@ function captureChanges(meta) {
|
|
|
3883
3984
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
3884
3985
|
meta.changes = d.changes;
|
|
3885
3986
|
meta.stats = d.stats;
|
|
3886
|
-
|
|
3987
|
+
fs24.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
3887
3988
|
}
|
|
3888
3989
|
function runVerify(meta) {
|
|
3889
3990
|
const r = spawnSync5(meta.verify, {
|
|
@@ -3896,7 +3997,7 @@ function runVerify(meta) {
|
|
|
3896
3997
|
env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
3897
3998
|
});
|
|
3898
3999
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
3899
|
-
|
|
4000
|
+
fs24.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
3900
4001
|
return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
3901
4002
|
}
|
|
3902
4003
|
function applyRun(meta, allowDelete = false) {
|
|
@@ -3941,7 +4042,7 @@ function discardRun(meta) {
|
|
|
3941
4042
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
3942
4043
|
}
|
|
3943
4044
|
function cleanupWorktree(meta) {
|
|
3944
|
-
if (meta.worktree &&
|
|
4045
|
+
if (meta.worktree && fs24.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
3945
4046
|
}
|
|
3946
4047
|
|
|
3947
4048
|
// src/cli/commands.ts
|
|
@@ -3951,7 +4052,8 @@ async function launch(p, meta) {
|
|
|
3951
4052
|
console.log(
|
|
3952
4053
|
has(p, "json") ? JSON.stringify(meta, null, 2) : `pitroom started ${meta.mode} run ${meta.id} in background${meta.group ? ` (group ${meta.group})` : ""}
|
|
3953
4054
|
wait: pitroom wait ${meta.id}
|
|
3954
|
-
status: pitroom status ${meta.id}`
|
|
4055
|
+
status: pitroom status ${meta.id}` + meta.warnings.map((w) => `
|
|
4056
|
+
warning: ${w}`).join("")
|
|
3955
4057
|
);
|
|
3956
4058
|
return 0;
|
|
3957
4059
|
}
|
|
@@ -3990,10 +4092,10 @@ async function cmdReview(p) {
|
|
|
3990
4092
|
writeMeta(meta);
|
|
3991
4093
|
}
|
|
3992
4094
|
} catch (e) {
|
|
3993
|
-
|
|
4095
|
+
fs25.rmSync(packageFile, { force: true });
|
|
3994
4096
|
throw e;
|
|
3995
4097
|
}
|
|
3996
|
-
|
|
4098
|
+
fs25.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
3997
4099
|
return launch(p, meta);
|
|
3998
4100
|
}
|
|
3999
4101
|
function cmdPlan(p) {
|
|
@@ -4013,7 +4115,7 @@ function cmdPlan(p) {
|
|
|
4013
4115
|
}
|
|
4014
4116
|
function cmdCrew(p) {
|
|
4015
4117
|
const file = flag(p, "task-file");
|
|
4016
|
-
const tasks = (file ?
|
|
4118
|
+
const tasks = (file ? fs25.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
4017
4119
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
4018
4120
|
if (has(p, "write") && tasks.length > 1) {
|
|
4019
4121
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -4211,7 +4313,7 @@ function sinceMs(s) {
|
|
|
4211
4313
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
4212
4314
|
return Date.now() - Number(m[1]) * 864e5;
|
|
4213
4315
|
}
|
|
4214
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
4316
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs25.readFileSync(0, "utf8");
|
|
4215
4317
|
function cmdStatusline(p) {
|
|
4216
4318
|
const input = readStdin();
|
|
4217
4319
|
const lines = [];
|
|
@@ -4280,8 +4382,8 @@ function cmdSavings(p) {
|
|
|
4280
4382
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
4281
4383
|
const out = flag(p, "card");
|
|
4282
4384
|
if (out) {
|
|
4283
|
-
|
|
4284
|
-
console.log(`card written to ${
|
|
4385
|
+
fs25.writeFileSync(out, card(t, period));
|
|
4386
|
+
console.log(`card written to ${path21.resolve(out)}`);
|
|
4285
4387
|
}
|
|
4286
4388
|
if (has(p, "badge")) console.log(`})`);
|
|
4287
4389
|
return 0;
|
|
@@ -4301,7 +4403,7 @@ function cmdConfig(p) {
|
|
|
4301
4403
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
4302
4404
|
return 0;
|
|
4303
4405
|
}
|
|
4304
|
-
console.log(`config file: ${configPath()}${
|
|
4406
|
+
console.log(`config file: ${configPath()}${fs25.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
4305
4407
|
for (const [key, s] of Object.entries(eff)) {
|
|
4306
4408
|
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);
|
|
4307
4409
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -4325,7 +4427,7 @@ function cmdClean(p) {
|
|
|
4325
4427
|
const m = readMeta(id);
|
|
4326
4428
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
4327
4429
|
recordRun(m);
|
|
4328
|
-
|
|
4430
|
+
fs25.rmSync(runDir(id), { recursive: true, force: true });
|
|
4329
4431
|
}
|
|
4330
4432
|
console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
|
|
4331
4433
|
return 0;
|
|
@@ -4335,9 +4437,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
4335
4437
|
|
|
4336
4438
|
// src/core/doctor.ts
|
|
4337
4439
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
4338
|
-
import
|
|
4440
|
+
import fs26 from "node:fs";
|
|
4339
4441
|
import os9 from "node:os";
|
|
4340
|
-
import
|
|
4442
|
+
import path22 from "node:path";
|
|
4341
4443
|
var MARK = { ok: "\u2714", warn: "!", fail: "\u2718" };
|
|
4342
4444
|
var READ_ONLY_HOW = {
|
|
4343
4445
|
"permission-rules": "per-run permission rules",
|
|
@@ -4351,7 +4453,7 @@ function doctor4(probe) {
|
|
|
4351
4453
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
4352
4454
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
4353
4455
|
const cfg = loadConfig();
|
|
4354
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
4456
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs26.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
4355
4457
|
for (const w of cfg.warnings) add("warn", w);
|
|
4356
4458
|
if (process.platform !== "win32") {
|
|
4357
4459
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
@@ -4452,13 +4554,13 @@ function skillChecks() {
|
|
|
4452
4554
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
4453
4555
|
}
|
|
4454
4556
|
if (viaPlugin) {
|
|
4455
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
4557
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path22.sep}.claude${path22.sep}`) && i.names.length);
|
|
4456
4558
|
checks.push(
|
|
4457
4559
|
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)" }
|
|
4458
4560
|
);
|
|
4459
4561
|
}
|
|
4460
4562
|
const launcher = launcherPath();
|
|
4461
|
-
if (
|
|
4563
|
+
if (fs26.existsSync(launcher)) {
|
|
4462
4564
|
const r = spawnSync7(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
4463
4565
|
checks.push(
|
|
4464
4566
|
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)}` }
|
|
@@ -4470,8 +4572,8 @@ function skillChecks() {
|
|
|
4470
4572
|
}
|
|
4471
4573
|
function pluginInstalled() {
|
|
4472
4574
|
try {
|
|
4473
|
-
const f =
|
|
4474
|
-
const plugins = JSON.parse(
|
|
4575
|
+
const f = path22.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
4576
|
+
const plugins = JSON.parse(fs26.readFileSync(f, "utf8")).plugins ?? {};
|
|
4475
4577
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
4476
4578
|
} catch {
|
|
4477
4579
|
return false;
|
|
@@ -4488,24 +4590,24 @@ function superpowersActive() {
|
|
|
4488
4590
|
for (const p of openCodePlugins()) {
|
|
4489
4591
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
4490
4592
|
}
|
|
4491
|
-
for (const base2 of [
|
|
4492
|
-
const dir =
|
|
4493
|
-
if (
|
|
4593
|
+
for (const base2 of [path22.join(os9.homedir(), ".agents", "skills"), path22.join(os9.homedir(), ".claude", "skills")]) {
|
|
4594
|
+
const dir = path22.join(base2, "using-superpowers");
|
|
4595
|
+
if (fs26.existsSync(path22.join(dir, "SKILL.md"))) found.push(dir);
|
|
4494
4596
|
}
|
|
4495
4597
|
return found;
|
|
4496
4598
|
}
|
|
4497
4599
|
function claudePlugins() {
|
|
4498
4600
|
try {
|
|
4499
|
-
const f =
|
|
4500
|
-
return Object.keys(JSON.parse(
|
|
4601
|
+
const f = path22.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
4602
|
+
return Object.keys(JSON.parse(fs26.readFileSync(f, "utf8")).plugins ?? {});
|
|
4501
4603
|
} catch {
|
|
4502
4604
|
return [];
|
|
4503
4605
|
}
|
|
4504
4606
|
}
|
|
4505
4607
|
function claudePluginEnabled(key) {
|
|
4506
4608
|
try {
|
|
4507
|
-
const f =
|
|
4508
|
-
return JSON.parse(
|
|
4609
|
+
const f = path22.join(os9.homedir(), ".claude", "settings.json");
|
|
4610
|
+
return JSON.parse(fs26.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
4509
4611
|
} catch {
|
|
4510
4612
|
return true;
|
|
4511
4613
|
}
|
|
@@ -4514,7 +4616,7 @@ function codexPlugins() {
|
|
|
4514
4616
|
const plugins = /* @__PURE__ */ new Map();
|
|
4515
4617
|
let text;
|
|
4516
4618
|
try {
|
|
4517
|
-
text =
|
|
4619
|
+
text = fs26.readFileSync(path22.join(process.env.CODEX_HOME ?? path22.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
|
|
4518
4620
|
} catch {
|
|
4519
4621
|
return plugins;
|
|
4520
4622
|
}
|
|
@@ -4532,11 +4634,11 @@ function codexPlugins() {
|
|
|
4532
4634
|
return plugins;
|
|
4533
4635
|
}
|
|
4534
4636
|
function openCodePlugins() {
|
|
4535
|
-
const dir =
|
|
4637
|
+
const dir = path22.join(process.env.XDG_CONFIG_HOME ?? path22.join(os9.homedir(), ".config"), "opencode");
|
|
4536
4638
|
const plugins = [];
|
|
4537
4639
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
4538
4640
|
try {
|
|
4539
|
-
const text =
|
|
4641
|
+
const text = fs26.readFileSync(path22.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
4540
4642
|
const list2 = JSON.parse(text).plugin;
|
|
4541
4643
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
4542
4644
|
} catch {
|
|
@@ -4544,7 +4646,7 @@ function openCodePlugins() {
|
|
|
4544
4646
|
}
|
|
4545
4647
|
for (const folder of ["plugin", "plugins"]) {
|
|
4546
4648
|
try {
|
|
4547
|
-
for (const f of
|
|
4649
|
+
for (const f of fs26.readdirSync(path22.join(dir, folder))) plugins.push(path22.join(dir, folder, f));
|
|
4548
4650
|
} catch {
|
|
4549
4651
|
}
|
|
4550
4652
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pitroom",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "A free pit crew for your expensive coding agent: the superpowers development workflow, run by cheap workers. Delegate research, fixes, tests, reviews and whole plans from Claude Code, Codex or any agent to OpenCode, Codex or Claude Code workers, with receipts that prove the savings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "TahaAtas",
|
|
@@ -57,7 +57,7 @@ Follow the group live without polling:
|
|
|
57
57
|
- `pitroom watch -g NAME --json`: one JSON line per change (`queued`, `started`, `progress`, `fallback`, `done`/`failed`, then `all-done`). Run it through your host's background or monitor facility so each line reaches you as it happens; it exits by itself when the group is done.
|
|
58
58
|
- No such facility: `pitroom wait -g NAME --timeout 540`, and call it again while it exits with 75.
|
|
59
59
|
- `pitroom status -g NAME`: one table snapshot.
|
|
60
|
-
- Tell the user how to see it live: `pitroom dash --detach` prints the address of a read-only page on 127.0.0.1 with every run
|
|
60
|
+
- Tell the user how to see it live: `pitroom dash --detach` prints the address of a read-only page on 127.0.0.1 with every run. Open it in the app's own built-in browser or preview pane when you have one (not with `--open`, which launches the system browser), and otherwise give the user the address; or `pitroom watch -g NAME` in a terminal.
|
|
61
61
|
|
|
62
62
|
While workers run, do the parts you kept for yourself.
|
|
63
63
|
|
|
@@ -65,7 +65,7 @@ Workers can take minutes. Start long work with `--bg` and keep working; follow i
|
|
|
65
65
|
- Worker output is draft work and a worker's report is a claim: verify what you rely on (`pitroom-verification`).
|
|
66
66
|
- You apply patches and commit; workers never commit or push. Push, merge and pull requests happen only after the user asks.
|
|
67
67
|
- Keep the decisions: delegate the reading and typing, not the architecture.
|
|
68
|
-
- Background workers: run `pitroom dash --detach` once
|
|
68
|
+
- Background workers: run `pitroom dash --detach` once. It prints the address of a live page of every run, for the apps (Claude Code, Codex) that show neither Pitroom's cards nor its status line. Open that address in the app's own built-in browser or preview pane if you have a tool for it (the Claude Code desktop app's browser pane, the Codex app's in-app browser) instead of only pasting the link in the chat. Do not use `--open`: it launches the system's default browser. Without such a tool, give the user the address once.
|
|
69
69
|
- Say what you delegated: one line per worker run naming the worker and model (the run report has both) and what it cost or saved. Not every host shows Pitroom's cards or status line, and the user should always know which model touched their code. `pitroom savings --models` lists them all.
|
|
70
70
|
- You set a worker's permissions when you create it, from the task and from how much autonomy the user gave you in this session (for example auto mode): read-only by default, `-i` (an isolated copy) for changes, `-w` (edits in place) when the user wants that, `--web` only when the task needs the web. A worker never widens its own permissions.
|
|
71
71
|
- Pitroom keeps a fixed floor that no flag lifts: no git history changes, no `sudo`, no publishing, no secrets, no killing processes. Everything above that floor is your call.
|