pitroom 0.22.0 → 0.24.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/docs/backends.md CHANGED
@@ -27,7 +27,7 @@ interface Backend {
27
27
  id: string; // "opencode", used in targets: "opencode:provider/model"
28
28
  name: string;
29
29
  capabilities: {
30
- readOnly: 'permission-rules' | 'os-sandbox' | 'tool-allowlist' | 'approval-mode';
30
+ readOnly: 'permission-rules' | 'os-sandbox' | 'tool-allowlist' | 'approval-mode' | 'snapshot';
31
31
  resume: 'by-id' | 'none';
32
32
  reportsCost: boolean;
33
33
  attachFiles: boolean;
@@ -59,6 +59,9 @@ CLI's **own** mechanism, and never with flags that switch safety off
59
59
  | **Codex** (`exec --json --ignore-user-config`) | `-c sandbox_mode="read-only"` (OS sandbox) | `-c sandbox_mode="workspace-write"` (writes only in the working dir, no network), `approval_policy="never"` | `exec resume <id>` | tokens only |
60
60
  | **Claude Code** (`-p --output-format stream-json --verbose`) | `--safe-mode --restricted --strict-mcp-config`, `--permission-mode dontAsk`, tools `Read,Grep,Glob` only | same lockdown, tools `+Edit,Write,Bash`, `--disallowedTools Bash(git commit:*)`… | `--resume <id>` | `total_cost_usd` |
61
61
  | **Gemini CLI** (`--prompt … --output-format stream-json`) | `--approval-mode plan` (the policy engine allows read tools only) | `auto_edit` + Pitroom's `--policy` rules (shell minus history-changing git) | by session id (`--resume <id>`) → `resume: 'by-id'` | tokens only |
62
+ | **Qwen Code** (`<prompt> --safe-mode --output-format stream-json`) | `--approval-mode plan` (edits, shell and reads outside the working directory declined) | `--approval-mode default` + `--allowed-tools Edit(./**) write_file(./**) notebook_edit(./**) run_shell_command`, shell minus history-changing git via `--exclude-tools` | `--resume <id>` | tokens only |
63
+ | **Kilo Code** (`kilo run --format json`) | OpenCode's read profile through `KILO_CONFIG_CONTENT` | OpenCode's write profile | `--session <id>` | reported |
64
+ | **Antigravity CLI** (`agy -p … --output-format stream-json --sandbox`) | a read-only **snapshot of its own** (agy has no read-only mode for `-p`), plus `--mode plan` and Pitroom's deny rules | `--mode accept-edits`, shell in agy's terminal sandbox, the same deny rules | `--conversation <id>` | tokens only |
62
65
 
63
66
  Whatever the CLI, the core still applies the git guard, closes stdin (Codex and
64
67
  Gemini otherwise read piped stdin into the prompt), sets `PWD` to the worker's
@@ -124,6 +127,85 @@ How a worker is started:
124
127
  - **Resume**: `--resume` takes the session id that the stream's `init` event reports (checked live on 0.62: the follow-up remembered what the first run read), as well as an index or `latest` (which are not safe with parallel workers, so Pitroom never uses them). `--continue` therefore works (`resume: 'by-id'`). Sessions are kept per project folder, so a follow-up runs in the same folder as its parent, which Pitroom does anyway.
125
128
  - **Sign-in**: a Google account sign-in (`oauth-personal`, Code Assist for individuals) is refused by Google for this client (`IneligibleTierError`); use an API key from Google AI Studio (`gemini` stores it, or set `GEMINI_API_KEY`).
126
129
 
130
+ ## Qwen Code notes
131
+
132
+ Status: **beta**, checked against Qwen Code 0.25.0. The fixtures in `test/fixtures/qwen` were recorded from the real CLI, started with Pitroom's own command line, against a **scripted OpenAI-compatible endpoint** (a small local server that answers with prepared tool calls and text): the events, permission decisions, refusals and error output are the CLI's, the model is not. It has not yet been run against a real model provider.
133
+
134
+ What was checked that way:
135
+
136
+ - **Read** runs (`--approval-mode plan`): `read_file`, `grep_search` and `glob` work; `write_file`, `run_shell_command` and a read outside the working directory are declined.
137
+ - **Write / isolate** runs: edits and new files inside the working directory land; an edit or a new file outside it (the user's real checkout from an isolated copy, another folder, a `../` path) is declined; `git commit` and `rm -rf` are refused by Pitroom's deny rules; other shell commands run. End to end (`pitroom run -i -W qwen:…`): the changes come back as the isolated copy's patch and the user's tree is untouched.
138
+ - **`--continue`**: `--resume <session id>` (the id of the stream's `init` event) sends the earlier turns along.
139
+ - **Failures**: a bad key (401, `auth`), an unknown model (404, `model-unavailable`) and a used-up quota (429, `rate-limited`) are classified, did no work, and let the chain move on.
140
+
141
+ How a worker is started, and why:
142
+
143
+ - **Not `--approval-mode auto-edit`.** It approves an edit *anywhere on disk*: in a test an isolated run edited the user's real checkout and wrote to `/tmp`. `default` declines whatever is not allowed (a headless run cannot ask), and the allow rules name edits inside the working directory only (`./**` is relative to the CLI's cwd, which the core sets to the isolated copy or the project).
144
+ - **`--safe-mode`** keeps the user's QWEN.md, hooks, extensions, skills and MCP servers out, and turns off the **background memory extraction**: without it, every run ends with an extra request in which a subagent with `write_file` and `edit` writes "durable facts" from the run into `~/.qwen/memories` (user-wide) and the project's memory folder.
145
+ - **`--exclude-tools`**: even in plan mode Qwen Code runs `enter_worktree` (it made a git worktree and a branch in the project in a test), and it has subagents, memory, cron, messaging and goal tools. They are excluded in every mode, and `web_fetch` / `web_search` unless `--web`. An excluded tool the model calls anyway is declined, with a message the model sees.
146
+ - **The prompt is the first argument**: `--exclude-tools` and `--allowed-tools` take every following word, and Qwen Code reads no prompt after `--`. A prompt that starts with `-` gets a leading space.
147
+ - **`QWEN_CODE_SYSTEM_DEFAULTS_PATH`** points at `policies/qwen/worker-defaults.json`: no auto-update, no memory extraction, and `maxRetries: 0`, which removes the CLI's 10 × 60 s waits on a rate limit. Its other retry layer (7 tries with backoff) stays, so a used-up quota surfaces after about 80 s, not 10 minutes. These are *defaults*: the user's own settings win, and an enterprise system settings file is left alone. `QWEN_CODE_DISABLE_CRON=1` and `QWEN_DISABLE_AUTO_TITLE=1` are set too.
148
+ - **Sign-in and model** come from the user's `~/.qwen/settings.json` (`security.auth.selectedType`, `model.name`) and environment (`OPENAI_API_KEY`, `OPENAI_BASE_URL`, …); `-W qwen:<model>` passes `--model`. There is no effort option: `#level` is dropped. Cost is not reported (tokens only): give it a price with `workerPrices`.
149
+
150
+ What is **not** enforced, so do not rely on it:
151
+
152
+ - **Secret files.** `read_file` reads `.env` inside the working directory. What protects a read run is Pitroom's **read snapshot** (a directory with secret-looking files is read as a clean snapshot without them); edit runs rely on the worker being told not to read them.
153
+ - **The shell is not confined.** An allowed shell command can write outside the working directory, as with the other workers that have a shell; the git guard still holds, and the deny rules are prefix based.
154
+
155
+ ## Kilo Code notes
156
+
157
+ Status: **beta**, checked against `@kilocode/cli` 7.8.8. Kilo Code's CLI is a fork of OpenCode: the same `run --format json` events, agents and permission rules, so the adapter reuses OpenCode's profiles and parser. Like the Qwen Code fixtures, `test/fixtures/kilo` was recorded from the real CLI, started with Pitroom's own command line, against a **scripted OpenAI-compatible endpoint**; not yet with a real provider.
158
+
159
+ What was checked that way, with a `kilo daemon` running:
160
+
161
+ - **Read** runs: `read` and `grep` work; the edit tools are not offered at all, and `bash`, a `.env` read and a read outside the project are refused by the profile.
162
+ - **Write / isolate**: edits and new files inside the project land; a write outside it and `git commit` are refused. End to end (`pitroom run -i -W kilo:…`), the change comes back as the isolated copy's patch and the user's tree is untouched.
163
+ - **`--continue`**: `--session <id>` sends the earlier turns along.
164
+ - **Failures**: an unknown model (`model-unavailable`), a bad key (`auth`) and a used-up quota (`rate-limited`, after about 80 s of the CLI's own retries) did no work, so the chain moves on.
165
+
166
+ How a worker is started, and why:
167
+
168
+ - **`KILO_CONFIG_CONTENT`**, not `OPENCODE_CONFIG_CONTENT`: Kilo reads its own variable (checked with `kilo debug config`). The profiles' scratch-folder rules point at Kilo's folders (`~/.local/share/kilo/…`, `$TMPDIR/kilo/…`), and the config sets `autoupdate: false`.
169
+ - **A private `XDG_STATE_HOME`** (`<pitroom home>/kilo-state`). `kilo run` has no `--standalone`: when the user's `kilo daemon` is up, it attaches to it (found through `~/.local/state/kilo/daemon.json`), and the daemon, started with its own environment, ignores the injected config, the git guard and `PITROOM_ACTIVE` (seen: "Agent not found: pitroom-read"; `--port` and `--pure` do not prevent it). With a state folder of its own the worker finds no daemon and starts a private server. Sign-in, config and sessions live in the data and config folders, which stay the user's.
170
+ - **`--log-level ERROR`** in capitals: Kilo rejects `error` and prints its help instead of running.
171
+ - Never `--auto` ("auto-approve permissions that are not explicitly denied"), never `--attach`.
172
+
173
+ ## Antigravity CLI notes
174
+
175
+ Status: **beta**. Checked against **agy 1.3.5** on macOS (October 2026): a read run, a follow-up, an isolated edit, shell commands in the sandbox, the deny rules, an unknown model, a used-up quota and a missing sign-in were run against the real CLI, and `test/fixtures/antigravity` holds their recorded output (home paths replaced). Not yet checked on Linux (the sign-in file link) or Windows (`doctor` says to use `-i` there).
176
+
177
+ Why it is here: Google refuses a Google account sign-in to Gemini CLI (`IneligibleTierError`, "migrate to the Antigravity suite"), so for someone without an AI Studio key, `agy` is the way to use their Google account's models (Gemini, and the Claude and GPT-OSS models Antigravity offers).
178
+
179
+ How a worker is started, and why:
180
+
181
+ - **`-p <prompt> --output-format stream-json`**: one prompt, a JSON event stream, exit. `-p` takes the prompt as its value; a prompt that starts with `-` gets a leading space.
182
+ - **Read runs read a snapshot of their own** (`readOnly: 'snapshot'`). agy has no read-only mode for `-p` runs (issue 45 of google-antigravity/antigravity-cli), and `--mode plan` is reported to still write in them (issue 1181: the file is created in the same turn). So the core never lets Antigravity read the project: right before its attempt it makes the run a snapshot of its own (`snapshots/<tree>-<run id>`, made like the shared clean snapshot: from git objects, uncommitted and untracked files included, secret-looking and ignored files left out, files read-only), whatever `readIn` says. Not the shared one: directories there stay writable, so a new or replaced file would be seen by every later read run on the same state. Its follow-ups read the same snapshot. After the attempt, whatever git sees newly changed there is reported as `READ-ONLY VIOLATION` (the stream need not mention it; a follow-up is not blamed for its parent's writes), and the answer is not cached. A worker after it in the chain reads where it was meant to (the project or the shared snapshot), never Antigravity's, and a second Antigravity target gets a fresh snapshot if the first wrote in its own. As a fallback it costs nothing until it runs. These snapshots are full checkouts: pruning keeps the newest few of them apart from the shared ones, so a burst of Antigravity runs does not push those out, and a follow-up whose snapshot was pruned gets it made again from the same tree. If the snapshot cannot be made, Antigravity is skipped like a failed attempt (the next worker runs, or the run fails: as the worker before it failed, a rate limit staying one, or, when no worker ran at all, without counting against any worker's record); it never falls back to the project. `--in-place`, reviews (whose package lives in the git folder) and folders outside git read the directory itself, with a warning. `pitroom doctor --probe` runs it in an empty temporary folder. `--mode plan` is passed anyway; write and isolate use `--mode accept-edits`. Never `--dangerously-skip-permissions` (the contract tests reject it).
183
+ - **`--sandbox`** in every mode: shell commands run in agy's terminal sandbox (the workspace writable, its `.git` read-only, `.env` and `~/.ssh` unreadable, no network). The sandbox and the permission rules are for macOS and Linux; on Windows agy still uses its previous permission system, and `doctor` says to use `-i` there.
184
+ - **A home of its own per workspace.** agy reads its settings only from `~/.gemini/antigravity-cli/settings.json` (and its permissions from `~/.gemini/config`); `ANTIGRAVITY_APP_DATA_DIR` and every other variable tried leave its data folder where it is (its log says `CLI app data directory: …`). So a worker runs with `HOME` set to `<pitroom home>/antigravity-home/<id>`, one per workspace (the run's snapshot, its isolated copy, or the project for `-w`), where Pitroom writes `settings.json` (atomically: a parallel worker never reads half a file). The sign-in is linked in: on macOS agy finds it in the login keychain through `$HOME/Library/Keychains`, which is linked to yours; elsewhere it is the file `antigravity-oauth-token`, linked from `~/.gemini/antigravity-cli` (and a dangling link is repaired). Conversations stay in that home, so a follow-up, which runs in the same workspace, finds its own; homes not used for two weeks are removed. The worker's commands see that home too: your dotfiles are out of their reach, your own agy settings, plugins and MCP servers do not apply, and agy picks its default model unless `-W antigravity:<model>` names one. The settings: `enableTerminalSandbox`, `toolPermission: proceed-in-sandbox` and the grant `command(*)` (a headless run cannot answer a prompt: an ungranted command is refused, and the turn may end without an answer), `artifactReviewPolicy: always-proceed`, `allowNonWorkspaceAccess: false`, `write_file` for its own `brain` folder (plan mode writes its plan there, outside the workspace), `read_file` for the folders of your PATH under your home (the sandbox sees only the system and what is granted: without it an nvm `node` is `Operation not permitted`; a `bin` in a versioned install brings the install along, other folders only themselves, as `~/.cargo` holds credentials), and deny rules (deny beats allow): the workspace's secret-looking files and the secrets in your home (`.ssh`, `.aws`, `.gnupg`, `.netrc`, `.config/gh`, …) by absolute path, the shared forbidden commands (`src/backends/deny.ts`), `mcp(*)`, `read_url(*)` and `execute_url(*)`.
185
+ - **How agy matches the rules**, as seen in 1.3.5 (its docs say only "prefix, word by word"): `read_file` takes an absolute path or one relative to the workspace, as a folder prefix, and no pattern; a `regex:` target is not read, and a relative path breaks the sandbox (`sandbox configuration error: … non-absolute file path`) without denying anything, so secret files are listed one by one. A `command` rule is compared word by word with the start of each command in the line, after agy has split it at `;`, `&&`, `||` and `|` and taken off wrappers such as `env -i` and `timeout 60` (but not `xargs`, and not what find runs with `-exec`). A `regex:` command rule is split at spaces too, each word a regex for one word (`.*` is exactly one word), and backslashes are dropped. A line with `{}` in it is not split: a rule is matched against the whole line, start to end. The rules are built for that: each forbidden command as it is and by path, git behind its global options, a recursive `rm` wherever its option stands, `sh -c` and `eval`, find with `-exec`, `-ok` or `-delete` refused outright (a rule for what it runs only holds when it spans the whole line), xargs running a forbidden command, and, for lines with `{}`, `git <subcommand>`, `rm -r`, xargs and find anywhere in them. `test/backends.test.mjs` checks them with that matching; they remain pattern rules, **a layer on top, not a guarantee**: other spellings of a command can get past them. The git guard is what holds for git whatever the spelling (`.git` is read-only in the sandbox too), and a read run's snapshot or an isolated copy is what holds for your files: use `-i` for changes rather than `-w`, which lets a worker write (and delete) in the project itself. One settings file serves every mode, since a follow-up may run in another: so **`--web` does not apply** to Antigravity workers.
186
+ - **`--print-timeout 24h`**: agy's docs give its own limit as 5 minutes (1.3.5's help says none), and a run that hits it ends with partial output; Pitroom's timeout governs instead.
187
+ - **Model**: `-W antigravity:<model>` passes `--model` (`agy models` lists yours), and `#level` passes `--effort`. Without one, agy picks. Its quota is per Google account and can run out for hours: give it a fallback.
188
+ - **`--continue`**: `--conversation <id>` with the id of the stream's `init` event, never `-c` (the latest conversation, which is unsafe beside parallel workers).
189
+
190
+ The stream, as agy 1.3.5 writes it: every line is `{"event": "init" | "step_update" | "result", …}` with the payload under the event's name. `init.model` names the model when `--model` was given. `step_update.step_type` is `user_input`, `agent_response` (text in `text_delta` chunks), `tool` (`tool_name`, and `tool_info` with the tool's name, parameters and `output`, or `error: {type, message}`), `checkpoint` or `error_message`; `state` is `ACTIVE`, `DONE`, or `ERROR` for a failed or refused call. The answer is `result.response`; tokens are `result.usage` (`input_tokens` with `cache_read_tokens` inside it, `output_tokens`, `thinking_tokens`, `total_tokens`), or the sum of the finished steps' own usage when a run is cut short; `result.denied_actions` lists calls refused for want of a grant. Tools are agy's own names: `run_command` (`CommandLine`) is the shell, `view_file` (`AbsolutePath`) reads, `write_to_file`, `replace_file_content` and `multi_replace_file_content` (`TargetFile`) are edits. Every field is read tolerantly.
191
+
192
+ Failures, as recorded. A command refused for want of a grant ends the turn with **status `SUCCESS`, no answer and exit 0**, and a stderr line (`jetski: no output produced — a tool required the "command" permission …`); a command refused by a deny rule usually lets the agent answer that it was refused. So an Antigravity run without an answer is always a failure, never an empty answer. A used-up quota exits 3 with `result.status: ERROR` (`Individual quota reached … Resets in 2h23m`) and an `AGY_ERROR: {"short_error": "RESOURCE_EXHAUSTED (code 429): …"}` line on stderr: `rate-limited`, so the chain moves on; it can hit one model (Claude Opus) while another (Gemini Flash) still answers. An unknown model exits 1 (`is not recognized as a known model`): `model-unavailable`. A prompt the model's safety filter blocks comes back as an ordinary answer with status `SUCCESS` (`This request was blocked by Gemini's filters…`): it is reported as a failure, not as the answer. No sign-in waits 60 seconds for a browser sign-in, then exits 1 (`authentication failed or timed out`): `auth`.
193
+
194
+ Known agy problems Pitroom cannot work around: a follow-up whose conversation id agy does not know starts a new conversation (stderr: `warning: conversation "…" not found`), which the run shows as a warning, as the earlier turns are then missing; a turn can end while a `run_command` it started is still running, which is then killed (issue 1044); a subagent's output may not reach the stream (issue 1077). A model can also refuse a task by itself (reading `.env`, or a `git commit` the worker contract forbids) before agy's rules come into it.
195
+
196
+ What is still open: Linux (the token file link, the secret-service keyring) and Windows. A stronger guard would be a `PreToolUse` hook that denies (agy honours a hook's `deny` in `-p` runs, issue 1053, though not its `allow`); the worker home would be the place for its `hooks.json`.
197
+
198
+ ## Hermes Agent: not supported yet
199
+
200
+ [Hermes Agent](https://pypi.org/project/hermes-agent/) (Nous Research; looked at 0.19.0 from PyPI) was considered and left out, because it lacks the controls every other worker gets from its own CLI:
201
+
202
+ - **No read-only mode.** Tools come in toolsets, and the `file` toolset holds `read_file` together with `write_file` and `patch`; single tools cannot be turned off, and there is no plan or approval mode for files.
203
+ - **Writes outside the working directory are allowed.** The file tools warn when a path resolves outside the workspace and write anyway.
204
+ - **Dangerous shell commands are auto-approved without a person.** In a non-interactive run that is not its messaging gateway, the approval check lets them through (`-z` bypasses approvals outright); a fail-closed path exists in the code, but not as a CLI switch.
205
+ - **No event stream.** `chat -q -Q` prints the final answer and the session id, nothing about steps, tool calls or tokens (`--usage-file` only works with `-z`).
206
+
207
+ Pitroom's own guards (the git guard, isolated copies, the read snapshot) would still apply, but not the CLI-level confinement the other workers have, so it would be the one worker that can write anywhere. `-W hermes` therefore says it is not supported yet. What would change that: a read-only (or per-tool) switch, file tools confined to the working directory, a fail-closed approval switch for headless runs, and a JSON event stream; or its Docker terminal backend with the project mounted, which would need Docker on the user's machine.
208
+
127
209
  ## Adding a worker
128
210
 
129
211
  1. Create `src/backends/<id>/index.ts` exporting a `Backend`; keep CLI knowledge
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pitroom",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "A free pit crew for your expensive coding agent: hand reading, fixes, tests and reviews to cheap worker agents and get back the answer, the exact diff and a receipt. Fourteen skills adapt a full development workflow. Needs the pitroom command (npm i -g pitroom).",
5
5
  "contextFileName": "gemini-context.md"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pitroom",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "mcpName": "io.github.ATASTECH/pitroom",
5
5
  "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, Claude Code or Gemini CLI workers, with receipts that prove the savings.",
6
6
  "type": "module",
@@ -34,6 +34,9 @@ commandPrefix = [
34
34
  "git add",
35
35
  "rm -rf",
36
36
  "rm -fr",
37
+ "rm -r",
38
+ "rm -R",
39
+ "rm --recursive",
37
40
  "sudo",
38
41
  "chown",
39
42
  "kill",
@@ -45,6 +48,10 @@ commandPrefix = [
45
48
  "opencode",
46
49
  "claude",
47
50
  "codex",
48
- "gemini"
51
+ "gemini",
52
+ "qwen",
53
+ "kilo",
54
+ "kilocode",
55
+ "agy"
49
56
  ]
50
57
  deny_message = "Pitroom: history-changing git, destructive commands and other agents are not for workers."
@@ -0,0 +1,9 @@
1
+ {
2
+ "general": { "enableAutoUpdate": false },
3
+ "model": { "generationConfig": { "maxRetries": 0 } },
4
+ "memory": {
5
+ "enableManagedAutoMemory": false,
6
+ "enableManagedAutoDream": false
7
+ },
8
+ "$version": 4
9
+ }