@basein/runner 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +276 -0
  3. package/dist/auth/client.d.ts +85 -0
  4. package/dist/auth/client.js +284 -0
  5. package/dist/bin/bir-hooks.d.ts +48 -0
  6. package/dist/bin/bir-hooks.js +201 -0
  7. package/dist/bin/bir-proxy.d.ts +45 -0
  8. package/dist/bin/bir-proxy.js +207 -0
  9. package/dist/bin/bir-scenario.d.ts +24 -0
  10. package/dist/bin/bir-scenario.js +177 -0
  11. package/dist/bin/bir.d.ts +21 -0
  12. package/dist/bin/bir.js +876 -0
  13. package/dist/config/adapters/claude-code.d.ts +76 -0
  14. package/dist/config/adapters/claude-code.js +181 -0
  15. package/dist/config/adapters/generic.d.ts +17 -0
  16. package/dist/config/adapters/generic.js +36 -0
  17. package/dist/config/generate.d.ts +127 -0
  18. package/dist/config/generate.js +114 -0
  19. package/dist/config/resolve.d.ts +68 -0
  20. package/dist/config/resolve.js +132 -0
  21. package/dist/control/client.d.ts +56 -0
  22. package/dist/control/client.js +86 -0
  23. package/dist/control/correlation.d.ts +86 -0
  24. package/dist/control/correlation.js +0 -0
  25. package/dist/control/discovery.d.ts +50 -0
  26. package/dist/control/discovery.js +123 -0
  27. package/dist/control/ordering.d.ts +38 -0
  28. package/dist/control/ordering.js +44 -0
  29. package/dist/control/paths.d.ts +32 -0
  30. package/dist/control/paths.js +56 -0
  31. package/dist/control/server.d.ts +272 -0
  32. package/dist/control/server.js +1131 -0
  33. package/dist/control/transcript.d.ts +75 -0
  34. package/dist/control/transcript.js +241 -0
  35. package/dist/index.d.ts +37 -0
  36. package/dist/index.js +32 -0
  37. package/dist/jsonrpc/framing.d.ts +49 -0
  38. package/dist/jsonrpc/framing.js +143 -0
  39. package/dist/jsonrpc/types.d.ts +52 -0
  40. package/dist/jsonrpc/types.js +46 -0
  41. package/dist/proxy/intercept.d.ts +55 -0
  42. package/dist/proxy/intercept.js +147 -0
  43. package/dist/proxy/relay.d.ts +97 -0
  44. package/dist/proxy/relay.js +166 -0
  45. package/dist/proxy/session.d.ts +116 -0
  46. package/dist/proxy/session.js +319 -0
  47. package/dist/record/housekeeping.d.ts +34 -0
  48. package/dist/record/housekeeping.js +39 -0
  49. package/dist/record/queue.d.ts +48 -0
  50. package/dist/record/queue.js +96 -0
  51. package/dist/record/recorder.d.ts +111 -0
  52. package/dist/record/recorder.js +39 -0
  53. package/dist/record/redact.d.ts +37 -0
  54. package/dist/record/redact.js +119 -0
  55. package/dist/record/remote-recorder.d.ts +110 -0
  56. package/dist/record/remote-recorder.js +301 -0
  57. package/dist/record/truncate.d.ts +36 -0
  58. package/dist/record/truncate.js +85 -0
  59. package/dist/replay/bundle.d.ts +36 -0
  60. package/dist/replay/bundle.js +89 -0
  61. package/dist/replay/controller.d.ts +300 -0
  62. package/dist/replay/controller.js +807 -0
  63. package/dist/replay/coverage.d.ts +41 -0
  64. package/dist/replay/coverage.js +56 -0
  65. package/dist/replay/derive.d.ts +58 -0
  66. package/dist/replay/derive.js +166 -0
  67. package/dist/replay/executor.d.ts +78 -0
  68. package/dist/replay/executor.js +233 -0
  69. package/dist/replay/logic.d.ts +31 -0
  70. package/dist/replay/logic.js +50 -0
  71. package/dist/replay/plan.d.ts +181 -0
  72. package/dist/replay/plan.js +397 -0
  73. package/dist/replay/pricing.d.ts +41 -0
  74. package/dist/replay/pricing.js +76 -0
  75. package/dist/replay/source-run.d.ts +50 -0
  76. package/dist/replay/source-run.js +98 -0
  77. package/dist/replay/tool-error.d.ts +22 -0
  78. package/dist/replay/tool-error.js +60 -0
  79. package/dist/replay/types.d.ts +116 -0
  80. package/dist/replay/types.js +35 -0
  81. package/dist/upstream/client.d.ts +78 -0
  82. package/dist/upstream/client.js +114 -0
  83. package/dist/upstream/http-client.d.ts +78 -0
  84. package/dist/upstream/http-client.js +261 -0
  85. package/dist/upstream/lazy-client.d.ts +31 -0
  86. package/dist/upstream/lazy-client.js +53 -0
  87. package/dist/upstream/stdio-client.d.ts +57 -0
  88. package/dist/upstream/stdio-client.js +203 -0
  89. package/dist/util/log.d.ts +27 -0
  90. package/dist/util/log.js +51 -0
  91. package/dist/util/version.d.ts +2 -0
  92. package/dist/util/version.js +40 -0
  93. package/docs/BaseInstRunner.md +621 -0
  94. package/docs/calculatedReplay.md +1185 -0
  95. package/docs/calculatedReplayGuide.md +448 -0
  96. package/docs/installRun.md +413 -0
  97. package/docs/mcpmark.md +752 -0
  98. package/docs/quickstart.md +201 -0
  99. package/docs/t-bench.md +394 -0
  100. package/package.json +56 -0
@@ -0,0 +1,413 @@
1
+ # Packaging BaseInstRunner for runner machines
2
+
3
+ **The approach: publish to npm as `@basein/runner`, and install it globally at a
4
+ pinned version.** The code already assumed this — the installer writes an `npx`
5
+ invocation of its own package name into every wrapped MCP entry — so publishing
6
+ was less a new distribution channel than closing a loop that was already there.
7
+
8
+ This is the fleet-facing reference: what ships, how a runner machine goes from
9
+ bare to recording, how it gets updated, and what to do when there is no registry
10
+ to reach. For one person setting up one machine, [quickstart.md](quickstart.md)
11
+ is shorter and assumes nothing.
12
+
13
+ **Status.** Everything here is implemented and verified except §7: headless
14
+ authentication is still a stopgap, and it is the one open item that needs a
15
+ service-side decision rather than a change here.
16
+
17
+ ---
18
+
19
+ ## 1. Why this package is an easy case
20
+
21
+ | Property | Value | Why it matters for packaging |
22
+ |---|---|---|
23
+ | Runtime dependencies | **none** | The tarball is self-contained. No lockfile resolution, no transitive CVEs, no native builds, and offline install just works. |
24
+ | Module format | pure ESM (`"type": "module"`) | No dual-build gymnastics. |
25
+ | Node floor | `>=20` | One prerequisite to check on a runner. |
26
+ | Build | `tsc` → `dist/`, `prepublishOnly` wired | `npm publish` cannot ship a stale `dist/`. |
27
+ | Bins | `bir`, `bir-proxy`, `bir-hooks`, `bir-scenario`, all with `#!/usr/bin/env node` | npm creates the shims, including `.cmd`/`.ps1` on Windows. |
28
+ | Tarball | 97 files, 230 kB | `dist/` + types + docs. Sourcemaps and `docs/next.md` are excluded. |
29
+
30
+ A zero-dependency, pure-JS, four-bin package is the best-behaved thing npm
31
+ distributes. The delivery does not need to be clever.
32
+
33
+ ### What is excluded, and why
34
+
35
+ `files` carries two negations:
36
+
37
+ - `!dist/**/*.map` — 88 files, 287 kB. The maps carry no `sourcesContent`, and
38
+ `src/` is not published, so they cannot resolve a single stack frame. They were
39
+ bytes that did nothing on every install and every `npx` cold start.
40
+ - `!docs/next.md` — a working scratchpad, not documentation.
41
+
42
+ The rest of `docs/` **does** ship, and deliberately: the GitHub repository is
43
+ private, so for anyone on a runner machine the tarball is the only place this
44
+ documentation exists.
45
+
46
+ ---
47
+
48
+ ## 2. The options, and why this one
49
+
50
+ | Approach | Runner install | Good for | Cost |
51
+ |---|---|---|---|
52
+ | **npm registry (public)** ← chosen | `npm i -g @basein/runner@0.1.0` | the default; makes the generated entries resolvable | the code is public |
53
+ | npm registry (private, scoped) | same, plus an `.npmrc` token | code you cannot publish | every runner needs a read token, and so does `npx` |
54
+ | GitHub Packages | same, `registry=https://npm.pkg.github.com` | no second account | GH tokens are clumsier to rotate across a fleet |
55
+ | Tarball from a Release | `npm i -g ./basein-runner-0.1.0.tgz` | airgapped runners | no registry resolution — see §6 |
56
+ | `npm i -g git+ssh://…#v0.1.0` | one line, no registry | a handful of machines | needs git and a toolchain on every runner; slowest and most fragile |
57
+ | Node SEA / bundled binary | copy one file | machines where Node cannot be installed | you own a three-OS build matrix |
58
+ | **Docker** | — | **not suitable** | The proxy spawns the host's own MCP servers as child processes, rewrites config in the user's home, and shares a cwd with the agent session. A container isolates exactly what this tool must reach. |
59
+
60
+ The package is public; the repository is private. Those are separate decisions
61
+ and this project makes them differently. Nothing operational leaks either way —
62
+ the service is gated by `BIR_AUTH_URL` plus credentials.
63
+
64
+ ---
65
+
66
+ ## 3. Publishing
67
+
68
+ ### 3.1 Manifest — done
69
+
70
+ `package.json` carries `repository` (npm requires it for provenance) and
71
+ `publishConfig.access: public` (a scoped package is private by default, and
72
+ `npm publish` on a scope you have no paid plan for otherwise fails).
73
+
74
+ There is deliberately **no** `provenance`, and no `--provenance` in the workflow:
75
+ npm's attestation requires a *public* repository. Turn both on the day that
76
+ changes; nothing else has to move.
77
+
78
+ `homepage` and `bugs` are also omitted, for the same reason — they would be dead
79
+ links on a public package page.
80
+
81
+ ### 3.2 The name is load-bearing
82
+
83
+ `PACKAGE_NAME` in [generate.ts](../src/config/generate.ts) is written into every
84
+ config file the installer produces. Renaming after runners are deployed points
85
+ every previously-installed project at a package that no longer resolves. It is
86
+ settled now; treat it as fixed.
87
+
88
+ ### 3.3 The publishing credential
89
+
90
+ The `basein` org exists on npmjs.com (free tier, public packages), and
91
+ `NPM_TOKEN` is set as a repository secret in GitHub Actions.
92
+
93
+ It is a **granular** token, not a classic automation one, because npm now steers
94
+ you there:
95
+
96
+ | Property | Value |
97
+ |---|---|
98
+ | Name | `github-actions-release` |
99
+ | Scope | `basein`, read and write |
100
+ | Organizations | no access — publishing needs the scope, not org administration |
101
+ | 2FA | bypassed, which is what lets CI publish unattended |
102
+ | **Expires** | **2026-10-05** |
103
+
104
+ **Two dates to keep.**
105
+
106
+ *2026-10-05 — the token dies.* Granular tokens must carry an expiry; npm offers
107
+ no perpetual option. When a release starts failing with `ENEEDAUTH` around then,
108
+ this is why. Regenerate with the same settings and overwrite the secret.
109
+
110
+ *January 2027 — direct token publishing ends.* The token summary says so
111
+ outright: "Publish new versions directly (deprecated — ends January 2027)". The
112
+ replacement is **trusted publishing**, where the workflow authenticates to npm
113
+ over OIDC and there is no stored credential at all — no secret to leak, and no
114
+ expiry to forget. Migrating also removes the first date above, so doing it before
115
+ 2026-10-05 turns two chores into one.
116
+
117
+ Releasing is then a tag:
118
+
119
+ ```bash
120
+ git tag v0.1.0 && git push origin v0.1.0
121
+ ```
122
+
123
+ ### 3.4 The workflows — done
124
+
125
+ - [`.github/workflows/ci.yml`](../.github/workflows/ci.yml) — every push and PR
126
+ to main, on Ubuntu, Windows and macOS.
127
+ - [`.github/workflows/release.yml`](../.github/workflows/release.yml) — on a
128
+ `v*` tag: the same three-platform suite, then publish, then attach the tarball
129
+ to a GitHub Release.
130
+
131
+ Three platforms is not decoration. `resolveCommand` shells a `.cmd` upstream
132
+ through `cmd.exe` with its own quoting, and `normalizePath` reconciles Claude
133
+ Code's forward slashes with `process.cwd()`'s backslashes — the failure the code
134
+ itself calls "the number one cause of *installed but never runs*". Neither is
135
+ exercised on Linux.
136
+
137
+ Both workflows run `npm run build` before the tests, because `test:smoke` spawns
138
+ the real binaries out of `dist/` while `npm test` only builds `.test-build/`.
139
+
140
+ The release job refuses to publish when the tag disagrees with `package.json` —
141
+ npm has no undo past 72 hours, and a mismatched tag publishes a version nobody
142
+ asked for.
143
+
144
+ **One known flake.** `run_scenario finds the session whose plan is armed` loses
145
+ an arming race roughly one run in three. It is unrelated to packaging, and the
146
+ test steps retry once so it cannot redden a release at random. Remove the retry
147
+ the day that test is made deterministic.
148
+
149
+ ---
150
+
151
+ ## 4. What `bir install` writes
152
+
153
+ Three invocation modes, one flag each:
154
+
155
+ | Mode | Flag | Entry |
156
+ |---|---|---|
157
+ | **npx** | *(default)* | `npx -y -p @basein/runner@0.1.0 bir-proxy …` |
158
+ | **global** | `--global` | `<node> <prefix>/node_modules/@basein/runner/dist/bin/bir-proxy.js …` |
159
+ | **local** | `--local` | `<node> <this checkout>/dist/bin/bir-proxy.js …` |
160
+
161
+ **The npx spec is pinned.** It was not, and that was the one real defect in the
162
+ original packaging story: two runners provisioned a week apart could run
163
+ different proxy versions against the same service, with nothing in a recording to
164
+ say which.
165
+
166
+ **`--global` is what a fleet wants**, and it is what the bootstrap scripts pass.
167
+ It removes the registry from the hot path entirely: no resolution before the MCP
168
+ server can start, no dependence on the MCP host seeing the same `PATH` as the
169
+ shell that installed things — which GUI-launched hosts on Windows do not — and
170
+ it works with no network at all.
171
+
172
+ It also survives upgrades. The path
173
+ `<prefix>/node_modules/@basein/runner/dist/bin/bir-proxy.js` contains no version,
174
+ so `npm i -g @basein/runner@0.2.0` changes what that path points at without
175
+ changing the path. **A pinned-npx install, by contrast, must re-run `bir install`
176
+ on every upgrade** to move the pin.
177
+
178
+ `--global` refuses to run from a checkout: it asserts a path that outlives the
179
+ shell, and a checkout's `dist/` moves with the checkout. Use `--local` when you
180
+ mean the checkout on purpose.
181
+
182
+ ---
183
+
184
+ ## 5. Provisioning a runner
185
+
186
+ Four scopes, and conflating them is the usual source of "installed but never
187
+ runs":
188
+
189
+ | Scope | How often | What it does |
190
+ |---|---|---|
191
+ | **machine** | once | Node ≥ 20, and the package installed globally |
192
+ | **account** | once per user | `BIR_AUTH_URL`, then `bir login` → `~/.baseinstrunner/credentials.json` (0600) |
193
+ | **project** | once per repo | `bir install --global` rewrites that project's MCP entries and wires the hooks |
194
+ | **session** | while recording | `bir-hooks` running, discovered by `sha256(normalised cwd)` |
195
+
196
+ That cwd keying is why `bir-hooks` must start in the *same directory* the agent
197
+ session runs in. A control server started elsewhere is invisible, and the run
198
+ silently degrades to Tier 2.
199
+
200
+ ### 5.1 One command
201
+
202
+ [`scripts/install-runner.ps1`](../scripts/install-runner.ps1) and
203
+ [`scripts/install-runner.sh`](../scripts/install-runner.sh) do machine, account
204
+ and project scope together:
205
+
206
+ ```powershell
207
+ .\scripts\install-runner.ps1 C:\work\repo -AuthUrl https://basein.example.com
208
+ ```
209
+
210
+ ```bash
211
+ BIR_AUTH_URL=https://basein.example.com ./scripts/install-runner.sh ~/work/repo
212
+ ```
213
+
214
+ Both are idempotent, both validate everything checkable **before** changing
215
+ anything (a failed run leaves no half-provisioned machine), both skip `bir login`
216
+ when a session is already cached, and both accept a local tarball
217
+ (`-Tarball` / `BIR_TARBALL`) instead of the registry.
218
+
219
+ Two things they deliberately do not do: supervise `bir-hooks`, and automate
220
+ authentication. See §5.2 and §7.
221
+
222
+ The PowerShell script is **pure ASCII** on purpose. Windows PowerShell 5.1
223
+ decodes a BOM-less `.ps1` using the machine's ANSI codepage, and PowerShell
224
+ accepts curly quotes as string delimiters — so a UTF-8 em-dash in a comment can
225
+ decode into a quote and swallow the rest of the file. It parsed fine on one
226
+ machine and failed on another before this was fixed. Keep it ASCII.
227
+
228
+ ### 5.2 Keeping `bir-hooks` alive
229
+
230
+ One process per project cwd. On a headless runner, supervise it rather than
231
+ leaving it in a shell.
232
+
233
+ **Windows** — a Scheduled Task at logon, with *Start in* set to the project
234
+ directory:
235
+
236
+ ```powershell
237
+ $action = New-ScheduledTaskAction -Execute 'bir-hooks' -WorkingDirectory 'C:\work\repo'
238
+ $trigger = New-ScheduledTaskTrigger -AtLogOn
239
+ Register-ScheduledTask -TaskName 'bir-hooks (repo)' -Action $action -Trigger $trigger
240
+ ```
241
+
242
+ **macOS** — a launchd agent at `~/Library/LaunchAgents/com.basein.hooks.plist`,
243
+ with `WorkingDirectory` set to the project and `StandardErrorPath` to a log file,
244
+ then `launchctl load` it.
245
+
246
+ **Linux** — a systemd user unit with `WorkingDirectory=`, plus
247
+ `loginctl enable-linger "$USER"` so it survives logout.
248
+
249
+ Whichever you use, redirect stderr to a file. Every decision is one `key=value`
250
+ line on stderr and there is no file sink to configure, so the supervisor's
251
+ redirection *is* the sink:
252
+
253
+ ```bash
254
+ grep version.skew ~/.baseinstrunner/audit.log
255
+ grep run.lossy ~/.baseinstrunner/audit.log
256
+ ```
257
+
258
+ ### 5.3 Verify
259
+
260
+ ```bash
261
+ bir doctor --json # exits non-zero if it is not working right now
262
+ ```
263
+
264
+ `bir doctor` asks the control server which proxies actually registered; it never
265
+ reads the config file back. That makes it the right gate for a provisioning
266
+ script. `bir status` only describes intent.
267
+
268
+ ---
269
+
270
+ ## 6. Machines with no registry
271
+
272
+ Zero dependencies makes offline install trivial — no `npm ci --offline`, no
273
+ vendored `node_modules`:
274
+
275
+ ```bash
276
+ # on a connected machine
277
+ npm pack @basein/runner@0.1.0 # or download the Release asset
278
+
279
+ # on the runner
280
+ BIR_TARBALL=./basein-runner-0.1.0.tgz ./scripts/install-runner.sh ~/work/repo
281
+ ```
282
+
283
+ The bootstrap scripts already pass `--global`, so the resulting configs contain
284
+ absolute paths and never reach for a registry. Nothing further is needed.
285
+
286
+ Note that a private repository's Release assets require a GitHub token to fetch,
287
+ so the tarball has to be carried to the runner by whatever means you already use.
288
+
289
+ ---
290
+
291
+ ## 7. Headless authentication — still a stopgap
292
+
293
+ `bir login` prompts for email and password on a TTY and caches
294
+ `{accessToken, refreshToken, accessExpiresAt, user}` at
295
+ `~/.baseinstrunner/credentials.json`, mode 0600. For a fleet, in order of
296
+ preference:
297
+
298
+ 1. **Pre-seed the credentials file.** Log in once as a dedicated service account,
299
+ copy `credentials.json` to each machine at 0600. The refresh token keeps it
300
+ alive and nothing prompts. Works today — but it is a shared long-lived secret,
301
+ so never a person's account.
302
+ 2. **`BIR_NO_AUTH` / `BIR_AUTH_DISABLE=1`** for runners that should record
303
+ nothing. Everything else still works.
304
+ 3. **Ask the service for a non-interactive grant** — client credentials, or a
305
+ long-lived runner token read from an env var. This is the right answer for a
306
+ fleet, and it is a service-side change. Worth raising before the fleet grows.
307
+
308
+ `BIR_HOME` relocates all of this if state belongs somewhere other than
309
+ `~/.baseinstrunner`.
310
+
311
+ ---
312
+
313
+ ## 8. Updating a runner
314
+
315
+ Re-run the bootstrap script with the new version. It is idempotent, it will not
316
+ prompt for a login it already has, and `bir install` will not double-wrap:
317
+
318
+ ```powershell
319
+ .\scripts\install-runner.ps1 C:\work\repo -Version 0.2.0 -AuthUrl https://basein.example.com
320
+ ```
321
+
322
+ ```bash
323
+ BIR_VERSION=0.2.0 BIR_AUTH_URL=https://basein.example.com ./scripts/install-runner.sh ~/work/repo
324
+ ```
325
+
326
+ Then two rules:
327
+
328
+ - **Restart `bir-hooks`.** A running process is still the old code. This is the
329
+ step people forget.
330
+ - **Re-run `bir install` only if the entry shape changed** — or always, since it
331
+ is idempotent. With `--global` entries an upgrade needs no re-install at all
332
+ (§4). With pinned-npx entries it always does.
333
+
334
+ Rolling back is the same command with the older version.
335
+
336
+ ### The half-upgraded machine
337
+
338
+ This is the update failure that looks like success: the package is new, but a
339
+ proxy is still the old code because a config points elsewhere or `bir-hooks` was
340
+ never restarted. Everything keeps working, and quietly runs code you did not
341
+ ship.
342
+
343
+ A proxy now reports its version when it registers, the control server compares it
344
+ with its own, and a mismatch is one line on the audit log:
345
+
346
+ ```
347
+ [bir] … version.skew server=fleet proxy=0.0.9 hooks=0.1.0 fix="restart bir-hooks after upgrading, …"
348
+ ```
349
+
350
+ `bir doctor --json` carries the same facts structurally — `hooksVersion` at the
351
+ top and a `version` on each entry of `registeredProxies` — so a provisioning
352
+ script can assert agreement rather than grep a log. A proxy too old to report a
353
+ version is not treated as skewed: silence, not a false alarm.
354
+
355
+ ---
356
+
357
+ ## 9. Per-release verification
358
+
359
+ CI does this on three platforms. To do it by hand on a clean machine:
360
+
361
+ ```bash
362
+ npm run build && npm test # 233 tests
363
+ npm run test:smoke # real processes: hooks + proxy + scenario over stdio
364
+ npm pack && npm i -g ./basein-runner-*.tgz
365
+ cd /tmp/scratch && bir install --global && bir-hooks &
366
+ bir doctor --json # must exit 0
367
+ bir uninstall # must restore the config byte-for-byte
368
+ ```
369
+
370
+ `test:smoke` is the one that matters for packaging: it is the only test that
371
+ crosses process boundaries, which is exactly where a bad `files` list, a missing
372
+ shebang, or a broken bin mapping shows up.
373
+
374
+ Sandbox a manual run with `BIR_HOME` and `BIR_CLAUDE_JSON` so it cannot touch
375
+ your real Claude Code configuration.
376
+
377
+ ---
378
+
379
+ ## 10. Where things stand
380
+
381
+ | # | Item | State |
382
+ |---|---|---|
383
+ | 1 | Name settled; `PACKAGE_NAME` aligned | done |
384
+ | 2 | `repository` + `publishConfig`; provenance deferred (private repo) | done |
385
+ | 3 | Tarball trimmed — no dead sourcemaps, no scratchpad | done |
386
+ | 4 | npx spec pinned; `--global` added | done |
387
+ | 5 | CI and release workflows, three platforms | done |
388
+ | 6 | `scripts/install-runner.{ps1,sh}` | done |
389
+ | 7 | Version-skew detection on registration | done |
390
+ | 8 | UTF-8 BOM tolerated in MCP configs | done |
391
+ | 9 | npm org `basein`, `NPM_TOKEN` secret | done |
392
+ | 10 | Tag `v0.1.0` to publish | ready |
393
+ | 11 | Non-interactive auth grant for fleets | **needs the service** |
394
+ | 12 | Migrate to trusted publishing before Jan 2027 (§3.3) | scheduled |
395
+
396
+ Item 11 is the only one that needs somebody else.
397
+
398
+ ---
399
+
400
+ ## Appendix: the BOM
401
+
402
+ `bir` used to reject any MCP config saved with a UTF-8 byte order mark. Notepad
403
+ writes one, so does VS Code's "UTF-8 with BOM", and so does PowerShell's
404
+ `Out-File -Encoding utf8`. `readFileSync(path, "utf8")` decodes the BOM to U+FEFF
405
+ rather than consuming it, and `JSON.parse` then rejects the file.
406
+
407
+ The failure mode was the bad part: `bir install` logged the parse error and
408
+ **still reported success**, so an operator saw a green run and no wrapping. On
409
+ Windows runners this was a matter of time.
410
+
411
+ `parseJsonFile` now strips it, which covers every user-authored config read.
412
+ Restoring is unaffected — the uninstall backup stashes raw text on a separate
413
+ path, so a BOM'd file comes back with its BOM intact.