pitroom 0.26.0 → 0.27.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 +19 -0
- package/README.md +9 -8
- package/dist/lib.mjs +122 -14
- package/dist/pitroom.mjs +537 -385
- package/docs/backends.md +5 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/skills/pitroom-driven-development/re-review-prompt.md +2 -2
- package/skills/pitroom-driven-development/task-reviewer-prompt.md +2 -2
- package/skills/pitroom-review/code-reviewer.md +2 -2
|
@@ -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.27.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.27.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,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.27.0
|
|
4
|
+
|
|
5
|
+
### Workers
|
|
6
|
+
- **Antigravity edits work again with agy 1.4.** agy 1.4 asks for a `write_file` grant before an edit in the workspace, `--mode accept-edits` or not, and a headless run cannot ask: every `-i` and `-w` run with Antigravity ended without an answer ("a tool required the write_file permission"). Edit runs now grant writes in their own workspace; read runs still may not write, and the workspace's secret files stay closed.
|
|
7
|
+
- **Antigravity is out of beta on macOS.** `scripts/check-worker.mjs` held in all eight checks with agy 1.4.7 there; on Linux and Windows, where it has not been run yet, it stays beta, and `pitroom doctor` says so there.
|
|
8
|
+
- **`node scripts/check-worker.mjs <worker>`** tries a worker CLI the way Pitroom uses it (a read, a follow-up, a read outside the project, a shell command, an isolated edit, a commit the git guard refuses, an unknown model) in a scratch repository with a Pitroom state folder of its own, and prints what held: for a platform a worker has not been tried on. It found the agy 1.4 change above.
|
|
9
|
+
- **`autoFallback`** (off by default): with no `fallback` list, a run falls back to the other workers your config already names (tiers, `models`, `worker`), the installed ones, cheapest first by `costs`, at most four. It never adds a model you did not name somewhere, nor the worker itself at another effort level; `pitroom doctor` shows the chain and says where it came from.
|
|
10
|
+
|
|
11
|
+
### Reviews
|
|
12
|
+
- **A review's file:line is the file's again.** A review package shows the change as a diff, and a reviewer that reads files with a line-numbered tool saw the package's own line numbers: Claude Code cited those for a third of the references in its task reviews and half of those in its branch reviews (`browser-observation.ts:177-250` for a file of 75 lines), so the reference check failed answers that were right. Every line of a hunk in the package now starts with its line number in the file after the change (`+ 99| new`), and all three review templates say to cite those (the branch review's said nothing about it).
|
|
13
|
+
- **The reference check finds more of the files an answer means.** A path with a folder left out (`api-contracts/integrations.ts` for `packages/api-contracts/src/integrations.ts`) is matched when the folders written appear in the file's path in order, and a file only a dependency or the build output has (`streamableHttp.d.ts:73`, under `node_modules`) is looked for there when the project has none of that name; the lines and the symbol are still checked. On one machine's past answers, checked again against today's files, a quarter of OpenCode's failed references and a sixth of Claude's held up.
|
|
14
|
+
|
|
15
|
+
### Windows
|
|
16
|
+
- **The git shim is a `git.exe` now**, built on the spot with the C# compiler that ships with Windows (.NET Framework 4). It hands its command line to `sh.exe` unread, so every argument arrives as written, an unquoted `& | < >` from PowerShell too (the `cmd /c` line a batch file is started with acted on those), and a program that starts git without a shell no longer skips the shim. Without a compiler, `git.cmd` stays the door, and `pitroom doctor` says so.
|
|
17
|
+
|
|
18
|
+
### Docs and tests
|
|
19
|
+
- `benchmarks/outcome-prediction` again, on four weeks of runs (1,455), with two more labels: a review's verdict on a change, and whether an answer's references check out. Who answers predicts the latter well (AUC 0.85), but the label measures the reference checker too; failures are still about providers, and audit and review verdicts are still too few. No learned model yet.
|
|
20
|
+
- The review benchmark's harmless packages are reviewed in full: `big-pickle` raised no false alarm in 16, `nemotron-3-ultra` 4 and `mimo-v2.6-flash` 2.
|
|
21
|
+
|
|
3
22
|
## 0.26.0
|
|
4
23
|
|
|
5
24
|
### Workers
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Pitroom uses your worker CLI's default model: a free tier, a local MLX/Ollama mo
|
|
|
72
72
|
|
|
73
73
|
### Any worker CLI
|
|
74
74
|
|
|
75
|
-
OpenCode, Codex CLI, Claude Code, Gemini CLI, Qwen Code, Kilo Code and Antigravity CLI (beta).
|
|
75
|
+
OpenCode, Codex CLI, Claude Code, Gemini CLI, Qwen Code, Kilo Code and Antigravity CLI (beta on Linux and Windows).
|
|
76
76
|
|
|
77
77
|
One group can mix them, and a fallback chain can cross them ([how adapters work](docs/backends.md)).
|
|
78
78
|
|
|
@@ -252,12 +252,12 @@ Only workers that ran are in the table. Models that could not answer at all (a p
|
|
|
252
252
|
|
|
253
253
|
| Worker and model | Bug found | Exact line | False alarm | Reviews | Mean time | Slowest |
|
|
254
254
|
|---|---|---|---|---|---|---|
|
|
255
|
-
| OpenCode `nemotron-3-ultra-free` | 100% | 83% |
|
|
255
|
+
| OpenCode `nemotron-3-ultra-free` | 100% | 83% | 25% (4 of 16) | 29 + 16 | 1 min 30 s | 3 min 29 s |
|
|
256
256
|
| OpenCode `space-bunny-free` | 100% | 100% | - | 29 | 2 min 19 s | 5 min 46 s |
|
|
257
257
|
| Codex `gpt-6-sol` | 100% | 100% | - | 9 | 38 s | 52 s |
|
|
258
|
-
| OpenCode `big-pickle` | 97% | 90% | **0% (0 of
|
|
258
|
+
| OpenCode `big-pickle` | 97% | 90% | **0% (0 of 16)** | 29 + 16 | 1 min 45 s | 6 min 16 s |
|
|
259
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% |
|
|
260
|
+
| OpenCode `mimo-v2.6-flash-free` | 90% | 90% | 13% (2 of 16) | 29 + 16 (4 timed out) | 4 min 36 s | 8 min 1 s |
|
|
261
261
|
| OpenCode `longcat-2.5-preview-free` | 86% | 86% | - | 29 (1 timed out) | 2 min 53 s | 8 min 0 s |
|
|
262
262
|
|
|
263
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).
|
|
@@ -278,14 +278,14 @@ To repeat it on your own repository: `pitroom crew -d <repo> -g bench "<question
|
|
|
278
278
|
|
|
279
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:
|
|
280
280
|
|
|
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
|
|
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 adds friendly refusals and also blocks `add`, `restore`, `clean` and the rest. Its doors are `git.exe`, a small launcher Pitroom builds with the C# compiler that ships with Windows (.NET Framework 4), for every program that starts git, with a shell or without one; `git` for Git Bash; and `git.cmd` where no compiler is found. `git.exe` hands its command line to `sh.exe` unread, so arguments arrive as written (`HEAD^`, `%cd%`, an unquoted `&`). Through `git.cmd` alone, a program that starts git without a shell skips the shim and is left with the config layer, and the `cmd /c` line Windows starts a batch file with still acts on an unquoted `& | < >` (a `^` and a `%name%` are kept: the shim reads the raw line). Git installed with Scoop works too (its `shims\git.exe` is followed to the real one, and so to its `sh.exe`). `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.
|
|
282
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.
|
|
283
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`.
|
|
284
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`.
|
|
285
285
|
|
|
286
286
|
## Quick start
|
|
287
287
|
|
|
288
|
-
You need Node.js 22.13+ and at least one worker CLI: [OpenCode](https://opencode.ai) v2+ (the default worker, with free models), [Codex CLI](https://github.com/openai/codex), [Claude Code](https://claude.com/claude-code) [Gemini CLI](https://github.com/google-gemini/gemini-cli) (beta; it needs an API key from [Google AI Studio](https://aistudio.google.com/apikey), a Google account sign-in no longer works for it) [Qwen Code](https://github.com/QwenLM/qwen-code) (beta; any OpenAI-compatible provider, a DashScope key, or a local model) [Kilo Code](https://kilo.ai)'s CLI (beta; `npm i -g @kilocode/cli`, its 500+ models and providers) or the [Antigravity CLI](https://antigravity.google) (
|
|
288
|
+
You need Node.js 22.13+ and at least one worker CLI: [OpenCode](https://opencode.ai) v2+ (the default worker, with free models), [Codex CLI](https://github.com/openai/codex), [Claude Code](https://claude.com/claude-code) [Gemini CLI](https://github.com/google-gemini/gemini-cli) (beta; it needs an API key from [Google AI Studio](https://aistudio.google.com/apikey), a Google account sign-in no longer works for it) [Qwen Code](https://github.com/QwenLM/qwen-code) (beta; any OpenAI-compatible provider, a DashScope key, or a local model) [Kilo Code](https://kilo.ai)'s CLI (beta; `npm i -g @kilocode/cli`, its 500+ models and providers) or the [Antigravity CLI](https://antigravity.google) (`agy`, signed in with your Google account; beta on Linux and Windows).
|
|
289
289
|
|
|
290
290
|
### 1. Install: pick your agent
|
|
291
291
|
|
|
@@ -684,7 +684,7 @@ pitroom run -W claude:haiku "…" # Claude Code on a ch
|
|
|
684
684
|
pitroom run -W gemini:gemini-3.8-flash "…" # Gemini CLI (beta; needs a Google AI Studio API key)
|
|
685
685
|
pitroom run -W qwen:qwen3-coder-plus "…" # Qwen Code (beta; your provider from ~/.qwen/settings.json)
|
|
686
686
|
pitroom run -W kilo:kilo/<model> "…" # Kilo Code's CLI (beta; any model from `kilo models`)
|
|
687
|
-
pitroom run -i -W antigravity "…" # Antigravity CLI (
|
|
687
|
+
pitroom run -i -W antigravity "…" # Antigravity CLI (your Google account, models from `agy models`; beta on Linux and Windows)
|
|
688
688
|
pitroom run -m nvidia/z-ai/glm-5.3 "…" # another model on the preferred worker
|
|
689
689
|
```
|
|
690
690
|
|
|
@@ -696,7 +696,7 @@ pitroom run -m nvidia/z-ai/glm-5.3 "…" # another model on th
|
|
|
696
696
|
| Gemini CLI | beta | `--approval-mode plan` plus Pitroom's policy rules | tokens only | an API key from Google AI Studio (sign in with `gemini` or set `GEMINI_API_KEY`; Google account sign-in is refused by Google); workers run in a private Gemini home without your hooks, MCP servers and skills; secret-file rules are not enforced by Gemini 0.62, but a read run with secret-looking files reads a [clean snapshot](#modes) without them; pin a model, e.g. `-W gemini:gemini-3.8-flash`; `--continue` works (by session id; see [the notes](docs/backends.md#gemini-cli-notes)) |
|
|
697
697
|
| Qwen Code | beta | `--approval-mode plan`; edits only inside the working directory, the rest declined | tokens only | checked against Qwen Code 0.25 with a scripted model, not yet with a real provider; uses your sign-in and model from `~/.qwen/settings.json` (any OpenAI-compatible provider, DashScope, a local model); `--safe-mode` keeps your QWEN.md, hooks, skills and MCP servers out and stops the CLI writing memories of the run; reads `.env` if asked, so a read run with secret-looking files reads a [clean snapshot](#modes); see [the notes](docs/backends.md#qwen-code-notes) |
|
|
698
698
|
| Kilo Code | beta | per-run permission rules (OpenCode's profiles) | reported | the CLI of `@kilocode/cli` (an OpenCode fork): the same `pitroom-read`/`pitroom-write` profiles through `KILO_CONFIG_CONTENT`, your sign-in and providers as they are; workers use a private state folder so a running `kilo daemon` (which would ignore the profiles) is never attached to; checked against Kilo 7.8.8 with a scripted model, not yet with a real provider; see [the notes](docs/backends.md#kilo-code-notes) |
|
|
699
|
-
| Antigravity CLI | beta | a read-only [snapshot](#modes) of its own for every read run (agy has no read-only mode for headless runs) | tokens only | checked against agy 1.
|
|
699
|
+
| Antigravity CLI | macOS; beta on Linux and Windows | a read-only [snapshot](#modes) of its own for every read run (agy has no read-only mode for headless runs) | tokens only | checked against agy 1.4.7 on macOS (`scripts/check-worker.mjs`), not yet on Linux or Windows; use `-i` for changes, not `-w` (its deny rules are patterns, a layer on top of the git guard and the isolated copy); runs with a home of its own per workspace (agy reads its settings only from `~/.gemini`), so its commands do not see your dotfiles; an answerless run (a refused command can end its turn with no answer and exit 0) is a failure; your Google account sign-in (the one Gemini CLI now refuses); shell in agy's terminal sandbox (macOS, Linux); no web access for these workers; give it a fallback, its quota is per account; see [the notes](docs/backends.md#antigravity-cli-notes) |
|
|
700
700
|
|
|
701
701
|
### Models, costs and effort
|
|
702
702
|
|
|
@@ -771,6 +771,7 @@ Or put defaults in `~/.config/pitroom/config.json` (flags and env still win); `p
|
|
|
771
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).
|
|
772
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`).
|
|
773
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`).
|
|
774
|
+
- `autoFallback`: with no `fallback` of yours, fall back to the other workers your config already names (its `tiers`, its `models` and its `worker`), the ones installed here, cheapest first by `costs` (those without a cost after, in the order named), at most four; never the worker itself at another effort level (default `false`; `PITROOM_AUTO_FALLBACK=1`). Pitroom still picks no model you did not name: it only reuses your own choices. Your `fallback` list, when you have one, and `--no-fallback` win; `pitroom doctor` says when the chain came from it.
|
|
774
775
|
- `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).
|
|
775
776
|
|
|
776
777
|
</details>
|
package/dist/lib.mjs
CHANGED
|
@@ -542,7 +542,7 @@ function denyCommandRules() {
|
|
|
542
542
|
"command(eval)"
|
|
543
543
|
];
|
|
544
544
|
}
|
|
545
|
-
function workerSettings(workspace, workerHome3, reads = []) {
|
|
545
|
+
function workerSettings(workspace, workerHome3, reads = [], writes = false) {
|
|
546
546
|
const real = (p) => {
|
|
547
547
|
try {
|
|
548
548
|
return fs6.realpathSync.native(p);
|
|
@@ -566,6 +566,8 @@ function workerSettings(workspace, workerHome3, reads = []) {
|
|
|
566
566
|
allow: [
|
|
567
567
|
"command(*)",
|
|
568
568
|
...both(path5.join(dataDir(workerHome3), "brain")).map((p) => `write_file(${rulePath(p)})`),
|
|
569
|
+
// agy 1.4 asks for edits in the workspace too, --mode accept-edits or not, and a headless run cannot ask
|
|
570
|
+
...writes ? both(workspace).map((p) => `write_file(${rulePath(p)})`) : [],
|
|
569
571
|
...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`),
|
|
570
572
|
// what the task names outside the workspace: a headless agy cannot ask, and would end the turn unanswered
|
|
571
573
|
...reads.map((r) => r.path).flatMap(both).map((p) => `read_file(${rulePath(p)})`)
|
|
@@ -691,12 +693,12 @@ function baseSettings() {
|
|
|
691
693
|
}
|
|
692
694
|
var rulePath = (p) => p.replace(/^[A-Za-z]:(?=[\\/])/, "").replace(/\\/g, "/");
|
|
693
695
|
var HOMES_KEPT_DAYS = 14;
|
|
694
|
-
function workerHome(workspace, reads = []) {
|
|
696
|
+
function workerHome(workspace, reads = [], writes = false) {
|
|
695
697
|
const root = path5.join(home(), "antigravity-home");
|
|
696
698
|
const id2 = crypto3.createHash("sha256").update(path5.resolve(workspace)).digest("hex").slice(0, 16);
|
|
697
699
|
const dir = path5.join(root, id2);
|
|
698
700
|
fs6.mkdirSync(dataDir(dir), { recursive: true });
|
|
699
|
-
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads), null, 2)}
|
|
701
|
+
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads, writes), null, 2)}
|
|
700
702
|
`);
|
|
701
703
|
if (process.platform === "darwin") {
|
|
702
704
|
fs6.mkdirSync(path5.join(dir, "Library"), { recursive: true });
|
|
@@ -753,7 +755,7 @@ function invocation(req) {
|
|
|
753
755
|
if (model) args.push("--model", model);
|
|
754
756
|
if (effort) args.push("--effort", effort);
|
|
755
757
|
const { command, prefix } = resolveCommand(binary());
|
|
756
|
-
const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root));
|
|
758
|
+
const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root), req.mode !== "read");
|
|
757
759
|
const env = { HOME: workerDir };
|
|
758
760
|
if (process.platform === "win32") env.USERPROFILE = workerDir;
|
|
759
761
|
return { command, args: [...prefix, ...args], env };
|
|
@@ -810,9 +812,10 @@ function doctor({ models, hasFallback }) {
|
|
|
810
812
|
}
|
|
811
813
|
const checks = [
|
|
812
814
|
{ level: "ok", message: `Antigravity CLI ${version.out.trim().split("\n")[0]} at ${binary()}` },
|
|
813
|
-
|
|
815
|
+
// checked on macOS (agy 1.4.7, scripts/check-worker.mjs); Linux and Windows not run yet
|
|
816
|
+
process.platform === "darwin" ? { level: "ok", message: "agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes" } : {
|
|
814
817
|
level: "warn",
|
|
815
|
-
message:
|
|
818
|
+
message: `the Antigravity worker is in beta on ${process.platform === "win32" ? "Windows" : "Linux"} (checked on macOS only; node scripts/check-worker.mjs antigravity tries it here). agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes`
|
|
816
819
|
}
|
|
817
820
|
];
|
|
818
821
|
if (process.platform === "win32") {
|
|
@@ -2315,6 +2318,7 @@ var FILE_EXTENSIONS = new Set(
|
|
|
2315
2318
|
);
|
|
2316
2319
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
2317
2320
|
var MAX_INDEXED = 6e4;
|
|
2321
|
+
var MAX_SKIPPED = 3e5;
|
|
2318
2322
|
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
2319
2323
|
var REF = new RegExp(
|
|
2320
2324
|
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
@@ -2373,13 +2377,17 @@ function verifyRefs(refs, dirs) {
|
|
|
2373
2377
|
return lineCache.get(file);
|
|
2374
2378
|
};
|
|
2375
2379
|
let index;
|
|
2380
|
+
let skipped;
|
|
2376
2381
|
const byName = () => index ??= indexFiles(roots);
|
|
2382
|
+
const inSkipped = () => skipped ??= indexFiles(roots, true);
|
|
2377
2383
|
const candidates = (ref) => {
|
|
2378
2384
|
const direct = resolve(ref.file, roots);
|
|
2379
2385
|
if (direct) return [direct];
|
|
2380
2386
|
if (path12.isAbsolute(ref.file)) return [];
|
|
2381
2387
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
2382
|
-
|
|
2388
|
+
const fits = (f) => path12.basename(f) === wanted || foldersInOrder(f.split(path12.sep).join("/"), wanted);
|
|
2389
|
+
const own = (byName().get(path12.basename(wanted)) ?? []).filter(fits);
|
|
2390
|
+
return (own.length || !isFileName(wanted) ? own : (inSkipped().get(path12.basename(wanted)) ?? []).filter(fits)).slice(0, 20);
|
|
2383
2391
|
};
|
|
2384
2392
|
const check = (file, ref) => {
|
|
2385
2393
|
const lines = load(file);
|
|
@@ -2418,10 +2426,23 @@ var isFileName = (name) => {
|
|
|
2418
2426
|
const base = path12.basename(name);
|
|
2419
2427
|
return !base.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base) || FILE_EXTENSIONS.has(base.split(".").pop().toLowerCase());
|
|
2420
2428
|
};
|
|
2421
|
-
function
|
|
2429
|
+
function foldersInOrder(file, wanted) {
|
|
2430
|
+
const want = wanted.split("/");
|
|
2431
|
+
const have = file.split("/");
|
|
2432
|
+
if (have.at(-1) !== want.at(-1)) return false;
|
|
2433
|
+
let i = have.length - 2;
|
|
2434
|
+
for (let j = want.length - 2; j >= 0; j--) {
|
|
2435
|
+
while (i >= 0 && have[i] !== want[j]) i--;
|
|
2436
|
+
if (i < 0) return false;
|
|
2437
|
+
i--;
|
|
2438
|
+
}
|
|
2439
|
+
return true;
|
|
2440
|
+
}
|
|
2441
|
+
function indexFiles(roots, skipped = false) {
|
|
2422
2442
|
const index = /* @__PURE__ */ new Map();
|
|
2443
|
+
const cap = skipped ? MAX_SKIPPED : MAX_INDEXED;
|
|
2423
2444
|
let count = 0;
|
|
2424
|
-
const walk = (dir) => {
|
|
2445
|
+
const walk = (dir, within) => {
|
|
2425
2446
|
let entries;
|
|
2426
2447
|
try {
|
|
2427
2448
|
entries = fs12.readdirSync(dir, { withFileTypes: true });
|
|
@@ -2429,10 +2450,12 @@ function indexFiles(roots) {
|
|
|
2429
2450
|
return;
|
|
2430
2451
|
}
|
|
2431
2452
|
for (const e of entries) {
|
|
2432
|
-
if (count >=
|
|
2453
|
+
if (count >= cap) return;
|
|
2433
2454
|
if (e.isDirectory()) {
|
|
2434
|
-
if (
|
|
2435
|
-
|
|
2455
|
+
if (e.name === ".git") continue;
|
|
2456
|
+
const skip = SKIP_DIRS2.has(e.name);
|
|
2457
|
+
if (skipped || !skip) walk(path12.join(dir, e.name), within || skip);
|
|
2458
|
+
} else if (e.isFile() && within === skipped) {
|
|
2436
2459
|
count++;
|
|
2437
2460
|
const list2 = index.get(e.name);
|
|
2438
2461
|
if (list2) list2.push(path12.join(dir, e.name));
|
|
@@ -2440,7 +2463,7 @@ function indexFiles(roots) {
|
|
|
2440
2463
|
}
|
|
2441
2464
|
}
|
|
2442
2465
|
};
|
|
2443
|
-
for (const r of roots) walk(r);
|
|
2466
|
+
for (const r of roots) walk(r, false);
|
|
2444
2467
|
return index;
|
|
2445
2468
|
}
|
|
2446
2469
|
function resolve(ref, roots) {
|
|
@@ -2601,7 +2624,8 @@ var SCHEMA = {
|
|
|
2601
2624
|
priceFeedUrl: "string",
|
|
2602
2625
|
priceFeedHours: "number",
|
|
2603
2626
|
budgetDaily: "number",
|
|
2604
|
-
rankWorkers: "boolean"
|
|
2627
|
+
rankWorkers: "boolean",
|
|
2628
|
+
autoFallback: "boolean"
|
|
2605
2629
|
};
|
|
2606
2630
|
function configPath() {
|
|
2607
2631
|
if (process.env.PITROOM_CONFIG) return path15.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -2711,6 +2735,7 @@ function effective(flags = {}) {
|
|
|
2711
2735
|
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2712
2736
|
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24),
|
|
2713
2737
|
rankWorkers: setting(void 0, flag01(e.PITROOM_RANK_WORKERS), c.rankWorkers, false),
|
|
2738
|
+
autoFallback: setting(void 0, flag01(e.PITROOM_AUTO_FALLBACK), c.autoFallback, false),
|
|
2714
2739
|
budgetDaily: setting(void 0, amount(e.PITROOM_BUDGET_DAILY), amount(c.budgetDaily), void 0)
|
|
2715
2740
|
};
|
|
2716
2741
|
}
|
|
@@ -3156,6 +3181,7 @@ function summarize(rows) {
|
|
|
3156
3181
|
}
|
|
3157
3182
|
|
|
3158
3183
|
// src/vcs/guard.ts
|
|
3184
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
3159
3185
|
import fs19 from "node:fs";
|
|
3160
3186
|
import path18 from "node:path";
|
|
3161
3187
|
var VERSION = 2;
|
|
@@ -3293,6 +3319,63 @@ var cmdShim = (sh) => [
|
|
|
3293
3319
|
"exit /b %errorlevel%",
|
|
3294
3320
|
""
|
|
3295
3321
|
].join("\r\n");
|
|
3322
|
+
var launcherSource = (sh) => `// pitroom git guard: git.exe launcher v${VERSION} (generated; edit src/vcs/guard.ts)
|
|
3323
|
+
using System;
|
|
3324
|
+
using System.Diagnostics;
|
|
3325
|
+
using System.IO;
|
|
3326
|
+
|
|
3327
|
+
static class PitroomGit {
|
|
3328
|
+
const string Sh = @"${sh.replace(/"/g, '""')}";
|
|
3329
|
+
|
|
3330
|
+
static int Main() {
|
|
3331
|
+
string line = Environment.CommandLine;
|
|
3332
|
+
// this program's own name, as Windows reads it: up to the closing quote when it starts with one, else to a space
|
|
3333
|
+
int i = 0;
|
|
3334
|
+
if (line.Length > 0 && line[0] == '"') { int q = line.IndexOf('"', 1); i = q < 0 ? line.Length : q + 1; }
|
|
3335
|
+
else { while (i < line.Length && line[i] != ' ' && line[i] != '\\t') i++; }
|
|
3336
|
+
string script = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "git");
|
|
3337
|
+
ProcessStartInfo start = new ProcessStartInfo(Sh, "\\"" + script + "\\"" + line.Substring(i));
|
|
3338
|
+
start.UseShellExecute = false;
|
|
3339
|
+
start.EnvironmentVariables["MSYS2_ARG_CONV_EXCL"] = "*";
|
|
3340
|
+
// Ctrl+C reaches git too: wait for it to end and pass its exit code on
|
|
3341
|
+
Console.CancelKeyPress += delegate (object o, ConsoleCancelEventArgs e) { e.Cancel = true; };
|
|
3342
|
+
using (Process git = Process.Start(start)) {
|
|
3343
|
+
git.WaitForExit();
|
|
3344
|
+
return git.ExitCode;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
`;
|
|
3349
|
+
function findCsc() {
|
|
3350
|
+
const windir = process.env.WINDIR ?? process.env.SystemRoot ?? "C:\\Windows";
|
|
3351
|
+
for (const fw of ["Framework64", "Framework"]) {
|
|
3352
|
+
const p = path18.join(windir, "Microsoft.NET", fw, "v4.0.30319", "csc.exe");
|
|
3353
|
+
if (fs19.existsSync(p)) return p;
|
|
3354
|
+
}
|
|
3355
|
+
return void 0;
|
|
3356
|
+
}
|
|
3357
|
+
function buildLauncher(dir, sh) {
|
|
3358
|
+
const source = launcherSource(sh);
|
|
3359
|
+
const src = path18.join(dir, "git-launcher.cs");
|
|
3360
|
+
const exe = path18.join(dir, "git.exe");
|
|
3361
|
+
if (fs19.existsSync(exe) && fs19.existsSync(src) && fs19.readFileSync(src, "utf8") === source) return;
|
|
3362
|
+
const csc = findCsc();
|
|
3363
|
+
if (!csc) return;
|
|
3364
|
+
const tmp = path18.join(dir, `git.${process.pid}.tmp.exe`);
|
|
3365
|
+
const tmpSrc = `${src}.${process.pid}.tmp`;
|
|
3366
|
+
try {
|
|
3367
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
3368
|
+
fs19.writeFileSync(tmpSrc, source);
|
|
3369
|
+
const r = spawnSync9(csc, ["/nologo", "/optimize+", "/target:exe", `/out:${tmp}`, tmpSrc], { encoding: "utf8", timeout: 6e4, windowsHide: true });
|
|
3370
|
+
if (r.status !== 0 || !fs19.existsSync(tmp)) return;
|
|
3371
|
+
renameOver(tmp, exe);
|
|
3372
|
+
renameOver(tmpSrc, src);
|
|
3373
|
+
} catch {
|
|
3374
|
+
} finally {
|
|
3375
|
+
fs19.rmSync(tmp, { force: true });
|
|
3376
|
+
fs19.rmSync(tmpSrc, { force: true });
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3296
3379
|
var HOOK_VERSION = 1;
|
|
3297
3380
|
var REF_HOOK = `#!/bin/sh
|
|
3298
3381
|
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
@@ -3350,6 +3433,7 @@ function guardEnv(env) {
|
|
|
3350
3433
|
sh = findGitSh(real);
|
|
3351
3434
|
if (!sh) return { ...env, ...quiet };
|
|
3352
3435
|
writeIfChanged2(path18.join(dir, "git.cmd"), cmdShim(sh));
|
|
3436
|
+
buildLauncher(dir, sh);
|
|
3353
3437
|
}
|
|
3354
3438
|
writeIfChanged2(path18.join(dir, "git"), SHIM);
|
|
3355
3439
|
const key = pathKey(env);
|
|
@@ -3360,6 +3444,29 @@ function shimReady() {
|
|
|
3360
3444
|
return Boolean(env[pathKey(env)]?.startsWith(shimDir()));
|
|
3361
3445
|
}
|
|
3362
3446
|
|
|
3447
|
+
// src/vcs/git.ts
|
|
3448
|
+
function numberDiff(diff) {
|
|
3449
|
+
let next = 0;
|
|
3450
|
+
let width = 0;
|
|
3451
|
+
let inHunk = false;
|
|
3452
|
+
return diff.split("\n").map((l) => {
|
|
3453
|
+
if (l.startsWith("diff --git")) {
|
|
3454
|
+
inHunk = false;
|
|
3455
|
+
return l;
|
|
3456
|
+
}
|
|
3457
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(l);
|
|
3458
|
+
if (hunk) {
|
|
3459
|
+
inHunk = true;
|
|
3460
|
+
next = Number(hunk[1]);
|
|
3461
|
+
width = String(next + Number(hunk[2] ?? 1)).length;
|
|
3462
|
+
return l;
|
|
3463
|
+
}
|
|
3464
|
+
if (!inHunk || !l || !"+- ".includes(l[0])) return l;
|
|
3465
|
+
if (l[0] === "-") return `-${" ".repeat(width)}| ${l.slice(1)}`;
|
|
3466
|
+
return `${l[0]}${String(next++).padStart(width)}| ${l.slice(1)}`;
|
|
3467
|
+
}).join("\n");
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3363
3470
|
// src/core/slots.ts
|
|
3364
3471
|
import crypto4 from "node:crypto";
|
|
3365
3472
|
import fs20 from "node:fs";
|
|
@@ -3520,6 +3627,7 @@ export {
|
|
|
3520
3627
|
loadTemplate,
|
|
3521
3628
|
looksSecret,
|
|
3522
3629
|
newRunId,
|
|
3630
|
+
numberDiff,
|
|
3523
3631
|
parseAudit,
|
|
3524
3632
|
parsePlan,
|
|
3525
3633
|
parsePriceSpec,
|