pitroom 0.24.1 → 0.26.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 +29 -0
- package/README.md +19 -17
- package/dist/lib.mjs +84 -18
- package/dist/pitroom.mjs +236 -63
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +31 -31
- package/docs/backends.md +4 -1
- package/gemini-extension.json +1 -1
- package/package.json +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.
|
|
4
|
+
"version": "0.26.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TahaAtas"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pitroom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.26.0
|
|
4
|
+
|
|
5
|
+
### Workers
|
|
6
|
+
- **A limit part way no longer fails the run.** A worker that hit a plan's usage limit or a used-up quota after it had started (a Codex or Claude session limit, an Antigravity quota) failed the run, fallbacks or not: about a third of the runs that failed on a limit had a fallback that was never tried. Now the next worker takes the task over. An edit carries on in the same tree, told which files were already changed and to finish rather than start over; a read starts over. The run's cost is what both used, and the run's notes say who handed over.
|
|
7
|
+
- **Audits have fallbacks.** An audit ran on one auditor only, and lost the audit when that one was rate-limited. It now falls back to the other auditors (the audit and cheap tiers, the fallbacks, the worker), never to the worker it audits, at any effort level.
|
|
8
|
+
- **Cooldowns last as long as the limit.** "try again at 8:56 PM" and "resets 7:40pm (Europe/Istanbul)" are read as times of day (in the zone named, else the machine's), so an exhausted model is not tried again every 20 minutes until then. A used-up allowance with no time given (OpenCode's free usage limit) waits an hour instead of 20 minutes.
|
|
9
|
+
- **"usage limit" and "session limit" count as limits on every worker** (they did on Claude Code and Codex only).
|
|
10
|
+
- **`rankWorkers` ranks by the same kind of work.** A worker's record mixed its answers and its changes, though they part ways: in one machine's month of runs, `muse-spark` finished all 61 of its questions and 15 of its 19 changes, `space-bunny` 25 of 26 and 7 of 9. The chain is now ordered by the record of the run's kind (a question, a change or a review), and a change's record counts the reviews of it as its checks (spec met and nothing to fix), as an answer's counts its audits. The note says which record it used (`ranked by record on changes`).
|
|
11
|
+
|
|
12
|
+
### Windows
|
|
13
|
+
- **Git from Scoop gets the git shim.** Scoop's `shims\git.exe` is a launcher far from Git for Windows' `sh.exe`, so the shim (layer 1) was left out and only git's own config guarded the worker. Pitroom now follows the launcher's `git.shim` to the real git.
|
|
14
|
+
- **`HEAD^` from PowerShell is `HEAD^` again.** Windows starts a batch file such as `git.cmd` through a `cmd /c` line, which removes a `^` and fills in a `%name%` that names a variable before the batch file runs: from PowerShell, `git show HEAD^` showed `HEAD`, and `--format=%cd%n` got the current folder, where git.exe would have got them as written. When Windows starts `git.cmd` itself, the shim now takes the arguments from that raw line; a `cmd /d /s /c` line, which may hold more than one command, keeps `%*`. An unquoted `& | < >` is still acted on by that `cmd /c` line, as for any batch file.
|
|
15
|
+
|
|
16
|
+
### Savings
|
|
17
|
+
- **No finished run is missing from the savings ledger.** A run whose process was killed between its final record and its ledger line, and a worker that crashed, were never counted (about 1.5% of finished runs). `pitroom savings`, `pitroom history` and the dashboard now record the finished runs the ledger missed, each once.
|
|
18
|
+
- **A done run counts even when its CLI reported no steps** (some OpenCode reviews): it answered, so it is in "Finished". Runs that failed before reaching the model stay out, as before.
|
|
19
|
+
- **An estimated cost says so.** `pitroom history` has a COST column, and `pitroom savings --models` its cost: a cost the worker's CLI did not report (Codex, Gemini) and that was estimated from a price (your `workerPrices`, else the catalog's) reads `~$0.40`, a reported one `$0.40`, a free one `free`. `history --json` rows carry `cost` and `costEstimated`.
|
|
20
|
+
- **Prices from your agent**: `pitroom_info` has a `prices` topic: the primary's price the savings are measured against and the price each worker in use gets, or the `worker` named. `pitroom prices WORKER…` does the same on the command line, and now also prints the primary's price.
|
|
21
|
+
|
|
22
|
+
### Docs and tests
|
|
23
|
+
- **The review benchmark measures false alarms.** 16 new packages make a change that cannot alter what the code does (a comparison mirrored, `i < n` to `n > i`, only where it is the same expression; Pitroom's own also in its committed `dist/`), looking like the planted bugs. `big-pickle` raised no false alarm in 13 reviews; `nemotron-3-ultra` raised 4 in 14 and `mimo-v2.6-flash` 2 in 13, each filing a change it called harmless as Important for style. The README's table has the new column. `test/bench-equiv.test.mjs` checks the rule: what it rejects, and that each mirrored expression gives the same value for thousands of inputs.
|
|
24
|
+
- The benchmark's scorer read a review with no Critical heading (it went straight to Important) as having no findings: a few found bugs were counted as misses (`nemotron-3-ultra` 97% to 100%, `muse-spark` 93% to 97%, `longcat` 83% to 86%).
|
|
25
|
+
|
|
26
|
+
## 0.25.0
|
|
27
|
+
|
|
28
|
+
### Dashboard
|
|
29
|
+
- **"Finished" no longer goes down as you start runs.** It counted the finished runs among the ones on the page (the newest 40), so each new run pushed a finished one off and the number dropped. It now counts every finished run of the period, from the ledger the savings come from (a picked group's from its own runs); audits are left out, as in the savings.
|
|
30
|
+
- **A period on the Finished and Est. saved cards**: 24 hours, 7 days (the default), 30 days, 6 months or all time, picked on each card and remembered in the browser.
|
|
31
|
+
|
|
3
32
|
## 0.24.1
|
|
4
33
|
|
|
5
34
|
### Workers
|
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ In isolate mode nothing reaches your tree until you apply it.
|
|
|
107
107
|
<summary>12 more features</summary>
|
|
108
108
|
|
|
109
109
|
- **Receipts, not vibes.** Tokens the worker burned, tokens returned to your agent, compression ratio, and an estimate of what your primary model would have charged. `pitroom savings --card card.svg` makes a shareable card.
|
|
110
|
-
- **Self-healing worker chain.** Free models get rate-limited or retired. List fallback workers once (`fallback`) and a run that hits "model not found", 429 or quota errors moves to the next model automatically. Still no model hardcoded: the chain is yours.
|
|
110
|
+
- **Self-healing worker chain.** Free models get rate-limited or retired. List fallback workers once (`fallback`) and a run that hits "model not found", 429 or quota errors moves to the next model automatically, also when a plan's usage limit stops a worker part way (the next one finishes the task). Still no model hardcoded: the chain is yours.
|
|
111
111
|
- **A crew, not just one worker.** `pitroom crew` starts several workers at once, `pitroom watch --json` streams one line per change so your agent follows them live, `pitroom wait -g` collects every answer, `pitroom apply -g` lands isolated patches in order. Up to 20 workers run at once by default (`maxParallel`, 30 at most), the rest queue, and the fallback chain absorbs free-tier rate limits; parallel writers are refused.
|
|
112
112
|
- **Faster, not only cheaper.** Workers run in the background and side by side (up to 20 at once) while your agent keeps working, so slow reading and review stop blocking it. In the [benchmark](#benchmarks), 470 s of worker time finished in 156 s of wall clock, about 3× faster than one question after another, and your agent's own context stayed small.
|
|
113
113
|
- **Free first, dearer only when needed.** Runs and plan tasks start on the `cheap` tier, OpenCode's free model; `standard` (Codex) and `capable` (Claude Code) are for tasks that need them, and an optional `review` tier names who reviews. Gemini CLI with a free Google AI Studio key is one more cheap worker (small daily quota per model). `pitroom models` shows what each worker offers, the effort levels (`low` … `xhigh`) each model accepts, what you say it costs (`costs`) and what it used; `--effort` sets the level per run. Details: [Models, costs and effort](#models-costs-and-effort).
|
|
@@ -244,21 +244,23 @@ Only workers that ran are in the table. Models that could not answer at all (a p
|
|
|
244
244
|
</details>
|
|
245
245
|
|
|
246
246
|
<details>
|
|
247
|
-
<summary>Does a review find a planted bug?</summary>
|
|
247
|
+
<summary>Does a review find a planted bug, and leave a harmless change alone?</summary>
|
|
248
248
|
|
|
249
249
|
**Does a review find a planted bug?** A bug was changed into real code (an inverted check, a swapped `&&`/`||`, a flipped `return`) in React, Django, Kubernetes and Pitroom itself, committed as a bare "tidy", and `pitroom review` was asked to review the commit. 29 packages (14 with the bug alone, 15 with the bug and three comment-only edits around it). A review counts as a find when a Critical or Important finding names the changed file and either cites a line within 5 or names the changed identifier or its function; "exact line" is the strict version (within 3).
|
|
250
250
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
|
254
|
-
|
|
255
|
-
| OpenCode `
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
| OpenCode `big-pickle` |
|
|
259
|
-
| OpenCode `
|
|
251
|
+
**Does it leave a harmless change alone?** 16 more packages look the same (a comparison changed on one or two lines, the same "tidy" message) but cannot change what the code does: the two sides of a comparison are swapped and the operator mirrored (`i < n` becomes `n > i`), only where that is the same expression. A Critical or Important finding on one of them is a false alarm.
|
|
252
|
+
|
|
253
|
+
| Worker and model | Bug found | Exact line | False alarm | Reviews | Mean time | Slowest |
|
|
254
|
+
|---|---|---|---|---|---|---|
|
|
255
|
+
| OpenCode `nemotron-3-ultra-free` | 100% | 83% | 29% (4 of 14) | 29 + 14 | 1 min 33 s | 3 min 29 s |
|
|
256
|
+
| OpenCode `space-bunny-free` | 100% | 100% | - | 29 | 2 min 19 s | 5 min 46 s |
|
|
257
|
+
| Codex `gpt-6-sol` | 100% | 100% | - | 9 | 38 s | 52 s |
|
|
258
|
+
| OpenCode `big-pickle` | 97% | 90% | **0% (0 of 13)** | 29 + 13 | 1 min 50 s | 6 min 16 s |
|
|
259
|
+
| OpenCode `muse-spark-1.3-contributor-free` | 97% | 62% | - | 29 | 43 s | 2 min 0 s |
|
|
260
|
+
| OpenCode `mimo-v2.6-flash-free` | 90% | 90% | 15% (2 of 13) | 29 + 13 (4 timed out) | 4 min 33 s | 8 min 1 s |
|
|
261
|
+
| OpenCode `longcat-2.5-preview-free` | 86% | 86% | - | 29 (1 timed out) | 2 min 53 s | 8 min 0 s |
|
|
260
262
|
|
|
261
|
-
Almost every model found almost every bug, so
|
|
263
|
+
Almost every model found almost every bug, so finding them does not separate the models; leaving the harmless change alone does. `big-pickle` never raised a false alarm. `nemotron-3-ultra` and `mimo-v2.6-flash` each said, in their own words, that the change altered nothing, and still filed it as Important ("a Yoda condition, inconsistent with the rest of the file"), where the review template puts style under Minor; an agent that acts on Important findings would have reverted a harmless commit. `muse-spark` described the right bug but often quoted a wrong line number (62% exact). The models with a dash were not available any more, or out of quota, when the harmless packages were added; Codex `gpt-6-sol` reached its plan's usage limit after 9 reviews, and the others' left-out reviews hit the free tier's limit, so they are not counted as misses. Every change is only 2 to 5 lines, which is easy; the harmless edits are all of one kind (a mirrored comparison); and React, Django and Kubernetes may be in a model's training data. The harness, the rule that keeps an edit harmless (and its test) and every raw review are in [`benchmarks/review-bugs`](benchmarks/review-bugs).
|
|
262
264
|
|
|
263
265
|
</details>
|
|
264
266
|
|
|
@@ -276,7 +278,7 @@ To repeat it on your own repository: `pitroom crew -d <repo> -g bench "<question
|
|
|
276
278
|
|
|
277
279
|
macOS and Linux are the main platforms. **Windows** runs the same test suite in CI (Windows Server, Node 22 and 24, with the fake workers; the worker CLIs themselves are not run there), with these differences:
|
|
278
280
|
|
|
279
|
-
- **The git guard** is the same two layers as elsewhere, built on the `sh.exe` that Git for Windows ships: git's own config (a `reference-transaction` hook that refuses every ref update, and push URLs that go nowhere) is always on; the `git` shim (`git.cmd` for cmd.exe and PowerShell, `git` for Git Bash) adds friendly refusals and also blocks `add`, `restore`, `clean` and the rest. A worker that starts git without a shell (a plain `CreateProcess`) skips the shim, and is left with the config layer. `pitroom doctor` says which is in effect. It is verified in CI with Git for Windows, not yet with the worker CLIs on a real Windows machine.
|
|
281
|
+
- **The git guard** is the same two layers as elsewhere, built on the `sh.exe` that Git for Windows ships: git's own config (a `reference-transaction` hook that refuses every ref update, and push URLs that go nowhere) is always on; the `git` shim (`git.cmd` for cmd.exe and PowerShell, `git` for Git Bash) adds friendly refusals and also blocks `add`, `restore`, `clean` and the rest. A worker that starts git without a shell (a plain `CreateProcess`) skips the shim, and is left with the config layer. Git installed with Scoop works too (its `shims\git.exe` is followed to the real one, and so to its `sh.exe`). PowerShell starts `git.cmd` through a `cmd /c` line, which removes a `^` and fills in a `%name%` that names a variable before the shim runs; the shim takes the arguments from the raw line instead, so `git show HEAD^` shows the parent, not `HEAD`. That line still acts on an unquoted `& | < >` (as it does for any batch file): quote such an argument. `pitroom doctor` says which is in effect. It is verified in CI with Git for Windows, not yet with the worker CLIs on a real Windows machine.
|
|
280
282
|
- **The launcher is `pitroom.cmd`** (`%USERPROFILE%\.local\bin`), started with the Node that ran `pitroom install` or the one in `PITROOM_NODE`; it does not search for another Node.
|
|
281
283
|
- **Line endings.** Pitroom compares and applies patches byte for byte. Keep `core.autocrlf` off (or an `eol=lf` in `.gitattributes`, as this repository has); a checkout converted to CRLF has not been tested with `apply`.
|
|
282
284
|
- A read snapshot's files are not made read-only (Windows has no POSIX modes), and a `--verify` command runs in `cmd.exe`, which reports a missing command as `is not recognized`.
|
|
@@ -353,7 +355,7 @@ Each tool runs the matching `pitroom` command, so every rule of the CLI applies
|
|
|
353
355
|
- **Progress.** While a call waits for a run, a client that sent a progress token gets a `notifications/progress` every few seconds ("running · read · 12s · 3 steps, 2 tool calls · last: …"), and a client that resets its timeout when progress comes in (the protocol allows it; not every client does) does not give up on a long run. **Cancelling** a `pitroom_run`, `pitroom_review` or `pitroom_audit` call stops the run it started (a cancelled `pitroom_wait` only stops waiting).
|
|
354
356
|
- **Resources.** The latest runs are listed as `pitroom://run/<id>` (the report) and, for runs that changed files, `pitroom://run/<id>/patch` (the exact diff), so a client can attach one to a conversation. A client can **subscribe** to a run (`resources/subscribe`) and is told when it changes state (`notifications/resources/updated`), and every client is told when the newest run changes (`notifications/resources/list_changed`), so it need not poll; over HTTP these arrive on the session's event stream (a GET).
|
|
355
357
|
- **Prompts.** `research`, `implement`, `review` and `crew` say how to use Pitroom for that job (in Claude Code they appear as `/mcp__pitroom__research`, …), and every skill is a prompt of the same name (`using-pitroom`, `pitroom-research`, …), so a client without skills gets the same guidance. The server's instructions tell the agent to use the skills alongside the tools.
|
|
356
|
-
- **More than runs:** `pitroom_run` takes `tasks` (independent tasks in parallel, one worker each) and `continue` (a follow-up in the same worker session); `pitroom_info` reports without changing anything, by `topic`: `runs`, `history` (find an earlier answer before asking again), `stats` and `models` (pick a worker), `savings`, `cooldown`, `config`, `doctor`; `pitroom_stop` with `cooldowns` tries rate-limited models again.
|
|
358
|
+
- **More than runs:** `pitroom_run` takes `tasks` (independent tasks in parallel, one worker each) and `continue` (a follow-up in the same worker session); `pitroom_info` reports without changing anything, by `topic`: `runs`, `history` (find an earlier answer before asking again), `stats` and `models` (pick a worker), `savings`, `prices` (USD per 1M tokens of the primary and of each worker in use, or of the `worker` named), `cooldown`, `config`, `doctor`; `pitroom_stop` with `cooldowns` tries rate-limited models again.
|
|
357
359
|
- **Over HTTP.** `pitroom mcp --http [--port N]` (default 7117) serves the same tools at `http://127.0.0.1:7117/mcp` for clients that connect to a URL, for example `claude mcp add --transport http pitroom http://127.0.0.1:7117/mcp --header "Authorization: Bearer $(cat ~/.local/state/pitroom/mcp-token)"` (the command is printed when it starts). It is a local service: it listens on 127.0.0.1 only, needs the bearer token (made on first start, kept at `<state dir>/mcp-token` with owner-only permissions, or set with `PITROOM_MCP_TOKEN`), and refuses a Host or Origin that is not local, so a web page cannot use it. Anyone who has the token can run workers as you, so keep it private. It works in the directory it was started in (printed when it starts; `-d DIR` for another). Each client has its own session; a call that asks for progress is answered as an event stream. Ending a session or stopping the server ends the waiting, not the runs: collect them with `pitroom_wait` or the CLI. `pitroom install --mcp` registers the stdio command, which needs no token and no running process.
|
|
358
360
|
|
|
359
361
|
</details>
|
|
@@ -712,7 +714,7 @@ claude haiku low/medium/high/xhigh/max ? - -
|
|
|
712
714
|
|
|
713
715
|
Pitroom knows no vendor prices of its own and, unless you turn on `priceFeed` (see the config notes), fetches none, so a cost is what you enter: `"costs": {"codex:gpt-6-sol": 1, "codex:gpt-6.1-sol": 2}` in the config, in any unit (they are only compared; dollar prices for the receipts of Codex and Gemini go in `workerPrices`, see the config notes). `pitroom doctor` then prints the cost of the models in use and says when you priced a cheaper one of the same worker. A target that names only an effort (`codex:#low`) uses the model from `models`; a Codex or Claude Code worker with no pinned model gets a warning, because it would run the vendor's own default, which can change and cost more. `--effort LEVEL` sets the level for one run; your agent picks model and level from `pitroom models` (cheapest that fits: `low` for lookups, `medium` for ordinary changes, `high` for reviews).
|
|
714
716
|
|
|
715
|
-
Fallbacks cross backends (e.g. `"fallback": ["codex:#low", "opencode"]`): a worker that is rate-limited, logged out or missing its model hands the task to the next. Follow-ups (`--continue`) always stay on the worker that owns the session. Writing an adapter: [docs/backends.md](docs/backends.md). A model that says "rate limited" (a daily quota used up, an overloaded provider) is remembered for the time its message gives, or a guess: the next runs skip it while a fallback is left, so they do not each wait for it to fail. `pitroom cooldown` lists what is skipped and until when, `--clear` tries it again, and `doctor` shows it too.
|
|
717
|
+
Fallbacks cross backends (e.g. `"fallback": ["codex:#low", "opencode"]`): a worker that is rate-limited, logged out or missing its model hands the task to the next. A limit can come part way, after the worker started (a Codex or Claude plan's usage limit, an Antigravity quota): the next worker then takes the task over instead of the run failing. An edit (`-i`, `-w`) carries on in the same tree, and the next worker is told which files the one before it already changed and to finish, not start over; a read starts over. What both used is the run's cost. An audit falls back to the other auditors (the audit and cheap tiers, the fallbacks, the worker), never to the worker it audits. Follow-ups (`--continue`) always stay on the worker that owns the session. Writing an adapter: [docs/backends.md](docs/backends.md). A model that says "rate limited" (a daily quota used up, an overloaded provider) is remembered for the time its message gives ("retry in 4h", "try again at 8:56 PM", "resets 7:40pm (Europe/Istanbul)"), or a guess (an overload 20 minutes, a used-up allowance an hour, a daily quota four hours): the next runs skip it while a fallback is left, so they do not each wait for it to fail. `pitroom cooldown` lists what is skipped and until when, `--clear` tries it again, and `doctor` shows it too.
|
|
716
718
|
|
|
717
719
|
</details>
|
|
718
720
|
|
|
@@ -763,12 +765,12 @@ Or put defaults in `~/.config/pitroom/config.json` (flags and env still win); `p
|
|
|
763
765
|
- `audit`: the chance (0 to 1, default 0 = off) that a finished read run is re-checked in the background, see [Audits](#audits). The `audit` tier (else `cheap`) names who does it.
|
|
764
766
|
- `costs`: your relative cost per `worker:model`, only compared with each other; `maxParallel`: workers at once (default 20, at most 30).
|
|
765
767
|
- `workerPrices`: USD per 1M tokens of a worker model, for the CLIs that report no cost (Codex, Gemini): `"workerPrices": {"codex:gpt-6.1-sol": "1.25,10,0.125"}` is "in,out[,cachedIn]", keyed `backend:model` (the model that ran, else the one you asked for) or `backend` alone. Without one the receipt says `worker cost n/a` and the run counts as free in the savings, which overstates them (`pitroom doctor` warns); with one the cost is estimated from the tokens, shown as `~$0.016 (estimated from your workerPrices)`, kept in the ledger, history and dashboard, and taken off the savings. Pitroom knows no vendor prices and does not fetch them, so this is your number.
|
|
766
|
-
- `priceFeed`: keep a price catalog up to date (default `false`; `PITROOM_PRICE_FEED=1`). The catalog is the public [models.dev](https://models.dev) list OpenCode also reads (USD per 1M tokens of about 8,000 models): at most once a day (`priceFeedHours`, default 24), in a background process after a run, never while a run waits, Pitroom sends a plain `GET` for that one file (`priceFeedUrl` / `PITROOM_PRICE_FEED_URL` to use a mirror, `priceFeedHours` / `PITROOM_PRICE_FEED_HOURS` for the interval) and keeps a trimmed copy (about 400 KB) in its state directory; a failed refresh keeps the old copy and is not retried for an hour. It is only a fallback: your `workerPrices` come first, and the receipt says which was used (`estimated from models.dev prices`). With it, `"primary": "<model id>"` can name any model of the catalog for the savings comparison (`"primary": "claude-opus-5-5"`), where without it only the four presets exist. `pitroom prices` shows the feed, the catalog and the price each worker in use gets; `pitroom prices --refresh` fetches now. What it sends: [PRIVACY.md](PRIVACY.md). The listed prices are API list prices: a worker on a subscription (Codex with a ChatGPT account) does not pay them, so for such a worker the estimate is what the same tokens would cost on the API.
|
|
768
|
+
- `priceFeed`: keep a price catalog up to date (default `false`; `PITROOM_PRICE_FEED=1`). The catalog is the public [models.dev](https://models.dev) list OpenCode also reads (USD per 1M tokens of about 8,000 models): at most once a day (`priceFeedHours`, default 24), in a background process after a run, never while a run waits, Pitroom sends a plain `GET` for that one file (`priceFeedUrl` / `PITROOM_PRICE_FEED_URL` to use a mirror, `priceFeedHours` / `PITROOM_PRICE_FEED_HOURS` for the interval) and keeps a trimmed copy (about 400 KB) in its state directory; a failed refresh keeps the old copy and is not retried for an hour. It is only a fallback: your `workerPrices` come first, and the receipt says which was used (`estimated from models.dev prices`). With it, `"primary": "<model id>"` can name any model of the catalog for the savings comparison (`"primary": "claude-opus-5-5"`), where without it only the four presets exist. `pitroom prices` shows the feed, the catalog, the primary's price and the price each worker in use gets (`pitroom prices codex:gpt-6.1-sol` the price of the one named); `pitroom prices --refresh` fetches now. What it sends: [PRIVACY.md](PRIVACY.md). The listed prices are API list prices: a worker on a subscription (Codex with a ChatGPT account) does not pay them, so for such a worker the estimate is what the same tokens would cost on the API.
|
|
767
769
|
- `notify`: a desktop notification when a run that went to the background (`--bg`, a crew, an MCP call) has ended, so a long job does not have to be watched (default `false`; `PITROOM_NOTIFY=1`). Runs that went well but took less than `notifyAfter` seconds (default 15; `PITROOM_NOTIFY_AFTER`) and audits are not announced (a failure always is), and a crew says so once, when its last run ends, judged as a whole. A worker process that is killed outright never gets to say so. The built-in notifier uses `osascript` on macOS and `notify-send` on Linux (none for Windows yet); `notifyCommand` runs a command of your own instead, in a shell, with the text in `PITROOM_NOTIFY_TITLE` and `PITROOM_NOTIFY_BODY` (and `PITROOM_NOTIFY_RUN`, `PITROOM_NOTIFY_STATE`): for example `"notifyCommand": "curl -s -d \"$PITROOM_NOTIFY_BODY\" ntfy.sh/my-topic"`.
|
|
768
770
|
- `mcpDash`: whether a run that an MCP client starts, and that is still going after a few seconds, also starts the dashboard and returns its address (default `true`; `PITROOM_MCP_DASH=0` or `"mcpDash": false` turns it off), see [the MCP server](#1-install-pick-your-agent).
|
|
769
771
|
- `auditFix` and `auditFocus`: send a disputed answer back to its worker for one correction, and lean the audit sample to list and count questions and to disputed workers (both default `false`; `PITROOM_AUDIT_FIX`, `PITROOM_AUDIT_FOCUS`), see [Audits](#audits).
|
|
770
772
|
- `budgetDaily`: what the workers may cost per day, in USD (as their CLIs report it, or estimated from `workerPrices` and the price catalog; audits and corrections included). Past 80% a run warns; once it is spent only workers that cost nothing still run (a price of `0`, or a worker whose recent runs all cost exactly 0), a run with none in its chain is refused (exit 3), and audits and corrections do not start. A run that is still going counts with what it has used so far; runs started at the same moment count from their first finished step, so a crew can still end a day a little over. `0` allows only free workers. Default: no budget (`PITROOM_BUDGET_DAILY`).
|
|
771
|
-
- `rankWorkers`: order the worker and its fallbacks by their record of the last 30 days: the share of runs that ended well (rate limits left out), times the share of
|
|
773
|
+
- `rankWorkers`: order the worker and its fallbacks by their record of the last 30 days on the same kind of work (a question, a change or a review: a model can answer well and still leave changes unfinished): the share of runs that ended well (rate limits left out), times the share of checked work that held up, with 3 checks or more (for an answer its audits, for a change the reviews of it that found the spec met and nothing to fix). Only workers with 5 runs or more of that kind move, among the places such workers hold, in steps of 10%; a worker you name (`-W`, `-m`, a tier) is never reordered, and the run says when the first worker changed (default `false`; `PITROOM_RANK_WORKERS=1`).
|
|
772
774
|
- `countRateLimits`: whether runs that failed on a rate limit or quota count in `pitroom history stats` and the dashboard's Stats (default `false`: they are left out of the run counts, success rates, times and tokens and shown apart as rate-limited, since they say nothing about the worker's work; `true` counts them as not ok).
|
|
773
775
|
|
|
774
776
|
</details>
|
package/dist/lib.mjs
CHANGED
|
@@ -759,7 +759,7 @@ function invocation(req) {
|
|
|
759
759
|
return { command, args: [...prefix, ...args], env };
|
|
760
760
|
}
|
|
761
761
|
function classify(message) {
|
|
762
|
-
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
|
|
762
|
+
if (/usage limit|session limit|RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
|
|
763
763
|
if (/not (signed|logged) in|\bsign.?in\b|\blog ?in\b|authenticat|PERMISSION_DENIED|oauth|credential|keyring|keychain|\b401\b|unauthori[sz]ed/i.test(message)) {
|
|
764
764
|
return "auth";
|
|
765
765
|
}
|
|
@@ -997,7 +997,7 @@ function classify2(message) {
|
|
|
997
997
|
if (/authentication_failed|failed to authenticate|oauth|not logged in|\/login|invalid api key|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
|
|
998
998
|
return "auth";
|
|
999
999
|
}
|
|
1000
|
-
if (/rate.?limit|too many requests|\b429\b|\b529\b|overloaded|
|
|
1000
|
+
if (/usage limit|session limit|limit reached|rate.?limit|too many requests|\b429\b|\b529\b|overloaded|quota|capacity/i.test(message)) return "rate-limited";
|
|
1001
1001
|
if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
|
|
1002
1002
|
return "other";
|
|
1003
1003
|
}
|
|
@@ -1198,7 +1198,7 @@ function classify3(message) {
|
|
|
1198
1198
|
if (/not supported|model[^.]*(not found|does not exist|unknown|unavailable)|unsupported model|no such model/i.test(message)) {
|
|
1199
1199
|
return "model-unavailable";
|
|
1200
1200
|
}
|
|
1201
|
-
if (/rate.?limit|too many requests|\b429\b|
|
|
1201
|
+
if (/usage limit|session limit|rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|capacity/i.test(message)) return "rate-limited";
|
|
1202
1202
|
if (/unauthori[sz]ed|\b40[13]\b|not logged in|log ?in|api.?key|credits?\b|billing|subscription/i.test(message)) return "auth";
|
|
1203
1203
|
return "other";
|
|
1204
1204
|
}
|
|
@@ -1436,7 +1436,7 @@ function classify4(message) {
|
|
|
1436
1436
|
if (/IneligibleTier|no longer supported for Gemini|error authenticating|not logged in|please (log ?in|sign in)|credentials|api key|UNAUTHENTICATED|PERMISSION_DENIED|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
|
|
1437
1437
|
return "auth";
|
|
1438
1438
|
}
|
|
1439
|
-
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
|
|
1439
|
+
if (/usage limit|session limit|RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
|
|
1440
1440
|
if (/model.{0,80}(not found|not available|no longer available|does not exist|unsupported|invalid)|\b404\b|NOT_FOUND|invalid model|is not supported/i.test(message)) return "model-unavailable";
|
|
1441
1441
|
return "other";
|
|
1442
1442
|
}
|
|
@@ -1880,7 +1880,7 @@ function majorVersion(output) {
|
|
|
1880
1880
|
}
|
|
1881
1881
|
function classify5(message) {
|
|
1882
1882
|
if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
|
|
1883
|
-
if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
|
|
1883
|
+
if (/usage limit|session limit|rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
|
|
1884
1884
|
if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
|
|
1885
1885
|
return "auth";
|
|
1886
1886
|
}
|
|
@@ -2225,7 +2225,7 @@ function invocation7(req) {
|
|
|
2225
2225
|
}
|
|
2226
2226
|
function classify6(message) {
|
|
2227
2227
|
if (/\b40[13]\b|incorrect api key|invalid api key|unauthori[sz]ed|authenticat|not logged in|no auth/i.test(message)) return "auth";
|
|
2228
|
-
if (
|
|
2228
|
+
if (/usage limit|session limit|\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
|
|
2229
2229
|
if (/model[^.]*(not found|does not exist|not available|not supported)|invalid model|model_not_found|\b404\b/i.test(message)) return "model-unavailable";
|
|
2230
2230
|
return "other";
|
|
2231
2231
|
}
|
|
@@ -2493,6 +2493,7 @@ function fill(template, values) {
|
|
|
2493
2493
|
// src/core/cooldown.ts
|
|
2494
2494
|
var MAX_MS = 24 * 36e5;
|
|
2495
2495
|
var OVERLOAD_MS = 20 * 6e4;
|
|
2496
|
+
var USAGE_MS = 36e5;
|
|
2496
2497
|
var DAILY_MS = 4 * 36e5;
|
|
2497
2498
|
function retryAfterMs(message) {
|
|
2498
2499
|
const compact2 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
|
|
@@ -2505,10 +2506,41 @@ function retryAfterMs(message) {
|
|
|
2505
2506
|
}
|
|
2506
2507
|
return ms > 0 ? ms : void 0;
|
|
2507
2508
|
}
|
|
2508
|
-
function
|
|
2509
|
-
const
|
|
2509
|
+
function wallClock(at, timeZone) {
|
|
2510
|
+
const p = Object.fromEntries(
|
|
2511
|
+
new Intl.DateTimeFormat("en-US", { timeZone, hourCycle: "h23", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric" }).formatToParts(at).map((x) => [x.type, Number(x.value)])
|
|
2512
|
+
);
|
|
2513
|
+
return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
2514
|
+
}
|
|
2515
|
+
function retryAtMs(message, now = Date.now()) {
|
|
2516
|
+
const m = /(?:try again at|resets?(?: at)?)\s+(\d{1,2})(?::(\d{2}))?\s*([ap])\.?m\b\.?(?:\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)*)\))?/i.exec(message) ?? /(?:try again at|resets?(?: at)?)\s+(\d{1,2}):(\d{2})()(?!\d)(?:\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)*)\))?/i.exec(message);
|
|
2517
|
+
if (!m) return void 0;
|
|
2518
|
+
let hour = Number(m[1]);
|
|
2519
|
+
const minute = Number(m[2] ?? 0);
|
|
2520
|
+
const half = m[3]?.toLowerCase();
|
|
2521
|
+
if (minute > 59 || (half ? hour < 1 || hour > 12 : hour > 23)) return void 0;
|
|
2522
|
+
if (half === "p" && hour < 12) hour += 12;
|
|
2523
|
+
if (half === "a" && hour === 12) hour = 0;
|
|
2524
|
+
let zone = m[4];
|
|
2525
|
+
try {
|
|
2526
|
+
if (zone) new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
2527
|
+
} catch {
|
|
2528
|
+
zone = void 0;
|
|
2529
|
+
}
|
|
2530
|
+
const wall = wallClock(now, zone);
|
|
2531
|
+
for (const day of [0, 1]) {
|
|
2532
|
+
const d = new Date(wall);
|
|
2533
|
+
const target = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + day, hour, minute);
|
|
2534
|
+
const at = target - (wallClock(target, zone) - target);
|
|
2535
|
+
if (at > now) return at - now;
|
|
2536
|
+
}
|
|
2537
|
+
return void 0;
|
|
2538
|
+
}
|
|
2539
|
+
function cooldownMs(message, now = Date.now()) {
|
|
2540
|
+
const told = retryAfterMs(message) ?? retryAtMs(message, now);
|
|
2510
2541
|
if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
|
|
2511
|
-
|
|
2542
|
+
if (/daily|per.?day|per day|exhausted your/i.test(message)) return DAILY_MS;
|
|
2543
|
+
return /usage limit|session limit|quota|FreeUsageLimit/i.test(message) ? USAGE_MS : OVERLOAD_MS;
|
|
2512
2544
|
}
|
|
2513
2545
|
|
|
2514
2546
|
// src/core/answers.ts
|
|
@@ -2722,7 +2754,7 @@ function scoreOf(r) {
|
|
|
2722
2754
|
return r.audited >= MIN_AUDITS ? ok * (r.agreed / r.audited) : ok;
|
|
2723
2755
|
}
|
|
2724
2756
|
var pct = (x) => `${Math.round(x * 100)}%`;
|
|
2725
|
-
function rankChain(chain, recordOf) {
|
|
2757
|
+
function rankChain(chain, recordOf, work) {
|
|
2726
2758
|
const scored = chain.map((t, i) => ({ t, i, score: scoreOf(recordOf(t)) }));
|
|
2727
2759
|
const slots = scored.filter((s) => s.score !== void 0);
|
|
2728
2760
|
if (slots.length < 2) return { chain };
|
|
@@ -2733,7 +2765,7 @@ function rankChain(chain, recordOf) {
|
|
|
2733
2765
|
const by = (t) => scored.find((s) => s.t === t).score;
|
|
2734
2766
|
return {
|
|
2735
2767
|
chain: out,
|
|
2736
|
-
note: `ranked by record ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
2768
|
+
note: `ranked by record${work ? ` on ${work}` : ""} ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
2737
2769
|
};
|
|
2738
2770
|
}
|
|
2739
2771
|
|
|
@@ -2821,11 +2853,16 @@ function savedUsd(usage, returnedTokens, price = primaryPrice()) {
|
|
|
2821
2853
|
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2822
2854
|
return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
|
|
2823
2855
|
}
|
|
2856
|
+
var SETTLING_MS = 10 * 6e4;
|
|
2824
2857
|
function readLedger(sinceMs) {
|
|
2825
2858
|
if (!fs16.existsSync(ledgerFile())) return [];
|
|
2859
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2826
2860
|
return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2827
2861
|
try {
|
|
2828
|
-
|
|
2862
|
+
const e = JSON.parse(l);
|
|
2863
|
+
if (seen.has(e.id)) return [];
|
|
2864
|
+
seen.add(e.id);
|
|
2865
|
+
return [e];
|
|
2829
2866
|
} catch {
|
|
2830
2867
|
return [];
|
|
2831
2868
|
}
|
|
@@ -3199,6 +3236,15 @@ exec "$real" "$@"
|
|
|
3199
3236
|
function shimDir() {
|
|
3200
3237
|
return path18.join(home(), "shim", `v${VERSION}`);
|
|
3201
3238
|
}
|
|
3239
|
+
function scoopTarget(exe) {
|
|
3240
|
+
try {
|
|
3241
|
+
const text = fs19.readFileSync(exe.replace(/\.exe$/i, ".shim"), "utf8");
|
|
3242
|
+
const target = /^\s*path\s*=\s*"?([^"\r\n]+?)"?\s*$/m.exec(text)?.[1];
|
|
3243
|
+
return target && fs19.statSync(target).isFile() ? target : void 0;
|
|
3244
|
+
} catch {
|
|
3245
|
+
return void 0;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3202
3248
|
function findRealGit(skip) {
|
|
3203
3249
|
const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
3204
3250
|
for (const dir of (process.env.PATH ?? "").split(path18.delimiter).filter(Boolean)) {
|
|
@@ -3206,7 +3252,7 @@ function findRealGit(skip) {
|
|
|
3206
3252
|
const p = path18.join(dir, WINDOWS ? "git.exe" : "git");
|
|
3207
3253
|
try {
|
|
3208
3254
|
fs19.accessSync(p, fs19.constants.X_OK);
|
|
3209
|
-
if (fs19.statSync(p).isFile()) return p;
|
|
3255
|
+
if (fs19.statSync(p).isFile()) return WINDOWS && scoopTarget(p) || p;
|
|
3210
3256
|
} catch {
|
|
3211
3257
|
}
|
|
3212
3258
|
}
|
|
@@ -3225,11 +3271,28 @@ function findGitSh(realGit) {
|
|
|
3225
3271
|
}
|
|
3226
3272
|
return void 0;
|
|
3227
3273
|
}
|
|
3228
|
-
var cmdShim = (sh) =>
|
|
3229
|
-
|
|
3230
|
-
"
|
|
3231
|
-
|
|
3232
|
-
|
|
3274
|
+
var cmdShim = (sh) => [
|
|
3275
|
+
"@echo off",
|
|
3276
|
+
"setlocal EnableExtensions DisableDelayedExpansion",
|
|
3277
|
+
'set "MSYS2_ARG_CONV_EXCL=*"',
|
|
3278
|
+
'set "PITROOM_SELF=%~f0"',
|
|
3279
|
+
'set q=^"',
|
|
3280
|
+
"setlocal EnableDelayedExpansion",
|
|
3281
|
+
'set "line=!cmdcmdline!"',
|
|
3282
|
+
'if not "!line: /d =!"=="!line!" goto plain',
|
|
3283
|
+
'if not "!line: /s =!"=="!line!" goto plain',
|
|
3284
|
+
'set "tail=!line:*/c ""%PITROOM_SELF%"=!"',
|
|
3285
|
+
'if "!tail!"=="!line!" set "tail=!line:*/c "%PITROOM_SELF%=!"',
|
|
3286
|
+
'if "!tail!"=="!line!" goto plain',
|
|
3287
|
+
'if "!tail:~-1!"=="!q!" set "tail=!tail:~0,-1!"',
|
|
3288
|
+
`"${sh}" "%~dp0git" !tail!`,
|
|
3289
|
+
"exit /b !errorlevel!",
|
|
3290
|
+
":plain",
|
|
3291
|
+
"endlocal",
|
|
3292
|
+
`"${sh}" "%~dp0git" %*`,
|
|
3293
|
+
"exit /b %errorlevel%",
|
|
3294
|
+
""
|
|
3295
|
+
].join("\r\n");
|
|
3233
3296
|
var HOOK_VERSION = 1;
|
|
3234
3297
|
var REF_HOOK = `#!/bin/sh
|
|
3235
3298
|
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
@@ -3431,6 +3494,7 @@ export {
|
|
|
3431
3494
|
brief,
|
|
3432
3495
|
card,
|
|
3433
3496
|
catalogPrice,
|
|
3497
|
+
cmdShim,
|
|
3434
3498
|
compact,
|
|
3435
3499
|
cooldownMs,
|
|
3436
3500
|
costAt,
|
|
@@ -3475,11 +3539,13 @@ export {
|
|
|
3475
3539
|
requestStop,
|
|
3476
3540
|
resolveRun,
|
|
3477
3541
|
retryAfterMs,
|
|
3542
|
+
retryAtMs,
|
|
3478
3543
|
retryCommands,
|
|
3479
3544
|
runDir,
|
|
3480
3545
|
runFile,
|
|
3481
3546
|
runsDir,
|
|
3482
3547
|
savedUsd,
|
|
3548
|
+
scoopTarget,
|
|
3483
3549
|
scoreAnswer,
|
|
3484
3550
|
scoreOf,
|
|
3485
3551
|
secretWarning,
|