@basein/runner 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +276 -0
  3. package/dist/auth/client.d.ts +85 -0
  4. package/dist/auth/client.js +284 -0
  5. package/dist/bin/bir-hooks.d.ts +48 -0
  6. package/dist/bin/bir-hooks.js +201 -0
  7. package/dist/bin/bir-proxy.d.ts +45 -0
  8. package/dist/bin/bir-proxy.js +207 -0
  9. package/dist/bin/bir-scenario.d.ts +24 -0
  10. package/dist/bin/bir-scenario.js +177 -0
  11. package/dist/bin/bir.d.ts +21 -0
  12. package/dist/bin/bir.js +876 -0
  13. package/dist/config/adapters/claude-code.d.ts +76 -0
  14. package/dist/config/adapters/claude-code.js +181 -0
  15. package/dist/config/adapters/generic.d.ts +17 -0
  16. package/dist/config/adapters/generic.js +36 -0
  17. package/dist/config/generate.d.ts +127 -0
  18. package/dist/config/generate.js +114 -0
  19. package/dist/config/resolve.d.ts +68 -0
  20. package/dist/config/resolve.js +132 -0
  21. package/dist/control/client.d.ts +56 -0
  22. package/dist/control/client.js +86 -0
  23. package/dist/control/correlation.d.ts +86 -0
  24. package/dist/control/correlation.js +0 -0
  25. package/dist/control/discovery.d.ts +50 -0
  26. package/dist/control/discovery.js +123 -0
  27. package/dist/control/ordering.d.ts +38 -0
  28. package/dist/control/ordering.js +44 -0
  29. package/dist/control/paths.d.ts +32 -0
  30. package/dist/control/paths.js +56 -0
  31. package/dist/control/server.d.ts +272 -0
  32. package/dist/control/server.js +1131 -0
  33. package/dist/control/transcript.d.ts +75 -0
  34. package/dist/control/transcript.js +241 -0
  35. package/dist/index.d.ts +37 -0
  36. package/dist/index.js +32 -0
  37. package/dist/jsonrpc/framing.d.ts +49 -0
  38. package/dist/jsonrpc/framing.js +143 -0
  39. package/dist/jsonrpc/types.d.ts +52 -0
  40. package/dist/jsonrpc/types.js +46 -0
  41. package/dist/proxy/intercept.d.ts +55 -0
  42. package/dist/proxy/intercept.js +147 -0
  43. package/dist/proxy/relay.d.ts +97 -0
  44. package/dist/proxy/relay.js +166 -0
  45. package/dist/proxy/session.d.ts +116 -0
  46. package/dist/proxy/session.js +319 -0
  47. package/dist/record/housekeeping.d.ts +34 -0
  48. package/dist/record/housekeeping.js +39 -0
  49. package/dist/record/queue.d.ts +48 -0
  50. package/dist/record/queue.js +96 -0
  51. package/dist/record/recorder.d.ts +111 -0
  52. package/dist/record/recorder.js +39 -0
  53. package/dist/record/redact.d.ts +37 -0
  54. package/dist/record/redact.js +119 -0
  55. package/dist/record/remote-recorder.d.ts +110 -0
  56. package/dist/record/remote-recorder.js +301 -0
  57. package/dist/record/truncate.d.ts +36 -0
  58. package/dist/record/truncate.js +85 -0
  59. package/dist/replay/bundle.d.ts +36 -0
  60. package/dist/replay/bundle.js +89 -0
  61. package/dist/replay/controller.d.ts +300 -0
  62. package/dist/replay/controller.js +807 -0
  63. package/dist/replay/coverage.d.ts +41 -0
  64. package/dist/replay/coverage.js +56 -0
  65. package/dist/replay/derive.d.ts +58 -0
  66. package/dist/replay/derive.js +166 -0
  67. package/dist/replay/executor.d.ts +78 -0
  68. package/dist/replay/executor.js +233 -0
  69. package/dist/replay/logic.d.ts +31 -0
  70. package/dist/replay/logic.js +50 -0
  71. package/dist/replay/plan.d.ts +181 -0
  72. package/dist/replay/plan.js +397 -0
  73. package/dist/replay/pricing.d.ts +41 -0
  74. package/dist/replay/pricing.js +76 -0
  75. package/dist/replay/source-run.d.ts +50 -0
  76. package/dist/replay/source-run.js +98 -0
  77. package/dist/replay/tool-error.d.ts +22 -0
  78. package/dist/replay/tool-error.js +60 -0
  79. package/dist/replay/types.d.ts +116 -0
  80. package/dist/replay/types.js +35 -0
  81. package/dist/upstream/client.d.ts +78 -0
  82. package/dist/upstream/client.js +114 -0
  83. package/dist/upstream/http-client.d.ts +78 -0
  84. package/dist/upstream/http-client.js +261 -0
  85. package/dist/upstream/lazy-client.d.ts +31 -0
  86. package/dist/upstream/lazy-client.js +53 -0
  87. package/dist/upstream/stdio-client.d.ts +57 -0
  88. package/dist/upstream/stdio-client.js +203 -0
  89. package/dist/util/log.d.ts +27 -0
  90. package/dist/util/log.js +51 -0
  91. package/dist/util/version.d.ts +2 -0
  92. package/dist/util/version.js +40 -0
  93. package/docs/BaseInstRunner.md +621 -0
  94. package/docs/calculatedReplay.md +1185 -0
  95. package/docs/calculatedReplayGuide.md +448 -0
  96. package/docs/installRun.md +413 -0
  97. package/docs/mcpmark.md +752 -0
  98. package/docs/quickstart.md +201 -0
  99. package/docs/t-bench.md +394 -0
  100. package/package.json +56 -0
@@ -0,0 +1,448 @@
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). It is off by default and
8
+ > it should stay off until you have read §5. Nothing in §1–§4 changes any behaviour.
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 | `curl -fsS "$BIR_AUTH_URL/health"` |
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 `bir-hooks` running | `bir doctor` |
37
+ | 6 | *(replay only)* `ANTHROPIC_API_KEY` in the `bir-hooks` environment | optional — 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 `tier: standalone`, replay cannot arm, and that is the
41
+ honest ceiling rather than a bug.
42
+
43
+ ```bash
44
+ export BIR_AUTH_URL=https://your-basein-service
45
+ npm install && npm run build
46
+ node dist/bin/bir.js install # or `bir install` once linked/published
47
+ bir login
48
+ bir-hooks # leave running in its own terminal
49
+ ```
50
+
51
+ ---
52
+
53
+ ## 2. Record a run worth reusing
54
+
55
+ Nothing special — work normally. Two server-side facts decide whether the run becomes reusable, and
56
+ both bite quietly:
57
+
58
+ **A run needs at least `RECORDING_MIN_ACTIONS` tool actions (default 4).** Below that it is not
59
+ embedded, does not enter the similarity pool, does not appear in the list, and is eventually pruned.
60
+ A two-tool run will never match anything, no matter how many times you repeat it.
61
+
62
+ **The prompt is what gets embedded.** `findSimilarRun` embeds `run.input` — the text of your
63
+ `UserPromptSubmit` — not the transcript, not the answer. So the prompt that *starts* the turn is the
64
+ one future prompts are compared against. Write the first prompt of a reusable task as if someone else
65
+ will have to phrase it again.
66
+
67
+ Watch it land:
68
+
69
+ ```
70
+ [bir] … run.start run=run_… sess=birsess_… tier=bound
71
+ [bir] … run.finish run=run_… steps=14 durationMs=48210 lossy=false recorded=true
72
+ ```
73
+
74
+ `recorded=false` means the service declined the run — usually because it matched something you
75
+ already have. That is §4 working.
76
+
77
+ List what you have:
78
+
79
+ ```bash
80
+ bir scenario list # or: GET /recordings/runs
81
+ ```
82
+
83
+ ```
84
+ RUN ITER STEPS SCENARIO STATE TITLE
85
+ run_5f3a… 3 14 scn_9c1b… ready Fleet risk sweep
86
+ run_a812… 1 9 — — Rewrite the billing README
87
+ run_c004… 1 6 scn_44d0… calculating Check open PRs for stale reviews
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 3. Calculate the scenario
93
+
94
+ A recorded run is a transcript. A **scenario** is what BaseIn derives from it: an intent, a parameter
95
+ schema, and one JavaScript logic body per step that recomputes that step's tool arguments from new
96
+ parameters. Until a run has one, a match can only stop recording — there is nothing to run.
97
+
98
+ ```bash
99
+ bir scenario calc run_5f3a… # POST /recordings/runs/:runId/calculate → 202
100
+ bir scenario show run_5f3a… # GET /recordings/runs/:runId/scenario (poll)
101
+ ```
102
+
103
+ Raw, if you prefer — the token lives in `~/.baseinstrunner/credentials.json`:
104
+
105
+ ```bash
106
+ TOKEN=$(node -p "require(require('os').homedir()+'/.baseinstrunner/credentials.json').accessToken")
107
+ curl -fsS -X POST "$BIR_AUTH_URL/recordings/runs/run_5f3a…/calculate" \
108
+ -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{}'
109
+ ```
110
+
111
+ ```powershell
112
+ $t = (Get-Content "$HOME\.baseinstrunner\credentials.json" | ConvertFrom-Json).accessToken
113
+ Invoke-RestMethod -Method Post -Uri "$env:BIR_AUTH_URL/recordings/runs/run_5f3a…/calculate" `
114
+ -Headers @{ authorization = "Bearer $t" } -ContentType 'application/json' -Body '{}'
115
+ ```
116
+
117
+ Calculation is a background job. Poll `show` until `state` leaves `calculating`:
118
+
119
+ | `state` | Meaning | Next |
120
+ |---|---|---|
121
+ | `calculating` | job running | poll again in a few seconds |
122
+ | `ready` | usable | §4 |
123
+ | `failed` | see the `error` field | fix and recalculate with `--force` |
124
+
125
+ `409 scenario_exists` means this run already has one. Re-derive it in place — keeping the id and its
126
+ replay history — with `bir scenario calc run_… --force`.
127
+
128
+ ### 3.1 Check the scenario before you trust it
129
+
130
+ **Dry replay is the cheapest thing you will do all day. Do it every time.** It derives parameters
131
+ from a fresh prompt and runs the stored logic against the *original run's recorded outputs* — no real
132
+ tools, no side effects, one Haiku call:
133
+
134
+ ```bash
135
+ bir scenario replay scn_9c1b… --prompt "which devices in the fleet look risky right now?" --dry
136
+ ```
137
+
138
+ ```
139
+ params { "fleetId": "eu-west-1", "riskThreshold": 0.8 }
140
+ step 0 mcp__chrome-devtools__navigate_page { "url": "https://fleet.internal/eu-west-1" }
141
+ step 1 mcp__chrome-devtools__take_snapshot {}
142
+ → emitted: devices[24]
143
+ step 2 mcp__fleet-api__device_detail { "id": "dev_88f1" }
144
+ → emitted: highestRiskDeviceId, riskScore
145
+ response { "fleet": "eu-west-1", "atRisk": 3, "worst": "dev_88f1" }
146
+ cost $0.0012
147
+ ```
148
+
149
+ Read it for three things:
150
+
151
+ 1. **Did the parameters come out right** for a prompt phrased differently from the original?
152
+ 2. **Do the step inputs look computed**, or are they the original run's constants copied through? A
153
+ scenario whose inputs never vary with the parameters is a recording, not a scenario.
154
+ 3. **Is `emitted` non-empty** for steps that later steps depend on? An empty `emitted` means
155
+ `toolOutputLogic` derived nothing, and every downstream input reading it will be wrong.
156
+
157
+ A dry replay is recorded as `outcome: 'dry'` and is excluded from savings by construction — it costs
158
+ you a Haiku call and nothing else.
159
+
160
+ ---
161
+
162
+ ## 4. Confirm the match fires
163
+
164
+ Now ask the same thing in different words, in a new session.
165
+
166
+ ```
167
+ you: can you check the eu-west fleet and tell me which boxes are in trouble?
168
+ ```
169
+
170
+ ```
171
+ [bir] … run.matched run=run_… matchedRun=run_5f3a… similarity=0.94
172
+ why="similar prompt — the service kept its own run; not recording this one"
173
+ ```
174
+
175
+ That line is the whole feature working up to the point of replay. If it does not appear, the prompt
176
+ did not clear the server's `SIMILARITY_THRESHOLD` (default 0.9) — see §8.
177
+
178
+ **With replay off (the default), this is where it stops.** The server keeps its canonical run, bumps
179
+ its `iterations`, this turn is not recorded, and the model answers the ordinary way. Nothing is lost
180
+ and nothing is skipped.
181
+
182
+ ---
183
+
184
+ ## 5. Turn replay on
185
+
186
+ > Read §5.1 before running anything in this section.
187
+
188
+ ### 5.1 What you are agreeing to
189
+
190
+ A calculated scenario is, in effect, **a pre-approved list of tool calls with computed arguments.**
191
+ When replay arms:
192
+
193
+ - **Steered steps are auto-approved.** `PreToolUse` is answered `allow`, so a pinned `Bash` or `Edit`
194
+ runs without the prompt you would normally see.
195
+ - **Direct steps are not visible to the permission system at all.** They are dispatched straight to a
196
+ `bir-proxy`, which calls the upstream on a connection it already holds. Claude Code never sees them.
197
+
198
+ That is what makes replay fast, and it is the whole risk. Two things follow:
199
+
200
+ 1. Only enable it in a project whose scenarios you have dry-replayed and read (§3.1).
201
+ 2. Set `BIR_REPLAY_ALLOW_SERVERS` (§5.3). An allowlist of the servers you are comfortable having
202
+ called unattended is cheap insurance, and it is the difference between "replay reads a dashboard"
203
+ and "replay can do anything any wrapped server can do".
204
+
205
+ ### 5.2 Enable it
206
+
207
+ ```bash
208
+ bir install --replay # adds the `bir` MCP server; raises the prompt-hook timeout
209
+ export BIR_REPLAY=1 # in the terminal that runs bir-hooks
210
+ bir-hooks # restart it
211
+ ```
212
+
213
+ `bir install --replay` does exactly two things beyond a normal install, and `bir uninstall` reverses
214
+ both:
215
+
216
+ - registers a first-party MCP server under the key **`bir`**, exposing one tool,
217
+ `mcp__bir__run_scenario` — the channel a fully-wrapped scenario's results come back through;
218
+ - raises `UserPromptSubmit`'s hook timeout from 5 s to 15 s, so the match round trip fits.
219
+
220
+ The `bir` tool is visible to the model in every session in this project. That is a real cost — one
221
+ more tool in the list, a little context per turn — and it is why replay is a separate flag rather
222
+ than part of `bir install`.
223
+
224
+ Confirm:
225
+
226
+ ```bash
227
+ bir doctor
228
+ ```
229
+
230
+ ```
231
+ replay ON servers=chrome-devtools,fleet-api minSimilarity=0.92
232
+ derive=claude-haiku-4-5-20251001 (ANTHROPIC_API_KEY present)
233
+ control http://127.0.0.1:53411 sess=birsess_…
234
+ wrapped chrome-devtools ✓ proxy pid 41822 fleet-api ✓ proxy pid 41823
235
+ recording yes (https://your-basein-service)
236
+ ```
237
+
238
+ ### 5.3 Environment
239
+
240
+ **Set these in one place: the terminal that runs `bir-hooks`.** The proxies need no replay
241
+ configuration of their own — each one is told whether to open its work loop when it registers, so
242
+ there is exactly one switch and it cannot get out of step with itself.
243
+
244
+ | Variable | Default | What it does |
245
+ |---|---|---|
246
+ | `BIR_REPLAY` | *(unset)* | `1` enables replay. Nothing below matters until it is set |
247
+ | `BIR_REPLAY_ALLOW_SERVERS` | *(all wrapped)* | Comma-separated server keys eligible for **direct** execution. **Set this** |
248
+ | `BIR_MIN_STEER_SIMILARITY` | `0.92` | Below this a match is detected but not replayed (§8) |
249
+ | `ANTHROPIC_API_KEY` | *(unset)* | Enables parameter derivation. Without it, replay uses the scenario's **recorded sample values** — free, and often still correct |
250
+ | `BIR_DERIVE_MODEL` | `claude-haiku-4-5-20251001` | The derivation model |
251
+ | `BIR_MATCH_BUDGET_MS` | `2500` | How long the prompt hook waits for a match before giving up |
252
+ | `BIR_DERIVE_BUDGET_MS` | `8000` | How long the first `PreToolUse` waits for parameters |
253
+ | `BIR_REPLAY_BUDGET_MS` | `120000` | Whole-plan ceiling for direct execution |
254
+ | `BIR_STEP_TIMEOUT_MS` | `60000` | Per-step ceiling for one direct `tools/call` |
255
+ | `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
256
+
257
+ **No API key is a supported configuration, not a broken one.** Derivation is skipped and every
258
+ parameter takes the value it had in the recorded run. For a scenario whose parameters rarely change
259
+ — a dashboard sweep, a fixed report — that is a complete, free replay. For one that keys off the
260
+ prompt ("check *eu-west*"), you want the key.
261
+
262
+ ---
263
+
264
+ ## 6. Run it, and read what happened
265
+
266
+ Ask the matching question again, with replay on.
267
+
268
+ ### 6.1 A fully-wrapped scenario — direct
269
+
270
+ ```
271
+ [bir] … run.matched run=run_… matchedRun=run_5f3a… similarity=0.94
272
+ [bir] … plan.armed scenario=scn_9c1b… mode=direct steps=3
273
+ tools="mcp__chrome-devtools__navigate_page,mcp__chrome-devtools__take_snapshot,mcp__fleet-api__device_detail"
274
+ [bir] … replay.derived params=2 costUsd=0.0011 ms=840
275
+ [bir] … replay.step n=0 tool=navigate_page server=chrome-devtools ms=612 ok=true
276
+ [bir] … replay.step n=1 tool=take_snapshot server=chrome-devtools ms=1104 ok=true emitted=devices
277
+ [bir] … replay.step n=2 tool=device_detail server=fleet-api ms=210 ok=true emitted=highestRiskDeviceId,riskScore
278
+ [bir] … replay.done scenario=scn_9c1b… mode=direct steps=3/3 outcome=steered_full ms=2766
279
+ [bir] … execution.reported scenario=scn_9c1b… outcome=steered_full derive=0.0011 session=0.0083 fallback=0 savedUsd=0.114
280
+ ```
281
+
282
+ In the session you will see one tool call, `mcp__bir__run_scenario`, returning the composed results.
283
+ The model reads them and answers. It never emitted the three underlying calls.
284
+
285
+ ### 6.2 A mixed scenario — steer
286
+
287
+ The model emits each call itself; `bir` overrides the arguments.
288
+
289
+ ```
290
+ [bir] … plan.armed scenario=scn_44d0… mode=steer steps=4
291
+ [bir] … replay.pin n=0 tool=mcp__github__list_pull_requests
292
+ [bir] … replay.thread n=0 via=proxy emitted=prNumbers
293
+ [bir] … replay.pin n=1 tool=Read
294
+ [bir] … replay.thread n=1 via=hook emitted=reviewers
295
+ [bir] … replay.done scenario=scn_44d0… mode=steer steps=4/4 outcome=steered_full
296
+ ```
297
+
298
+ `via=proxy` versus `via=hook` matters: a wrapped MCP step's output is threaded from the **proxy's**
299
+ report, because that is the shape the scenario's logic was written against (design §7.2). A
300
+ `via=hook fallback=true` line means the proxy did not report in time and the hook's differently-shaped
301
+ view was used instead — expect derived values to be empty, and treat repeats as a bug.
302
+
303
+ ### 6.3 The model goes off-script
304
+
305
+ ```
306
+ [bir] … replay.diverge scenario=scn_44d0… expected=Read called=Bash step=2/4
307
+ [bir] … replay.compose remaining=2 direct=1 recorded=1 skipped=0 bytes=8412 costUsd=0
308
+ [bir] … replay.done outcome=diverged steps=4/4
309
+ ```
310
+
311
+ `costUsd=0` on the compose line is the point of the whole design: the remaining steps ran through
312
+ proxies that were already connected, so recovering from a divergence cost no tokens at all.
313
+
314
+ ### 6.4 The audit vocabulary
315
+
316
+ Everything replay decides is one line. `grep` for these:
317
+
318
+ | Line | Means |
319
+ |---|---|
320
+ | `run.matched` | The server recognised the prompt |
321
+ | `replay.decision … verdict=no-steer` | Matched, and declined. `why=` says which gate |
322
+ | `plan.armed` | A plan exists. Carries mode, step count and the ordered tool names — **before** anything runs |
323
+ | `replay.derived` / `replay.derive_failed` | Parameters resolved, or fell back to sample values |
324
+ | `replay.pin` / `replay.thread` | One steered step's input pinned / output threaded |
325
+ | `replay.step` | One direct step executed |
326
+ | `replay.diverge` / `replay.compose` | Off-script, and the recovery |
327
+ | `replay.done` | Final outcome, steps completed, elapsed |
328
+ | `execution.reported` | The ticket was redeemed and the saving booked |
329
+
330
+ Keep them: `bir-hooks 2>&1 | tee -a ~/.baseinstrunner/audit.log`.
331
+
332
+ ---
333
+
334
+ ## 7. What each outcome means for your numbers
335
+
336
+ Reported once per matched turn, to `POST /scenarios/:id/executions`, redeeming the match's
337
+ `executionTicket`.
338
+
339
+ | Outcome | When | Where it lands |
340
+ |---|---|---|
341
+ | `steered_full` | Every planned step ran under the scenario's control | savings ledger |
342
+ | `diverged` | Off-script; the composed bundle answered | savings ledger |
343
+ | `not_steered` | Matched, declined at a gate — the agent ran the task normally | **baseline sample** |
344
+ | `failed` | Armed, then gave up — the agent ran the task normally | **baseline sample** |
345
+
346
+ `not_steered` is not an error, and it is not wasted. A saving is `baseline − actual`, and the baseline
347
+ is *what this task costs when the agent does it*. Every declined match is a fresh measurement of
348
+ exactly that. A deployment that never declines has no baseline to measure against — which is a good
349
+ reason to leave `BIR_MIN_STEER_SIMILARITY` where it is rather than driving it to zero.
350
+
351
+ An **unredeemed** ticket books nothing at all. Silence loses a saving; it never invents one.
352
+
353
+ ---
354
+
355
+ ## 8. Tuning
356
+
357
+ Only one knob matters in practice.
358
+
359
+ **`BIR_MIN_STEER_SIMILARITY` (default 0.92).** The server detects matches at 0.9 and uses that to
360
+ decide *"don't record this again"* — cheap: the worst case is a lost recording. Steering makes a much
361
+ stronger claim, *"don't think about this again"*, and a 0.90-similar prompt usually covers only part
362
+ of the live task. Steering it produces a confident, thin, wrong answer.
363
+
364
+ Tune it from evidence, not taste. Run for a week and read the declines:
365
+
366
+ ```bash
367
+ grep 'replay.decision' audit.log | grep 'why="similarity below threshold"'
368
+ ```
369
+
370
+ - Prompts you *would* have wanted replayed, sitting at 0.90–0.92 → lower it, in steps of 0.01.
371
+ - Replays that answered the wrong question → raise it, and re-read the scenario's `intent`.
372
+
373
+ If a specific pair of prompts *should* match and does not, the fix is usually upstream of the
374
+ threshold: the two prompts genuinely mean different things to the embedding model. Re-record with a
375
+ first prompt that states the task plainly.
376
+
377
+ ---
378
+
379
+ ## 9. Troubleshooting
380
+
381
+ | Symptom | Cause | Fix |
382
+ |---|---|---|
383
+ | 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 |
384
+ | `run.matched` but no `plan.armed` | A gate declined | Read the `replay.decision` line's `why=`. Ranked by frequency: `BIR_REPLAY` unset · scenario not `ready` · similarity below threshold · no step is executable |
385
+ | `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 |
386
+ | `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 |
387
+ | 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` |
388
+ | `replay.thread … emitted=` empty | `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 |
389
+ | `via=hook fallback=true` 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 |
390
+ | `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 |
391
+ | `execution.reported … 409 invalid_ticket` | The ticket was already redeemed, or belongs to another scenario | Harmless if `duplicate: true`. Otherwise a rollover raced `SessionEnd`; the server books once |
392
+ | `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 |
393
+ | `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` |
394
+ | 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 |
395
+
396
+ ---
397
+
398
+ ## 10. What is built
399
+
400
+ All of it. Phases R0–R7 of [calculatedReplay.md](calculatedReplay.md) §17 are implemented and
401
+ tested; everything this guide describes is code you can run.
402
+
403
+ | Phase | What landed | Where |
404
+ |---|---|---|
405
+ | **R0** | Plan, logic shims, coverage, bundle | [src/replay/](../src/replay/) — `plan.ts`, `logic.ts`, `coverage.ts`, `bundle.ts` |
406
+ | **R1** | Derivation over raw `fetch`; pricing | [derive.ts](../src/replay/derive.ts), [pricing.ts](../src/replay/pricing.ts) |
407
+ | **R2** | Gate ladder, mode choice, arming, directive | [controller.ts](../src/replay/controller.ts), [control/server.ts](../src/control/server.ts) |
408
+ | **R3** | Pin, thread, retire | `onToolPre` / `onToolPost` / `onProxyStep` in [control/server.ts](../src/control/server.ts) |
409
+ | **R4** | Long-poll work queue; direct execution | [executor.ts](../src/replay/executor.ts), [proxy/session.ts](../src/proxy/session.ts) |
410
+ | **R5** | The `bir` MCP server, `/scenario/run`, `--replay` | [bin/bir-scenario.ts](../src/bin/bir-scenario.ts), [bin/bir.ts](../src/bin/bir.ts) |
411
+ | **R6** | Divergence, source-run fallback, `bir replay` | `diverge()` in [controller.ts](../src/replay/controller.ts), [source-run.ts](../src/replay/source-run.ts) |
412
+ | **R7** | Usage watermark, reporting, ticket, doctor | [control/transcript.ts](../src/control/transcript.ts), [remote-recorder.ts](../src/record/remote-recorder.ts) |
413
+
414
+ ```bash
415
+ npm test # 195 tests, incl. test/replay.test.ts and test/replay-server.test.ts
416
+ npm run build && node test/smoke-replay.mjs
417
+ ```
418
+
419
+ The smoke test is the one worth running after any change to the seams. It starts a stand-in BaseIn
420
+ service, a real `bir-hooks`, a real `bir-proxy` wrapping a real MCP server, and a real `bir-scenario`,
421
+ then drives a two-step scenario end to end through actual stdio — proving that a pinned argument
422
+ reaches the real tool, that step 1's input was computed from step 0's real output, and that the turn
423
+ books `steered_full` with `fallbackCostUsd: 0`.
424
+
425
+ Three things the implementation settled that the design left open:
426
+
427
+ - **`SerializedScenarioStep.recordedOutput` really is never sent.** Recorded outputs come from a lazy
428
+ `GET /recordings/runs/:runId` ([source-run.ts](../src/replay/source-run.ts)), as design §8.1 anticipated.
429
+ - **The work queue must hold work for a proxy that is mid-round-trip.** Failing fast when no poller is
430
+ *parked* lost every step after the first in a multi-step direct plan, because the proxy is between
431
+ polls exactly then. It now fails fast only for a server no proxy has served in the last minute.
432
+ - **The proxies need no replay configuration.** `/proxy/register` tells each one whether to open its
433
+ work loop, so `BIR_REPLAY` is a single switch on the control server.
434
+
435
+ ---
436
+
437
+ ## 11. Turning it off
438
+
439
+ Three levels, least to most.
440
+
441
+ ```bash
442
+ unset BIR_REPLAY # stop arming. Matches still detected; recording still stops on a match
443
+ bir uninstall --replay # remove the `bir` MCP server, restore the prompt-hook timeout
444
+ bir uninstall # remove everything: proxies, hooks, files restored byte-for-byte
445
+ ```
446
+
447
+ With `BIR_REPLAY` unset the system is exactly v1 again: it recognises a repeated prompt, declines to
448
+ record it a second time, and lets the model do the work.