@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 +31 -11
- package/dist/bin/bir-scenario.d.ts +9 -8
- package/dist/bin/bir-scenario.js +90 -27
- package/dist/bin/bir.d.ts +2 -1
- package/dist/bin/bir.js +98 -10
- package/dist/bin/investigate.js +19 -3
- package/dist/bin/scenario-edit.d.ts +39 -2
- package/dist/bin/scenario-edit.js +200 -24
- package/dist/bin/setup.d.ts +5 -1
- package/dist/bin/setup.js +16 -2
- package/dist/config/adapters/claude-code.d.ts +27 -0
- package/dist/config/adapters/claude-code.js +69 -1
- package/dist/config/generate.d.ts +21 -4
- package/dist/config/generate.js +12 -0
- package/dist/control/server.js +20 -0
- package/docs/calculatedReplay.md +11 -9
- package/docs/calculatedReplayGuide.md +83 -21
- package/docs/quickstart.md +20 -10
- package/package.json +1 -1
- package/production.md +2 -2
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`)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
`
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* this server runs in every session
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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`).
|
package/dist/bin/bir-scenario.js
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
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* this server runs in every session
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
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: {
|
|
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
|
-
|
|
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
|
|
589
|
-
"the newest edit that can be undone (of `step`, when given); calling it again goes
|
|
590
|
-
"checks
|
|
591
|
-
"`note` undoes it anyway. To redo, undo the undo: pass its
|
|
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: {
|
|
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" || !/^
|
|
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
|
|
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
|
|
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 <
|
|
114
|
-
take back the newest edit (of step n
|
|
115
|
-
one further back; undo an undo's
|
|
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
|
|
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
|
}),
|
package/dist/bin/investigate.js
CHANGED
|
@@ -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
|
|
195
|
-
fix:
|
|
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."),
|