@basein/runner 0.3.20 → 0.3.22

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 (51) hide show
  1. package/README.md +6 -5
  2. package/dist/bin/bir-hooks.d.ts +17 -19
  3. package/dist/bin/bir-hooks.js +17 -19
  4. package/dist/bin/bir-scenario.js +1 -1
  5. package/dist/bin/bir.js +1 -21
  6. package/dist/bin/investigate.d.ts +0 -9
  7. package/dist/bin/investigate.js +15 -43
  8. package/dist/bin/scenario-edit.js +13 -44
  9. package/dist/control/recorder-main.d.ts +0 -15
  10. package/dist/control/recorder-main.js +22 -42
  11. package/dist/control/server.d.ts +24 -137
  12. package/dist/control/server.js +107 -1001
  13. package/dist/record/recorder.d.ts +11 -142
  14. package/dist/record/recorder.js +0 -3
  15. package/dist/record/remote-recorder.d.ts +1 -9
  16. package/dist/record/remote-recorder.js +3 -49
  17. package/dist/record/switchable-recorder.js +0 -2
  18. package/dist/replay/builtins.d.ts +45 -9
  19. package/dist/replay/builtins.js +131 -59
  20. package/dist/replay/bundle.d.ts +2 -2
  21. package/dist/replay/bundle.js +3 -3
  22. package/dist/replay/controller.d.ts +34 -125
  23. package/dist/replay/controller.js +171 -528
  24. package/dist/replay/derive.d.ts +0 -10
  25. package/dist/replay/derive.js +0 -3
  26. package/dist/replay/handover.d.ts +2 -37
  27. package/dist/replay/handover.js +38 -106
  28. package/dist/replay/logic.d.ts +0 -11
  29. package/dist/replay/logic.js +0 -24
  30. package/dist/replay/plan.d.ts +43 -163
  31. package/dist/replay/plan.js +144 -556
  32. package/dist/replay/safety.d.ts +4 -5
  33. package/dist/replay/safety.js +7 -7
  34. package/dist/replay/source-run.d.ts +15 -24
  35. package/dist/replay/source-run.js +25 -58
  36. package/dist/replay/targets.d.ts +11 -21
  37. package/dist/replay/targets.js +30 -69
  38. package/dist/replay/types.d.ts +9 -96
  39. package/dist/replay/types.js +3 -16
  40. package/dist/util/service-headers.d.ts +1 -2
  41. package/dist/util/service-headers.js +1 -2
  42. package/docs/calculatedReplay.md +30 -81
  43. package/docs/calculatedReplayGuide.md +17 -34
  44. package/package.json +1 -1
  45. package/production.md +7 -36
  46. package/dist/replay/condition-words.d.ts +0 -28
  47. package/dist/replay/condition-words.js +0 -59
  48. package/dist/replay/flatten.d.ts +0 -161
  49. package/dist/replay/flatten.js +0 -222
  50. package/dist/replay/reads-only.d.ts +0 -29
  51. package/dist/replay/reads-only.js +0 -261
package/README.md CHANGED
@@ -360,13 +360,13 @@ same command, the same file, the same kind of arguments); any other call runs as
360
360
  written and is asked about as usual. A *directly executed* step never reaches the
361
361
  permission system at all — it is dispatched to a `bir-proxy` and called on a
362
362
  connection that is already open, or, for Claude Code's own `Read`, `Glob`,
363
- `Grep`, `Edit`, `Write` and `Bash`, run by the recorder itself in the project
364
- folder — and `run_scenario`, the call that starts the
363
+ `Grep`, `Edit`, `Write` and `Bash`, run by the recorder itself from the project
364
+ folder, on any file the agent could reach — and `run_scenario`, the call that starts the
365
365
  run, is answered `allow` while the recorder has announced the plan. In plan mode
366
366
  nothing is announced and nothing is approved. Set `BIR_REPLAY_ALLOW_SERVERS` to
367
367
  the servers you are content to have called unattended (it also covers a steered
368
- plan's recovery), `BIR_BUILTIN_TOOLS` and `BIR_BASH_ALLOW` to the built-ins and
369
- commands, and read [the guide's §5.1](docs/calculatedReplayGuide.md)
368
+ plan's recovery), `BIR_BUILTIN_TOOLS`, `BIR_PATH_ALLOW` and `BIR_BASH_ALLOW` to
369
+ the built-ins, folders and commands, and read [the guide's §5.1](docs/calculatedReplayGuide.md)
370
370
  before enabling it anywhere that matters.
371
371
 
372
372
  What it buys: on a scenario whose every step is a wrapped MCP tool or one of
@@ -489,8 +489,9 @@ what to do when they register, so there is one switch and no second copy of it:
489
489
  |---|---|
490
490
  | `BIR_REPLAY` | `0` turns replay off and any other value on, over the project's stored switch (`bir replay on` / `off`; a first plain `bir install` stores off). Nothing below matters while it is off. |
491
491
  | `BIR_REPLAY_ALLOW_SERVERS` | Server keys bir may call unattended: **direct** execution, and a steered plan's recovery. Unset means the list `bir replay allow` stored for the project, else every wrapped server; narrowing it is the recommendation. |
492
- | `BIR_BUILTIN_TOOLS` | Built-in tools bir may execute itself during a replay — in the session's own project directory, and confined to it. Default all six: `Read,Glob,Grep,Edit,Write,Bash` (up to 0.3.7 only the read-only `Read,Glob,Grep`). A runner-side step never reaches Claude Code's permission prompts, so leave off the list any tool you want the model to call itself. `none` turns it off, and built-in steps are steered in the session. |
492
+ | `BIR_BUILTIN_TOOLS` | Built-in tools bir may execute itself during a replay — from the session's own project directory; which folders the file tools may touch is `BIR_PATH_ALLOW`. Default all six: `Read,Glob,Grep,Edit,Write,Bash` (up to 0.3.7 only the read-only `Read,Glob,Grep`). A runner-side step never reaches Claude Code's permission prompts, so leave off the list any tool you want the model to call itself. `none` turns it off, and built-in steps are steered in the session. |
493
493
  | `BIR_BASH_ALLOW` | Command prefixes a runner-side `Bash` step may run. Unset means any command (`*`); a list such as `git,npm test` narrows it; `none` allows no command, and `Bash` steps are then steered in the session. A plan's Bash steps share a working directory, as Claude Code's do (variables and functions do not persist, also as in Claude Code). |
494
+ | `BIR_PATH_ALLOW` | Folders a runner-side `Read`, `Glob`, `Grep`, `Edit` or `Write` may touch. Unset means any folder (`*`), as the agent's own tools: a plan that reads the agent's memory file under `~/.claude` runs (from 0.3.21; up to 0.3.20 only the project directory). `project` keeps them inside the session's project directory; add folders after a comma, `~` for your home folder: `project,~/.claude`. `none` allows no folder, and file steps are then steered in the session. |
494
495
  | `BIR_DERIVE_API_KEY` | **Optional.** Working out what a new request is about — which fleet, which file, which date — is done for you by the service on its own key, as long as you are signed in. Set this (an Anthropic key) only to keep that reading on your machine, on your key. Signed out *and* unset, a scenario whose values change between requests is declined rather than replayed on stale ones. |
495
496
  | `ANTHROPIC_API_KEY` | Not used by the background recorder, which has Claude Code's environment and would bill your own key — unless `BIR_USE_ANTHROPIC_API_KEY=1`. A `bir-hooks` run by hand in a terminal uses that terminal's key. |
496
497
  | `BIR_DERIVE_MODEL` | Derivation model, when you set a key of your own (default `claude-haiku-4-5-20251001`). |
@@ -62,9 +62,9 @@
62
62
  * BIR_REPLAY_ALLOW_SERVERS comma-separated server keys eligible for *direct*
63
63
  * execution. Unset means every wrapped server
64
64
  * BIR_BUILTIN_TOOLS comma-separated built-ins the runner may execute
65
- * itself during a replay, in the session's own
66
- * project directory and confined to it
67
- * (replay/builtins.ts). Default all six:
65
+ * itself during a replay, from the session's own
66
+ * project directory (replay/builtins.ts; which
67
+ * folders: BIR_PATH_ALLOW). Default all six:
68
68
  * Read,Glob,Grep,Edit,Write,Bash. A runner-side
69
69
  * step never reaches the host's permission
70
70
  * prompts; Read,Glob,Grep is the read-only set
@@ -73,6 +73,13 @@
73
73
  * BIR_BASH_ALLOW command prefixes a runner-side Bash step may run
74
74
  * (`*` for all). Unset means `*`; `none` means no
75
75
  * command, and Bash steps are steered
76
+ * BIR_PATH_ALLOW folders a runner-side Read, Glob, Grep, Edit or
77
+ * Write may touch (`*` for any). Unset means `*`:
78
+ * any path, as the agent's own tools (up to 0.3.20
79
+ * the project directory alone). `project` is the
80
+ * session's project directory, `~` the home
81
+ * folder: `project,~/.claude`. `none` means no
82
+ * folder, and the file steps are steered
76
83
  * BIR_DERIVE_API_KEY OPTIONAL. Derivation — reading what this turn acts
77
84
  * on — is done by the service for a signed-in
78
85
  * runner. Set this (an Anthropic key) only to keep
@@ -97,22 +104,13 @@
97
104
  * found (default 5; 0 off)
98
105
  * BIR_INTENT_SOURCES which text a step's intent may come from
99
106
  * (default text,thinking,tool)
100
- * BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40).
101
- * Separate from BIR_INTENT_MATCHES_PER_TURN: arming
102
- * is a claim on the turn, counting hits is not
103
- * BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 4000).
104
- * Not a wait of its own: PreToolUse waits for a
105
- * probe only while arming is on (the default from
106
- * 0.3.19; off with BIR_SEGMENT_ARM=0 or
107
- * BIR_INTENT_ARM=0), and then at most
108
- * BIR_INTENT_ARM_BUDGET_MS (default 500)
109
- * BIR_SEGMENT_ARM unset or 1 (the default from 0.3.19): a segment
110
- * the service hands out runs mid-task — one found
111
- * by repeated work, or a half after a plan's
112
- * decision — and so may a whole plan whose intent
113
- * a step matches (BIR_INTENT_ARM follows it unless
114
- * set). 0 is observe-only: what would have armed
115
- * is logged and nothing is replaced
107
+ * BIR_INTENT_ARM 1 lets a whole plan whose intent matches the
108
+ * agent's reasoning mid-turn arm in the place of the
109
+ * call. Unset (or 0) is off: no probe is sent and
110
+ * nothing arms mid-turn
111
+ * BIR_INTENT_ARM_BUDGET_MS how long a call waits for that probe (default 500)
112
+ * BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40)
113
+ * BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 1500)
116
114
  * BIR_REPLAY_BUDGET_MS whole-plan ceiling, direct mode (unset: the
117
115
  * controller's 100000, BIR_REPLAY_PLAN_BUDGET_MS)
118
116
  * BIR_STEP_TIMEOUT_MS one direct tools/call (default 60000)
@@ -62,9 +62,9 @@
62
62
  * BIR_REPLAY_ALLOW_SERVERS comma-separated server keys eligible for *direct*
63
63
  * execution. Unset means every wrapped server
64
64
  * BIR_BUILTIN_TOOLS comma-separated built-ins the runner may execute
65
- * itself during a replay, in the session's own
66
- * project directory and confined to it
67
- * (replay/builtins.ts). Default all six:
65
+ * itself during a replay, from the session's own
66
+ * project directory (replay/builtins.ts; which
67
+ * folders: BIR_PATH_ALLOW). Default all six:
68
68
  * Read,Glob,Grep,Edit,Write,Bash. A runner-side
69
69
  * step never reaches the host's permission
70
70
  * prompts; Read,Glob,Grep is the read-only set
@@ -73,6 +73,13 @@
73
73
  * BIR_BASH_ALLOW command prefixes a runner-side Bash step may run
74
74
  * (`*` for all). Unset means `*`; `none` means no
75
75
  * command, and Bash steps are steered
76
+ * BIR_PATH_ALLOW folders a runner-side Read, Glob, Grep, Edit or
77
+ * Write may touch (`*` for any). Unset means `*`:
78
+ * any path, as the agent's own tools (up to 0.3.20
79
+ * the project directory alone). `project` is the
80
+ * session's project directory, `~` the home
81
+ * folder: `project,~/.claude`. `none` means no
82
+ * folder, and the file steps are steered
76
83
  * BIR_DERIVE_API_KEY OPTIONAL. Derivation — reading what this turn acts
77
84
  * on — is done by the service for a signed-in
78
85
  * runner. Set this (an Anthropic key) only to keep
@@ -97,22 +104,13 @@
97
104
  * found (default 5; 0 off)
98
105
  * BIR_INTENT_SOURCES which text a step's intent may come from
99
106
  * (default text,thinking,tool)
100
- * BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40).
101
- * Separate from BIR_INTENT_MATCHES_PER_TURN: arming
102
- * is a claim on the turn, counting hits is not
103
- * BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 4000).
104
- * Not a wait of its own: PreToolUse waits for a
105
- * probe only while arming is on (the default from
106
- * 0.3.19; off with BIR_SEGMENT_ARM=0 or
107
- * BIR_INTENT_ARM=0), and then at most
108
- * BIR_INTENT_ARM_BUDGET_MS (default 500)
109
- * BIR_SEGMENT_ARM unset or 1 (the default from 0.3.19): a segment
110
- * the service hands out runs mid-task — one found
111
- * by repeated work, or a half after a plan's
112
- * decision — and so may a whole plan whose intent
113
- * a step matches (BIR_INTENT_ARM follows it unless
114
- * set). 0 is observe-only: what would have armed
115
- * is logged and nothing is replaced
107
+ * BIR_INTENT_ARM 1 lets a whole plan whose intent matches the
108
+ * agent's reasoning mid-turn arm in the place of the
109
+ * call. Unset (or 0) is off: no probe is sent and
110
+ * nothing arms mid-turn
111
+ * BIR_INTENT_ARM_BUDGET_MS how long a call waits for that probe (default 500)
112
+ * BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40)
113
+ * BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 1500)
116
114
  * BIR_REPLAY_BUDGET_MS whole-plan ceiling, direct mode (unset: the
117
115
  * controller's 100000, BIR_REPLAY_PLAN_BUDGET_MS)
118
116
  * BIR_STEP_TIMEOUT_MS one direct tools/call (default 60000)
@@ -488,7 +488,7 @@ class BadArguments extends Error {
488
488
  }
489
489
  const TARGET = {
490
490
  type: "string",
491
- description: "A scn_ id, or the run_ id of a whole-run scenario (a sub-task scenario has only its scn_ id) — as investigate and scenario_show print them.",
491
+ description: "A scn_ id, or the run_ id of the recording it was calculated from — as investigate and scenario_show print them.",
492
492
  };
493
493
  const STEP = {
494
494
  type: "integer",
package/dist/bin/bir.js CHANGED
@@ -114,7 +114,7 @@ Commands:
114
114
  a scenario: intent, params, steps (--step: just that one)
115
115
  scenario calc <runId|scnId> [--force [--discard-edits]]
116
116
  calculate (or re-derive) a run's scenario; a scn_ id
117
- (a sub-task's too) is recalculated in place, with --force
117
+ is recalculated in place, with --force
118
118
  scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
119
119
  try a change to one step against the recording; saves nothing
120
120
  scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
@@ -3118,20 +3118,6 @@ async function doctor(args) {
3118
3118
  "For headless runs (`claude -p`) allow them in permissions.allow or --allowedTools, or set " +
3119
3119
  "BIR_CORRELATION_DECISION=allow where the recorder starts");
3120
3120
  }
3121
- // Whether a segment the service hands out may run mid-task (segmented.md
3122
- // R-LIFE-8). On by default from 0.3.19 (the service's segmentsbreakdown.md
3123
- // SB-21); either state must be visible without reading a log, and an older
3124
- // recorder still running here keeps the old default until it is replaced.
3125
- // A segment starts only where the probe is waited on, so `intentArm: false`
3126
- // (BIR_INTENT_ARM=0) starts nothing mid-task, segments included; a
3127
- // recorder older than that field says nothing of it.
3128
- notes.push(health.segmentArm === true && health.intentArm !== false
3129
- ? "segments run mid-task — recurring sub-tasks, and the halves after a plan's decision — and so may whole plans (the default; BIR_SEGMENT_ARM=0 turns this off)"
3130
- : health.segmentArm === true
3131
- ? "segments are observed only — nothing starts mid-task (BIR_INTENT_ARM=0 where the recorder started)"
3132
- : theirs && theirs !== VERSION
3133
- ? `segments are observed only — this recorder (${theirs}) is older than this bir, which runs them by default: \`bir up --restart\``
3134
- : `segments are observed only — nothing is replaced (BIR_SEGMENT_ARM=0 where the recorder started)${health.intentArm === true ? "; whole plans may still start mid-task (BIR_INTENT_ARM=1)" : ""}`);
3135
3121
  const sessions = health.sessions ?? [];
3136
3122
  const probes = sessions.reduce((n, s) => n + (s.intent?.probes ?? 0), 0);
3137
3123
  const armed = sessions.reduce((n, s) => n + (s.intent?.armed ?? 0), 0);
@@ -3721,12 +3707,6 @@ async function replayCommand(args) {
3721
3707
  }
3722
3708
  out(`response ${JSON.stringify(result.responseModel ?? {})}`);
3723
3709
  out(`steps ${result.steps ?? 0}${result.partial ? " (partial — budget reached)" : ""}`);
3724
- // A plan's decision (the service's segmentsbreakdown.md SB-6): the steps
3725
- // from there are the half the recording took, so the run stops before them.
3726
- if (result.stopped?.reason === "decision") {
3727
- out(`stopped before step ${result.stopped.step}${result.stopped.tool ? ` (${result.stopped.tool})` : ""}: ` +
3728
- "the plan's decision, which the agent makes — the steps from there did not run");
3729
- }
3730
3710
  return 0;
3731
3711
  }
3732
3712
  // ── wrap ───────────────────────────────────────────────────────────────────
@@ -245,13 +245,6 @@ interface ServiceInvestigation {
245
245
  runId?: string | null;
246
246
  } | null;
247
247
  corrections?: ServiceCorrection[];
248
- /**
249
- * The step the plan stops in front of so that the agent decides, and the
250
- * condition in words (the service's segmentsbreakdown.md SB-6). Null when
251
- * the plan has none; absent from an older service.
252
- */
253
- decisionStep?: number | null;
254
- decisionText?: string | null;
255
248
  } | null;
256
249
  executions?: Array<{
257
250
  id: string;
@@ -272,8 +265,6 @@ interface ServiceInvestigation {
272
265
  status: string;
273
266
  error?: string;
274
267
  }> | null;
275
- /** Where a `fell_back` run handed over, and why (`decision`, …): null for any other run. */
276
- fallbackKind?: string | null;
277
268
  /** The repeat the agent redid the task in, and its own calls there (AC1). */
278
269
  redoneByRunId?: string | null;
279
270
  redoneOwnCalls?: number | null;
@@ -35,7 +35,6 @@ const INCIDENTS = new Set([
35
35
  "replay.arm_failed",
36
36
  "replay.derive_failed",
37
37
  "replay.compose_failed",
38
- "replay.flatten_failed",
39
38
  "replay.thread_failed",
40
39
  "replay.step_failed",
41
40
  "replay.failed",
@@ -203,8 +202,8 @@ export function findTurn(turns, id) {
203
202
  /** The runner's own gate vocabulary, with the cause and the fix (guide §9). */
204
203
  export const DECLINES = {
205
204
  replay_disabled: {
206
- cause: "calculated replay was off in this bir-hooks (BIR_REPLAY=0), or a sub-task was handed out to a recorder that only observes sub-tasks (BIR_SEGMENT_ARM=0, or a recorder older than 0.3.19).",
207
- fix: "start bir-hooks with BIR_REPLAY=1 (the default); for sub-tasks leave BIR_SEGMENT_ARM unset (on from 0.3.19), then `bir up --restart`.",
205
+ cause: "calculated replay was off in this bir-hooks (BIR_REPLAY=0).",
206
+ fix: "start bir-hooks with BIR_REPLAY=1 (the default).",
208
207
  },
209
208
  not_ready: {
210
209
  cause: "the matched recording had no ready scenario at that moment.",
@@ -222,16 +221,12 @@ export const DECLINES = {
222
221
  },
223
222
  known_bad_first_step: {
224
223
  cause: "the chain's first step is parked after repeated failures.",
225
- fix: "wait: the service retries it once an hour for a day, then repairs the plan from the next run of the prompt — unless the plan has steps edited by hand, or is a sub-task, which it does not repair on its own. To repair now: `bir scenario calc <scnId> --force` (add --discard-edits to drop hand edits), or fix the step: `bir scenario edit <scnId> --step 0 --input-logic <file>`. `bir investigate <scnId>` names the scenario id.",
224
+ fix: "wait: the service retries it once an hour for a day, then repairs the plan from the next run of the prompt — unless the plan has steps edited by hand, which it does not repair on its own. To repair now: `bir scenario calc <scnId> --force` (add --discard-edits to drop hand edits), or fix the step: `bir scenario edit <scnId> --step 0 --input-logic <file>`. `bir investigate <scnId>` names the scenario id.",
226
225
  },
227
226
  nondeterministic_first_step: {
228
227
  cause: "the chain's first step needs a judgement the calculation could not write code for, so the plan cannot start.",
229
228
  fix: "if its code only has a slip, fix the step yourself: read it with `bir scenario show <scnId> --step 0`, write the input logic to a file, and run `bir scenario edit <scnId> --step 0 --input-logic <file>`; the service checks it against the recording before it saves it. A real judgement — a choice with no fixed rule — stays with the agent until a model step exists (plan-services.md W3.1).",
230
229
  },
231
- unusable_first_step: {
232
- cause: "the first step calls a sub-task that is gone, switched off or stale.",
233
- fix: "enable or recalculate that sub-task, or recalculate this scenario.",
234
- },
235
230
  missing_target: {
236
231
  cause: "the plan needs a target — a value a prompt must name, such as a file, a table or a region — and the prompt did not name it. A replay never takes a target from the recording.",
237
232
  fix: 'name it in the prompt. If the value is the same every time — a database, a host, a working folder — it is a setting, not a target: `bir scenario edit <scnId> --param <name> --kind setting --note "why"` lets the runner use its recorded value when a prompt names none (`bir scenario show <scnId>` lists the parameters and their kinds).',
@@ -246,11 +241,6 @@ export const DECLINES = {
246
241
  cause: "the plan was not ready before the hook that would have announced it had to answer: the prompt hook waits 7.5 s in all, match included, and a tool call that starts a plan mid-task waits 1.5 s. What arming waits for is the reading of the prompt's values (the derivation). A plan that was not announced is never run.",
247
242
  fix: "run the prompt again. If it keeps happening the reading is slow: the recorder's log (`bir status` names the file) shows when its `replay.derived` line landed, and a dry replay shows what the service's reading took, under `reading` (`bir scenario replay <scnId> --prompt \"…\" --dry --json`). BIR_HOOK_DERIVE_BUDGET_MS does not help here, because the hook's own limit was reached first. To read values on this machine instead of asking the service, set BIR_DERIVE_API_KEY where Claude Code starts.",
248
243
  },
249
- // Nor this one: the control server's own rule (the service's offer1.md O8).
250
- would_repeat: {
251
- cause: "a plan the intent check matched would have started after the agent's call, not in its place, and its steps call that same tool: it would have made the call a second time — an e-mail sent twice (issue fx2). Never twice: it was not started, and the agent went on.",
252
- fix: "nothing to fix in the plan. A scenario starts in the call's place only when the agent's call is its first step, value for value; one recorded in the order the agent works in (the agent's own run after a decision becomes one) is what starts there.",
253
- },
254
244
  // Nor is this one: the match itself was late, so nothing was armed at all.
255
245
  match_late: {
256
246
  cause: "the service's answer to the match landed too late: after the prompt hook had stopped waiting for it (3 s, BIR_MATCH_BUDGET_MS); while prompts were not waiting on the match at all — after one slow answer they stop waiting for a few minutes (`replay.match_gate state=holding`); or with the hook not waiting from the start, because a failure to reach the service was still on record (a `service.degraded` line, perhaps hours before, and no `service.recovered` until after this prompt) or the match request's first attempt failed. The agent did the task itself.",
@@ -264,10 +254,6 @@ export const DECLINES = {
264
254
  cause: "the service matched the prompt and handed over no plan: this runner is older than its plans need.",
265
255
  fix: "update the runner (`bir update`), then run `bir setup` in each project that uses it; `bir doctor` says when the service is satisfied.",
266
256
  },
267
- flatten_failed: {
268
- cause: "the chain could not be flattened — a sub-task it calls no longer resolves.",
269
- fix: "recalculate the scenario.",
270
- },
271
257
  // The gates of a host that is not Claude Code (a LangChain application).
272
258
  host_records_only: {
273
259
  cause: "the runner in that application's folder is older than replay for LangChain needs, so the LangChain package only records.",
@@ -413,7 +399,7 @@ export function localFindings(turn) {
413
399
  const elsewhere = names.filter((n) => !EXECUTABLE_BUILTINS.has(n));
414
400
  const fixes = [];
415
401
  if (offList.length > 0) {
416
- fixes.push(`The runner runs ${offList.join(", ")} itself when ${offList.length === 1 ? "it is" : "they are"} on \`BIR_BUILTIN_TOOLS\` (unset means all six, from runner 0.3.8) and, for Bash, \`BIR_BASH_ALLOW\` is not \`none\`: fix those where the recorder starts, then \`bir up --restart\`.`);
402
+ fixes.push(`The runner runs ${offList.join(", ")} itself when ${offList.length === 1 ? "it is" : "they are"} on \`BIR_BUILTIN_TOOLS\` (unset means all six, from runner 0.3.8) and, for Bash, \`BIR_BASH_ALLOW\` is not \`none\` (for the file tools, \`BIR_PATH_ALLOW\`): fix those where the recorder starts, then \`bir up --restart\`.`);
417
403
  }
418
404
  if (elsewhere.length > 0) {
419
405
  fixes.push(`Put the data ${elsewhere.join(", ")} ${elsewhere.length === 1 ? "reaches" : "reach"} behind an MCP server, wrap it (\`bir install --server <name>\`), and record the prompt again so its steps are MCP calls.`);
@@ -439,11 +425,9 @@ export function localFindings(turn) {
439
425
  }
440
426
  for (const i of turn.incidents) {
441
427
  const fields = Object.fromEntries(Object.entries(i).filter(([k]) => k !== "at" && k !== "event"));
442
- // A stop at the plan's decision is the plan working as split: no fault.
443
- const decisionStop = i.event === "replay.handover" && i.kind === "decision";
444
428
  out.push({
445
429
  code: i.event.replace(/\./g, "_"),
446
- severity: i.event.endsWith("_failed") || i.event === "replay.failed" ? "problem" : decisionStop ? "info" : "warn",
430
+ severity: i.event.endsWith("_failed") || i.event === "replay.failed" ? "problem" : "warn",
447
431
  title: `${i.event}${i.error ? `: ${String(i.error)}` : ""}`,
448
432
  cause: Object.entries(fields)
449
433
  .map(([k, v]) => `${k}=${String(v)}`)
@@ -454,15 +438,13 @@ export function localFindings(turn) {
454
438
  : "the turn's cost could not be read from the transcript, or the runner has no price for the model: update it. Its report is unmeasured; run bir-hooks where the Claude Code transcript is readable."
455
439
  : i.event === "replay.handover" && i.kind === "other_branch"
456
440
  ? "this request was the other half of the task the plan holds (a step's test said so), and the agent did it; that run is how the service learns the other half."
457
- : decisionStop
458
- ? "nothing to fix: the plan stopped at its decision, as planned. It ran the steps before it, and the agent chose the way. A segment of the half it chose may have run next (a `replay.intent_armed` line with kind segment); a `run.fragment` line with decision=true is the agent's own work, from which the service makes the other half."
459
- : i.event === "replay.diverge" || i.event === "replay.handover"
460
- ? "the model left the script or the runner handed the turn back; the service section shows what the fallback cost."
461
- : i.event === "run.incomplete"
462
- ? "the recording is not the whole run, so it is never calculated and no longer catches its prompt; run the task again to record it whole."
463
- : i.event === "run.host_failed"
464
- ? "the application's run failed; its recording is closed with that error and never becomes a plan."
465
- : "see the guide's troubleshooting table for this line.",
441
+ : i.event === "replay.diverge" || i.event === "replay.handover"
442
+ ? "the model left the script or the runner handed the turn back; the service section shows what the fallback cost."
443
+ : i.event === "run.incomplete"
444
+ ? "the recording is not the whole run, so it is never calculated and no longer catches its prompt; run the task again to record it whole."
445
+ : i.event === "run.host_failed"
446
+ ? "the application's run failed; its recording is closed with that error and never becomes a plan."
447
+ : "see the guide's troubleshooting table for this line.",
466
448
  });
467
449
  }
468
450
  const r = turn.reported;
@@ -545,12 +527,6 @@ export function savedPct(turn) {
545
527
  return baseline > 0 ? r.savedUsd / baseline : null;
546
528
  }
547
529
  const short = (id) => (id ? id.slice(0, 13) : "—");
548
- /**
549
- * An execution's outcome, and where it handed the task over when the service
550
- * says (`fell_back · decision`). The service sends `fallbackKind: null` for a
551
- * run that did not hand over, and an older one sends nothing.
552
- */
553
- const outcomeOf = (e) => typeof e.fallbackKind === "string" && e.fallbackKind !== "" ? `${e.outcome} · ${e.fallbackKind}` : e.outcome;
554
530
  /** 4624 → "4,624", whatever the machine's locale. */
555
531
  const count = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
556
532
  const money = (v) => (v == null ? "—" : usd(v));
@@ -653,10 +629,6 @@ export function renderService(out, s, viewerIsOwner) {
653
629
  if (sc.held) {
654
630
  out(` HELD since ${when(sc.held.at ?? undefined)}${sc.held.reason ? ` (${sc.held.reason})` : ""}: not handed out — the agent does the task until the plan is corrected or let go${sc.held.runId ? `; the redo is ${sc.held.runId}` : ""}`);
655
631
  }
656
- if (typeof sc.decisionStep === "number") {
657
- const text = typeof sc.decisionText === "string" ? sc.decisionText.replace(/\s+/g, " ").trim() : "";
658
- out(` decision at step ${sc.decisionStep}: the plan stops in front of it, and the agent decides${text ? ` (${text.length > 160 ? `${text.slice(0, 160)}…` : text})` : ""}`);
659
- }
660
632
  for (const line of stepLines(sc.steps))
661
633
  out(` ${line}`);
662
634
  const correction = newestCorrection(sc.corrections);
@@ -674,7 +646,7 @@ export function renderService(out, s, viewerIsOwner) {
674
646
  if (ex.length > 0) {
675
647
  out(` executions (${ex.length}, newest first)`);
676
648
  for (const e of ex.slice(0, 10)) {
677
- out(` ${when(e.createdAt)} ${short(e.id)} ${outcomeOf(e).padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
649
+ out(` ${when(e.createdAt)} ${short(e.id)} ${e.outcome.padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
678
650
  const failed = e.steps?.filter((st) => st.status === "failed") ?? [];
679
651
  for (const st of failed)
680
652
  out(` step ${st.stepIndex} ${st.toolName ?? ""} failed${st.error ? `: ${st.error.slice(0, 120)}` : ""}`);
@@ -702,7 +674,7 @@ function stepLines(steps) {
702
674
  if (st.correctionId)
703
675
  parts.push(`${st.correctionKind ?? "changed"} by correction ${st.correctionId}`);
704
676
  if (parts.length > 0)
705
- lines.push(`step ${st.stepIndex} ${st.toolName ?? "(a sub-task)"}: ${parts.join("; ")}`);
677
+ lines.push(`step ${st.stepIndex} ${st.toolName ?? "?"}: ${parts.join("; ")}`);
706
678
  }
707
679
  return lines;
708
680
  }
@@ -809,7 +781,7 @@ export async function investigateCommand(args, deps) {
809
781
  if (rows.length === 0)
810
782
  out(" none yet");
811
783
  for (const e of rows) {
812
- out(` ${when(e.createdAt)} ${e.id} ${outcomeOf(e).padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
784
+ out(` ${when(e.createdAt)} ${e.id} ${e.outcome.padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
813
785
  out(` scenario ${e.scenarioId}${e.runId ? ` run ${e.runId}` : ""}${e.intent ? ` "${e.intent.slice(0, 60)}"` : ""}`);
814
786
  }
815
787
  out();
@@ -100,9 +100,8 @@ export async function scenarioEditCommand(args, deps) {
100
100
  }
101
101
  /**
102
102
  * The raw JSON, as it always was — scripts and the guide read it. A `scn_` id
103
- * goes to `GET /scenarios/:id`, which also serves sub-task scenarios (they have
104
- * no run of their own); `--step` narrows the print to that one step object,
105
- * with the scenario's id and `chainRevision` put in front of it. The revision
103
+ * goes to `GET /scenarios/:id`; `--step` narrows the print to that one step
104
+ * object, with the scenario's id and `chainRevision` put in front of it. The revision
106
105
  * is a scenario's, not a step's, and it is what `edit --revision` (and the MCP
107
106
  * `scenario_edit`'s `revision`) takes: a step printed without it sent the model
108
107
  * that had just read the step off to guess one (C20).
@@ -130,7 +129,7 @@ async function show(args, deps) {
130
129
  const served = reply.body;
131
130
  const scenario = served?.scenario;
132
131
  if (!scenario)
133
- return noWholeRunScenario(target, served?.segments, args, deps);
132
+ return noScenario(target, args, deps);
134
133
  if (!Array.isArray(scenario.steps)) {
135
134
  out(`${scenario.id ?? target} is ${scenario.state ?? "not ready"}, so it has no steps to show yet.`);
136
135
  return 1;
@@ -510,7 +509,7 @@ function inputPhrase(status, verifiable) {
510
509
  case "not_checked":
511
510
  return verifiable
512
511
  ? "not checked: the recorded call is plain text, not an object"
513
- : "not checked: the step comes after a sub-task call, whose outputs the check cannot rebuild";
512
+ : "not checked: the recording holds no call to check this step against";
514
513
  case "differs":
515
514
  return "differs from the recorded call";
516
515
  case "throws":
@@ -955,12 +954,10 @@ function paramUndone(answer, editId, target, args, deps) {
955
954
  * step from the recording, so the service refuses `--force` on a plan with
956
955
  * hand edits (D4) unless `--discard-edits` says to throw them away.
957
956
  *
958
- * A `run_` id posts `/recordings/runs/:id/calculate`, which reaches only the
959
- * run's **whole-run** scenario. A `scn_` id posts `/scenarios/:id/recalculate`
960
- * — the only way to recalculate a sub-task (segment) scenario from here, and
961
- * so the only way to take a segment's hand edits back once undo cannot (C12).
962
- * A scenario id always names one that exists, so it is always a
963
- * recalculation in place, and asks for `--force` as the run form does.
957
+ * A `run_` id posts `/recordings/runs/:id/calculate`. A `scn_` id posts
958
+ * `/scenarios/:id/recalculate`. A scenario id always names one that exists,
959
+ * so it is always a recalculation in place, and asks for `--force` as the run
960
+ * form does.
964
961
  */
965
962
  async function calc(args, deps) {
966
963
  const { out } = deps;
@@ -1106,28 +1103,13 @@ function localFailure(args, deps, code, text, extra = {}) {
1106
1103
  deps.out(args.json ? jsonText({ error: code, details: text, ...extra }) : text);
1107
1104
  return 1;
1108
1105
  }
1109
- /**
1110
- * A run with no whole-run scenario. When sub-task (segment) scenarios were
1111
- * calculated from it, they are listed with their `scn_` ids — the only ids
1112
- * they have — instead of suggesting a calculation, which would start a new
1113
- * whole-run plan beside them rather than reach the one the person meant (C21).
1114
- */
1115
- function noWholeRunScenario(target, segments, args, deps) {
1116
- const listed = (Array.isArray(segments) ? segments : []).filter((s) => typeof s?.id === "string");
1117
- if (listed.length === 0) {
1118
- return localFailure(args, deps, "no_scenario", `${target} has no calculated scenario yet — \`bir scenario calc ${target}\` first.`);
1119
- }
1120
- const range = (s) => typeof s.stepFrom === "number" && typeof s.stepTo === "number" ? ` steps ${s.stepFrom}–${s.stepTo} of the recording` : "";
1121
- const text = [
1122
- `${target} has no whole-run scenario, only sub-task scenarios — and a sub-task scenario is addressed by its own scn_ id:`,
1123
- ...listed.map((s) => ` ${s.id}${range(s)}`),
1124
- ].join("\n");
1125
- return localFailure(args, deps, "no_whole_run_scenario", text, { segments: listed.map((s) => s.id) });
1106
+ /** A run with no calculated scenario yet. */
1107
+ function noScenario(target, args, deps) {
1108
+ return localFailure(args, deps, "no_scenario", `${target} has no calculated scenario yet — \`bir scenario calc ${target}\` first.`);
1126
1109
  }
1127
1110
  /**
1128
1111
  * A `run_` id is the recording's; the routes that change a scenario take its
1129
- * `scn_` id (D7). A `run_` id reaches only the run's **whole-run** scenario; a
1130
- * `scn_` id is used as it is — sub-task scenarios have nothing else.
1112
+ * `scn_` id (D7), and a `scn_` id is used as it is.
1131
1113
  */
1132
1114
  async function resolveTarget(target, args, deps, clock = new ReadClock()) {
1133
1115
  if (target.startsWith("scn_"))
@@ -1145,7 +1127,7 @@ async function resolveTarget(target, args, deps, clock = new ReadClock()) {
1145
1127
  const served = reply.body;
1146
1128
  const id = served?.scenario?.id;
1147
1129
  if (typeof id !== "string")
1148
- return { code: noWholeRunScenario(target, served?.segments, args, deps) };
1130
+ return { code: noScenario(target, args, deps) };
1149
1131
  return { id };
1150
1132
  }
1151
1133
  /**
@@ -1199,9 +1181,6 @@ function explain(reply, args, deps, e) {
1199
1181
  out(`${e.verb} (invalid_input): the service found the request invalid${details ? ` — ${details}` : ""}.`);
1200
1182
  return 1;
1201
1183
  }
1202
- case "not_a_tool_step":
1203
- out(`${e.verb} (not_a_tool_step): step ${e.step ?? "?"} is a sub-task call row, not a tool step. Call rows belong to the service; only tool steps can be edited.`);
1204
- return 1;
1205
1184
  case "nothing_to_change":
1206
1185
  // The empty command is refused on this side before any request, so a
1207
1186
  // service `nothing_to_change` always means the change is already the
@@ -1231,16 +1210,6 @@ function explain(reply, args, deps, e) {
1231
1210
  // newer entry puts the step back as this one left it, and then this one
1232
1211
  // can be undone. It used to loop, when undo only toggled (C23).
1233
1212
  const latest = typeof body.latestEditId === "string" ? body.latestEditId : "<the newer edit>";
1234
- if (latest.startsWith("ssplit_")) {
1235
- // The plan was split at its decision since (the service's
1236
- // segmentsbreakdown.md). No `--edit` takes a split back: its Undo is
1237
- // on the recording page's Split card, and only then can this go.
1238
- const what = aboutCorrection ? "correction" : "edit";
1239
- out(`${e.verb} (not_latest_edit): the plan was split at its decision after that ${what} (${latest}). ` +
1240
- `It cannot be taken back while the split is in use: undo the split first (Undo on the Split card of the plan's recording page), then this ${what}:`);
1241
- out(` bir scenario undo ${target} --edit ${e.editId ?? "<this edit>"}`);
1242
- return 1;
1243
- }
1244
1213
  out(aboutCorrection
1245
1214
  ? `${e.verb} (not_latest_edit): the plan has changed since that correction — ${latest} changed it after. Undo the newer one first, then this one:`
1246
1215
  : `${e.verb} (not_latest_edit): the ${aboutParam ? "parameter" : "step"} has changed since that edit — ${latest} changed it after. Undo the newer one first, then this one:`);
@@ -52,21 +52,6 @@ export declare function derivationKey(daemon?: boolean): {
52
52
  apiKey?: string;
53
53
  source?: string;
54
54
  };
55
- /**
56
- * `BIR_SEGMENT_ARM`: whether a segment the service hands out may run mid-task.
57
- * On unless it is `0`, from runner 0.3.19 (the service's segmentsbreakdown.md
58
- * SB-21): segments found by repeated work, and the halves after a plan's
59
- * decision. `0` is observe-only: what would have armed is logged, and nothing
60
- * is replaced. While it is on, whole plans may start mid-task by intent too:
61
- * `BIR_INTENT_ARM` follows it unless that is set.
62
- */
63
- export declare function segmentArmFromEnv(env?: NodeJS.ProcessEnv): boolean;
64
- /**
65
- * What the `replay.enabled` startup line says segments do. A segment starts
66
- * only where the probe is waited on, so `BIR_INTENT_ARM=0` starts nothing
67
- * mid-task, segments included — the rule `bir doctor` words from /health.
68
- */
69
- export declare function segmentsAtStartup(segmentArm: boolean, env?: NodeJS.ProcessEnv): string;
70
55
  /** Where the two replay switches came from, for `/health` and `bir doctor`. */
71
56
  export type ReplaySource = "env" | "sidecar" | "default";
72
57
  /** What a recorder still serves (K4, `ControlServer.activity()`). */