@basein/runner 0.3.9 → 0.3.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 (50) hide show
  1. package/README.md +46 -5
  2. package/dist/auth/client.d.ts +15 -0
  3. package/dist/auth/client.js +34 -12
  4. package/dist/bin/bir-hooks.d.ts +1 -1
  5. package/dist/bin/bir-hooks.js +1 -1
  6. package/dist/bin/bir.d.ts +2 -0
  7. package/dist/bin/bir.js +60 -6
  8. package/dist/bin/investigate.d.ts +5 -0
  9. package/dist/bin/investigate.js +117 -15
  10. package/dist/bin/setup.d.ts +12 -0
  11. package/dist/bin/setup.js +185 -56
  12. package/dist/bin/update.d.ts +81 -0
  13. package/dist/bin/update.js +194 -0
  14. package/dist/config/adapters/claude-code.d.ts +17 -0
  15. package/dist/config/adapters/claude-code.js +28 -1
  16. package/dist/config/generate.d.ts +14 -0
  17. package/dist/control/daemon.d.ts +26 -1
  18. package/dist/control/daemon.js +53 -3
  19. package/dist/control/pin-guard.d.ts +6 -1
  20. package/dist/control/pin-guard.js +15 -1
  21. package/dist/control/recorder-keeper.js +2 -1
  22. package/dist/control/server.d.ts +200 -2
  23. package/dist/control/server.js +741 -48
  24. package/dist/control/transcript.d.ts +4 -0
  25. package/dist/control/transcript.js +4 -0
  26. package/dist/proxy/session.js +1 -0
  27. package/dist/record/housekeeping.d.ts +7 -1
  28. package/dist/record/housekeeping.js +11 -3
  29. package/dist/record/recorder.d.ts +34 -0
  30. package/dist/record/remote-recorder.d.ts +6 -0
  31. package/dist/record/remote-recorder.js +31 -4
  32. package/dist/record/switchable-recorder.js +1 -0
  33. package/dist/replay/controller.d.ts +132 -3
  34. package/dist/replay/controller.js +248 -24
  35. package/dist/replay/derive.js +2 -0
  36. package/dist/replay/handover.d.ts +13 -1
  37. package/dist/replay/handover.js +19 -0
  38. package/dist/replay/safety.d.ts +15 -4
  39. package/dist/replay/safety.js +19 -6
  40. package/dist/replay/source-run.js +2 -1
  41. package/dist/util/service-headers.d.ts +24 -0
  42. package/dist/util/service-headers.js +47 -0
  43. package/docs/BaseInstRunner.md +115 -1
  44. package/docs/calculatedReplay.md +209 -32
  45. package/docs/calculatedReplayGuide.md +133 -12
  46. package/docs/installRun.md +91 -10
  47. package/docs/quickstart.md +64 -9
  48. package/package.json +1 -1
  49. package/production.md +81 -17
  50. package/scripts/install.ps1 +25 -1
package/production.md CHANGED
@@ -55,6 +55,7 @@ In practice: the runner must never be the reason a Claude Code session is slow,
55
55
  - **Stale local copies.** A local-scope copy goes stale after `.mcp.json` is edited, until `bir setup` runs. `bir doctor` flags it.
56
56
  - **Corporate proxies.** Proxy support depends on the Node version and on NO_PROXY exempting 127.0.0.1.
57
57
  - **Unparseable stdio answers.** An unparseable stdout answer line from a stdio server still hangs that one call, as it would unwrapped.
58
+ - **A host other than Claude Code.** A LangChain application (§3.2) was run on a bench only: no real model and no real service yet. The service's part is built and not deployed, and the Python package is not on PyPI.
58
59
 
59
60
  ### 2.5 Must happen before release (details in §10)
60
61
 
@@ -64,6 +65,7 @@ In practice: the runner must never be the reason a Claude Code session is slow,
64
65
  - The service's served install scripts updated.
65
66
  - The manual end-to-end checks with the real Claude Code.
66
67
  - The service team told about the id-deduplication and "no scenarios" asks.
68
+ - The checks for a host other than Claude Code (§3.2): the service's part, the Python package, and a run with a real model.
67
69
 
68
70
  ---
69
71
 
@@ -120,8 +122,8 @@ In practice: the runner must never be the reason a Claude Code session is slow,
120
122
  |---|---|---|---|---|---|
121
123
  | SessionStart: command hook `bir-hooks ensure` | the payload on stdin (≤ 2 s); finding or starting the recorder (local only, never the service); relaying the payload (≤ 2.5 s); a read-only stale-copy check (only with ≥ 3 s left) | warm about 0.1 s (Node start-up); cold about 0.7 s on Windows, once per project per boot | 11.5 s (11 s budget plus a 0.5 s hard exit). npx installs pass `--budget-ms 8000`, after npx's own start-up | `/session/start`: 2 s | 15 s (30) |
122
124
  | SessionStart: HTTP form (older installs only; `bir install` now always writes the command hook, and `bir setup` replaces the HTTP one) | nothing | milliseconds | 2 s | 2 s | 5 s (10) |
123
- | UserPromptSubmit | replay on: the similar-prompt match (≤ 3 s; skipped while the breaker is open or the match gate holds). For a matched scenario: arming, derivation (≤ 4 s) and the directive's argument preview (≤ 1.2 s), all inside 6 s. Replay off: nothing | about 17 ms with an instant local service; plus one service round trip (about 0.3-0.5 s) with replay on | 6 s, only for a matched scenario that must derive; 3 s on the first prompt of a hang | 7 s | 10 s (15) |
124
- | PreToolUse | nothing, unless a replay plan steers this turn. Then: in-hook steps (≤ 9 s, `BIR_PRETOOL_REPLAY_BUDGET_MS`), derivation at the first call (≤ 4 s), the previous pinned step (≤ 1.75 s), and after a hand-over the new fragment run's create (≤ 0.3 s, breaker-gated). Intent arming only when opted in (≤ 0.5 s probe + 1.5 s arming) | 2-5 ms | 12 s, replay only | 12 s | 15 s (30) |
125
+ | UserPromptSubmit | replay on: the similar-prompt match (≤ 3 s; skipped while the breaker is open or the match gate holds). For a matched scenario: arming, derivation (budget 8 s, cut short by this hook's 6 s) and the directive's argument preview (≤ 1.2 s), all inside 6 s. Replay off: nothing | about 17 ms with an instant local service; plus one service round trip (about 0.3-0.5 s) with replay on | 6 s, only for a matched scenario that must derive; 3 s on the first prompt of a hang | 7 s | 10 s (15) |
126
+ | PreToolUse | nothing, unless a replay plan steers this turn. Then: in-hook steps (≤ 9 s, `BIR_PRETOOL_REPLAY_BUDGET_MS`), derivation at the first call (≤ 8 s), the previous pinned step (≤ 1.75 s), and after a hand-over the new fragment run's create (≤ 0.3 s, breaker-gated). Intent arming only when opted in (≤ 0.5 s probe + 1.5 s arming) | 2-5 ms | 12 s, replay only | 12 s | 15 s (30) |
125
127
  | PostToolUse, PostToolUseFailure | nothing: the proxy's report is joined in the background | 1-2 ms | 2 s | 2 s | 5 s (30) |
126
128
  | SubagentStart, SubagentStop | nothing | not measured (bookkeeping only) | 2 s | 2 s | 3 s (5) |
127
129
  | Stop, StopFailure | nothing: seals the run, and the upload drains in the background | 2-3 ms | 2 s | 2 s | 5 s (10) |
@@ -129,6 +131,14 @@ In practice: the runner must never be the reason a Claude Code session is slow,
129
131
 
130
132
  Hard deadlines are in `HOOK_DEADLINE_MS` (`src/control/server.ts`). The timeouts are in `HOOK_ROUTES` and `ENSURE_TIMEOUT_S` (`src/config/adapters/claude-code.ts`). Re-running `bir setup` rewrites the timeouts of an existing install.
131
133
 
134
+ **A host that is not Claude Code.** The same routes also serve a LangChain application: a Python program whose agent is built with LangChain's `create_agent` and the package `langchain-basic2202`. The package (the adapter) is a LangChain middleware that speaks the recorder's local protocol. Its folder is set up with `bir setup --host langchain`.
135
+
136
+ - **What setup does.** It signs in, gives the folder an entry of its own in `installed.json` (a fixed port, a token and `hosts: ["langchain"]`), stores replay on (off with `--no-replay`), starts the recorder in the background and prints the two lines of Python to add to the agent.
137
+ - **What it leaves alone.** It writes nothing under `.claude/` and nothing into `~/.claude.json`: no hooks, no wrapped servers, no `bir` MCP server. A folder that is already set up for Claude Code keeps that set-up as it is, and only its entry notes the host.
138
+ - **Deadlines.** The server deadlines above are set per route, so they hold for the adapter's requests too: `/session/prompt` 7 s, `/tool/pre` 12 s, `/tool/post` and `/session/stop` 2 s. The timeouts `bir install` writes are Claude Code's own.
139
+
140
+ The fields the adapter adds to each route, and what the recorder does for such a session, are in `docs/BaseInstRunner.md`, Phase 5. The how-to is `langChainGuide.md` in the repository's root; it is not in the npm package. The runner's part is all in this package. The Python package is not on PyPI, and the service's part is built and not deployed yet.
141
+
132
142
  ### 3.3 Failure modes
133
143
 
134
144
  | Condition | What the user sees | What the runner does | Where it shows |
@@ -136,6 +146,7 @@ Hard deadlines are in `HOOK_DEADLINE_MS` (`src/control/server.ts`). The timeouts
136
146
  | Service unreachable, 5xx, 429 or an HTML page (captive portal, WAF, CDN) | Nothing. The first prompt of the outage takes 12-26 ms. | The first failure opens the breaker, and hooks stop waiting. Steps queue in the background: at most 500 sends or 32 MiB, oldest dropped. A send still facing a cool-down after 60 s is dropped unsent. The runner retries once, only for 429/503 or a connection that never reached the service. Recording resumes by itself. | `bir status` "Service : not answering"; doctor warning; `/health.service`, `/health.sending`; `service.degraded` / `service.recovered`, `recorder.backlog_full`, `recorder.run_incomplete`, `recorder.run_abandoned reason=service_down` |
137
147
  | Service hung (accepts, never answers) | One prompt per outage waits 3 s. Nothing else waits. | The match overrun marks the service slow, which opens the breaker and the match gate. Background requests time out at 10 s. | `/health.service`, `/health.matchGate`; `replay.decision code=match_timeout`, `replay.match_gate state=holding` |
138
148
  | Service slow but answering (match over 2 s) | One prompt waits about the service's time, then none for 5 min. The hold doubles to 30 min while the service stays slow. | Match gate: prompts stop waiting for the match. It is still asked in the background, so a hit still stops that turn's duplicate recording. Nothing is steered meanwhile. A fast match ends the hold. | `/health.matchGate {holding, holdMs, until}`; `replay.match_gate`; `replay.decision code=match_slow` (verbose) |
149
+ | The account is over one of the service's limits: `429` with `Retry-After` (the limits are built and not deployed yet) | Nothing. The turn runs as an ordinary one. | A create, a step or a report is sent once more, at most 2 s later, then dropped. A dropped step is a hole in its recording. A run whose create got a 429 runs without its plan. A 429 that the second try did not cure counts as the service being unwell: the breaker opens, and for 5 to 60 s no prompt of that recorder waits for a match. The defaults per account, each a setting of the service: 120 runs a minute; 3,000 steps a minute (a tool call is two steps); 10 title requests a minute; 20 requests in any one hour for Calculate, Prefer and Recalculate together. Intent probes have a limit too, 1,500 a minute per account: over it a probe is answered `200` with no match, never `429`. | `recorder.send_failed` with `HTTP 429`; `service.degraded`; `/health.service`, `/health.sending` |
139
150
  | Sign-in refused (the service's own JSON 400/401 to a refresh) | Nothing | `credentials.json` is re-read once. Recording then pauses (`auth_rejected`) and is retried in the background; `bir login` resumes it at once. An HTML or non-JSON 4xx counts as "unreachable", never as a sign-out. | `/health.recording=false`, `/health.sending.state`; doctor warning "nowhere to send steps"; `auth.refresh_refused`, `recorder.paused` |
140
151
  | Signed out, or no service configured | Nothing. Hooks take about 1-3 ms. | No network calls. After `bir login` or `bir setup`, the running recorder records from the next prompt with no restart. After `bir logout`, it stops sending within about 2 s. | `/health.recording=false`; doctor warning; `recorder.signed_out`, `recorder.enabled` |
141
152
  | Recorder killed mid-session | "hook error" lines on prompts and tool calls; the session keeps working | Once the pid is dead and the port refuses connections, the session's bound proxies and `bir-scenario` launch `bir-hooks revive` (detached). This never happens after `bir down`, and not in `bir-scenario`'s first 12 s. The next SessionStart (`/clear`, a new session) also starts the recorder. | doctor; `proxy.recorder_revive`, `scenario.recorder_revive` (MCP log); `daemon.revived` (recorder log) |
@@ -151,6 +162,8 @@ Hard deadlines are in `HOOK_DEADLINE_MS` (`src/control/server.ts`). The timeouts
151
162
  | Windows | A cold SessionStart costs about 0.7 s, once per project per boot; no console windows | See the Windows notes after this table. | none |
152
163
  | Project port held by another program or reserved by the system | Hook errors until the port changes | The recorder records the holder in `control/<key>.port.json` with the fix (`bir setup --port <n>`, ideally below 49152), and new sessions use the new port. | doctor PROBLEM, `bir status` "Port :", `control.port_busy` |
153
164
  | A replay step fails, times out, or its server refuses it | The model reads a `[BaseInstRunner calculated replay]` hand-over note and continues the task itself | The plan stops at that step and names the steps that ran. A timed-out step "may or may not have completed". Nothing after it runs. | `replay.handover`; `bir investigate` |
165
+ | A session of another host (§3.2): a person says no to a plan's step, answers in the tool's place or changes the call, or a call limit blocks the call | The model takes over, with the hand-over note | The plan stops at that step. The step's verdict is `skipped`, not `failed`, and nothing is counted against the plan. After a refusal the report is `measured: false`; after a changed call the cost stays measured. Neither turn is a sample of the usual cost (`baselineEligible: false`). | `replay.step_host_stopped`, `replay.handover`; `bir investigate` |
166
+ | A session of another host: its run fails, goes quiet for 2 h, or is closed with its plan still live | Nothing from the recorder | A failed run is finished with the failure's text as its `error` (cleaned of secrets, cut) and no final answer; one sealed after 2 h of silence, with `error: "turn ended early: idle"`. A plan that is still live is handed over first, so its report says `fell_back` (a step ran) or `failed` (none ran), with `measured: false` and `baselineEligible: false`. A recording that its host calls not whole is finished with the `incomplete` mark. | `run.host_failed`, `run.idle_sealed`, `run.incomplete`, `run.finish … ended=<reason>`; `bir investigate` |
154
167
  | `bir` scenario server not connected | Nothing | Direct plans are declined (`no_delivery_tool`). A direct plan whose first model call is not `run_scenario` is retired; nothing is executed. | `/health.deliveryTool`, `/health.scenarioServers` |
155
168
  | `.mcp.json` edited after install | The server keeps its old configuration until `bir setup`, because the local copy outranks `.mcp.json` | SessionStart logs `overrides.stale` and does not rewrite anything. | doctor PROBLEM; `bir status` STALE |
156
169
 
@@ -390,13 +403,14 @@ Over 2-minute runs with one turn every 8 s (92 hooks):
390
403
  - **Direct plans.** A direct plan (run through `run_scenario`) is announced only while a live `bir-scenario` has sent `/scenario/hello` within 30 minutes. A direct plan whose first model call is not `run_scenario` is retired, and nothing is executed.
391
404
  - **`/scenario/run`.** It runs only the calling session's plan, bound by `toolUseId` or `sessionId`. If it cannot tell whose it is, it runs nothing (`replay.run_ambiguous`). A second concurrent run is refused, and Esc aborts the run.
392
405
  - **Plan mode.** Plan mode gets no directive, no steering and no `allow`.
406
+ - **Another host (§3.2).** The runner runs no step itself there. The plan's mode is always `steer`: the adapter asks for each step's arguments, and the host runs its own tools. A call the model makes itself is never given the plan's arguments, and a plan that is live then is handed over. Tool names carry no Claude Code meaning: `Bash`, `Read`, `TodoWrite` or `ToolSearch` are the application's own tools there, the pin guard's rules by tool name do not apply, and nothing is skipped as housekeeping except the `bir` server's own tools.
393
407
 
394
408
  **Bounded state.**
395
409
 
396
410
  - **Transcript.** The transcript is read incrementally, only the bytes appended since the last hook. Retention is bounded: 128 messages of up to 16K characters each for text and thinking, 4,096 tool-use ids, and 32 tool results of up to 8K characters.
397
411
  - **Sessions.** A session is "active" when it has an open run and was heard from in the last 15 min.
398
412
  - Sessions are forgotten after 6 h idle, 10 min after SessionEnd, or beyond 500.
399
- - Open runs idle for 2 h are sealed.
413
+ - Open runs idle for 2 h are sealed. In a session of another host (§3.2), such a run is finished with `error: "turn ended early: idle"` and no final answer.
400
414
  - `/health` lists at most 50 sessions.
401
415
  - **`BIR_OFF=1` sessions.** A session started with `BIR_OFF=1` is kept off for up to 7 days of silence, at most 2,000 of them.
402
416
 
@@ -418,6 +432,7 @@ Over 2-minute runs with one turn every 8 s (92 hooks):
418
432
 
419
433
  - The recorder runs in `~/.baseinstrunner`; the project is passed as `BIR_DAEMON_CWD`. The project folder can therefore be renamed, deleted or removed as a git worktree while the recorder runs.
420
434
  - The recorder never inherits `BIR_OFF`, the setup token, or `ANTHROPIC_API_KEY` (unless `BIR_USE_ANTHROPIC_API_KEY=1`).
435
+ - **The small environment.** A recorder started for a folder that is set up for another host and not for Claude Code (§3.2) does not get the whole environment of the window that started it. It keeps only the names listed in §9, and every `BIR_*` variable. Step code runs inside the recorder with no sandbox, so a model key or a database password of the application must not reach it. `bir setup --host langchain` and `bir up` both start the recorder this way in such a folder. A folder that is set up for Claude Code too keeps the whole environment, because Claude Code's own steps may need it. Nothing changes for a Claude Code project.
421
436
 
422
437
  **Finding and replacing a recorder (`ensureDaemon`).**
423
438
 
@@ -508,7 +523,7 @@ Over 2-minute runs with one turn every 8 s (92 hooks):
508
523
 
509
524
  - **Backlog.** The backlog is bounded: 500 sends or 32 MiB, oldest steps dropped first. A queued send still facing a cool-down after 60 s is dropped unsent, not sent into the outage.
510
525
  - **Flush.** A flush waits at most 2 s (`BIR_FLUSH_TIMEOUT_MS`), and not at all while the service is cooling down.
511
- - **Retries.** A retry happens once, only after 429, 503, or a connection error that proves nothing reached the service. A 409 on the retry counts as "already landed". There is no retry after 502, 504 or a mid-request reset, until the service confirms it deduplicates ids (§10).
526
+ - **Retries.** A retry happens once, only after 429, 503, or a connection error that proves nothing reached the service. A 409 on the retry counts as "already landed". There is no retry after 502, 504 or a mid-request reset, until the service confirms it deduplicates ids (§10). What a 429 costs is in §3.3. The service's new build answers a report whose write failed with `503 report_not_booked`, so this one retry sends the report once more (built, not deployed yet).
512
527
 
513
528
  **Size caps on uploads.**
514
529
 
@@ -518,10 +533,16 @@ Over 2-minute runs with one turn every 8 s (92 hooks):
518
533
  - Non-JSON tool errors: 8 KiB.
519
534
  - Report errors: 2 KiB.
520
535
  - Tool payloads: 64 KiB.
521
- - A replayed step's input and output, in the execution report: 3,800 bytes each, redacted like a recorded step's. The whole report stays under 96 KiB, because the service reads that route with its 100 kB parser: a payload that does not fit is left out, and its step keeps its verdict.
536
+ - A replayed step's input and output, in the execution report: 3,800 bytes each, redacted like a recorded step's. The whole report stays under 96 KiB, because the deployed service reads that route with its 100 kB parser: a payload that does not fit is left out, and its step keeps its verdict. The service's new build accepts 1 MB on that route and on `/scenarios/:id/derive` (built, not deployed yet). The runner keeps its limit all the same, because older services have the 100 kB one.
522
537
 
523
538
  Truncation is always marked with the original size.
524
539
 
540
+ **What the service does with a report.** The service's new build refuses a report less easily for its size or its age (built, not deployed yet):
541
+
542
+ - The prompt inside a report is cut at 10,000 characters. It used to be refused with 400.
543
+ - The prompt for reading new values is cut at 20,000 characters, and the last five tool results are kept.
544
+ - A report may arrive for 24 hours after its plan was handed out. It was one hour, and after it the answer was 409.
545
+
525
546
  **Redaction.**
526
547
 
527
548
  - **Key matching.** Keys are matched on whole name segments, with 0.2.12's coverage restored for plurals, trailing `auth`, `oauth` and `auth_code`. Ordinary arguments such as `author`, `max_tokens` and `nextPageToken` survive.
@@ -569,10 +590,18 @@ Truncation is always marked with the original size.
569
590
  - Timeouts, abandoned calls and shutdowns are worded "may or may not have completed". A step whose output logic failed says "do not run it again".
570
591
  - A cancelled direct run's note is delivered on the session's next prompt or tool call.
571
592
  - An Esc-interrupted turn is never booked as a saving (`measured:false`).
593
+ - A step that a host other than Claude Code (§3.2) stopped itself has two stop reasons of its own. `host_rejected`: a person said no, or answered in the tool's place, or a call limit blocked the call. The note says "Step N of M (tool) was not run by the host. The result above was written by the host, not by the tool. Do not run the step again unless the user asks." `host_edited`: a person changed the call. The note says "Step N of M (tool) ran with arguments a person chose, not the scenario's. Its result above is real and its side effects are in place. Do not run it again." The report's `fallbackKind` stays `step_failed`.
594
+
595
+ **Measured cost.**
596
+
597
+ - A turn's cost is measured only when tokens were found and every model of the turn has a price in the runner's table. This holds for every host, Claude Code too. A model outside the table used to cost $0, and that zero was called measured.
598
+ - A `run.unmeasured` line gives the reason (`why`), and the models (`models`) when one has no price. No price table and no `PRICING_VERSION` changed.
599
+ - In a session of another host the cost comes from the `usage` its adapter sends with the stop, not from a transcript. It is measured only when at least one entry has tokens and every model in the list has a price. With more than one plan in one run, none is measured.
600
+ - The service's new build holds the same line (built, not deployed yet): a report that says `measured: true` with a session cost of $0 is stored as not measured, and a finish with a cost of 0 stores no cost.
572
601
 
573
602
  **Budgets.**
574
603
 
575
- - The hook derivation wait is 4 s. All replay work in one PreToolUse is capped at 9 s (maximum 10 s).
604
+ - The hook derivation wait is 8 s (the prompt hook stops at its own 6 s). All replay work in one PreToolUse is capped at 9 s (maximum 10 s).
576
605
  - The direct plan budget is 100 s (was 120 s). Direct runs are clamped so that derivation, plan and slack stay within 270 s, under `run_scenario`'s 290 s client timeout.
577
606
  - The worst case by budget is 120 s: 8 s of derivation, 100 s of plan, and 12 s of slack.
578
607
 
@@ -587,6 +616,7 @@ Truncation is always marked with the original size.
587
616
  - It must also keep the sample's shape: numbers stay numbers, nothing starts with `-`, and no new whitespace, glob, brace or parenthesis appears.
588
617
  - File-write-only plans reject only line breaks and NUL.
589
618
  - Plans with a command sink do not feed recent tool results to derivation.
619
+ - In a session of another host (§3.2) the tool names are the application's own. The sinks there are the tools the host declared in `sinks`, and any tool whose plain name has a command word such as `sql`, `query`, `exec`, `shell`, `command`, `script` or `eval`. `Bash`, `Edit` and an `mcp__` prefix mean nothing by themselves.
590
620
 
591
621
  **Steer mode** pins one call at a time. A call really running beside a pin hands the plan over, booked `fell_back` or `failed`. A failed pinned step (PostToolUseFailure, or a JSON-RPC error) hands over; nothing after it is approved.
592
622
 
@@ -626,6 +656,7 @@ Truncation is always marked with the original size.
626
656
  - `bir uninstall` touches only this project. It repairs `~/.claude.json` entry by entry, never restores a stale snapshot, and stops the project's recorder. If a session still uses the recorder, it leaves it running with a per-project off switch.
627
657
  - It exits 1 with "Not everything could be undone" if anything still runs through the runner.
628
658
  - `--keep-recorder` leaves the recorder alone. `bir uninstall --all` handles every project, every recorder and the user scope; with the install scripts, `BIR_UNINSTALL=1` does the same.
659
+ - In a folder that is set up for another host (§3.2), `bir uninstall` removes the folder's entry and stops its recorder. It prints `← this folder's entry for langchain removed (its port, token and replay switch)`.
629
660
 
630
661
  **Commands.**
631
662
 
@@ -633,6 +664,10 @@ Truncation is always marked with the original size.
633
664
  - **`bir install` without `--replay`** stores replay off for the project unless the user said otherwise. `bir setup` keeps it on.
634
665
  - **`bir login` on Windows** opens the browser without cmd.exe, and only for https links.
635
666
  - **install.ps1** adds `--use-system-ca` only when the Node accepts it.
667
+ - **`bir setup --host langchain`** sets a folder up for a LangChain application and wires nothing of Claude Code (§3.2). In such a folder `bir status` and `bir doctor` print a `Host` line (§8.2), and `bir replay on` and `bir replay off` work as anywhere. Its errors:
668
+ - a host the runner does not know: `--host <x>: not a host this runner knows. Known: langchain.`, exit 1;
669
+ - a machine that was never told the service's address: `No service address. Pass --auth-url https://api.your-service, or set BIR_AUTH_URL.` The fix is `bir setup --host langchain --auth-url https://api.bi2202.com`;
670
+ - the released 0.3.9, which does not know `--host`: it prints its usage and exits 2. It does not fall back to a Claude Code set-up.
636
671
 
637
672
  ### 6.8 The `bir` MCP server (`src/bin/bir-scenario.ts`, `scenario-liveness.ts`, `service-read.ts`)
638
673
 
@@ -695,6 +730,7 @@ Truncation is always marked with the original size.
695
730
  | Answer caps are 128 MiB, both stdio frames and remote answers (stdio was 32 MiB). | A 33 MB result used to hang the call. | `BIR_UPSTREAM_MAX_MESSAGE_BYTES` for remote answers. |
696
731
  | Remote credentials travel in the entry's env (`BIR_UPSTREAM_HEADERS`), not in `--header` arguments. | Command lines are visible in process listings. | Old `--header` entries still work until the next `bir setup` rewrites them. |
697
732
  | Hook routes answer `200 {}` to stale tokens, unknown routes, non-POST requests, oversize bodies and malformed bodies (was 401, 404 or a reset). | Each of these was a hook error on every prompt and tool call until the recorder restarted. | None. |
733
+ | A turn whose model has no price in the runner's table is reported not measured, for every host. It used to cost $0, and that zero was called measured. | A zero that is called measured is booked as a saving nobody made. | None. `run.unmeasured` names the model; update the runner when it has no price for it. |
698
734
 
699
735
  ### 7.2 Upgrading from 0.2.12
700
736
 
@@ -803,7 +839,7 @@ Truncation is always marked with the original size.
803
839
  - **Notes** are facts:
804
840
  - hook calls a deadline guard answered early (`/health.hooks.overran`);
805
841
  - the runner is OFF;
806
- - the recorder is not running yet (the next session starts it);
842
+ - the recorder is not running yet (the next session starts it; in a folder that is set up for another host, §3.2, the application starts it when it next runs);
807
843
  - where recording goes, and as whom;
808
844
  - when the service last answered;
809
845
  - who approves wrapped MCP calls (the recorder's correlation mode);
@@ -812,11 +848,14 @@ Truncation is always marked with the original size.
812
848
  - an npx-started scenario server;
813
849
  - other installed projects.
814
850
 
851
+ In a folder that is set up for another host (§3.2), doctor prints `Host : langchain (no Claude Code hooks are needed here)`, or `Host : langchain, and Claude Code` where the folder has both. It does not report missing hooks as a problem there.
852
+
815
853
  `--quick` makes no call to the service; the local recorder is still asked. `--json` prints `{ok, problems, warnings, notes, health, wrapped, off, service, otherProjects, shellAuthUrl, shellAuthProblem, portProblem}`. The full `/health` answer is under `health`, including every hook counter (the text output reports only the unauthorized and overran ones).
816
854
 
817
855
  **`bir status`** gives the recorder's view, with a 1.5 s timeout:
818
856
 
819
857
  - `Directory`, `BaseIn`, `Control` (URL and pid, background or not, answering or not), `Port` (a port problem), `Superseded` (one line per superseded recorder), `Log`.
858
+ - `Host`, only in a folder that is set up for another host (§3.2): `set up for langchain — its adapter reports every call, so no Claude Code hooks or wrapped servers are needed here`, or `set up for langchain, and for Claude Code` where the folder has both.
820
859
  - `Service` (answering the recorder, or not, with the last error).
821
860
  - `Runner` (on, or OFF with its source; also when the recorder reports itself OFF or predates the switch).
822
861
  - `Elsewhere` (other installed projects).
@@ -825,6 +864,7 @@ Truncation is always marked with the original size.
825
864
  `--json` prints:
826
865
 
827
866
  - `cwd`, `authUrl`;
867
+ - `hosts`, only in a folder that is set up for another host;
828
868
  - `controlServer {url, pid, answering}`;
829
869
  - `discoveryFile`, `portProblem`, `stateDir`;
830
870
  - `off {source, detail}`;
@@ -851,6 +891,7 @@ Truncation is always marked with the original size.
851
891
  | `replay` | `{enabled, source, allowServers, deriveKey, deriveVia, pollingProxies}`. `deriveVia` is `service`, `key` or `samples`; `samples` means no scenario with a target can run. |
852
892
  | `registeredProxies[]`, `wrappedServers` | proxies that registered (`serverName`, `pid`, `cwd`, `version`) and the wrapped server names |
853
893
  | `hooksVersion`, `pid`, `cwd`, `url` | the recorder's version and identity |
894
+ | `hostFields` | `2`: how much of another host's protocol (§3.2) this recorder understands. An adapter reads it before it relies on a field: with 1 it records, and from 2 it may also ask for a plan and step through it. A recorder that does not know another host answers no `hostFields`. |
854
895
  | `segmentArm` | whether service-found sub-tasks may run mid-task (default: observe only) |
855
896
  | `sessionCount`, `sessions[]` | at most 50, active ones first: `{sessionId, runId, active, idleMs, ended, steps, recording, replay, intent, fragment}`. `replay` includes `armed`, `unannounced`, `running`, `outcome` and `handover`. |
856
897
 
@@ -874,7 +915,9 @@ Lines read `[bir] <ISO time> <event> key=value …`, with each field clipped at
874
915
  | Hooks | `control.listening` (port, `recorder=<state>`, `correlation=`). `control.correlation_allow` (auto-approval opted in). `control.hook_deadline`, `control.hook_unauthorized` (a token from another install), `control.hook_unknown`, `control.hook_oversize`, `control.hook_event_mismatch`, `control.route_failed`, `control.drain_abandoned`. |
875
916
  | Recorder lifecycle | `daemon.restart` (replaced an older recorder), `daemon.restart_deferred` (mid-run: next start), `daemon.start_alongside`, `daemon.newer_kept`, `daemon.revived`. `control.port_busy` (holder and fix), `control.already_running`, `control.superseded`, `control.discovery_reclaimed`, `control.log_reopened`, `control.fatal`. `overrides.stale` (a local copy no longer matches its source: run `bir setup`). |
876
917
  | Proxy (MCP log) | `proxy.upstream_gone` (exiting with the server), `proxy.recorder_revive`, `tier.downgrade`, `tier.rebound`, `relay.undeliverable`. `upstream.auth_required` (the remote refused the handshake: credentials), `upstream.needs_sign_in`, `upstream.headers_invalid`, `upstream.proxy_env_ignored` (this Node cannot use HTTPS_PROXY), `upstream.proxy_env_invalid`, `upstream.standing_stream_failed` (at most hourly). Verbose only: `proxy.started`, `tier.bound`, `tier.passthrough`, `run.start` / `run.finish`, `upstream.stream_*`. |
877
- | Replay | `replay.enabled` (at start: servers, threshold, who derives). `plan.armed`. `replay.decision` (also `arm_timeout`, `arm_unannounced`, `no_directive`, `no_delivery_tool`, `missing_target`). `replay.pin`, `replay.pin_declined`, `replay.pin_abandoned`, `replay.pin_overran`, `replay.diverge`, `replay.diverge_overran`, `replay.handover`, `replay.vetoed`, `replay.unsafe_param`, `replay.derive_timeout`, `replay.budget_clamped`, `replay.concurrent_call`, `replay.no_session_proxy`, `replay.run_ambiguous`, `replay.run_abandoned`. |
918
+ | Replay | `replay.enabled` (at start: servers, threshold, who derives). `plan.armed`. `replay.decision` (also `arm_timeout`, `arm_unannounced`, `no_directive`, `no_delivery_tool`, `missing_target`, `host_records_only`, `host_tool_missing`). `replay.pin`, `replay.pin_declined`, `replay.pin_abandoned`, `replay.pin_overran`, `replay.diverge`, `replay.diverge_overran`, `replay.handover`, `replay.vetoed`, `replay.unsafe_param`, `replay.derive_timeout`, `replay.budget_clamped`, `replay.concurrent_call`, `replay.no_session_proxy`, `replay.run_ambiguous`, `replay.run_abandoned`. |
919
+ | Run cost | `run.unmeasured` (the turn's cost is not measured: `why` gives the reason, and `models` the models that have no price in the runner's table). |
920
+ | A host other than Claude Code (§3.2) | `run.start … host=langchain` (`bir investigate` reads the host from it, and raises no `steer_mode` finding for such a turn: steer is the full mode there), `plan.armed … mode=steer host=langchain`. `run.finish … ended=<reason>` (the run did not end with an ordinary stop). `run.unmeasured` (`why="the host sent no usage"`, a model with no price, or a run that made no model call). `run.incomplete` (the host says the recording is not the whole run: its finish carries the `incomplete` mark). `run.host_failed` (the host says the run failed: its recording is closed with that error). The service's new build never picks a run with an `error` or that mark for automatic calculation, and such a run no longer catches its prompt, unless its owner preferred it or it already has a plan (built, not deployed yet). `replay.decision verdict=no-steer code=host_records_only` (the adapter asked for no plan with its prompt) and `code=host_tool_missing` (the plan names a tool that the agent which asked does not have). `replay.own_call` (the model made a call of its own while the plan was live: the plan is handed over). `replay.step_host_stopped` (`stop=rejected` or `stop=edited`). `session.host_refused` (a scope that is empty, longer than 64 characters or not a text: nothing of that session is recorded or replayed). |
878
921
  | `bir-scenario` (MCP log) | `scenario.answered` (`ok` means results were delivered; also `partial`, `bound`), `scenario.recorder_revive`, `scenario.switched`, `scenario.hello_failed`, `scenario.tool_timeout`, `scenario.tool_truncated`. |
879
922
 
880
923
  ### 8.5 Incident playbooks
@@ -953,7 +996,15 @@ Lines read `[bir] <ISO time> <event> key=value …`, with each field clipped at
953
996
 
954
997
  **Read by.**
955
998
 
956
- - **recorder:** `bir-hooks`. When the SessionStart hook starts it, it inherits the environment Claude Code started in, minus `BIR_OFF`, the setup token and `ANTHROPIC_API_KEY`. With `bir up`, it inherits that shell's environment.
999
+ - **recorder:** `bir-hooks`. When the SessionStart hook starts it, it inherits the environment Claude Code started in, minus `BIR_OFF`, the setup token and `ANTHROPIC_API_KEY`. With `bir up`, it inherits that shell's environment. The one exception is a folder that is set up for another host and not for Claude Code (§3.2). There `bir setup --host langchain` and `bir up` start it with a small environment, which keeps only:
1000
+ - `PATH`, `HOME`, `TMPDIR`, `TEMP`, `TMP`;
1001
+ - `USERPROFILE`, `HOMEDRIVE`, `HOMEPATH`, `APPDATA`, `LOCALAPPDATA`;
1002
+ - `SYSTEMROOT`, `SYSTEMDRIVE`, `WINDIR`, `COMSPEC`, `PATHEXT`;
1003
+ - `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, `ALL_PROXY`;
1004
+ - `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `SSL_CERT_DIR`;
1005
+ - every `BIR_*` variable.
1006
+
1007
+ `BIR_SETUP_TOKEN` and `BIR_OFF` are taken out there too. The list has no `ANTHROPIC_API_KEY`: that key does not reach such a recorder, with `BIR_USE_ANTHROPIC_API_KEY=1` or without. `BIR_DERIVE_API_KEY` does.
957
1008
  - **proxy:** `bir-proxy`, from the MCP entry's `env` plus Claude Code's environment.
958
1009
  - **scenario:** `bir-scenario`.
959
1010
  - **CLI:** `bir`.
@@ -985,24 +1036,24 @@ Defaults were checked in `src/`.
985
1036
  | `BIR_MATCH_BUDGET_MS` | recorder | 3000 | Longest a prompt waits for the match. |
986
1037
  | `BIR_MATCH_SLOW_MS` | recorder | 2000 | A match slower than this, or one that timed out, stops prompts waiting for 5 min, doubling to 30 min. |
987
1038
  | `BIR_DERIVE_BUDGET_MS` | recorder | 8000 | Derivation wait for `/scenario/run`. Hooks use the smaller of this and `BIR_HOOK_DERIVE_BUDGET_MS`. |
988
- | `BIR_HOOK_DERIVE_BUDGET_MS` | recorder | 4000 | Derivation wait inside a hook: prompt arming, and the first PreToolUse. |
1039
+ | `BIR_HOOK_DERIVE_BUDGET_MS` | recorder | 8000 | Derivation wait inside a hook: prompt arming, and the first PreToolUse. |
989
1040
  | `BIR_PRETOOL_REPLAY_BUDGET_MS` | recorder | 9000 (capped at 10000) | All replay work inside one PreToolUse. |
990
1041
  | `BIR_REPLAY_PLAN_BUDGET_MS` | recorder | 100000 | Whole-plan budget for a direct run. |
991
1042
  | `BIR_REPLAY_BUDGET_MS` | recorder | unset | Older name; overrides the plan budget when set. Direct runs are clamped to 270 s in total (`replay.budget_clamped`); `bir replay` is not clamped. |
992
1043
  | `BIR_STEP_TIMEOUT_MS` | recorder | 60000 | One direct `tools/call`. |
993
1044
  | `BIR_LOGIC_TIMEOUT_MS` | recorder | 1000 (capped at 10000) | Synchronous time limit for one scenario logic body. |
994
1045
  | `BIR_REPLAY_RECORDED_OUTPUTS` | recorder | unset | `1`: a step that could not be sent may use its recorded output, labelled as such. |
995
- | `BIR_BUILTIN_TOOLS` | recorder | `Read,Glob,Grep,Edit,Write,Bash` | Built-ins the runner executes itself during a replay, in the session's project directory and confined to it (up to 0.3.7 the default was `Read,Glob,Grep`). Runner-side steps bypass the host's permission prompts; leave a tool off to have the model call it. `none` turns it off. |
1046
+ | `BIR_BUILTIN_TOOLS` | recorder | `Read,Glob,Grep,Edit,Write,Bash` | Built-ins the runner executes itself during a replay, in the session's project directory and confined to it (up to 0.3.7 the default was `Read,Glob,Grep`). Runner-side steps bypass the host's permission prompts; leave a tool off to have the model call it. `none` turns it off. Not in a session of another host (§3.2): the runner runs no step itself there, whatever a tool is named. |
996
1047
  | `BIR_BASH_ALLOW` | recorder | unset (any command) | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset means any command; `none` means no command, and `Bash` steps are steered. A plan's Bash steps share a working directory, as the host's do; variables do not persist, also as the host's. |
997
- | `BIR_DERIVE_RECENT_RESULTS` | recorder | 5 | How many of this prompt's tool results derivation may read (`0`: none). Never for plans with a command sink. |
1048
+ | `BIR_DERIVE_RECENT_RESULTS` | recorder | 5 | How many of this prompt's tool results derivation may read (`0`: none). Never for plans with a command sink, and never for a plan started at the prompt: a prompt just submitted has no results yet, and an earlier prompt's are not its own. So in a session of another host, where a plan starts at the prompt only, none are read. |
998
1049
  | `BIR_INTENT_MATCH` | recorder | on | `0` turns mid-turn intent probes off. |
999
1050
  | `BIR_INTENT_MATCH_BUDGET_MS` | recorder | 4000 | One probe's own ceiling. It is not a hook wait. |
1000
1051
  | `BIR_INTENT_MATCHES_PER_TURN` | recorder | 3 | Plans intent may arm per turn. |
1001
1052
  | `BIR_INTENT_REQUESTS_PER_TURN` | recorder | 40 | Probes per turn. |
1002
1053
  | `BIR_INTENT_SOURCES` | recorder | `text,thinking,tool` | Which text an intent may be read from. |
1003
- | `BIR_INTENT_ARM` | recorder | unset (follows `BIR_SEGMENT_ARM`) | `1`: a probe may arm a plan in its own PreToolUse, and that hook waits for it. `0`: never. |
1054
+ | `BIR_INTENT_ARM` | recorder | unset (follows `BIR_SEGMENT_ARM`) | `1`: a probe may arm a plan in its own PreToolUse, and that hook waits for it. `0`: never. Never in a session of another host (§3.2), where a probe only counts. |
1004
1055
  | `BIR_INTENT_ARM_BUDGET_MS` | recorder | 500 | How long that PreToolUse waits for the probe. |
1005
- | `BIR_SEGMENT_ARM` | recorder | unset (observe only) | `1`: recurring sub-tasks the service found may run mid-task. |
1056
+ | `BIR_SEGMENT_ARM` | recorder | unset (observe only) | `1`: recurring sub-tasks the service found may run mid-task. Not in a session of another host. |
1006
1057
  | `BIR_CONTROL_URL` | proxy, scenario | written into wrapped entries | The project's control server. Together with `BIR_CONTROL_TOKEN` it is the SDK channel and wins over discovery. |
1007
1058
  | `BIR_CONTROL_TOKEN` | proxy, scenario | unset | Bearer token for the SDK channel. |
1008
1059
  | `BIR_PROXY_STAY_UP` | proxy | unset | `1`: keep answering errors after the upstream dies (0.2.12). |
@@ -1049,8 +1100,17 @@ Defaults were checked in `src/`.
1049
1100
  - the doctor exit codes;
1050
1101
  - the NO_PROXY requirement;
1051
1102
  - `BIR_UPSTREAM_HEADERS`;
1052
- - the `run_scenario` answers.
1103
+ - the `run_scenario` answers;
1104
+ - `bir setup --host langchain`, and that it wires nothing of Claude Code;
1105
+ - the recorder's small environment in such a folder;
1106
+ - that a model with no price is not measured.
1053
1107
  6. **Served install scripts.** The API serves copies of `install.sh` and `install.ps1` with the version and address filled in. Update the served copies with the new scripts; they gained `BIR_UNINSTALL` and the NO_PROXY hint.
1108
+ 7. **A host other than Claude Code (§3.2).** Three things are outside this repository's release. Check them for the release that has `bir setup --host langchain`:
1109
+ - whether the service's part is deployed: matching inside a host (`docs/BaseInstRunner.md`, Phase 5), the limits per account (§3.3), the report rules (§6.5) and the `incomplete` mark;
1110
+ - whether the Python package `langchain-basic2202` is on PyPI;
1111
+ - `langChainGuide.md`, which is in the repository's root and not in the npm package: its "Where it stands" table and its part 5 describe the time before the release.
1112
+
1113
+ The install scripts cannot set a folder up for such a host, so there is no one-line install for it.
1054
1114
 
1055
1115
  **Manual end-to-end checks with the real Claude Code.** These are behaviours of Claude Code itself, which the automated tests simulate but cannot prove. Use the Claude Code version you will support; the approval rules and hook schema were read from 2.1.280.
1056
1116
 
@@ -1071,16 +1131,18 @@ Defaults were checked in `src/`.
1071
1131
  | 13 | `claude -p` in an untrusted workspace with wrapped tools | Calls are denied without `permissions.allow` or `--allowedTools`, and allowed with them, as the release notes say |
1072
1132
  | 14 | A 2-minute direct run | Check how Claude Code treats a `run_scenario` call near 2 minutes (auto-background) and 5 minutes (MCP idle timeout) |
1073
1133
 
1134
+ **Manual end-to-end check with a LangChain application.** Not done yet. So far a real LangChain agent has run against the real recorder with a stand-in service and a fake model only (`examples/langchain-proof/`, which is not in the npm package). Still to run: a folder set up with `bir setup --host langchain` (§3.2), with a real model and the real service. Record a task, then run it again with another value. Pass when the first run is recorded with `framework` `langchain`, and the second runs its plan through the application's own tools.
1135
+
1074
1136
  **Asks to the service team.**
1075
1137
 
1076
1138
  1. **Deduplicate client-generated `run_`, `step_` and execution-ticket ids.** Answer 409 to a repeat, so that one transient retry cannot record twice. The runner treats a 409 on a retry as "already landed". Once this is in place, the runner can safely retry after 502, 504 and connection resets, which it does not do today.
1077
1139
  2. **A cheap "this user or project has no scenarios" hint**, for example on sign-in or in the run-create answer, cacheable. With it, a prompt can skip the create-and-match wait: the last per-prompt service round trip (288-464 ms) with replay on.
1078
1140
  3. **Keep create-and-match under 2 s at p95.** Above that, each recorder's match gate stops steering for 5 to 30 minutes.
1079
1141
  4. **Keep `GET /health` answering JSON.** The runner uses it to tell the API from a website: an HTML answer means "not a BaseIn service". Refuse a spent refresh token with a JSON 400 or 401. The runner treats an HTML or non-JSON 4xx as "unreachable", so a WAF or CDN challenge never signs users out.
1080
- 5. **Send Retry-After on 429 and 503.** The runner honours up to 2 s for its one background retry.
1142
+ 5. **Send Retry-After on 429 and 503.** The runner honours up to 2 s for its one background retry. *Answered for 429, built and not deployed yet:* past one of its limits per account, the service answers `429` with `Retry-After` (§3.3).
1081
1143
  6. **Sandbox or validate scenario logic on the service side.** On users' machines it runs in the recorder process with only a synchronous time limit.
1082
1144
  7. **Expect redacted text.** Derivation prompts and uploads now arrive redacted (`[redacted]` in place of secrets).
1083
- 8. **Keep `/scenarios/match-intent` compatible.** A 404 there disables intent probing for 10 minutes.
1145
+ 8. **Keep `/scenarios/match-intent` compatible.** A 404 there disables intent probing for 10 minutes. *The service's new limit on intent probes keeps to this, built and not deployed yet:* past 1,500 probes a minute per account, a probe is answered `200` with no match, never `429`.
1084
1146
 
1085
1147
  ---
1086
1148
 
@@ -1114,6 +1176,8 @@ Defaults were checked in `src/`.
1114
1176
  - **Direct-run worst case.** The worst case by budget is 120 s: 8 s of derivation, 100 s of plan, and 12 s of slack. It is reached only if every logic body hits its limit, and sits at Claude Code's 2-minute MCP auto-background. Setting `BIR_REPLAY_BUDGET_MS=120000` (the old default) makes it 140 s.
1115
1177
  - **Retries are conservative.** A send lost to a 502, 504 or mid-request reset is not retried until the service deduplicates ids.
1116
1178
  - **Healthy-service prompt cost.** With an instant local service the prompt hook is about 6 ms slower than 0.2.12 (about 17 vs 11 ms median); the cause was not isolated. Under 503 or slow answers the recorder also keeps more idle keep-alive sockets. Both are small, but visible in measurements.
1179
+ - **A host other than Claude Code is not checked end to end.** What §3.2 describes ran on a bench only: a real LangChain agent against the real recorder, with a stand-in service and a fake model (`examples/langchain-proof/`). A real model, the real service and the production server have not been tried. The service's part is built and not deployed, and the Python package is not on PyPI. What that means for a LangChain application today is in `langChainGuide.md`, part 5.
1180
+ - **An interrupted or lossy Claude Code run is not marked.** Only a run of another host is finished with an `error` or with the `incomplete` mark today. A Claude Code run that was interrupted or lost steps is finished as before.
1117
1181
 
1118
1182
  ---
1119
1183
 
@@ -25,7 +25,7 @@
25
25
  2. the runner: @basein/runner is installed into ~\.baseinstrunner\npm,
26
26
  always through npm-cli.js (never the npm shim); bir.cmd and
27
27
  bir-hooks.cmd are written to ~\.baseinstrunner\bin, which goes first
28
- on the user Path
28
+ on the user Path, with `bir` and `bir-hooks` beside them for Git Bash
29
29
  3. Claude Code, through its official installer, when `claude` is missing
30
30
  4. `bir setup --auth-url <api> --project <folder>`: signs in, wires the
31
31
  project, starts the recorder, and prints what is left to do
@@ -378,6 +378,28 @@ function Invoke-BirInstall {
378
378
  Write-Note "$Path -> $($content.Trim())"
379
379
  }
380
380
 
381
+ # The same launcher for Git Bash, the shell Claude Code runs its commands
382
+ # in on Windows: it looks for a file named exactly `bir` and never tries
383
+ # bir.cmd, so without this file `bir` is "command not found" there.
384
+ # Forward slashes and LF line ends, as sh wants them, and UTF-8 with no
385
+ # byte order mark, which would break the #! line. PowerShell and cmd keep
386
+ # running the .cmd file: they try the names with an extension first.
387
+ function Write-ShLauncher([string]$Path, [string]$NodeExe, [string[]]$NodeFlags, [string]$Script) {
388
+ $flags = ''
389
+ if (@($NodeFlags).Count -gt 0) { $flags = ' ' + (@($NodeFlags) -join ' ') }
390
+ # Inside sh's double quotes a dollar sign and a backtick still mean
391
+ # something, and both are legal in a Windows folder name.
392
+ $node = $NodeExe.Replace('\', '/').Replace('$', '\$').Replace('`', '\`')
393
+ $target = $Script.Replace('\', '/').Replace('$', '\$').Replace('`', '\`')
394
+ $line = 'exec "' + $node + '"' + $flags + ' "' + $target + '" "$@"'
395
+ if ($dryRun) {
396
+ Write-Would "write $Path`: $line"
397
+ return
398
+ }
399
+ [System.IO.File]::WriteAllText($Path, "#!/bin/sh`n" + $line + "`n", (New-Object System.Text.UTF8Encoding($false)))
400
+ Write-Note "$Path -> $line"
401
+ }
402
+
381
403
  # Explorer, and so every terminal started from the taskbar or Start, reads
382
404
  # the user environment again only after this broadcast. setx sends it,
383
405
  # Set-ItemProperty does not. Best effort: a host without a C# compiler still
@@ -653,6 +675,8 @@ public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wP
653
675
  # typed later - and everything it wires - trusts the same certificates.
654
676
  Write-Launcher (Join-Path $binDir 'bir.cmd') $nodeExe $nodeFlags $birJs
655
677
  Write-Launcher (Join-Path $binDir 'bir-hooks.cmd') $nodeExe $nodeFlags $hooksJs
678
+ Write-ShLauncher (Join-Path $binDir 'bir') $nodeExe $nodeFlags $birJs
679
+ Write-ShLauncher (Join-Path $binDir 'bir-hooks') $nodeExe $nodeFlags $hooksJs
656
680
 
657
681
  # The user Path is read raw and written back as REG_EXPAND_SZ. Not
658
682
  # [Environment]::SetEnvironmentVariable: it flattens the value to REG_SZ,