@basein/runner 0.2.11 → 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/README.md CHANGED
@@ -148,8 +148,21 @@ between the two is not guaranteed.
148
148
  - **Change what the model does.** Tool descriptions are never edited. The
149
149
  model-visible changes are schema relaxation, below, and — only with
150
150
  `--replay` — the first-party `bir` server's tools: `run_scenario`, and three
151
- that only read a scenario. The three that change one appear only after
152
- `bir scenario editing on` in that project.
151
+ that only read a scenario. The three for changing one (`scenario_check`,
152
+ `scenario_edit`, `scenario_undo`) appear only after `bir scenario editing on`
153
+ in that project (not yet released, see below).
154
+ - **Record its own `bir` calls** (next version, not yet released). A session
155
+ that fixes a plan is not a task to replay. The `bir` server's tools other
156
+ than `run_scenario`, and a `Bash` or `PowerShell` command made only of `bir`
157
+ commands, are never recorded as steps and never count as leaving a plan.
158
+ `npm test && bir investigate` is still recorded: `npm test` is real work.
159
+ - **Sandbox scenario code on this machine.** A calculated or hand-edited step
160
+ runs here with `new Function`, next to your sign-in and your wrapped servers.
161
+ The service will sandbox its own runs of the same code (with its next deploy);
162
+ the runner does not. The service's check of a hand edit only judges what the
163
+ code returns for the recording's values, so it cannot prove what the code
164
+ does here: read a hand edit before it is saved. See
165
+ [the design's §13.1](docs/calculatedReplay.md).
153
166
 
154
167
  ---
155
168
 
@@ -194,22 +207,28 @@ sequence runs through connections the proxies already hold — **zero model toke
194
207
  scenarios steer the live session instead, pinning each step's arguments. Either
195
208
  way the turn reports what it cost, so the savings ledger measures something real.
196
209
 
197
- **Fixing a step.** A calculated step is code, and code can be wrong.
198
- `bir investigate` names such a step; `bir scenario edit` fixes it, and the
199
- service checks the change against the recording before it saves it. **A change
200
- is saved only if it reproduces the recording — or on purpose, with
201
- `--force --note "why"`.** Only the owner can change a scenario. Claude can do it
202
- too: in its terminal, or with the `bir` server's tools once
203
- `bir scenario editing on` was run in the project. Built 2026-09-25, not yet
204
- released: it needs the next `@basein/runner` version and a service deploy. Step
205
- by step: [the guide's §9.2](docs/calculatedReplayGuide.md).
210
+ **Fixing a step.** Built 2026-09-25, not yet released: it needs the next
211
+ `@basein/runner` version and a service deploy. A calculated step is code, and
212
+ code can be wrong. `bir investigate` names such a step; `bir scenario edit`
213
+ fixes it, and the service checks the change against the recording before it
214
+ saves it. **A change is saved only if it reproduces the recording and is not a
215
+ copy — or on purpose, with `--force --note "why"`.** Only the owner can change
216
+ a scenario. `bir scenario undo` takes changes back one at a time: run it again
217
+ to go one further back. An undo is checked too, and is refused if it would
218
+ break a later step or the final answer, unless you add `--force --note "why"`.
219
+ Claude can do all of this: in its terminal, or with the `bir` server's tools
220
+ once `bir scenario editing on` was run in the project. Step by step, with real
221
+ output:
222
+ [the guide's §9.2](docs/calculatedReplayGuide.md).
206
223
 
207
224
  ```bash
208
- bir scenario show scn_… --step 3 # read one step
225
+ bir scenario show scn_… --step 3 # read one step (scenarioId and chainRevision first)
209
226
  bir scenario check scn_… --step 3 --input-logic step3.js # try a change; nothing is saved
210
227
  bir scenario edit scn_… --step 3 --input-logic step3.js --note "why" # the same check, then save
211
- bir scenario edits scn_… # the history
212
- bir scenario undo scn_… # undo the newest edit
228
+ bir scenario edits scn_… # the history, newest first
229
+ bir scenario undo scn_… # take back the newest edit; again to go further back
230
+ bir scenario undo scn_… --step 3 # take back step 3's newest edit only
231
+ bir scenario calc scn_… --force --discard-edits # recalculate this plan, dropping its hand edits
213
232
  bir scenario editing on # let Claude's `bir` tools change scenarios here
214
233
  ```
215
234
 
@@ -26,6 +26,17 @@
26
26
  * Logic bodies travel through temporary files, never argv, and a non-zero exit
27
27
  * is an `isError` result that carries the output, refusal and report included.
28
28
  *
29
+ * A CALL IN FLIGHT IS OWNED UNTIL IT ENDS. Each scenario call's `bir` child and
30
+ * its temporary directory are tracked. When the host goes away (stdin closes,
31
+ * SIGINT, SIGTERM) the children are stopped and the directories removed before
32
+ * the process exits — `process.exit` never resumes a pending `finally`, so
33
+ * cleanup left to one was cleanup that did not happen, and an orphaned `bir`
34
+ * went on to save an edit for a session that had ended (C13). A
35
+ * `notifications/cancelled` (the person pressed Esc) stops that call's child
36
+ * the same way, and the call gets no answer, as MCP asks. A request the
37
+ * service already received may still complete there; nothing here can call
38
+ * it back, and `bir scenario edits` shows it.
39
+ *
29
40
  * The tools that CHANGE a scenario are offered only where a person ran
30
41
  * `bir scenario editing on` (D2): this server runs in every session of every
31
42
  * installed project, a fleet included, and a plan must not change because some
@@ -26,6 +26,17 @@
26
26
  * Logic bodies travel through temporary files, never argv, and a non-zero exit
27
27
  * is an `isError` result that carries the output, refusal and report included.
28
28
  *
29
+ * A CALL IN FLIGHT IS OWNED UNTIL IT ENDS. Each scenario call's `bir` child and
30
+ * its temporary directory are tracked. When the host goes away (stdin closes,
31
+ * SIGINT, SIGTERM) the children are stopped and the directories removed before
32
+ * the process exits — `process.exit` never resumes a pending `finally`, so
33
+ * cleanup left to one was cleanup that did not happen, and an orphaned `bir`
34
+ * went on to save an edit for a session that had ended (C13). A
35
+ * `notifications/cancelled` (the person pressed Esc) stops that call's child
36
+ * the same way, and the call gets no answer, as MCP asks. A request the
37
+ * service already received may still complete there; nothing here can call
38
+ * it back, and `bir scenario edits` shows it.
39
+ *
29
40
  * The tools that CHANGE a scenario are offered only where a person ran
30
41
  * `bir scenario editing on` (D2): this server runs in every session of every
31
42
  * installed project, a fleet included, and a plan must not change because some
@@ -37,7 +48,7 @@
37
48
  * stream, and every log line goes to stderr instead.
38
49
  */
39
50
  import { readFrames, writeFrame } from "../jsonrpc/framing.js";
40
- import { JsonRpcErrorCode, errorResponse, isRequest, paramsObject, } from "../jsonrpc/types.js";
51
+ import { JsonRpcErrorCode, errorResponse, isNotification, isRequest, paramsObject, } from "../jsonrpc/types.js";
41
52
  import { spawn } from "node:child_process";
42
53
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
43
54
  import { tmpdir } from "node:os";
@@ -134,7 +145,7 @@ class BadArguments extends Error {
134
145
  }
135
146
  const TARGET = {
136
147
  type: "string",
137
- description: "The scenario's scn_ id, or the run_ id of the recording it was calculated from — as investigate and scenario_show print them.",
148
+ description: "A scn_ id, or the run_ id of a whole-run scenario (a sub-task scenario has only its scn_ id) — as investigate and scenario_show print them.",
138
149
  };
139
150
  const STEP = {
140
151
  type: "integer",
@@ -185,13 +196,22 @@ function flag(args, key, name) {
185
196
  throw new BadArguments(`\`${key}\` must be true or false`);
186
197
  return [name];
187
198
  }
199
+ /** `--note <text>`. The text is one argv element, so no quoting can split it. */
200
+ function note(args) {
201
+ const value = args.note;
202
+ if (value === undefined || value === null)
203
+ return [];
204
+ if (typeof value !== "string")
205
+ throw new BadArguments("`note` must be a string");
206
+ return ["--note", value];
207
+ }
188
208
  const SCENARIO_TOOLS = [
189
209
  {
190
210
  name: "scenario_show",
191
211
  changes: false,
192
212
  description: "Read a calculated scenario of this account: the whole plan, or with `step` just that step — its input logic, " +
193
213
  "output logic, whether it is marked *needs a judgement* (nondeterministic) and the recorded input and output " +
194
- "it has to reproduce. Read a step here before changing it.",
214
+ "it has to reproduce, with the scenario's `scenarioId` and `chainRevision` at the top. Read a step here before changing it.",
195
215
  inputSchema: {
196
216
  type: "object",
197
217
  properties: { target: TARGET, step: STEP },
@@ -266,9 +286,11 @@ const SCENARIO_TOOLS = [
266
286
  changes: true,
267
287
  description: "Save a change to one step of a calculated scenario. It runs the same check as scenario_check and saves only " +
268
288
  "if the step reproduces the recording and is not a copy of the recorded call; always give a `note` saying why, " +
269
- "and `revision` (the chainRevision scenario_show printed) so a plan that changed meanwhile is not overwritten. " +
289
+ "and `revision` (the `chainRevision` at the top of a scenario_show answer with `step`) so a plan that changed " +
290
+ "meanwhile is not overwritten. " +
270
291
  "`force` saves what the check refused and is only for a recording that was itself wrong — never a way past a " +
271
- "failing check. Every save can be undone with scenario_undo.",
292
+ "failing check. A save can be taken back with scenario_undo, which the service checks too: it refuses an undo " +
293
+ "that would break a later step, and a recalculation with --discard-edits replaces edits for good.",
272
294
  inputSchema: {
273
295
  type: "object",
274
296
  properties: {
@@ -289,7 +311,7 @@ const SCENARIO_TOOLS = [
289
311
  revision: {
290
312
  type: "integer",
291
313
  minimum: 1,
292
- description: "The chainRevision the change was made against; the save is refused if the plan moved on.",
314
+ 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.",
293
315
  },
294
316
  },
295
317
  required: ["target", "step"],
@@ -307,11 +329,7 @@ const SCENARIO_TOOLS = [
307
329
  ...flag(a, "unfreeze", "--unfreeze"),
308
330
  ...flag(a, "force", "--force"),
309
331
  ];
310
- if (a.note !== undefined && a.note !== null) {
311
- if (typeof a.note !== "string")
312
- throw new BadArguments("`note` must be a string");
313
- argv.push("--note", a.note);
314
- }
332
+ argv.push(...note(a));
315
333
  if (a.revision !== undefined && a.revision !== null) {
316
334
  if (typeof a.revision !== "number" || !Number.isInteger(a.revision) || a.revision < 1) {
317
335
  throw new BadArguments("`revision` must be a whole number from 1");
@@ -324,13 +342,24 @@ const SCENARIO_TOOLS = [
324
342
  {
325
343
  name: "scenario_undo",
326
344
  changes: true,
327
- description: "Put one step of a calculated scenario back as it was before its newest hand edit — without `edit`, the newest " +
328
- "edit that can be undone. The undo is itself an edit, so undoing it again redoes the change.",
345
+ description: "Take back a hand edit of a calculated scenario: its step goes back as it was before that edit. Without `edit`, " +
346
+ "the newest edit that can be undone (of `step`, when given); calling it again goes one further back. The service " +
347
+ "checks the undo like a save and refuses it when a later step or the final answer would break — `force` with a " +
348
+ "`note` undoes it anyway. To redo, undo the undo: pass its sedit_ id as `edit` (the answer names it).",
329
349
  inputSchema: {
330
350
  type: "object",
331
351
  properties: {
332
352
  target: TARGET,
333
- edit: { type: "string", description: "The sedit_ id to undo, as scenario_edits lists it." },
353
+ edit: { type: "string", description: "The sedit_ id to undo, as scenario_edits lists it. Not with `step`." },
354
+ step: {
355
+ ...STEP,
356
+ description: "Undo the newest edit of this step (its stepIndex, as scenario_show prints it). Not with `edit`.",
357
+ },
358
+ force: {
359
+ type: "boolean",
360
+ description: "Undo although the check refused it (a later step or the answer would break). Needs `note`.",
361
+ },
362
+ note: { type: "string", description: "Why; kept in the history. Required with `force`." },
334
363
  },
335
364
  required: ["target"],
336
365
  additionalProperties: false,
@@ -341,8 +370,12 @@ const SCENARIO_TOOLS = [
341
370
  if (typeof a.edit !== "string" || !/^sedit_[\w-]+$/.test(a.edit)) {
342
371
  throw new BadArguments("`edit` must be a sedit_… id");
343
372
  }
373
+ if (a.step !== undefined && a.step !== null) {
374
+ throw new BadArguments("`edit` names one entry and `step` picks one; give one of them");
375
+ }
344
376
  argv.push("--edit", a.edit);
345
377
  }
378
+ argv.push(...step(a, false), ...flag(a, "force", "--force"), ...note(a));
346
379
  return { argv };
347
380
  },
348
381
  },
@@ -372,7 +405,36 @@ function listTools(cwd) {
372
405
  function birPath() {
373
406
  return resolvePath(dirname(fileURLToPath(import.meta.url)), "bir.js");
374
407
  }
375
- function runBir(argv, cwd) {
408
+ /** Scenario tool calls in flight, by JSON-RPC request id. */
409
+ const inFlight = new Map();
410
+ /** Remove a call's logic directory, once. Never throws: this runs on the way out. */
411
+ function removeDir(flight) {
412
+ const dir = flight.dir;
413
+ if (!dir)
414
+ return;
415
+ flight.dir = undefined;
416
+ try {
417
+ // A child killed a moment ago may still hold a handle on Windows; retry briefly.
418
+ rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
419
+ }
420
+ catch (err) {
421
+ logLine("scenario.tmp_left", { dir, error: errText(err) });
422
+ }
423
+ }
424
+ /** Stop a call's `bir` (if it still runs) and remove its logic files. Synchronous: it runs before `process.exit`. */
425
+ function stopFlight(flight) {
426
+ const child = flight.child;
427
+ if (child && child.exitCode === null && child.signalCode === null) {
428
+ try {
429
+ child.kill();
430
+ }
431
+ catch {
432
+ // Already gone.
433
+ }
434
+ }
435
+ removeDir(flight);
436
+ }
437
+ function runBir(argv, cwd, flight) {
376
438
  return new Promise((resolve) => {
377
439
  const child = spawn(process.execPath, [...nodeFlagsToCarry(), birPath(), ...argv], {
378
440
  cwd,
@@ -381,6 +443,8 @@ function runBir(argv, cwd) {
381
443
  stdio: ["ignore", "pipe", "pipe"],
382
444
  windowsHide: true,
383
445
  });
446
+ if (flight)
447
+ flight.child = child;
384
448
  let stdout = "";
385
449
  let stderr = "";
386
450
  let timedOut = false;
@@ -412,9 +476,11 @@ const failed = (text) => ({ content: [{ type: "text", text }], isError: true });
412
476
  * Run one scenario tool. Never throws. The logic bodies are written to a
413
477
  * directory of their own under the OS temp directory, owner-only, and the
414
478
  * directory is removed whatever happens — the code may carry a table name or a
415
- * host the person would not want left lying around.
479
+ * host the person would not want left lying around. "Whatever happens"
480
+ * includes the host leaving mid-call: the directory and the child live in
481
+ * `flight`, which `main` stops on the way out ({@link stopFlight}).
416
482
  */
417
- async function callScenarioTool(tool, args, cwd) {
483
+ async function callScenarioTool(tool, args, cwd, flight = { tool: tool.name, cancelled: false }) {
418
484
  let plan;
419
485
  try {
420
486
  plan = tool.plan(args);
@@ -422,11 +488,11 @@ async function callScenarioTool(tool, args, cwd) {
422
488
  catch (err) {
423
489
  return failed(`invalid arguments for ${tool.name}: ${errText(err)}`);
424
490
  }
425
- let dir;
426
491
  try {
427
492
  const argv = [...plan.argv];
428
493
  if (plan.inputLogic !== undefined || plan.outputLogic !== undefined) {
429
- dir = mkdtempSync(join(tmpdir(), "bir-logic-"));
494
+ const dir = mkdtempSync(join(tmpdir(), "bir-logic-"));
495
+ flight.dir = dir;
430
496
  if (plan.inputLogic !== undefined) {
431
497
  const file = join(dir, "input.js");
432
498
  writeFileSync(file, plan.inputLogic, { encoding: "utf8", mode: 0o600 });
@@ -439,7 +505,11 @@ async function callScenarioTool(tool, args, cwd) {
439
505
  }
440
506
  }
441
507
  argv.push("--json");
442
- const run = await runBir(argv, cwd);
508
+ if (flight.cancelled)
509
+ return failed("cancelled before bir started");
510
+ const run = await runBir(argv, cwd, flight);
511
+ if (flight.cancelled)
512
+ return failed("cancelled");
443
513
  if (run.timedOut)
444
514
  return failed(`bir did not finish in ${BIR_TIMEOUT_MS / 1000} s`);
445
515
  const stdout = run.stdout.trim();
@@ -454,18 +524,46 @@ async function callScenarioTool(tool, args, cwd) {
454
524
  return failed(`could not run bir: ${errText(err)}`);
455
525
  }
456
526
  finally {
457
- if (dir)
458
- rmSync(dir, { recursive: true, force: true });
527
+ removeDir(flight);
459
528
  }
460
529
  }
461
530
  function main() {
462
531
  const cwd = process.cwd();
463
532
  let closing = false;
464
533
  const reply = (msg) => writeFrame(process.stdout, msg);
534
+ /**
535
+ * The host is gone: stop every `bir` still running for it and remove its
536
+ * logic files, then exit. Everything here is synchronous, because nothing
537
+ * after `process.exit` runs — not a pending call's `finally` either.
538
+ */
539
+ const shutdown = (why) => {
540
+ if (closing)
541
+ return;
542
+ closing = true;
543
+ if (inFlight.size > 0) {
544
+ logLine("scenario.shutdown", { why, stopped: [...inFlight.values()].map((f) => f.tool) });
545
+ }
546
+ for (const flight of inFlight.values())
547
+ stopFlight(flight);
548
+ inFlight.clear();
549
+ process.exit(0);
550
+ };
465
551
  readFrames(process.stdin, {
466
552
  onMessage: (msg) => {
467
- if (!isRequest(msg))
468
- return; // notifications need no answer
553
+ if (!isRequest(msg)) {
554
+ // Notifications need no answer; one says to stop a call (MCP
555
+ // `notifications/cancelled`), and a stopped call is answered by nobody.
556
+ if (isNotification(msg) && msg.method === "notifications/cancelled") {
557
+ const requestId = paramsObject(msg.params).requestId;
558
+ const flight = typeof requestId === "string" || typeof requestId === "number" ? inFlight.get(requestId) : undefined;
559
+ if (flight && !flight.cancelled) {
560
+ flight.cancelled = true;
561
+ logLine("scenario.tool_cancelled", { tool: flight.tool, running: flight.child !== undefined });
562
+ stopFlight(flight);
563
+ }
564
+ }
565
+ return;
566
+ }
469
567
  const { id, method } = msg;
470
568
  if (method === "initialize") {
471
569
  const params = paramsObject(msg.params);
@@ -503,7 +601,12 @@ function main() {
503
601
  return;
504
602
  }
505
603
  logDetail("scenario.tool", { cwd, tool: name });
506
- void callScenarioTool(scenarioTool, paramsObject(params.arguments), cwd).then((result) => {
604
+ const flight = { tool: name, cancelled: false };
605
+ inFlight.set(id, flight);
606
+ void callScenarioTool(scenarioTool, paramsObject(params.arguments), cwd, flight).then((result) => {
607
+ inFlight.delete(id);
608
+ if (flight.cancelled || closing)
609
+ return;
507
610
  logLine("scenario.tool_answered", { tool: name, isError: result.isError === true });
508
611
  reply({ jsonrpc: "2.0", id, result });
509
612
  });
@@ -529,17 +632,12 @@ function main() {
529
632
  reply(errorResponse(id, JsonRpcErrorCode.MethodNotFound, `unsupported method: ${method}`));
530
633
  },
531
634
  onParseError: (detail) => logLine("scenario.frame_error", { reason: detail.reason }),
532
- onEnd: () => {
533
- // The host closed our stdin: the session is over.
534
- if (closing)
535
- return;
536
- closing = true;
537
- process.exit(0);
538
- },
635
+ // The host closed our stdin: the session is over.
636
+ onEnd: () => shutdown("stdin closed"),
539
637
  });
540
638
  logLine("scenario.started", { cwd, pid: process.pid });
541
- process.on("SIGINT", () => process.exit(0));
542
- process.on("SIGTERM", () => process.exit(0));
639
+ process.on("SIGINT", () => shutdown("SIGINT"));
640
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
543
641
  process.on("uncaughtException", (err) => logLine("scenario.uncaught", { error: errText(err) }));
544
642
  process.on("unhandledRejection", (err) => logLine("scenario.unhandled_rejection", { error: errText(err) }));
545
643
  }
package/dist/bin/bir.d.ts CHANGED
@@ -12,9 +12,9 @@
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]]
15
+ * bir scenario list | show <runId|scnId> [--step <n>] | calc <runId|scnId> [--force [--discard-edits]]
16
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>]
17
+ * bir scenario edits | undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
18
18
  * bir scenario editing on|off|status ← may this project's `bir` MCP server change a scenario?
19
19
  * bir scenario replay <scnId> --prompt "…" [--dry] | bir replay status|on|off|allow …
20
20
  * bir investigate [<id>] | list | executions
package/dist/bin/bir.js CHANGED
@@ -12,9 +12,9 @@
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]]
15
+ * bir scenario list | show <runId|scnId> [--step <n>] | calc <runId|scnId> [--force [--discard-edits]]
16
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>]
17
+ * bir scenario edits | undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
18
18
  * bir scenario editing on|off|status ← may this project's `bir` MCP server change a scenario?
19
19
  * bir scenario replay <scnId> --prompt "…" [--dry] | bir replay status|on|off|allow …
20
20
  * bir investigate [<id>] | list | executions
@@ -72,16 +72,18 @@ Commands:
72
72
  scenario list recorded runs and their calculated scenarios
73
73
  scenario show <runId|scnId> [--step <n>]
74
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
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
77
78
  scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
78
79
  try a change to one step against the recording; saves nothing
79
80
  scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
80
81
  [--freeze | --unfreeze] [--note "why"] [--force --note "why"] [--revision <n>]
81
82
  the same check, and save the change if it passes
82
83
  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
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
85
87
  scenario editing on|off|status whether this project's bir MCP server offers the tools
86
88
  that change a scenario (restart Claude Code after)
87
89
  scenario replay <scnId> --prompt "…" [--dry]
@@ -111,14 +113,15 @@ Options:
111
113
  --dry replay against recorded outputs only; run no real tools
112
114
  --force scenario calc: recalculate in place · scenario edit: save although
113
115
  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
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
115
118
  --input-logic <file|-> scenario check|edit: the new input logic, from a file or stdin (-)
116
119
  --output-logic <file|-> scenario check|edit: the new output logic, from a file or stdin (-)
117
120
  --freeze scenario edit: mark the step "needs a judgement" (runs hand over before it)
118
121
  --unfreeze scenario check|edit: ask to remove that mark; the check decides
119
122
  --note <text> scenario edit|undo: why, kept in the history (required with --force)
120
123
  --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)
124
+ --edit <id> scenario undo: the sedit_ id to undo (default: the newest edit that can be)
122
125
  --discard-edits scenario calc --force: recalculate a plan with hand edits, replacing them
123
126
  --no-browser login: print the link and code, open nothing (SSH, headless)
124
127
  --token <value> login: redeem a one-time setup token from the console (no browser)
@@ -179,7 +179,7 @@ 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.",
@@ -7,8 +7,8 @@
7
7
  * bir scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
8
8
  * [--freeze | --unfreeze] [--note "why"] [--force --note "why"] [--revision <n>]
9
9
  * bir scenario edits <runId|scnId>
10
- * bir scenario undo <runId|scnId> [--edit <sedit_id>]
11
- * bir scenario calc <runId> [--force [--discard-edits]]
10
+ * bir scenario undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
11
+ * bir scenario calc <runId|scnId> [--force [--discard-edits]]
12
12
  * bir scenario editing on|off|status
13
13
  *
14
14
  * WHY THIS IS A CLIENT AND NOTHING MORE. Whether a change may be saved is
@@ -70,6 +70,10 @@ export interface EditReport {
70
70
  before: MarkState;
71
71
  after: MarkState;
72
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;
73
77
  problems: string[];
74
78
  }
75
79
  interface MarkState {
@@ -131,7 +135,7 @@ export interface ScenarioEditArgs {
131
135
  freeze: boolean;
132
136
  unfreeze: boolean;
133
137
  note?: string;
134
- /** `undo --edit`: the `sedit_` id to put back. */
138
+ /** `undo --edit`: the `sedit_` id to put back. `undo --step` is {@link step}. */
135
139
  edit?: string;
136
140
  /** `--revision`: save only if the plan is still at this `chainRevision`. */
137
141
  revision?: number;
@@ -155,19 +159,42 @@ export declare function decodeLogic(bytes: Buffer): string;
155
159
  * The report in the lines editSteps.md "What a person sees" shows:
156
160
  * `input`, `output` (when the output logic changed), `later`, `answer` (when
157
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.
158
174
  */
159
- export declare function renderReport(out: (line?: string) => void, r: EditReport): void;
175
+ export declare function renderReport(out: (line?: string) => void, r: EditReport, opts?: {
176
+ brief?: boolean;
177
+ undo?: boolean;
178
+ }): void;
160
179
  /**
161
180
  * About {@link CONTEXT} characters either side of `at`. A text already that
162
181
  * short — a window the service cut itself, or a small call — is printed whole.
163
182
  */
164
183
  export declare function around(text: string, at: number): string;
165
184
  /**
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.
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.
170
197
  */
171
- export declare function newestUndoable(edits: EditEntry[]): EditEntry | undefined;
198
+ export declare function newestUndoable(edits: EditEntry[], step?: number): EditEntry | undefined;
172
199
  export {};
173
200
  //# sourceMappingURL=scenario-edit.d.ts.map