@basein/runner 0.2.11 → 0.3.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.
Files changed (144) hide show
  1. package/README.md +540 -350
  2. package/dist/auth/client.d.ts +65 -3
  3. package/dist/auth/client.js +305 -53
  4. package/dist/bin/bir-hooks.d.ts +72 -31
  5. package/dist/bin/bir-hooks.js +176 -284
  6. package/dist/bin/bir-proxy.d.ts +17 -2
  7. package/dist/bin/bir-proxy.js +222 -28
  8. package/dist/bin/bir-scenario.d.ts +61 -10
  9. package/dist/bin/bir-scenario.js +704 -135
  10. package/dist/bin/bir.d.ts +14 -8
  11. package/dist/bin/bir.js +2937 -488
  12. package/dist/bin/investigate.js +41 -16
  13. package/dist/bin/scenario-edit.d.ts +43 -10
  14. package/dist/bin/scenario-edit.js +281 -80
  15. package/dist/bin/scenario-liveness.d.ts +194 -0
  16. package/dist/bin/scenario-liveness.js +459 -0
  17. package/dist/bin/scenario-stderr.d.ts +43 -0
  18. package/dist/bin/scenario-stderr.js +76 -0
  19. package/dist/bin/service-read.d.ts +88 -0
  20. package/dist/bin/service-read.js +128 -0
  21. package/dist/bin/setup.d.ts +19 -0
  22. package/dist/bin/setup.js +146 -20
  23. package/dist/config/adapters/claude-code.d.ts +92 -14
  24. package/dist/config/adapters/claude-code.js +291 -101
  25. package/dist/config/adapters/generic.d.ts +4 -1
  26. package/dist/config/adapters/generic.js +13 -14
  27. package/dist/config/generate.d.ts +392 -14
  28. package/dist/config/generate.js +666 -19
  29. package/dist/config/mcp-approval.d.ts +76 -0
  30. package/dist/config/mcp-approval.js +210 -0
  31. package/dist/config/node-path.d.ts +42 -0
  32. package/dist/config/node-path.js +102 -0
  33. package/dist/config/overrides.d.ts +62 -0
  34. package/dist/config/overrides.js +136 -0
  35. package/dist/config/resolve.d.ts +70 -8
  36. package/dist/config/resolve.js +186 -15
  37. package/dist/control/client.d.ts +22 -0
  38. package/dist/control/client.js +26 -7
  39. package/dist/control/correlation.d.ts +23 -5
  40. package/dist/control/correlation.js +0 -0
  41. package/dist/control/daemon.d.ts +400 -18
  42. package/dist/control/daemon.js +1105 -102
  43. package/dist/control/discovery.d.ts +77 -5
  44. package/dist/control/discovery.js +186 -11
  45. package/dist/control/ensure-hook.d.ts +61 -0
  46. package/dist/control/ensure-hook.js +160 -8
  47. package/dist/control/paths.d.ts +26 -0
  48. package/dist/control/paths.js +34 -0
  49. package/dist/control/pin-guard.d.ts +83 -0
  50. package/dist/control/pin-guard.js +534 -0
  51. package/dist/control/recorder-keeper.d.ts +229 -0
  52. package/dist/control/recorder-keeper.js +721 -0
  53. package/dist/control/recorder-main.d.ts +73 -0
  54. package/dist/control/recorder-main.js +778 -0
  55. package/dist/control/server.d.ts +707 -31
  56. package/dist/control/server.js +2803 -306
  57. package/dist/control/transcript.d.ts +21 -1
  58. package/dist/control/transcript.js +378 -112
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +5 -1
  61. package/dist/jsonrpc/framing.d.ts +76 -12
  62. package/dist/jsonrpc/framing.js +216 -59
  63. package/dist/proxy/intercept.d.ts +49 -5
  64. package/dist/proxy/intercept.js +188 -59
  65. package/dist/proxy/relay.d.ts +16 -0
  66. package/dist/proxy/relay.js +77 -5
  67. package/dist/proxy/revive.d.ts +122 -0
  68. package/dist/proxy/revive.js +251 -0
  69. package/dist/proxy/session.d.ts +222 -14
  70. package/dist/proxy/session.js +708 -99
  71. package/dist/record/housekeeping.d.ts +71 -0
  72. package/dist/record/housekeeping.js +417 -0
  73. package/dist/record/recorder.d.ts +51 -3
  74. package/dist/record/recorder.js +3 -0
  75. package/dist/record/redact.d.ts +103 -9
  76. package/dist/record/redact.js +659 -30
  77. package/dist/record/remote-recorder.d.ts +327 -32
  78. package/dist/record/remote-recorder.js +1517 -187
  79. package/dist/record/switchable-recorder.d.ts +126 -0
  80. package/dist/record/switchable-recorder.js +351 -0
  81. package/dist/record/truncate.d.ts +40 -1
  82. package/dist/record/truncate.js +111 -4
  83. package/dist/replay/bash-channel.d.ts +35 -0
  84. package/dist/replay/bash-channel.js +133 -0
  85. package/dist/replay/bundle.d.ts +10 -0
  86. package/dist/replay/bundle.js +10 -0
  87. package/dist/replay/controller.d.ts +371 -20
  88. package/dist/replay/controller.js +1185 -111
  89. package/dist/replay/derive.d.ts +1 -0
  90. package/dist/replay/derive.js +81 -24
  91. package/dist/replay/executor.d.ts +51 -16
  92. package/dist/replay/executor.js +114 -41
  93. package/dist/replay/handover.d.ts +54 -1
  94. package/dist/replay/handover.js +55 -5
  95. package/dist/replay/logic.d.ts +62 -10
  96. package/dist/replay/logic.js +140 -22
  97. package/dist/replay/plan.d.ts +201 -27
  98. package/dist/replay/plan.js +581 -141
  99. package/dist/replay/safety.d.ts +72 -0
  100. package/dist/replay/safety.js +303 -0
  101. package/dist/replay/source-run.d.ts +10 -0
  102. package/dist/replay/source-run.js +80 -35
  103. package/dist/replay/targets.d.ts +15 -1
  104. package/dist/replay/targets.js +62 -4
  105. package/dist/replay/tool-error.d.ts +15 -1
  106. package/dist/replay/tool-error.js +33 -1
  107. package/dist/replay/types.d.ts +7 -4
  108. package/dist/upstream/client.d.ts +56 -3
  109. package/dist/upstream/client.js +61 -4
  110. package/dist/upstream/headers.d.ts +43 -0
  111. package/dist/upstream/headers.js +90 -0
  112. package/dist/upstream/http-client.d.ts +213 -22
  113. package/dist/upstream/http-client.js +1034 -168
  114. package/dist/upstream/http-transport.d.ts +143 -0
  115. package/dist/upstream/http-transport.js +391 -0
  116. package/dist/upstream/lazy-client.d.ts +8 -0
  117. package/dist/upstream/lazy-client.js +29 -1
  118. package/dist/upstream/sse.d.ts +78 -0
  119. package/dist/upstream/sse.js +189 -0
  120. package/dist/upstream/stdio-client.d.ts +61 -1
  121. package/dist/upstream/stdio-client.js +356 -33
  122. package/dist/util/atomic-write.d.ts +29 -0
  123. package/dist/util/atomic-write.js +96 -0
  124. package/dist/util/journal.d.ts +15 -0
  125. package/dist/util/journal.js +48 -3
  126. package/dist/util/log.d.ts +10 -0
  127. package/dist/util/log.js +28 -1
  128. package/dist/util/off-switch.d.ts +46 -0
  129. package/dist/util/off-switch.js +91 -0
  130. package/dist/util/service-health.d.ts +133 -0
  131. package/dist/util/service-health.js +217 -0
  132. package/docs/BaseInstRunner.md +694 -621
  133. package/docs/calculatedReplay.md +1625 -1244
  134. package/docs/calculatedReplayGuide.md +1212 -879
  135. package/docs/installRun.md +1013 -772
  136. package/docs/loginWeb.md +627 -607
  137. package/docs/mcpmark.md +49 -16
  138. package/docs/my-first-sample.md +569 -545
  139. package/docs/quickstart.md +384 -271
  140. package/docs/t-bench.md +406 -394
  141. package/package.json +58 -57
  142. package/production.md +1371 -0
  143. package/scripts/install.ps1 +97 -6
  144. package/scripts/install.sh +104 -7
@@ -1,545 +1,569 @@
1
- # Your first sample — run one MCPMark task and watch it pay for itself
2
-
3
- A complete, from-zero manual. By the end you will have run one real agent
4
- benchmark task, graded it against a live database, and then watched
5
- BaseInstRunner replay the *same* task for **zero model tokens** — and you will
6
- see the dollar figure that saved.
7
-
8
- This is the hands-on companion to [mcpmark.md](mcpmark.md), which explains the
9
- *why* in depth. This document is the *what to type*, in order, with the numbers
10
- a real run produced on 2026-09-07 so you know what "working" looks like.
11
-
12
- > **The one task.** Everything below uses a single MCPMark task,
13
- > `postgres/easy/employees/hiring_year_summary`. It is the smallest task in the
14
- > benchmark that still writes to the database, which makes it the honest place
15
- > to start.
16
-
17
- ---
18
-
19
- ## 0. What MCPMark is (and why we use it)
20
-
21
- **MCPMark is a standard agents benchmark.** It measures how well an AI agent can
22
- drive real tools, through the [Model Context Protocol
23
- (MCP)](https://modelcontextprotocol.io), to change real systems — not to chat
24
- about them.
25
-
26
- The facts worth knowing before you start:
27
-
28
- | | |
29
- |---|---|
30
- | Who | The `eval-sys` research group · [github.com/eval-sys/mcpmark](https://github.com/eval-sys/mcpmark) · [paper (arXiv 2509.24002)](https://arxiv.org/abs/2509.24002) · [mcpmark.ai](https://mcpmark.ai) |
31
- | What it tests | An agent using **five MCP services**: Postgres, GitHub, Filesystem, Notion, Playwright |
32
- | Size | **127 standard tasks + 50 easy** (the repo has grown past the paper's five services) |
33
- | How it grades | Each task ships a `verify.py` that opens a real connection and **queries the actual state**, not the agent's transcript. Exit 0 = PASS |
34
- | Metrics | `pass@1` (one try), `pass@k`, and `pass^k` — *stability*: did it pass **all** k independent runs |
35
- | State of the art | On MCPMark's verified set, the strongest model reports about **92.9%**; most models sit far lower, and `pass^4` collapses for everyone |
36
-
37
- Two of those facts are why this benchmark suits BaseInstRunner so well:
38
-
39
- 1. **It grades the world, not the words.** A replayed run that produces the
40
- right rows passes exactly like a hand-typed one. There is no transcript to
41
- fool.
42
- 2. **`pass^k` punishes inconsistency.** The benchmark runs each task several
43
- times against a reset database and asks whether *every* run passed. A model
44
- that re-decides the plan token-by-token every time is exactly what
45
- BaseInstRunner removes — it runs the *same* known-good plan each time.
46
-
47
- That is the whole idea you are about to see working: **solve the task once, then
48
- stop paying to re-solve it.**
49
-
50
- ---
51
-
52
- ## 1. What you are about to build
53
-
54
- ```
55
- Part A — the benchmark, on its own
56
- seed a Postgres DB → agent solves the task → verify.py grades it → PASS
57
-
58
- Parts B–D — the same task, through BaseInstRunner
59
- claude -p ──mcp__postgres__*──▶ bir-proxy ──▶ postgres-mcp ──▶ PostgreSQL
60
- │ │ records every call
61
- └──mcp__bir__run_scenario──▶ bir-hooks (control server)
62
- │ matches the prompt, arms the plan,
63
- ▼ runs all 6 steps for $0 model tokens
64
- BaseIn service ──▶ books the saving
65
- ```
66
-
67
- You will run the task **three ways**, and the difference between them is the
68
- point:
69
-
70
- | Run | Runner | What the model does | What it costs |
71
- |---|---|---|---|
72
- | **Baseline** | off | discovers and solves the task | full price |
73
- | **Record** | on, replay **off** | solves it once; bir *records* the plan | full price, one time |
74
- | **Replay** | on, replay **on** | one tool call reads a pre-computed result | a fraction |
75
-
76
- ---
77
-
78
- ## 2. Prerequisites
79
-
80
- Check each one. The whole setup takes about ten minutes, most of it downloads.
81
-
82
- | # | You need | Check | If missing |
83
- |---|---|---|---|
84
- | 1 | **Node 20+** | `node -v` | [nodejs.org](https://nodejs.org) |
85
- | 2 | **Claude Code** | `claude --version` | Anthropic's CLI; this manual drives it with `claude -p` |
86
- | 3 | **PostgreSQL 17**, running | `psql --version` and the service is up | see §2.1 |
87
- | 4 | **uv / uvx** | `uvx --version` | [astral.sh/uv](https://docs.astral.sh/uv/) — runs `postgres-mcp` and `verify.py` |
88
- | 5 | **The runner** | `bir --version` | `npm i -g @basein/runner` |
89
- | 6 | **A BaseIn account + API URL** | `bir login` succeeds | your service address, e.g. `https://api.your-domain.com` |
90
-
91
- > **`BIR_AUTH_URL` must be the API host**, normally `https://api.<domain>`, not
92
- > the docs or app website. Point it at a website and `bir login` refuses it,
93
- > saying what answered instead. Set it once, then open a **fresh** terminal — an
94
- > existing one keeps the value it started with.
95
-
96
- `bir login` signs you in through the browser: it prints a link and a short code
97
- and waits for you to approve it. Nothing is typed into the terminal, and it works
98
- for accounts that only sign in with Google.
99
-
100
- ### 2.1 Postgres, without Docker
101
-
102
- MCPMark's own harness runs Postgres in Docker (`pgvector/pgvector:pg17`). If the
103
- machine has no Docker — this one did not — install PostgreSQL 17 **natively**;
104
- it is the same major version, and this task needs no vector support. Disclose
105
- the deviation in any published result.
106
-
107
- ```powershell
108
- winget install --id PostgreSQL.PostgreSQL.17 -e `
109
- --accept-package-agreements --accept-source-agreements `
110
- --custom "--mode unattended --superpassword password --serverport 5432 --unattendedmodeui none"
111
- ```
112
-
113
- That sets the `postgres` superuser password to `password`, which is the value
114
- every command and connection string below expects. On macOS:
115
- `brew install postgresql@17 && brew services start postgresql@17`.
116
-
117
- ---
118
-
119
- ## 3. Part A — run the benchmark task on its own
120
-
121
- First prove the plain benchmark works, with no runner in the picture at all.
122
-
123
- ### 3.1 Make a run folder and fetch the task
124
-
125
- ```bash
126
- mkdir -p ~/my-first-sample && cd ~/my-first-sample
127
-
128
- TASK=https://raw.githubusercontent.com/eval-sys/mcpmark/main/tasks/postgres/easy/employees/hiring_year_summary
129
- curl -fsSLO "$TASK/description.md" # what the agent is asked to do
130
- curl -fsSLO "$TASK/verify.py" # MCPMark's grader — unmodified
131
- curl -fsSLO "$TASK/meta.json" # task metadata (difficulty L1, mcp: postgres)
132
- ```
133
-
134
- The task asks the agent to build one table, `employees.hiring_year_summary`,
135
- with a row per hiring year: how many were hired, how many are still employed,
136
- and the retention rate. Small, but a genuine write.
137
-
138
- ### 3.2 The exact prompt
139
-
140
- MCPMark sends `description.md` **verbatim** plus one fixed suffix. Assemble it
141
- once so every run is byte-identical:
142
-
143
- ```bash
144
- { cat description.md
145
- printf '\n\nNote: Based on your understanding, solve the task all at once by yourself, don'\''t ask for my opinions on anything.\n'
146
- } > prompt.txt
147
- ```
148
-
149
- ### 3.3 Seed the database
150
-
151
- Download MCPMark's 33 MB backup and build the `employees` **template**
152
- database from it. Save this as `seed.sh` and run it once:
153
-
154
- ```bash
155
- #!/usr/bin/env bash
156
- set -euo pipefail
157
- PGBIN="${PGBIN:-/c/Program Files/PostgreSQL/17/bin}" # macOS: "$(brew --prefix postgresql@17)/bin"
158
- export PGPASSWORD="${POSTGRES_PASSWORD:-password}"
159
- PGUSER=postgres PGHOST=localhost PGPORT=5432
160
-
161
- [ -f employees.backup ] || curl -fsSL -o employees.backup https://storage.mcpmark.ai/postgres/employees.backup
162
-
163
- "$PGBIN/psql" -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -v ON_ERROR_STOP=1 \
164
- -c "DROP DATABASE IF EXISTS employees;" -c "CREATE DATABASE employees;"
165
- "$PGBIN/pg_restore" -h $PGHOST -p $PGPORT -U $PGUSER -d employees \
166
- --no-owner --no-privileges employees.backup \
167
- || echo "[seed] pg_restore printed notices — checking the result instead"
168
- "$PGBIN/psql" -h $PGHOST -p $PGPORT -U $PGUSER -d employees \
169
- -c "SELECT count(*) AS employees FROM employees.employee;"
170
- echo "[seed] template database 'employees' is ready"
171
- ```
172
-
173
- A healthy seed reports **300024** employees.
174
-
175
- ### 3.4 A fresh database for every run
176
-
177
- MCPMark isolates each run with `CREATE DATABASE … WITH TEMPLATE …` and asserts
178
- the target table is absent. Reproduce that as `reset-db.sh` — **run it before
179
- every single trial**, or the second run finds the table already built and the
180
- task stops being the task:
181
-
182
- ```bash
183
- #!/usr/bin/env bash
184
- set -euo pipefail
185
- PGBIN="${PGBIN:-/c/Program Files/PostgreSQL/17/bin}"
186
- export PGPASSWORD=password; PGUSER=postgres PGHOST=localhost PGPORT=5432
187
- DB="${TRIAL_DB:-hys_trial}"
188
- "$PGBIN/psql" -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -v ON_ERROR_STOP=1 \
189
- -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity
190
- WHERE datname='$DB' AND pid <> pg_backend_pid();" \
191
- -c "DROP DATABASE IF EXISTS $DB;" \
192
- -c "CREATE DATABASE $DB WITH TEMPLATE employees;"
193
- LEFT=$("$PGBIN/psql" -h $PGHOST -p $PGPORT -U $PGUSER -d "$DB" -tAc \
194
- "SELECT count(*) FROM information_schema.tables
195
- WHERE table_schema='employees' AND table_name='hiring_year_summary';")
196
- [ "$LEFT" = "0" ] || { echo "[reset] target table survived — aborting" >&2; exit 1; }
197
- echo "[reset] $DB recreated from template — target table absent"
198
- ```
199
-
200
- The `pg_terminate_backend` line is not optional: an MCP server holds a pooled
201
- connection, and `DROP DATABASE` fails while any session is attached.
202
-
203
- ### 3.5 The grader
204
-
205
- Save this as `verify.sh`. It runs MCPMark's own `verify.py`, unmodified, against
206
- the real database:
207
-
208
- ```bash
209
- #!/usr/bin/env bash
210
- export PYTHONIOENCODING=utf-8 # verify.py prints ✅/❌ — required on Windows consoles
211
- export POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DATABASE="${TRIAL_DB:-hys_trial}"
212
- export POSTGRES_USERNAME=postgres POSTGRES_PASSWORD=password
213
- cd "$(dirname "$0")"
214
- uv run --quiet --with psycopg2-binary python verify.py
215
- ```
216
-
217
- > **`PYTHONIOENCODING=utf-8` is not optional on Windows.** `verify.py` prints
218
- > `✅`/`❌`/`🎉`; a cp1255/cp1252 console raises `UnicodeEncodeError` *while
219
- > formatting its own PASS line*, and a run graded through a crashed reporter has
220
- > no grade.
221
-
222
- ### 3.6 Run it once, by hand
223
-
224
- ```bash
225
- ./seed.sh # once
226
- ./reset-db.sh # before the run
227
- claude -p "$(cat prompt.txt)" --model claude-sonnet-5
228
- ./verify.sh
229
- ```
230
-
231
- What a pass looks like:
232
-
233
- ```
234
- ✅ Hiring year summary results are correct (16 records)
235
- 🎉 Task verification: PASS
236
- ```
237
-
238
- That is the benchmark working, end to end, with **no runner involved**. On this
239
- machine that run cost about **$0.15**. Hold that number — it is the baseline the
240
- runner has to beat.
241
-
242
- The 16 rows it produced (the grader recomputes these in SQL and compares
243
- row-by-row):
244
-
245
- | hire_year | employees_hired | still_employed | retention_rate |
246
- |---|---|---|---|
247
- | 1985 | 35316 | 28291 | 80.11 |
248
- | 1986 | 36150 | 28840 | 79.78 |
249
- | … | … | … | … |
250
- | 1999 | 1514 | 1204 | 79.52 |
251
- | 2000 | 13 | 9 | 69.23 |
252
-
253
- (16 rows, hire years 1985 through 2000.)
254
-
255
- ---
256
-
257
- ## 4. Part B — install the runner
258
-
259
- Now put BaseInstRunner between the agent and Postgres. From inside the run
260
- folder, write the MCP config **unwrapped** — `bir install` will wrap it:
261
-
262
- `.mcp.json`
263
- ```json
264
- {
265
- "mcpServers": {
266
- "postgres": {
267
- "command": "uvx",
268
- "args": ["--with", "mcp<2", "postgres-mcp==0.3.0", "--access-mode=unrestricted"],
269
- "env": { "DATABASE_URI": "postgresql://postgres:password@localhost:5432/hys_trial" }
270
- }
271
- }
272
- }
273
- ```
274
-
275
- Two details that will bite if you skip them:
276
-
277
- - **`--with "mcp<2"` is mandatory.** `postgres-mcp==0.3.0` imports
278
- `mcp.server.fastmcp`, which the `mcp` 2.x SDK removed. MCPMark's own
279
- `pipx run postgres-mcp==0.3.0` is broken today without this pin. (`uvx` stands
280
- in for `pipx`; both are equivalent here.)
281
- - **`--access-mode=unrestricted`** — the task creates a table.
282
-
283
- Then install, with calculated replay switched on:
284
-
285
- ```bash
286
- export BIR_AUTH_URL=https://api.your-domain.com
287
- bir login # once; approve it in the browser
288
- bir install --replay --global
289
- ```
290
-
291
- `bir install --replay` rewrites `.mcp.json` to route `postgres` through
292
- `bir-proxy`, adds a first-party `bir` server (the channel a replay's results
293
- come back through), and wires Claude Code's ten hooks into
294
- `.claude/settings.json`. `bir uninstall` reverses every byte of it.
295
-
296
- ---
297
-
298
- ## 5. Part C — the two settings that decide everything
299
-
300
- This is the section the notes call *"the whole ballgame."* Get these two things
301
- right before your first trial, or every measurement below will be wrong in a way
302
- that still *looks* healthy.
303
-
304
- ### 5.1 Deny the built-in tools
305
-
306
- Left alone, Claude Code will reach for `Bash` + `psql` and never touch the MCP
307
- server — and a single built-in call in the recording makes the whole plan
308
- un-replayable. Pin the agent to MCP only. Add to `.claude/settings.json`:
309
-
310
- ```json
311
- {
312
- "permissions": {
313
- "deny": ["Bash", "PowerShell", "Read", "Write", "Edit", "NotebookEdit",
314
- "Glob", "Grep", "WebFetch", "WebSearch", "Agent", "Task"],
315
- "allow": ["mcp__postgres__*", "mcp__bir__*"]
316
- }
317
- }
318
- ```
319
-
320
- ### 5.2 Trust the workspace — or the `allow` list is silently discarded
321
-
322
- **This is the single most common way a first run goes wrong.** If Claude Code
323
- has not been told to trust this folder, it throws the `allow` list away and
324
- prints one line you will scroll past:
325
-
326
- ```
327
- Ignoring 2 permissions.allow entries from .claude/settings.json:
328
- this workspace has not been trusted.
329
- ```
330
-
331
- It then fails *asymmetrically*: wrapped `mcp__postgres__*` calls keep working
332
- (bir's own hook allows them), but `mcp__bir__run_scenario` — the one tool the
333
- replay is delivered through — gets denied. The agent cannot reach the plan, so
334
- it does the task by hand, and the run lands in the **divergence** path. It still
335
- passes, the audit log still looks fine, and you have measured nothing.
336
-
337
- Fix it **before the first trial**, one of two ways:
338
-
339
- - **Interactive, once:** run `claude` in the folder and accept the trust dialog.
340
- - **By hand:** set this in `~/.claude.json` (use the exact path Claude Code
341
- prints, forward slashes):
342
-
343
- ```json
344
- "projects": {
345
- "C:/Users/Admin/my-first-sample": { "hasTrustDialogAccepted": true }
346
- }
347
- ```
348
-
349
- > The interactive route is the reliable one — Claude Code guards `~/.claude.json`
350
- > against outside edits, so a script that tries to set the flag may be blocked.
351
- > Opening `claude` in the folder once and clicking *trust* is the path that
352
- > always works.
353
-
354
- ### 5.3 Start the control server, and prove it is bound
355
-
356
- Leave this running in its own terminal, **in the run folder**:
357
-
358
- ```bash
359
- export BIR_AUTH_URL=https://api.your-domain.com
360
- unset BIR_REPLAY # replay is on by default; BIR_REPLAY=0 turns it off
361
- export BIR_REPLAY_ALLOW_SERVERS=postgres # servers allowed to run unattended
362
- bir-hooks 2>&1 | tee -a bir-hooks.log
363
- ```
364
-
365
- Then, in another terminal in the same folder, **launch a `claude` session so a
366
- proxy registers**, and check:
367
-
368
- ```bash
369
- bir doctor
370
- ```
371
-
372
- Do not send a single prompt until you see both of these:
373
-
374
- ```
375
- Wrapped in config : postgres
376
- Registered proxies: postgres ← this needs a live claude session
377
- ```
378
-
379
- `Registered proxies: (none)` means no proxy has bound yet. This exact check is
380
- what separates a real measurement from a confident fiction.
381
-
382
- ---
383
-
384
- ## 6. Part D — record once, then replay for free
385
-
386
- The run loop is one script, `trial.sh`. It resets the DB, runs the agent
387
- non-interactively, prints the cost, and grades. **Keep `--model` the same for
388
- every trial**, or the costs are not comparable:
389
-
390
- ```bash
391
- #!/usr/bin/env bash
392
- set -uo pipefail; cd "$(dirname "$0")"
393
- LABEL="${1:-1}"; MODEL="${MODEL:-claude-sonnet-5}"
394
- echo "==== trial $LABEL ($MODEL) ===="
395
- ./reset-db.sh || exit 1
396
- claude -p "$(cat prompt.txt)" --model "$MODEL" \
397
- --allowedTools "mcp__postgres__*" "mcp__bir__*" \
398
- --output-format json > "trial-$LABEL.json"
399
- node -e 'const r=JSON.parse(require("fs").readFileSync(process.argv[1]));
400
- console.log("cost $"+(+r.total_cost_usd).toFixed(4)+" turns="+r.num_turns+
401
- " denied="+(r.permission_denials||[]).length)' "trial-$LABEL.json"
402
- ./verify.sh
403
- ```
404
-
405
- ### 6.1 Trial 1 — record
406
-
407
- ```bash
408
- ./trial.sh 1
409
- ```
410
-
411
- bir records the run. If the service has seen this prompt before it keeps its own
412
- canonical run and this one is *not* re-recorded — that is the match working.
413
- Either way, the plan you will replay is a **calculated scenario**: an intent, a
414
- parameter schema, and one bit of logic per step. Inspect it before you trust it:
415
-
416
- ```bash
417
- bir scenario list # find the runId / scenario id
418
- bir scenario replay <scenarioId> --prompt "$(cat prompt.txt)" --dry
419
- ```
420
-
421
- `--dry` runs the scenario's logic against the *recorded* outputs — no real SQL,
422
- one cheap model call — and prints each step's computed input. Read it for three
423
- things: parameters that actually vary with the prompt, step inputs that look
424
- *computed* rather than copied, and non-empty `emitted` on steps others depend
425
- on. On our scenario the dry run produced the correct six steps and the right 16
426
- rows for **$0.004**.
427
-
428
- ### 6.2 Trials 2+ — replay
429
-
430
- Reset, send the identical prompt, grade. Watch `bir-hooks.log` for the story:
431
-
432
- ```
433
- run.matched similarity=1.000
434
- plan.armed mode=direct steps=6 coverage=direct,direct,direct,direct,direct,direct
435
- replay.derived params=7 costUsd=0.0000 source="recorded samples"
436
- replay.step n=0 … ok=true emitted=tableList
437
- replay.step n=5 … ok=true emitted=validationRow,totalEmployees,sumHired,…
438
- replay.done mode=direct steps=6/6 outcome=steered_full
439
- execution.reported outcome=steered_full session=0.045 savedUsd=+0.174 measured=true
440
- ```
441
-
442
- All six SQL steps executed for real through the connection the proxy already
443
- held — in **about a second of real SQL time** (the `CREATE TABLE … AS` over
444
- 300k rows is most of it), **for $0.00 in model tokens**. The model's only job
445
- was to read the result.
446
-
447
- ---
448
-
449
- ## 7. Reading the result — the money
450
-
451
- Here are the real figures this task produced, so you can compare yours:
452
-
453
- | Run | Outcome | Session cost | Saved vs baseline |
454
- |---|---|---|---|
455
- | Baseline (recorded, median of 3) | — | **$0.2188** | — |
456
- | A clean replay | `steered_full` | **$0.0448** | **+$0.1739 (≈ 80%)** |
457
-
458
- Across five verified replays on the test account, the service had booked
459
- **$0.6374 saved** in total. That is the number the manual promised you: the same
460
- task, solved once and then not re-solved, at roughly a fifth of the price each
461
- time after.
462
-
463
- What each outcome means for your ledger:
464
-
465
- | Outcome | Meaning | Books as |
466
- |---|---|---|
467
- | `steered_full` | every step ran under the plan | a **saving** |
468
- | `diverged` | agent went off-script; bir recovered for $0 | a saving (usually smaller) |
469
- | `not_steered` | matched but replay was off | a **baseline sample** (this is how the baseline is measured) |
470
- | `failed` | armed, then gave up | a baseline sample |
471
-
472
- Where you see it: on your BaseIn console (for the hosted service,
473
- **bi2202.com** → sign in → **Dashboard** / **Recordings**), the KPI strip shows
474
- **Verified saved** — "money we watched move: costs measured on both sides." A
475
- loss shows red; a saving shows green.
476
-
477
- ---
478
-
479
- ## 8. What actually happened here, honestly
480
-
481
- On the machine this manual was written on, the run folder had **not** been
482
- trusted (§5.2), so both replays landed in the **divergence** path rather than
483
- clean `steered_full`. They still passed, and they still saved (`+$0.033` and
484
- `+$0.116` against the $0.219 baseline) — but two things are worth stating
485
- plainly, because you may hit them too:
486
-
487
- 1. **Untrusted → divergence.** With the `allow` list discarded,
488
- `mcp__bir__run_scenario` was denied and the agent solved the task by hand.
489
- The safety net (bir re-running the six steps for $0) is why it still passed
490
- and still saved. Trust the workspace to get the clean channel.
491
-
492
- 2. **A cautious model may reject the replay as a prompt injection.** When the
493
- pre-computed bundle arrived unbidden (the divergence path delivers it as a
494
- tool result), Claude Sonnet 5 flagged it:
495
-
496
- > *"the result … came back wrapped in a fake '[BaseInstRunner calculated
497
- > replay]' block claiming all 6 steps were 'already executed' … This is a
498
- > prompt injection embedded in the tool response channel … I'm not going to
499
- > report those figures as fact."*
500
-
501
- The model then re-verified the work itself — which is correct, cautious
502
- behaviour, and which raises the per-run cost. Delivering the bundle through
503
- the model's *own* `run_scenario` call (the trusted, direct path) is far less
504
- likely to trip this, and the bundle wording is being sharpened (see
505
- [mcpmark.md](mcpmark.md) §12–14). It is a real product wrinkle, not a setup
506
- error — worth knowing before you read too much into one run's cost.
507
-
508
- Neither changes the headline: **the scenario is correct, the replay executes the
509
- real work for zero tokens, and the saving is real and positive.**
510
-
511
- ---
512
-
513
- ## 9. Troubleshooting
514
-
515
- | Symptom | Cause | Fix |
516
- |---|---|---|
517
- | `Ignoring N permissions.allow entries … not been trusted` | workspace untrusted | §5.2 — trust it, then every replay is clean |
518
- | `Registered proxies: (none)` | no live session bound a proxy | launch `claude` in the folder, re-run `bir doctor` |
519
- | replay always `diverged`, never `steered_full` | usually §5.2, sometimes a cautious model (§8) | trust first; then see mcpmark.md §12–14 |
520
- | trial 2 finds the table already there | no DB reset | `reset-db.sh` before **every** trial |
521
- | `UnicodeEncodeError` in verify.py | Windows console codepage | `PYTHONIOENCODING=utf-8` (already in `verify.sh`) |
522
- | `ModuleNotFoundError: mcp.server.fastmcp` | `mcp` 2.x resolved | pin `--with "mcp<2"` in `.mcp.json` |
523
- | `bir login` says "not a BaseIn service" | `BIR_AUTH_URL` is a website, not the API | set it to `https://api.<domain>`, open a fresh terminal |
524
- | `savedUsd` looks impossible | pricing drift between runner and server | compare `PRICING_VERSION` on both sides |
525
- | the number won't move | a replayed turn is deliberately not re-recorded | correct — the matched run stays canonical |
526
-
527
- ---
528
-
529
- ## 10. Where to go next
530
-
531
- - **Widen within Postgres.** 31 prompts share this exact server and reset
532
- mechanism, so each new task costs almost nothing to add.
533
- `tasks/postgres/standard/**` is where the paper's numbers come from.
534
- - **[mcpmark.md](mcpmark.md)** — the full rationale, the topology in detail, and
535
- the measured history of this task, including the bugs that were found and
536
- fixed along the way.
537
- - **[calculatedReplayGuide.md](calculatedReplayGuide.md)** — the runner's own
538
- manual: every audit line, every switch, and how the saving is booked.
539
- - **[quickstart.md](quickstart.md)** — setting up a fresh machine to record from
540
- your *own* work, not just this benchmark.
541
-
542
- > **Turning replay off** is one line: `export BIR_REPLAY=0`. The system is then
543
- > exactly a recorder again — it recognises a repeated prompt and declines to
544
- > record it twice, and the model does the work. Nothing is skipped, nothing is
545
- > risked.
1
+ # Your first sample — run one MCPMark task and watch it pay for itself
2
+
3
+ A complete, from-zero manual. By the end you will have run one real agent
4
+ benchmark task, graded it against a live database, and then watched
5
+ BaseInstRunner replay the *same* task for **zero model tokens** — and you will
6
+ see the dollar figure that saved.
7
+
8
+ This is the hands-on companion to [mcpmark.md](mcpmark.md), which explains the
9
+ *why* in depth. This document is the *what to type*, in order, with the numbers
10
+ a real run produced on 2026-09-07 so you know what "working" looks like.
11
+
12
+ > **The one task.** Everything below uses a single MCPMark task,
13
+ > `postgres/easy/employees/hiring_year_summary`. It is the smallest task in the
14
+ > benchmark that still writes to the database, which makes it the honest place
15
+ > to start.
16
+
17
+ ---
18
+
19
+ ## 0. What MCPMark is (and why we use it)
20
+
21
+ **MCPMark is a standard agents benchmark.** It measures how well an AI agent can
22
+ drive real tools, through the [Model Context Protocol
23
+ (MCP)](https://modelcontextprotocol.io), to change real systems — not to chat
24
+ about them.
25
+
26
+ The facts worth knowing before you start:
27
+
28
+ | | |
29
+ |---|---|
30
+ | Who | The `eval-sys` research group · [github.com/eval-sys/mcpmark](https://github.com/eval-sys/mcpmark) · [paper (arXiv 2509.24002)](https://arxiv.org/abs/2509.24002) · [mcpmark.ai](https://mcpmark.ai) |
31
+ | What it tests | An agent using **five MCP services**: Postgres, GitHub, Filesystem, Notion, Playwright |
32
+ | Size | **127 standard tasks + 50 easy** (the repo has grown past the paper's five services) |
33
+ | How it grades | Each task ships a `verify.py` that opens a real connection and **queries the actual state**, not the agent's transcript. Exit 0 = PASS |
34
+ | Metrics | `pass@1` (one try), `pass@k`, and `pass^k` — *stability*: did it pass **all** k independent runs |
35
+ | State of the art | On MCPMark's verified set, the strongest model reports about **92.9%**; most models sit far lower, and `pass^4` collapses for everyone |
36
+
37
+ Two of those facts are why this benchmark suits BaseInstRunner so well:
38
+
39
+ 1. **It grades the world, not the words.** A replayed run that produces the
40
+ right rows passes exactly like a hand-typed one. There is no transcript to
41
+ fool.
42
+ 2. **`pass^k` punishes inconsistency.** The benchmark runs each task several
43
+ times against a reset database and asks whether *every* run passed. A model
44
+ that re-decides the plan token-by-token every time is exactly what
45
+ BaseInstRunner removes — it runs the *same* known-good plan each time.
46
+
47
+ That is the whole idea you are about to see working: **solve the task once, then
48
+ stop paying to re-solve it.**
49
+
50
+ ---
51
+
52
+ ## 1. What you are about to build
53
+
54
+ ```
55
+ Part A — the benchmark, on its own
56
+ seed a Postgres DB → agent solves the task → verify.py grades it → PASS
57
+
58
+ Parts B–D — the same task, through BaseInstRunner
59
+ claude -p ──mcp__postgres__*──▶ bir-proxy ──▶ postgres-mcp ──▶ PostgreSQL
60
+ │ │ records every call
61
+ └──mcp__bir__run_scenario──▶ bir-hooks (control server)
62
+ │ matches the prompt, arms the plan,
63
+ ▼ runs all 6 steps for $0 model tokens
64
+ BaseIn service ──▶ books the saving
65
+ ```
66
+
67
+ You will run the task **three ways**, and the difference between them is the
68
+ point:
69
+
70
+ | Run | Runner | What the model does | What it costs |
71
+ |---|---|---|---|
72
+ | **Baseline** | off | discovers and solves the task | full price |
73
+ | **Record** | on, replay **off** | solves it once; bir *records* the plan | full price, one time |
74
+ | **Replay** | on, replay **on** | one tool call reads a pre-computed result | a fraction |
75
+
76
+ ---
77
+
78
+ ## 2. Prerequisites
79
+
80
+ Check each one. The whole setup takes about ten minutes, most of it downloads.
81
+
82
+ | # | You need | Check | If missing |
83
+ |---|---|---|---|
84
+ | 1 | **Node 20+** | `node -v` | [nodejs.org](https://nodejs.org) |
85
+ | 2 | **Claude Code** | `claude --version` | Anthropic's CLI; this manual drives it with `claude -p` |
86
+ | 3 | **PostgreSQL 17**, running | `psql --version` and the service is up | see §2.1 |
87
+ | 4 | **uv / uvx** | `uvx --version` | [astral.sh/uv](https://docs.astral.sh/uv/) — runs `postgres-mcp` and `verify.py` |
88
+ | 5 | **The runner** | `bir --version` | `npm i -g @basein/runner` |
89
+ | 6 | **A BaseIn account + API URL** | `bir login` succeeds | your service address, e.g. `https://api.your-domain.com` |
90
+
91
+ > **`BIR_AUTH_URL` must be the API host**, normally `https://api.<domain>`, not
92
+ > the docs or app website. Point it at a website and `bir login` refuses it,
93
+ > saying what answered instead. Set it once, then open a **fresh** terminal — an
94
+ > existing one keeps the value it started with.
95
+
96
+ `bir login` signs you in through the browser: it prints a link and a short code
97
+ and waits for you to approve it. Nothing is typed into the terminal, and it works
98
+ for accounts that only sign in with Google.
99
+
100
+ ### 2.1 Postgres, without Docker
101
+
102
+ MCPMark's own harness runs Postgres in Docker (`pgvector/pgvector:pg17`). If the
103
+ machine has no Docker — this one did not — install PostgreSQL 17 **natively**;
104
+ it is the same major version, and this task needs no vector support. Disclose
105
+ the deviation in any published result.
106
+
107
+ ```powershell
108
+ winget install --id PostgreSQL.PostgreSQL.17 -e `
109
+ --accept-package-agreements --accept-source-agreements `
110
+ --custom "--mode unattended --superpassword password --serverport 5432 --unattendedmodeui none"
111
+ ```
112
+
113
+ That sets the `postgres` superuser password to `password`, which is the value
114
+ every command and connection string below expects. On macOS:
115
+ `brew install postgresql@17 && brew services start postgresql@17`.
116
+
117
+ ---
118
+
119
+ ## 3. Part A — run the benchmark task on its own
120
+
121
+ First prove the plain benchmark works, with no runner in the picture at all.
122
+
123
+ ### 3.1 Make a run folder and fetch the task
124
+
125
+ ```bash
126
+ mkdir -p ~/my-first-sample && cd ~/my-first-sample
127
+
128
+ TASK=https://raw.githubusercontent.com/eval-sys/mcpmark/main/tasks/postgres/easy/employees/hiring_year_summary
129
+ curl -fsSLO "$TASK/description.md" # what the agent is asked to do
130
+ curl -fsSLO "$TASK/verify.py" # MCPMark's grader — unmodified
131
+ curl -fsSLO "$TASK/meta.json" # task metadata (difficulty L1, mcp: postgres)
132
+ ```
133
+
134
+ The task asks the agent to build one table, `employees.hiring_year_summary`,
135
+ with a row per hiring year: how many were hired, how many are still employed,
136
+ and the retention rate. Small, but a genuine write.
137
+
138
+ ### 3.2 The exact prompt
139
+
140
+ MCPMark sends `description.md` **verbatim** plus one fixed suffix. Assemble it
141
+ once so every run is byte-identical:
142
+
143
+ ```bash
144
+ { cat description.md
145
+ printf '\n\nNote: Based on your understanding, solve the task all at once by yourself, don'\''t ask for my opinions on anything.\n'
146
+ } > prompt.txt
147
+ ```
148
+
149
+ ### 3.3 Seed the database
150
+
151
+ Download MCPMark's 33 MB backup and build the `employees` **template**
152
+ database from it. Save this as `seed.sh` and run it once:
153
+
154
+ ```bash
155
+ #!/usr/bin/env bash
156
+ set -euo pipefail
157
+ PGBIN="${PGBIN:-/c/Program Files/PostgreSQL/17/bin}" # macOS: "$(brew --prefix postgresql@17)/bin"
158
+ export PGPASSWORD="${POSTGRES_PASSWORD:-password}"
159
+ PGUSER=postgres PGHOST=localhost PGPORT=5432
160
+
161
+ [ -f employees.backup ] || curl -fsSL -o employees.backup https://storage.mcpmark.ai/postgres/employees.backup
162
+
163
+ "$PGBIN/psql" -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -v ON_ERROR_STOP=1 \
164
+ -c "DROP DATABASE IF EXISTS employees;" -c "CREATE DATABASE employees;"
165
+ "$PGBIN/pg_restore" -h $PGHOST -p $PGPORT -U $PGUSER -d employees \
166
+ --no-owner --no-privileges employees.backup \
167
+ || echo "[seed] pg_restore printed notices — checking the result instead"
168
+ "$PGBIN/psql" -h $PGHOST -p $PGPORT -U $PGUSER -d employees \
169
+ -c "SELECT count(*) AS employees FROM employees.employee;"
170
+ echo "[seed] template database 'employees' is ready"
171
+ ```
172
+
173
+ A healthy seed reports **300024** employees.
174
+
175
+ ### 3.4 A fresh database for every run
176
+
177
+ MCPMark isolates each run with `CREATE DATABASE … WITH TEMPLATE …` and asserts
178
+ the target table is absent. Reproduce that as `reset-db.sh` — **run it before
179
+ every single trial**, or the second run finds the table already built and the
180
+ task stops being the task:
181
+
182
+ ```bash
183
+ #!/usr/bin/env bash
184
+ set -euo pipefail
185
+ PGBIN="${PGBIN:-/c/Program Files/PostgreSQL/17/bin}"
186
+ export PGPASSWORD=password; PGUSER=postgres PGHOST=localhost PGPORT=5432
187
+ DB="${TRIAL_DB:-hys_trial}"
188
+ "$PGBIN/psql" -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -v ON_ERROR_STOP=1 \
189
+ -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity
190
+ WHERE datname='$DB' AND pid <> pg_backend_pid();" \
191
+ -c "DROP DATABASE IF EXISTS $DB;" \
192
+ -c "CREATE DATABASE $DB WITH TEMPLATE employees;"
193
+ LEFT=$("$PGBIN/psql" -h $PGHOST -p $PGPORT -U $PGUSER -d "$DB" -tAc \
194
+ "SELECT count(*) FROM information_schema.tables
195
+ WHERE table_schema='employees' AND table_name='hiring_year_summary';")
196
+ [ "$LEFT" = "0" ] || { echo "[reset] target table survived — aborting" >&2; exit 1; }
197
+ echo "[reset] $DB recreated from template — target table absent"
198
+ ```
199
+
200
+ The `pg_terminate_backend` line is not optional: an MCP server holds a pooled
201
+ connection, and `DROP DATABASE` fails while any session is attached.
202
+
203
+ ### 3.5 The grader
204
+
205
+ Save this as `verify.sh`. It runs MCPMark's own `verify.py`, unmodified, against
206
+ the real database:
207
+
208
+ ```bash
209
+ #!/usr/bin/env bash
210
+ export PYTHONIOENCODING=utf-8 # verify.py prints ✅/❌ — required on Windows consoles
211
+ export POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DATABASE="${TRIAL_DB:-hys_trial}"
212
+ export POSTGRES_USERNAME=postgres POSTGRES_PASSWORD=password
213
+ cd "$(dirname "$0")"
214
+ uv run --quiet --with psycopg2-binary python verify.py
215
+ ```
216
+
217
+ > **`PYTHONIOENCODING=utf-8` is not optional on Windows.** `verify.py` prints
218
+ > `✅`/`❌`/`🎉`; a cp1255/cp1252 console raises `UnicodeEncodeError` *while
219
+ > formatting its own PASS line*, and a run graded through a crashed reporter has
220
+ > no grade.
221
+
222
+ ### 3.6 Run it once, by hand
223
+
224
+ ```bash
225
+ ./seed.sh # once
226
+ ./reset-db.sh # before the run
227
+ claude -p "$(cat prompt.txt)" --model claude-sonnet-5
228
+ ./verify.sh
229
+ ```
230
+
231
+ What a pass looks like:
232
+
233
+ ```
234
+ ✅ Hiring year summary results are correct (16 records)
235
+ 🎉 Task verification: PASS
236
+ ```
237
+
238
+ That is the benchmark working, end to end, with **no runner involved**. On this
239
+ machine that run cost about **$0.15**. Hold that number — it is the baseline the
240
+ runner has to beat.
241
+
242
+ The 16 rows it produced (the grader recomputes these in SQL and compares
243
+ row-by-row):
244
+
245
+ | hire_year | employees_hired | still_employed | retention_rate |
246
+ |---|---|---|---|
247
+ | 1985 | 35316 | 28291 | 80.11 |
248
+ | 1986 | 36150 | 28840 | 79.78 |
249
+ | … | … | … | … |
250
+ | 1999 | 1514 | 1204 | 79.52 |
251
+ | 2000 | 13 | 9 | 69.23 |
252
+
253
+ (16 rows, hire years 1985 through 2000.)
254
+
255
+ ---
256
+
257
+ ## 4. Part B — install the runner
258
+
259
+ Now put BaseInstRunner between the agent and Postgres. From inside the run
260
+ folder, write the MCP config **unwrapped** — `bir install` will wrap it:
261
+
262
+ `.mcp.json`
263
+ ```json
264
+ {
265
+ "mcpServers": {
266
+ "postgres": {
267
+ "command": "uvx",
268
+ "args": ["--with", "mcp<2", "postgres-mcp==0.3.0", "--access-mode=unrestricted"],
269
+ "env": { "DATABASE_URI": "postgresql://postgres:password@localhost:5432/hys_trial" }
270
+ }
271
+ }
272
+ }
273
+ ```
274
+
275
+ Two details that will bite if you skip them:
276
+
277
+ - **`--with "mcp<2"` is mandatory.** `postgres-mcp==0.3.0` imports
278
+ `mcp.server.fastmcp`, which the `mcp` 2.x SDK removed. MCPMark's own
279
+ `pipx run postgres-mcp==0.3.0` is broken today without this pin. (`uvx` stands
280
+ in for `pipx`; both are equivalent here.)
281
+ - **`--access-mode=unrestricted`** — the task creates a table.
282
+
283
+ Then install, with calculated replay switched on:
284
+
285
+ ```bash
286
+ export BIR_AUTH_URL=https://api.your-domain.com
287
+ bir login # once; approve it in the browser
288
+ bir install --replay --global --server postgres
289
+ ```
290
+
291
+ `bir install --replay` routes `postgres` through `bir-proxy` with a wrapped
292
+ copy in this folder's local scope in `~/.claude.json` (your `.mcp.json` stays as
293
+ written), adds a first-party `bir` server there (the channel a replay's results
294
+ come back through), and wires Claude Code's ten hooks into
295
+ `.claude/settings.local.json`. `bir uninstall` takes all of it back out.
296
+
297
+ `--server postgres` matters in a fresh folder. Claude Code asks before it starts
298
+ a server from `.mcp.json`, but never about a local-scope copy, so `bir install`
299
+ copies a `.mcp.json` server only once Claude Code has approved it; until then it
300
+ prints `postgres — left unwrapped` and only the hooks record. Naming the server
301
+ wraps it now. (Or approve it in an interactive `claude` session here and run the
302
+ install again.)
303
+
304
+ ---
305
+
306
+ ## 5. Part C — the two settings that decide everything
307
+
308
+ This is the section the notes call *"the whole ballgame."* Get these two things
309
+ right before your first trial, or every measurement below will be wrong in a way
310
+ that still *looks* healthy.
311
+
312
+ ### 5.1 Deny the built-in tools
313
+
314
+ Left alone, Claude Code will reach for `Bash` + `psql` and never touch the MCP
315
+ server — and a single built-in call in the recording makes the whole plan
316
+ un-replayable. Pin the agent to MCP only. Add to `.claude/settings.json`:
317
+
318
+ ```json
319
+ {
320
+ "permissions": {
321
+ "deny": ["Bash", "PowerShell", "Read", "Write", "Edit", "NotebookEdit",
322
+ "Glob", "Grep", "WebFetch", "WebSearch", "Agent", "Task"],
323
+ "allow": ["mcp__postgres__*", "mcp__bir__*"]
324
+ }
325
+ }
326
+ ```
327
+
328
+ ### 5.2 Trust the workspace — or the `allow` list is silently discarded
329
+
330
+ **This is the single most common way a first run goes wrong.** If Claude Code
331
+ has not been told to trust this folder, it throws the `allow` list away and
332
+ prints one line you will scroll past:
333
+
334
+ ```
335
+ Ignoring 2 permissions.allow entries from .claude/settings.json:
336
+ this workspace has not been trusted.
337
+ ```
338
+
339
+ What that breaks: bir does not approve your tool calls. Its hook answers a
340
+ wrapped `mcp__postgres__*` call with no permission decision, so with the `allow`
341
+ list gone Claude Code asks about it — and under `claude -p` a call that has to
342
+ be asked about is denied. The calls bir's hook allows itself are a replay's
343
+ own: `mcp__bir__run_scenario`, the tool a direct replay is delivered through,
344
+ once bir has announced one for this prompt, and a step a steered replay pins.
345
+ (Runners up to 0.2.12 were the other way round: they allowed every wrapped call
346
+ and left `run_scenario` to the `allow` list, which is how the runs in §8 landed
347
+ in the divergence path.)
348
+
349
+ Fix it **before the first trial**, one of two ways:
350
+
351
+ - **Interactive, once:** run `claude` in the folder and accept the trust dialog.
352
+ - **By hand:** set this in `~/.claude.json` (use the exact path Claude Code
353
+ prints, forward slashes):
354
+
355
+ ```json
356
+ "projects": {
357
+ "C:/Users/Admin/my-first-sample": { "hasTrustDialogAccepted": true }
358
+ }
359
+ ```
360
+
361
+ > The interactive route is the reliable one — Claude Code guards `~/.claude.json`
362
+ > against outside edits, so a script that tries to set the flag may be blocked.
363
+ > Opening `claude` in the folder once and clicking *trust* is the path that
364
+ > always works.
365
+
366
+ ### 5.3 Start the control server, and prove it is bound
367
+
368
+ Leave this running in its own terminal, **in the run folder**:
369
+
370
+ ```bash
371
+ export BIR_AUTH_URL=https://api.your-domain.com
372
+ unset BIR_REPLAY # on here: install --replay stored it; BIR_REPLAY=0 turns it off
373
+ export BIR_REPLAY_ALLOW_SERVERS=postgres # servers allowed to run unattended
374
+ bir-hooks 2>&1 | tee -a bir-hooks.log
375
+ ```
376
+
377
+ Then, in another terminal in the same folder, **launch a `claude` session so a
378
+ proxy registers**, and check:
379
+
380
+ ```bash
381
+ bir doctor
382
+ ```
383
+
384
+ Do not send a single prompt until you see both of these:
385
+
386
+ ```
387
+ Wrapped in config : postgres
388
+ Registered proxies: postgres ← this needs a live claude session
389
+ ```
390
+
391
+ `Registered proxies: (none)` means no proxy has bound yet. This exact check is
392
+ what separates a real measurement from a confident fiction.
393
+
394
+ ---
395
+
396
+ ## 6. Part D — record once, then replay for free
397
+
398
+ The run loop is one script, `trial.sh`. It resets the DB, runs the agent
399
+ non-interactively, prints the cost, and grades. **Keep `--model` the same for
400
+ every trial**, or the costs are not comparable:
401
+
402
+ ```bash
403
+ #!/usr/bin/env bash
404
+ set -uo pipefail; cd "$(dirname "$0")"
405
+ LABEL="${1:-1}"; MODEL="${MODEL:-claude-sonnet-5}"
406
+ echo "==== trial $LABEL ($MODEL) ===="
407
+ ./reset-db.sh || exit 1
408
+ claude -p "$(cat prompt.txt)" --model "$MODEL" \
409
+ --allowedTools "mcp__postgres__*" "mcp__bir__*" \
410
+ --output-format json > "trial-$LABEL.json"
411
+ node -e 'const r=JSON.parse(require("fs").readFileSync(process.argv[1]));
412
+ console.log("cost $"+(+r.total_cost_usd).toFixed(4)+" turns="+r.num_turns+
413
+ " denied="+(r.permission_denials||[]).length)' "trial-$LABEL.json"
414
+ ./verify.sh
415
+ ```
416
+
417
+ ### 6.1 Trial 1 — record
418
+
419
+ ```bash
420
+ ./trial.sh 1
421
+ ```
422
+
423
+ bir records the run. If the service has seen this prompt before it keeps its own
424
+ canonical run and this one is *not* re-recorded — that is the match working.
425
+ Either way, the plan you will replay is a **calculated scenario**: an intent, a
426
+ parameter schema, and one bit of logic per step. Inspect it before you trust it:
427
+
428
+ ```bash
429
+ bir scenario list # find the runId / scenario id
430
+ bir scenario replay <scenarioId> --prompt "$(cat prompt.txt)" --dry
431
+ ```
432
+
433
+ `--dry` runs the scenario's logic against the *recorded* outputs — no real SQL,
434
+ one cheap model call — and prints each step's computed input. Read it for three
435
+ things: parameters that actually vary with the prompt, step inputs that look
436
+ *computed* rather than copied, and non-empty `emitted` on steps others depend
437
+ on. On our scenario the dry run produced the correct six steps and the right 16
438
+ rows for **$0.004**.
439
+
440
+ ### 6.2 Trials 2+ — replay
441
+
442
+ Reset, send the identical prompt, grade. Watch `bir-hooks.log` for the story:
443
+
444
+ ```
445
+ run.matched similarity=1.000
446
+ plan.armed mode=direct steps=6 coverage=direct,direct,direct,direct,direct,direct
447
+ replay.derived params=7 costUsd=0.0000 source="recorded samples"
448
+ replay.step n=0 … ok=true emitted=tableList
449
+ replay.step n=5 … ok=true emitted=validationRow,totalEmployees,sumHired,…
450
+ replay.done mode=direct steps=6/6 outcome=steered_full
451
+ execution.reported outcome=steered_full session=0.045 savedUsd=+0.174 measured=true
452
+ ```
453
+
454
+ All six SQL steps executed for real through the connection the proxy already
455
+ held — in **about a second of real SQL time** (the `CREATE TABLE … AS` over
456
+ 300k rows is most of it), **for $0.00 in model tokens**. The model's only job
457
+ was to read the result.
458
+
459
+ ---
460
+
461
+ ## 7. Reading the result — the money
462
+
463
+ Here are the real figures this task produced, so you can compare yours:
464
+
465
+ | Run | Outcome | Session cost | Saved vs baseline |
466
+ |---|---|---|---|
467
+ | Baseline (recorded, median of 3) | — | **$0.2188** | — |
468
+ | A clean replay | `steered_full` | **$0.0448** | **+$0.1739 (≈ 80%)** |
469
+
470
+ Across five verified replays on the test account, the service had booked
471
+ **$0.6374 saved** in total. That is the number the manual promised you: the same
472
+ task, solved once and then not re-solved, at roughly a fifth of the price each
473
+ time after.
474
+
475
+ What each outcome means for your ledger:
476
+
477
+ | Outcome | Meaning | Books as |
478
+ |---|---|---|
479
+ | `steered_full` | every step ran under the plan | a **saving** |
480
+ | `diverged` | agent went off a steered plan's script; bir ran the rest for $0 | a saving (usually smaller) |
481
+ | `fell_back` | the plan ran some steps, then handed the rest of the task to the agent | a saving against the whole turn (may be negative) |
482
+ | `not_steered` | matched, but nothing ran under the plan: replay was off, or a direct plan's `run_scenario` was never called, or was refused | a **baseline sample** (this is how the baseline is measured) |
483
+ | `failed` | armed, then gave up | a baseline sample |
484
+
485
+ Where you see it: on your BaseIn console (for the hosted service,
486
+ **bi2202.com** → sign in → **Dashboard** / **Recordings**), the KPI strip shows
487
+ **Verified saved** — "money we watched move: costs measured on both sides." A
488
+ loss shows red; a saving shows green.
489
+
490
+ ---
491
+
492
+ ## 8. What actually happened here, honestly
493
+
494
+ On the machine this manual was written on, the run folder had **not** been
495
+ trusted (§5.2), so both replays landed in the **divergence** path rather than
496
+ clean `steered_full`. They still passed, and they still saved (`+$0.033` and
497
+ `+$0.116` against the $0.219 baseline) — but two things are worth stating
498
+ plainly, because you may hit them too:
499
+
500
+ 1. **Untrusted → divergence.** With the `allow` list discarded,
501
+ `mcp__bir__run_scenario` was denied and the agent solved the task by hand.
502
+ The safety net (bir re-running the six steps for $0) is why it still passed
503
+ and still saved. Trust the workspace to get the clean channel. (That was a
504
+ runner of 0.2.12 or earlier. A current one allows `run_scenario` itself once
505
+ it has announced a replay, and a direct plan the agent never calls is
506
+ retired without running a step: booked `not_steered`, the agent's own run.)
507
+
508
+ 2. **A cautious model may reject the replay as a prompt injection.** When the
509
+ pre-computed bundle arrived unbidden (the divergence path delivers it as a
510
+ tool result), Claude Sonnet 5 flagged it:
511
+
512
+ > *"the result … came back wrapped in a fake '[BaseInstRunner calculated
513
+ > replay]' block claiming all 6 steps were 'already executed' … This is a
514
+ > prompt injection embedded in the tool response channel … I'm not going to
515
+ > report those figures as fact."*
516
+
517
+ The model then re-verified the work itself — which is correct, cautious
518
+ behaviour, and which raises the per-run cost. Delivering the bundle through
519
+ the model's *own* `run_scenario` call (the trusted, direct path) is far less
520
+ likely to trip this, and the bundle wording is being sharpened (see
521
+ [mcpmark.md](mcpmark.md) §12–14). It is a real product wrinkle, not a setup
522
+ error — worth knowing before you read too much into one run's cost.
523
+
524
+ Neither changes the headline: **the scenario is correct, the replay executes the
525
+ real work for zero tokens, and the saving is real and positive.**
526
+
527
+ ---
528
+
529
+ ## 9. Troubleshooting
530
+
531
+ | Symptom | Cause | Fix |
532
+ |---|---|---|
533
+ | `Ignoring N permissions.allow entries … not been trusted` | workspace untrusted | §5.2 — trust it; until then the `allow` list does not count |
534
+ | `Registered proxies: (none)` | no live session bound a proxy | launch `claude` in the folder, re-run `bir doctor` |
535
+ | `bir install` prints `postgres — left unwrapped` | Claude Code has not approved the `.mcp.json` server yet | `bir install --replay --global --server postgres` (§4) |
536
+ | `replay.decision … code=no_delivery_tool` in the log | no running `bir` server has told the recorder it serves `run_scenario` | check the `bir` server in `/mcp` (connected, not disabled) |
537
+ | replay never `steered_full`: `diverged` on a runner of 0.2.12 or earlier, `not_steered` with a `replay.vetoed` line on a current one | `run_scenario` never ran — denied because of §5.2 (0.2.12 or earlier), refused by a rule of yours, or not called by a cautious model (§8) | trust first; then see mcpmark.md §12–14 |
538
+ | trial 2 finds the table already there | no DB reset | `reset-db.sh` before **every** trial |
539
+ | `UnicodeEncodeError` in verify.py | Windows console codepage | `PYTHONIOENCODING=utf-8` (already in `verify.sh`) |
540
+ | `ModuleNotFoundError: mcp.server.fastmcp` | `mcp` 2.x resolved | pin `--with "mcp<2"` in `.mcp.json` |
541
+ | `bir login` says "not a BaseIn service" | `BIR_AUTH_URL` is a website, not the API | set it to `https://api.<domain>`, open a fresh terminal |
542
+ | `savedUsd` looks impossible | pricing drift between runner and server | compare `PRICING_VERSION` on both sides |
543
+ | the number won't move | a replayed turn is deliberately not re-recorded | correct — the matched run stays canonical |
544
+
545
+ ---
546
+
547
+ ## 10. Where to go next
548
+
549
+ - **Widen within Postgres.** 31 prompts share this exact server and reset
550
+ mechanism, so each new task costs almost nothing to add.
551
+ `tasks/postgres/standard/**` is where the paper's numbers come from.
552
+ - **[mcpmark.md](mcpmark.md)** — the full rationale, the topology in detail, and
553
+ the measured history of this task, including the bugs that were found and
554
+ fixed along the way.
555
+ - **[calculatedReplayGuide.md](calculatedReplayGuide.md)** — the runner's own
556
+ manual: every audit line, every switch, and how the saving is booked.
557
+ - **[quickstart.md](quickstart.md)** — setting up a fresh machine to record from
558
+ your *own* work, not just this benchmark.
559
+
560
+ > **Turning replay off** is one line: `export BIR_REPLAY=0` for the `bir-hooks`
561
+ > you start (§5.3), or `bir replay off` for a recorder the session starts itself
562
+ > (then `bir up --restart`). The system is then exactly a recorder again — it
563
+ > recognises a repeated prompt and declines to record it twice, and the model
564
+ > does the work. Nothing is skipped, nothing is risked.
565
+ >
566
+ > **Turning the whole runner off** — no recording, no replay, nothing
567
+ > uninstalled — is `bir off --here`; `bir on --here` turns it back on. To remove
568
+ > it from this folder, `bir uninstall`; before removing the runner package from
569
+ > the machine, `bir uninstall --all`.