@basein/runner 0.2.10 → 0.2.12

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|scnId> [--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> | --step <n>] [--force --note "why"]
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,22 @@ 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|scnId> [--force [--discard-edits]]
76
+ calculate (or re-derive) a run's scenario; a scn_ id
77
+ (a sub-task's too) is recalculated in place, with --force
78
+ scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
79
+ try a change to one step against the recording; saves nothing
80
+ scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
81
+ [--freeze | --unfreeze] [--note "why"] [--force --note "why"] [--revision <n>]
82
+ the same check, and save the change if it passes
83
+ scenario edits <runId|scnId> a scenario's hand edits, newest first
84
+ scenario undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
85
+ take back the newest edit (of step n); run it again to go
86
+ one further back; undo an undo's sedit_ id to redo
87
+ scenario editing on|off|status whether this project's bir MCP server offers the tools
88
+ that change a scenario (restart Claude Code after)
64
89
  scenario replay <scnId> --prompt "…" [--dry]
65
90
  replay --scenario <scnId> --prompt "…" [--dry]
66
91
  replay status | on | off | allow <a,b> | allow all
@@ -83,8 +108,21 @@ Options:
83
108
  --replay install/remove the scenario server, enabling calculated replay
84
109
  --port <n> install/setup: control-server port to write into the hook URLs
85
110
  (default: this project's, else the lowest free one from ${DEFAULT_CONTROL_PORT})
86
- --json machine-readable output for status / doctor
111
+ --json machine-readable output for status / doctor / scenario / investigate
112
+ (scenario check|edit|edits|undo: the service's answer, unchanged)
87
113
  --dry replay against recorded outputs only; run no real tools
114
+ --force scenario calc: recalculate in place · scenario edit: save although
115
+ the check refused it (needs --note; only for a recording that was wrong)
116
+ · scenario undo: undo although a later step or the answer would break
117
+ --step <n> scenario show|check|edit|undo: the step, by the stepIndex \`show\` prints
118
+ --input-logic <file|-> scenario check|edit: the new input logic, from a file or stdin (-)
119
+ --output-logic <file|-> scenario check|edit: the new output logic, from a file or stdin (-)
120
+ --freeze scenario edit: mark the step "needs a judgement" (runs hand over before it)
121
+ --unfreeze scenario check|edit: ask to remove that mark; the check decides
122
+ --note <text> scenario edit|undo: why, kept in the history (required with --force)
123
+ --revision <n> scenario edit|undo: only if the plan is still at this chainRevision
124
+ --edit <id> scenario undo: the sedit_ id to undo (default: the newest edit that can be)
125
+ --discard-edits scenario calc --force: recalculate a plan with hand edits, replacing them
88
126
  --no-browser login: print the link and code, open nothing (SSH, headless)
89
127
  --token <value> login: redeem a one-time setup token from the console (no browser)
90
128
  --password login: use the old email/password prompt (deprecated)
@@ -118,6 +156,9 @@ function parseArgs(argv) {
118
156
  noDaemon: false,
119
157
  restart: false,
120
158
  portExplicit: false,
159
+ freeze: false,
160
+ unfreeze: false,
161
+ discardEdits: false,
121
162
  };
122
163
  for (let i = 1; i < argv.length; i += 1) {
123
164
  const arg = argv[i];
@@ -214,6 +255,53 @@ function parseArgs(argv) {
214
255
  args.limit = n;
215
256
  break;
216
257
  }
258
+ case "--step": {
259
+ // Digits only: `Number("")` is 0 and `Number("0x4")` is 4, and a step
260
+ // index nobody typed is a step nobody meant to change.
261
+ const raw = argv[++i] ?? "";
262
+ if (!/^\d+$/.test(raw))
263
+ usage(2);
264
+ args.step = Number(raw);
265
+ break;
266
+ }
267
+ case "--input-logic":
268
+ // A value flag, so a bare `-` (stdin) is taken here and never reaches
269
+ // the default branch, which refuses anything that starts with a dash.
270
+ args.inputLogic = argv[++i];
271
+ if (!args.inputLogic)
272
+ usage(2);
273
+ break;
274
+ case "--output-logic":
275
+ args.outputLogic = argv[++i];
276
+ if (!args.outputLogic)
277
+ usage(2);
278
+ break;
279
+ case "--freeze":
280
+ args.freeze = true;
281
+ break;
282
+ case "--unfreeze":
283
+ args.unfreeze = true;
284
+ break;
285
+ case "--note":
286
+ args.note = argv[++i];
287
+ if (args.note === undefined)
288
+ usage(2);
289
+ break;
290
+ case "--edit":
291
+ args.edit = argv[++i];
292
+ if (!args.edit)
293
+ usage(2);
294
+ break;
295
+ case "--revision": {
296
+ const raw = argv[++i] ?? "";
297
+ if (!/^\d+$/.test(raw) || Number(raw) <= 0)
298
+ usage(2);
299
+ args.revision = Number(raw);
300
+ break;
301
+ }
302
+ case "--discard-edits":
303
+ args.discardEdits = true;
304
+ break;
217
305
  default:
218
306
  if (arg.startsWith("-"))
219
307
  usage(2);
@@ -221,6 +309,10 @@ function parseArgs(argv) {
221
309
  args.positionals.push(arg);
222
310
  }
223
311
  }
312
+ if (args.freeze && args.unfreeze) {
313
+ process.stderr.write("[bir] --freeze and --unfreeze are mutually exclusive\n");
314
+ process.exit(2);
315
+ }
224
316
  return args;
225
317
  }
226
318
  /** Absolute path to this package's built `bir-proxy.js`, for `--local`. */
@@ -960,51 +1052,59 @@ async function scenarioCommand(args) {
960
1052
  }
961
1053
  return 0;
962
1054
  }
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;
1055
+ // show, calc, and everything that reads or changes one step (editSteps.md).
1056
+ if (SCENARIO_EDIT_SUBCOMMANDS.has(sub)) {
1057
+ return scenarioEditCommand({
1058
+ positionals: args.positionals,
1059
+ json: args.json,
1060
+ force: args.force,
1061
+ step: args.step,
1062
+ inputLogic: args.inputLogic,
1063
+ outputLogic: args.outputLogic,
1064
+ freeze: args.freeze,
1065
+ unfreeze: args.unfreeze,
1066
+ note: args.note,
1067
+ edit: args.edit,
1068
+ revision: args.revision,
1069
+ discardEdits: args.discardEdits,
1070
+ }, {
1071
+ cwd: process.cwd(),
1072
+ out,
1073
+ service: async (method, path, body) => {
1074
+ try {
1075
+ return await service(method, path, body);
1076
+ }
1077
+ catch (err) {
1078
+ return { error: errText(err) };
1079
+ }
1080
+ },
1081
+ readFile: (path) => readFileSync(resolvePath(process.cwd(), path)),
1082
+ readStdin,
1083
+ });
1001
1084
  }
1002
1085
  if (sub === "replay") {
1003
1086
  return replayCommand({ ...args, scenarioId: args.scenarioId ?? target });
1004
1087
  }
1005
- out("usage: bir scenario <list|show|calc|replay> …");
1088
+ out("usage: bir scenario <list|show|calc|check|edit|edits|undo|editing|replay> …");
1006
1089
  return 2;
1007
1090
  }
1091
+ /**
1092
+ * All of stdin, for `--input-logic -`. A person at a terminal gets told how to
1093
+ * end it; a pipe never sees the line (it goes to stderr, and only on a TTY).
1094
+ */
1095
+ function readStdin() {
1096
+ if (process.stdin.isTTY) {
1097
+ process.stderr.write("[bir] reading the logic from stdin — end it with Ctrl-D (Ctrl-Z, Enter on Windows)\n");
1098
+ }
1099
+ return new Promise((resolve, reject) => {
1100
+ const chunks = [];
1101
+ process.stdin.on("data", (chunk) => {
1102
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
1103
+ });
1104
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks)));
1105
+ process.stdin.on("error", reject);
1106
+ });
1107
+ }
1008
1108
  /**
1009
1109
  * `--dry` evaluates the scenario's stored logic against the **source run's
1010
1110
  * recorded outputs**, server-side: no real tools, no side effects, one Haiku
@@ -179,11 +179,11 @@ export const DECLINES = {
179
179
  },
180
180
  known_bad_first_step: {
181
181
  cause: "the chain's first step is parked after repeated failures.",
182
- fix: "wait: the service retries it once an hour for a day, then repairs the plan from the next run of the prompt. To repair now: `bir scenario calc <runId> --force`.",
182
+ fix: "wait: the service retries it once an hour for a day, then repairs the plan from the next run of the prompt — unless the plan has steps edited by hand, or is a sub-task, which it does not repair on its own. To repair now: `bir scenario calc <scnId> --force` (add --discard-edits to drop hand edits), or fix the step: `bir scenario edit <scnId> --step 0 --input-logic <file>`. `bir investigate <scnId>` names the scenario id.",
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,200 @@
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> | --step <n>] [--force --note "why"]
11
+ * bir scenario calc <runId|scnId> [--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
+ /** The logic and mark asked for are what the step already has (rule 10). */
74
+ noChange?: boolean;
75
+ /** The check stopped at the service's sandbox-time budget: *too costly to check* (rule 11). */
76
+ budgetExceeded?: boolean;
77
+ problems: string[];
78
+ }
79
+ interface MarkState {
80
+ marked: boolean;
81
+ why: string | null;
82
+ }
83
+ export interface StepState {
84
+ toolInputLogic: string | null;
85
+ toolOutputLogic: string | null;
86
+ nondeterministic: boolean;
87
+ nondeterministicWhy: string | null;
88
+ editedAt: string | null;
89
+ editForced: boolean;
90
+ }
91
+ export interface EditEntry {
92
+ id: string;
93
+ scenarioId: string;
94
+ stepIndex: number;
95
+ kind: "edit" | "freeze" | "revert";
96
+ revertsEditId: string | null;
97
+ note: string | null;
98
+ forced: boolean;
99
+ chainRevisionBefore: number;
100
+ chainRevisionAfter: number;
101
+ before: StepState;
102
+ after: StepState;
103
+ report: EditReport | null;
104
+ createdAt: string;
105
+ /** A later undo put this one back. */
106
+ revertedAt: string | null;
107
+ /** A recalculation replaced the chain; the entry is history now. */
108
+ replacedAt: string | null;
109
+ }
110
+ export type ServiceReply = {
111
+ status: number;
112
+ body: unknown;
113
+ } | {
114
+ error: string;
115
+ };
116
+ export interface ScenarioEditDeps {
117
+ cwd: string;
118
+ out: (line?: string) => void;
119
+ /** An authenticated call to the service; a failure to reach it is `{ error }`, never a throw. */
120
+ service: (method: string, path: string, body?: unknown) => Promise<ServiceReply>;
121
+ /** A logic file's bytes. Relative paths are the caller's to resolve. */
122
+ readFile: (path: string) => Buffer;
123
+ /** All of stdin, for `--input-logic -` / `--output-logic -`. */
124
+ readStdin: () => Promise<Buffer>;
125
+ }
126
+ export interface ScenarioEditArgs {
127
+ /** The words after `bir scenario`: `[sub, target | on|off|status, …]`. */
128
+ positionals: string[];
129
+ json: boolean;
130
+ force: boolean;
131
+ step?: number;
132
+ /** A path, or `-` for stdin. */
133
+ inputLogic?: string;
134
+ outputLogic?: string;
135
+ freeze: boolean;
136
+ unfreeze: boolean;
137
+ note?: string;
138
+ /** `undo --edit`: the `sedit_` id to put back. `undo --step` is {@link step}. */
139
+ edit?: string;
140
+ /** `--revision`: save only if the plan is still at this `chainRevision`. */
141
+ revision?: number;
142
+ discardEdits: boolean;
143
+ }
144
+ /** The `bir scenario` words this module answers; `list` and `replay` stay in bir.ts. */
145
+ export declare const SCENARIO_EDIT_SUBCOMMANDS: ReadonlySet<string>;
146
+ /** The `bir` MCP server's tools that only read: always offered (D2). */
147
+ export declare const READ_TOOLS: readonly ["scenario_show", "scenario_edits", "investigate"];
148
+ /** The ones that change a scenario: offered only after `bir scenario editing on` (D2). */
149
+ export declare const EDIT_TOOLS: readonly ["scenario_check", "scenario_edit", "scenario_undo"];
150
+ export declare function scenarioEditCommand(args: ScenarioEditArgs, deps: ScenarioEditDeps): Promise<number>;
151
+ /**
152
+ * A logic file's text. UTF-8, with a byte-order mark dropped — and UTF-16 with
153
+ * its mark decoded, because that is what `"…" > step.js` writes in Windows
154
+ * PowerShell 5.1, and a service answering "SyntaxError" to a file that looks
155
+ * fine in every editor is a riddle nobody should have to solve.
156
+ */
157
+ export declare function decodeLogic(bytes: Buffer): string;
158
+ /**
159
+ * The report in the lines editSteps.md "What a person sees" shows:
160
+ * `input`, `output` (when the output logic changed), `later`, `answer` (when
161
+ * the final answer read anything before) and `mark`.
162
+ *
163
+ * `check` prints every line: it is where a person explores a change, and "step
164
+ * 5 still reproduces" is an answer there. `edit` and `undo` are acts, and print
165
+ * `brief`ly — what changed and what went wrong: `input` always, `output` when
166
+ * it changed, `later` only for a step that broke, `answer` only when it lost its
167
+ * values, `mark` only when it moves. The report is the same one either way (D11);
168
+ * only the lines that say "nothing to see" are left out.
169
+ *
170
+ * An `undo` does not judge the step itself — its code and mark go back exactly
171
+ * as they were (editSteps.md, "Undo") — so its report's input status is
172
+ * `not_checked` by design, and printing the usual reason for that ("the
173
+ * recorded call is plain text") would be false. It says what happened instead.
174
+ */
175
+ export declare function renderReport(out: (line?: string) => void, r: EditReport, opts?: {
176
+ brief?: boolean;
177
+ undo?: boolean;
178
+ }): void;
179
+ /**
180
+ * About {@link CONTEXT} characters either side of `at`. A text already that
181
+ * short — a window the service cut itself, or a small call — is printed whole.
182
+ */
183
+ export declare function around(text: string, at: number): string;
184
+ /**
185
+ * What `undo` without `--edit` takes back (D9): the newest **edit** — never an
186
+ * undo entry — that nothing has put back and no recalculation replaced; of
187
+ * step `step` when it is given.
188
+ *
189
+ * Undo entries are skipped on purpose. They used to be picked like any other,
190
+ * so a second `undo` redid the first one, and a step edited twice toggled
191
+ * between its two edits for ever and never got back to the calculation's code
192
+ * (C12). Now a second `undo` goes one further back; a redo is asked for by
193
+ * name, `--edit <the undo's id>`, which the undo prints.
194
+ *
195
+ * Whether the step is still exactly as that edit left it is the service's
196
+ * question, not this list's: it answers `not_latest_edit` when it is not.
197
+ */
198
+ export declare function newestUndoable(edits: EditEntry[], step?: number): EditEntry | undefined;
199
+ export {};
200
+ //# sourceMappingURL=scenario-edit.d.ts.map