@basein/runner 0.2.10 → 0.2.11

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/dist/bin/bir.js CHANGED
@@ -12,6 +12,16 @@
12
12
  * bir login | logout
13
13
  * bir --version
14
14
  *
15
+ * bir scenario list | show <runId|scnId> [--step <n>] | calc <runId> [--force [--discard-edits]]
16
+ * bir scenario check|edit <runId|scnId> --step <n> --input-logic <file|-> … ← fix one step
17
+ * bir scenario edits | undo <runId|scnId> [--edit <sedit_id>]
18
+ * bir scenario editing on|off|status ← may this project's `bir` MCP server change a scenario?
19
+ * bir scenario replay <scnId> --prompt "…" [--dry] | bir replay status|on|off|allow …
20
+ * bir investigate [<id>] | list | executions
21
+ *
22
+ * The `scenario` words that read or change one step live in scenario-edit.ts
23
+ * (editSteps.md in the BaseIn repository); `investigate` in investigate.ts.
24
+ *
15
25
  * `bir install` is reversible by construction: every file it edits is stashed
16
26
  * verbatim first, so `bir uninstall` restores it byte-for-byte unless somebody
17
27
  * else edited it in the meantime (in which case it repairs the entries and says
@@ -20,7 +30,7 @@
20
30
  * from `serverInfo`, which must stay the upstream's (Phase 3).
21
31
  */
22
32
  import { randomBytes } from "node:crypto";
23
- import { existsSync, realpathSync, writeFileSync } from "node:fs";
33
+ import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
24
34
  import { dirname, join, resolve as resolvePath } from "node:path";
25
35
  import { fileURLToPath } from "node:url";
26
36
  import { ControlClient } from "../control/client.js";
@@ -37,6 +47,7 @@ import { DEFAULT_CONTROL_PORT } from "../control/server.js";
37
47
  import { errText } from "../util/log.js";
38
48
  import { journalPath, readJournal } from "../util/journal.js";
39
49
  import { investigateCommand } from "./investigate.js";
50
+ import { SCENARIO_EDIT_SUBCOMMANDS, scenarioEditCommand } from "./scenario-edit.js";
40
51
  import { loadCredentials } from "../auth/client.js";
41
52
  import { packageVersion } from "../util/version.js";
42
53
  const VERSION = packageVersion();
@@ -59,8 +70,20 @@ Commands:
59
70
  logout revoke this machine's session and forget it
60
71
 
61
72
  scenario list recorded runs and their calculated scenarios
62
- scenario show <runId> a run's scenario: intent, params, steps
63
- scenario calc <runId> [--force] calculate (or re-derive) a run's scenario
73
+ scenario show <runId|scnId> [--step <n>]
74
+ a scenario: intent, params, steps (--step: just that one)
75
+ scenario calc <runId> [--force [--discard-edits]]
76
+ calculate (or re-derive) a run's scenario
77
+ scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
78
+ try a change to one step against the recording; saves nothing
79
+ scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
80
+ [--freeze | --unfreeze] [--note "why"] [--force --note "why"] [--revision <n>]
81
+ the same check, and save the change if it passes
82
+ scenario edits <runId|scnId> a scenario's hand edits, newest first
83
+ scenario undo <runId|scnId> [--edit <sedit_id>]
84
+ put a step back as it was before its newest edit
85
+ scenario editing on|off|status whether this project's bir MCP server offers the tools
86
+ that change a scenario (restart Claude Code after)
64
87
  scenario replay <scnId> --prompt "…" [--dry]
65
88
  replay --scenario <scnId> --prompt "…" [--dry]
66
89
  replay status | on | off | allow <a,b> | allow all
@@ -83,8 +106,20 @@ Options:
83
106
  --replay install/remove the scenario server, enabling calculated replay
84
107
  --port <n> install/setup: control-server port to write into the hook URLs
85
108
  (default: this project's, else the lowest free one from ${DEFAULT_CONTROL_PORT})
86
- --json machine-readable output for status / doctor
109
+ --json machine-readable output for status / doctor / scenario / investigate
110
+ (scenario check|edit|edits|undo: the service's answer, unchanged)
87
111
  --dry replay against recorded outputs only; run no real tools
112
+ --force scenario calc: recalculate in place · scenario edit: save although
113
+ the check refused it (needs --note; only for a recording that was wrong)
114
+ --step <n> scenario show|check|edit: the step, by the stepIndex \`show\` prints
115
+ --input-logic <file|-> scenario check|edit: the new input logic, from a file or stdin (-)
116
+ --output-logic <file|-> scenario check|edit: the new output logic, from a file or stdin (-)
117
+ --freeze scenario edit: mark the step "needs a judgement" (runs hand over before it)
118
+ --unfreeze scenario check|edit: ask to remove that mark; the check decides
119
+ --note <text> scenario edit|undo: why, kept in the history (required with --force)
120
+ --revision <n> scenario edit|undo: only if the plan is still at this chainRevision
121
+ --edit <id> scenario undo: the sedit_ id to undo (default: the newest that can be)
122
+ --discard-edits scenario calc --force: recalculate a plan with hand edits, replacing them
88
123
  --no-browser login: print the link and code, open nothing (SSH, headless)
89
124
  --token <value> login: redeem a one-time setup token from the console (no browser)
90
125
  --password login: use the old email/password prompt (deprecated)
@@ -118,6 +153,9 @@ function parseArgs(argv) {
118
153
  noDaemon: false,
119
154
  restart: false,
120
155
  portExplicit: false,
156
+ freeze: false,
157
+ unfreeze: false,
158
+ discardEdits: false,
121
159
  };
122
160
  for (let i = 1; i < argv.length; i += 1) {
123
161
  const arg = argv[i];
@@ -214,6 +252,53 @@ function parseArgs(argv) {
214
252
  args.limit = n;
215
253
  break;
216
254
  }
255
+ case "--step": {
256
+ // Digits only: `Number("")` is 0 and `Number("0x4")` is 4, and a step
257
+ // index nobody typed is a step nobody meant to change.
258
+ const raw = argv[++i] ?? "";
259
+ if (!/^\d+$/.test(raw))
260
+ usage(2);
261
+ args.step = Number(raw);
262
+ break;
263
+ }
264
+ case "--input-logic":
265
+ // A value flag, so a bare `-` (stdin) is taken here and never reaches
266
+ // the default branch, which refuses anything that starts with a dash.
267
+ args.inputLogic = argv[++i];
268
+ if (!args.inputLogic)
269
+ usage(2);
270
+ break;
271
+ case "--output-logic":
272
+ args.outputLogic = argv[++i];
273
+ if (!args.outputLogic)
274
+ usage(2);
275
+ break;
276
+ case "--freeze":
277
+ args.freeze = true;
278
+ break;
279
+ case "--unfreeze":
280
+ args.unfreeze = true;
281
+ break;
282
+ case "--note":
283
+ args.note = argv[++i];
284
+ if (args.note === undefined)
285
+ usage(2);
286
+ break;
287
+ case "--edit":
288
+ args.edit = argv[++i];
289
+ if (!args.edit)
290
+ usage(2);
291
+ break;
292
+ case "--revision": {
293
+ const raw = argv[++i] ?? "";
294
+ if (!/^\d+$/.test(raw) || Number(raw) <= 0)
295
+ usage(2);
296
+ args.revision = Number(raw);
297
+ break;
298
+ }
299
+ case "--discard-edits":
300
+ args.discardEdits = true;
301
+ break;
217
302
  default:
218
303
  if (arg.startsWith("-"))
219
304
  usage(2);
@@ -221,6 +306,10 @@ function parseArgs(argv) {
221
306
  args.positionals.push(arg);
222
307
  }
223
308
  }
309
+ if (args.freeze && args.unfreeze) {
310
+ process.stderr.write("[bir] --freeze and --unfreeze are mutually exclusive\n");
311
+ process.exit(2);
312
+ }
224
313
  return args;
225
314
  }
226
315
  /** Absolute path to this package's built `bir-proxy.js`, for `--local`. */
@@ -960,51 +1049,59 @@ async function scenarioCommand(args) {
960
1049
  }
961
1050
  return 0;
962
1051
  }
963
- if (sub === "show") {
964
- if (!target) {
965
- out("usage: bir scenario show <runId>");
966
- return 2;
967
- }
968
- const { status, body } = await service("GET", `/recordings/runs/${target}/scenario`);
969
- if (status === 404) {
970
- out("No scenario for that run yet — `bir scenario calc <runId>` first.");
971
- return 1;
972
- }
973
- if (status !== 200) {
974
- out(`Could not read the scenario (HTTP ${status}).`);
975
- return 1;
976
- }
977
- out(JSON.stringify(body, null, 2));
978
- return 0;
979
- }
980
- if (sub === "calc") {
981
- if (!target) {
982
- out("usage: bir scenario calc <runId> [--force]");
983
- return 2;
984
- }
985
- const { status, body } = await service("POST", `/recordings/runs/${target}/calculate`, args.force ? { force: true } : {});
986
- if (status === 202) {
987
- const b = body;
988
- out(`Calculating ${b.scenarioId ?? ""} — poll with \`bir scenario show ${target}\`.`);
989
- return 0;
990
- }
991
- if (status === 409) {
992
- out("That run already has a scenario. Re-derive it in place with --force.");
993
- return 1;
994
- }
995
- if (status === 503) {
996
- out("The service has no Anthropic configuration, so it cannot calculate scenarios.");
997
- return 1;
998
- }
999
- out(`Could not start calculation (HTTP ${status}): ${JSON.stringify(body)}`);
1000
- return 1;
1052
+ // show, calc, and everything that reads or changes one step (editSteps.md).
1053
+ if (SCENARIO_EDIT_SUBCOMMANDS.has(sub)) {
1054
+ return scenarioEditCommand({
1055
+ positionals: args.positionals,
1056
+ json: args.json,
1057
+ force: args.force,
1058
+ step: args.step,
1059
+ inputLogic: args.inputLogic,
1060
+ outputLogic: args.outputLogic,
1061
+ freeze: args.freeze,
1062
+ unfreeze: args.unfreeze,
1063
+ note: args.note,
1064
+ edit: args.edit,
1065
+ revision: args.revision,
1066
+ discardEdits: args.discardEdits,
1067
+ }, {
1068
+ cwd: process.cwd(),
1069
+ out,
1070
+ service: async (method, path, body) => {
1071
+ try {
1072
+ return await service(method, path, body);
1073
+ }
1074
+ catch (err) {
1075
+ return { error: errText(err) };
1076
+ }
1077
+ },
1078
+ readFile: (path) => readFileSync(resolvePath(process.cwd(), path)),
1079
+ readStdin,
1080
+ });
1001
1081
  }
1002
1082
  if (sub === "replay") {
1003
1083
  return replayCommand({ ...args, scenarioId: args.scenarioId ?? target });
1004
1084
  }
1005
- out("usage: bir scenario <list|show|calc|replay> …");
1085
+ out("usage: bir scenario <list|show|calc|check|edit|edits|undo|editing|replay> …");
1006
1086
  return 2;
1007
1087
  }
1088
+ /**
1089
+ * All of stdin, for `--input-logic -`. A person at a terminal gets told how to
1090
+ * end it; a pipe never sees the line (it goes to stderr, and only on a TTY).
1091
+ */
1092
+ function readStdin() {
1093
+ if (process.stdin.isTTY) {
1094
+ process.stderr.write("[bir] reading the logic from stdin — end it with Ctrl-D (Ctrl-Z, Enter on Windows)\n");
1095
+ }
1096
+ return new Promise((resolve, reject) => {
1097
+ const chunks = [];
1098
+ process.stdin.on("data", (chunk) => {
1099
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
1100
+ });
1101
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks)));
1102
+ process.stdin.on("error", reject);
1103
+ });
1104
+ }
1008
1105
  /**
1009
1106
  * `--dry` evaluates the scenario's stored logic against the **source run's
1010
1107
  * recorded outputs**, server-side: no real tools, no side effects, one Haiku
@@ -183,7 +183,7 @@ export const DECLINES = {
183
183
  },
184
184
  nondeterministic_first_step: {
185
185
  cause: "the chain's first step needs a judgement the calculation could not write code for, so the plan cannot start.",
186
- fix: "the agent does this task itself; a model step for such judgements is planned (plan-services.md W3.1).",
186
+ 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).",
187
187
  },
188
188
  unusable_first_step: {
189
189
  cause: "the first step calls a sub-task that is gone, switched off or stale.",
@@ -0,0 +1,173 @@
1
+ /**
2
+ * scenario-edit — read, check, change and undo one step of a calculated
3
+ * scenario (editSteps.md in the BaseIn repository; docs/calculatedReplayGuide.md §9.2).
4
+ *
5
+ * bir scenario show <runId|scnId> [--step <n>]
6
+ * bir scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
7
+ * bir scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
8
+ * [--freeze | --unfreeze] [--note "why"] [--force --note "why"] [--revision <n>]
9
+ * bir scenario edits <runId|scnId>
10
+ * bir scenario undo <runId|scnId> [--edit <sedit_id>]
11
+ * bir scenario calc <runId> [--force [--discard-edits]]
12
+ * bir scenario editing on|off|status
13
+ *
14
+ * WHY THIS IS A CLIENT AND NOTHING MORE. Whether a change may be saved is
15
+ * decided on the service, against the recording, by the calculation's own
16
+ * check (D3) — and `check` and `edit` are the same function there (D11). A
17
+ * runner-side pre-check would be a second opinion that can disagree with the
18
+ * one that counts, so this module reads logic from files, sends it, and says
19
+ * in words what the service answered. `--json` prints the answer unchanged,
20
+ * which is what the `bir` MCP server hands the model.
21
+ *
22
+ * Logic never comes from the command line itself. A JavaScript body is full
23
+ * of quotes, braces and `$`, and every shell mangles a different subset of
24
+ * them; a file, or stdin with `-`, arrives byte for byte.
25
+ *
26
+ * Everything the commands touch is injected ({@link ScenarioEditDeps}), as in
27
+ * `investigate.ts`, so all of it is tested in-process against a fake service.
28
+ * The exception is `editing`, which writes the install sidecar — tests point
29
+ * `BIR_HOME` at a temporary directory for that.
30
+ */
31
+ export type InputStatus = "reproduces" | "differs" | "throws" | "copy" | "not_checked";
32
+ export interface EditReport {
33
+ ok: boolean;
34
+ stepIndex: number;
35
+ toolName: string | null;
36
+ /** The recording the change was checked against. */
37
+ sourceRunId: string;
38
+ verifiable: boolean;
39
+ input: {
40
+ changed: boolean;
41
+ before: InputStatus;
42
+ after: InputStatus;
43
+ computed?: unknown;
44
+ recorded?: unknown;
45
+ firstDifference?: {
46
+ at: number;
47
+ computed: string;
48
+ recorded: string;
49
+ };
50
+ error?: string;
51
+ };
52
+ output: {
53
+ changed: boolean;
54
+ status: "runs" | "throws" | "not_object" | "unchanged";
55
+ emittedKeys?: string[];
56
+ error?: string;
57
+ };
58
+ later: Array<{
59
+ stepIndex: number;
60
+ toolName: string | null;
61
+ before: InputStatus;
62
+ after: InputStatus;
63
+ regressed: boolean;
64
+ }>;
65
+ answer: {
66
+ informativeBefore: boolean;
67
+ informativeAfter: boolean;
68
+ } | null;
69
+ mark: {
70
+ before: MarkState;
71
+ after: MarkState;
72
+ };
73
+ problems: string[];
74
+ }
75
+ interface MarkState {
76
+ marked: boolean;
77
+ why: string | null;
78
+ }
79
+ export interface StepState {
80
+ toolInputLogic: string | null;
81
+ toolOutputLogic: string | null;
82
+ nondeterministic: boolean;
83
+ nondeterministicWhy: string | null;
84
+ editedAt: string | null;
85
+ editForced: boolean;
86
+ }
87
+ export interface EditEntry {
88
+ id: string;
89
+ scenarioId: string;
90
+ stepIndex: number;
91
+ kind: "edit" | "freeze" | "revert";
92
+ revertsEditId: string | null;
93
+ note: string | null;
94
+ forced: boolean;
95
+ chainRevisionBefore: number;
96
+ chainRevisionAfter: number;
97
+ before: StepState;
98
+ after: StepState;
99
+ report: EditReport | null;
100
+ createdAt: string;
101
+ /** A later undo put this one back. */
102
+ revertedAt: string | null;
103
+ /** A recalculation replaced the chain; the entry is history now. */
104
+ replacedAt: string | null;
105
+ }
106
+ export type ServiceReply = {
107
+ status: number;
108
+ body: unknown;
109
+ } | {
110
+ error: string;
111
+ };
112
+ export interface ScenarioEditDeps {
113
+ cwd: string;
114
+ out: (line?: string) => void;
115
+ /** An authenticated call to the service; a failure to reach it is `{ error }`, never a throw. */
116
+ service: (method: string, path: string, body?: unknown) => Promise<ServiceReply>;
117
+ /** A logic file's bytes. Relative paths are the caller's to resolve. */
118
+ readFile: (path: string) => Buffer;
119
+ /** All of stdin, for `--input-logic -` / `--output-logic -`. */
120
+ readStdin: () => Promise<Buffer>;
121
+ }
122
+ export interface ScenarioEditArgs {
123
+ /** The words after `bir scenario`: `[sub, target | on|off|status, …]`. */
124
+ positionals: string[];
125
+ json: boolean;
126
+ force: boolean;
127
+ step?: number;
128
+ /** A path, or `-` for stdin. */
129
+ inputLogic?: string;
130
+ outputLogic?: string;
131
+ freeze: boolean;
132
+ unfreeze: boolean;
133
+ note?: string;
134
+ /** `undo --edit`: the `sedit_` id to put back. */
135
+ edit?: string;
136
+ /** `--revision`: save only if the plan is still at this `chainRevision`. */
137
+ revision?: number;
138
+ discardEdits: boolean;
139
+ }
140
+ /** The `bir scenario` words this module answers; `list` and `replay` stay in bir.ts. */
141
+ export declare const SCENARIO_EDIT_SUBCOMMANDS: ReadonlySet<string>;
142
+ /** The `bir` MCP server's tools that only read: always offered (D2). */
143
+ export declare const READ_TOOLS: readonly ["scenario_show", "scenario_edits", "investigate"];
144
+ /** The ones that change a scenario: offered only after `bir scenario editing on` (D2). */
145
+ export declare const EDIT_TOOLS: readonly ["scenario_check", "scenario_edit", "scenario_undo"];
146
+ export declare function scenarioEditCommand(args: ScenarioEditArgs, deps: ScenarioEditDeps): Promise<number>;
147
+ /**
148
+ * A logic file's text. UTF-8, with a byte-order mark dropped — and UTF-16 with
149
+ * its mark decoded, because that is what `"…" > step.js` writes in Windows
150
+ * PowerShell 5.1, and a service answering "SyntaxError" to a file that looks
151
+ * fine in every editor is a riddle nobody should have to solve.
152
+ */
153
+ export declare function decodeLogic(bytes: Buffer): string;
154
+ /**
155
+ * The report in the lines editSteps.md "What a person sees" shows:
156
+ * `input`, `output` (when the output logic changed), `later`, `answer` (when
157
+ * the final answer read anything before) and `mark`.
158
+ */
159
+ export declare function renderReport(out: (line?: string) => void, r: EditReport): void;
160
+ /**
161
+ * About {@link CONTEXT} characters either side of `at`. A text already that
162
+ * short — a window the service cut itself, or a small call — is printed whole.
163
+ */
164
+ export declare function around(text: string, at: number): string;
165
+ /**
166
+ * Undo puts one step back as it was before its newest edit (D9). Without
167
+ * `--edit`, that is the newest entry nothing has put back and no
168
+ * recalculation replaced — which is by construction the newest of its step.
169
+ * An undo is itself such an entry, so a second `undo` redoes.
170
+ */
171
+ export declare function newestUndoable(edits: EditEntry[]): EditEntry | undefined;
172
+ export {};
173
+ //# sourceMappingURL=scenario-edit.d.ts.map