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.
@@ -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.9",
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.9",
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
  [![Release](https://img.shields.io/github/v/release/ATASTECH/pitroom?label=release)](https://github.com/ATASTECH/pitroom/releases/latest)
14
- [![Downloads](https://img.shields.io/github/downloads/ATASTECH/pitroom/total?label=downloads)](https://github.com/ATASTECH/pitroom/releases)
14
+ [![Downloads](https://img.shields.io/npm/dm/pitroom?label=npm%20downloads)](https://www.npmjs.com/package/pitroom)
15
15
  [![npm](https://img.shields.io/npm/v/pitroom?label=npm)](https://www.npmjs.com/package/pitroom)
16
16
  [![Stars](https://img.shields.io/github/stars/ATASTECH/pitroom?style=flat&label=stars)](https://github.com/ATASTECH/pitroom/stargazers)
17
17
  [![CI](https://github.com/ATASTECH/pitroom/actions/workflows/ci.yml/badge.svg)](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 four idle hours (`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.
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 file = resolve(ref.file, roots);
1102
- if (!file) {
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
- const lines = load(file);
1107
- if (!lines) continue;
1108
- if (ref.end > lines.length) {
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: refs.length, valid: refs.length - invalid.length, invalid };
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 fs24 from "node:fs";
1395
- import path20 from "node:path";
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", "4h"], {
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 fs23 from "node:fs";
3134
- import path19 from "node:path";
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 file = resolve(ref.file, roots);
3556
- if (!file) {
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
- const lines = load(file);
3561
- if (!lines) continue;
3562
- if (ref.end > lines.length) {
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: refs.length, valid: refs.length - invalid.length, invalid };
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.9" : "0.0.0-dev";
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 || !fs23.existsSync(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 = path19.resolve(parent?.dir ?? o.dir);
3654
- if (!fs23.existsSync(dir) || !fs23.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
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) => path19.resolve(f));
3661
- for (const f of files) if (!fs23.existsSync(f)) throw new UserError(`file not found: ${f}`);
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
- fs23.rmSync(runDir(meta.id), { recursive: true, force: true });
3797
+ fs24.rmSync(runDir(meta.id), { recursive: true, force: true });
3697
3798
  throw e;
3698
3799
  }
3699
3800
  }
3700
- fs23.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3801
+ fs24.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3701
3802
  `);
3702
- if (work.brief) fs23.writeFileSync(runFile(meta.id, "brief.md"), 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 (fs23.existsSync(from)) fs23.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
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 = path19.join(worktreesDir(), meta.id);
3896
+ meta.worktree = path20.join(worktreesDir(), meta.id);
3796
3897
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
3797
- meta.cwd = path19.join(meta.worktree, path19.relative(root, meta.dir));
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) => fs23.existsSync(f) ? fs23.readFileSync(f, "utf8") : "";
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
- fs23.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
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) fs23.rmSync(meta.packageFile, { force: true });
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
- fs23.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
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
- fs23.writeFileSync(runFile(meta.id, "verify.log"), output);
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 && fs23.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
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
- fs24.rmSync(packageFile, { force: true });
4095
+ fs25.rmSync(packageFile, { force: true });
3994
4096
  throw e;
3995
4097
  }
3996
- fs24.writeFileSync(runFile(meta.id, "package.md"), job.package);
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 ? fs24.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
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 ? "" : fs24.readFileSync(0, "utf8");
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
- fs24.writeFileSync(out, card(t, period));
4284
- console.log(`card written to ${path20.resolve(out)}`);
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(`![pitroom](${badgeUrl(t)})`);
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()}${fs24.existsSync(configPath()) ? "" : " (not present)"}`);
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
- fs24.rmSync(runDir(id), { recursive: true, force: true });
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 fs25 from "node:fs";
4440
+ import fs26 from "node:fs";
4339
4441
  import os9 from "node:os";
4340
- import path21 from "node:path";
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()}${fs25.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
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(`${path21.sep}.claude${path21.sep}`) && i.names.length);
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 (fs25.existsSync(launcher)) {
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 = path21.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4474
- const plugins = JSON.parse(fs25.readFileSync(f, "utf8")).plugins ?? {};
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 [path21.join(os9.homedir(), ".agents", "skills"), path21.join(os9.homedir(), ".claude", "skills")]) {
4492
- const dir = path21.join(base2, "using-superpowers");
4493
- if (fs25.existsSync(path21.join(dir, "SKILL.md"))) found.push(dir);
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 = path21.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4500
- return Object.keys(JSON.parse(fs25.readFileSync(f, "utf8")).plugins ?? {});
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 = path21.join(os9.homedir(), ".claude", "settings.json");
4508
- return JSON.parse(fs25.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
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 = fs25.readFileSync(path21.join(process.env.CODEX_HOME ?? path21.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
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 = path21.join(process.env.XDG_CONFIG_HOME ?? path21.join(os9.homedir(), ".config"), "opencode");
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 = fs25.readFileSync(path21.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
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 fs25.readdirSync(path21.join(dir, folder))) plugins.push(path21.join(dir, folder, f));
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.9",
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 (open it in a browser or the app's browser pane), or `pitroom watch -g NAME` in a terminal.
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 and give the user the address it prints: a live page of every run for the apps (Claude Code, Codex) that show neither Pitroom's cards nor its status line.
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.