@basein/runner 0.3.3 → 0.3.5

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.
package/README.md CHANGED
@@ -79,6 +79,8 @@ written; wraps an older install made there are put back. `--mcp-json` (or
79
79
  `BIR_WRAP_MCP_JSON=1`) is that old in-place shape, for a `.mcp.json` nobody
80
80
  commits. The hooks go into `.claude/settings.local.json` — your file, not the
81
81
  one a team commits; in a git repository it is added to `.git/info/exclude`.
82
+ `bir setup` also puts `mcp__bir` in that file's `permissions.allow`, so Claude
83
+ Code runs the `bir` server's tools without asking (a forced change still asks).
82
84
  Uninstall repairs `~/.claude.json` (and a `--config` file) entry by entry;
83
85
  `settings.local.json` it puts back byte-for-byte when nothing else has edited
84
86
  it since, and repairs entry by entry when something has. A project remembers
@@ -248,12 +250,18 @@ between the two is not guaranteed.
248
250
  `bir` server's `run_scenario` (only with `--replay`), and replay's own
249
251
  `[BaseInstRunner calculated replay]` notes when a prompt matches a scenario.
250
252
  The `bir` server's three tools that only read a scenario (`scenario_show`,
251
- `scenario_edits`, `investigate`) are listed only after `bir scenario editing on`,
252
- or with `BIR_SCENARIO_TOOLS=read` in its environment: their definitions ride
253
- along with every request. The three for changing one (`scenario_check`,
254
- `scenario_edit`, `scenario_undo`) appear only after `bir scenario editing on`
255
- in that project (not yet released, see below). With the runner off it lists
256
- no tools at all.
253
+ `scenario_edits`, `investigate`) and the three for changing one
254
+ (`scenario_check`, `scenario_edit`, `scenario_undo`) come with scenario
255
+ editing, which `bir setup` turns on. Setup also lets Claude Code run every
256
+ `bir` tool without asking: it adds `mcp__bir` to `permissions.allow` in
257
+ `.claude/settings.local.json`, and uninstall takes it out again. A change
258
+ forced past the scenario check (`force`) still asks the person first. Their
259
+ definitions ride along with every request, so `bir scenario editing off`
260
+ takes the six away, and a later setup leaves that off. A bare
261
+ `bir install --replay` lists `run_scenario` alone until
262
+ `bir scenario editing on` (or `BIR_SCENARIO_TOOLS=read` in the server's
263
+ environment, for the reading three). With the runner off it lists no tools
264
+ at all.
257
265
  - **Record its own `bir` calls** (next version, not yet released). A session
258
266
  that fixes a plan is not a task to replay. The `bir` server's tools other
259
267
  than `run_scenario`, and a `Bash` or `PowerShell` command made only of `bir`
@@ -331,9 +339,9 @@ copy — or on purpose, with `--force --note "why"`.** Only the owner can change
331
339
  a scenario. `bir scenario undo` takes changes back one at a time: run it again
332
340
  to go one further back. An undo is checked too, and is refused if it would
333
341
  break a later step or the final answer, unless you add `--force --note "why"`.
334
- Claude can do all of this: in its terminal, or with the `bir` server's tools
335
- once `bir scenario editing on` was run in the project. Step by step, with real
336
- output:
342
+ Claude can do all of this: in its terminal, or with the `bir` server's tools,
343
+ which `bir setup` turns on (`bir scenario editing on` in a project wired by
344
+ `bir install`). Step by step, with real output:
337
345
  [the guide's §9.2](docs/calculatedReplayGuide.md).
338
346
 
339
347
  ```bash
@@ -344,7 +352,19 @@ bir scenario edits scn_… # the history, newe
344
352
  bir scenario undo scn_… # take back the newest edit; again to go further back
345
353
  bir scenario undo scn_… --step 3 # take back step 3's newest edit only
346
354
  bir scenario calc scn_… --force --discard-edits # recalculate this plan, dropping its hand edits
347
- bir scenario editing on # let Claude's `bir` tools change scenarios here
355
+ bir scenario editing on # let Claude's `bir` tools change scenarios here (setup did)
356
+ ```
357
+
358
+ **A value no prompt names.** Built 2026-09-28, not yet released. Every
359
+ parameter is a *target* (a prompt must name it, or the plan is declined:
360
+ `missing_target`) or a *setting* (its recorded value is used when a prompt
361
+ names none). When the calculation made a database, a host or a working folder a
362
+ target, the plan never runs. Make it a setting, with a note; a recalculation
363
+ keeps the change ([the guide's §9.2](docs/calculatedReplayGuide.md)):
364
+
365
+ ```bash
366
+ bir scenario edit scn_… --param database --kind setting --note "why" # use the recorded database when a prompt names none
367
+ bir scenario undo scn_… --param database # take that back
348
368
  ```
349
369
 
350
370
  ---
@@ -427,7 +447,7 @@ what to do when they register, so there is one switch and no second copy of it:
427
447
  | `BIR_REPLAY_RECORDED_OUTPUTS=1` | Let a step's recorded output stand in when the step could not be sent. Unset, the plan stops at that step and hands the task back. |
428
448
 
429
449
  The `bir` server reads its own: `BIR_SCENARIO_TOOLS=read` lists its three reading
430
- tools without `bir scenario editing on`, and `BIR_SCENARIO_RUN_TIMEOUT_MS` is how
450
+ tools where scenario editing is off, and `BIR_SCENARIO_RUN_TIMEOUT_MS` is how
431
451
  long `run_scenario` waits for a run (default `290000`). Everything else — remote
432
452
  upstreams, the recorder's backlog, service timeouts, the `bir` server's output
433
453
  caps — is in [production.md](production.md).
@@ -61,14 +61,15 @@
61
61
  * WHICH TOOLS ARE OFFERED. `run_scenario`, always — unless the runner is
62
62
  * switched off (`bir off`), when nothing is: an inert runner adds nothing to
63
63
  * the model's context, and a call from a list taken before the switch starts
64
- * nothing and asks nobody. The reading tools come with `bir scenario editing on`
65
- * (or `BIR_SCENARIO_TOOLS=read` in this server's environment): their
66
- * definitions ride along with every request of every session, and only
67
- * somebody working on a scenario ever calls them. The tools that CHANGE a
68
- * scenario are offered only where a person ran `bir scenario editing on` (D2):
69
- * this server runs in every session of every installed project, a fleet
70
- * included, and a plan must not change because some agent there decided it
71
- * should. The switch is read when the host lists the tools, and again on every
64
+ * nothing and asks nobody. The rest come with scenario editing, which
65
+ * `bir setup` turns on (a person's `bir scenario editing off` is kept): the
66
+ * reading tools — also with `BIR_SCENARIO_TOOLS=read` in this server's
67
+ * environment — and the tools that CHANGE a scenario (D2). A project wired by
68
+ * a bare `bir install` (a fleet, say) offers `run_scenario` alone until a
69
+ * person runs `bir scenario editing on`: this server runs in every session
70
+ * there, each listed tool's definition rides along with every request, and a
71
+ * plan must not change because some agent there decided it should. The
72
+ * switch is read when the host lists the tools, and again on every
72
73
  * call, so a call to a tool that was never offered — or was offered before the
73
74
  * switch went off — is refused rather than run. Turning the runner off or on
74
75
  * mid-session tells the host its list changed (`notifications/tools/list_changed`).
@@ -61,14 +61,15 @@
61
61
  * WHICH TOOLS ARE OFFERED. `run_scenario`, always — unless the runner is
62
62
  * switched off (`bir off`), when nothing is: an inert runner adds nothing to
63
63
  * the model's context, and a call from a list taken before the switch starts
64
- * nothing and asks nobody. The reading tools come with `bir scenario editing on`
65
- * (or `BIR_SCENARIO_TOOLS=read` in this server's environment): their
66
- * definitions ride along with every request of every session, and only
67
- * somebody working on a scenario ever calls them. The tools that CHANGE a
68
- * scenario are offered only where a person ran `bir scenario editing on` (D2):
69
- * this server runs in every session of every installed project, a fleet
70
- * included, and a plan must not change because some agent there decided it
71
- * should. The switch is read when the host lists the tools, and again on every
64
+ * nothing and asks nobody. The rest come with scenario editing, which
65
+ * `bir setup` turns on (a person's `bir scenario editing off` is kept): the
66
+ * reading tools — also with `BIR_SCENARIO_TOOLS=read` in this server's
67
+ * environment — and the tools that CHANGE a scenario (D2). A project wired by
68
+ * a bare `bir install` (a fleet, say) offers `run_scenario` alone until a
69
+ * person runs `bir scenario editing on`: this server runs in every session
70
+ * there, each listed tool's definition rides along with every request, and a
71
+ * plan must not change because some agent there decided it should. The
72
+ * switch is read when the host lists the tools, and again on every
72
73
  * call, so a call to a tool that was never offered — or was offered before the
73
74
  * switch went off — is refused rather than run. Turning the runner off or on
74
75
  * mid-session tells the host its list changed (`notifications/tools/list_changed`).
@@ -442,6 +443,31 @@ function note(args) {
442
443
  throw new BadArguments("`note` must be a string");
443
444
  return ["--note", value];
444
445
  }
446
+ /** `--revision <n>`, the chainRevision a change was made against. */
447
+ function revision(args) {
448
+ const value = args.revision;
449
+ if (value === undefined || value === null)
450
+ return [];
451
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
452
+ throw new BadArguments("`revision` must be a whole number from 1");
453
+ }
454
+ return ["--revision", String(value)];
455
+ }
456
+ const PARAM = {
457
+ type: "string",
458
+ description: "A parameter's name, as scenario_show lists it under `parameters` (for example `database`).",
459
+ };
460
+ /** A parameter's name: one argv element after `--param`, so it cannot read as a flag. */
461
+ function param(args) {
462
+ const value = args.param;
463
+ if (value === undefined || value === null)
464
+ return undefined;
465
+ if (typeof value !== "string" || value.length === 0 || value.length > 200 || /[\u0000-\u001f\u007f]/.test(value)) {
466
+ throw new BadArguments("`param` must be a parameter's name, as scenario_show lists it");
467
+ }
468
+ return value;
469
+ }
470
+ const given = (value) => value !== undefined && value !== null && value !== false;
445
471
  const SCENARIO_TOOLS = [
446
472
  {
447
473
  name: "scenario_show",
@@ -462,8 +488,8 @@ const SCENARIO_TOOLS = [
462
488
  name: "scenario_edits",
463
489
  changes: false,
464
490
  service: true,
465
- description: "List a calculated scenario's hand edits, newest first: step, note, revision, and whether each was forced, " +
466
- "undone or replaced by a recalculation.",
491
+ description: "List a calculated scenario's hand edits, newest first — step edits and parameter kind changes: step or " +
492
+ "parameter, note, revision, and whether each was forced, undone or replaced by a recalculation.",
467
493
  inputSchema: {
468
494
  type: "object",
469
495
  properties: { target: TARGET },
@@ -532,12 +558,24 @@ const SCENARIO_TOOLS = [
532
558
  "meanwhile is not overwritten. " +
533
559
  "`force` saves what the check refused and is only for a recording that was itself wrong — never a way past a " +
534
560
  "failing check. A save can be taken back with scenario_undo, which the service checks too: it refuses an undo " +
535
- "that would break a later step, and a recalculation with --discard-edits replaces edits for good.",
561
+ "that would break a later step, and a recalculation with --discard-edits replaces edits for good. " +
562
+ "Instead of `step`, `param` with `kind` changes what a prompt must name: a target must be named or the plan is " +
563
+ "declined (missing_target); a setting uses its recorded value when a prompt names none — for a value that is the " +
564
+ "same every time, such as a database, a host or a working folder. Making one a setting needs `note`.",
536
565
  inputSchema: {
537
566
  type: "object",
538
567
  properties: {
539
568
  target: TARGET,
540
569
  step: STEP,
570
+ param: {
571
+ ...PARAM,
572
+ description: `${PARAM.description} Changes that parameter's kind (with \`kind\`) instead of a step. Not with \`step\`.`,
573
+ },
574
+ kind: {
575
+ type: "string",
576
+ enum: ["target", "setting"],
577
+ description: "With `param`: target (a prompt must name it) or setting (its recorded value is used when a prompt names none; needs `note`).",
578
+ },
541
579
  inputLogic: INPUT_LOGIC,
542
580
  outputLogic: OUTPUT_LOGIC,
543
581
  freeze: {
@@ -549,17 +587,34 @@ const SCENARIO_TOOLS = [
549
587
  type: "boolean",
550
588
  description: "Save although the check refused it. Needs `note`. Only when the recording itself was wrong.",
551
589
  },
552
- note: { type: "string", description: "Why the step is changed; kept in the history. Required with `force`." },
590
+ note: {
591
+ type: "string",
592
+ description: "Why the step (or the parameter's kind) is changed; kept in the history. Required with `force`, and to make a parameter a setting.",
593
+ },
553
594
  revision: {
554
595
  type: "integer",
555
596
  minimum: 1,
556
597
  description: "The chainRevision the change was made against — at the top of a scenario_show answer with `step`; the save is refused if the plan moved on.",
557
598
  },
558
599
  },
559
- required: ["target", "step"],
600
+ // `step`, or `param` with `kind`: which one is checked below, in words.
601
+ required: ["target"],
560
602
  additionalProperties: false,
561
603
  },
562
604
  plan: (a) => {
605
+ const name = param(a);
606
+ if (name !== undefined || given(a.kind)) {
607
+ // A parameter's kind (findoffer.md): nothing runs, so nothing of a step's belongs here.
608
+ if (name === undefined)
609
+ throw new BadArguments("`kind` needs `param`: the parameter to change");
610
+ if (a.kind !== "target" && a.kind !== "setting")
611
+ throw new BadArguments("`kind` must be target or setting");
612
+ for (const key of ["step", "inputLogic", "outputLogic", "freeze", "unfreeze", "force"]) {
613
+ if (given(a[key]))
614
+ throw new BadArguments(`\`param\` changes only a parameter's kind; \`${key}\` belongs to a step`);
615
+ }
616
+ return { argv: ["scenario", "edit", target(a), "--param", name, "--kind", a.kind, ...note(a), ...revision(a)] };
617
+ }
563
618
  if (a.freeze === true && a.unfreeze === true)
564
619
  throw new BadArguments("`freeze` and `unfreeze` ask for opposite things");
565
620
  const argv = [
@@ -571,13 +626,7 @@ const SCENARIO_TOOLS = [
571
626
  ...flag(a, "unfreeze", "--unfreeze"),
572
627
  ...flag(a, "force", "--force"),
573
628
  ];
574
- argv.push(...note(a));
575
- if (a.revision !== undefined && a.revision !== null) {
576
- if (typeof a.revision !== "number" || !Number.isInteger(a.revision) || a.revision < 1) {
577
- throw new BadArguments("`revision` must be a whole number from 1");
578
- }
579
- argv.push("--revision", String(a.revision));
580
- }
629
+ argv.push(...note(a), ...revision(a));
581
630
  return { argv, inputLogic: logic(a, "inputLogic"), outputLogic: logic(a, "outputLogic") };
582
631
  },
583
632
  },
@@ -585,19 +634,27 @@ const SCENARIO_TOOLS = [
585
634
  name: "scenario_undo",
586
635
  changes: true,
587
636
  service: true,
588
- description: "Take back a hand edit of a calculated scenario: its step goes back as it was before that edit. Without `edit`, " +
589
- "the newest edit that can be undone (of `step`, when given); calling it again goes one further back. The service " +
590
- "checks the undo like a save and refuses it when a later step or the final answer would break — `force` with a " +
591
- "`note` undoes it anyway. To redo, undo the undo: pass its sedit_ id as `edit` (the answer names it).",
637
+ description: "Take back a hand edit of a calculated scenario: its step (or a parameter's kind) goes back as it was before that " +
638
+ "edit. Without `edit`, the newest edit that can be undone (of `step` or `param`, when given); calling it again goes " +
639
+ "one further back. The service checks a step's undo like a save and refuses it when a later step or the final " +
640
+ "answer would break — `force` with a `note` undoes it anyway. To redo, undo the undo: pass its id as `edit` (the " +
641
+ "answer names it).",
592
642
  inputSchema: {
593
643
  type: "object",
594
644
  properties: {
595
645
  target: TARGET,
596
- edit: { type: "string", description: "The sedit_ id to undo, as scenario_edits lists it. Not with `step`." },
646
+ edit: {
647
+ type: "string",
648
+ description: "The sedit_ or pedit_ id to undo, as scenario_edits lists it. Not with `step` or `param`.",
649
+ },
597
650
  step: {
598
651
  ...STEP,
599
652
  description: "Undo the newest edit of this step (its stepIndex, as scenario_show prints it). Not with `edit`.",
600
653
  },
654
+ param: {
655
+ ...PARAM,
656
+ description: `${PARAM.description} Undo the newest kind change of this parameter. Not with \`edit\` or \`step\`.`,
657
+ },
601
658
  force: {
602
659
  type: "boolean",
603
660
  description: "Undo although the check refused it (a later step or the answer would break). Needs `note`.",
@@ -609,15 +666,21 @@ const SCENARIO_TOOLS = [
609
666
  },
610
667
  plan: (a) => {
611
668
  const argv = ["scenario", "undo", target(a)];
669
+ const name = param(a);
670
+ if (name !== undefined && (given(a.edit) || given(a.step))) {
671
+ throw new BadArguments("`edit`, `step` and `param` each pick what to undo; give one of them");
672
+ }
612
673
  if (a.edit !== undefined && a.edit !== null) {
613
- if (typeof a.edit !== "string" || !/^sedit_[\w-]+$/.test(a.edit)) {
614
- throw new BadArguments("`edit` must be a sedit_… id");
674
+ if (typeof a.edit !== "string" || !/^(sedit|pedit)_[\w-]+$/.test(a.edit)) {
675
+ throw new BadArguments("`edit` must be a sedit_… or pedit_… id");
615
676
  }
616
677
  if (a.step !== undefined && a.step !== null) {
617
678
  throw new BadArguments("`edit` names one entry and `step` picks one; give one of them");
618
679
  }
619
680
  argv.push("--edit", a.edit);
620
681
  }
682
+ if (name !== undefined)
683
+ argv.push("--param", name);
621
684
  argv.push(...step(a, false), ...flag(a, "force", "--force"), ...note(a));
622
685
  return { argv };
623
686
  },
package/dist/bin/bir.d.ts CHANGED
@@ -15,7 +15,8 @@
15
15
  *
16
16
  * bir scenario list | show <runId|scnId> [--step <n>] | calc <runId|scnId> [--force [--discard-edits]]
17
17
  * bir scenario check|edit <runId|scnId> --step <n> --input-logic <file|-> … ← fix one step
18
- * bir scenario edits | undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
18
+ * bir scenario edit <runId|scnId> --param <name> --kind target|setting … ← what a prompt must name
19
+ * bir scenario edits | undo <runId|scnId> [--edit <id> | --step <n> | --param <name>] [--force --note "why"]
19
20
  * bir scenario editing on|off|status ← may this project's `bir` MCP server change a scenario?
20
21
  * bir scenario replay <scnId> --prompt "…" [--dry] | bir replay status|on|off|allow …
21
22
  * bir investigate [<id>] | list | executions
package/dist/bin/bir.js CHANGED
@@ -15,7 +15,8 @@
15
15
  *
16
16
  * bir scenario list | show <runId|scnId> [--step <n>] | calc <runId|scnId> [--force [--discard-edits]]
17
17
  * bir scenario check|edit <runId|scnId> --step <n> --input-logic <file|-> … ← fix one step
18
- * bir scenario edits | undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
18
+ * bir scenario edit <runId|scnId> --param <name> --kind target|setting … ← what a prompt must name
19
+ * bir scenario edits | undo <runId|scnId> [--edit <id> | --step <n> | --param <name>] [--force --note "why"]
19
20
  * bir scenario editing on|off|status ← may this project's `bir` MCP server change a scenario?
20
21
  * bir scenario replay <scnId> --prompt "…" [--dry] | bir replay status|on|off|allow …
21
22
  * bir investigate [<id>] | list | executions
@@ -45,14 +46,14 @@ import * as daemonApi from "../control/daemon.js";
45
46
  import { activeSessions, daemonStatus, ensureDaemon, probeHealth, readPortProblem, stopDaemon, } from "../control/daemon.js";
46
47
  import { configDir, controlDir, controlKey, discoveryPath, installedPath, normalizePath } from "../control/paths.js";
47
48
  import { offFlagPath, offSource, projectOffFlagPath, setRunnerOff } from "../util/off-switch.js";
48
- import { allocateFreeProjectPort, backupIsCreation, backupOriginalText, configSha, envProxyHint, exemptLoopbackFromProxy, isNpxEntry, isScenarioServer, isWrapped, nodeFlagsToCarry, normaliseRecordKeys, npxHookInvocation, protectBackupText, recordKey, sameServerConfig, PACKAGE_NAME, projectRecord, readSidecar, redactSecrets, restoreSecrets, remoteWrapBlocker, scenarioEntry, setProjectRecord, settleReplaySwitch, sidecarKey, unwrapEntry, UPSTREAM_HEADERS_ENV, wrapEntry, writeSidecar, SCENARIO_SERVER_KEY, } from "../config/generate.js";
49
+ import { allocateFreeProjectPort, backupIsCreation, backupOriginalText, configSha, envProxyHint, exemptLoopbackFromProxy, isNpxEntry, isScenarioServer, isWrapped, nodeFlagsToCarry, normaliseRecordKeys, npxHookInvocation, protectBackupText, recordKey, sameServerConfig, PACKAGE_NAME, projectRecord, readSidecar, redactSecrets, restoreSecrets, remoteWrapBlocker, scenarioEntry, editingEnabled, setProjectRecord, settleEditing, settleReplaySwitch, sidecarKey, unwrapEntry, UPSTREAM_HEADERS_ENV, wrapEntry, writeSidecar, SCENARIO_SERVER_KEY, } from "../config/generate.js";
49
50
  import { chooseNodePath } from "../config/node-path.js";
50
- import { managedMcpPolicyBlocker, mcpjsonApprovals } from "../config/mcp-approval.js";
51
+ import { claudeUserSettingsPath, managedMcpPolicyBlocker, mcpjsonApprovals } from "../config/mcp-approval.js";
51
52
  import { checkOverrides, recordServerName } from "../config/overrides.js";
52
53
  import { awaitWithin, isUnhealthyStatus, serviceHealth, serviceTimeoutMs, timeoutSignal } from "../util/service-health.js";
53
54
  import { runSetup } from "./setup.js";
54
55
  import { claudeConfigHome, claudeProjectKey, claudeProjectRoot, isRemote, linkedWorktrees, parseJsonFile, projectKeyFor, projectKeysFor, projectScope, resolveServers, } from "../config/resolve.js";
55
- import { assertEditableJson, buildHooksBlock, claudeCodePaths, disableMcpjsonServers, enableMcpjsonServers, ENSURE_ARG, ensureGitExcludeLine, hasBirHooks, installHooks, isBirHook, LOCAL_SETTINGS_EXCLUDE, readTextOrNull, removeGitExclude, setLocalEntries, setServerEntry, sha256, uninstallHooks, } from "../config/adapters/claude-code.js";
56
+ import { addAllowRules, allowRules, assertEditableJson, BIR_TOOLS_RULE, buildHooksBlock, claudeCodePaths, disableMcpjsonServers, enableMcpjsonServers, ENSURE_ARG, ensureGitExcludeLine, hasBirHooks, installHooks, isBirHook, LOCAL_SETTINGS_EXCLUDE, readTextOrNull, removeAllowRules, removeGitExclude, setLocalEntries, setServerEntry, sha256, uninstallHooks, } from "../config/adapters/claude-code.js";
56
57
  import { writeFileAtomic } from "../util/atomic-write.js";
57
58
  import { readGenericServers, setGenericServerEntry } from "../config/adapters/generic.js";
58
59
  import { AUTH_URL_HINT, DeviceFlowAborted, DeviceFlowUnsupported, authenticate, clearCredentials, deviceLogin, describeAuthService, legacyPasswordLogin, tokenLogin, normalizeAuthUrl, rememberAuthUrl, resolveAuthUrl, } from "../auth/client.js";
@@ -79,7 +80,8 @@ function usage(code = 0) {
79
80
  out(`bir ${VERSION} — a recording MCP proxy
80
81
 
81
82
  Commands:
82
- setup the one command: sign in, wire this project, start the recorder
83
+ setup the one command: sign in, wire this project, start the recorder, and give
84
+ Claude Code every bir tool (run_scenario, investigate, scenario_*) with no prompt
83
85
  up start the recorder for this project in the background (--restart: replace it)
84
86
  down stop it (open sessions here then show a hook error until they restart;
85
87
  \`bir off --here\` is the quiet way to take it out of the way)
@@ -109,12 +111,17 @@ Commands:
109
111
  scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
110
112
  [--freeze | --unfreeze] [--note "why"] [--force --note "why"] [--revision <n>]
111
113
  the same check, and save the change if it passes
114
+ scenario edit <runId|scnId> --param <name> --kind target|setting [--note "why"]
115
+ a target must be named by the prompt or the plan is
116
+ declined; a setting uses its recorded value when the
117
+ prompt names none (needs --note)
112
118
  scenario edits <runId|scnId> a scenario's hand edits, newest first
113
- scenario undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
114
- take back the newest edit (of step n); run it again to go
115
- one further back; undo an undo's sedit_ id to redo
119
+ scenario undo <runId|scnId> [--edit <id> | --step <n> | --param <name>] [--force --note "why"]
120
+ take back the newest edit (of step n, or parameter name);
121
+ run it again to go one further back; undo an undo's id to redo
116
122
  scenario editing on|off|status whether this project's bir MCP server offers the tools
117
- that change a scenario (restart Claude Code after)
123
+ that read and change a scenario (restart Claude Code after);
124
+ \`bir setup\` turns it on, and an off stays off
118
125
  scenario replay <scnId> --prompt "…" [--dry]
119
126
  replay --scenario <scnId> --prompt "…" [--dry]
120
127
  replay status | on | off | allow <a,b> | allow all
@@ -156,7 +163,10 @@ Options:
156
163
  --unfreeze scenario check|edit: ask to remove that mark; the check decides
157
164
  --note <text> scenario edit|undo: why, kept in the history (required with --force)
158
165
  --revision <n> scenario edit|undo: only if the plan is still at this chainRevision
159
- --edit <id> scenario undo: the sedit_ id to undo (default: the newest edit that can be)
166
+ --edit <id> scenario undo: the sedit_ or pedit_ id to undo (default: the newest that can be)
167
+ --param <name> scenario edit: the parameter whose kind changes · scenario undo: its newest change
168
+ --kind <kind> scenario edit --param: target (a prompt must name it) or setting (its
169
+ recorded value is used when a prompt names none; needs --note)
160
170
  --discard-edits scenario calc --force: recalculate a plan with hand edits, replacing them
161
171
  --no-browser login: print the link and code, open nothing (SSH, headless)
162
172
  --token <value> login: redeem a one-time setup token from the console (no browser)
@@ -355,6 +365,18 @@ function parseArgs(argv) {
355
365
  case "--discard-edits":
356
366
  args.discardEdits = true;
357
367
  break;
368
+ case "--param":
369
+ // A value flag, like --note: the name is whatever follows, and
370
+ // scenario-edit.ts says what is wrong with it in words.
371
+ args.param = argv[++i];
372
+ if (!args.param)
373
+ usage(2);
374
+ break;
375
+ case "--kind":
376
+ args.kind = argv[++i];
377
+ if (!args.kind)
378
+ usage(2);
379
+ break;
358
380
  case "--here":
359
381
  args.here = true;
360
382
  break;
@@ -750,6 +772,15 @@ async function installWriting(args, cwd, sidecar, plan) {
750
772
  ? " - calculated replay is off here (--no-replay); `bir replay on` turns it on"
751
773
  : " - calculated replay is off here: it was not asked for (`bir install --replay` adds it)");
752
774
  }
775
+ // EVERY `bir` TOOL, for `bir setup`: investigate and the scenario tools are
776
+ // offered as well as run_scenario (scenario editing on — an `off` a person
777
+ // stored stays off), and Claude Code runs them without asking (below, with
778
+ // the hooks). Only where `bir` is the scenario server: a `bir` server of the
779
+ // person's own gets neither.
780
+ const birToolsHere = args.birTools === true && scenarioServerHere(args, cwd);
781
+ if (birToolsHere && settleEditing(sidecar, cwd)) {
782
+ out(" + scenario editing on: the bir server offers investigate and the scenario tools too (`bir scenario editing off` takes them away)");
783
+ }
753
784
  // SessionStart starts the recorder itself when this directory has none
754
785
  // (control/ensure-hook.ts). Every invocation gets it — an npx one through
755
786
  // npx, once per session — because without it nothing starts the recorder,
@@ -794,9 +825,35 @@ async function installWriting(args, cwd, sidecar, plan) {
794
825
  // never asks about; an older install's pre-approval has nothing to approve.
795
826
  text = disableMcpjsonServers(paths.settingsLocal, [SCENARIO_SERVER_KEY]) ?? text;
796
827
  }
828
+ let allowLine;
829
+ if (args.birTools) {
830
+ // A prompt for a tool the person never named reads as the runner getting
831
+ // in the way, and under `claude -p` it is a refusal. `run_scenario` runs
832
+ // only a plan the recorder armed, the reading tools only read, and a
833
+ // change must reproduce the recording or be saved on purpose with a note
834
+ // (editSteps.md D3) — and `scenario_undo` takes it back.
835
+ const granted = projectRecord(sidecar, cwd)?.allowRules ?? [];
836
+ if (birToolsHere) {
837
+ const allowed = addAllowRules(paths.settingsLocal, [BIR_TOOLS_RULE]);
838
+ text = allowed.text;
839
+ if (allowed.added.length > 0) {
840
+ setProjectRecord(sidecar, cwd, { port, allowRules: [...new Set([...granted, ...allowed.added])] });
841
+ allowLine = ` + ${BIR_TOOLS_RULE} → permissions.allow in the same file (Claude Code runs the bir tools without asking)`;
842
+ }
843
+ }
844
+ else if (granted.length > 0) {
845
+ // `bir` here is not the scenario server any more: the rule an earlier
846
+ // setup added must not approve another server's tools by its name.
847
+ text = removeAllowRules(paths.settingsLocal, granted) ?? text;
848
+ setProjectRecord(sidecar, cwd, { port, allowRules: undefined });
849
+ allowLine = ` - ${granted.join(", ")} — taken out of permissions.allow there: \`${SCENARIO_SERVER_KEY}\` here is not the scenario server`;
850
+ }
851
+ }
797
852
  noteWritten(sidecar, paths.settingsLocal, text, claudeJson);
798
853
  sidecar.hookFiles = [...new Set([...(sidecar.hookFiles ?? []), paths.settingsLocal])];
799
854
  out(` + hooks → ${paths.settingsLocal} (control server on ${controlUrl}; the recorder starts itself with each session${invocation === "npx" ? ", through npx" : ""})`);
855
+ if (allowLine)
856
+ out(allowLine);
800
857
  const exclude = ensureGitExcludeLine(cwd);
801
858
  if (exclude) {
802
859
  out(` + ${LOCAL_SETTINGS_EXCLUDE} is listed in ${exclude.path}`);
@@ -1827,6 +1884,22 @@ function uninstallProject(cwd, sidecar, opts) {
1827
1884
  }
1828
1885
  }
1829
1886
  }
1887
+ // 4b. The rule `bir setup` added so the `bir` server's tools run without a
1888
+ // prompt: only the rules it added, and once that server is gone — so a `bir`
1889
+ // server of the person's own never inherits it. `--replay` takes it too.
1890
+ const projectRec = projectRecord(sidecar, cwd);
1891
+ const granted = projectRec?.allowRules ?? [];
1892
+ if (projectRec && granted.length > 0 && wanted(SCENARIO_SERVER_KEY) && !remaining.has(SCENARIO_SERVER_KEY)) {
1893
+ try {
1894
+ if (removeAllowRules(paths.settingsLocal, granted) !== undefined) {
1895
+ out(` ← ${granted.join(", ")} — taken out of ${paths.settingsLocal} (permissions.allow)`);
1896
+ }
1897
+ setProjectRecord(sidecar, cwd, { port: projectRec.port, allowRules: undefined });
1898
+ }
1899
+ catch (err) {
1900
+ remaining.set(`allow:${normalizePath(paths.settingsLocal)}`, `${granted.join(", ")} in ${paths.settingsLocal} — ${errText(err)}`);
1901
+ }
1902
+ }
1830
1903
  // 5. Read it back: what would a session here spawn now — in this
1831
1904
  // environment, and in any other profile the install wrote to?
1832
1905
  const after = runnerLeftovers(cwd, sidecar, opts.configPath, otherClaude);
@@ -3143,6 +3216,18 @@ async function doctor(args) {
3143
3216
  notes.push("the bir scenario server starts via npx, which can take longer than the first tool call " +
3144
3217
  "of a turn — run `bir setup` (or `bir install --replay --global`) to pin it to the installed copy");
3145
3218
  }
3219
+ // WHICH `bir` TOOLS A SESSION HERE IS GIVEN. `bir setup` offers every one and
3220
+ // lets Claude Code run them without asking; said only when that is not so.
3221
+ if (scenarioEntry && isScenarioServer(scenarioEntry.config)) {
3222
+ if (!editingEnabled(sidecar, cwd)) {
3223
+ notes.push("the bir MCP server offers run_scenario only: scenario editing is off here — `bir scenario editing on` " +
3224
+ "adds investigate and the scenario tools (restart Claude Code after)");
3225
+ }
3226
+ else if (![claudeUserSettingsPath(), paths.settings, paths.settingsLocal].some((file) => allowRules(file).some((rule) => rule === BIR_TOOLS_RULE || rule === `${BIR_TOOLS_RULE}__*`))) {
3227
+ notes.push("Claude Code asks before investigate and the scenario tools run here — `bir setup` allows every bir tool " +
3228
+ `(${BIR_TOOLS_RULE} in ${paths.settingsLocal})`);
3229
+ }
3230
+ }
3146
3231
  // THE PORT. The hooks' own URLs are the ground truth — they are what every
3147
3232
  // hook posts to — checked against the recorder actually answering here. Not
3148
3233
  // the machine-wide "last port any install wrote", which named another
@@ -3374,6 +3459,8 @@ async function scenarioCommand(args) {
3374
3459
  edit: args.edit,
3375
3460
  revision: args.revision,
3376
3461
  discardEdits: args.discardEdits,
3462
+ param: args.param,
3463
+ kind: args.kind,
3377
3464
  }, {
3378
3465
  cwd: process.cwd(),
3379
3466
  out,
@@ -3739,6 +3826,7 @@ async function main() {
3739
3826
  npx: false,
3740
3827
  forceInvocation: invocation,
3741
3828
  replay,
3829
+ birTools: true,
3742
3830
  hooks: true,
3743
3831
  servers: [],
3744
3832
  }),
@@ -191,8 +191,8 @@ export const DECLINES = {
191
191
  fix: "enable or recalculate that sub-task, or recalculate this scenario.",
192
192
  },
193
193
  missing_target: {
194
- cause: "the prompt names a target the runner could not find where the recording had it.",
195
- fix: "run from the recording's directory, or name the target as the recording did.",
194
+ 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.",
195
+ 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).',
196
196
  },
197
197
  no_derive_key: {
198
198
  cause: "the scenario has parameters and the runner had no way to derive them: no service session and no ANTHROPIC_API_KEY.",
@@ -203,6 +203,22 @@ export const DECLINES = {
203
203
  fix: "recalculate the scenario.",
204
204
  },
205
205
  };
206
+ /**
207
+ * The `missing_target` fix with what this turn knows put in: the scenario, and
208
+ * the target the gate named (`target database not found`). The code alone once
209
+ * sent a reader after a lookup that failed, when the prompt had simply not
210
+ * named the value.
211
+ */
212
+ function missingTargetFix(fix, turn) {
213
+ let text = fix;
214
+ const scenarioId = turn.matched?.scenarioId ?? turn.plan?.scenarioId;
215
+ if (scenarioId)
216
+ text = text.replaceAll("<scnId>", scenarioId);
217
+ const name = /\btarget (\S+) not found/.exec(turn.decision?.why ?? "")?.[1];
218
+ if (name)
219
+ text = text.replaceAll("<name>", name);
220
+ return text;
221
+ }
206
222
  export const LOW_SAVING_PCT = 0.3;
207
223
  const usd = (v) => `$${v.toFixed(4)}`;
208
224
  const pct = (v) => `${(v * 100).toFixed(1)}%`;
@@ -225,7 +241,7 @@ export function localFindings(turn) {
225
241
  severity: code === "not_ready" && !turn.matched.scenarioId ? "info" : "problem",
226
242
  title: `Matched ${turn.matched.runId} but declined to steer: ${turn.decision.why ?? code}.`,
227
243
  cause: known?.cause ?? "the runner's log line carries the reason text.",
228
- fix: known?.fix ??
244
+ fix: (known && code === "missing_target" ? missingTargetFix(known.fix, turn) : known?.fix) ??
229
245
  (turn.matched.scenarioId
230
246
  ? "read the `replay.decision` line above."
231
247
  : "the matched recording has no scenario yet — calculate it, or let the service pick it up after enough hits."),