@basein/runner 0.2.12 → 0.3.1

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 (142) hide show
  1. package/README.md +540 -369
  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 -21
  9. package/dist/bin/bir-scenario.js +593 -122
  10. package/dist/bin/bir.d.ts +12 -6
  11. package/dist/bin/bir.js +2935 -489
  12. package/dist/bin/investigate.js +40 -15
  13. package/dist/bin/scenario-edit.d.ts +7 -1
  14. package/dist/bin/scenario-edit.js +43 -21
  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 +2787 -302
  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/recorder.d.ts +51 -3
  72. package/dist/record/recorder.js +3 -0
  73. package/dist/record/redact.d.ts +103 -9
  74. package/dist/record/redact.js +659 -30
  75. package/dist/record/remote-recorder.d.ts +327 -32
  76. package/dist/record/remote-recorder.js +1517 -187
  77. package/dist/record/switchable-recorder.d.ts +126 -0
  78. package/dist/record/switchable-recorder.js +351 -0
  79. package/dist/record/truncate.d.ts +40 -1
  80. package/dist/record/truncate.js +111 -4
  81. package/dist/replay/bash-channel.d.ts +35 -0
  82. package/dist/replay/bash-channel.js +133 -0
  83. package/dist/replay/bundle.d.ts +10 -0
  84. package/dist/replay/bundle.js +10 -0
  85. package/dist/replay/controller.d.ts +369 -20
  86. package/dist/replay/controller.js +1176 -107
  87. package/dist/replay/derive.d.ts +1 -0
  88. package/dist/replay/derive.js +81 -24
  89. package/dist/replay/executor.d.ts +51 -16
  90. package/dist/replay/executor.js +114 -41
  91. package/dist/replay/handover.d.ts +54 -1
  92. package/dist/replay/handover.js +55 -5
  93. package/dist/replay/logic.d.ts +62 -10
  94. package/dist/replay/logic.js +140 -22
  95. package/dist/replay/plan.d.ts +201 -27
  96. package/dist/replay/plan.js +581 -141
  97. package/dist/replay/safety.d.ts +72 -0
  98. package/dist/replay/safety.js +303 -0
  99. package/dist/replay/source-run.d.ts +10 -0
  100. package/dist/replay/source-run.js +80 -35
  101. package/dist/replay/targets.d.ts +15 -1
  102. package/dist/replay/targets.js +62 -4
  103. package/dist/replay/tool-error.d.ts +15 -1
  104. package/dist/replay/tool-error.js +33 -1
  105. package/dist/replay/types.d.ts +7 -4
  106. package/dist/upstream/client.d.ts +56 -3
  107. package/dist/upstream/client.js +61 -4
  108. package/dist/upstream/headers.d.ts +43 -0
  109. package/dist/upstream/headers.js +90 -0
  110. package/dist/upstream/http-client.d.ts +213 -22
  111. package/dist/upstream/http-client.js +1034 -168
  112. package/dist/upstream/http-transport.d.ts +143 -0
  113. package/dist/upstream/http-transport.js +391 -0
  114. package/dist/upstream/lazy-client.d.ts +8 -0
  115. package/dist/upstream/lazy-client.js +29 -1
  116. package/dist/upstream/sse.d.ts +78 -0
  117. package/dist/upstream/sse.js +189 -0
  118. package/dist/upstream/stdio-client.d.ts +61 -1
  119. package/dist/upstream/stdio-client.js +356 -33
  120. package/dist/util/atomic-write.d.ts +29 -0
  121. package/dist/util/atomic-write.js +96 -0
  122. package/dist/util/journal.d.ts +15 -0
  123. package/dist/util/journal.js +48 -3
  124. package/dist/util/log.d.ts +10 -0
  125. package/dist/util/log.js +28 -1
  126. package/dist/util/off-switch.d.ts +46 -0
  127. package/dist/util/off-switch.js +91 -0
  128. package/dist/util/service-health.d.ts +133 -0
  129. package/dist/util/service-health.js +217 -0
  130. package/docs/BaseInstRunner.md +694 -621
  131. package/docs/calculatedReplay.md +1625 -1304
  132. package/docs/calculatedReplayGuide.md +1212 -1087
  133. package/docs/installRun.md +1013 -772
  134. package/docs/loginWeb.md +627 -607
  135. package/docs/mcpmark.md +49 -16
  136. package/docs/my-first-sample.md +569 -545
  137. package/docs/quickstart.md +384 -311
  138. package/docs/t-bench.md +406 -394
  139. package/package.json +58 -57
  140. package/production.md +1371 -0
  141. package/scripts/install.ps1 +97 -6
  142. package/scripts/install.sh +104 -7
@@ -1,1087 +1,1212 @@
1
- # Calculated Replay — instructions
2
-
3
- > The operator's half of [calculatedReplay.md](calculatedReplay.md). That document says *why*; this
4
- > one says *what to type*, in order, from an empty machine to a prompt that answers itself.
5
- >
6
- > **Read this first.** Replay executes tool calls with arguments a scenario computed, and it does so
7
- > **without the permission prompts you would normally see** (design §13.2). `bir setup` (§1) turns it
8
- > on. Read §5 before you leave it on; `bir replay off` turns it off, and §2–§4 work either way.
9
-
10
- ---
11
-
12
- ## 0. The shape of it, in thirty seconds
13
-
14
- ```
15
- 1. Record you work normally; bir records the run
16
- 2. Calculate BaseIn analyses that run into a reusable scenario
17
- 3. Match next time you ask the same thing in different words,
18
- the server recognises it and hands the scenario back
19
- 4. Replay bir runs the scenario's tools instead of the model
20
- rediscovering the plan
21
- ```
22
-
23
- All four are implemented — see §10 for the map from design phase to code, and for how to run the
24
- end-to-end smoke test.
25
-
26
- ---
27
-
28
- ## 1. Prerequisites
29
-
30
- | # | You need | Check it with |
31
- |---|---|---|
32
- | 1 | A BaseIn service you can reach | `bir status` — its `BaseIn :` line is the address `bir setup` stored; `curl -fsS "<that address>/health"` proves it answers |
33
- | 2 | Anthropic configured **on the server** — calculation and derivation both need it | a `503 anthropic_not_configured` from `/calculate` means it is not |
34
- | 3 | `SIMILARITY_DETECTION_ENABLED=true` on the server (default) | otherwise no prompt ever matches |
35
- | 4 | BaseInstRunnerMCP built and installed in your project | `bir status` |
36
- | 5 | Tier 1 — the hooks wired and the recorder running | `bir doctor`; the SessionStart hook starts it |
37
- | 6 | *(replay only)* signed in, so the service can read what each request acts on | `bir doctor` — `derive=the service`; an `ANTHROPIC_API_KEY` here replaces it, see §5.3 |
38
-
39
- Tier 1 is not optional for replay. A match is a match on **the prompt**, and a standalone proxy never
40
- sees one (design §1.1). If `bir doctor` says `Recording tier : standalone (Tier 2)`, replay cannot
41
- arm, and that is the honest ceiling rather than a bug.
42
-
43
- One line, pasted into a terminal opened in the project you start Claude Code in. The console's *Set
44
- up the runner* page shows it with a one-time setup token filled in. Pasted in the home folder or a
45
- drive root it stops before downloading anything — `cd` to the project and paste the same line again:
46
-
47
- ```powershell
48
- $env:BIR_SETUP_TOKEN="<token>"; irm https://api.bi2202.com/install.ps1 | iex # Windows (PowerShell)
49
- ```
50
-
51
- ```bash
52
- curl -fsSL https://api.bi2202.com/install.sh | BIR_SETUP_TOKEN="<token>" sh # macOS / Linux
53
- ```
54
-
55
- From cmd.exe: `powershell -NoProfile -Command "$env:BIR_SETUP_TOKEN='<token>'; irm https://api.bi2202.com/install.ps1 | iex"`.
56
-
57
- It installs Node and Claude Code if they are missing, installs the package, and runs `bir setup`:
58
- sign-in, the service address stored in `~/.baseinstrunner/config.json`, the project's MCP servers
59
- wrapped, the hooks wired into `.claude/settings.local.json`, the scenario server added, the recorder
60
- started in the background. Nothing stays open: the SessionStart hook starts the recorder whenever
61
- it is missing, and its audit lines go to `~/.baseinstrunner/logs/<key>.log`. Re-running the line
62
- upgrades. With the package already installed, the same thing is:
63
-
64
- ```bash
65
- bir setup --auth-url https://api.bi2202.com # without a token it signs in through the browser
66
- ```
67
-
68
- ---
69
-
70
- ## 2. Record a run worth reusing
71
-
72
- Nothing special — work normally. Two server-side facts decide whether the run becomes reusable, and
73
- both bite quietly:
74
-
75
- **A run needs at least `RECORDING_MIN_ACTIONS` tool actions (default 4).** Below that it is not
76
- embedded, does not enter the similarity pool, does not appear in the list, and is eventually pruned.
77
- A two-tool run will never match anything, no matter how many times you repeat it.
78
-
79
- **The prompt is what gets embedded.** `findSimilarRun` embeds `run.input` — the text of your
80
- `UserPromptSubmit` — not the transcript, not the answer. So the prompt that *starts* the turn is the
81
- one future prompts are compared against. Write the first prompt of a reusable task as if someone else
82
- will have to phrase it again.
83
-
84
- Watch it land:
85
-
86
- ```
87
- [bir] … run.start run=run_… sess=birsess_… tier=bound
88
- [bir] … run.finish run=run_… steps=14 durationMs=48210 lossy=false recorded=true
89
- ```
90
-
91
- `recorded=false` means the service declined the run — usually because it matched something you
92
- already have. That is §4 working.
93
-
94
- List what you have:
95
-
96
- ```bash
97
- bir scenario list # or: GET /recordings/runs
98
- ```
99
-
100
- ```
101
- run_5f3a… Fleet risk sweep
102
- iterations=3 steps=14 scenario=scn_9c1b… (ready)
103
- run_a812… Rewrite the billing README
104
- iterations=1 steps=9 scenario=none — run `bir scenario calc <runId>`
105
- run_c004… Check open PRs for stale reviews
106
- iterations=1 steps=6 scenario=scn_44d0… (calculating)
107
- ```
108
-
109
- Ids are printed in full on their own line (shortened here), so they can be copied straight into
110
- `calc` and `replay`.
111
-
112
- ---
113
-
114
- ## 3. Calculate the scenario
115
-
116
- A recorded run is a transcript. A **scenario** is what BaseIn derives from it: an intent, a parameter
117
- schema, and one JavaScript logic body per step that recomputes that step's tool arguments from new
118
- parameters. Until a run has one, a match can only stop recording — there is nothing to run.
119
-
120
- ```bash
121
- bir scenario calc run_5f3a… # POST /recordings/runs/:runId/calculate → 202
122
- bir scenario show run_5f3a… # GET /recordings/runs/:runId/scenario (poll)
123
- ```
124
-
125
- Raw, if you prefer — the service address lives in `~/.baseinstrunner/config.json` (`BIR_AUTH_URL`
126
- is normally unset after `bir setup`) and the token in `~/.baseinstrunner/credentials.json`:
127
-
128
- ```bash
129
- BASEIN=$(node -p "require(require('os').homedir()+'/.baseinstrunner/config.json').authUrl")
130
- TOKEN=$(node -p "require(require('os').homedir()+'/.baseinstrunner/credentials.json').accessToken")
131
- curl -fsS -X POST "$BASEIN/recordings/runs/run_5f3a…/calculate" \
132
- -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{}'
133
- ```
134
-
135
- ```powershell
136
- $b = (Get-Content "$HOME\.baseinstrunner\config.json" | ConvertFrom-Json).authUrl
137
- $t = (Get-Content "$HOME\.baseinstrunner\credentials.json" | ConvertFrom-Json).accessToken
138
- Invoke-RestMethod -Method Post -Uri "$b/recordings/runs/run_5f3a…/calculate" `
139
- -Headers @{ authorization = "Bearer $t" } -ContentType 'application/json' -Body '{}'
140
- ```
141
-
142
- Calculation is a background job. Poll `show` until `state` leaves `calculating`:
143
-
144
- | `state` | Meaning | Next |
145
- |---|---|---|
146
- | `calculating` | job running | poll again in a few seconds |
147
- | `ready` | usable | §4 |
148
- | `failed` | see the `error` field | fix and recalculate with `--force` |
149
-
150
- `409 scenario_exists` means this run already has one. Re-derive it in place — keeping the id and its
151
- replay history — with `bir scenario calc run_… --force`. A `run_` id reaches only the run's
152
- whole-run scenario. From the next version (not yet released), `bir scenario calc scn_… --force`
153
- does the same by the scenario's own id, a sub-task scenario included; it is the only way to
154
- recalculate a sub-task scenario from `bir`.
155
-
156
- To change one step instead, without recalculating, see §9.2. A plan with steps edited by hand is
157
- not rebuilt unless you add `--discard-edits`, which replaces those edits. They stay in
158
- `bir scenario edits`, marked replaced, and can no longer be undone.
159
-
160
- ### 3.1 Check the scenario before you trust it
161
-
162
- **Dry replay is the cheapest thing you will do all day. Do it every time.** It derives parameters
163
- from a fresh prompt and runs the stored logic against the *original run's recorded outputs* — no real
164
- tools, no side effects, one Haiku call:
165
-
166
- ```bash
167
- bir scenario replay scn_9c1b… --prompt "which devices in the fleet look risky right now?" --dry
168
- ```
169
-
170
- ```
171
- params { "fleetId": "eu-west-1", "riskThreshold": 0.8 }
172
- step 0 mcp__chrome-devtools__navigate_page { "url": "https://fleet.internal/eu-west-1" }
173
- step 1 mcp__chrome-devtools__take_snapshot {}
174
- → emitted: devices
175
- step 2 mcp__fleet-api__device_detail { "id": "dev_88f1" }
176
- → emitted: highestRiskDeviceId, riskScore
177
- response { "fleet": "eu-west-1", "atRisk": 3, "worst": "dev_88f1" }
178
- cost $0.0012
179
- ```
180
-
181
- Read it for three things:
182
-
183
- 1. **Did the parameters come out right** for a prompt phrased differently from the original?
184
- 2. **Do the step inputs look computed**, or are they the original run's constants copied through? A
185
- scenario whose inputs never vary with the parameters is a recording, not a scenario.
186
- 3. **Is `emitted` non-empty** for steps that later steps depend on? `emitted` lists key names only;
187
- `→ emitted: (nothing — toolOutputLogic derived no keys)` means every downstream input reading
188
- that step will be wrong.
189
-
190
- A `↑ not named by the prompt:` line under `params` lists targets the dry run filled from the
191
- recording. A live turn finds those in an earlier step, or does not run — so a green trace with
192
- that line is not yet evidence that a session would steer.
193
-
194
- A dry replay is recorded as `outcome: 'dry'` and is excluded from savings by construction — it costs
195
- you a Haiku call and nothing else.
196
-
197
- ---
198
-
199
- ## 4. Confirm the match fires
200
-
201
- Now ask the same thing in different words, in a new session.
202
-
203
- ```
204
- you: can you check the eu-west fleet and tell me which boxes are in trouble?
205
- ```
206
-
207
- ```
208
- [bir] … run.matched run=run_… matchedRun=run_5f3a… similarity=0.94
209
- why="similar prompt — the service kept its own run; not recording this one"
210
- ```
211
-
212
- That line is the whole feature working up to the point of replay. If it does not appear, the prompt
213
- did not clear the server's `SIMILARITY_THRESHOLD` (default 0.9) — see §8.
214
-
215
- **With replay off (`bir replay off`), this is where it stops.** The server keeps its canonical run, bumps
216
- its `iterations`, this turn is not recorded, and the model answers the ordinary way. Nothing is lost
217
- and nothing is skipped.
218
-
219
- ---
220
-
221
- ## 5. Turn replay on
222
-
223
- > Read §5.1 before running anything in this section.
224
-
225
- ### 5.1 What you are agreeing to
226
-
227
- A calculated scenario is, in effect, **a pre-approved list of tool calls with computed arguments.**
228
- When replay arms:
229
-
230
- - **Steered steps are auto-approved.** `PreToolUse` is answered `allow`, so a pinned `Bash` or `Edit`
231
- runs without the prompt you would normally see.
232
- - **Direct steps are not visible to the permission system at all.** They are dispatched straight to a
233
- `bir-proxy`, which calls the upstream on a connection it already holds. Claude Code never sees them.
234
-
235
- That is what makes replay fast, and it is the whole risk. Two things follow:
236
-
237
- 1. Only enable it in a project whose scenarios you have dry-replayed and read (§3.1).
238
- 2. Run `bir replay allow <servers>` (§5.3). An allowlist of the servers you are comfortable having
239
- called unattended is cheap insurance, and it is the difference between "replay reads a dashboard"
240
- and "replay can do anything any wrapped server can do".
241
-
242
- ### 5.2 Enable it
243
-
244
- `bir setup` already did this: it installed the scenario server and pre-approved it, so Claude Code
245
- asks no question about it. By hand:
246
-
247
- ```bash
248
- bir install --replay # adds the `bir` MCP server (pre-approved)
249
- bir up --restart # the recorder reads the change when it restarts
250
- ```
251
-
252
- A flag-less `bir install` keeps the invocation the project was set up with — `bir setup` installs
253
- with `--global` from an installed package and `--local` from a checkout — so this downgrades
254
- nothing. `bir install --replay` does exactly two things beyond a normal install, and
255
- `bir uninstall --replay` takes the scenario server out again and leaves everything else as it is:
256
-
257
- - registers a first-party MCP server under the key **`bir`** in the project's `.mcp.json` (never
258
- `~/.claude.json`), exposing `mcp__bir__run_scenario` — the channel a fully-wrapped scenario's
259
- results come back through. It also offers three tools that only read a scenario
260
- (`scenario_show`, `scenario_edits`, `investigate`) and, only after `bir scenario editing on`,
261
- three for changing one (`scenario_check` tries a change, `scenario_edit` saves it,
262
- `scenario_undo` takes it back). §9.2 lists them with their arguments; the editing it describes
263
- is built but not yet released. From the next version, a call to any of them except
264
- `run_scenario` is never recorded;
265
- - pre-approves that server in `.claude/settings.local.json` (`enabledMcpjsonServers`), because a
266
- "No" to Claude Code's approval dialog leaves a direct replay with nowhere to deliver its results.
267
-
268
- The `UserPromptSubmit` hook's 15 s timeout, which the match round trip needs, is not one of them:
269
- every install writes it, replay or not.
270
-
271
- The `bir` tools are visible to the model in every session in this project. That is a real cost — a
272
- few more tools in the list, a little context per turn — and it is why replay is a separate flag
273
- rather than part of `bir install`. The tools that change a scenario are not among them until you
274
- run `bir scenario editing on` (§9.2).
275
-
276
- Confirm:
277
-
278
- ```bash
279
- bir doctor
280
- ```
281
-
282
- ```
283
- Replay : ON servers=chrome-devtools,fleet-api minSimilarity=0.92 source=sidecar
284
- derive=the service (no key needed here)
285
- Wrapped in config : chrome-devtools, fleet-api
286
- Registered proxies: chrome-devtools, fleet-api
287
- Control server : http://127.0.0.1:53411
288
- BaseIn (shell) : https://api.bi2202.com (answers /health)
289
- Recording tier : bound (Tier 1)
290
- Recording : yes
291
-
292
- · recording to https://api.bi2202.com as you@example.com
293
- · the recorder runs in the background (pid 41820); its audit log is ~/.baseinstrunner/logs/<key>.log
294
- · recurring sub-tasks are observed only — nothing is replaced (BIR_SEGMENT_ARM unset)
295
-
296
- No problems found.
297
- ```
298
-
299
- `BaseIn (shell)` is the address this terminal's `bir` commands would use — `BIR_AUTH_URL` if it
300
- is set, else the one `bir setup` stored — and `recording to` is the one the running recorder
301
- uses, with the account it is signed in as. The two normally agree.
302
-
303
- ### 5.3 Environment
304
-
305
- The first three switches are stored per project, and that is where to set them:
306
-
307
- ```bash
308
- bir replay allow chrome-devtools,fleet-api # the servers direct execution may call; `allow all` clears it
309
- bir replay off # and back with `bir replay on`
310
- bir replay status # what is stored for this project
311
- bir up --restart # a running recorder reads them when it restarts
312
- ```
313
-
314
- They are kept in `~/.baseinstrunner/installed.json`. A recorder started by the SessionStart hook has
315
- Claude Code's environment, not your terminal's, and reads them from there. The environment variables
316
- below override them wherever the recorder sees them; `bir doctor` says which is in effect
317
- (`source=sidecar` or `source=env`). The proxies need no replay configuration of their own — each one
318
- is told whether to open its work loop when it registers, so there is exactly one switch and it cannot
319
- get out of step with itself.
320
-
321
- | Variable | Default | What it does |
322
- |---|---|---|
323
- | `BIR_REPLAY` | *(on)* | `0` disables replay. Unset or any other value keeps it on; nothing below matters while it is `0` |
324
- | `BIR_REPLAY_ALLOW_SERVERS` | *(the stored list, else all wrapped)* | Comma-separated server keys eligible for **direct** execution. Overrides `bir replay allow` |
325
- | `BIR_MIN_STEER_SIMILARITY` | `0.92` | Below this a match is detected but not replayed (§8) |
326
- | `ANTHROPIC_API_KEY` | *(unset)* | **Not required.** Derivation — reading what this request acts on — is done by the service on its key for a signed-in runner. Set this to keep the reading on this machine instead: the prompt then never leaves it, and it is one round trip faster. Signed out *and* unset, only a scenario with nothing to work out replays |
327
- | `BIR_DERIVE_MODEL` | `claude-haiku-4-5-20251001` | The derivation model, when this machine does the reading |
328
- | `BIR_MATCH_BUDGET_MS` | `2500` | How long the prompt hook waits for a match before giving up |
329
- | `BIR_DERIVE_BUDGET_MS` | `8000` | How long the first `PreToolUse` waits for parameters |
330
- | `BIR_REPLAY_BUDGET_MS` | `120000` | Whole-plan ceiling for direct execution |
331
- | `BIR_STEP_TIMEOUT_MS` | `60000` | Per-step ceiling for one direct `tools/call` |
332
- | `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
333
-
334
- **No API key is a supported configuration, not a broken one.** Signed in, the service reads the
335
- prompt on its own key and the replay is the same replay. Signed out *and* without a key, nothing
336
- can read the prompt: a *setting* takes the value it had in the recorded run, so a scenario whose
337
- parameters are all settings — a dashboard sweep, a fixed report — is still a complete, free replay;
338
- but a scenario with a *target* (the thing the task acts on: "check *eu-west*") is declined rather
339
- than run on last week's value, and the agent does the task normally. `bir doctor` names the state
340
- you are in: `derive=this machine (ANTHROPIC_API_KEY)`, `derive=the service (no key needed here)`,
341
- or `derive=recorded sample values — scenarios with a target will NOT run`.
342
-
343
- ---
344
-
345
- ## 6. Run it, and read what happened
346
-
347
- Ask the matching question again, with replay on.
348
-
349
- ### 6.1 A fully-wrapped scenario — direct
350
-
351
- ```
352
- [bir] … run.matched run=run_… matchedRun=run_5f3a… similarity=0.94
353
- [bir] … plan.armed scenario=scn_9c1b… mode=direct steps=3
354
- tools="mcp__chrome-devtools__navigate_page,mcp__chrome-devtools__take_snapshot,mcp__fleet-api__device_detail"
355
- [bir] … replay.derived scenario=scn_9c1b… params=2 costUsd=0.0011 source=the service
356
- [bir] … replay.step n=0 tool=navigate_page server=chrome-devtools ms=612 ok=true
357
- [bir] … replay.step n=1 tool=take_snapshot server=chrome-devtools ms=1104 ok=true emitted=devices
358
- [bir] … replay.step n=2 tool=device_detail server=fleet-api ms=210 ok=true emitted=highestRiskDeviceId,riskScore
359
- [bir] … replay.done scenario=scn_9c1b… mode=direct steps=3/3 outcome=steered_full ms=2766
360
- [bir] … execution.reported scenario=scn_9c1b… outcome=steered_full derive=0.0011 session=0.0083 fallback=0.0000 savedUsd=0.114
361
- ```
362
-
363
- In the session you will see one tool call, `mcp__bir__run_scenario`, returning the composed results.
364
- The model reads them and answers. It never emitted the three underlying calls.
365
-
366
- ### 6.2 A mixed scenario — steer
367
-
368
- The model emits each call itself; `bir` overrides the arguments. The per-step `replay.pin` and
369
- `replay.thread` lines are detail lines, printed only under `BIR_VERBOSE=1`; without it a steer run
370
- shows `plan.armed`, `replay.done`, and any `replay.thread_fallback` between them.
371
-
372
- ```
373
- [bir] … plan.armed scenario=scn_44d0… mode=steer steps=4
374
- [bir] … replay.pin n=0 tool=mcp__github__list_pull_requests
375
- [bir] … replay.thread n=0 via=proxy emitted=prNumbers done=false
376
- [bir] … replay.pin n=1 tool=Read
377
- [bir] … replay.thread n=1 via=hook emitted=reviewers done=false
378
- [bir] … replay.done scenario=scn_44d0… mode=steer steps=4/4 outcome=steered_full
379
- ```
380
-
381
- `via=proxy` versus `via=hook` matters: a wrapped MCP step's output is threaded from the **proxy's**
382
- report, because that is the shape the scenario's logic was written against (design §7.2). A
383
- `replay.thread_fallback run=… tool=… why="no proxy report — threading the hook's view; shapes may differ"`
384
- line — printed whether or not you are verbose — means the proxy did not report within its grace
385
- window and the hook's differently-shaped view was used instead. Expect derived values to be empty,
386
- and treat repeats as a bug.
387
-
388
- ### 6.3 The model goes off-script
389
-
390
- ```
391
- [bir] … replay.diverge scenario=scn_44d0… expected=Read called=Bash step=2/4
392
- [bir] … replay.compose scenario=scn_44d0… remaining=2 executed=1 recorded=1 skipped=0 bytes=8412 costUsd=0.00
393
- [bir] … replay.done scenario=scn_44d0… mode=steer steps=2/4 outcome=diverged
394
- ```
395
-
396
- `costUsd=0.00` on the compose line is the point of the whole design: the remaining steps ran through
397
- proxies that were already connected, so recovering from a divergence cost no tokens at all.
398
-
399
- ### 6.4 The audit vocabulary
400
-
401
- Everything replay decides is one line. `grep` for these:
402
-
403
- | Line | Means |
404
- |---|---|
405
- | `run.matched` | The server recognised the prompt |
406
- | `replay.decision … verdict=no-steer` | Matched, and declined. `why=` says which gate |
407
- | `plan.armed` | A plan exists. Carries mode, step count and the ordered tool names — **before** anything runs |
408
- | `replay.derived` / `replay.derive_failed` | Parameters resolved, or fell back to sample values |
409
- | `replay.pin` / `replay.thread` | One steered step's input pinned / output threaded — detail lines, only under `BIR_VERBOSE=1` |
410
- | `replay.thread_fallback` | A wrapped step's output was threaded from the hook's view because the proxy did not report in time |
411
- | `replay.step` | One direct step executed |
412
- | `replay.diverge` / `replay.compose` | Off-script, and the recovery |
413
- | `replay.done` | Final outcome, steps completed, elapsed |
414
- | `execution.reported` | The ticket was redeemed and the saving booked |
415
-
416
- A recorder started in the background — by the SessionStart hook, `bir up` or `bir setup` — keeps
417
- them in `~/.baseinstrunner/logs/<key>.log`; `bir status` names the file. One started in a terminal
418
- writes them to that terminal; keep them yourself: `bir-hooks 2>&1 | tee -a ~/.baseinstrunner/audit.log`.
419
-
420
- ---
421
-
422
- ## 7. What each outcome means for your numbers
423
-
424
- Reported once per matched turn, to `POST /scenarios/:id/executions`, redeeming the match's
425
- `executionTicket`.
426
-
427
- | Outcome | When | Where it lands |
428
- |---|---|---|
429
- | `steered_full` | Every planned step ran under the scenario's control | savings ledger |
430
- | `diverged` | Off-script; the composed bundle answered | savings ledger |
431
- | `not_steered` | Matched, declined at a gate — the agent ran the task normally | **baseline sample** |
432
- | `failed` | Armed, then gave up — the agent ran the task normally | **baseline sample** |
433
-
434
- `not_steered` is not an error, and it is not wasted. A saving is `baseline − actual`, and the baseline
435
- is *what this task costs when the agent does it*. Every declined match is a fresh measurement of
436
- exactly that. A deployment that never declines has no baseline to measure against — which is a good
437
- reason to leave `BIR_MIN_STEER_SIMILARITY` where it is rather than driving it to zero.
438
-
439
- An **unredeemed** ticket books nothing at all. Silence loses a saving; it never invents one.
440
-
441
- ---
442
-
443
- ## 8. Tuning
444
-
445
- Only one knob matters in practice.
446
-
447
- **`BIR_MIN_STEER_SIMILARITY` (default 0.92).** The server detects matches at 0.9 and uses that to
448
- decide *"don't record this again"* — cheap: the worst case is a lost recording. Steering makes a much
449
- stronger claim, *"don't think about this again"*, and a 0.90-similar prompt usually covers only part
450
- of the live task. Steering it produces a confident, thin, wrong answer.
451
-
452
- Tune it from evidence, not taste. Run for a week and read the declines:
453
-
454
- ```bash
455
- grep 'replay.decision' audit.log | grep 'why="similarity below threshold"'
456
- ```
457
-
458
- - Prompts you *would* have wanted replayed, sitting at 0.90–0.92 → lower it, in steps of 0.01.
459
- - Replays that answered the wrong question → raise it, and re-read the scenario's `intent`.
460
-
461
- If a specific pair of prompts *should* match and does not, the fix is usually upstream of the
462
- threshold: the two prompts genuinely mean different things to the embedding model. Re-record with a
463
- first prompt that states the task plainly.
464
-
465
- ---
466
-
467
- ## 9. Troubleshooting
468
-
469
- | Symptom | Cause | Fix |
470
- |---|---|---|
471
- | No `run.matched`, ever | Prompt below the server's `SIMILARITY_THRESHOLD` | Rephrase closer, or lower it server-side. Confirm the original run is in the list — a sub-`RECORDING_MIN_ACTIONS` run is never embedded |
472
- | `run.matched` but no `plan.armed` | A gate declined | Read the `replay.decision` line's `why=`. Ranked by frequency: `bir replay off` or `BIR_REPLAY=0` · scenario not `ready` · similarity below threshold · no step is executable |
473
- | `why="no step is executable"` | None of the scenario's tools is a wrapped MCP server and none is a built-in reachable in this session | Wrap the servers the scenario uses (`bir install --server …`) and recalculate |
474
- | `plan.armed mode=steer` where you expected `direct` | At least one step is a built-in, an unwrapped MCP server, or `claude-in-chrome` | `bir status` shows what is wrapped. `claude-in-chrome` is `scope: "dynamic"` and can never be wrapped |
475
- | Model ignores the directive and diverges every turn | Steering is advisory — the model chooses; `bir` only pins arguments | Expected occasionally. Persistent divergence usually means the scenario's tools do not fit the live task; check `intent` |
476
- | A `replay.thread` line with no `emitted=` (under `BIR_VERBOSE=1`) | `toolOutputLogic` derived nothing — usually the output shape changed since recording | Dry-replay (§3.1). If the dry run emits and the live run does not, the tool's output shape has drifted; recalculate |
477
- | `replay.thread_fallback` on a wrapped step | The proxy did not report within the grace window | Check that proxy is alive (`bir doctor`). Repeats mean a slow or dying upstream |
478
- | `run_scenario` returns "no scenario is armed" | The model called it on a turn with no plan | Harmless. It happens when the model remembers the tool from an earlier turn |
479
- | `recorder.send_failed … error="HTTP 409 … invalid_ticket"` | The execution report's ticket was already redeemed, or belongs to another scenario | Harmless if the body says `duplicate: true`. Otherwise a rollover raced `SessionEnd`; the server books once |
480
- | `savedUsd` looks impossible | Pricing drift between the runner's table and the server's | Compare `PRICING_VERSION` on both sides. Design §11.3 documents a known live drift between two existing copies |
481
- | The hand-over note says a step "did not run: its input needs a judgement" | The service marked the step non-deterministic when it calculated the plan: the generated logic could not compute its input from the prompt or from earlier outputs, so the recorded value was a copy (the service's plan-services design, D8 kind 3 — it lives in the BaseIn repository, not this one). `nondeterministicWhy: by_hand` means someone marked it with `bir scenario edit --freeze` | If the code only has a slip, fix the step yourself: `bir scenario show scn_… --step <n>`, write the input logic to a file, then `bir scenario edit scn_… --step <n> --input-logic <file>` — the service checks it against the recording before it saves it (§9.2, not yet released). Of the edits, only `--unfreeze` lifts a mark made by hand (`by_hand`); a code edit, even a forced one, leaves it in place. A real judgement — a choice with no fixed rule — cannot be written as code: the agent makes that choice and finishes the task; a model step for it is planned (W3.1). The journal's `plan.armed` line lists the step under `stops=` |
482
- | A parked step ran again after an hour, or a plan was recalculated by itself | The service's grace period (D8): a parked step is served with `stop: null` once an hour for a day and a success clears its count; if it keeps failing, or its tool is gone, the next repeat of the prompt is recorded in full and the plan is calculated again from it | Nothing: that is the repair. `bir investigate` says which it is — a step still being retried, a parked step whose repair is due, or a plan being calculated again. To repair now by hand: `bir scenario calc <runId> --force` for a whole-run plan; from the next version, `bir scenario calc <scnId> --force` works for any plan, a sub-task one included. A plan with steps edited by hand is not repaired by itself: the parked step stays parked, and `bir investigate` says the repair was skipped. Then fix the step (§9.2); or take the edits back with `bir scenario undo`, once per edit, and it is repaired as usual; or recalculate now and drop the edits: `bir scenario calc <scnId> --force --discard-edits` |
483
- | `sessionCostUsd` grows every prompt in a session | The per-run usage watermark is missing or not taken | Design §11.4. Reports without a mark must carry `measured: false` |
484
- | Everything works, nothing is saved | A replayed turn is deliberately not recorded (design §12) | Correct. The matched run stays canonical and its `iterations` is bumped |
485
- | `bir doctor`: the SessionStart hook points at a Node that is gone | The hook names Node by absolute path. That Node was uninstalled, moved, or was a version manager's per-shell copy; the hook then fails on every session, no recorder is started, and runs record Tier 2 | `bir setup` here again — it rewrites the hook to the Node that runs it |
486
- | `control.port_busy` in the log; the recorder is not running | Another program holds this project's port, or the system excludes it (on Windows, a range Hyper-V or WSL reserved). A background recorder with a recorded port does not fall back to another, because every hook but SessionStart names this one; only a `bir-hooks` you run by hand in a terminal still does | `bir setup --port <another>` in this directory — the fix the log line itself names. It rewrites the hook URLs and the scenario server's address together and restarts the recorder |
487
- | The recorder was started by the hook, and `bir doctor` says `servers=(all wrapped)` although you set `BIR_REPLAY_ALLOW_SERVERS` | The hook starts the recorder with Claude Code's environment, not your terminal's; a variable set in one terminal is not seen | `bir replay allow <a,b>` — stored for the project, read by every recorder — then `bir up --restart` |
488
-
489
- ---
490
-
491
- ### 9.1 `bir investigate` — the table above, applied for you
492
-
493
- Every audit line that explains a turn is also kept in a **journal**, one JSON
494
- object per line, under `~/.baseinstrunner/control/journal/` (one file per
495
- directory, rotated at 4 MB). The recorder names it at start — the `journal=`
496
- field of its `control.listening` line, in the audit log for a background
497
- recorder. Ask the journal and the service together:
498
-
499
- ```
500
- bir investigate # the newest turn in this directory
501
- bir investigate <run_|scn_|sexec_ id> # that run, scenario or execution
502
- bir investigate list [--limit n] # recent turns and their verdicts
503
- bir investigate executions [--limit n] [--user <email|id>] # the ledger
504
- ```
505
-
506
- The output is the turn as the journal tells it (matched what, which gate
507
- declined and why, what mode armed with which coverage, what the execution
508
- reported), then **runner findings**, then the service's view (the recording,
509
- the scenario, the baseline and its sample count, the executions with their
510
- saved $ and %) and **service findings**. Every finding is *problem → cause →
511
- fix*. The three questions it answers:
512
-
513
- | Question | Where the answer comes from |
514
- |---|---|
515
- | Why did this turn not run its calculated scenario? | `replay.decision code=…` in the journal, mapped to the cause and the fix; and the service: is the run a recording, is it embedded, how many hits, is the scenario ready / failed / switched off, which step is parked |
516
- | Why did it save so little, or cost more? | the execution's cost breakdown (derive, session, fallback) against the baseline; steer mode with built-in steps is the usual answer, a single-sample baseline the second |
517
- | What happened, step by step? | the turn's lines in order, the per-step verdicts on the execution, and `replay.*_failed`, `replay.diverge`, `replay.handover` as incidents |
518
-
519
- You see your own data. An admin of the service sees everyone's and may pass
520
- `--user` to `executions`. `--json` on any form prints the raw merge.
521
-
522
- ### 9.2 Fix a step — `bir scenario check`, `edit`, `edits`, `undo`
523
-
524
- > Built 2026-09-25, **not yet released**. It needs the next `@basein/runner` version *and* a
525
- > service deploy: the `/scenarios/:id/steps/…` routes and the sandbox the service runs step code
526
- > in. Version 0.2.11 on npm has an early form of these commands. It is not what this section
527
- > describes. The design is `editSteps.md` in the BaseIn repository.
528
- >
529
- > The hiring-year outputs below were printed by the real `bir`, against the real service, on the
530
- > real recording. Only the ids are shortened, with `…`.
531
-
532
- `bir investigate` says *what* is wrong. This section is how you fix it. A calculated step is code:
533
- its **input logic** says what to send to the tool, and its **output logic** says what to keep from
534
- the answer. When that code is wrong, you do not have to recalculate and hope any more. You change
535
- the step, and the service checks your change against the recording before it saves it.
536
-
537
- **A change is saved only if it reproduces the recording and is not a copy — or on purpose, with
538
- `--force --note "why"`.**
539
-
540
- ```
541
- bir scenario show <runId|scnId> [--step <n>]
542
- bir scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
543
- bir scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
544
- [--freeze | --unfreeze] [--note "<why>"] [--force --note "<why>"] [--revision <n>]
545
- bir scenario edits <runId|scnId>
546
- bir scenario undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "<why>"]
547
- bir scenario calc <runId|scnId> [--force [--discard-edits]]
548
- bir scenario editing on|off|status
549
- ```
550
-
551
- A `run_` id finds the run's whole-run scenario. A `scn_` id (what `bir investigate` prints) is used
552
- as it is. A sub-task scenario has only a `scn_` id. When a run has only sub-task scenarios, `bir`
553
- lists their `scn_` ids instead of guessing. `--step` is the `stepIndex` that `show` prints; it
554
- starts at 0. `--revision <n>` saves only if the plan is still at revision `n`: the `chainRevision`
555
- at the top of `show --step`. `--json` on `check`, `edit`, `edits` or `undo` prints the service's
556
- answer unchanged. Exit codes: `0` checked OK, saved or undone · `1` refused, not found, or a
557
- service error · `2` a wrong command line.
558
-
559
- #### What the code is
560
-
561
- Each logic is the *body* of a JavaScript function. It must end in `return { … }`.
562
-
563
- | Logic | Reads | Returns |
564
- |---|---|---|
565
- | input logic (`toolInputLogic`) | `parameters` — this request's values · `respParams` — what earlier steps kept · `intent` | the tool's input, as an object |
566
- | output logic (`toolOutputLogic`) | `toolOutput` — the tool's answer, as text · and the same three | the values to keep for later steps, as an object |
567
-
568
- The service runs your code in a sandbox (design §13.1). In the sandbox only JavaScript itself
569
- exists: `JSON`, `Math`, `Date`, `RegExp` and the like. There is no `require`, `process`, `fetch`,
570
- `Buffer` or timer. A few built-ins are taken away too: `ArrayBuffer`, the typed arrays such as
571
- `Uint8Array`, `Atomics`, `WebAssembly`, `FinalizationRegistry` and `Symbol.for`. Code that uses
572
- one of them with the recording's values throws in the check.
573
-
574
- **But the check only judges what your code returns for the recording's values.** A `fetch` behind
575
- a `typeof` guard (`if (typeof fetch === "function") …`), or on a branch the recording's values
576
- never take, is never reached in the check. A `fetch` inside a `try` fails in the check, but the
577
- `catch` hides the error. Either way the code passes. On your machine the runner runs the same
578
- code with `new Function`, where `fetch` and `process` do exist. So read every change before it is
579
- saved — yours and Claude's.
580
-
581
- **Only a step's code and its mark can be changed.** Not the parameters, not their sample values, not
582
- the logic that builds the final answer. Only tool steps: a sub-task call row belongs to the service.
583
-
584
- #### The fix, step by step
585
-
586
- On 2026-09-25 two SQL steps of the hiring-year scenario, 3 and 4, were marked *needs a judgement*.
587
- Step 4 is shown here; step 3 was fixed the same way. The calculation's code for step 4 wrote
588
- `schema + '.' + summary_table`, but the table name already carried its schema, so the code did not
589
- give the recorded call. The calculation then put a copy of the recorded call in its place, and
590
- marked the step.
591
-
592
- **1. Read the step.**
593
-
594
- ```bash
595
- bir scenario show scn_4a17a126-… --step 4
596
- ```
597
-
598
- You should see this (shortened: each `…` line stands for lines left out):
599
-
600
- ```
601
- {
602
- "scenarioId": "scn_4a17a126-…",
603
- "chainRevision": 1,
604
- "stepIndex": 4,
605
- "kind": "tool",
606
- "toolName": "mcp__postgres__execute_sql",
607
- "toolInputLogic": "return {\"sql\":\"SELECT * FROM employees.hiring_year_summary ORDER BY hire_year;\"};",
608
- …
609
- "nondeterministic": true,
610
- "nondeterministicWhy": "input_mismatch",
611
- "editedAt": null,
612
- "editForced": false,
613
- "stop": {
614
- "kind": "nondeterministic",
615
- "why": "input_mismatch"
616
- },
617
- "sampleInput": "{\"sql\":\"SELECT * FROM employees.hiring_year_summary ORDER BY hire_year;\"}",
618
- …
619
- }
620
- ```
621
-
622
- `scenarioId` and `chainRevision` come first. They belong to the whole scenario, and
623
- `chainRevision` is the number `edit --revision` takes. The rest is the step. Its input logic is
624
- that copy: compare it with `sampleInput`. A copy does the old job, on the old table, whatever the
625
- new request says. That is why the step is marked (`nondeterministic: true`), and why runs hand over
626
- to the agent in front of it.
627
-
628
- **2. Write the new code to a file.**
629
-
630
- ```bash
631
- cat step4.js
632
- ```
633
-
634
- ```
635
- return { sql: "SELECT * FROM " + parameters.summary_table + " ORDER BY hire_year;" };
636
- ```
637
-
638
- Code always goes in from a file, or from stdin with `-`. Never on the command line: the shell's
639
- quoting breaks JavaScript.
640
-
641
- **3. Check it.** Nothing is saved.
642
-
643
- ```bash
644
- bir scenario check scn_4a17a126-… --step 4 --input-logic step4.js
645
- ```
646
-
647
- ```
648
- Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
649
- input reproduces the recorded call
650
- later not affected: only the input logic changed
651
- mark needs a judgement (input_mismatch) → runs by itself
652
- Nothing saved. To save: bir scenario edit scn_4a17a126-… --step 4 --input-logic step4.js
653
- ```
654
-
655
- - `input` — does the code give the recorded call?
656
- - `later` — do the steps after it still give theirs? Here only the input logic changed. Nothing a
657
- later step reads has changed, so no later step was judged.
658
- - `mark` — what happens to the *needs a judgement* mark. `→ runs by itself` means the mark goes
659
- when you save, and runs stop handing over in front of this step.
660
-
661
- **4. Save it.** `edit` runs the same check, and saves only what the check approved. Add a `--note`
662
- that says why; the history keeps it.
663
-
664
- ```bash
665
- bir scenario edit scn_4a17a126-… --step 4 --input-logic step4.js --note "summary_table already carries its schema"
666
- ```
667
-
668
- ```
669
- Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
670
- input reproduces the recorded call
671
- mark needs a judgement (input_mismatch) → runs by itself
672
- Saved as sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda (revision 2). Undo: bir scenario undo scn_4a17a126-… --edit sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda
673
- ```
674
-
675
- `edit` prints fewer lines than `check`. It always prints `input`, and the other lines only when
676
- something moved or broke. Step 3 was then saved the same way, as revision 3.
677
-
678
- **5. Try it without real tools.** A dry replay (§3.1) runs the new code over the recorded outputs:
679
- `bir scenario replay scn_4a17a126-… --prompt "…" --dry`. The next real prompt uses the new code.
680
- There is nothing to restart.
681
-
682
- #### How the check decides
683
-
684
- The service runs your code the way the recorded run would have run it: with the recording's
685
- parameter values, and with what the earlier steps kept from their recorded answers.
686
-
687
- | It asks | It refuses when |
688
- |---|---|
689
- | Does the code give the recorded call? Extra spaces inside strings, and the order of keys, do not count | it gives something else (`differs`), or it crashes (`throws`) |
690
- | Is it a copy? It changes one parameter, or one earlier output, at a time — only the ones the code reads | the call never moves: the code returns the recorded call whatever the request says |
691
- | Do the later steps still give their calls? Asked only when the **output** logic changed, and only of later steps that are not marked and can be checked | a later step that gave its recorded call before does not any more |
692
- | Does the final answer still get its values? Asked only when the **output** logic changed | it did before, and does not after |
693
- | Can the mark go? | you asked to unmark the step (`--unfreeze`), and its input does not reproduce |
694
-
695
- A recorded input of `{}` is never a copy. A recorded input that is plain text, not an object, is
696
- `not_checked`.
697
-
698
- **The check is kept small.** On the service it runs in a worker thread of its own, never in the
699
- thread that answers requests. By default it may use 5 seconds of sandbox time
700
- (`EDIT_CHECK_BUDGET_MS`, 5000 ms). Past that it stops, and the change is refused as *too costly to
701
- check*. The reason `bir` prints is the service's own sentence:
702
-
703
- ```
704
- too costly to check: the check used up its 5000 ms of sandbox time (EDIT_CHECK_BUDGET_MS) before it finished.
705
- ```
706
-
707
- One run of your code may take 1 second (`LOGIC_TIMEOUT_MS`) and 64 MB of memory
708
- (`LOGIC_MEMORY_MB`), by default. Code that ran out of time or memory, or stopped the sandbox twice,
709
- is held back for 10 minutes: every run of that same code fails at once, with the same message.
710
-
711
- A refused change says where it differs. Here `wrong.js` adds the schema a second time:
712
-
713
- ```bash
714
- bir scenario check scn_4a17a126-… --step 4 --input-logic wrong.js
715
- ```
716
-
717
- ```
718
- Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
719
- input differs from the recorded call at character 32
720
- computed … FROM employees.employees.hiring_year_summary ORDER BY h…
721
- recorded … FROM employees.hiring_year_summary ORDER BY hire_year;"…
722
- later not affected: only the input logic changed
723
- mark needs a judgement (input_mismatch) → stays marked
724
- Nothing saved, and `bir scenario edit` would refuse it: the change does not reproduce the recording.
725
- Fix the code and check again. If the recording itself was wrong, edit can save it on purpose: add --force --note "why".
726
- ```
727
-
728
- `bir scenario edit` with the same file refuses it, and saves nothing:
729
-
730
- ```
731
- Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
732
- input differs from the recorded call at character 32
733
- computed … FROM employees.employees.hiring_year_summary ORDER BY h…
734
- recorded … FROM employees.hiring_year_summary ORDER BY hire_year;"…
735
- Not saved: the change does not reproduce the recording.
736
- Fix the code and try again, or save it on purpose: add --force --note "why".
737
- ```
738
-
739
- A pasted value is refused as a copy. Here `pasted.js` returns the recorded SQL as fixed text:
740
-
741
- ```bash
742
- bir scenario check scn_4a17a126-… --step 4 --input-logic pasted.js
743
- ```
744
-
745
- ```
746
- Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
747
- input is a copy: it returns the recorded call whatever the parameters say
748
- later not affected: only the input logic changed
749
- mark needs a judgement (input_mismatch) → stays marked
750
- Nothing saved, and `bir scenario edit` would refuse it: the input logic is a copy: it returns the recorded call whatever the parameters and earlier outputs say.
751
- Fix the code and check again. If the recording itself was wrong, edit can save it on purpose: add --force --note "why".
752
- ```
753
-
754
- Any other refusal prints the service's own reason, one line for each problem.
755
-
756
- Code and a mark the step already has change nothing. `check` then says
757
- `Nothing would change: the step already has this code and mark.` (exit 0), and `edit` refuses:
758
-
759
- ```bash
760
- bir scenario edit scn_4a17a126-… --step 4 --input-logic step4.js
761
- ```
762
-
763
- ```
764
- Nothing to change: the step already has this code and this mark.
765
- ```
766
-
767
- #### `--force --note`: save it on purpose
768
-
769
- **Use `--force` only when the recording itself was wrong** — for example, the recorded run sent a
770
- call with a mistake you do not want repeated. Then no correct code can reproduce it.
771
-
772
- - `--force` needs a `--note` that says why (500 characters at most). Without one, `bir` stops
773
- before it sends anything (`note_required`).
774
- - **When the check found any problem, a forced save is recorded as forced.** `bir` says so under
775
- the `Saved as …` line and repeats what the check said. `bir investigate` keeps reporting the step
776
- as a `warn`.
777
- - A forced save removes a mark **the calculation** set, unless you also pass `--freeze`. A mark
778
- made **by hand** (`--freeze`) stays. A save lifts it only with `--unfreeze`.
779
- - A step after a sub-task call row can only be saved with `--force`: the check cannot rebuild the
780
- outputs that step reads.
781
-
782
- #### Mark or unmark a step by hand
783
-
784
- - `--freeze` marks the step *needs a judgement* (`nondeterministicWhy: by_hand`). Runs then hand
785
- over to the agent in front of it. No check blocks this: a mark only makes replay more careful.
786
- - `--unfreeze` asks to remove a mark. The check decides: the step's input must reproduce. With
787
- `--force --note "why"` the mark goes anyway, and the edit is recorded as forced.
788
- `bir scenario check … --unfreeze` asks the same question without saving.
789
- - **A mark made by hand is lifted only when someone asks.** A code edit does not lift it, even one
790
- that reproduces the recording, and neither does a forced save. What lifts it: `--unfreeze`,
791
- undoing the edit that made it, or `bir scenario calc … --force --discard-edits`.
792
-
793
- #### The history, and undo
794
-
795
- After both steps were saved:
796
-
797
- ```bash
798
- bir scenario edits scn_4a17a126-…
799
- ```
800
-
801
- ```
802
- sedit_02cb1210-1938-4902-9395-5a82b321276d 2026-09-25 11:10:59Z step 3 edit revision 2 → 3 "use the settings, not the recorded names"
803
- input logic changed; needs a judgement (input_mismatch) → runs by itself
804
- sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda 2026-09-25 11:10:59Z step 4 edit revision 1 → 2 "summary_table already carries its schema"
805
- input logic changed; needs a judgement (input_mismatch) → runs by itself
806
- ```
807
-
808
- The newest entry is first. Each id is printed in full, because it is what `undo --edit` takes.
809
-
810
- Take back the newest edit:
811
-
812
- ```bash
813
- bir scenario undo scn_4a17a126-…
814
- ```
815
-
816
- ```
817
- Step 3 (mcp__postgres__execute_sql) — checked against run_67afb010-…
818
- input back as it was before that edit
819
- mark runs by itself → needs a judgement (input_mismatch)
820
- Step 3 is back as it was before sedit_02cb1210-1938-4902-9395-5a82b321276d (revision 4). Redo: bir scenario undo scn_4a17a126-… --edit sedit_1bcba735-a526-4a1c-a890-261addf2d82b
821
- One further back: bir scenario undo scn_4a17a126-…
822
- ```
823
-
824
- The same command again goes one further back:
825
-
826
- ```bash
827
- bir scenario undo scn_4a17a126-…
828
- ```
829
-
830
- ```
831
- Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
832
- input back as it was before that edit
833
- mark runs by itself → needs a judgement (input_mismatch)
834
- Step 4 is back as it was before sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda (revision 5). Redo: bir scenario undo scn_4a17a126-… --edit sedit_8d3da392-1d96-4b2e-8c5d-34f8ab668b53
835
- One further back: bir scenario undo scn_4a17a126-…
836
- ```
837
-
838
- Now the history shows the two undos, and both edits as undone:
839
-
840
- ```bash
841
- bir scenario edits scn_4a17a126-…
842
- ```
843
-
844
- ```
845
- sedit_8d3da392-1d96-4b2e-8c5d-34f8ab668b53 2026-09-25 11:11:00Z step 4 revert revision 4 → 5
846
- undoes sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda; input logic changed; runs by itself → needs a judgement (input_mismatch)
847
- sedit_1bcba735-a526-4a1c-a890-261addf2d82b 2026-09-25 11:11:00Z step 3 revert revision 3 → 4
848
- undoes sedit_02cb1210-1938-4902-9395-5a82b321276d; input logic changed; runs by itself → needs a judgement (input_mismatch)
849
- sedit_02cb1210-1938-4902-9395-5a82b321276d 2026-09-25 11:10:59Z step 3 edit revision 2 → 3 "use the settings, not the recorded names"
850
- input logic changed; needs a judgement (input_mismatch) → runs by itself; undone 2026-09-25 11:11:00Z
851
- sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda 2026-09-25 11:10:59Z step 4 edit revision 1 → 2 "summary_table already carries its schema"
852
- input logic changed; needs a judgement (input_mismatch) → runs by itself; undone 2026-09-25 11:11:00Z
853
- ```
854
-
855
- Step 4 is the calculation's code again: `bir scenario show scn_4a17a126-… --step 4` now prints
856
- `"chainRevision": 5`, `"editedAt": null` and the mark `"nondeterministicWhy": "input_mismatch"`.
857
-
858
- How undo works:
859
-
860
- - **Undo walks back.** `bir scenario undo <id>` takes back the newest edit that can be undone. Its
861
- step goes back as it was before that edit: its code and its mark. Run it again to go one further
862
- back. Keep going, and each step gets back to the calculation's code, unless an undo is refused
863
- (below).
864
- - **`--step <n>` limits it to one step.** `bir scenario undo scn_… --step 4` takes back step 4's
865
- newest edit, and nothing else.
866
- - `--edit <sedit_id>` takes back that one entry.
867
- - An undo is a new line in the history (`revert`). **To redo, undo the undo** by its id. The undo
868
- prints that command after `Redo:`.
869
- - **An undo is checked too:** for the later steps and for the final answer, like a save (so only
870
- when the undo changes the step's output logic). The step itself is not judged: it goes back
871
- exactly as it was. If the undo would break a later step or the answer, it is refused
872
- (`undo_refused`). `bir` prints `Not undone:` with the reason, then
873
- `To undo it anyway, on purpose: add --force --note "why".` An undo forced past a refusal is
874
- recorded as forced.
875
- - Once every hand edit of a plan is taken back, the service repairs and rebuilds it as usual again.
876
- An undo forced past a refusal is the exception: it leaves its step counted as edited and
877
- forced, so `bir investigate` keeps warning about it.
878
-
879
- #### What a save changes
880
-
881
- - **The next prompt uses the new code.** The service hands out the plan fresh on every prompt. A
882
- turn that is already running keeps the old plan until it ends.
883
- - **The revision goes up by one, and the step's failure count is cleared.** Reports from turns that
884
- started before the change do not count against the new code. An undo does the same.
885
- - **A saved step runs unattended, exactly like calculated code** (§5.1), on this machine, with
886
- `new Function`. The service's sandbox does not protect this machine. Read your change as carefully
887
- as you read a calculated plan, and dry-replay it (§3.1).
888
- - **Edits are protected.** A recalculation rebuilds every step from the recording, and would throw
889
- your fix away. So `calc --force` refuses:
890
-
891
- ```bash
892
- bir scenario calc run_67afb010-… --force
893
- ```
894
-
895
- ```
896
- Not recalculated: steps 3, 4 were edited by hand, and a recalculation rebuilds every step.
897
- To recalculate anyway and discard the edits: bir scenario calc run_67afb010-… --force --discard-edits
898
- (the edits stay in `bir scenario edits`, marked replaced)
899
- ```
900
-
901
- A `run_` id reaches only the run's whole-run scenario. `bir scenario calc <scnId> --force`
902
- recalculates any scenario in place, by its own id, a sub-task scenario included. Add
903
- `--discard-edits` when it has hand edits. A `scn_` id without `--force` is refused before anything
904
- is sent: a scenario that exists can only be recalculated.
905
-
906
- The console's *Recalculate* button (on a sub-task's scenario) is refused too while a plan has hand
907
- edits; the console does not discard them. The service's own rebuilds — the repair of a parked step
908
- (§9) and the adoption of a sub-task — skip a plan with hand edits. `bir investigate` says that the
909
- service does not rebuild the plan on its own, and warns when it skipped a repair
910
- (`repair_skipped_edits`). After `--discard-edits` the old edits stay readable, but they cannot be
911
- undone (`edit_replaced`).
912
-
913
- #### What `bir investigate` says
914
-
915
- It reports every edited step, and names the command that takes it back. After both steps were
916
- saved, its service findings began like this:
917
-
918
- ```
919
- Service findings
920
- 1. [info] Step 3 (mcp__postgres__execute_sql) was edited by hand on 2026-09-25; the chain is at revision 3.
921
- why: Its code or its mark was changed by hand rather than by the calculation, and the change was checked against the recording before it was saved. The service does not rebuild a plan with hand edits on its own.
922
- fix: To see it: `bir scenario edits scn_4a17a126-…`; to take it back: `bir scenario undo scn_4a17a126-… --step 3`. A recalculation will not replace it unless you pass --discard-edits.
923
- 2. [info] Step 4 (mcp__postgres__execute_sql) was edited by hand on 2026-09-25; the chain is at revision 3.
924
- why: Its code or its mark was changed by hand rather than by the calculation, and the change was checked against the recording before it was saved. The service does not rebuild a plan with hand edits on its own.
925
- fix: To see it: `bir scenario edits scn_4a17a126-…`; to take it back: `bir scenario undo scn_4a17a126-… --step 4`. A recalculation will not replace it unless you pass --discard-edits.
926
- ```
927
-
928
- A forced edit is a `warn`, not an `info`. So is a repair the service skipped because of hand edits.
929
-
930
- #### Let Claude do it
931
-
932
- Claude Code can do all of this for you, in two ways. Both use the same service routes and the same
933
- check.
934
-
935
- 1. **In its terminal.** Claude runs `bir scenario …` like any other command. You see each command,
936
- and Claude Code asks you before it runs one, unless you allowed it. This works in every project.
937
- 2. **As tools on the `bir` MCP server** — the server `bir setup` or `bir install --replay` adds
938
- (§5.2). This is smoother for the model. **Reading is always offered. Changing is off until you
939
- turn it on, per project:**
940
-
941
- ```bash
942
- bir scenario editing on # this project's `bir` server also offers the tools that change a scenario
943
- bir scenario editing off # back to the tools that only read
944
- bir scenario editing status # what is stored for this project
945
- ```
946
-
947
- The tools, as the `bir` server defines them. **Bold** arguments are required.
948
-
949
- | Tool (`mcp__bir__…`) | Offered | Arguments | Runs |
950
- |---|---|---|---|
951
- | `run_scenario` | always | none | the plan armed for this turn (§6.1) |
952
- | `scenario_show` | always | **`target`**, `step` | `bir scenario show` |
953
- | `scenario_edits` | always | **`target`** | `bir scenario edits` |
954
- | `investigate` | always | `id` — a `run_`, `scn_` or `sexec_` id; without it, the newest turn here | `bir investigate` |
955
- | `scenario_check` | after `bir scenario editing on` | **`target`**, **`step`**, `inputLogic`, `outputLogic`, `unfreeze` | `bir scenario check` |
956
- | `scenario_edit` | after `bir scenario editing on` | **`target`**, **`step`**, `inputLogic`, `outputLogic`, `freeze`, `unfreeze`, `force`, `note`, `revision` | `bir scenario edit` |
957
- | `scenario_undo` | after `bir scenario editing on` | **`target`**, `edit` or `step` (not both), `force`, `note` | `bir scenario undo` |
958
-
959
- - `target` is a `scn_` id, or the `run_` id of a whole-run scenario. `step` is the `stepIndex`.
960
- - `inputLogic` and `outputLogic` are the code itself, as text. The server writes them to temporary
961
- files for `bir`, and removes the files afterwards.
962
- - `revision` is the `chainRevision` at the top of `scenario_show` with `step`.
963
- - `force` needs `note`, as on the command line.
964
- - Each tool runs this package's own `bir` with `--json`. A refusal comes back as an error result
965
- that carries the check's report, so the model can read why.
966
- - When the session ends, or you press Esc during a call, that call's `bir` is stopped and its
967
- temporary files are removed. A request the service had already received may still finish there;
968
- `bir scenario edits` shows it.
969
-
970
- Why changing is off by default: the `bir` server runs in every session of every project it is
971
- installed in, a fleet included. A plan must not change because some agent there decided to change
972
- it. The switch is kept in `~/.baseinstrunner/installed.json`, next to the replay switches. It only
973
- decides which tools the `bir` server offers; the `bir scenario` commands work everywhere. The server
974
- reads the switch when Claude Code lists its tools, and again on every call. **Restart the Claude
975
- Code session after you switch it** — a running session does not see the change.
976
-
977
- **`bir`'s own calls are never recorded.** A session that fixes a plan must not become a plan that
978
- edits plans when it is replayed. So these calls are never recorded as steps, and never count as the
979
- model leaving a plan: the `bir` server's tools other than `run_scenario`, and a `Bash` or
980
- `PowerShell` command made only of `bir` commands (a plain `cd` may come with them).
981
- `npm test && bir investigate` is still recorded: the `npm test` half is real work. So is writing
982
- the code file, which is an ordinary step. Under `BIR_VERBOSE=1` a skipped call shows as
983
- `tool.pre.skipped … why="host housekeeping"`.
984
-
985
- **Do this.** In the Claude Code session, in the project folder, paste:
986
-
987
- ```
988
- Run `bir investigate` in this folder. If a step needs a judgement or its code
989
- is wrong, read it with `bir scenario show`, write the corrected code to a file,
990
- and test it with `bir scenario check`. Show me the check and explain it in plain
991
- words. Do not save anything until I say yes. Then save it with
992
- `bir scenario edit` and a --note that says why. Never use --force.
993
- ```
994
-
995
- For the hiring-year case, Claude's report would be: *step 4's code is a copy of the recorded query,
996
- so it was marked; the new code builds the query from `summary_table`; the check says it reproduces
997
- the recording, and the step will run by itself.*
998
-
999
- #### When it does not work
1000
-
1001
- | Symptom | Cause | Fix |
1002
- |---|---|---|
1003
- | `input differs from the recorded call at character N` | The code gives a different call from the recorded one | Read the `computed` and `recorded` lines, fix the code, check again |
1004
- | `input is a copy` | The code returns the recorded call whatever the request says — usually a pasted value | Build the call from `parameters.…` or `respParams.…` |
1005
- | `input throws` | The code crashed; the message is shown | Fix it. `require`, `process`, `fetch`, `Buffer`, timers, `ArrayBuffer`, the typed arrays and `Symbol.for` do not exist in the check |
1006
- | `… did not finish in N ms`, `… ran out of memory (the limit is N MB)`, or `… stopped the logic sandbox before it finished` | The code went past the sandbox's time or memory limit (`LOGIC_TIMEOUT_MS`, `LOGIC_MEMORY_MB` on the service), or stopped the sandbox twice. That same code is then refused at once for 10 minutes | Fix the loop or the size. Changed code is new code, and is checked again |
1007
- | A `later` step no longer reproduces | The change broke a step after it — usually output logic that stopped keeping a value a later step reads | Keep every key the later steps read |
1008
- | `too costly to check` | The check used up its 5 seconds of sandbox time (`EDIT_CHECK_BUDGET_MS`). Usually code that reads or keeps a great many values, so the check has to run it very many times | Make the code cheaper to run, then check again |
1009
- | `Nothing to change: the step already has this code and this mark.` | The code and the mark you gave are what the step already has (`nothing_to_change`) | Nothing to do. `check` says `Nothing would change` for the same case |
1010
- | `not_a_tool_step` | The index is a sub-task call row, not a tool step | Call rows cannot be edited; the service writes them |
1011
- | `note_required` | `--force` without `--note` | Add `--note "why"` |
1012
- | `revision_changed` | The plan changed since you read it: another edit, or a recalculation | `show` it again, check again, save again |
1013
- | `not_ready` or `scenario_calculating` | The scenario is not `ready`, or a recalculation is running | Wait for it to finish |
1014
- | `undo_refused` | The undo would stop a later step from giving its recorded call, or leave the final answer without its values | Fix that later step first. Or undo it on purpose: `--force --note "why"` |
1015
- | `not_latest_edit` | The step has changed since the edit you named: a newer entry changed it | `bir` prints two commands: undo the newer one first, then yours |
1016
- | `already_undone` | That edit was undone already | To redo it, undo the undo. `bir` prints the command |
1017
- | `edit_replaced` | A recalculation replaced the plan after that edit | Nothing to undo; the edit is history now |
1018
- | `logic_sandbox_unavailable` | The service could not run step code just now (HTTP 503). Nothing was checked, saved or undone | Try again in a minute |
1019
- | `invalid_input` | The service found the request invalid; `bir` prints its details. For example, a logic body longer than 20 000 characters, or a note longer than 500 | Shorten it |
1020
- | `payload_too_large` | The whole request is larger than the service accepts (HTTP 413, over 256 kB) | A logic body may be at most 20 000 characters, and a note 500 |
1021
- | `rate_limited` | More checks, edits and undos in a minute than the service allows (`SCENARIO_EDITS_PER_MINUTE`, 30 by default) | Wait a minute |
1022
- | `not found` for a scenario you can see | It is not yours. **Only the owner can change a scenario** — an admin gets `not found` too | Ask the owner |
1023
- | The `scenario_check`, `scenario_edit` and `scenario_undo` tools are missing | `bir scenario editing on` was not run in this project, or the session started before it | Run it here, then restart the session |
1024
-
1025
- ---
1026
-
1027
- ## 10. What is built
1028
-
1029
- All of it. Phases R0–R7 of [calculatedReplay.md](calculatedReplay.md) §17 are implemented and
1030
- tested; everything this guide describes is code you can run. §9.2 is built and tested too, but not
1031
- yet released.
1032
-
1033
- | Phase | What landed | Where |
1034
- |---|---|---|
1035
- | **R0** | Plan, logic shims, coverage, bundle | [src/replay/](../src/replay/) — `plan.ts`, `logic.ts`, `coverage.ts`, `bundle.ts` |
1036
- | **R1** | Derivation over raw `fetch`; pricing | [derive.ts](../src/replay/derive.ts), [pricing.ts](../src/replay/pricing.ts) |
1037
- | **R2** | Gate ladder, mode choice, arming, directive | [controller.ts](../src/replay/controller.ts), [control/server.ts](../src/control/server.ts) |
1038
- | **R3** | Pin, thread, retire | `onToolPre` / `onToolPost` / `onProxyStep` in [control/server.ts](../src/control/server.ts) |
1039
- | **R4** | Long-poll work queue; direct execution | [executor.ts](../src/replay/executor.ts), [proxy/session.ts](../src/proxy/session.ts) |
1040
- | **R5** | The `bir` MCP server, `/scenario/run`, `--replay` | [bin/bir-scenario.ts](../src/bin/bir-scenario.ts), [bin/bir.ts](../src/bin/bir.ts) |
1041
- | **R6** | Divergence, source-run fallback, `bir replay` | `diverge()` in [controller.ts](../src/replay/controller.ts), [source-run.ts](../src/replay/source-run.ts) |
1042
- | **R7** | Usage watermark, reporting, ticket, doctor | [control/transcript.ts](../src/control/transcript.ts), [remote-recorder.ts](../src/record/remote-recorder.ts) |
1043
- | **§9.2** *(not yet released)* | `bir scenario show/check/edit/edits/undo/calc/editing`, the `bir` server's scenario tools, `bir` calls never recorded (`editSteps.md` D13) | [bin/scenario-edit.ts](../src/bin/scenario-edit.ts), [bin/bir-scenario.ts](../src/bin/bir-scenario.ts), [record/housekeeping.ts](../src/record/housekeeping.ts); tests: [scenario-edit.test.ts](../test/scenario-edit.test.ts), [bir-scenario.test.ts](../test/bir-scenario.test.ts), [housekeeping.test.ts](../test/housekeeping.test.ts), [replay-server.test.ts](../test/replay-server.test.ts) |
1044
-
1045
- ```bash
1046
- npm test # ~450 tests, incl. test/replay.test.ts and test/replay-server.test.ts
1047
- npm run build && npm run pretest && npm run test:smoke # the smoke test needs the test build too
1048
- ```
1049
-
1050
- The smoke test is the one worth running after any change to the seams. It starts a stand-in BaseIn
1051
- service, a real `bir-hooks`, a real `bir-proxy` wrapping a real MCP server, and a real `bir-scenario`,
1052
- then drives a two-step scenario end to end through actual stdio — proving that a pinned argument
1053
- reaches the real tool, that step 1's input was computed from step 0's real output, and that the turn
1054
- books `steered_full` with `fallbackCostUsd: 0`.
1055
-
1056
- Three things the implementation settled that the design left open:
1057
-
1058
- - **`SerializedScenarioStep.recordedOutput` really is never sent.** Recorded outputs come from a lazy
1059
- `GET /recordings/runs/:runId` ([source-run.ts](../src/replay/source-run.ts)), as design §8.1 anticipated.
1060
- - **The work queue must hold work for a proxy that is mid-round-trip.** Failing fast when no poller is
1061
- *parked* lost every step after the first in a multi-step direct plan, because the proxy is between
1062
- polls exactly then. It now fails fast only for a server no proxy has served in the last minute.
1063
- - **The proxies need no replay configuration.** `/proxy/register` tells each one whether to open its
1064
- work loop, so `BIR_REPLAY` is a single switch on the control server.
1065
-
1066
- ---
1067
-
1068
- ## 11. Turning it off
1069
-
1070
- Three levels, least to most.
1071
-
1072
- ```bash
1073
- bir replay off # stop arming. Matches still detected; recording still stops on a match.
1074
- # Stored for this project; `bir up --restart` applies it, `bir replay on` undoes it
1075
- export BIR_REPLAY=0 # the same, wherever the recorder is started from (the SessionStart hook
1076
- # gives it Claude Code's environment, not your terminal's)
1077
- bir uninstall --replay # remove the `bir` MCP server only; proxies, hooks and the prompt-hook timeout stay
1078
- bir uninstall # remove everything: proxies, hooks, the pre-approval
1079
- ```
1080
-
1081
- `bir uninstall` restores a file byte-for-byte when nothing else edited it since the install, and
1082
- repairs it entry by entry otherwise. A file the install created — usually
1083
- `.claude/settings.local.json` — stays behind with our entries removed, and the line that hid it in
1084
- `.git/info/exclude` goes with them.
1085
-
1086
- With replay off the system is exactly v1 again: it recognises a repeated prompt, declines to
1087
- record it a second time, and lets the model do the work.
1
+ # Calculated Replay — instructions
2
+
3
+ > The operator's half of [calculatedReplay.md](calculatedReplay.md). That document says *why*; this
4
+ > one says *what to type*, in order, from an empty machine to a prompt that answers itself.
5
+ >
6
+ > **Read this first.** Replay executes tool calls with arguments a scenario computed, and it does so
7
+ > **without the permission prompts you would normally see** (design §13.2). `bir setup` (§1) turns it
8
+ > on. Read §5 before you leave it on; `bir replay off` turns it off, and §2–§4 work either way.
9
+
10
+ ---
11
+
12
+ ## 0. The shape of it, in thirty seconds
13
+
14
+ ```
15
+ 1. Record you work normally; bir records the run
16
+ 2. Calculate BaseIn analyses that run into a reusable scenario
17
+ 3. Match next time you ask the same thing in different words,
18
+ the server recognises it and hands the scenario back
19
+ 4. Replay bir runs the scenario's tools instead of the model
20
+ rediscovering the plan
21
+ ```
22
+
23
+ All four are implemented — see §10 for the map from design phase to code, and for how to run the
24
+ end-to-end smoke test.
25
+
26
+ ---
27
+
28
+ ## 1. Prerequisites
29
+
30
+ | # | You need | Check it with |
31
+ |---|---|---|
32
+ | 1 | A BaseIn service you can reach | `bir status` — its `BaseIn :` line is the address `bir setup` stored; `curl -fsS "<that address>/health"` proves it answers |
33
+ | 2 | Anthropic configured **on the server** — calculation and derivation both need it | a `503 anthropic_not_configured` from `/calculate` means it is not |
34
+ | 3 | `SIMILARITY_DETECTION_ENABLED=true` on the server (default) | otherwise no prompt ever matches |
35
+ | 4 | BaseInstRunnerMCP built and installed in your project | `bir status` |
36
+ | 5 | Tier 1 — the hooks wired and the recorder running | `bir doctor`; the SessionStart hook starts it |
37
+ | 6 | *(replay only)* signed in, so the service can read what each request acts on | `bir doctor` — `derive=the service`; a key of your own here (`BIR_DERIVE_API_KEY`) replaces it, see §5.3 |
38
+
39
+ Tier 1 is not optional for replay. A match is a match on **the prompt**, and a standalone proxy never
40
+ sees one (design §1.1). If `bir doctor` says `Recording tier : standalone (Tier 2)`, replay cannot
41
+ arm, and that is the honest ceiling rather than a bug.
42
+
43
+ One line, pasted into a terminal opened in the project you start Claude Code in. The console's *Set
44
+ up the runner* page shows it with a one-time setup token filled in. Pasted in the home folder or a
45
+ drive root it stops before downloading anything — `cd` to the project and paste the same line again:
46
+
47
+ ```powershell
48
+ $env:BIR_SETUP_TOKEN="<token>"; irm https://api.bi2202.com/install.ps1 | iex # Windows (PowerShell)
49
+ ```
50
+
51
+ ```bash
52
+ curl -fsSL https://api.bi2202.com/install.sh | BIR_SETUP_TOKEN="<token>" sh # macOS / Linux
53
+ ```
54
+
55
+ From cmd.exe: `powershell -NoProfile -Command "$env:BIR_SETUP_TOKEN='<token>'; irm https://api.bi2202.com/install.ps1 | iex"`.
56
+
57
+ It installs Node and Claude Code if they are missing, installs the package, and runs `bir setup`:
58
+ sign-in, the service address stored in `~/.baseinstrunner/config.json`, the project's MCP servers
59
+ wrapped (a `.mcp.json` server only once Claude Code has approved it — on a fresh clone, run
60
+ `bir setup` again after the first session), the hooks wired into `.claude/settings.local.json`, the
61
+ scenario server added and replay switched on, the recorder started in the background. Nothing stays
62
+ open: the SessionStart hook starts the recorder whenever it is missing, and its audit lines go to
63
+ `~/.baseinstrunner/logs/<key>.log`. Re-running the line upgrades. With the package already
64
+ installed, the same thing is:
65
+
66
+ ```bash
67
+ bir setup --auth-url https://api.bi2202.com # without a token it signs in through the browser
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 2. Record a run worth reusing
73
+
74
+ Nothing special — work normally. Two server-side facts decide whether the run becomes reusable, and
75
+ both bite quietly:
76
+
77
+ **A run needs at least `RECORDING_MIN_ACTIONS` tool actions (default 4).** Below that it is not
78
+ embedded, does not enter the similarity pool, does not appear in the list, and is eventually pruned.
79
+ A two-tool run will never match anything, no matter how many times you repeat it.
80
+
81
+ **The prompt is what gets embedded.** `findSimilarRun` embeds `run.input` — the text of your
82
+ `UserPromptSubmit` — not the transcript, not the answer. So the prompt that *starts* the turn is the
83
+ one future prompts are compared against. Write the first prompt of a reusable task as if someone else
84
+ will have to phrase it again.
85
+
86
+ Watch it land:
87
+
88
+ ```
89
+ [bir] … run.start run=run_… sess=birsess_… tier=bound
90
+ [bir] … run.finish run=run_… steps=14 durationMs=48210 lossy=false recorded=true
91
+ ```
92
+
93
+ `recorded=false` means the service declined the run — usually because it matched something you
94
+ already have. That is §4 working.
95
+
96
+ List what you have:
97
+
98
+ ```bash
99
+ bir scenario list # or: GET /recordings/runs
100
+ ```
101
+
102
+ ```
103
+ run_5f3a… Fleet risk sweep
104
+ iterations=3 steps=14 scenario=scn_9c1b… (ready)
105
+ run_a812… Rewrite the billing README
106
+ iterations=1 steps=9 scenario=none — run `bir scenario calc <runId>`
107
+ run_c004… Check open PRs for stale reviews
108
+ iterations=1 steps=6 scenario=scn_44d0… (calculating)
109
+ ```
110
+
111
+ Ids are printed in full on their own line (shortened here), so they can be copied straight into
112
+ `calc` and `replay`.
113
+
114
+ ---
115
+
116
+ ## 3. Calculate the scenario
117
+
118
+ A recorded run is a transcript. A **scenario** is what BaseIn derives from it: an intent, a parameter
119
+ schema, and one JavaScript logic body per step that recomputes that step's tool arguments from new
120
+ parameters. Until a run has one, a match can only stop recording — there is nothing to run.
121
+
122
+ ```bash
123
+ bir scenario calc run_5f3a… # POST /recordings/runs/:runId/calculate → 202
124
+ bir scenario show run_5f3a… # GET /recordings/runs/:runId/scenario (poll)
125
+ ```
126
+
127
+ Raw, if you prefer — the service address lives in `~/.baseinstrunner/config.json` (`BIR_AUTH_URL`
128
+ is normally unset after `bir setup`) and the token in `~/.baseinstrunner/credentials.json`:
129
+
130
+ ```bash
131
+ BASEIN=$(node -p "require(require('os').homedir()+'/.baseinstrunner/config.json').authUrl")
132
+ TOKEN=$(node -p "require(require('os').homedir()+'/.baseinstrunner/credentials.json').accessToken")
133
+ curl -fsS -X POST "$BASEIN/recordings/runs/run_5f3a…/calculate" \
134
+ -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{}'
135
+ ```
136
+
137
+ ```powershell
138
+ $b = (Get-Content "$HOME\.baseinstrunner\config.json" | ConvertFrom-Json).authUrl
139
+ $t = (Get-Content "$HOME\.baseinstrunner\credentials.json" | ConvertFrom-Json).accessToken
140
+ Invoke-RestMethod -Method Post -Uri "$b/recordings/runs/run_5f3a…/calculate" `
141
+ -Headers @{ authorization = "Bearer $t" } -ContentType 'application/json' -Body '{}'
142
+ ```
143
+
144
+ Calculation is a background job. Poll `show` until `state` leaves `calculating`:
145
+
146
+ | `state` | Meaning | Next |
147
+ |---|---|---|
148
+ | `calculating` | job running | poll again in a few seconds |
149
+ | `ready` | usable | §4 |
150
+ | `failed` | see the `error` field | fix and recalculate with `--force` |
151
+
152
+ `409 scenario_exists` means this run already has one. Re-derive it in place — keeping the id and its
153
+ replay history — with `bir scenario calc run_… --force`. A `run_` id reaches only the run's
154
+ whole-run scenario. From the next version (not yet released), `bir scenario calc scn_… --force`
155
+ does the same by the scenario's own id, a sub-task scenario included; it is the only way to
156
+ recalculate a sub-task scenario from `bir`.
157
+
158
+ To change one step instead, without recalculating, see §9.2. A plan with steps edited by hand is
159
+ not rebuilt unless you add `--discard-edits`, which replaces those edits. They stay in
160
+ `bir scenario edits`, marked replaced, and can no longer be undone.
161
+
162
+ ### 3.1 Check the scenario before you trust it
163
+
164
+ **Dry replay is the cheapest thing you will do all day. Do it every time.** It derives parameters
165
+ from a fresh prompt and runs the stored logic against the *original run's recorded outputs* — no real
166
+ tools, no side effects, one Haiku call:
167
+
168
+ ```bash
169
+ bir scenario replay scn_9c1b… --prompt "which devices in the fleet look risky right now?" --dry
170
+ ```
171
+
172
+ ```
173
+ params { "fleetId": "eu-west-1", "riskThreshold": 0.8 }
174
+ step 0 mcp__chrome-devtools__navigate_page { "url": "https://fleet.internal/eu-west-1" }
175
+ step 1 mcp__chrome-devtools__take_snapshot {}
176
+ → emitted: devices
177
+ step 2 mcp__fleet-api__device_detail { "id": "dev_88f1" }
178
+ → emitted: highestRiskDeviceId, riskScore
179
+ response { "fleet": "eu-west-1", "atRisk": 3, "worst": "dev_88f1" }
180
+ cost $0.0012
181
+ ```
182
+
183
+ Read it for three things:
184
+
185
+ 1. **Did the parameters come out right** for a prompt phrased differently from the original?
186
+ 2. **Do the step inputs look computed**, or are they the original run's constants copied through? A
187
+ scenario whose inputs never vary with the parameters is a recording, not a scenario.
188
+ 3. **Is `emitted` non-empty** for steps that later steps depend on? `emitted` lists key names only;
189
+ `→ emitted: (nothing — toolOutputLogic derived no keys)` means every downstream input reading
190
+ that step will be wrong.
191
+
192
+ A `↑ not named by the prompt:` line under `params` lists targets the dry run filled from the
193
+ recording. A live turn finds those in an earlier step, or does not run — so a green trace with
194
+ that line is not yet evidence that a session would steer.
195
+
196
+ A dry replay is recorded as `outcome: 'dry'` and is excluded from savings by construction — it costs
197
+ you a Haiku call and nothing else.
198
+
199
+ ---
200
+
201
+ ## 4. Confirm the match fires
202
+
203
+ Now ask the same thing in different words, in a new session.
204
+
205
+ ```
206
+ you: can you check the eu-west fleet and tell me which boxes are in trouble?
207
+ ```
208
+
209
+ ```
210
+ [bir] … run.matched run=run_… matchedRun=run_5f3a… similarity=0.94
211
+ why="similar prompt — the service kept its own run; not recording this one"
212
+ ```
213
+
214
+ That line is the whole feature working up to the point of replay. If it does not appear, the prompt
215
+ did not clear the server's `SIMILARITY_THRESHOLD` (default 0.9) — see §8.
216
+
217
+ **With replay off (`bir replay off`), this is where it stops.** The server keeps its canonical run, bumps
218
+ its `iterations`, this turn is not recorded, and the model answers the ordinary way. Nothing is lost
219
+ and nothing is skipped.
220
+
221
+ ---
222
+
223
+ ## 5. Turn replay on
224
+
225
+ > Read §5.1 before running anything in this section.
226
+
227
+ ### 5.1 What you are agreeing to
228
+
229
+ A calculated scenario is, in effect, **a pre-approved list of tool calls with computed arguments.**
230
+ When replay arms:
231
+
232
+ - **Steered steps are auto-approved.** `PreToolUse` is answered `allow` for a pinned call, so a
233
+ pinned `Bash` or `Edit` runs without the prompt you would normally see. A call is pinned only
234
+ when it plausibly *is* the step — the same command, the same file, an argument in common (design
235
+ §7) — and never in plan mode.
236
+ - **Direct steps are not visible to the permission system at all.** They are dispatched straight to a
237
+ `bir-proxy`, which calls the upstream on a connection it already holds. Claude Code never sees them.
238
+ The `run_scenario` call that starts them is answered `allow` too, while the runner has announced a
239
+ direct plan; your own deny or ask rules for it still apply.
240
+
241
+ That is what makes replay fast, and it is the whole risk. Two things follow:
242
+
243
+ 1. Only enable it in a project whose scenarios you have dry-replayed and read (§3.1).
244
+ 2. Run `bir replay allow <servers>` (§5.3). An allowlist of the servers you are comfortable having
245
+ called unattended is cheap insurance, and it is the difference between "replay reads a dashboard"
246
+ and "replay can do anything any wrapped server can do".
247
+
248
+ Nothing else is approved on your behalf. Outside a matched scenario's own steps, a wrapped MCP call
249
+ keeps its permission prompt, as it would without the runner (`bir doctor` says so in a note);
250
+ `BIR_CORRELATION_DECISION=allow` where the recorder starts is the explicit opt-in to approve them
251
+ all.
252
+
253
+ ### 5.2 Enable it
254
+
255
+ `bir setup` already did this: it installed the scenario server in the local scope, which Claude
256
+ Code never asks about, and switched replay on for the project. By hand:
257
+
258
+ ```bash
259
+ bir install --replay # adds the `bir` MCP server and switches replay on here
260
+ bir up --restart # the recorder reads the change when it restarts
261
+ ```
262
+
263
+ A flag-less `bir install` keeps the invocation the project was set up with — `bir setup` installs
264
+ with `--global` from an installed package and `--local` from a checkout — so this downgrades
265
+ nothing. But a `bir install` **without** `--replay` leaves replay off in a project that never asked
266
+ for it (one that already has the scenario server, or where you ran `bir replay on`, keeps what it
267
+ had), and a later `bir install`, with or without `--replay`, never overrules `bir replay on|off`.
268
+ `bir install --replay` does exactly two things beyond a normal install, and `bir uninstall --replay`
269
+ undoes both, leaving everything else as it is:
270
+
271
+ - registers a first-party MCP server under the key **`bir`** in the local scope — `~/.claude.json`,
272
+ under Claude Code's key for this project, never the `.mcp.json` a team commits (`--mcp-json` puts
273
+ it there instead, pre-approved in `.claude/settings.local.json`, the older shape). It exposes
274
+ `mcp__bir__run_scenario` — the channel a fully-wrapped scenario's results come back through — and
275
+ by default nothing else. Three tools that only read a scenario (`scenario_show`,
276
+ `scenario_edits`, `investigate`) are listed after `bir scenario editing on`, or with
277
+ `BIR_SCENARIO_TOOLS=read` in the server's environment, and three for changing one
278
+ (`scenario_check` tries a change, `scenario_edit` saves it, `scenario_undo` takes it back) only
279
+ after `bir scenario editing on`. §9.2 lists them with their arguments; the editing it describes
280
+ is built but not yet released. From the next version, a call to any of them except
281
+ `run_scenario` is never recorded. In a subdirectory or linked worktree of a git repository the
282
+ server is not added — in the local scope it would apply to the whole repository — and replay
283
+ steers without it;
284
+ - switches replay on for the project (`bir uninstall --replay` switches it off, and restarts a
285
+ recorder no session is using so that applies at once).
286
+
287
+ The hook timeouts, including `UserPromptSubmit`'s 10 s, are not among them: every install writes
288
+ them, replay or not.
289
+
290
+ `run_scenario` is visible to the model in every session in this project. That is a real cost — one
291
+ more tool in the list, a little context per turn — and it is why replay is a separate flag rather
292
+ than part of `bir install`. The other `bir` tools are not listed until you ask for them (§9.2).
293
+
294
+ Confirm:
295
+
296
+ ```bash
297
+ bir doctor
298
+ ```
299
+
300
+ ```
301
+ Replay : ON servers=chrome-devtools,fleet-api minSimilarity=0.92 source=sidecar
302
+ derive=the service (no key needed here)
303
+ Wrapped in config : chrome-devtools, fleet-api
304
+ Registered proxies: chrome-devtools, fleet-api
305
+ Control server : http://127.0.0.1:53411
306
+ BaseIn (shell) : https://api.bi2202.com (answers /health)
307
+ Recording tier : bound (Tier 1)
308
+ Recording : yes
309
+
310
+ · recording to https://api.bi2202.com as you@example.com
311
+ · the recorder runs in the background (pid 41820); its audit log is ~/.baseinstrunner/logs/<key>.log
312
+ · the BaseIn service last answered the recorder 12 s ago
313
+ · wrapped MCP calls are not approved by the runner: Claude Code asks as it would without it. For headless runs (`claude -p`) allow them in permissions.allow or --allowedTools, or set BIR_CORRELATION_DECISION=allow where the recorder starts
314
+ · recurring sub-tasks are observed only — nothing is replaced (BIR_SEGMENT_ARM unset)
315
+
316
+ No problems found.
317
+ ```
318
+
319
+ `BaseIn (shell)` is the address this terminal's `bir` commands would use — `BIR_AUTH_URL` if it
320
+ is set, else the one `bir setup` stored — and `recording to` is the one the running recorder
321
+ uses, with the account it is signed in as. The two normally agree.
322
+
323
+ ### 5.3 Environment
324
+
325
+ The first three switches are stored per project, and that is where to set them:
326
+
327
+ ```bash
328
+ bir replay allow chrome-devtools,fleet-api # the servers bir may call by itself; `allow all` clears it
329
+ bir replay off # and back with `bir replay on`
330
+ bir replay status # what is stored for this project
331
+ bir up --restart # a running recorder reads them when it restarts
332
+ ```
333
+
334
+ They are kept in `~/.baseinstrunner/installed.json`. A recorder started by the SessionStart hook has
335
+ Claude Code's environment, not your terminal's, and reads them from there. The environment variables
336
+ below override them wherever the recorder sees them; `bir doctor` says which is in effect
337
+ (`source=sidecar` or `source=env`). The proxies need no replay configuration of their own — every
338
+ bound proxy polls the recorder for work, and slows to one poll every 30 s while replay is off there,
339
+ so there is exactly one switch and it cannot get out of step with itself.
340
+
341
+ | Variable | Default | What it does |
342
+ |---|---|---|
343
+ | `BIR_REPLAY` | *(the stored switch)* | `0` disables replay and any other value turns it on, whatever `bir replay on` or `off` or the install stored. Unset, the stored switch decides: on after `bir setup` or `bir install --replay`, off after a plain `bir install`. Nothing below matters while replay is off |
344
+ | `BIR_REPLAY_ALLOW_SERVERS` | *(the stored list, else all wrapped)* | Comma-separated server keys bir may call by itself: **direct** execution and divergence recovery. Overrides `bir replay allow` |
345
+ | `BIR_MIN_STEER_SIMILARITY` | `0.92` | Below this a match is detected but not replayed (§8) |
346
+ | `BIR_DERIVE_API_KEY` | *(unset)* | **Not required.** Derivation — reading what this request acts on — is done by the service on its key for a signed-in runner. Set this to keep the reading on this machine instead: the prompt then never leaves it, and it is one round trip faster. Signed out *and* unset, only a scenario with nothing to work out replays. A recorder running in the background (started by the SessionStart hook, `bir up` or `bir setup`) ignores `ANTHROPIC_API_KEY` (usually Claude Code's own, and billed) unless `BIR_USE_ANTHROPIC_API_KEY=1`; a `bir-hooks` you run in a terminal uses it |
347
+ | `BIR_DERIVE_MODEL` | `claude-haiku-4-5-20251001` | The derivation model, when this machine does the reading |
348
+ | `BIR_MATCH_BUDGET_MS` | `3000` | How long the prompt hook waits for a match before giving up. After a match slower than `BIR_MATCH_SLOW_MS` (`2000`), prompts stop waiting for one for 5 minutes, doubling up to 30 while the service stays slow |
349
+ | `BIR_DERIVE_BUDGET_MS` | `8000` | How long `run_scenario` waits for parameters |
350
+ | `BIR_HOOK_DERIVE_BUDGET_MS` | `4000` | How long a hook waits for them: the prompt, for a scenario with a target, and the first `PreToolUse` (never longer than `BIR_DERIVE_BUDGET_MS`) |
351
+ | `BIR_PRETOOL_REPLAY_BUDGET_MS` | `9000` | All replay work inside one `PreToolUse`, a divergence's steps included. At most 10 000 |
352
+ | `BIR_REPLAY_PLAN_BUDGET_MS` | `100000` | Whole-plan ceiling for direct execution; `BIR_REPLAY_BUDGET_MS`, where set, overrides it. Past about 110 000 a long run may be moved to the background by Claude Code. Derivation plus plan is held under 270 s |
353
+ | `BIR_STEP_TIMEOUT_MS` | `60000` | Per-step ceiling for one direct `tools/call`, clipped to what the plan has left |
354
+ | `BIR_LOGIC_TIMEOUT_MS` | `1000` | How long one scenario logic body may run before it is stopped. At most 10 000 |
355
+ | `BIR_REPLAY_RECORDED_OUTPUTS` | *(unset)* | `1` lets a step that could not be sent (a built-in, no proxy, a server off the allowlist) stand on its recorded output instead of stopping the plan. Never for a timeout or an upstream error |
356
+ | `BIR_INTENT_ARM` | *(unset)* | `1` lets a match found mid-turn arm a plan in the calling tool's place, which means waiting for it (`BIR_INTENT_ARM_BUDGET_MS`, 500); `0` never waits; unset follows `BIR_SEGMENT_ARM` |
357
+ | `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
358
+
359
+ The `bir` server reads its own, from the environment Claude Code starts it with:
360
+ `BIR_SCENARIO_RUN_TIMEOUT_MS` (`290000`, how long `run_scenario` waits for its run),
361
+ `BIR_SCENARIO_TOOLS=read` (list the reading tools, §9.2) and `BIR_SCENARIO_DEBUG=1` (raw reasons,
362
+ §9). [production.md](../production.md) lists every variable.
363
+
364
+ **No API key is a supported configuration, not a broken one.** Signed in, the service reads the
365
+ prompt on its own key and the replay is the same replay. Signed out *and* without a key, nothing
366
+ can read the prompt: a *setting* takes the value it had in the recorded run, so a scenario whose
367
+ parameters are all settings — a dashboard sweep, a fixed report — is still a complete, free replay;
368
+ but a scenario with a *target* (the thing the task acts on: "check *eu-west*") is declined rather
369
+ than run on last week's value, and the agent does the task normally. `bir doctor` names the state
370
+ you are in: `derive=this machine (a derivation key: BIR_DERIVE_API_KEY)`,
371
+ `derive=the service (no key needed here)`, or
372
+ `derive=recorded sample values — scenarios with a target will NOT run`. The recorder's own
373
+ `replay.enabled` line at start names the variable.
374
+
375
+ ---
376
+
377
+ ## 6. Run it, and read what happened
378
+
379
+ Ask the matching question again, with replay on.
380
+
381
+ ### 6.1 A fully-wrapped scenario — direct
382
+
383
+ ```
384
+ [bir] … run.matched run=run_… matchedRun=run_5f3a… similarity=0.94
385
+ [bir] … plan.armed scenario=scn_9c1b… mode=direct steps=3
386
+ tools="mcp__chrome-devtools__navigate_page,mcp__chrome-devtools__take_snapshot,mcp__fleet-api__device_detail"
387
+ [bir] … replay.derived scenario=scn_9c1b… params=2 costUsd=0.0011 source=the service
388
+ [bir] … replay.step n=0 tool=navigate_page server=chrome-devtools ms=612 ok=true
389
+ [bir] … replay.step n=1 tool=take_snapshot server=chrome-devtools ms=1104 ok=true emitted=devices
390
+ [bir] … replay.step n=2 tool=device_detail server=fleet-api ms=210 ok=true emitted=highestRiskDeviceId,riskScore
391
+ [bir] … replay.done scenario=scn_9c1b… mode=direct steps=3/3 outcome=steered_full ms=2766
392
+ [bir] … execution.reported scenario=scn_9c1b… outcome=steered_full derive=0.0011 session=0.0083 fallback=0.0000 savedUsd=0.114
393
+ ```
394
+
395
+ In the session you will see one tool call, `mcp__bir__run_scenario`, with no permission prompt,
396
+ returning the composed results. The model reads them and answers. It never emitted the three
397
+ underlying calls.
398
+
399
+ A direct plan runs only through that call. If the model's first call is anything else, the plan is
400
+ retired without running a step (`replay.vetoed`), that call runs as written, and the model is told
401
+ the scenario is not being run. A plan whose `run_scenario` is refused, or never reaches the
402
+ recorder, is retired the same way. A direct plan is not even announced unless a running `bir`
403
+ server has told the recorder it serves `run_scenario` (`replay.decision … code=no_delivery_tool`
404
+ otherwise).
405
+
406
+ ### 6.2 A mixed scenario — steer
407
+
408
+ The model emits each call itself; `bir` overrides the arguments. The per-step `replay.pin` and
409
+ `replay.thread` lines are detail lines, printed only under `BIR_VERBOSE=1`; without it a steer run
410
+ shows `plan.armed`, `replay.done`, and any `replay.thread_fallback` between them.
411
+
412
+ ```
413
+ [bir] … plan.armed scenario=scn_44d0… mode=steer steps=4
414
+ [bir] … replay.pin n=0 tool=mcp__github__list_pull_requests
415
+ [bir] … replay.thread n=0 via=proxy emitted=prNumbers done=false
416
+ [bir] … replay.pin n=1 tool=Read
417
+ [bir] … replay.thread n=1 via=hook emitted=reviewers done=false
418
+ [bir] … replay.done scenario=scn_44d0… mode=steer steps=4/4 outcome=steered_full
419
+ ```
420
+
421
+ `via=proxy` versus `via=hook` matters: a wrapped MCP step's output is threaded from the **proxy's**
422
+ report, because that is the shape the scenario's logic was written against (design §7.2). A
423
+ `replay.thread_fallback run=… tool=… why="no proxy report — threading the hook's view; shapes may differ"`
424
+ line — printed whether or not you are verbose — means the proxy did not report within its grace
425
+ window and the hook's differently-shaped view was used instead. Expect derived values to be empty,
426
+ and treat repeats as a bug.
427
+
428
+ A pin needs the model's call to plausibly *be* the step: the same command, the same file, an
429
+ argument in common (design §7). The directive shows each step's arguments where they are known, so
430
+ the model can pass them as shown. A call to the expected tool that is not the step — `ls` where the
431
+ step is `git push` — is not pinned: it runs as written, the plan stays armed, and
432
+ `replay.pin_declined` says why. If the model then moves on to another tool, it did the step its own
433
+ way: `replay.pin_abandoned` retires the plan without running anything, and the model is told the
434
+ arguments are its own from then on. The plan runs one call at a time: a call made while a pinned
435
+ step is still running hands the rest over (`replay.concurrent_call`).
436
+
437
+ ### 6.3 The model goes off-script
438
+
439
+ ```
440
+ [bir] … replay.diverge scenario=scn_44d0… expected=mcp__github__get_pull_request called=Bash step=2/4
441
+ [bir] … replay.compose scenario=scn_44d0… remaining=2 executed=2 recorded=0 skipped=0 bytes=8412 costUsd=0.00
442
+ [bir] … replay.done scenario=scn_44d0… mode=steer steps=2/4 outcome=diverged
443
+ ```
444
+
445
+ `costUsd=0.00` on the compose line is the point of the whole design: the remaining steps ran through
446
+ proxies that were already connected, so recovering from a divergence cost no tokens at all.
447
+
448
+ Only a wrapped server's step can run that way, and only one on the allowlist. A step that cannot —
449
+ a built-in such as `Read`, a server off `BIR_REPLAY_ALLOW_SERVERS`, a proxy that does not answer —
450
+ ends the bundle there: `replay.compose` shows `skipped=1 stopped=<n>:<reason>`, the bundle opens
451
+ with a note that names the step and says what may be assumed about it, and the model carries on
452
+ from that step. A recorded output stands in for it only with `BIR_REPLAY_RECORDED_OUTPUTS=1`. The
453
+ recovery runs inside the diverging call's `PreToolUse`, so it starts no step after
454
+ `BIR_PRETOOL_REPLAY_BUDGET_MS` (9 s). Direct plans never diverge (§6.1).
455
+
456
+ ### 6.4 The audit vocabulary
457
+
458
+ Everything replay decides is one line. `grep` for these:
459
+
460
+ | Line | Means |
461
+ |---|---|
462
+ | `run.matched` | The server recognised the prompt |
463
+ | `replay.decision … verdict=no-steer` | Matched, and declined. `why=` says which gate, `code=` names it: `match_timeout`, `service_unhealthy`, `arm_timeout`, `arm_unannounced`, `no_delivery_tool`, `missing_target`, … |
464
+ | `replay.match_gate` | Prompts stopped waiting on a slow match (`state=holding`), or started again (`state=open`) |
465
+ | `plan.armed` | A plan exists. Carries mode, step count and the ordered tool names — **before** anything runs |
466
+ | `replay.derived` / `replay.derive_failed` / `replay.derive_timeout` | Parameters resolved, fell back to sample values, or ran out of time |
467
+ | `replay.unsafe_param` | A derived value failed the safety check for a command-like step; the plan is declined before it arms, or hands over |
468
+ | `replay.pin` / `replay.thread` | One steered step's input pinned / output threaded — detail lines, only under `BIR_VERBOSE=1` |
469
+ | `replay.pin_declined` / `replay.pin_abandoned` | A call to the expected tool was not the step and ran as written / the model moved on, and the plan was retired |
470
+ | `replay.concurrent_call` | A call ran beside a pinned step; the plan handed over |
471
+ | `replay.thread_fallback` | A wrapped step's output was threaded from the hook's view because the proxy did not report in time |
472
+ | `replay.step` | One direct step executed |
473
+ | `replay.diverge` / `replay.compose` | Off-script, and the recovery |
474
+ | `replay.handover` | The plan stopped and handed the rest of the task to the model; `kind=` and `step=` say where |
475
+ | `replay.vetoed` | A direct plan retired without running anything: `run_scenario` was not the first call, or never arrived |
476
+ | `replay.run_ambiguous` | Two sessions had `run_scenario` pending and the request named neither; nothing ran |
477
+ | `replay.cancelled` / `replay.note_kept` | A direct run's caller went away (Esc); no further step started, and the note is kept for the model's next call |
478
+ | `replay.pin_overran` / `replay.diverge_overran` | `PreToolUse` reached its deadline while replay was still working |
479
+ | `replay.budget_clamped` | A budget set past its ceiling was brought back under it, at start |
480
+ | `replay.delivery_tool` | A `bir` server said it can, or cannot, deliver a direct plan |
481
+ | `control.hook_deadline` | A hook route answered `{}` at its deadline |
482
+ | `replay.done` | Final outcome, steps completed, elapsed |
483
+ | `execution.reported` | The ticket was redeemed and the saving booked |
484
+
485
+ `scenario.answered` is the `bir` server's own line, on its stderr (Claude Code's log for that MCP
486
+ server): what `run_scenario` answered, and the real reason when the model was told only that the
487
+ results are not available.
488
+
489
+ A recorder started in the background — by the SessionStart hook, `bir up` or `bir setup` — keeps
490
+ them in `~/.baseinstrunner/logs/<key>.log`; `bir status` names the file. One started in a terminal
491
+ writes them to that terminal; keep them yourself: `bir-hooks 2>&1 | tee -a ~/.baseinstrunner/audit.log`.
492
+
493
+ ---
494
+
495
+ ## 7. What each outcome means for your numbers
496
+
497
+ Reported once per matched turn, to `POST /scenarios/:id/executions`, redeeming the match's
498
+ `executionTicket`.
499
+
500
+ | Outcome | When | Where it lands |
501
+ |---|---|---|
502
+ | `steered_full` | Every planned step ran under the scenario's control | savings ledger |
503
+ | `diverged` | Off-script, and some steps ran under the plan; the composed bundle answered | savings ledger |
504
+ | `fell_back` | Some steps did their work, then the plan handed the rest of the task to the model | savings ledger, measured against the whole turn (it can be negative) |
505
+ | `not_steered` | Matched, declined at a gate — or a direct plan retired before it ran anything — and the agent ran the task normally | **baseline sample** |
506
+ | `failed` | Armed, then gave up before any step did its work — the agent ran the task normally | **baseline sample** |
507
+
508
+ A recorded output that stood in for a step (`BIR_REPLAY_RECORDED_OUTPUTS=1`) is never counted as
509
+ work done. A direct run you cancel (Esc) is booked `fell_back` or `failed` with `measured: false`:
510
+ a turn cut short says nothing about what the task costs.
511
+
512
+ `not_steered` is not an error, and it is not wasted. A saving is `baseline − actual`, and the baseline
513
+ is *what this task costs when the agent does it*. Every declined match is a fresh measurement of
514
+ exactly that. A deployment that never declines has no baseline to measure against — which is a good
515
+ reason to leave `BIR_MIN_STEER_SIMILARITY` where it is rather than driving it to zero.
516
+
517
+ An **unredeemed** ticket books nothing at all. Silence loses a saving; it never invents one.
518
+
519
+ ---
520
+
521
+ ## 8. Tuning
522
+
523
+ Only one knob matters in practice.
524
+
525
+ **`BIR_MIN_STEER_SIMILARITY` (default 0.92).** The server detects matches at 0.9 and uses that to
526
+ decide *"don't record this again"* — cheap: the worst case is a lost recording. Steering makes a much
527
+ stronger claim, *"don't think about this again"*, and a 0.90-similar prompt usually covers only part
528
+ of the live task. Steering it produces a confident, thin, wrong answer.
529
+
530
+ Tune it from evidence, not taste. Run for a week and read the declines:
531
+
532
+ ```bash
533
+ grep 'replay.decision' audit.log | grep 'why="similarity below threshold"'
534
+ ```
535
+
536
+ - Prompts you *would* have wanted replayed, sitting at 0.90–0.92 → lower it, in steps of 0.01.
537
+ - Replays that answered the wrong question → raise it, and re-read the scenario's `intent`.
538
+
539
+ If a specific pair of prompts *should* match and does not, the fix is usually upstream of the
540
+ threshold: the two prompts genuinely mean different things to the embedding model. Re-record with a
541
+ first prompt that states the task plainly.
542
+
543
+ ---
544
+
545
+ ## 9. Troubleshooting
546
+
547
+ | Symptom | Cause | Fix |
548
+ |---|---|---|
549
+ | No `run.matched`, ever | Prompt below the server's `SIMILARITY_THRESHOLD` | Rephrase closer, or lower it server-side. Confirm the original run is in the list — a sub-`RECORDING_MIN_ACTIONS` run is never embedded |
550
+ | `run.matched` but no `plan.armed` | A gate declined | Read the `replay.decision` line's `why=`. Ranked by frequency: replay off — `bir replay off`, `BIR_REPLAY=0`, or never switched on here (a `bir install` without `--replay`; `bir replay status` says which) · scenario not `ready` · similarity below threshold · no step is executable |
551
+ | `replay.decision … code=match_timeout` or `code=service_unhealthy` on many prompts | The service answered the match slower than `BIR_MATCH_BUDGET_MS` (3 s), or is failing and in its cool-down | Nothing breaks: those turns run normally, and after a slow match prompts stop waiting on it for a while by themselves (`replay.match_gate`). `bir doctor` shows the service's state. Raise `BIR_MATCH_BUDGET_MS` only if the service is reliably just over it — every prompt pays it |
552
+ | `replay.decision … code=no_delivery_tool` | A direct plan, and no running `bir` server has told the recorder it serves `run_scenario`: the server is missing, disabled or failed in `/mcp`, was not added (a subdirectory or worktree of a repository), or the session was started with `BIR_OFF=1` | `/mcp` shows whether `bir` is connected. `bir install --replay` adds it, in the project's own directory; then start a new session |
553
+ | `replay.vetoed … the model's first call was …, not run_scenario` | A direct plan, and the model reached for another tool first — usually because it has no `run_scenario` | As for `no_delivery_tool`. Nothing ran; the model did the task |
554
+ | `replay.pin_declined` on most steered turns | The model's calls to the expected tool are not the step — another command, another file — so nothing is pinned; if it then moves on, `replay.pin_abandoned` retires the plan | Harmless for those calls, which ran as the model wrote them. Persistent declines on one step usually mean its arguments cannot be shown in the directive (they depend on an earlier step's output, or the command runs to several lines), and the model writes its own |
555
+ | `why="no step is executable"` | None of the scenario's tools is a wrapped MCP server and none is a built-in reachable in this session | Wrap the servers the scenario uses (`bir install --server …`) and recalculate |
556
+ | `plan.armed mode=steer` where you expected `direct` | At least one step is a built-in, an unwrapped MCP server, or `claude-in-chrome` | `bir status` shows what is wrapped. `claude-in-chrome` is `scope: "dynamic"` and can never be wrapped |
557
+ | Model ignores the directive and diverges every turn | Steering is advisory — the model chooses; `bir` only pins arguments | Expected occasionally. Persistent divergence usually means the scenario's tools do not fit the live task; check `intent` |
558
+ | A `replay.thread` line with no `emitted=` (under `BIR_VERBOSE=1`) | `toolOutputLogic` derived nothing — usually the output shape changed since recording | Dry-replay (§3.1). If the dry run emits and the live run does not, the tool's output shape has drifted; recalculate |
559
+ | `replay.thread_fallback` on a wrapped step | The proxy did not report within the grace window | Check that proxy is alive (`bir doctor`). Repeats mean a slow or dying upstream |
560
+ | `run_scenario` answers "Nothing to run for this request; continue normally." | The model called it on a turn with no plan, the runner is off, or no recorder is running for this directory | Harmless, and not an error. It happens when the model remembers the tool from an earlier turn |
561
+ | `run_scenario` answers "The prepared results are not available for this request. Do the task normally with your tools." | The plan could not be delivered — a recorder that does not answer, a timeout, an HTTP error — and the model does the task itself | The `scenario.answered` line on the `bir` server's stderr has the real reason. `BIR_SCENARIO_DEBUG=1` in that server's environment hands the raw reason to the model as an error, for debugging |
562
+ | A hand-over note says a step "was sent but did not answer … may or may not have completed" | The step timed out, or its upstream went away mid-call; nothing after it ran | Check whether its work happened before running it again — the model is told the same. Repeats: look at that upstream, or raise `BIR_STEP_TIMEOUT_MS` |
563
+ | A hand-over note says "a step never runs on another session's upstream" | Two Claude Code sessions are open in the project, and this one has not yet made a call of its own to that server, so bir cannot tell its proxy from the other session's | Expected: the model calls the tool itself. Once the session's own call to the server has gone through, later replays use its proxy |
564
+ | `recorder.send_failed … error="HTTP 409 … invalid_ticket"` | The execution report's ticket was already redeemed, or belongs to another scenario | Harmless if the body says `duplicate: true`. Otherwise a rollover raced `SessionEnd`; the server books once |
565
+ | `savedUsd` looks impossible | Pricing drift between the runner's table and the server's | Compare `PRICING_VERSION` on both sides. Design §11.3 documents a known live drift between two existing copies |
566
+ | The hand-over note says a step "did not run: its input needs a judgement" | The service marked the step non-deterministic when it calculated the plan: the generated logic could not compute its input from the prompt or from earlier outputs, so the recorded value was a copy (the service's plan-services design, D8 kind 3 — it lives in the BaseIn repository, not this one). `nondeterministicWhy: by_hand` means someone marked it with `bir scenario edit --freeze` | If the code only has a slip, fix the step yourself: `bir scenario show scn_… --step <n>`, write the input logic to a file, then `bir scenario edit scn_… --step <n> --input-logic <file>` — the service checks it against the recording before it saves it (§9.2, not yet released). Of the edits, only `--unfreeze` lifts a mark made by hand (`by_hand`); a code edit, even a forced one, leaves it in place. A real judgement — a choice with no fixed rule — cannot be written as code: the agent makes that choice and finishes the task; a model step for it is planned (W3.1). The journal's `plan.armed` line lists the step under `stops=` |
567
+ | A parked step ran again after an hour, or a plan was recalculated by itself | The service's grace period (D8): a parked step is served with `stop: null` once an hour for a day and a success clears its count; if it keeps failing, or its tool is gone, the next repeat of the prompt is recorded in full and the plan is calculated again from it | Nothing: that is the repair. `bir investigate` says which it is — a step still being retried, a parked step whose repair is due, or a plan being calculated again. To repair now by hand: `bir scenario calc <runId> --force` for a whole-run plan; from the next version, `bir scenario calc <scnId> --force` works for any plan, a sub-task one included. A plan with steps edited by hand is not repaired by itself: the parked step stays parked, and `bir investigate` says the repair was skipped. Then fix the step (§9.2); or take the edits back with `bir scenario undo`, once per edit, and it is repaired as usual; or recalculate now and drop the edits: `bir scenario calc <scnId> --force --discard-edits` |
568
+ | `sessionCostUsd` grows every prompt in a session | The per-run usage watermark is missing or not taken | Design §11.4. Reports without a mark must carry `measured: false` |
569
+ | Everything works, nothing is saved | A replayed turn is deliberately not recorded (design §12) | Correct. The matched run stays canonical and its `iterations` is bumped |
570
+ | `bir doctor`: the SessionStart hook points at a Node that is gone | The hook names Node by absolute path. That Node was uninstalled, moved, or was a version manager's per-shell copy; the hook then fails on every session, no recorder is started, and runs record Tier 2 | `bir setup` here again — it rewrites the hook to the Node that runs it |
571
+ | `control.port_busy` in the log; the recorder is not running | Another program holds this project's port, or the system excludes it (on Windows, a range Hyper-V or WSL reserved). A background recorder with a recorded port does not fall back to another, because every hook but SessionStart names this one; only a `bir-hooks` you run by hand in a terminal still does | `bir setup --port <another>` in this directory — the fix the log line itself names. It rewrites the hook URLs and the scenario server's address together and restarts the recorder |
572
+ | The recorder was started by the hook, and `bir doctor` says `servers=(all wrapped)` although you set `BIR_REPLAY_ALLOW_SERVERS` | The hook starts the recorder with Claude Code's environment, not your terminal's; a variable set in one terminal is not seen | `bir replay allow <a,b>` — stored for the project, read by every recorder — then `bir up --restart` |
573
+ | A session misbehaves, and you want to rule the runner out now | — | `bir off --here` (or `bir off` for every project): hooks answer with nothing within about a second, open sessions included, the `bir` server lists no tools, and nothing is recorded or replayed; wrapped servers run as plain pipes from their next start. Nothing is uninstalled. `bir on --here` (or `bir on`) undoes it (§11) |
574
+
575
+ ---
576
+
577
+ ### 9.1 `bir investigate` — the table above, applied for you
578
+
579
+ Every audit line that explains a turn is also kept in a **journal**, one JSON
580
+ object per line, under `~/.baseinstrunner/control/journal/` (one file per
581
+ directory, rotated at 4 MB). The recorder names it at start — the `journal=`
582
+ field of its `control.listening` line, in the audit log for a background
583
+ recorder. Ask the journal and the service together:
584
+
585
+ ```
586
+ bir investigate # the newest turn in this directory
587
+ bir investigate <run_|scn_|sexec_ id> # that run, scenario or execution
588
+ bir investigate list [--limit n] # recent turns and their verdicts
589
+ bir investigate executions [--limit n] [--user <email|id>] # the ledger
590
+ ```
591
+
592
+ The output is the turn as the journal tells it (matched what, which gate
593
+ declined and why, what mode armed with which coverage, what the execution
594
+ reported), then **runner findings**, then the service's view (the recording,
595
+ the scenario, the baseline and its sample count, the executions with their
596
+ saved $ and %) and **service findings**. Every finding is *problem → cause →
597
+ fix*. The three questions it answers:
598
+
599
+ | Question | Where the answer comes from |
600
+ |---|---|
601
+ | Why did this turn not run its calculated scenario? | `replay.decision code=…` in the journal, mapped to the cause and the fix; and the service: is the run a recording, is it embedded, how many hits, is the scenario ready / failed / switched off, which step is parked |
602
+ | Why did it save so little, or cost more? | the execution's cost breakdown (derive, session, fallback) against the baseline; steer mode with built-in steps is the usual answer, a single-sample baseline the second |
603
+ | What happened, step by step? | the turn's lines in order, the per-step verdicts on the execution, and `replay.*_failed`, `replay.diverge`, `replay.handover` as incidents |
604
+
605
+ You see your own data. An admin of the service sees everyone's and may pass
606
+ `--user` to `executions`. `--json` on any form prints the raw merge.
607
+
608
+ ### 9.2 Fix a step — `bir scenario check`, `edit`, `edits`, `undo`
609
+
610
+ > Built 2026-09-25, **not yet released**. It needs the next `@basein/runner` version *and* a
611
+ > service deploy: the `/scenarios/:id/steps/…` routes and the sandbox the service runs step code
612
+ > in. Version 0.2.11 on npm has an early form of these commands. It is not what this section
613
+ > describes. The design is `editSteps.md` in the BaseIn repository.
614
+ >
615
+ > The hiring-year outputs below were printed by the real `bir`, against the real service, on the
616
+ > real recording. Only the ids are shortened, with `…`.
617
+
618
+ `bir investigate` says *what* is wrong. This section is how you fix it. A calculated step is code:
619
+ its **input logic** says what to send to the tool, and its **output logic** says what to keep from
620
+ the answer. When that code is wrong, you do not have to recalculate and hope any more. You change
621
+ the step, and the service checks your change against the recording before it saves it.
622
+
623
+ **A change is saved only if it reproduces the recording and is not a copy — or on purpose, with
624
+ `--force --note "why"`.**
625
+
626
+ ```
627
+ bir scenario show <runId|scnId> [--step <n>]
628
+ bir scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
629
+ bir scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
630
+ [--freeze | --unfreeze] [--note "<why>"] [--force --note "<why>"] [--revision <n>]
631
+ bir scenario edits <runId|scnId>
632
+ bir scenario undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "<why>"]
633
+ bir scenario calc <runId|scnId> [--force [--discard-edits]]
634
+ bir scenario editing on|off|status
635
+ ```
636
+
637
+ A `run_` id finds the run's whole-run scenario. A `scn_` id (what `bir investigate` prints) is used
638
+ as it is. A sub-task scenario has only a `scn_` id. When a run has only sub-task scenarios, `bir`
639
+ lists their `scn_` ids instead of guessing. `--step` is the `stepIndex` that `show` prints; it
640
+ starts at 0. `--revision <n>` saves only if the plan is still at revision `n`: the `chainRevision`
641
+ at the top of `show --step`. `--json` on `check`, `edit`, `edits` or `undo` prints the service's
642
+ answer unchanged. Exit codes: `0` checked OK, saved or undone · `1` refused, not found, or a
643
+ service error · `2` a wrong command line.
644
+
645
+ #### What the code is
646
+
647
+ Each logic is the *body* of a JavaScript function. It must end in `return { … }`.
648
+
649
+ | Logic | Reads | Returns |
650
+ |---|---|---|
651
+ | input logic (`toolInputLogic`) | `parameters` — this request's values · `respParams` — what earlier steps kept · `intent` | the tool's input, as an object |
652
+ | output logic (`toolOutputLogic`) | `toolOutput` — the tool's answer, as text · and the same three | the values to keep for later steps, as an object |
653
+
654
+ The service runs your code in a sandbox (design §13.1). In the sandbox only JavaScript itself
655
+ exists: `JSON`, `Math`, `Date`, `RegExp` and the like. There is no `require`, `process`, `fetch`,
656
+ `Buffer` or timer. A few built-ins are taken away too: `ArrayBuffer`, the typed arrays such as
657
+ `Uint8Array`, `Atomics`, `WebAssembly`, `FinalizationRegistry` and `Symbol.for`. Code that uses
658
+ one of them with the recording's values throws in the check.
659
+
660
+ **But the check only judges what your code returns for the recording's values.** A `fetch` behind
661
+ a `typeof` guard (`if (typeof fetch === "function") …`), or on a branch the recording's values
662
+ never take, is never reached in the check. A `fetch` inside a `try` fails in the check, but the
663
+ `catch` hides the error. Either way the code passes. On your machine the runner runs the same
664
+ code in its own process, with the global scope and only a time limit, where `fetch` and `process`
665
+ do exist. So read every change before it is saved — yours and Claude's.
666
+
667
+ **Only a step's code and its mark can be changed.** Not the parameters, not their sample values, not
668
+ the logic that builds the final answer. Only tool steps: a sub-task call row belongs to the service.
669
+
670
+ #### The fix, step by step
671
+
672
+ On 2026-09-25 two SQL steps of the hiring-year scenario, 3 and 4, were marked *needs a judgement*.
673
+ Step 4 is shown here; step 3 was fixed the same way. The calculation's code for step 4 wrote
674
+ `schema + '.' + summary_table`, but the table name already carried its schema, so the code did not
675
+ give the recorded call. The calculation then put a copy of the recorded call in its place, and
676
+ marked the step.
677
+
678
+ **1. Read the step.**
679
+
680
+ ```bash
681
+ bir scenario show scn_4a17a126-… --step 4
682
+ ```
683
+
684
+ You should see this (shortened: each `…` line stands for lines left out):
685
+
686
+ ```
687
+ {
688
+ "scenarioId": "scn_4a17a126-…",
689
+ "chainRevision": 1,
690
+ "stepIndex": 4,
691
+ "kind": "tool",
692
+ "toolName": "mcp__postgres__execute_sql",
693
+ "toolInputLogic": "return {\"sql\":\"SELECT * FROM employees.hiring_year_summary ORDER BY hire_year;\"};",
694
+ …
695
+ "nondeterministic": true,
696
+ "nondeterministicWhy": "input_mismatch",
697
+ "editedAt": null,
698
+ "editForced": false,
699
+ "stop": {
700
+ "kind": "nondeterministic",
701
+ "why": "input_mismatch"
702
+ },
703
+ "sampleInput": "{\"sql\":\"SELECT * FROM employees.hiring_year_summary ORDER BY hire_year;\"}",
704
+ …
705
+ }
706
+ ```
707
+
708
+ `scenarioId` and `chainRevision` come first. They belong to the whole scenario, and
709
+ `chainRevision` is the number `edit --revision` takes. The rest is the step. Its input logic is
710
+ that copy: compare it with `sampleInput`. A copy does the old job, on the old table, whatever the
711
+ new request says. That is why the step is marked (`nondeterministic: true`), and why runs hand over
712
+ to the agent in front of it.
713
+
714
+ **2. Write the new code to a file.**
715
+
716
+ ```bash
717
+ cat step4.js
718
+ ```
719
+
720
+ ```
721
+ return { sql: "SELECT * FROM " + parameters.summary_table + " ORDER BY hire_year;" };
722
+ ```
723
+
724
+ Code always goes in from a file, or from stdin with `-`. Never on the command line: the shell's
725
+ quoting breaks JavaScript.
726
+
727
+ **3. Check it.** Nothing is saved.
728
+
729
+ ```bash
730
+ bir scenario check scn_4a17a126-… --step 4 --input-logic step4.js
731
+ ```
732
+
733
+ ```
734
+ Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
735
+ input reproduces the recorded call
736
+ later not affected: only the input logic changed
737
+ mark needs a judgement (input_mismatch) → runs by itself
738
+ Nothing saved. To save: bir scenario edit scn_4a17a126-… --step 4 --input-logic step4.js
739
+ ```
740
+
741
+ - `input` — does the code give the recorded call?
742
+ - `later` — do the steps after it still give theirs? Here only the input logic changed. Nothing a
743
+ later step reads has changed, so no later step was judged.
744
+ - `mark` — what happens to the *needs a judgement* mark. `→ runs by itself` means the mark goes
745
+ when you save, and runs stop handing over in front of this step.
746
+
747
+ **4. Save it.** `edit` runs the same check, and saves only what the check approved. Add a `--note`
748
+ that says why; the history keeps it.
749
+
750
+ ```bash
751
+ bir scenario edit scn_4a17a126-… --step 4 --input-logic step4.js --note "summary_table already carries its schema"
752
+ ```
753
+
754
+ ```
755
+ Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
756
+ input reproduces the recorded call
757
+ mark needs a judgement (input_mismatch) → runs by itself
758
+ Saved as sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda (revision 2). Undo: bir scenario undo scn_4a17a126-… --edit sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda
759
+ ```
760
+
761
+ `edit` prints fewer lines than `check`. It always prints `input`, and the other lines only when
762
+ something moved or broke. Step 3 was then saved the same way, as revision 3.
763
+
764
+ **5. Try it without real tools.** A dry replay (§3.1) runs the new code over the recorded outputs:
765
+ `bir scenario replay scn_4a17a126-… --prompt "…" --dry`. The next real prompt uses the new code.
766
+ There is nothing to restart.
767
+
768
+ #### How the check decides
769
+
770
+ The service runs your code the way the recorded run would have run it: with the recording's
771
+ parameter values, and with what the earlier steps kept from their recorded answers.
772
+
773
+ | It asks | It refuses when |
774
+ |---|---|
775
+ | Does the code give the recorded call? Extra spaces inside strings, and the order of keys, do not count | it gives something else (`differs`), or it crashes (`throws`) |
776
+ | Is it a copy? It changes one parameter, or one earlier output, at a time — only the ones the code reads | the call never moves: the code returns the recorded call whatever the request says |
777
+ | Do the later steps still give their calls? Asked only when the **output** logic changed, and only of later steps that are not marked and can be checked | a later step that gave its recorded call before does not any more |
778
+ | Does the final answer still get its values? Asked only when the **output** logic changed | it did before, and does not after |
779
+ | Can the mark go? | you asked to unmark the step (`--unfreeze`), and its input does not reproduce |
780
+
781
+ A recorded input of `{}` is never a copy. A recorded input that is plain text, not an object, is
782
+ `not_checked`.
783
+
784
+ **The check is kept small.** On the service it runs in a worker thread of its own, never in the
785
+ thread that answers requests. By default it may use 5 seconds of sandbox time
786
+ (`EDIT_CHECK_BUDGET_MS`, 5000 ms). Past that it stops, and the change is refused as *too costly to
787
+ check*. The reason `bir` prints is the service's own sentence:
788
+
789
+ ```
790
+ too costly to check: the check used up its 5000 ms of sandbox time (EDIT_CHECK_BUDGET_MS) before it finished.
791
+ ```
792
+
793
+ One run of your code may take 1 second (`LOGIC_TIMEOUT_MS`) and 64 MB of memory
794
+ (`LOGIC_MEMORY_MB`), by default. Code that ran out of time or memory, or stopped the sandbox twice,
795
+ is held back for 10 minutes: every run of that same code fails at once, with the same message.
796
+
797
+ A refused change says where it differs. Here `wrong.js` adds the schema a second time:
798
+
799
+ ```bash
800
+ bir scenario check scn_4a17a126-… --step 4 --input-logic wrong.js
801
+ ```
802
+
803
+ ```
804
+ Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
805
+ input differs from the recorded call at character 32
806
+ computed … FROM employees.employees.hiring_year_summary ORDER BY h…
807
+ recorded … FROM employees.hiring_year_summary ORDER BY hire_year;"…
808
+ later not affected: only the input logic changed
809
+ mark needs a judgement (input_mismatch) → stays marked
810
+ Nothing saved, and `bir scenario edit` would refuse it: the change does not reproduce the recording.
811
+ Fix the code and check again. If the recording itself was wrong, edit can save it on purpose: add --force --note "why".
812
+ ```
813
+
814
+ `bir scenario edit` with the same file refuses it, and saves nothing:
815
+
816
+ ```
817
+ Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
818
+ input differs from the recorded call at character 32
819
+ computed … FROM employees.employees.hiring_year_summary ORDER BY h…
820
+ recorded … FROM employees.hiring_year_summary ORDER BY hire_year;"…
821
+ Not saved: the change does not reproduce the recording.
822
+ Fix the code and try again, or save it on purpose: add --force --note "why".
823
+ ```
824
+
825
+ A pasted value is refused as a copy. Here `pasted.js` returns the recorded SQL as fixed text:
826
+
827
+ ```bash
828
+ bir scenario check scn_4a17a126-… --step 4 --input-logic pasted.js
829
+ ```
830
+
831
+ ```
832
+ Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
833
+ input is a copy: it returns the recorded call whatever the parameters say
834
+ later not affected: only the input logic changed
835
+ mark needs a judgement (input_mismatch) → stays marked
836
+ Nothing saved, and `bir scenario edit` would refuse it: the input logic is a copy: it returns the recorded call whatever the parameters and earlier outputs say.
837
+ Fix the code and check again. If the recording itself was wrong, edit can save it on purpose: add --force --note "why".
838
+ ```
839
+
840
+ Any other refusal prints the service's own reason, one line for each problem.
841
+
842
+ Code and a mark the step already has change nothing. `check` then says
843
+ `Nothing would change: the step already has this code and mark.` (exit 0), and `edit` refuses:
844
+
845
+ ```bash
846
+ bir scenario edit scn_4a17a126-… --step 4 --input-logic step4.js
847
+ ```
848
+
849
+ ```
850
+ Nothing to change: the step already has this code and this mark.
851
+ ```
852
+
853
+ #### `--force --note`: save it on purpose
854
+
855
+ **Use `--force` only when the recording itself was wrong** — for example, the recorded run sent a
856
+ call with a mistake you do not want repeated. Then no correct code can reproduce it.
857
+
858
+ - `--force` needs a `--note` that says why (500 characters at most). Without one, `bir` stops
859
+ before it sends anything (`note_required`).
860
+ - **When the check found any problem, a forced save is recorded as forced.** `bir` says so under
861
+ the `Saved as …` line and repeats what the check said. `bir investigate` keeps reporting the step
862
+ as a `warn`.
863
+ - A forced save removes a mark **the calculation** set, unless you also pass `--freeze`. A mark
864
+ made **by hand** (`--freeze`) stays. A save lifts it only with `--unfreeze`.
865
+ - A step after a sub-task call row can only be saved with `--force`: the check cannot rebuild the
866
+ outputs that step reads.
867
+
868
+ #### Mark or unmark a step by hand
869
+
870
+ - `--freeze` marks the step *needs a judgement* (`nondeterministicWhy: by_hand`). Runs then hand
871
+ over to the agent in front of it. No check blocks this: a mark only makes replay more careful.
872
+ - `--unfreeze` asks to remove a mark. The check decides: the step's input must reproduce. With
873
+ `--force --note "why"` the mark goes anyway, and the edit is recorded as forced.
874
+ `bir scenario check … --unfreeze` asks the same question without saving.
875
+ - **A mark made by hand is lifted only when someone asks.** A code edit does not lift it, even one
876
+ that reproduces the recording, and neither does a forced save. What lifts it: `--unfreeze`,
877
+ undoing the edit that made it, or `bir scenario calc … --force --discard-edits`.
878
+
879
+ #### The history, and undo
880
+
881
+ After both steps were saved:
882
+
883
+ ```bash
884
+ bir scenario edits scn_4a17a126-…
885
+ ```
886
+
887
+ ```
888
+ sedit_02cb1210-1938-4902-9395-5a82b321276d 2026-09-25 11:10:59Z step 3 edit revision 2 → 3 "use the settings, not the recorded names"
889
+ input logic changed; needs a judgement (input_mismatch) → runs by itself
890
+ sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda 2026-09-25 11:10:59Z step 4 edit revision 1 → 2 "summary_table already carries its schema"
891
+ input logic changed; needs a judgement (input_mismatch) → runs by itself
892
+ ```
893
+
894
+ The newest entry is first. Each id is printed in full, because it is what `undo --edit` takes.
895
+
896
+ Take back the newest edit:
897
+
898
+ ```bash
899
+ bir scenario undo scn_4a17a126-…
900
+ ```
901
+
902
+ ```
903
+ Step 3 (mcp__postgres__execute_sql) — checked against run_67afb010-…
904
+ input back as it was before that edit
905
+ mark runs by itself → needs a judgement (input_mismatch)
906
+ Step 3 is back as it was before sedit_02cb1210-1938-4902-9395-5a82b321276d (revision 4). Redo: bir scenario undo scn_4a17a126-… --edit sedit_1bcba735-a526-4a1c-a890-261addf2d82b
907
+ One further back: bir scenario undo scn_4a17a126-…
908
+ ```
909
+
910
+ The same command again goes one further back:
911
+
912
+ ```bash
913
+ bir scenario undo scn_4a17a126-…
914
+ ```
915
+
916
+ ```
917
+ Step 4 (mcp__postgres__execute_sql) — checked against run_67afb010-…
918
+ input back as it was before that edit
919
+ mark runs by itself → needs a judgement (input_mismatch)
920
+ Step 4 is back as it was before sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda (revision 5). Redo: bir scenario undo scn_4a17a126-… --edit sedit_8d3da392-1d96-4b2e-8c5d-34f8ab668b53
921
+ One further back: bir scenario undo scn_4a17a126-…
922
+ ```
923
+
924
+ Now the history shows the two undos, and both edits as undone:
925
+
926
+ ```bash
927
+ bir scenario edits scn_4a17a126-…
928
+ ```
929
+
930
+ ```
931
+ sedit_8d3da392-1d96-4b2e-8c5d-34f8ab668b53 2026-09-25 11:11:00Z step 4 revert revision 4 → 5
932
+ undoes sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda; input logic changed; runs by itself → needs a judgement (input_mismatch)
933
+ sedit_1bcba735-a526-4a1c-a890-261addf2d82b 2026-09-25 11:11:00Z step 3 revert revision 3 → 4
934
+ undoes sedit_02cb1210-1938-4902-9395-5a82b321276d; input logic changed; runs by itself → needs a judgement (input_mismatch)
935
+ sedit_02cb1210-1938-4902-9395-5a82b321276d 2026-09-25 11:10:59Z step 3 edit revision 2 → 3 "use the settings, not the recorded names"
936
+ input logic changed; needs a judgement (input_mismatch) → runs by itself; undone 2026-09-25 11:11:00Z
937
+ sedit_633dba0d-bbbe-4517-ac29-74cc85a3abda 2026-09-25 11:10:59Z step 4 edit revision 1 → 2 "summary_table already carries its schema"
938
+ input logic changed; needs a judgement (input_mismatch) → runs by itself; undone 2026-09-25 11:11:00Z
939
+ ```
940
+
941
+ Step 4 is the calculation's code again: `bir scenario show scn_4a17a126-… --step 4` now prints
942
+ `"chainRevision": 5`, `"editedAt": null` and the mark `"nondeterministicWhy": "input_mismatch"`.
943
+
944
+ How undo works:
945
+
946
+ - **Undo walks back.** `bir scenario undo <id>` takes back the newest edit that can be undone. Its
947
+ step goes back as it was before that edit: its code and its mark. Run it again to go one further
948
+ back. Keep going, and each step gets back to the calculation's code, unless an undo is refused
949
+ (below).
950
+ - **`--step <n>` limits it to one step.** `bir scenario undo scn_… --step 4` takes back step 4's
951
+ newest edit, and nothing else.
952
+ - `--edit <sedit_id>` takes back that one entry.
953
+ - An undo is a new line in the history (`revert`). **To redo, undo the undo** by its id. The undo
954
+ prints that command after `Redo:`.
955
+ - **An undo is checked too:** for the later steps and for the final answer, like a save (so only
956
+ when the undo changes the step's output logic). The step itself is not judged: it goes back
957
+ exactly as it was. If the undo would break a later step or the answer, it is refused
958
+ (`undo_refused`). `bir` prints `Not undone:` with the reason, then
959
+ `To undo it anyway, on purpose: add --force --note "why".` An undo forced past a refusal is
960
+ recorded as forced.
961
+ - Once every hand edit of a plan is taken back, the service repairs and rebuilds it as usual again.
962
+ An undo forced past a refusal is the exception: it leaves its step counted as edited and
963
+ forced, so `bir investigate` keeps warning about it.
964
+
965
+ #### What a save changes
966
+
967
+ - **The next prompt uses the new code.** The service hands out the plan fresh on every prompt. A
968
+ turn that is already running keeps the old plan until it ends.
969
+ - **The revision goes up by one, and the step's failure count is cleared.** Reports from turns that
970
+ started before the change do not count against the new code. An undo does the same.
971
+ - **A saved step runs unattended, exactly like calculated code** (§5.1), on this machine, in the
972
+ recorder's own process with a time limit and no sandbox. The service's sandbox does not protect
973
+ this machine. Read your change as carefully as you read a calculated plan, and dry-replay it
974
+ (§3.1).
975
+ - **Edits are protected.** A recalculation rebuilds every step from the recording, and would throw
976
+ your fix away. So `calc --force` refuses:
977
+
978
+ ```bash
979
+ bir scenario calc run_67afb010-… --force
980
+ ```
981
+
982
+ ```
983
+ Not recalculated: steps 3, 4 were edited by hand, and a recalculation rebuilds every step.
984
+ To recalculate anyway and discard the edits: bir scenario calc run_67afb010-… --force --discard-edits
985
+ (the edits stay in `bir scenario edits`, marked replaced)
986
+ ```
987
+
988
+ A `run_` id reaches only the run's whole-run scenario. `bir scenario calc <scnId> --force`
989
+ recalculates any scenario in place, by its own id, a sub-task scenario included. Add
990
+ `--discard-edits` when it has hand edits. A `scn_` id without `--force` is refused before anything
991
+ is sent: a scenario that exists can only be recalculated.
992
+
993
+ The console's *Recalculate* button (on a sub-task's scenario) is refused too while a plan has hand
994
+ edits; the console does not discard them. The service's own rebuilds — the repair of a parked step
995
+ (§9) and the adoption of a sub-task — skip a plan with hand edits. `bir investigate` says that the
996
+ service does not rebuild the plan on its own, and warns when it skipped a repair
997
+ (`repair_skipped_edits`). After `--discard-edits` the old edits stay readable, but they cannot be
998
+ undone (`edit_replaced`).
999
+
1000
+ #### What `bir investigate` says
1001
+
1002
+ It reports every edited step, and names the command that takes it back. After both steps were
1003
+ saved, its service findings began like this:
1004
+
1005
+ ```
1006
+ Service findings
1007
+ 1. [info] Step 3 (mcp__postgres__execute_sql) was edited by hand on 2026-09-25; the chain is at revision 3.
1008
+ why: Its code or its mark was changed by hand rather than by the calculation, and the change was checked against the recording before it was saved. The service does not rebuild a plan with hand edits on its own.
1009
+ fix: To see it: `bir scenario edits scn_4a17a126-…`; to take it back: `bir scenario undo scn_4a17a126-… --step 3`. A recalculation will not replace it unless you pass --discard-edits.
1010
+ 2. [info] Step 4 (mcp__postgres__execute_sql) was edited by hand on 2026-09-25; the chain is at revision 3.
1011
+ why: Its code or its mark was changed by hand rather than by the calculation, and the change was checked against the recording before it was saved. The service does not rebuild a plan with hand edits on its own.
1012
+ fix: To see it: `bir scenario edits scn_4a17a126-…`; to take it back: `bir scenario undo scn_4a17a126-… --step 4`. A recalculation will not replace it unless you pass --discard-edits.
1013
+ ```
1014
+
1015
+ A forced edit is a `warn`, not an `info`. So is a repair the service skipped because of hand edits.
1016
+
1017
+ #### Let Claude do it
1018
+
1019
+ Claude Code can do all of this for you, in two ways. Both use the same service routes and the same
1020
+ check.
1021
+
1022
+ 1. **In its terminal.** Claude runs `bir scenario …` like any other command. You see each command,
1023
+ and Claude Code asks you before it runs one, unless you allowed it. This works in every project.
1024
+ 2. **As tools on the `bir` MCP server** — the server `bir setup` or `bir install --replay` adds
1025
+ (§5.2). This is smoother for the model. **By default the server lists only `run_scenario`:
1026
+ every tool definition rides along with every request of every session. The tools that read and
1027
+ change a scenario come when you turn editing on, per project:**
1028
+
1029
+ ```bash
1030
+ bir scenario editing on # this project's `bir` server also offers the tools that read and change a scenario
1031
+ bir scenario editing off # back to run_scenario alone
1032
+ bir scenario editing status # what is stored for this project
1033
+ ```
1034
+
1035
+ `BIR_SCENARIO_TOOLS=read` in the `bir` server's environment (its entry's `env`, or the shell that
1036
+ starts `claude`) lists the reading tools without turning editing on. A reading tool answers when it
1037
+ is called unlisted, too.
1038
+
1039
+ The tools, as the `bir` server defines them. **Bold** arguments are required.
1040
+
1041
+ | Tool (`mcp__bir__…`) | Offered | Arguments | Runs |
1042
+ |---|---|---|---|
1043
+ | `run_scenario` | always, unless the runner is off | none | the plan armed for this turn (§6.1) |
1044
+ | `scenario_show` | after `bir scenario editing on`, or with `BIR_SCENARIO_TOOLS=read` | **`target`**, `step` | `bir scenario show` |
1045
+ | `scenario_edits` | after `bir scenario editing on`, or with `BIR_SCENARIO_TOOLS=read` | **`target`** | `bir scenario edits` |
1046
+ | `investigate` | after `bir scenario editing on`, or with `BIR_SCENARIO_TOOLS=read` | `id` — a `run_`, `scn_` or `sexec_` id; without it, the newest turn here | `bir investigate` |
1047
+ | `scenario_check` | after `bir scenario editing on` | **`target`**, **`step`**, `inputLogic`, `outputLogic`, `unfreeze` | `bir scenario check` |
1048
+ | `scenario_edit` | after `bir scenario editing on` | **`target`**, **`step`**, `inputLogic`, `outputLogic`, `freeze`, `unfreeze`, `force`, `note`, `revision` | `bir scenario edit` |
1049
+ | `scenario_undo` | after `bir scenario editing on` | **`target`**, `edit` or `step` (not both), `force`, `note` | `bir scenario undo` |
1050
+
1051
+ - `target` is a `scn_` id, or the `run_` id of a whole-run scenario. `step` is the `stepIndex`.
1052
+ - `inputLogic` and `outputLogic` are the code itself, as text. The server writes them to temporary
1053
+ files for `bir`, and removes the files afterwards.
1054
+ - `revision` is the `chainRevision` at the top of `scenario_show` with `step`.
1055
+ - `force` needs `note`, as on the command line.
1056
+ - Each tool runs this package's own `bir` with `--json`. A refusal comes back as an error result
1057
+ that carries the check's report, so the model can read why.
1058
+ - A reading tool's answer is cut at 24 000 characters (`BIR_SCENARIO_MAX_OUTPUT_CHARS`), with a
1059
+ hint to ask for one step at a time, and it gives up after `BIR_READ_TIMEOUT_MS` (15 s) plus 10 s.
1060
+ A tool that changes a scenario gets 120 s.
1061
+ - When the session ends, or you press Esc during a call, that call's `bir` is stopped and its
1062
+ temporary files are removed. A request the service had already received may still finish there;
1063
+ `bir scenario edits` shows it.
1064
+ - While the runner is off (`bir off`), the server lists no tools; a reading tool called anyway says
1065
+ there is nothing to read, and a changing one is refused.
1066
+
1067
+ Why changing is off by default: the `bir` server runs in every session of every project it is
1068
+ installed in, a fleet included. A plan must not change because some agent there decided to change
1069
+ it. The switch is kept in `~/.baseinstrunner/installed.json`, next to the replay switches. It only
1070
+ decides which tools the `bir` server offers; the `bir scenario` commands work everywhere. The server
1071
+ reads the switch when Claude Code lists its tools, and again on every call. **Restart the Claude
1072
+ Code session after you switch it** — a running session does not see the change.
1073
+
1074
+ **`bir`'s own calls are never recorded.** A session that fixes a plan must not become a plan that
1075
+ edits plans when it is replayed. So these calls are never recorded as steps, and never count as the
1076
+ model leaving a plan: the `bir` server's tools other than `run_scenario`, and a `Bash` or
1077
+ `PowerShell` command made only of `bir` commands (a plain `cd` may come with them).
1078
+ `npm test && bir investigate` is still recorded: the `npm test` half is real work. So is writing
1079
+ the code file, which is an ordinary step. Under `BIR_VERBOSE=1` a skipped call shows as
1080
+ `tool.pre.skipped … why="host housekeeping"`.
1081
+
1082
+ **Do this.** In the Claude Code session, in the project folder, paste:
1083
+
1084
+ ```
1085
+ Run `bir investigate` in this folder. If a step needs a judgement or its code
1086
+ is wrong, read it with `bir scenario show`, write the corrected code to a file,
1087
+ and test it with `bir scenario check`. Show me the check and explain it in plain
1088
+ words. Do not save anything until I say yes. Then save it with
1089
+ `bir scenario edit` and a --note that says why. Never use --force.
1090
+ ```
1091
+
1092
+ For the hiring-year case, Claude's report would be: *step 4's code is a copy of the recorded query,
1093
+ so it was marked; the new code builds the query from `summary_table`; the check says it reproduces
1094
+ the recording, and the step will run by itself.*
1095
+
1096
+ #### When it does not work
1097
+
1098
+ | Symptom | Cause | Fix |
1099
+ |---|---|---|
1100
+ | `input differs from the recorded call at character N` | The code gives a different call from the recorded one | Read the `computed` and `recorded` lines, fix the code, check again |
1101
+ | `input is a copy` | The code returns the recorded call whatever the request says — usually a pasted value | Build the call from `parameters.…` or `respParams.…` |
1102
+ | `input throws` | The code crashed; the message is shown | Fix it. `require`, `process`, `fetch`, `Buffer`, timers, `ArrayBuffer`, the typed arrays and `Symbol.for` do not exist in the check |
1103
+ | `… did not finish in N ms`, `… ran out of memory (the limit is N MB)`, or `… stopped the logic sandbox before it finished` | The code went past the sandbox's time or memory limit (`LOGIC_TIMEOUT_MS`, `LOGIC_MEMORY_MB` on the service), or stopped the sandbox twice. That same code is then refused at once for 10 minutes | Fix the loop or the size. Changed code is new code, and is checked again |
1104
+ | A `later` step no longer reproduces | The change broke a step after it — usually output logic that stopped keeping a value a later step reads | Keep every key the later steps read |
1105
+ | `too costly to check` | The check used up its 5 seconds of sandbox time (`EDIT_CHECK_BUDGET_MS`). Usually code that reads or keeps a great many values, so the check has to run it very many times | Make the code cheaper to run, then check again |
1106
+ | `Nothing to change: the step already has this code and this mark.` | The code and the mark you gave are what the step already has (`nothing_to_change`) | Nothing to do. `check` says `Nothing would change` for the same case |
1107
+ | `not_a_tool_step` | The index is a sub-task call row, not a tool step | Call rows cannot be edited; the service writes them |
1108
+ | `note_required` | `--force` without `--note` | Add `--note "why"` |
1109
+ | `revision_changed` | The plan changed since you read it: another edit, or a recalculation | `show` it again, check again, save again |
1110
+ | `not_ready` or `scenario_calculating` | The scenario is not `ready`, or a recalculation is running | Wait for it to finish |
1111
+ | `undo_refused` | The undo would stop a later step from giving its recorded call, or leave the final answer without its values | Fix that later step first. Or undo it on purpose: `--force --note "why"` |
1112
+ | `not_latest_edit` | The step has changed since the edit you named: a newer entry changed it | `bir` prints two commands: undo the newer one first, then yours |
1113
+ | `already_undone` | That edit was undone already | To redo it, undo the undo. `bir` prints the command |
1114
+ | `edit_replaced` | A recalculation replaced the plan after that edit | Nothing to undo; the edit is history now |
1115
+ | `logic_sandbox_unavailable` | The service could not run step code just now (HTTP 503). Nothing was checked, saved or undone | Try again in a minute |
1116
+ | `invalid_input` | The service found the request invalid; `bir` prints its details. For example, a logic body longer than 20 000 characters, or a note longer than 500 | Shorten it |
1117
+ | `payload_too_large` | The whole request is larger than the service accepts (HTTP 413, over 256 kB) | A logic body may be at most 20 000 characters, and a note 500 |
1118
+ | `rate_limited` | More checks, edits and undos in a minute than the service allows (`SCENARIO_EDITS_PER_MINUTE`, 30 by default) | Wait a minute |
1119
+ | `not found` for a scenario you can see | It is not yours. **Only the owner can change a scenario** — an admin gets `not found` too | Ask the owner |
1120
+ | The `scenario_check`, `scenario_edit` and `scenario_undo` tools are missing | `bir scenario editing on` was not run in this project, or the session started before it | Run it here, then restart the session |
1121
+ | Only `run_scenario` is listed; `scenario_show`, `scenario_edits` and `investigate` are missing | That is the default: the reading tools come with `bir scenario editing on`, or with `BIR_SCENARIO_TOOLS=read` in the server's environment | Either one, then restart the session. Or run the `bir scenario …` commands in the terminal |
1122
+ | The `bir` server lists no tools at all | The runner is off here (`bir off`, `bir off --here`, or `BIR_OFF=1` where Claude Code started) | `bir on` (or `bir on --here`), and the server tells the open session its list changed. For `BIR_OFF=1`, start Claude Code without it |
1123
+
1124
+ ---
1125
+
1126
+ ## 10. What is built
1127
+
1128
+ All of it. Phases R0–R7 of [calculatedReplay.md](calculatedReplay.md) §17 are implemented and
1129
+ tested; everything this guide describes is code you can run. §9.2 is built and tested too, but not
1130
+ yet released.
1131
+
1132
+ | Phase | What landed | Where |
1133
+ |---|---|---|
1134
+ | **R0** | Plan, logic shims, coverage, bundle | [src/replay/](../src/replay/) — `plan.ts`, `logic.ts`, `coverage.ts`, `bundle.ts` |
1135
+ | **R1** | Derivation over raw `fetch`; pricing | [derive.ts](../src/replay/derive.ts), [pricing.ts](../src/replay/pricing.ts) |
1136
+ | **R2** | Gate ladder, mode choice, arming, directive | [controller.ts](../src/replay/controller.ts), [control/server.ts](../src/control/server.ts) |
1137
+ | **R3** | Pin, thread, retire | `onToolPre` / `onToolPost` / `onProxyStep` in [control/server.ts](../src/control/server.ts) |
1138
+ | **R4** | Long-poll work queue; direct execution | [executor.ts](../src/replay/executor.ts), [proxy/session.ts](../src/proxy/session.ts) |
1139
+ | **R5** | The `bir` MCP server, `/scenario/run`, `--replay` | [bin/bir-scenario.ts](../src/bin/bir-scenario.ts), [bin/bir.ts](../src/bin/bir.ts) |
1140
+ | **R6** | Divergence, source-run fallback, `bir replay` | `diverge()` in [controller.ts](../src/replay/controller.ts), [source-run.ts](../src/replay/source-run.ts) |
1141
+ | **R7** | Usage watermark, reporting, ticket, doctor | [control/transcript.ts](../src/control/transcript.ts), [remote-recorder.ts](../src/record/remote-recorder.ts) |
1142
+ | **§9.2** *(not yet released)* | `bir scenario show/check/edit/edits/undo/calc/editing`, the `bir` server's scenario tools, `bir` calls never recorded (`editSteps.md` D13) | [bin/scenario-edit.ts](../src/bin/scenario-edit.ts), [bin/bir-scenario.ts](../src/bin/bir-scenario.ts), [record/housekeeping.ts](../src/record/housekeeping.ts); tests: [scenario-edit.test.ts](../test/scenario-edit.test.ts), [bir-scenario.test.ts](../test/bir-scenario.test.ts), [housekeeping.test.ts](../test/housekeeping.test.ts), [replay-server.test.ts](../test/replay-server.test.ts) |
1143
+
1144
+ ```bash
1145
+ npm test # ~1,000 tests, incl. test/replay.test.ts and test/replay-server.test.ts
1146
+ npm run build && npm run pretest && npm run test:smoke # the smoke test needs the test build too
1147
+ ```
1148
+
1149
+ The smoke test is the one worth running after any change to the seams. It starts a stand-in BaseIn
1150
+ service, a real `bir-hooks`, a real `bir-proxy` wrapping a real MCP server, and a real `bir-scenario`,
1151
+ then drives a two-step scenario end to end through actual stdio — proving that a pinned argument
1152
+ reaches the real tool, that step 1's input was computed from step 0's real output, and that the turn
1153
+ books `steered_full` with `fallbackCostUsd: 0`.
1154
+
1155
+ Three things the implementation settled that the design left open:
1156
+
1157
+ - **`SerializedScenarioStep.recordedOutput` really is never sent.** Recorded outputs come from a lazy
1158
+ `GET /recordings/runs/:runId` ([source-run.ts](../src/replay/source-run.ts)), as design §8.1
1159
+ anticipated — fetched only when `BIR_REPLAY_RECORDED_OUTPUTS=1` lets one stand in, or in
1160
+ `bir replay`. By default a step with no result stops the plan and hands over.
1161
+ - **The work queue must hold work for a proxy that is mid-round-trip.** Failing fast when no poller is
1162
+ *parked* lost every step after the first in a multi-step direct plan, because the proxy is between
1163
+ polls exactly then. It now fails fast only for a server no proxy has served in the last minute —
1164
+ and, with two sessions open, never runs a step on a proxy that may be the other session's.
1165
+ - **The proxies need no replay configuration.** Every bound proxy runs its work loop; while replay
1166
+ is off the control server answers `{ replay: false }` and the proxy polls every 30 s. So
1167
+ `BIR_REPLAY` is a single switch on the control server, and replay turned on with
1168
+ `bir up --restart` reaches open sessions within about 30 s.
1169
+
1170
+ ---
1171
+
1172
+ ## 11. Turning it off
1173
+
1174
+ From least to most.
1175
+
1176
+ ```bash
1177
+ bir replay off # stop arming. Matches still detected; recording still stops on a match.
1178
+ # Stored for this project; `bir up --restart` applies it, `bir replay on` undoes it
1179
+ export BIR_REPLAY=0 # the same, wherever the recorder is started from (the SessionStart hook
1180
+ # gives it Claude Code's environment, not your terminal's)
1181
+ bir off --here # the whole runner, this project, at once: hooks answer nothing, the `bir`
1182
+ # server lists no tools, nothing is recorded or replayed. `bir off` does it
1183
+ # for every project. Nothing is uninstalled; `bir on [--here]` undoes it
1184
+ bir uninstall --replay # remove the `bir` MCP server and switch replay off; proxies and hooks stay
1185
+ bir uninstall # remove everything here: wrapped servers, hooks, the `bir` server; stop the recorder
1186
+ bir uninstall --all # the same in every project on this machine
1187
+ ```
1188
+
1189
+ `bir uninstall` restores a project file (`.mcp.json`, `.claude/settings.local.json`) byte-for-byte
1190
+ when nothing else edited it since the install, and repairs it entry by entry otherwise.
1191
+ `~/.claude.json`, where the wrapped servers and the `bir` server live, is always repaired entry by
1192
+ entry. A file the install created — usually `.claude/settings.local.json` — stays behind with our
1193
+ entries removed, and the line that hid it in `.git/info/exclude` goes with them. A recorder that an
1194
+ open session is still using is left running, switched off for this project: restart that session,
1195
+ then `bir down` (`--keep-recorder` leaves the recorder alone). When something here still runs
1196
+ through the runner, `bir uninstall` says *Not everything could be undone*, lists it, and exits 1.
1197
+
1198
+ Before you remove the package (`npm rm -g @basein/runner`, or deleting `~/.baseinstrunner`), run
1199
+ `bir uninstall --all`. The install line does the same, and nothing else, with `BIR_UNINSTALL=1`:
1200
+
1201
+ ```bash
1202
+ curl -fsSL https://api.bi2202.com/install.sh | BIR_UNINSTALL=1 sh # macOS / Linux
1203
+ ```
1204
+
1205
+ ```powershell
1206
+ $env:BIR_UNINSTALL="1"; irm https://api.bi2202.com/install.ps1 | iex # Windows (PowerShell)
1207
+ ```
1208
+
1209
+ Otherwise every other project's wrapped servers and hooks name files that are gone.
1210
+
1211
+ With replay off the system is exactly v1 again: it recognises a repeated prompt, declines to
1212
+ record it a second time, and lets the model do the work.