@permissionbrick/auto-review-mcp 0.1.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/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/cli.js +126 -0
- package/dist/cli.js.map +1 -0
- package/dist/descriptions.js +71 -0
- package/dist/descriptions.js.map +1 -0
- package/dist/git.js +99 -0
- package/dist/git.js.map +1 -0
- package/dist/launch.js +62 -0
- package/dist/launch.js.map +1 -0
- package/dist/orchestrator.js +396 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/server.js +289 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio.js +91 -0
- package/dist/stdio.js.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 permissionBRICK
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# auto-review-mcp
|
|
2
|
+
|
|
3
|
+
A local MCP server that orchestrates a **continuous review loop between two coding agents**: one
|
|
4
|
+
**developer** and one **reviewer**. They both connect to this one long-running server at the same
|
|
5
|
+
time and hand work back and forth through it — no human in the middle of the loop.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
developer agent auto-review server reviewer agent
|
|
9
|
+
─────────────── ────────────────── ──────────────
|
|
10
|
+
(works on a batch)
|
|
11
|
+
request_review(summary, ──────▶ stages all changes, diffs vs HEAD
|
|
12
|
+
commit_message) registers the batch, BLOCKS dev
|
|
13
|
+
wakes reviewer ──────────────────▶ get_next_review()
|
|
14
|
+
(returns summary + full diff)
|
|
15
|
+
reviews it…
|
|
16
|
+
◀────────────────────────────────── submit_review(approved
|
|
17
|
+
approved → git commit (dev's msg) | changes_requested,
|
|
18
|
+
◀────── {approved, commit_sha} changes → forward the issue issue, category)
|
|
19
|
+
or {changes_requested, issue}
|
|
20
|
+
(continue / fix & resubmit) get_next_review() …
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The protocol is taught entirely through the MCP **tool descriptions**, so the two agents
|
|
24
|
+
self-orchestrate from the tools alone.
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
- **One shared coordinator, two role-scoped endpoints**:
|
|
29
|
+
- Developer agent → `…/developer/mcp` (tools: `initialize_review_session`, `request_review`,
|
|
30
|
+
`signal_complete`, `workflow_status`)
|
|
31
|
+
- Reviewer agent → `…/reviewer/mcp` (tools: `get_next_review`, `submit_review`,
|
|
32
|
+
`workflow_status`)
|
|
33
|
+
- Role is fixed by the endpoint, so there is no role/agent-id parameter on the calls.
|
|
34
|
+
- **The developer names the repo at runtime.** As its first step the developer agent calls
|
|
35
|
+
`initialize_review_session` with the absolute path of the repo it's editing. The coordinator
|
|
36
|
+
remembers it for its lifetime (or until called again). So there's nothing repo-specific to bake
|
|
37
|
+
into config. (You can still pre-set it with `--repo`/`AUTO_REVIEW_REPO` if you prefer.)
|
|
38
|
+
- **Two ways to attach** (see [Connect the two agents](#connect-the-two-agents)):
|
|
39
|
+
- **stdio (recommended)** — each agent's `.mcp.json` runs `npx -y @permissionbrick/auto-review-mcp
|
|
40
|
+
--role …`, a thin proxy. The first proxy auto-starts a single shared background coordinator; both
|
|
41
|
+
proxies forward to it. No server to start by hand. The shared state lives in the coordinator, not
|
|
42
|
+
the agents.
|
|
43
|
+
- **HTTP** — you start the coordinator yourself (`auto-review-server`) and point each agent at its
|
|
44
|
+
URL (good for remote agents or sharing one coordinator across machines).
|
|
45
|
+
- **Blocking handoffs.** `request_review` blocks until the reviewer rules; `get_next_review`
|
|
46
|
+
blocks until a batch arrives. Waiting is **event-driven**, so the actual handoff is instant. A
|
|
47
|
+
call only holds the connection open up to a bounded **poll window** (`AUTO_REVIEW_POLL_SECONDS`),
|
|
48
|
+
then returns `keep_waiting`; the agent immediately calls again. This makes the wait effectively
|
|
49
|
+
unbounded while surviving connection drops and client timeouts. See
|
|
50
|
+
[Tuning how long a call waits](#tuning-how-long-a-call-waits).
|
|
51
|
+
- **The server owns the commits.** On approval the server runs `git add -A` + `git commit` with
|
|
52
|
+
the developer's commit message (plus a `Reviewed-by: auto-review` trailer). HEAD advances, so
|
|
53
|
+
each review's diff is naturally just the new batch. The developer never commits.
|
|
54
|
+
- **One batch at a time**, identified by a `batch_id`. The diff shown to the reviewer is the full
|
|
55
|
+
unified diff of the working tree vs HEAD (new/deleted files included).
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
You don't need to install anything by hand — the recommended setup runs the server on demand with
|
|
60
|
+
`npx` (see [Connect the two agents](#connect-the-two-agents)). Prefer global CLIs? Install them with:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install -g @permissionbrick/auto-review-mcp
|
|
64
|
+
# bins: auto-review-mcp (stdio proxy) · auto-review-server (HTTP coordinator) · auto-review-cli (poller)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Build from source
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/permissionBRICK/auto-review.git
|
|
71
|
+
cd auto-review
|
|
72
|
+
npm install # installs deps and builds (via the prepare script)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Connect the two agents
|
|
76
|
+
|
|
77
|
+
Launch **two Claude Code instances** — one developer, one reviewer. Both must point at the **same
|
|
78
|
+
git repository** the developer edits (it must be a git repo, not this server's directory).
|
|
79
|
+
|
|
80
|
+
### Option A — `node`/stdio (recommended)
|
|
81
|
+
|
|
82
|
+
Each agent's `.mcp.json` runs the stdio proxy via `npx`; the first one auto-starts the shared
|
|
83
|
+
coordinator. Nothing to install or run by hand, and **no repo path in config** — the developer agent
|
|
84
|
+
declares it at runtime via `initialize_review_session`. Sample configs are in
|
|
85
|
+
[`configs/`](configs/):
|
|
86
|
+
|
|
87
|
+
```jsonc
|
|
88
|
+
// configs/developer.mcp.json (reviewer.mcp.json is identical with --role reviewer)
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"auto-review": {
|
|
92
|
+
"command": "npx",
|
|
93
|
+
"args": ["-y", "@permissionbrick/auto-review-mcp", "--role", "developer"],
|
|
94
|
+
"env": { "AUTO_REVIEW_POLL_SECONDS": "240" },
|
|
95
|
+
"timeout": 1800000
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The shipped configs use a **240 s** poll window (`AUTO_REVIEW_POLL_SECONDS`) — the practical max
|
|
102
|
+
per single hold (see [Tuning how long a call waits](#tuning-how-long-a-call-waits)). Idle waiters
|
|
103
|
+
re-poll every ~240 s; handoffs are still instant. For long *uninterrupted* waits, use the poller
|
|
104
|
+
CLI, which loops over these windows.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Developer instance (point at the config above, e.g. configs/developer.mcp.json)
|
|
108
|
+
claude --mcp-config configs/developer.mcp.json --strict-mcp-config
|
|
109
|
+
# Reviewer instance (separate terminal)
|
|
110
|
+
claude --mcp-config configs/reviewer.mcp.json --strict-mcp-config
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`--strict-mcp-config` makes each instance load *only* that file. No launch-time env is needed — the
|
|
114
|
+
config's `timeout` field raises Claude Code's per-call cap by itself.
|
|
115
|
+
|
|
116
|
+
Proxy env / args (also accepted as `--flags` in `args`): `AUTO_REVIEW_ROLE`, `AUTO_REVIEW_PORT`
|
|
117
|
+
(default `8765`), `AUTO_REVIEW_HOST` (default `127.0.0.1`), `AUTO_REVIEW_POLL_SECONDS` (default
|
|
118
|
+
`40` if unset; the shipped config sets `240`), `AUTO_REVIEW_MAX_DIFF_BYTES` (default `200000`), and
|
|
119
|
+
optionally `AUTO_REVIEW_REPO` to pre-set the repo instead of using `initialize_review_session`. The
|
|
120
|
+
coordinator logs to `${TMPDIR}/auto-review-coordinator-<port>.log` and keeps running after the
|
|
121
|
+
agents exit (kill it via that port if you want a clean reset — also needed to apply a changed poll
|
|
122
|
+
window, see below).
|
|
123
|
+
|
|
124
|
+
### Option B — HTTP (manual start / remote agents)
|
|
125
|
+
|
|
126
|
+
Start the coordinator yourself and point each agent at its URL (configs:
|
|
127
|
+
`configs/*.http.mcp.json`):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# start the shared HTTP coordinator (no install needed via npx; or use the global bin)
|
|
131
|
+
npx -y -p @permissionbrick/auto-review-mcp auto-review-server --port 8765 --poll-seconds 240 # keep ≤ ~270; optional: --repo /path
|
|
132
|
+
claude --mcp-config configs/developer.http.mcp.json --strict-mcp-config
|
|
133
|
+
claude --mcp-config configs/reviewer.http.mcp.json --strict-mcp-config
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The HTTP configs carry `"timeout": 3600000`, so no launch env is needed here either. Server flags
|
|
137
|
+
(env var in parens): `--repo` (`AUTO_REVIEW_REPO`, optional — else the developer sets it via
|
|
138
|
+
`initialize_review_session`), `--port` (`AUTO_REVIEW_PORT`, `8765`), `--host` (`AUTO_REVIEW_HOST`,
|
|
139
|
+
`0.0.0.0` — reachable as `agent-vm.mshome.net`), `--poll-seconds` (`AUTO_REVIEW_POLL_SECONDS`,
|
|
140
|
+
`1500`), `--max-diff-bytes` (`AUTO_REVIEW_MAX_DIFF_BYTES`, `200000`). `GET /healthz` returns a JSON
|
|
141
|
+
snapshot of the workflow.
|
|
142
|
+
|
|
143
|
+
### Tuning how long a call waits
|
|
144
|
+
|
|
145
|
+
`get_next_review` and `request_review` each block in **one** call for up to the **poll window**,
|
|
146
|
+
then return `keep_waiting` and the agent re-calls. Two limits bound that single call:
|
|
147
|
+
|
|
148
|
+
| Knob | Where | Effect |
|
|
149
|
+
|------|-------|--------|
|
|
150
|
+
| `AUTO_REVIEW_POLL_SECONDS` | config `env` (stdio) / `--poll-seconds` (HTTP) | how long the server holds before `keep_waiting`. **Must stay under ~270 s** (see below). |
|
|
151
|
+
| `timeout` (ms) | per-server field in `.mcp.json` | Claude Code's per-call cap (default **60000**). Must be **≥ the poll window**. Overrides `MCP_TOOL_TIMEOUT`; not extended by progress. |
|
|
152
|
+
|
|
153
|
+
Handoffs themselves are event-driven (instant) regardless — the poll window only sets how long an
|
|
154
|
+
*idle* waiter holds before re-polling.
|
|
155
|
+
|
|
156
|
+
> ⚠️ **Hard ~5-minute ceiling on a single HTTP hold.** Every MCP client here is Node-based (the
|
|
157
|
+
> stdio proxy and the poller CLI both use Node's `fetch`/undici, whose default `headersTimeout` is
|
|
158
|
+
> **300 s**), so a long-poll held longer than ~5 min dies as `fetch failed`. Worse, the abandoned
|
|
159
|
+
> request leaves a server-side waiter that could swallow a batch. **So keep
|
|
160
|
+
> `AUTO_REVIEW_POLL_SECONDS` ≤ ~270 s** (the shipped configs use **240**). You cannot get a longer
|
|
161
|
+
> *single* hold by raising the window — for longer **effective** waits, the poller CLI loops over
|
|
162
|
+
> many ≤270 s polls (see [the Codex section](#avoiding-re-polls-on-codex-the-shell-poll-command)),
|
|
163
|
+
> and the CLI never fails fatally on a hiccup.
|
|
164
|
+
|
|
165
|
+
Two more caveats:
|
|
166
|
+
|
|
167
|
+
- **The coordinator is a singleton.** Changing the poll window only takes effect on a fresh
|
|
168
|
+
coordinator — kill the running one (`fuser -k <port>/tcp`, or `kill` the pid on that port) so the
|
|
169
|
+
next agent restarts it with the new value. Both agents should use the same window.
|
|
170
|
+
- Across machines (HTTP transport), a dropped connection isn't retried until the next poll; prefer
|
|
171
|
+
a shorter window there too.
|
|
172
|
+
|
|
173
|
+
### Using Codex instead of Claude Code
|
|
174
|
+
|
|
175
|
+
Codex (OpenAI) works as the client, but it's the opposite of Claude Code on timeouts:
|
|
176
|
+
|
|
177
|
+
- It reads `~/.codex/config.toml`, **not** `.mcp.json`, and **ignores the `timeout` field**.
|
|
178
|
+
- All Codex harnesses (CLI, VS Code extension, Windows app) enforce a **hard ~120 s
|
|
179
|
+
`awaiting tools/call` deadline** that `tool_timeout_sec` does **not** reliably raise
|
|
180
|
+
([openai/codex#13831](https://github.com/openai/codex/issues/13831)). So unlike Claude Code, a
|
|
181
|
+
single blocking call **cannot exceed ~120 s** on Codex.
|
|
182
|
+
|
|
183
|
+
Therefore, do the **reverse** of the Claude tuning: keep the poll window safely **under** 120 s so
|
|
184
|
+
the coordinator returns `keep_waiting` before Codex gives up, and the agent re-polls. `90` is a good
|
|
185
|
+
value. See [`configs/codex.config.toml`](configs/codex.config.toml):
|
|
186
|
+
|
|
187
|
+
```toml
|
|
188
|
+
[mcp_servers.auto-review]
|
|
189
|
+
command = "npx"
|
|
190
|
+
args = ["-y", "@permissionbrick/auto-review-mcp", "--role", "developer"] # reviewer in its own config
|
|
191
|
+
env = { AUTO_REVIEW_POLL_SECONDS = "90" } # MUST be < Codex's ~120 s ceiling
|
|
192
|
+
startup_timeout_sec = 30
|
|
193
|
+
tool_timeout_sec = 110
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Handoffs are still instant — the window only sets how often an idle waiter re-polls. The
|
|
197
|
+
singleton-coordinator rule still applies: both agents need the same `AUTO_REVIEW_POLL_SECONDS`, and
|
|
198
|
+
you must kill any running coordinator for a changed window to take effect.
|
|
199
|
+
|
|
200
|
+
#### Avoiding re-polls on Codex: the shell poll command
|
|
201
|
+
|
|
202
|
+
Re-polling under 120 s works but is chatty. To get long, quiet waits anyway, there's a built-in
|
|
203
|
+
escape hatch: an MCP tool call is capped at ~120 s, but a **shell command isn't**. When
|
|
204
|
+
`get_next_review` / `request_review` is killed by Codex's `timed out awaiting tools/call` error, the
|
|
205
|
+
agent never receives a result — so the tool **descriptions** (not the responses) tell it to fall
|
|
206
|
+
back to a blocking shell command that waits without the limit and prints the same JSON:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
# the tool description already prints the exact command (absolute paths, ready to paste); by name:
|
|
210
|
+
npx -y -p @permissionbrick/auto-review-mcp auto-review-cli next-review --port 8765 --timeout 1500 # reviewer
|
|
211
|
+
npx -y -p @permissionbrick/auto-review-mcp auto-review-cli await-verdict --port 8765 --timeout 1500 # developer
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
These hit a plain-HTTP long-poll endpoint on the coordinator, hide `keep_waiting` internally, and
|
|
215
|
+
block until there's a real result (give the command a long *command* timeout, ~25 min). The agent
|
|
216
|
+
should use them **only after an actual timeout error** — a normal `keep_waiting` return just means
|
|
217
|
+
"call the tool again", and running the shell command then would be a redundant double-wait.
|
|
218
|
+
|
|
219
|
+
With this fallback you can even set a **long** poll window (so the MCP call carries the fast case
|
|
220
|
+
within 120 s and the shell command carries longer waits). Verify it with `npm run demo:cli`.
|
|
221
|
+
|
|
222
|
+
## Suggested prompts
|
|
223
|
+
|
|
224
|
+
The tools are self-describing, so the prompts can be short.
|
|
225
|
+
|
|
226
|
+
**Developer:**
|
|
227
|
+
> You are the *developer*. First call `initialize_review_session` (the `auto-review` MCP) with the
|
|
228
|
+
> absolute path of this repo. Then implement <the task> in small, self-contained batches. After each
|
|
229
|
+
> batch, call `request_review` with a clear summary and commit message, and follow whatever it
|
|
230
|
+
> returns: on `changes_requested`, fix the issue and resubmit; on `approved`, continue with the next
|
|
231
|
+
> batch. Do not run `git commit` yourself. When the whole task is done and the last batch is
|
|
232
|
+
> approved, call `signal_complete`.
|
|
233
|
+
|
|
234
|
+
**Reviewer:**
|
|
235
|
+
> You are the *reviewer*. Repeatedly call `get_next_review` (the `auto-review` MCP) to receive each
|
|
236
|
+
> batch (summary + full diff vs HEAD), review it against <the task/spec> and for code quality, then
|
|
237
|
+
> call `submit_review` — `approved`, or `changes_requested` with a clear `issue` and `category`
|
|
238
|
+
> (`spec`/`code`). Keep looping until you get `workflow_complete`.
|
|
239
|
+
|
|
240
|
+
On **Codex**, also allow shell commands and add: *"If a `get_next_review`/`request_review` call ever
|
|
241
|
+
fails with `timed out awaiting tools/call`, follow that tool's CLIENT-TIMEOUT FALLBACK instructions
|
|
242
|
+
— run the shell command it names to wait, then continue."*
|
|
243
|
+
|
|
244
|
+
## Verify
|
|
245
|
+
|
|
246
|
+
End-to-end tests that spin up a throwaway git repo and drive the full loop (keep_waiting → submit →
|
|
247
|
+
review → changes_requested → fix → approve → commit → complete):
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
npm run build
|
|
251
|
+
npm run demo # HTTP path: two SDK clients against a running server
|
|
252
|
+
npm run demo:stdio # node/stdio path: two spawned proxies + auto-started coordinator
|
|
253
|
+
npm run demo:cli # shell poll-command path: MCP for instant ops + cli.js for the blocking wait
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Each prints a checklist and exits non-zero if anything fails.
|
|
257
|
+
|
|
258
|
+
## Notes & limitations (v1)
|
|
259
|
+
|
|
260
|
+
- **State is in-memory.** Restarting the server resets the loop (no batch is mid-flight unless an
|
|
261
|
+
agent is actively blocked). There is no persistence yet.
|
|
262
|
+
- **One developer + one reviewer.** A single batch flows at a time; extra connections share the
|
|
263
|
+
same state.
|
|
264
|
+
- **Commit hooks are respected.** If a pre-commit hook rejects an approved batch, the reviewer gets
|
|
265
|
+
an error and the batch stays open to retry or send back as `changes_requested`.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Thin blocking poller for the auto-review coordinator — the shell command an
|
|
4
|
+
* agent runs when its MCP client kills a blocking tool call with a hard timeout
|
|
5
|
+
* (e.g. Codex's ~120s "timed out awaiting tools/call"). A shell process is NOT
|
|
6
|
+
* bound by that MCP per-call limit, so this can wait far longer.
|
|
7
|
+
*
|
|
8
|
+
* It hits a plain-HTTP long-poll endpoint on the coordinator and hides
|
|
9
|
+
* `keep_waiting` internally: it blocks until there is a REAL result, then prints
|
|
10
|
+
* that JSON to stdout and exits. The agent reads stdout and acts on it.
|
|
11
|
+
*
|
|
12
|
+
* next-review wait for the next batch to review (reviewer)
|
|
13
|
+
* await-verdict wait for the verdict of the batch you already submitted (developer)
|
|
14
|
+
* status print the current workflow status once and exit
|
|
15
|
+
*
|
|
16
|
+
* Flags: --port (8765) --host (127.0.0.1) --timeout <seconds> (1500) and the
|
|
17
|
+
* usual --poll-seconds / --max-diff-bytes (only used if it has to start the
|
|
18
|
+
* coordinator). On reaching --timeout it prints {"status":"keep_waiting"} and
|
|
19
|
+
* exits 0, so the agent can simply run it again.
|
|
20
|
+
*/
|
|
21
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
22
|
+
import { ensureCoordinator } from "./launch.js";
|
|
23
|
+
const ENDPOINTS = {
|
|
24
|
+
"next-review": { path: "/reviewer/next-review" },
|
|
25
|
+
"await-verdict": { path: "/developer/await-verdict" },
|
|
26
|
+
};
|
|
27
|
+
function out(obj) {
|
|
28
|
+
process.stdout.write(`${JSON.stringify(obj, null, 2)}\n`);
|
|
29
|
+
}
|
|
30
|
+
function errlog(msg) {
|
|
31
|
+
process.stderr.write(`[auto-review cli] ${msg}\n`);
|
|
32
|
+
}
|
|
33
|
+
function parse(argv) {
|
|
34
|
+
let sub = "";
|
|
35
|
+
const opts = new Map();
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const a = argv[i];
|
|
38
|
+
if (!a.startsWith("--")) {
|
|
39
|
+
if (!sub)
|
|
40
|
+
sub = a;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const key = a.slice(2);
|
|
44
|
+
const eq = key.indexOf("=");
|
|
45
|
+
if (eq >= 0)
|
|
46
|
+
opts.set(key.slice(0, eq), key.slice(eq + 1));
|
|
47
|
+
else if (i + 1 < argv.length && !argv[i + 1].startsWith("--"))
|
|
48
|
+
opts.set(key, argv[++i]);
|
|
49
|
+
else
|
|
50
|
+
opts.set(key, "true");
|
|
51
|
+
}
|
|
52
|
+
const get = (k, env, dflt) => opts.get(k) ?? process.env[env] ?? dflt;
|
|
53
|
+
return {
|
|
54
|
+
sub,
|
|
55
|
+
host: get("host", "AUTO_REVIEW_HOST", "127.0.0.1"),
|
|
56
|
+
port: Number(get("port", "AUTO_REVIEW_PORT", "8765")),
|
|
57
|
+
timeoutSeconds: Number(get("timeout", "AUTO_REVIEW_CLI_TIMEOUT", "3600")),
|
|
58
|
+
pollSeconds: Number(get("poll-seconds", "AUTO_REVIEW_POLL_SECONDS", "40")),
|
|
59
|
+
maxDiffBytes: Number(get("max-diff-bytes", "AUTO_REVIEW_MAX_DIFF_BYTES", "200000")),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function main() {
|
|
63
|
+
const cfg = parse(process.argv.slice(2));
|
|
64
|
+
const base = `http://${cfg.host}:${cfg.port}`;
|
|
65
|
+
if (cfg.sub === "status") {
|
|
66
|
+
await ensureCoordinator({ ...cfg, log: errlog });
|
|
67
|
+
const r = await fetch(`${base}/healthz`);
|
|
68
|
+
const body = (await r.json());
|
|
69
|
+
out(body.status ?? body);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const endpoint = ENDPOINTS[cfg.sub];
|
|
73
|
+
if (!endpoint) {
|
|
74
|
+
errlog(`unknown command '${cfg.sub}'. Use: next-review | await-verdict | status`);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
const target = { ...cfg, log: errlog };
|
|
78
|
+
await ensureCoordinator(target);
|
|
79
|
+
const url = `${base}${endpoint.path}`;
|
|
80
|
+
const deadline = Date.now() + cfg.timeoutSeconds * 1000;
|
|
81
|
+
// Cap each HTTP request well under Node/undici's default 300s headersTimeout,
|
|
82
|
+
// which otherwise kills a longer-held long-poll with "fetch failed". The
|
|
83
|
+
// coordinator returns keep_waiting within its (smaller) poll window, so this
|
|
84
|
+
// cap normally never bites; it just bounds a single request for safety.
|
|
85
|
+
const PER_REQUEST_MS = 270_000;
|
|
86
|
+
// Loop until a real result or our own --timeout budget. Transient errors
|
|
87
|
+
// (undici timeout, a coordinator restart, a network blip) are NEVER fatal
|
|
88
|
+
// mid-wait — we self-heal and keep polling, so one shell command can wait
|
|
89
|
+
// for the whole budget (default 1h) without ever returning "fetch failed".
|
|
90
|
+
for (;;) {
|
|
91
|
+
const remaining = deadline - Date.now();
|
|
92
|
+
if (remaining <= 0) {
|
|
93
|
+
out({ status: "keep_waiting", message: "Time budget elapsed; run this command again to keep waiting." });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let body;
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(Math.min(remaining, PER_REQUEST_MS)) });
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
errlog(`coordinator returned HTTP ${res.status}; retrying`);
|
|
101
|
+
await sleep(2000);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
body = (await res.json());
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
// Per-request cap reached, undici headers/body timeout, or a blip. Keep
|
|
108
|
+
// looping; re-ensure the coordinator in case it died, then retry.
|
|
109
|
+
if (deadline - Date.now() > 1500) {
|
|
110
|
+
errlog(`poll hiccup (${e?.name ?? e}); retrying`);
|
|
111
|
+
await ensureCoordinator({ ...target, log: () => { } }).catch(() => { });
|
|
112
|
+
await sleep(1000);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (body.status === "keep_waiting")
|
|
117
|
+
continue; // coordinator window elapsed → re-poll
|
|
118
|
+
out(body);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
main().catch((e) => {
|
|
123
|
+
errlog(`FATAL: ${e?.stack ?? String(e)}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool descriptions. These are the *contract*: they must teach the whole
|
|
3
|
+
* auto-review protocol so the two agents can self-orchestrate from the tool
|
|
4
|
+
* descriptions alone, with no extra prompting.
|
|
5
|
+
*/
|
|
6
|
+
export const INITIALIZE_SESSION_DESC = `Start the auto-review session by telling the server which git repository this workflow operates on. CALL THIS ONCE, FIRST — before request_review.
|
|
7
|
+
|
|
8
|
+
Pass 'repo_path': the ABSOLUTE path to the root of the git repository you are working in (normally your current working directory). The server performs ALL git operations there — it diffs your working tree against HEAD to show the reviewer, and commits each approved batch — so it MUST be the exact checkout you are editing.
|
|
9
|
+
|
|
10
|
+
The repository is remembered for the lifetime of the server, or until you call this again (which starts a fresh session). Returns {"status":"ok","repo":...,"head":...} on success, or {"status":"error","message":...} if the path is not a git repository.`;
|
|
11
|
+
export const REQUEST_REVIEW_DESC = `Submit a completed batch of work to the reviewer and BLOCK until the reviewer responds. (Call initialize_review_session once first; if you haven't, this returns {"status":"not_initialized"}.)
|
|
12
|
+
|
|
13
|
+
WHEN TO CALL: every time you finish a self-contained, coherent chunk of the task — a logical, commit-sized unit (a part of a feature, an enclosed subset of the task). Work in small, reviewable increments rather than one giant change.
|
|
14
|
+
|
|
15
|
+
WHAT TO PASS:
|
|
16
|
+
- summary: what you changed and WHY, in enough detail for a reviewer to judge it against the task/spec.
|
|
17
|
+
- commit_message: a good commit message for this batch (this exact message is used for the commit on approval).
|
|
18
|
+
|
|
19
|
+
IMPORTANT: Do NOT stage or commit anything yourself. The server automatically snapshots ALL current changes in the working tree (it runs 'git add -A') and computes the diff against HEAD. The server commits the batch for you, and only after the reviewer approves it. Never run 'git commit'.
|
|
20
|
+
|
|
21
|
+
This call BLOCKS until the review is finished. It returns exactly one of:
|
|
22
|
+
- {"status":"approved","commit_sha":...} → the reviewer approved and the server has committed this batch. Move on to the NEXT batch of work.
|
|
23
|
+
- {"status":"changes_requested","issue":...,"category":"spec"|"code"} → the reviewer found a problem. 'category' is "spec" (it does not meet the requirement) or "code" (a code-level defect/bug/quality issue). Read 'issue', fix it, then call request_review again with an updated summary/commit_message.
|
|
24
|
+
- {"status":"keep_waiting"} → the review is still in progress; this returned only to keep the connection alive. IMMEDIATELY call request_review again with the SAME arguments to keep waiting. (Re-submitting unchanged work does not create a duplicate review.)
|
|
25
|
+
- {"status":"nothing_to_review"} → there are no changes versus HEAD. Do some work first, then call again.
|
|
26
|
+
|
|
27
|
+
Only one batch is reviewed at a time.`;
|
|
28
|
+
export const SIGNAL_COMPLETE_DESC = `Signal that the ENTIRE task is finished and there is nothing left to implement (typically right after your final batch was approved).
|
|
29
|
+
|
|
30
|
+
This tells the reviewer the workflow is over: the reviewer's next get_next_review will return {"status":"workflow_complete"} so it can stop waiting. Optionally pass a closing 'note'. Do not call this while you still have batches to submit.`;
|
|
31
|
+
export const GET_NEXT_REVIEW_DESC = `Ask for the next batch of work to review and BLOCK until one is available.
|
|
32
|
+
|
|
33
|
+
WHEN TO CALL: FIRST, as soon as you start (you are the reviewer and wait for the developer). Then again every time after you submit a verdict, to wait for the next batch.
|
|
34
|
+
|
|
35
|
+
This call BLOCKS. It returns exactly one of:
|
|
36
|
+
- {"status":"review_ready","batch_id":...,"summary":...,"commit_message":...,"diff":...,"diff_stat":...} → a developer submitted a batch. 'summary' is the developer's description, 'commit_message' their proposed message, 'diff' is the FULL unified diff of all current changes versus HEAD (i.e. exactly this batch), 'diff_stat' is the file/line summary. Review it against BOTH the task/spec AND code quality, then call submit_review with this same batch_id.
|
|
37
|
+
- {"status":"keep_waiting"} → nothing is ready yet; this returned only to keep the connection alive. IMMEDIATELY call get_next_review again to keep waiting.
|
|
38
|
+
- {"status":"workflow_complete"} → the developer signalled the whole task is done. Stop; there is nothing left to review.
|
|
39
|
+
|
|
40
|
+
You review one batch at a time. After you receive a "review_ready" batch you must eventually call submit_review for that batch_id before any further work can flow.`;
|
|
41
|
+
export const SUBMIT_REVIEW_DESC = `Record your verdict on the batch you are currently reviewing and unblock the developer.
|
|
42
|
+
|
|
43
|
+
WHAT TO PASS:
|
|
44
|
+
- batch_id: the id you received from get_next_review.
|
|
45
|
+
- verdict: "approved" or "changes_requested".
|
|
46
|
+
- "approved" → the batch meets the requirement and the code is acceptable. The server IMMEDIATELY commits the batch (using the developer's commit_message) and reports the new commit to the developer. Do not commit anything yourself.
|
|
47
|
+
- "changes_requested" → something is wrong. You MUST also pass:
|
|
48
|
+
- issue: a clear, actionable description of what is wrong and what to change.
|
|
49
|
+
- category: "spec" (it fails to meet the task/requirement) or "code" (a code-level defect such as a bug, regression, or quality problem).
|
|
50
|
+
The developer receives this, fixes it, and resubmits.
|
|
51
|
+
|
|
52
|
+
After submitting, call get_next_review again to wait for the next batch.`;
|
|
53
|
+
export const WORKFLOW_STATUS_DESC = `Read-only. Return the current state of the auto-review workflow: the phase (idle / awaiting_review / reviewing), a summary of the batch currently under review (if any), the last verdict, the number of completed batches, and the repo path. Does not block and never changes anything.`;
|
|
54
|
+
/**
|
|
55
|
+
* Appended to a blocking tool's description so the agent can recover when its
|
|
56
|
+
* MCP client kills the call with a hard timeout (e.g. Codex's ~120s
|
|
57
|
+
* "timed out awaiting tools/call"). On that timeout there is NO tool response,
|
|
58
|
+
* so this guidance must live in the static description — not in any result.
|
|
59
|
+
*/
|
|
60
|
+
export function timeoutFallbackNote(pollCommand, waits) {
|
|
61
|
+
return `CLIENT-TIMEOUT FALLBACK — only when THIS call FAILS with a client-side timeout error (e.g. "timed out awaiting tools/call after 120s"): your MCP client gave up before the wait finished, so you never received a result. Do NOT just retry the tool (it will time out again the same way). Instead run this shell command to wait for ${waits} without the per-call MCP limit — it blocks until there is a real result and then prints the same JSON:
|
|
62
|
+
|
|
63
|
+
${pollCommand}
|
|
64
|
+
|
|
65
|
+
RUN IT SO IT ACTUALLY BLOCKS — it must stay in the foreground until it returns; do not let it drop into a background task and do not poll it with repeated short waits.
|
|
66
|
+
- On Codex: if exec_command returns a session_id because the process is still running (exec_command may yield a session after ~30s even when you pass a longer timeout), do NOT poll repeatedly with short waits. Immediately call write_stdin on that session with chars:"" and a long yield_time_ms (~1500000 ms, i.e. ~25 min) so you block until the command produces its result; if it yields again before finishing, repeat the blocking write_stdin.
|
|
67
|
+
- On other harnesses: just give the command a long command/exec timeout (~25 min) and let it block.
|
|
68
|
+
|
|
69
|
+
Only use this on an actual timeout error. If the tool RETURNS normally — including a "keep_waiting" result — do NOT run the shell command; just call the tool again as usual. Running it after a normal return would be a redundant double-wait.`;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=descriptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descriptions.js","sourceRoot":"","sources":["../src/descriptions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,uBAAuB,GAAG;;;;6PAIsN,CAAC;AAE9P,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;sCAgBG,CAAC;AAEvC,MAAM,CAAC,MAAM,oBAAoB,GAAG;;gPAE4M,CAAC;AAEjP,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;oKASgI,CAAC;AAErK,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;yEAWuC,CAAC;AAE1E,MAAM,CAAC,MAAM,oBAAoB,GAAG,2RAA2R,CAAC;AAEhU;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAmB,EAAE,KAAa;IACpE,OAAO,0UAA0U,KAAK;;MAElV,WAAW;;;;;;iPAMgO,CAAC;AAClP,CAAC"}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin async wrapper around the `git` CLI for the project repo under review.
|
|
3
|
+
*
|
|
4
|
+
* All commands use `git -C <dir> ...` and `execFile` (no shell) so paths and
|
|
5
|
+
* messages with spaces or special characters are safe.
|
|
6
|
+
*/
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
export class GitError extends Error {
|
|
12
|
+
stderr;
|
|
13
|
+
constructor(message, stderr) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.stderr = stderr;
|
|
16
|
+
this.name = "GitError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class GitRepo {
|
|
20
|
+
dir;
|
|
21
|
+
constructor(dir) {
|
|
22
|
+
this.dir = dir;
|
|
23
|
+
}
|
|
24
|
+
async git(args) {
|
|
25
|
+
try {
|
|
26
|
+
const { stdout, stderr } = await execFileAsync("git", ["-C", this.dir, ...args], {
|
|
27
|
+
maxBuffer: 64 * 1024 * 1024, // 64MB — big diffs are fine
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
});
|
|
30
|
+
return { stdout, stderr };
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const e = err;
|
|
34
|
+
throw new GitError(`git ${args.join(" ")} failed: ${e.message ?? "unknown error"}`, e.stderr ?? e.stdout);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Throws a helpful error if `dir` is not inside a git work tree. */
|
|
38
|
+
async assertRepo() {
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await this.git(["rev-parse", "--is-inside-work-tree"]);
|
|
41
|
+
if (stdout.trim() !== "true") {
|
|
42
|
+
throw new GitError(`${this.dir} is not a git work tree`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err instanceof GitError) {
|
|
47
|
+
throw new GitError(`'${this.dir}' is not a git repository. Point --repo / AUTO_REVIEW_REPO at the ` +
|
|
48
|
+
`project the developer agent is editing (it must be a git repo).`, err.stderr);
|
|
49
|
+
}
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Current HEAD sha, or null if the repo has no commits yet. */
|
|
54
|
+
async head() {
|
|
55
|
+
try {
|
|
56
|
+
const { stdout } = await this.git(["rev-parse", "HEAD"]);
|
|
57
|
+
return stdout.trim();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Stage everything (`git add -A`) and capture the full diff vs HEAD, including
|
|
65
|
+
* new and deleted files. Staging means a later commit captures exactly what
|
|
66
|
+
* was reviewed.
|
|
67
|
+
*/
|
|
68
|
+
async captureDiff(maxBytes) {
|
|
69
|
+
await this.git(["add", "-A"]);
|
|
70
|
+
const { stdout: stat } = await this.git(["diff", "--cached", "--stat"]);
|
|
71
|
+
const { stdout: fullDiff } = await this.git(["diff", "--cached"]);
|
|
72
|
+
const fullBytes = Buffer.byteLength(fullDiff, "utf8");
|
|
73
|
+
const isEmpty = fullDiff.trim() === "";
|
|
74
|
+
const diffHash = createHash("sha1").update(fullDiff).digest("hex");
|
|
75
|
+
let diff = fullDiff;
|
|
76
|
+
let truncated = false;
|
|
77
|
+
if (fullBytes > maxBytes) {
|
|
78
|
+
truncated = true;
|
|
79
|
+
diff =
|
|
80
|
+
fullDiff.slice(0, maxBytes) +
|
|
81
|
+
`\n\n... [diff truncated: ${fullBytes} bytes total, showing first ${maxBytes}. ` +
|
|
82
|
+
`See the --stat summary above for the full file list and inspect the working tree directly if needed.]`;
|
|
83
|
+
}
|
|
84
|
+
return { diff, diffStat: stat.trim(), diffHash, isEmpty, truncated, fullBytes };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Commit the currently-staged changes with `message` plus a Reviewed-by
|
|
88
|
+
* trailer. Returns the new commit sha. Throws GitError on failure (e.g. a
|
|
89
|
+
* pre-commit hook rejected the change) so the caller can keep the batch open.
|
|
90
|
+
*/
|
|
91
|
+
async commit(message) {
|
|
92
|
+
await this.git(["commit", "-m", message, "--trailer", "Reviewed-by: auto-review"]);
|
|
93
|
+
const sha = await this.head();
|
|
94
|
+
if (!sha)
|
|
95
|
+
throw new GitError("commit appeared to succeed but HEAD is still unset");
|
|
96
|
+
return sha;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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"}
|