@basein/runner 0.2.7 → 0.2.10

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 (45) hide show
  1. package/README.md +64 -21
  2. package/dist/auth/client.d.ts +40 -1
  3. package/dist/auth/client.js +77 -9
  4. package/dist/bin/bir-hooks.d.ts +18 -3
  5. package/dist/bin/bir-hooks.js +124 -38
  6. package/dist/bin/bir.d.ts +2 -0
  7. package/dist/bin/bir.js +362 -39
  8. package/dist/bin/investigate.js +5 -1
  9. package/dist/bin/setup.d.ts +72 -0
  10. package/dist/bin/setup.js +286 -0
  11. package/dist/config/adapters/claude-code.d.ts +90 -4
  12. package/dist/config/adapters/claude-code.js +164 -16
  13. package/dist/config/generate.d.ts +93 -1
  14. package/dist/config/generate.js +90 -3
  15. package/dist/control/client.d.ts +5 -0
  16. package/dist/control/client.js +8 -0
  17. package/dist/control/daemon.d.ts +116 -0
  18. package/dist/control/daemon.js +339 -0
  19. package/dist/control/discovery.d.ts +26 -0
  20. package/dist/control/discovery.js +41 -9
  21. package/dist/control/ensure-hook.d.ts +39 -0
  22. package/dist/control/ensure-hook.js +98 -0
  23. package/dist/control/paths.d.ts +14 -0
  24. package/dist/control/paths.js +20 -0
  25. package/dist/control/server.d.ts +28 -0
  26. package/dist/control/server.js +22 -6
  27. package/dist/proxy/session.d.ts +8 -1
  28. package/dist/proxy/session.js +28 -6
  29. package/dist/replay/controller.d.ts +24 -1
  30. package/dist/replay/controller.js +76 -20
  31. package/dist/replay/handover.js +5 -0
  32. package/dist/replay/plan.d.ts +2 -0
  33. package/dist/replay/plan.js +53 -6
  34. package/dist/replay/pricing.d.ts +1 -1
  35. package/dist/replay/pricing.js +12 -4
  36. package/dist/replay/tool-error.d.ts +15 -0
  37. package/dist/replay/tool-error.js +17 -0
  38. package/dist/replay/types.d.ts +48 -1
  39. package/docs/calculatedReplayGuide.md +157 -68
  40. package/docs/installRun.md +457 -111
  41. package/docs/loginWeb.md +1 -1
  42. package/docs/quickstart.md +193 -158
  43. package/package.json +2 -1
  44. package/scripts/install.ps1 +669 -0
  45. package/scripts/install.sh +586 -0
@@ -20,4 +20,19 @@
20
20
  * not. `serialized` is the proxy's own serialization — the whole result, as JSON.
21
21
  */
22
22
  export declare function toolResultError(serialized: string): string | undefined;
23
+ /**
24
+ * What a server says when a tool no longer exists — the one failure a grace
25
+ * period cannot cure (plan-services.md D8, kind 2).
26
+ *
27
+ * Read from the text, because the proxy keeps only a JSON-RPC error's message
28
+ * and servers word it differently: the TypeScript SDK answers `Tool X not
29
+ * found`, the Python SDK `Unknown tool: X`, a hand-rolled server whatever it
30
+ * likes. Kept narrow on purpose — `not available` is what a server says during
31
+ * an outage, and a repair in the middle of one is the wrong answer. A miss here
32
+ * costs only the wait; a false hit costs one recording and one calculation,
33
+ * which the service caps.
34
+ */
35
+ export declare const MISSING_TOOL_PATTERN: RegExp;
36
+ /** Whether an error text says the step's tool is gone from its server. */
37
+ export declare function isMissingToolError(text: string | undefined): boolean;
23
38
  //# sourceMappingURL=tool-error.d.ts.map
@@ -35,6 +35,23 @@ export function toolResultError(serialized) {
35
35
  return undefined;
36
36
  return clip(firstText(result.content) ?? "the tool reported an error");
37
37
  }
38
+ /**
39
+ * What a server says when a tool no longer exists — the one failure a grace
40
+ * period cannot cure (plan-services.md D8, kind 2).
41
+ *
42
+ * Read from the text, because the proxy keeps only a JSON-RPC error's message
43
+ * and servers word it differently: the TypeScript SDK answers `Tool X not
44
+ * found`, the Python SDK `Unknown tool: X`, a hand-rolled server whatever it
45
+ * likes. Kept narrow on purpose — `not available` is what a server says during
46
+ * an outage, and a repair in the middle of one is the wrong answer. A miss here
47
+ * costs only the wait; a false hit costs one recording and one calculation,
48
+ * which the service caps.
49
+ */
50
+ export const MISSING_TOOL_PATTERN = /\b(unknown|no such|unsupported|unrecognized) tool\b|\btool\s+['"`]?[\w.:-]+['"`]?\s+(was\s+)?(not found|does not exist|is not registered)\b/i;
51
+ /** Whether an error text says the step's tool is gone from its server. */
52
+ export function isMissingToolError(text) {
53
+ return typeof text === "string" && MISSING_TOOL_PATTERN.test(text);
54
+ }
38
55
  function firstText(content) {
39
56
  if (!Array.isArray(content))
40
57
  return undefined;
@@ -37,6 +37,25 @@ export interface InlinedSegment {
37
37
  }
38
38
  /** Why a call cannot run right now (segmented.md 9.9). */
39
39
  export type CallUnusable = "switched_off" | "not_ready" | "missing" | "stale" | "unresolved" | "depth" | "unsupported";
40
+ /**
41
+ * A reason the plan stops in front of a step, as the service decided it
42
+ * (plan-services.md D8). The service knows the grace period, the retry clock
43
+ * and the calculation's own verdict on the step; the runner only reads it.
44
+ */
45
+ export interface StepStop {
46
+ /**
47
+ * `parked`: the step has failed too often and its retry is not due, or its
48
+ * grace has ended. `nondeterministic`: the calculation could not write code
49
+ * that computes this step's input — a judgement — so the agent makes it.
50
+ */
51
+ kind: "parked" | "nondeterministic";
52
+ /** For `nondeterministic`: which check the step failed at calculation. */
53
+ why?: string;
54
+ /** For `parked` inside its grace: when the step may be tried again. */
55
+ retryAt?: string;
56
+ /** For `parked` past its grace, or whose tool is gone: a repair is due. */
57
+ repairDue?: boolean;
58
+ }
40
59
  export interface SerializedScenarioStep {
41
60
  stepIndex: number;
42
61
  /**
@@ -60,6 +79,15 @@ export interface SerializedScenarioStep {
60
79
  * model in front of it. Absent from an older service, which means never park.
61
80
  */
62
81
  failureCount?: number;
82
+ /**
83
+ * Why the plan must stop in front of this step, decided by the service
84
+ * (plan-services.md D8). Present on every step — `null` when there is no
85
+ * reason — from a service that decides stops itself. A parked step whose
86
+ * retry is due arrives with `stop: null` and is tried. Absent altogether
87
+ * from an older service, where `failureCount` against
88
+ * `fallback.maxStepFailures` is the only rule.
89
+ */
90
+ stop?: StepStop | null;
63
91
  /**
64
92
  * The literal output recorded for this step.
65
93
  *
@@ -117,6 +145,13 @@ export declare function hasTargets(schema: ParamsSchema | null | undefined): boo
117
145
  export interface SerializedScenario {
118
146
  id: string;
119
147
  runId: string;
148
+ /**
149
+ * The run the current chain was calculated from, when it is not `runId`: a
150
+ * plan repaired from a fresh recording (plan-services.md W2.4). A recorded
151
+ * output that stands in for a step comes from this run. Absent from an older
152
+ * service, and from a plan that was never repaired.
153
+ */
154
+ sourceRunId?: string;
120
155
  state: string;
121
156
  intent: string;
122
157
  parameters: unknown;
@@ -159,7 +194,13 @@ export type FallbackKind =
159
194
  * gone, switched off, out of date, or nested too deep (segmented.md
160
195
  * R-CALL-29). Like a known-bad step it counts nothing against any step.
161
196
  */
162
- | "unusable_call";
197
+ | "unusable_call"
198
+ /**
199
+ * The plan stopped in front of a step the calculation marked
200
+ * non-deterministic: its input needs a judgement no code computes
201
+ * (plan-services.md D8, kind 3). Never tried, counts nothing.
202
+ */
203
+ | "nondeterministic_step";
163
204
  /**
164
205
  * Upgrade order. `not_steered` is the floor — the control group, and the safe
165
206
  * answer if a turn dies mid-flight — and each later decision point can only move
@@ -205,6 +246,12 @@ export interface ExecutionStepResult {
205
246
  /** Message only — never a tool payload; this is rendered in a web page. */
206
247
  error?: string;
207
248
  durationMs?: number;
249
+ /**
250
+ * The step's tool no longer exists on its server, read from the error text
251
+ * (plan-services.md D8, kind 2). The service then repairs the plan without
252
+ * waiting out the grace period.
253
+ */
254
+ toolMissing?: boolean;
208
255
  /**
209
256
  * The scenario this step belongs to, when it ran inside a called segment: a
210
257
  * failure is counted where a repair would happen (segmented.md R-CALL-13).
@@ -4,8 +4,8 @@
4
4
  > one says *what to type*, in order, from an empty machine to a prompt that answers itself.
5
5
  >
6
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.
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
9
 
10
10
  ---
11
11
 
@@ -29,23 +29,40 @@ end-to-end smoke test.
29
29
 
30
30
  | # | You need | Check it with |
31
31
  |---|---|---|
32
- | 1 | A BaseIn service you can reach | `curl -fsS "$BIR_AUTH_URL/health"` |
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
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
34
  | 3 | `SIMILARITY_DETECTION_ENABLED=true` on the server (default) | otherwise no prompt ever matches |
35
35
  | 4 | BaseInstRunnerMCP built and installed in your project | `bir status` |
36
- | 5 | Tier 1 — the hooks wired and `bir-hooks` running | `bir doctor` |
36
+ | 5 | Tier 1 — the hooks wired and the recorder running | `bir doctor`; the SessionStart hook starts it |
37
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
38
 
39
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.
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
+ ```
42
50
 
43
51
  ```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 # prints a link and a code; approve in the browser
48
- bir-hooks # leave running in its own terminal
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
49
66
  ```
50
67
 
51
68
  ---
@@ -81,12 +98,17 @@ bir scenario list # or: GET /recordings/runs
81
98
  ```
82
99
 
83
100
  ```
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
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)
88
107
  ```
89
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
+
90
112
  ---
91
113
 
92
114
  ## 3. Calculate the scenario
@@ -100,17 +122,20 @@ bir scenario calc run_5f3a… # POST /recordings/runs/:runId/calculat
100
122
  bir scenario show run_5f3a… # GET /recordings/runs/:runId/scenario (poll)
101
123
  ```
102
124
 
103
- Raw, if you prefer — the token lives in `~/.baseinstrunner/credentials.json`:
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`:
104
127
 
105
128
  ```bash
129
+ BASEIN=$(node -p "require(require('os').homedir()+'/.baseinstrunner/config.json').authUrl")
106
130
  TOKEN=$(node -p "require(require('os').homedir()+'/.baseinstrunner/credentials.json').accessToken")
107
- curl -fsS -X POST "$BIR_AUTH_URL/recordings/runs/run_5f3a…/calculate" \
131
+ curl -fsS -X POST "$BASEIN/recordings/runs/run_5f3a…/calculate" \
108
132
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{}'
109
133
  ```
110
134
 
111
135
  ```powershell
136
+ $b = (Get-Content "$HOME\.baseinstrunner\config.json" | ConvertFrom-Json).authUrl
112
137
  $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" `
138
+ Invoke-RestMethod -Method Post -Uri "$b/recordings/runs/run_5f3a…/calculate" `
114
139
  -Headers @{ authorization = "Bearer $t" } -ContentType 'application/json' -Body '{}'
115
140
  ```
116
141
 
@@ -139,7 +164,7 @@ bir scenario replay scn_9c1b… --prompt "which devices in the fleet look risky
139
164
  params { "fleetId": "eu-west-1", "riskThreshold": 0.8 }
140
165
  step 0 mcp__chrome-devtools__navigate_page { "url": "https://fleet.internal/eu-west-1" }
141
166
  step 1 mcp__chrome-devtools__take_snapshot {}
142
- → emitted: devices[24]
167
+ → emitted: devices
143
168
  step 2 mcp__fleet-api__device_detail { "id": "dev_88f1" }
144
169
  → emitted: highestRiskDeviceId, riskScore
145
170
  response { "fleet": "eu-west-1", "atRisk": 3, "worst": "dev_88f1" }
@@ -151,8 +176,13 @@ Read it for three things:
151
176
  1. **Did the parameters come out right** for a prompt phrased differently from the original?
152
177
  2. **Do the step inputs look computed**, or are they the original run's constants copied through? A
153
178
  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.
179
+ 3. **Is `emitted` non-empty** for steps that later steps depend on? `emitted` lists key names only;
180
+ `→ emitted: (nothing — toolOutputLogic derived no keys)` means every downstream input reading
181
+ that step will be wrong.
182
+
183
+ A `↑ not named by the prompt:` line under `params` lists targets the dry run filled from the
184
+ recording. A live turn finds those in an earlier step, or does not run — so a green trace with
185
+ that line is not yet evidence that a session would steer.
156
186
 
157
187
  A dry replay is recorded as `outcome: 'dry'` and is excluded from savings by construction — it costs
158
188
  you a Haiku call and nothing else.
@@ -175,7 +205,7 @@ you: can you check the eu-west fleet and tell me which boxes are in trouble?
175
205
  That line is the whole feature working up to the point of replay. If it does not appear, the prompt
176
206
  did not clear the server's `SIMILARITY_THRESHOLD` (default 0.9) — see §8.
177
207
 
178
- **With replay off (the default), this is where it stops.** The server keeps its canonical run, bumps
208
+ **With replay off (`bir replay off`), this is where it stops.** The server keeps its canonical run, bumps
179
209
  its `iterations`, this turn is not recorded, and the model answers the ordinary way. Nothing is lost
180
210
  and nothing is skipped.
181
211
 
@@ -198,24 +228,33 @@ When replay arms:
198
228
  That is what makes replay fast, and it is the whole risk. Two things follow:
199
229
 
200
230
  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
231
+ 2. Run `bir replay allow <servers>` (§5.3). An allowlist of the servers you are comfortable having
202
232
  called unattended is cheap insurance, and it is the difference between "replay reads a dashboard"
203
233
  and "replay can do anything any wrapped server can do".
204
234
 
205
235
  ### 5.2 Enable it
206
236
 
237
+ `bir setup` already did this: it installed the scenario server and pre-approved it, so Claude Code
238
+ asks no question about it. By hand:
239
+
207
240
  ```bash
208
- bir install --replay # adds the `bir` MCP server; raises the prompt-hook timeout
209
- unset BIR_REPLAY # on by default; only BIR_REPLAY=0 turns it off
210
- bir-hooks # restart it
241
+ bir install --replay # adds the `bir` MCP server (pre-approved)
242
+ bir up --restart # the recorder reads the change when it restarts
211
243
  ```
212
244
 
213
- `bir install --replay` does exactly two things beyond a normal install, and `bir uninstall` reverses
214
- both:
245
+ A flag-less `bir install` keeps the invocation the project was set up with — `bir setup` installs
246
+ with `--global` from an installed package and `--local` from a checkout — so this downgrades
247
+ nothing. `bir install --replay` does exactly two things beyond a normal install, and
248
+ `bir uninstall --replay` takes the scenario server out again and leaves everything else as it is:
249
+
250
+ - registers a first-party MCP server under the key **`bir`** in the project's `.mcp.json` (never
251
+ `~/.claude.json`), exposing one tool, `mcp__bir__run_scenario` — the channel a fully-wrapped
252
+ scenario's results come back through;
253
+ - pre-approves that server in `.claude/settings.local.json` (`enabledMcpjsonServers`), because a
254
+ "No" to Claude Code's approval dialog leaves a direct replay with nowhere to deliver its results.
215
255
 
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.
256
+ The `UserPromptSubmit` hook's 15 s timeout, which the match round trip needs, is not one of them:
257
+ every install writes it, replay or not.
219
258
 
220
259
  The `bir` tool is visible to the model in every session in this project. That is a real cost — one
221
260
  more tool in the list, a little context per turn — and it is why replay is a separate flag rather
@@ -228,23 +267,48 @@ bir doctor
228
267
  ```
229
268
 
230
269
  ```
231
- replay ON servers=chrome-devtools,fleet-api minSimilarity=0.92
232
- derive=the service (no key needed here)
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)
270
+ Replay : ON servers=chrome-devtools,fleet-api minSimilarity=0.92 source=sidecar
271
+ derive=the service (no key needed here)
272
+ Wrapped in config : chrome-devtools, fleet-api
273
+ Registered proxies: chrome-devtools, fleet-api
274
+ Control server : http://127.0.0.1:53411
275
+ BaseIn (shell) : https://api.bi2202.com (answers /health)
276
+ Recording tier : bound (Tier 1)
277
+ Recording : yes
278
+
279
+ · recording to https://api.bi2202.com as you@example.com
280
+ · the recorder runs in the background (pid 41820); its audit log is ~/.baseinstrunner/logs/<key>.log
281
+ · recurring sub-tasks are observed only — nothing is replaced (BIR_SEGMENT_ARM unset)
282
+
283
+ No problems found.
236
284
  ```
237
285
 
286
+ `BaseIn (shell)` is the address this terminal's `bir` commands would use — `BIR_AUTH_URL` if it
287
+ is set, else the one `bir setup` stored — and `recording to` is the one the running recorder
288
+ uses, with the account it is signed in as. The two normally agree.
289
+
238
290
  ### 5.3 Environment
239
291
 
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.
292
+ The first three switches are stored per project, and that is where to set them:
293
+
294
+ ```bash
295
+ bir replay allow chrome-devtools,fleet-api # the servers direct execution may call; `allow all` clears it
296
+ bir replay off # and back with `bir replay on`
297
+ bir replay status # what is stored for this project
298
+ bir up --restart # a running recorder reads them when it restarts
299
+ ```
300
+
301
+ They are kept in `~/.baseinstrunner/installed.json`. A recorder started by the SessionStart hook has
302
+ Claude Code's environment, not your terminal's, and reads them from there. The environment variables
303
+ below override them wherever the recorder sees them; `bir doctor` says which is in effect
304
+ (`source=sidecar` or `source=env`). The proxies need no replay configuration of their own — each one
305
+ is told whether to open its work loop when it registers, so there is exactly one switch and it cannot
306
+ get out of step with itself.
243
307
 
244
308
  | Variable | Default | What it does |
245
309
  |---|---|---|
246
310
  | `BIR_REPLAY` | *(on)* | `0` disables replay. Unset or any other value keeps it on; nothing below matters while it is `0` |
247
- | `BIR_REPLAY_ALLOW_SERVERS` | *(all wrapped)* | Comma-separated server keys eligible for **direct** execution. **Set this** |
311
+ | `BIR_REPLAY_ALLOW_SERVERS` | *(the stored list, else all wrapped)* | Comma-separated server keys eligible for **direct** execution. Overrides `bir replay allow` |
248
312
  | `BIR_MIN_STEER_SIMILARITY` | `0.92` | Below this a match is detected but not replayed (§8) |
249
313
  | `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 |
250
314
  | `BIR_DERIVE_MODEL` | `claude-haiku-4-5-20251001` | The derivation model, when this machine does the reading |
@@ -254,10 +318,14 @@ there is exactly one switch and it cannot get out of step with itself.
254
318
  | `BIR_STEP_TIMEOUT_MS` | `60000` | Per-step ceiling for one direct `tools/call` |
255
319
  | `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
256
320
 
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.
321
+ **No API key is a supported configuration, not a broken one.** Signed in, the service reads the
322
+ prompt on its own key and the replay is the same replay. Signed out *and* without a key, nothing
323
+ can read the prompt: a *setting* takes the value it had in the recorded run, so a scenario whose
324
+ parameters are all settings — a dashboard sweep, a fixed report — is still a complete, free replay;
325
+ but a scenario with a *target* (the thing the task acts on: "check *eu-west*") is declined rather
326
+ than run on last week's value, and the agent does the task normally. `bir doctor` names the state
327
+ you are in: `derive=this machine (ANTHROPIC_API_KEY)`, `derive=the service (no key needed here)`,
328
+ or `derive=recorded sample values — scenarios with a target will NOT run`.
261
329
 
262
330
  ---
263
331
 
@@ -271,12 +339,12 @@ Ask the matching question again, with replay on.
271
339
  [bir] … run.matched run=run_… matchedRun=run_5f3a… similarity=0.94
272
340
  [bir] … plan.armed scenario=scn_9c1b… mode=direct steps=3
273
341
  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
342
+ [bir] … replay.derived scenario=scn_9c1b… params=2 costUsd=0.0011 source=the service
275
343
  [bir] … replay.step n=0 tool=navigate_page server=chrome-devtools ms=612 ok=true
276
344
  [bir] … replay.step n=1 tool=take_snapshot server=chrome-devtools ms=1104 ok=true emitted=devices
277
345
  [bir] … replay.step n=2 tool=device_detail server=fleet-api ms=210 ok=true emitted=highestRiskDeviceId,riskScore
278
346
  [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
347
+ [bir] … execution.reported scenario=scn_9c1b… outcome=steered_full derive=0.0011 session=0.0083 fallback=0.0000 savedUsd=0.114
280
348
  ```
281
349
 
282
350
  In the session you will see one tool call, `mcp__bir__run_scenario`, returning the composed results.
@@ -284,31 +352,35 @@ The model reads them and answers. It never emitted the three underlying calls.
284
352
 
285
353
  ### 6.2 A mixed scenario — steer
286
354
 
287
- The model emits each call itself; `bir` overrides the arguments.
355
+ The model emits each call itself; `bir` overrides the arguments. The per-step `replay.pin` and
356
+ `replay.thread` lines are detail lines, printed only under `BIR_VERBOSE=1`; without it a steer run
357
+ shows `plan.armed`, `replay.done`, and any `replay.thread_fallback` between them.
288
358
 
289
359
  ```
290
360
  [bir] … plan.armed scenario=scn_44d0… mode=steer steps=4
291
361
  [bir] … replay.pin n=0 tool=mcp__github__list_pull_requests
292
- [bir] … replay.thread n=0 via=proxy emitted=prNumbers
362
+ [bir] … replay.thread n=0 via=proxy emitted=prNumbers done=false
293
363
  [bir] … replay.pin n=1 tool=Read
294
- [bir] … replay.thread n=1 via=hook emitted=reviewers
364
+ [bir] … replay.thread n=1 via=hook emitted=reviewers done=false
295
365
  [bir] … replay.done scenario=scn_44d0… mode=steer steps=4/4 outcome=steered_full
296
366
  ```
297
367
 
298
368
  `via=proxy` versus `via=hook` matters: a wrapped MCP step's output is threaded from the **proxy's**
299
369
  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.
370
+ `replay.thread_fallback run=… tool=… why="no proxy report — threading the hook's view; shapes may differ"`
371
+ line — printed whether or not you are verbose — means the proxy did not report within its grace
372
+ window and the hook's differently-shaped view was used instead. Expect derived values to be empty,
373
+ and treat repeats as a bug.
302
374
 
303
375
  ### 6.3 The model goes off-script
304
376
 
305
377
  ```
306
378
  [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
379
+ [bir] … replay.compose scenario=scn_44d0… remaining=2 executed=1 recorded=1 skipped=0 bytes=8412 costUsd=0.00
380
+ [bir] … replay.done scenario=scn_44d0… mode=steer steps=2/4 outcome=diverged
309
381
  ```
310
382
 
311
- `costUsd=0` on the compose line is the point of the whole design: the remaining steps ran through
383
+ `costUsd=0.00` on the compose line is the point of the whole design: the remaining steps ran through
312
384
  proxies that were already connected, so recovering from a divergence cost no tokens at all.
313
385
 
314
386
  ### 6.4 The audit vocabulary
@@ -321,13 +393,16 @@ Everything replay decides is one line. `grep` for these:
321
393
  | `replay.decision … verdict=no-steer` | Matched, and declined. `why=` says which gate |
322
394
  | `plan.armed` | A plan exists. Carries mode, step count and the ordered tool names — **before** anything runs |
323
395
  | `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 |
396
+ | `replay.pin` / `replay.thread` | One steered step's input pinned / output threaded — detail lines, only under `BIR_VERBOSE=1` |
397
+ | `replay.thread_fallback` | A wrapped step's output was threaded from the hook's view because the proxy did not report in time |
325
398
  | `replay.step` | One direct step executed |
326
399
  | `replay.diverge` / `replay.compose` | Off-script, and the recovery |
327
400
  | `replay.done` | Final outcome, steps completed, elapsed |
328
401
  | `execution.reported` | The ticket was redeemed and the saving booked |
329
402
 
330
- Keep them: `bir-hooks 2>&1 | tee -a ~/.baseinstrunner/audit.log`.
403
+ A recorder started in the background — by the SessionStart hook, `bir up` or `bir setup` — keeps
404
+ them in `~/.baseinstrunner/logs/<key>.log`; `bir status` names the file. One started in a terminal
405
+ writes them to that terminal; keep them yourself: `bir-hooks 2>&1 | tee -a ~/.baseinstrunner/audit.log`.
331
406
 
332
407
  ---
333
408
 
@@ -381,17 +456,22 @@ first prompt that states the task plainly.
381
456
  | Symptom | Cause | Fix |
382
457
  |---|---|---|
383
458
  | 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=0` · scenario not `ready` · similarity below threshold · no step is executable |
459
+ | `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 |
385
460
  | `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
461
  | `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
462
  | 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 |
463
+ | 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 |
464
+ | `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 |
390
465
  | `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 |
466
+ | `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 |
392
467
  | `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 |
468
+ | 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) | Nothing on the runner. The agent makes that choice and finishes the task; a model step for such judgements is planned (W3.1). The journal's `plan.armed` line lists it under `stops=` |
469
+ | 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` |
393
470
  | `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
471
  | 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 |
472
+ | `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 |
473
+ | `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 |
474
+ | 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` |
395
475
 
396
476
  ---
397
477
 
@@ -399,8 +479,9 @@ first prompt that states the task plainly.
399
479
 
400
480
  Every audit line that explains a turn is also kept in a **journal**, one JSON
401
481
  object per line, under `~/.baseinstrunner/control/journal/` (one file per
402
- directory, rotated at 4 MB). `bir-hooks` prints its path at start. Ask the
403
- journal and the service together:
482
+ directory, rotated at 4 MB). The recorder names it at start — the `journal=`
483
+ field of its `control.listening` line, in the audit log for a background
484
+ recorder. Ask the journal and the service together:
404
485
 
405
486
  ```
406
487
  bir investigate # the newest turn in this directory
@@ -444,8 +525,8 @@ tested; everything this guide describes is code you can run.
444
525
  | **R7** | Usage watermark, reporting, ticket, doctor | [control/transcript.ts](../src/control/transcript.ts), [remote-recorder.ts](../src/record/remote-recorder.ts) |
445
526
 
446
527
  ```bash
447
- npm test # 195 tests, incl. test/replay.test.ts and test/replay-server.test.ts
448
- npm run build && node test/smoke-replay.mjs
528
+ npm test # ~400 tests, incl. test/replay.test.ts and test/replay-server.test.ts
529
+ npm run build && npm run pretest && npm run test:smoke # the smoke test needs the test build too
449
530
  ```
450
531
 
451
532
  The smoke test is the one worth running after any change to the seams. It starts a stand-in BaseIn
@@ -471,10 +552,18 @@ Three things the implementation settled that the design left open:
471
552
  Three levels, least to most.
472
553
 
473
554
  ```bash
474
- export BIR_REPLAY=0 # stop arming. Matches still detected; recording still stops on a match
475
- bir uninstall --replay # remove the `bir` MCP server, restore the prompt-hook timeout
476
- bir uninstall # remove everything: proxies, hooks, files restored byte-for-byte
555
+ bir replay off # stop arming. Matches still detected; recording still stops on a match.
556
+ # Stored for this project; `bir up --restart` applies it, `bir replay on` undoes it
557
+ export BIR_REPLAY=0 # the same, wherever the recorder is started from (the SessionStart hook
558
+ # gives it Claude Code's environment, not your terminal's)
559
+ bir uninstall --replay # remove the `bir` MCP server only; proxies, hooks and the prompt-hook timeout stay
560
+ bir uninstall # remove everything: proxies, hooks, the pre-approval
477
561
  ```
478
562
 
479
- With `BIR_REPLAY=0` the system is exactly v1 again: it recognises a repeated prompt, declines to
563
+ `bir uninstall` restores a file byte-for-byte when nothing else edited it since the install, and
564
+ repairs it entry by entry otherwise. A file the install created — usually
565
+ `.claude/settings.local.json` — stays behind with our entries removed, and the line that hid it in
566
+ `.git/info/exclude` goes with them.
567
+
568
+ With replay off the system is exactly v1 again: it recognises a repeated prompt, declines to
480
569
  record it a second time, and lets the model do the work.