@basein/runner 0.3.17 → 0.3.19

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.
Files changed (51) hide show
  1. package/dist/bin/bir-hooks.d.ts +11 -8
  2. package/dist/bin/bir-hooks.js +11 -8
  3. package/dist/bin/bir-scenario.d.ts +5 -1
  4. package/dist/bin/bir-scenario.js +155 -5
  5. package/dist/bin/bir.js +22 -9
  6. package/dist/bin/investigate.d.ts +9 -0
  7. package/dist/bin/investigate.js +26 -12
  8. package/dist/bin/scenario-edit.d.ts +7 -0
  9. package/dist/bin/scenario-edit.js +19 -2
  10. package/dist/bin/scenario-liveness.d.ts +12 -1
  11. package/dist/bin/scenario-liveness.js +8 -4
  12. package/dist/bin/setup.js +3 -3
  13. package/dist/control/client.d.ts +5 -0
  14. package/dist/control/client.js +7 -0
  15. package/dist/control/ensure-hook.d.ts +14 -3
  16. package/dist/control/ensure-hook.js +18 -5
  17. package/dist/control/recorder-main.d.ts +15 -0
  18. package/dist/control/recorder-main.js +33 -8
  19. package/dist/control/server.d.ts +192 -13
  20. package/dist/control/server.js +819 -41
  21. package/dist/control/transcript.d.ts +19 -0
  22. package/dist/control/transcript.js +57 -0
  23. package/dist/proxy/intercept.js +13 -0
  24. package/dist/proxy/session.d.ts +23 -0
  25. package/dist/proxy/session.js +71 -3
  26. package/dist/record/recorder.d.ts +156 -2
  27. package/dist/record/recorder.js +9 -0
  28. package/dist/record/remote-recorder.d.ts +59 -1
  29. package/dist/record/remote-recorder.js +297 -20
  30. package/dist/record/switchable-recorder.js +6 -0
  31. package/dist/replay/bundle.d.ts +8 -0
  32. package/dist/replay/bundle.js +20 -6
  33. package/dist/replay/condition-words.d.ts +28 -0
  34. package/dist/replay/condition-words.js +59 -0
  35. package/dist/replay/controller.d.ts +78 -7
  36. package/dist/replay/controller.js +318 -62
  37. package/dist/replay/derive.d.ts +15 -0
  38. package/dist/replay/derive.js +5 -0
  39. package/dist/replay/handover.d.ts +7 -0
  40. package/dist/replay/handover.js +89 -51
  41. package/dist/replay/plan.d.ts +101 -5
  42. package/dist/replay/plan.js +260 -18
  43. package/dist/replay/source-run.d.ts +2 -2
  44. package/dist/replay/source-run.js +3 -3
  45. package/dist/replay/targets.d.ts +40 -1
  46. package/dist/replay/targets.js +105 -3
  47. package/dist/replay/types.d.ts +25 -8
  48. package/docs/calculatedReplay.md +45 -5
  49. package/docs/calculatedReplayGuide.md +13 -4
  50. package/package.json +1 -1
  51. package/production.md +21 -7
@@ -38,10 +38,21 @@ export interface LiveCall {
38
38
  /** Redacted and capped, exactly as the probe sent it. */
39
39
  toolInput: string;
40
40
  }
41
+ /** The day the agent was told it is (control/transcript.ts `agentDayOf`; issue 1212). */
42
+ export interface AgentDay {
43
+ line: string;
44
+ day?: string;
45
+ source: "system" | "host";
46
+ }
41
47
  /** What a turn can tell derivation beyond its prompt (segmented.md R-PARAM-2). */
42
48
  export interface DeriveContext {
43
49
  liveCall?: LiveCall;
44
50
  recentResults?: string[];
51
+ /**
52
+ * The agent's own day, read again each time it is asked: its host writes it
53
+ * into the session only after the prompt hook (issue 1212).
54
+ */
55
+ agentDay?: () => AgentDay | undefined;
45
56
  }
46
57
  /**
47
58
  * The service, which derives when this runner has no key of its own
@@ -62,6 +73,8 @@ export interface DeriveOptions extends DeriveContext {
62
73
  paramsObject: ParamsSchema | null;
63
74
  /** The day on this machine. Injected by tests; defaults to {@link localDay}. */
64
75
  today?: string;
76
+ /** The words that told the agent its day, when its host showed it any (issue 1212). */
77
+ todaySaid?: string;
65
78
  /** An override that keeps the reading on this machine. Absent → the service. */
66
79
  apiKey?: string;
67
80
  /** Used when there is no `apiKey`. Absent too → recorded samples. */
@@ -91,6 +104,8 @@ export interface DeriveResult {
91
104
  missing: string[];
92
105
  /** Settings filled from their recorded sample (R-PARAM-2). */
93
106
  sampled: string[];
107
+ /** The day the service read the turn's dates from, when it said (issue 1212). */
108
+ dayUsed?: string;
94
109
  }
95
110
  /**
96
111
  * The day on this machine, `YYYY-MM-DD`, in its own time zone.
@@ -343,6 +343,10 @@ async function deriveViaService(opts, service, schema, keys) {
343
343
  // What "tomorrow" is counted from. A service that does not know the
344
344
  // field ignores it.
345
345
  today: opts.today ?? localDay(),
346
+ // The words that told the agent its day ("Today's date is Thursday,
347
+ // 2023-11-30 …"), which the service turns into the day it reads from
348
+ // (issue 1212). Older services ignore it.
349
+ ...(opts.todaySaid ? { todaySaid: opts.todaySaid } : {}),
346
350
  }),
347
351
  signal: opts.signal ?? timeoutSignal(),
348
352
  });
@@ -402,6 +406,7 @@ async function deriveViaService(opts, service, schema, keys) {
402
406
  model: body.model,
403
407
  missing,
404
408
  sampled,
409
+ ...(typeof body.today === "string" ? { dayUsed: body.today } : {}),
405
410
  };
406
411
  }
407
412
  //# sourceMappingURL=derive.js.map
@@ -156,6 +156,13 @@ export interface HandoverNoteInput {
156
156
  * from there on are for, which this request is not (autocorrect.md AC5).
157
157
  */
158
158
  whenText?: string | null;
159
+ /**
160
+ * For `decision`: the live request the plan serves, the user's own words.
161
+ * Its sentences with a condition word are quoted, so the agent sees the
162
+ * choice that is its to make (the service's segmentsbreakdown.md SB-18).
163
+ * Read for no other kind.
164
+ */
165
+ request?: string | null;
159
166
  }
160
167
  export declare function buildHandoverNote(n: HandoverNoteInput): string;
161
168
  /** "Step 2", "Steps 1–3", "Steps 1, 3 and 5–6": 1-based numbers in order, runs joined. */
@@ -11,6 +11,7 @@
11
11
  * and a tool's own payload is already in front of it where it ran.
12
12
  */
13
13
  import { truncate } from "./bundle.js";
14
+ import { conditionSentences } from "./condition-words.js";
14
15
  /** Hard ceiling on a note. Long enough to carry derived params, short enough to read. */
15
16
  export const MAX_HANDOVER_NOTE = 4_000;
16
17
  const MAX_ERROR_CHARS = 500;
@@ -26,6 +27,8 @@ const UNUSABLE_LINE = {
26
27
  unsupported: "this runner cannot run",
27
28
  };
28
29
  export function buildHandoverNote(n) {
30
+ if (n.kind === "decision")
31
+ return decisionNote(n);
29
32
  const intent = n.intent.length > 240 ? `${n.intent.slice(0, 240)}…` : n.intent;
30
33
  const human = n.stepIndex + 1;
31
34
  const tool = n.stepToolName ? ` (${n.stepToolName})` : "";
@@ -35,6 +38,91 @@ export function buildHandoverNote(n) {
35
38
  ];
36
39
  if (intent)
37
40
  lines.push(`Intent: ${intent}`);
41
+ lines.push(...ranLines(n));
42
+ if (n.kind === "unusable_call") {
43
+ // A call is a stretch of work, not a tool: the agent is told what the
44
+ // sub-task was, not which tool did not run.
45
+ const what = n.unusable?.callIntent ? ` ("${truncate(n.unusable.callIntent, 200)}")` : "";
46
+ const why = UNUSABLE_LINE[n.unusable?.reason ?? ""] ?? "that cannot run here";
47
+ lines.push(`Step ${human} of ${n.totalSteps} is a sub-task${what} ${why}.`);
48
+ }
49
+ else if (n.kind === "known_bad_step") {
50
+ const times = n.failureCount ? ` ${n.failureCount} times` : "";
51
+ // A parked step inside a called segment names the scenario that owns it:
52
+ // that is where the repair happens, and the agent's own step numbering has
53
+ // no row for it (R-CALL-30).
54
+ const owner = n.inSubTask
55
+ ? `, in sub-task "${truncate(n.inSubTask, 120)}"${n.ownerScenarioId ? `, scenario ${n.ownerScenarioId}` : ""}`
56
+ : "";
57
+ lines.push(`Step ${human} of ${n.totalSteps}${tool.replace(/\)$/, `${owner})`)} did not run: it has failed${times} before, so the scenario stops in front of it.`);
58
+ }
59
+ else if (n.kind === "nondeterministic_step") {
60
+ // The calculation could not write code for this step's input: it is a
61
+ // judgement, and the agent is the one to make it (plan-services.md D8).
62
+ lines.push(`Step ${human} of ${n.totalSteps}${tool} did not run: its input needs a judgement the scenario cannot compute. Make that choice yourself and carry on.`);
63
+ }
64
+ else if (n.kind === "other_branch") {
65
+ // The plan holds one half of an "if … otherwise" and this request is the
66
+ // other (autocorrect.md AC5). The steps from here are not wrong, they are
67
+ // not this request's: the model is told what they are for, never to run
68
+ // them, and the list of their tools is left out below for that reason.
69
+ const why = n.whenText ? `they are only for when ${n.whenText}, and that is not so for this request` : "this request is the other case of the task, which they are not for";
70
+ lines.push(n.stepIndex >= n.totalSteps
71
+ ? "None of the scenario's steps was needed for this request. Do the task as this request needs it."
72
+ : `Step ${human} of ${n.totalSteps}${tool} and the steps after it did not run: ${why}. Do the rest of the task yourself, as this request needs it.`);
73
+ }
74
+ else {
75
+ lines.push(stepFailedLine(n, human, tool));
76
+ }
77
+ // Not for the other half: those tools are the ones this request must not
78
+ // run, and naming them invites exactly that.
79
+ if (n.remainingTools.length > 0 && n.kind !== "other_branch") {
80
+ lines.push(`The remaining planned steps were: ${n.remainingTools.join(", ")}.`);
81
+ }
82
+ if (n.params && Object.keys(n.params).length > 0) {
83
+ let json;
84
+ try {
85
+ json = JSON.stringify(n.params);
86
+ }
87
+ catch {
88
+ json = "[unserializable]";
89
+ }
90
+ lines.push(`Parameters derived for this request: ${truncate(json, MAX_PARAMS_CHARS)}`);
91
+ }
92
+ return truncate(lines.join("\n"), MAX_HANDOVER_NOTE);
93
+ }
94
+ /**
95
+ * The note of a plan stopped at its decision (the service's
96
+ * segmentsbreakdown.md SB-18): what ran, and the request's own words for the
97
+ * choice the agent now makes. Nothing that names the recorded half: no
98
+ * intent, no remaining steps, no parameters, and never the condition the
99
+ * service keeps (`stop.why`). Each would hint which way the recording went.
100
+ */
101
+ function decisionNote(n) {
102
+ // Where the results of the steps that ran are, as the ran-steps line says.
103
+ const above = Math.max(0, Math.min(n.executedAbove ?? 0, n.executed));
104
+ const delivery = above >= n.executed ? "above" : n.delivery;
105
+ const from = n.executed === 0
106
+ ? ""
107
+ : delivery === "above"
108
+ ? " from the results above"
109
+ : delivery === "below"
110
+ ? above > 0
111
+ ? " from the results above and below"
112
+ : " from the results below"
113
+ : "";
114
+ const quote = conditionSentences(n.request);
115
+ const ask = quote ? `This request asks for a decision: “${quote}”.` : "This task has a decision to make here.";
116
+ const lines = [
117
+ "[BaseInstRunner calculated replay] The calculated scenario for this request ran the steps before a decision, and stopped there.",
118
+ ...ranLines(n),
119
+ `${ask} Decide${from} which way applies, then continue the task, calling tools as needed.`,
120
+ ];
121
+ return truncate(lines.join("\n"), MAX_HANDOVER_NOTE);
122
+ }
123
+ /** What ran before the stop, and what of it the model may assume: the lines every note carries. */
124
+ function ranLines(n) {
125
+ const lines = [];
38
126
  // Steps left out by their tests sit among those before the stop (AC5): the
39
127
  // ones that ran are then named, never "steps 1–N", which would say a step
40
128
  // ran that did not.
@@ -100,57 +188,7 @@ export function buildHandoverNote(n) {
100
188
  lines.push(`Step ${n.inFlight.stepIndex + 1} of ${n.totalSteps}${inTool} was started and has not reported back: ` +
101
189
  "it may not have run, or may still complete. Check whether its work happened before you repeat it.");
102
190
  }
103
- if (n.kind === "unusable_call") {
104
- // A call is a stretch of work, not a tool: the agent is told what the
105
- // sub-task was, not which tool did not run.
106
- const what = n.unusable?.callIntent ? ` ("${truncate(n.unusable.callIntent, 200)}")` : "";
107
- const why = UNUSABLE_LINE[n.unusable?.reason ?? ""] ?? "that cannot run here";
108
- lines.push(`Step ${human} of ${n.totalSteps} is a sub-task${what} ${why}.`);
109
- }
110
- else if (n.kind === "known_bad_step") {
111
- const times = n.failureCount ? ` ${n.failureCount} times` : "";
112
- // A parked step inside a called segment names the scenario that owns it:
113
- // that is where the repair happens, and the agent's own step numbering has
114
- // no row for it (R-CALL-30).
115
- const owner = n.inSubTask
116
- ? `, in sub-task "${truncate(n.inSubTask, 120)}"${n.ownerScenarioId ? `, scenario ${n.ownerScenarioId}` : ""}`
117
- : "";
118
- lines.push(`Step ${human} of ${n.totalSteps}${tool.replace(/\)$/, `${owner})`)} did not run: it has failed${times} before, so the scenario stops in front of it.`);
119
- }
120
- else if (n.kind === "nondeterministic_step") {
121
- // The calculation could not write code for this step's input: it is a
122
- // judgement, and the agent is the one to make it (plan-services.md D8).
123
- lines.push(`Step ${human} of ${n.totalSteps}${tool} did not run: its input needs a judgement the scenario cannot compute. Make that choice yourself and carry on.`);
124
- }
125
- else if (n.kind === "other_branch") {
126
- // The plan holds one half of an "if … otherwise" and this request is the
127
- // other (autocorrect.md AC5). The steps from here are not wrong, they are
128
- // not this request's: the model is told what they are for, never to run
129
- // them, and the list of their tools is left out below for that reason.
130
- const why = n.whenText ? `they are only for when ${n.whenText}, and that is not so for this request` : "this request is the other case of the task, which they are not for";
131
- lines.push(n.stepIndex >= n.totalSteps
132
- ? "None of the scenario's steps was needed for this request. Do the task as this request needs it."
133
- : `Step ${human} of ${n.totalSteps}${tool} and the steps after it did not run: ${why}. Do the rest of the task yourself, as this request needs it.`);
134
- }
135
- else {
136
- lines.push(stepFailedLine(n, human, tool));
137
- }
138
- // Not for the other half: those tools are the ones this request must not
139
- // run, and naming them invites exactly that.
140
- if (n.remainingTools.length > 0 && n.kind !== "other_branch") {
141
- lines.push(`The remaining planned steps were: ${n.remainingTools.join(", ")}.`);
142
- }
143
- if (n.params && Object.keys(n.params).length > 0) {
144
- let json;
145
- try {
146
- json = JSON.stringify(n.params);
147
- }
148
- catch {
149
- json = "[unserializable]";
150
- }
151
- lines.push(`Parameters derived for this request: ${truncate(json, MAX_PARAMS_CHARS)}`);
152
- }
153
- return truncate(lines.join("\n"), MAX_HANDOVER_NOTE);
191
+ return lines;
154
192
  }
155
193
  /** "Step 2", "Steps 1–3", "Steps 1, 3 and 5–6": 1-based numbers in order, runs joined. */
156
194
  export function stepsPhrase(numbers) {
@@ -24,6 +24,48 @@ import { type FlatEntry } from "./flatten.js";
24
24
  import type { ReplayMode } from "./coverage.js";
25
25
  import type { HandoverStopReason } from "./handover.js";
26
26
  import type { ExecutionStage, FallbackKind, ParamsSchema, SerializedScenario, SerializedScenarioStep } from "./types.js";
27
+ /**
28
+ * A value a step of the recording worked out — an address the directory
29
+ * returned, the id a search found; the service labels it `from: "step"` —
30
+ * is never replayed from the recording (issue 1212). The plan holds this mark
31
+ * in its place. A step whose input still holds it, because its earlier steps
32
+ * did not return the value and its code fell back on the parameter, stops the
33
+ * plan in front of it, for the agent; a summary that holds it is left out.
34
+ * Never the recorded e-mail address, never the recorded lead.
35
+ */
36
+ export declare const FROM_STEP_MARK = "\u2063bir-from-step:";
37
+ /**
38
+ * A value the request did not give, which only the steps of one half of an
39
+ * "if \u2026 otherwise" read (the service's review of calcscenarioautocorrect.md,
40
+ * item 11; targets.ts, `testedOnlyTargets`). The plan arms without it \u2014 the
41
+ * other half needs nothing it lacks \u2014 and holds this mark in its place, never
42
+ * `null` and never the recorded value. A step whose input reads or holds it
43
+ * stops in front of its call, and the agent does that half; a test that reads
44
+ * it decides nothing; a summary that reads it is left out.
45
+ */
46
+ export declare const MISSING_VALUE_MARK = "\u2063bir-missing:";
47
+ /**
48
+ * A step's code read a value a step works out, which here only the recording
49
+ * holds (issue 1212). Not a fault of the step: its earlier steps did not
50
+ * return the value this time. The plan stops in front of it, the step counts
51
+ * as not run, and the agent does the rest.
52
+ */
53
+ export declare class RecordedValueError extends Error {
54
+ readonly key: string;
55
+ constructor(key: string, message?: string);
56
+ }
57
+ /**
58
+ * A step's code read a value the request did not give ({@link
59
+ * MISSING_VALUE_MARK}). The same stop as {@link RecordedValueError} \u2014 not a
60
+ * fault of the step: it counts as not run, nothing is parked or repaired, and
61
+ * the agent does the rest \u2014 so it is one, and every place that tells that
62
+ * stop apart tells this one apart too. Its words name the value.
63
+ */
64
+ export declare class MissingValueError extends RecordedValueError {
65
+ constructor(key: string);
66
+ }
67
+ /** The value a mark stands for, when `value` holds one anywhere \u2014 either kind. */
68
+ export declare function markedKey(value: unknown): string | undefined;
27
69
  /**
28
70
  * Runs one step's tool and returns its output as a string.
29
71
  *
@@ -76,6 +118,14 @@ export interface DriveOptions {
76
118
  * so what the run reports about it is the truth.
77
119
  */
78
120
  signal?: AbortSignal;
121
+ /**
122
+ * The tool the model may call although the bundle says to call none: `bir`'s
123
+ * own `mcp__bir__report_wrong_result`, when the session has it (the service's
124
+ * review of calcscenarioautocorrect.md, item 19). Set by the caller only
125
+ * where the `bir` server is connected; absent, the bundle's words are as
126
+ * they always were (bundle.ts, `AssembleOptions.reportTool`).
127
+ */
128
+ reportTool?: string;
79
129
  }
80
130
  /**
81
131
  * Supplies a step's recorded output when its tool cannot run here (§8.1).
@@ -289,7 +339,17 @@ export declare class ScenarioReplayPlan {
289
339
  private readonly scenario;
290
340
  /** The flat list this plan walks. One entry per step that will really run. */
291
341
  private readonly entries;
292
- private readonly paramsPromise;
342
+ private paramsPromise;
343
+ /** The values a step worked out in the recording ({@link FROM_STEP_MARK}). */
344
+ private readonly fromStep;
345
+ /**
346
+ * The targets only the steps of one half read (targets.ts,
347
+ * `testedOnlyTargets`): each held as {@link MISSING_VALUE_MARK} when the
348
+ * request does not give it.
349
+ */
350
+ private readonly oneHalf;
351
+ /** Of {@link oneHalf}, the ones the request did not give: held as the mark now. */
352
+ private missingMarked;
293
353
  private params;
294
354
  /** The derivation's own answer, before `paramsLogic`. What safety.ts judges. */
295
355
  private derivedParams;
@@ -385,6 +445,12 @@ export declare class ScenarioReplayPlan {
385
445
  * throws.
386
446
  */
387
447
  useFallbackParams(): void;
448
+ /**
449
+ * The values read again, before any step ran (issue 1212): the agent's own
450
+ * day showed in the session only after the first reading. False once a step
451
+ * has run, when the values the steps ran on are the ones that stand.
452
+ */
453
+ resetParams(params: Promise<Record<string, unknown>>): boolean;
388
454
  /** Whether the parameters are final. */
389
455
  get parametersSettled(): boolean;
390
456
  /** The derivation's own values, before `paramsLogic`. Empty until settled. */
@@ -403,12 +469,33 @@ export declare class ScenarioReplayPlan {
403
469
  stopStep(): SerializedScenarioStep | undefined;
404
470
  /** The entry the plan stops in front of, with the frame that owns it. */
405
471
  stopEntry(): FlatEntry | undefined;
472
+ /**
473
+ * The flat position of the chain's decision stop (the service's
474
+ * segmentsbreakdown.md SB-18), or -1 when it has none. The steps from there
475
+ * on are the half the recording took, whatever stops the plan first.
476
+ */
477
+ decisionIndex(): number;
406
478
  /** Every entry in order — for the report, and for the owning scenario ids. */
407
479
  allEntries(): readonly FlatEntry[];
408
480
  /** The entry awaiting execution, or undefined when the plan is done. */
409
481
  currentEntry(): FlatEntry | undefined;
410
482
  /** The parameters in force. Empty until {@link ready} resolves. */
411
483
  get parameters(): Readonly<Record<string, unknown>>;
484
+ /**
485
+ * The values a body may not use: a value a step works out ({@link
486
+ * FROM_STEP_MARK}) and a value the request did not give ({@link
487
+ * MISSING_VALUE_MARK}). A read of one is the stop {@link valueError} names.
488
+ */
489
+ private watchedKeys;
490
+ /** The stop a read of `key` means: a value the request did not give, or one only the recording had. */
491
+ private valueError;
492
+ /**
493
+ * `parameters` for one body, with every read of a value a step works out
494
+ * noted (issue 1212): a read is the recorded value used, however the code
495
+ * changes it afterwards — upper-cased, encoded, sliced or counted. A value
496
+ * the request did not give is watched the same way (review item 11).
497
+ */
498
+ private watched;
412
499
  /** Everything the steps have accumulated so far. */
413
500
  get accumulated(): Readonly<Record<string, unknown>>;
414
501
  /** The scenario's parameter schema — what the target and safety checks judge. */
@@ -439,6 +526,14 @@ export declare class ScenarioReplayPlan {
439
526
  * a throw as divergence.
440
527
  */
441
528
  toolInputForCurrentStep(): Record<string, unknown>;
529
+ /**
530
+ * A step's input, refused when it holds a value only the recording had: its
531
+ * code fell back on a parameter a step works out, and the earlier steps did
532
+ * not return it (issue 1212). The caller stops the plan in front of it. So
533
+ * is one that reads, or holds, a value the request did not give (review
534
+ * item 11): {@link MissingValueError}.
535
+ */
536
+ private inputOf;
442
537
  /** Whether the plan's cursor rests in front of the step it is parked before. */
443
538
  atStop(): boolean;
444
539
  /**
@@ -450,11 +545,12 @@ export declare class ScenarioReplayPlan {
450
545
  text: string | null;
451
546
  } | undefined;
452
547
  /**
453
- * The tool names of the steps from flat position `from` to the end, each
454
- * that runs only under a test marked so — for a hand-over note's "remaining
455
- * planned steps", which must not read as steps this request needs.
548
+ * The tool names of the steps from flat position `from` to the end (or to
549
+ * `until`, not included), each that runs only under a test marked so — for a
550
+ * hand-over note's "remaining planned steps", which must not read as steps
551
+ * this request needs.
456
552
  */
457
- remainingFrom(from: number): string[];
553
+ remainingFrom(from: number, until?: number): string[];
458
554
  /** The steps the plan's own cursor has left out so far, in order (AC5). */
459
555
  leftOut(): readonly LeftOut[];
460
556
  /** Whether any planned step runs only under a test. */