@basein/runner 0.2.12 → 0.3.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 (142) hide show
  1. package/README.md +540 -369
  2. package/dist/auth/client.d.ts +65 -3
  3. package/dist/auth/client.js +305 -53
  4. package/dist/bin/bir-hooks.d.ts +72 -31
  5. package/dist/bin/bir-hooks.js +176 -284
  6. package/dist/bin/bir-proxy.d.ts +17 -2
  7. package/dist/bin/bir-proxy.js +222 -28
  8. package/dist/bin/bir-scenario.d.ts +61 -21
  9. package/dist/bin/bir-scenario.js +593 -122
  10. package/dist/bin/bir.d.ts +12 -6
  11. package/dist/bin/bir.js +2935 -489
  12. package/dist/bin/investigate.js +40 -15
  13. package/dist/bin/scenario-edit.d.ts +7 -1
  14. package/dist/bin/scenario-edit.js +43 -21
  15. package/dist/bin/scenario-liveness.d.ts +194 -0
  16. package/dist/bin/scenario-liveness.js +459 -0
  17. package/dist/bin/scenario-stderr.d.ts +43 -0
  18. package/dist/bin/scenario-stderr.js +76 -0
  19. package/dist/bin/service-read.d.ts +88 -0
  20. package/dist/bin/service-read.js +128 -0
  21. package/dist/bin/setup.d.ts +19 -0
  22. package/dist/bin/setup.js +146 -20
  23. package/dist/config/adapters/claude-code.d.ts +92 -14
  24. package/dist/config/adapters/claude-code.js +291 -101
  25. package/dist/config/adapters/generic.d.ts +4 -1
  26. package/dist/config/adapters/generic.js +13 -14
  27. package/dist/config/generate.d.ts +392 -14
  28. package/dist/config/generate.js +666 -19
  29. package/dist/config/mcp-approval.d.ts +76 -0
  30. package/dist/config/mcp-approval.js +210 -0
  31. package/dist/config/node-path.d.ts +42 -0
  32. package/dist/config/node-path.js +102 -0
  33. package/dist/config/overrides.d.ts +62 -0
  34. package/dist/config/overrides.js +136 -0
  35. package/dist/config/resolve.d.ts +70 -8
  36. package/dist/config/resolve.js +186 -15
  37. package/dist/control/client.d.ts +22 -0
  38. package/dist/control/client.js +26 -7
  39. package/dist/control/correlation.d.ts +23 -5
  40. package/dist/control/correlation.js +0 -0
  41. package/dist/control/daemon.d.ts +400 -18
  42. package/dist/control/daemon.js +1105 -102
  43. package/dist/control/discovery.d.ts +77 -5
  44. package/dist/control/discovery.js +186 -11
  45. package/dist/control/ensure-hook.d.ts +61 -0
  46. package/dist/control/ensure-hook.js +160 -8
  47. package/dist/control/paths.d.ts +26 -0
  48. package/dist/control/paths.js +34 -0
  49. package/dist/control/pin-guard.d.ts +83 -0
  50. package/dist/control/pin-guard.js +534 -0
  51. package/dist/control/recorder-keeper.d.ts +229 -0
  52. package/dist/control/recorder-keeper.js +721 -0
  53. package/dist/control/recorder-main.d.ts +73 -0
  54. package/dist/control/recorder-main.js +778 -0
  55. package/dist/control/server.d.ts +707 -31
  56. package/dist/control/server.js +2786 -302
  57. package/dist/control/transcript.d.ts +21 -1
  58. package/dist/control/transcript.js +378 -112
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +5 -1
  61. package/dist/jsonrpc/framing.d.ts +76 -12
  62. package/dist/jsonrpc/framing.js +216 -59
  63. package/dist/proxy/intercept.d.ts +49 -5
  64. package/dist/proxy/intercept.js +188 -59
  65. package/dist/proxy/relay.d.ts +16 -0
  66. package/dist/proxy/relay.js +77 -5
  67. package/dist/proxy/revive.d.ts +122 -0
  68. package/dist/proxy/revive.js +251 -0
  69. package/dist/proxy/session.d.ts +222 -14
  70. package/dist/proxy/session.js +708 -99
  71. package/dist/record/recorder.d.ts +51 -3
  72. package/dist/record/recorder.js +3 -0
  73. package/dist/record/redact.d.ts +103 -9
  74. package/dist/record/redact.js +659 -30
  75. package/dist/record/remote-recorder.d.ts +327 -32
  76. package/dist/record/remote-recorder.js +1517 -187
  77. package/dist/record/switchable-recorder.d.ts +126 -0
  78. package/dist/record/switchable-recorder.js +351 -0
  79. package/dist/record/truncate.d.ts +40 -1
  80. package/dist/record/truncate.js +111 -4
  81. package/dist/replay/bash-channel.d.ts +35 -0
  82. package/dist/replay/bash-channel.js +133 -0
  83. package/dist/replay/bundle.d.ts +10 -0
  84. package/dist/replay/bundle.js +10 -0
  85. package/dist/replay/controller.d.ts +369 -20
  86. package/dist/replay/controller.js +1176 -107
  87. package/dist/replay/derive.d.ts +1 -0
  88. package/dist/replay/derive.js +81 -24
  89. package/dist/replay/executor.d.ts +51 -16
  90. package/dist/replay/executor.js +114 -41
  91. package/dist/replay/handover.d.ts +54 -1
  92. package/dist/replay/handover.js +55 -5
  93. package/dist/replay/logic.d.ts +62 -10
  94. package/dist/replay/logic.js +140 -22
  95. package/dist/replay/plan.d.ts +201 -27
  96. package/dist/replay/plan.js +581 -141
  97. package/dist/replay/safety.d.ts +72 -0
  98. package/dist/replay/safety.js +303 -0
  99. package/dist/replay/source-run.d.ts +10 -0
  100. package/dist/replay/source-run.js +80 -35
  101. package/dist/replay/targets.d.ts +15 -1
  102. package/dist/replay/targets.js +62 -4
  103. package/dist/replay/tool-error.d.ts +15 -1
  104. package/dist/replay/tool-error.js +33 -1
  105. package/dist/replay/types.d.ts +7 -4
  106. package/dist/upstream/client.d.ts +56 -3
  107. package/dist/upstream/client.js +61 -4
  108. package/dist/upstream/headers.d.ts +43 -0
  109. package/dist/upstream/headers.js +90 -0
  110. package/dist/upstream/http-client.d.ts +213 -22
  111. package/dist/upstream/http-client.js +1034 -168
  112. package/dist/upstream/http-transport.d.ts +143 -0
  113. package/dist/upstream/http-transport.js +391 -0
  114. package/dist/upstream/lazy-client.d.ts +8 -0
  115. package/dist/upstream/lazy-client.js +29 -1
  116. package/dist/upstream/sse.d.ts +78 -0
  117. package/dist/upstream/sse.js +189 -0
  118. package/dist/upstream/stdio-client.d.ts +61 -1
  119. package/dist/upstream/stdio-client.js +356 -33
  120. package/dist/util/atomic-write.d.ts +29 -0
  121. package/dist/util/atomic-write.js +96 -0
  122. package/dist/util/journal.d.ts +15 -0
  123. package/dist/util/journal.js +48 -3
  124. package/dist/util/log.d.ts +10 -0
  125. package/dist/util/log.js +28 -1
  126. package/dist/util/off-switch.d.ts +46 -0
  127. package/dist/util/off-switch.js +91 -0
  128. package/dist/util/service-health.d.ts +133 -0
  129. package/dist/util/service-health.js +217 -0
  130. package/docs/BaseInstRunner.md +694 -621
  131. package/docs/calculatedReplay.md +1625 -1304
  132. package/docs/calculatedReplayGuide.md +1212 -1087
  133. package/docs/installRun.md +1013 -772
  134. package/docs/loginWeb.md +627 -607
  135. package/docs/mcpmark.md +49 -16
  136. package/docs/my-first-sample.md +569 -545
  137. package/docs/quickstart.md +384 -311
  138. package/docs/t-bench.md +406 -394
  139. package/package.json +58 -57
  140. package/production.md +1371 -0
  141. package/scripts/install.ps1 +97 -6
  142. package/scripts/install.sh +104 -7
@@ -22,8 +22,11 @@
22
22
  * with a working directory that must equal the project's, each needing the
23
23
  * person to know which one they are on. The hook already runs in the right
24
24
  * directory with the right environment at exactly the right moment, and Claude
25
- * Code already supervises it. What is lost is restart-on-crash mid-session,
26
- * which the proxies already survive (they fall to Tier 2 and say so).
25
+ * Code already supervises it. Restart-on-crash mid-session is the proxies'
26
+ * part: a bound proxy (or `bir-scenario`) that finds the recorder gone —
27
+ * refusing connections, its pid dead — spawns `bir-hooks revive`
28
+ * ({@link reviveDaemon}), rate limited, unless `bir down` stopped it on
29
+ * purpose.
27
30
  *
28
31
  * HOW IT IS LAUNCHED. The daemon opens its own log file (`BIR_DAEMON_LOG`) and
29
32
  * inherits nothing from our stdio. On POSIX that is a `detached` spawn with
@@ -32,50 +35,177 @@
32
35
  * short-lived `powershell.exe`: libuv's CreateProcess inherits *every*
33
36
  * inheritable handle of the parent, not only the three it was told about, and
34
37
  * a node started under a PowerShell layer (the `irm | iex` bootstrap, a `.ps1`
35
- * shim, a captured `bir up`) carries an extra copy of its own stdout pipe. A
36
- * daemon that inherits it holds that pipe open for days, and whoever is reading
37
- * it never sees EOF. `Start-Process` passes the environment and no handles.
38
+ * shim, a captured `bir up`, a `$r = claude -p …`) carries an extra copy of its
39
+ * own stdout pipe. A daemon that inherits it holds that pipe open for days, and
40
+ * whoever is reading it never sees EOF. `Start-Process` passes the environment
41
+ * and no handles. It costs about a second, once per project per boot; a direct
42
+ * spawn is faster but only safe when nothing above the hook captured its
43
+ * output, so it is an opt-in (`BIR_WINDOWS_LAUNCH=spawn`), not the default.
44
+ *
45
+ * WHERE IT RUNS. The daemon's working directory is the runner's own state
46
+ * directory, never the project: on Windows a process's current directory
47
+ * cannot be renamed or deleted, and an invisible recorder holding the project
48
+ * folder made "delete the folder", `git worktree remove` and a re-clone fail
49
+ * with "in use by another program". The project is passed as `BIR_DAEMON_CWD`.
50
+ *
51
+ * WHAT IT MAY NOT DO. Every path is bounded: the SessionStart hook has a budget
52
+ * well inside Claude Code's hook timeout (ensure-hook.ts), a recorder that is
53
+ * alive but slow is never mistaken for a dead one (that started a duplicate
54
+ * that could not bind), and nothing here touches the network — the recorder
55
+ * listens before it signs in (recorder-keeper.ts).
38
56
  */
39
- import { type DiscoveryInfo } from "./discovery.js";
40
- /** A log file larger than this is rolled over to `.1` when a daemon starts. */
57
+ import { type DiscoveryInfo, type SupersededRecorder } from "./discovery.js";
58
+ import { type InstalledSidecar } from "../config/generate.js";
59
+ export { listSupersededRecorders, type SupersededRecorder } from "./discovery.js";
60
+ /** A log file larger than this is rolled over to `.1` (at start, and while it runs). */
41
61
  export declare const LOG_ROTATE_BYTES: number;
42
62
  /** Marks a `bir-hooks` as started by this module; it says so in its discovery file. */
43
63
  export declare const DAEMON_ENV = "BIR_DAEMON";
44
64
  /** Where that process writes its audit lines. It opens the file itself. */
45
65
  export declare const DAEMON_LOG_ENV = "BIR_DAEMON_LOG";
66
+ /**
67
+ * The project a background recorder serves. It runs in the runner's state
68
+ * directory, not the project (see WHERE IT RUNS above), so it cannot learn the
69
+ * project from its own working directory.
70
+ */
71
+ export declare const DAEMON_CWD_ENV = "BIR_DAEMON_CWD";
46
72
  /**
47
73
  * The setup token's environment variable (`bir setup` reads it when `--token`
48
74
  * is absent). Never handed to the recorder: it is a one-use secret for signing
49
75
  * in, and a process that lives for days has no business holding it.
50
76
  */
51
77
  export declare const SETUP_TOKEN_ENV = "BIR_SETUP_TOKEN";
78
+ /**
79
+ * An Anthropic key meant for the runner's derivation, and nothing else. Set
80
+ * explicitly; it is the only key the recorder uses by default.
81
+ */
82
+ export declare const DERIVE_KEY_ENV = "BIR_DERIVE_API_KEY";
83
+ /**
84
+ * `1` lets the recorder use an `ANTHROPIC_API_KEY` from its environment for
85
+ * derivation, as it did before. Off by default: the hook-started recorder has
86
+ * Claude Code's environment, and an API-billed Claude Code has its key there —
87
+ * using it silently spent the person's own money on something the service
88
+ * does for a signed-in runner.
89
+ */
90
+ export declare const USE_API_KEY_ENV = "BIR_USE_ANTHROPIC_API_KEY";
91
+ /** `spawn` launches the Windows daemon directly instead of through PowerShell. */
92
+ export declare const WINDOWS_LAUNCH_ENV = "BIR_WINDOWS_LAUNCH";
93
+ /**
94
+ * `bir-hooks revive --cwd <project>`: bring the project's recorder back when
95
+ * it is gone. Spawned detached by a bound `bir-proxy` or `bir-scenario` whose
96
+ * recorder refuses connections and whose pid is dead (see {@link reviveDaemon}).
97
+ */
98
+ export declare const REVIVE_ARG = "revive";
99
+ /**
100
+ * {@link EnsureOptions.probeIdle}: the gap between the two `/health` probes
101
+ * that tell a recorder that only *reports* a run open from one doing work.
102
+ */
103
+ export declare const IDLE_PROBE_GAP_MS = 2500;
52
104
  export interface DaemonStatus {
53
105
  info: DiscoveryInfo;
54
106
  health: Record<string, unknown>;
55
107
  }
108
+ /** The recorder is running but did not answer in time: busy, not gone. */
109
+ export declare class RecorderBusyError extends Error {
110
+ readonly pid: number;
111
+ constructor(pid: number);
112
+ }
113
+ /** A revive found the project's recorder stopped on purpose (`bir down`): it is left down. */
114
+ export declare class RecorderStoppedError extends Error {
115
+ constructor();
116
+ }
56
117
  /** Absolute path of `dist/bin/bir-hooks.js` in the package this module runs from. */
57
118
  export declare function hooksScriptPath(): string;
119
+ /** What answered on a recorder's address, told apart as far as HTTP can. */
120
+ export type HealthProbe =
121
+ /** A recorder that accepted the token. */
122
+ {
123
+ kind: "ok";
124
+ health: Record<string, unknown>;
125
+ }
126
+ /** Nothing listening (connection refused, or unreachable). */
127
+ | {
128
+ kind: "refused";
129
+ reason: string;
130
+ }
131
+ /** Something accepted the connection and did not answer in time. */
132
+ | {
133
+ kind: "timeout";
134
+ }
135
+ /** A listener that refused the token. */
136
+ | {
137
+ kind: "unauthorized";
138
+ status: number;
139
+ }
140
+ /** A listener that answered, but not as a recorder. */
141
+ | {
142
+ kind: "foreign";
143
+ status: number;
144
+ };
145
+ /**
146
+ * `GET /health` with the bearer token, bounded, with the failure kept apart:
147
+ * "refused" means gone, "timeout" means busy, and the difference is the one
148
+ * that decides whether a second recorder is started.
149
+ */
150
+ export declare function probeHealth(url: string, token: string, timeoutMs?: number): Promise<HealthProbe>;
58
151
  /**
59
152
  * The live control server for `cwd`, or undefined.
60
153
  *
61
- * "Live" means the discovery file is fresh, its pid is alive, **and** it
62
- * answers `/health` with its token. A file left by a crash fails the second
63
- * test; a process that hung, or a recycled pid, fails the third.
154
+ * "Live" means the discovery file names a pid that is alive **and** that
155
+ * answers `/health` with its token. A file left by a crash fails the first
156
+ * test; a process that hung, or a recycled pid, fails the second.
64
157
  */
65
158
  export declare function daemonStatus(cwd: string): Promise<DaemonStatus | undefined>;
159
+ /**
160
+ * This project's recorder, found by its port rather than its discovery file,
161
+ * and its file written again. For a recorder whose file was removed (by hand,
162
+ * by an older runner that took a slow recorder for a dead one): starting a
163
+ * second one would only find the port taken.
164
+ *
165
+ * A file that already names the recorder that answered is left as it is. What
166
+ * is written here is rebuilt from `/health`, which does not say whether the
167
+ * process runs in the background or where its log is: overwriting the
168
+ * recorder's own, complete record with it made `bir status` lose the log path
169
+ * and the start time, and hid the recorder from the reinstall check (which
170
+ * only replaces background recorders) — for the half hour until its own
171
+ * heartbeat. The recorder also rewrites a record of itself that differs from
172
+ * what it would write (recorder-main.ts), so what this writes is short-lived.
173
+ */
174
+ export declare function rediscover(cwd: string, port: number, token: string, timeoutMs?: number): Promise<DaemonStatus | undefined>;
66
175
  export interface EnsureOptions {
67
176
  /** `dist/bin/bir-hooks.js` to run. Defaults to this package's. */
68
177
  hooksScript?: string;
69
178
  /** Environment for the new process. Defaults to this process's. */
70
179
  env?: NodeJS.ProcessEnv;
71
- /** How long to wait for a fresh process to answer `/health`. */
180
+ /**
181
+ * The whole call's budget: finding, replacing and starting the recorder.
182
+ * Default 15 s. The SessionStart hook passes less (ensure-hook.ts).
183
+ */
72
184
  timeoutMs?: number;
73
185
  /**
74
- * Restart a running recorder whose version is not ours, provided no session
75
- * is mid-run. Off by default: only `bir setup`/`bir up` and the SessionStart
76
- * hook — the moments a restart costs nothing — ask for it.
186
+ * Restart a running recorder that is an older version than ours, provided
187
+ * no session is mid-run. Off by default: only `bir setup`/`bir up` and the
188
+ * SessionStart hook — the moments a restart costs nothing — ask for it. A
189
+ * newer recorder (another runner copy on this machine) is kept.
77
190
  */
78
191
  restartOnSkew?: boolean;
192
+ /**
193
+ * Do not take the word of a recorder that predates the idle bound. A 0.2.12
194
+ * (or older) recorder's `/health` has no `sessions[].idleMs`, and its
195
+ * `active` means "a run was opened and never finished" — an Esc-interrupted
196
+ * turn, a crashed window or a `claude -p` run leaves one open for the
197
+ * recorder's whole life, and it was then never replaced. With this set, such
198
+ * a recorder counts as idle — and replaceable — when two `/health` probes
199
+ * {@link IDLE_PROBE_GAP_MS} apart show no change in its sessions' steps and
200
+ * no registered proxy of it is alive. Costs that gap, so only `bir setup`
201
+ * and `bir up` pass it; the SessionStart hook stays conservative.
202
+ */
203
+ probeIdle?: boolean;
204
+ /**
205
+ * Set by {@link reviveDaemon} only: honour a `bir down` (the down marker)
206
+ * instead of clearing it, and throw {@link RecorderStoppedError} when found.
207
+ */
208
+ revive?: boolean;
79
209
  }
80
210
  export interface EnsureResult {
81
211
  status: DaemonStatus;
@@ -86,14 +216,150 @@ export interface EnsureResult {
86
216
  }
87
217
  /** How many of the recorder's sessions are mid-run right now. */
88
218
  export declare function activeSessions(health: Record<string, unknown>): number;
219
+ /**
220
+ * `a` against `b` as versions: -1, 0 or 1, or undefined when either is not
221
+ * `x.y.z` (with an optional pre-release, which sorts below its release, and
222
+ * build metadata, which is ignored).
223
+ */
224
+ export declare function compareVersions(a: string, b: string): -1 | 0 | 1 | undefined;
225
+ /**
226
+ * True for the `/health` of a recorder that predates the idle bound (0.2.12
227
+ * and older): it lists sessions, and none of them says how long it has been
228
+ * idle. Its `active` is then "a run is open", with nothing to close one that
229
+ * was abandoned. An empty list says nothing either way (and counts none active).
230
+ */
231
+ export declare function isLegacyHealth(health: Record<string, unknown>): boolean;
232
+ /** A TCP port number, or undefined for anything that is not one. */
233
+ export declare function validPort(value: unknown): number | undefined;
234
+ /** The address this project's hooks were written with, as far as anything recorded it. */
235
+ export interface ProjectEndpoint {
236
+ /**
237
+ * The port the hooks post to: a valid `BIR_CONTROL_PORT`, else the
238
+ * project's record. Undefined when neither names one — the recorder then
239
+ * takes the machine-wide or default port and may fall back to another.
240
+ */
241
+ port?: number;
242
+ /** The project's token, when the project has a record. */
243
+ token?: string;
244
+ /** True when {@link port} is known: a recorder must bind exactly it. */
245
+ pinned: boolean;
246
+ /**
247
+ * True when `BIR_CONTROL_PORT=0` asks for any free port: the recorder binds
248
+ * an ephemeral one and says which in its discovery file, `/health` and its
249
+ * log. {@link port} is then undefined — no port is expected — so a running
250
+ * recorder is never replaced for listening on the "wrong" one. Absent
251
+ * otherwise. For tests and the smoke run, which must never take the real
252
+ * default port; a project's hooks name a fixed port, so 0 is not for them.
253
+ */
254
+ ephemeral?: true;
255
+ }
256
+ /**
257
+ * `BIR_CONTROL_PORT` read once, for everything that reads it: `"0"` is any
258
+ * free port, a valid port is that port, and anything else (unset, empty, not
259
+ * a number, out of range) is no setting at all.
260
+ */
261
+ export declare function controlPortFromEnv(env?: NodeJS.ProcessEnv): number | undefined;
262
+ /**
263
+ * The one reading of "which port and token does this project use", shared by
264
+ * the recorder (which binds it) and by {@link ensureDaemon} (which checks a
265
+ * running recorder against it). They used to read `BIR_CONTROL_PORT`
266
+ * differently: the recorder took an empty or invalid value as "the default
267
+ * port, pinned", the check ignored it and used the project's port — and every
268
+ * session start then "replaced" a recorder that could only ever bind the
269
+ * default port again. An invalid value is now ignored by both, and `0` means
270
+ * "any free port" to both (see {@link ProjectEndpoint.ephemeral}).
271
+ */
272
+ export declare function projectEndpoint(cwd: string, env?: NodeJS.ProcessEnv, sidecar?: InstalledSidecar): ProjectEndpoint;
89
273
  /**
90
274
  * The control server for `cwd`, started in the background if none is live.
91
275
  *
92
- * Idempotent: a live one is returned as-is. Throws when a fresh process did
93
- * not come up in time, with the tail of its log, because "it did not start" is
94
- * the one message that must carry its own diagnosis.
276
+ * Idempotent: a live one is returned as-is — unless it is an older version
277
+ * with nothing mid-run (`restartOnSkew`; with `probeIdle`, a legacy
278
+ * recorder whose "mid-run" is a run nobody will finish counts as idle), or a
279
+ * background recorder that the project's current hooks do not reach
280
+ * (another port or token). Any call but a revive ends a `bir down`.
281
+ *
282
+ * WHAT A REINSTALL MAY NOT BREAK. Claude Code reads a session's hooks when the
283
+ * session starts and keeps them: every window opened before `bir install
284
+ * --port N` or `bir uninstall` + `bir setup` still posts to the old port with
285
+ * the old token, until it is closed. The old recorder is what answers them. So
286
+ *
287
+ * - another **port**: the old recorder is left running for the sessions it
288
+ * has seen, and a second one is started on the new port — two ports, no
289
+ * conflict. The new one takes the discovery file; the old one steps back
290
+ * from it, is recorded as superseded (`listSupersededRecorders`, so
291
+ * `bir status`/`bir down` can find it), writes a log of its own, and
292
+ * exits once it has had nothing left to serve for a day
293
+ * (recorder-main.ts). A recorder that has never seen a session is simply
294
+ * replaced;
295
+ * - another **token** on the same port: only one process can listen there.
296
+ * It is replaced between turns (a restart then costs the old windows their
297
+ * next hooks, and gives every new one working hooks), never mid-run.
298
+ *
299
+ * A recorder that is alive but slow to answer is never replaced and never
300
+ * duplicated on its own port: this throws {@link RecorderBusyError} instead.
301
+ *
302
+ * Throws when a fresh process did not come up within the budget, with the
303
+ * tail of its log, because "it did not start" is the one message that must
304
+ * carry its own diagnosis.
95
305
  */
96
306
  export declare function ensureDaemon(cwd: string, opts?: EnsureOptions): Promise<EnsureResult>;
307
+ /**
308
+ * Mark `cwd`'s recorder as stopped on purpose. {@link stopDaemon} does it; a
309
+ * revive then leaves the project without a recorder until something that
310
+ * asks for one on purpose — a session start, `bir up`, `bir setup` — clears
311
+ * it. Never throws.
312
+ */
313
+ export declare function markDown(cwd: string): void;
314
+ /** Remove the {@link markDown} marker. Never throws. */
315
+ export declare function clearDownMarker(cwd: string): void;
316
+ /** True while `cwd`'s recorder is marked as stopped on purpose. */
317
+ export declare function isMarkedDown(cwd: string): boolean;
318
+ export interface ReviveResult {
319
+ outcome:
320
+ /** The project has no install record: nothing is started for it. */
321
+ "not_installed"
322
+ /** `bir down` stopped it on purpose, and nothing started it since. */
323
+ | "stopped_on_purpose"
324
+ /** A recorder was already there (another revive, or a session start, won). */
325
+ | "running"
326
+ /** This call started it. */
327
+ | "started"
328
+ /** Alive but not answering: left alone. */
329
+ | "busy" | "failed";
330
+ pid?: number;
331
+ error?: string;
332
+ }
333
+ /**
334
+ * `bir-hooks revive --cwd <project>` (K1): the recorder of an installed
335
+ * project, started again when it is gone.
336
+ *
337
+ * Every hook but SessionStart is an HTTP hook to the recorder, and a refused
338
+ * one is shown in the session as a hook error — on every prompt and every tool
339
+ * call — until the next SessionStart. A recorder killed mid-session (an
340
+ * antivirus, the OOM killer, a restart that missed its budget) used to stay
341
+ * gone until then. A bound `bir-proxy` or `bir-scenario` that finds it
342
+ * refusing connections with its pid dead spawns this, detached and rate
343
+ * limited, so the window of refused hooks is seconds.
344
+ *
345
+ * {@link ensureDaemon} with `restartOnSkew: false`, under the same start lock,
346
+ * so several revives at once start one recorder. Only for a project with an
347
+ * install record, and never after `bir down` (the down marker). Started even
348
+ * while the runner is off: the off recorder is what answers every hook `{}`.
349
+ * Never throws.
350
+ */
351
+ export declare function reviveDaemon(cwd: string, opts?: Pick<EnsureOptions, "hooksScript" | "env" | "timeoutMs">): Promise<ReviveResult>;
352
+ /**
353
+ * The environment a background recorder gets: the caller's, marked as a
354
+ * daemon, told its project, and without the secrets it has no use for.
355
+ *
356
+ * Nor `BIR_OFF` (K2), in any spelling: the recorder serves every window of
357
+ * the project, and a `BIR_OFF=1` in the environment of whichever window
358
+ * happened to start it switched all of them off for the recorder's life. That
359
+ * window's SessionStart hook tells the recorder its own session is off
360
+ * instead (ensure-hook.ts); the flag files still switch everything.
361
+ */
362
+ export declare function daemonEnv(base: NodeJS.ProcessEnv, cwd: string, logFile: string): NodeJS.ProcessEnv;
97
363
  /**
98
364
  * Stop the control server for `cwd`.
99
365
  *
@@ -103,14 +369,130 @@ export declare function ensureDaemon(cwd: string, opts?: EnsureOptions): Promise
103
369
  * the polite route exists. Only a process that answers `/health` as the
104
370
  * recorder is ever signalled: a discovery file survives a crash, and its pid
105
371
  * is recycled — on Windows quickly — so "the pid in the file is alive" is not
106
- * evidence that it is ours.
372
+ * evidence that it is ours. One that is alive but too busy to answer is asked
373
+ * and waited for, never signalled.
107
374
  */
108
375
  export declare function stopDaemon(cwd: string, opts?: {
109
376
  timeoutMs?: number;
377
+ /**
378
+ * Mark the project's recorder as stopped on purpose (default true), so a
379
+ * proxy that finds it gone does not bring it back (`bir-hooks revive`)
380
+ * until a session start, `bir up` or `bir setup` asks for one. A restart
381
+ * inside {@link ensureDaemon} passes false.
382
+ */
383
+ markDown?: boolean;
110
384
  }): Promise<{
111
385
  stopped: boolean;
112
386
  pid?: number;
113
387
  stale?: boolean;
114
388
  forced?: boolean;
115
389
  }>;
390
+ /**
391
+ * Stop one superseded recorder (see {@link listSupersededRecorders}) the same
392
+ * way {@link stopDaemon} stops the project's: asked, and signalled only when
393
+ * it answers `/health` as its own pid. Its record is removed once it is gone
394
+ * (its own exit does that too), or when nothing answers as it.
395
+ */
396
+ export declare function stopSupersededRecorder(record: SupersededRecorder, opts?: {
397
+ timeoutMs?: number;
398
+ }): Promise<{
399
+ stopped: boolean;
400
+ pid: number;
401
+ stale?: boolean;
402
+ forced?: boolean;
403
+ }>;
404
+ /**
405
+ * Stop every superseded recorder of `cwd` (or of every project, without
406
+ * one). For `bir down` and `bir uninstall`; the project's own recorder is
407
+ * {@link stopDaemon}'s.
408
+ */
409
+ export declare function stopSupersededRecorders(cwd?: string, opts?: {
410
+ timeoutMs?: number;
411
+ }): Promise<Array<{
412
+ record: SupersededRecorder;
413
+ stopped: boolean;
414
+ stale?: boolean;
415
+ forced?: boolean;
416
+ }>>;
417
+ /**
418
+ * Why the background recorder for a project could not bind the port its
419
+ * hooks name. Written by that recorder as it exits, read by `bir doctor` and
420
+ * the next SessionStart, removed by the first recorder that binds it.
421
+ */
422
+ export interface PortProblem {
423
+ port: number;
424
+ /** `EADDRINUSE` or `EACCES`. */
425
+ code: string;
426
+ /**
427
+ * `excluded`: the system reserved the port (Windows/Hyper-V/WSL ranges).
428
+ * `foreign`: another program answers there. `unauthorized`: a listener
429
+ * that refused this project's token — possibly a recorder from an earlier
430
+ * install. `other-project`: another project's recorder. `unknown`: busy,
431
+ * and nothing answered.
432
+ */
433
+ holder: "excluded" | "foreign" | "unauthorized" | "other-project" | "unknown";
434
+ at: number;
435
+ /** One sentence for a person. */
436
+ detail: string;
437
+ /** What to run. */
438
+ fix: string;
439
+ }
440
+ export declare function readPortProblem(cwd: string): PortProblem | undefined;
441
+ export declare function writePortProblem(cwd: string, problem: PortProblem): void;
442
+ export declare function clearPortProblem(cwd: string): void;
443
+ /**
444
+ * The log a background recorder writes its audit lines to.
445
+ *
446
+ * Three things the plain stream did not do. It never fails the process: a
447
+ * file (or a logs directory) that cannot be opened or written — a full disk,
448
+ * a read-only or locked profile — used to be an unhandled `'error'` that
449
+ * killed the recorder at start, and with it every hook of the session. Now the
450
+ * lines are dropped instead. It comes back: a failure is usually brief (the
451
+ * disk full for a minute, an antivirus scan holding the file), and a recorder
452
+ * that runs for days must not stay silent for all of them — the minute tick
453
+ * ({@link rotateIfLarge}) opens the file again. And it stays bounded while the
454
+ * recorder runs for weeks: rolled to `.1` once it passes
455
+ * {@link LOG_ROTATE_BYTES}, not only when a new one starts.
456
+ *
457
+ * And it shares a path safely. Two recorders of one project can run at once
458
+ * (a reinstall moved the hooks to another port while a window was open), and
459
+ * `ensureDaemon` rolls the log before a start. Whoever rolled it, the other
460
+ * writer's stream follows the renamed file — all its lines went to `.1`, which
461
+ * then grew without bound, and on Windows the held `.1` made every later roll
462
+ * fail. So the minute tick first checks that the file at the path is the one
463
+ * it writes, and opens the path again when it is not. A recorder superseded
464
+ * like that also moves to a file of its own ({@link moveTo}).
465
+ */
466
+ export declare class DaemonLog {
467
+ private stream?;
468
+ /** The descriptor {@link stream} writes to, once it is open. */
469
+ private fd?;
470
+ private current;
471
+ private readonly maxBytes;
472
+ /** Set once lines have been dropped, so the reopen can say so. */
473
+ private droppedSince?;
474
+ constructor(path: string, maxBytes?: number);
475
+ /** The file lines go to now. */
476
+ get path(): string;
477
+ /** Open the file and point `console` (every `logLine`) at it. Never throws. */
478
+ open(): void;
479
+ /**
480
+ * Write to `path` from now on — a superseded recorder's own file, so the
481
+ * project's log keeps one writer and one rotation. Never throws.
482
+ */
483
+ moveTo(path: string): void;
484
+ private silence;
485
+ private attach;
486
+ /**
487
+ * Roll the file to `.1` if it is past the limit, and open it again if it
488
+ * failed earlier, or if the file at the path is no longer the one written
489
+ * (another writer rolled or removed it). True when it rolled. Never throws.
490
+ */
491
+ rotateIfLarge(): boolean;
492
+ /** Whether `onDisk` is the file this log's stream writes (true when it cannot tell). */
493
+ private writes;
494
+ /** After a failure: try the file again. A stream that fails again just stays silent. */
495
+ private reopen;
496
+ close(): void;
497
+ }
116
498
  //# sourceMappingURL=daemon.d.ts.map