@basein/runner 0.3.3 → 0.3.4

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
@@ -347,6 +347,18 @@ bir scenario calc scn_… --force --discard-edits # recalculate this
347
347
  bir scenario editing on # let Claude's `bir` tools change scenarios here
348
348
  ```
349
349
 
350
+ **A value no prompt names.** Built 2026-09-28, not yet released. Every
351
+ parameter is a *target* (a prompt must name it, or the plan is declined:
352
+ `missing_target`) or a *setting* (its recorded value is used when a prompt
353
+ names none). When the calculation made a database, a host or a working folder a
354
+ target, the plan never runs. Make it a setting, with a note; a recalculation
355
+ keeps the change ([the guide's §9.2](docs/calculatedReplayGuide.md)):
356
+
357
+ ```bash
358
+ bir scenario edit scn_… --param database --kind setting --note "why" # use the recorded database when a prompt names none
359
+ bir scenario undo scn_… --param database # take that back
360
+ ```
361
+
350
362
  ---
351
363
 
352
364
  ## Two things to know before you turn it on
@@ -442,6 +442,31 @@ function note(args) {
442
442
  throw new BadArguments("`note` must be a string");
443
443
  return ["--note", value];
444
444
  }
445
+ /** `--revision <n>`, the chainRevision a change was made against. */
446
+ function revision(args) {
447
+ const value = args.revision;
448
+ if (value === undefined || value === null)
449
+ return [];
450
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
451
+ throw new BadArguments("`revision` must be a whole number from 1");
452
+ }
453
+ return ["--revision", String(value)];
454
+ }
455
+ const PARAM = {
456
+ type: "string",
457
+ description: "A parameter's name, as scenario_show lists it under `parameters` (for example `database`).",
458
+ };
459
+ /** A parameter's name: one argv element after `--param`, so it cannot read as a flag. */
460
+ function param(args) {
461
+ const value = args.param;
462
+ if (value === undefined || value === null)
463
+ return undefined;
464
+ if (typeof value !== "string" || value.length === 0 || value.length > 200 || /[\u0000-\u001f\u007f]/.test(value)) {
465
+ throw new BadArguments("`param` must be a parameter's name, as scenario_show lists it");
466
+ }
467
+ return value;
468
+ }
469
+ const given = (value) => value !== undefined && value !== null && value !== false;
445
470
  const SCENARIO_TOOLS = [
446
471
  {
447
472
  name: "scenario_show",
@@ -462,8 +487,8 @@ const SCENARIO_TOOLS = [
462
487
  name: "scenario_edits",
463
488
  changes: false,
464
489
  service: true,
465
- description: "List a calculated scenario's hand edits, newest first: step, note, revision, and whether each was forced, " +
466
- "undone or replaced by a recalculation.",
490
+ description: "List a calculated scenario's hand edits, newest first — step edits and parameter kind changes: step or " +
491
+ "parameter, note, revision, and whether each was forced, undone or replaced by a recalculation.",
467
492
  inputSchema: {
468
493
  type: "object",
469
494
  properties: { target: TARGET },
@@ -532,12 +557,24 @@ const SCENARIO_TOOLS = [
532
557
  "meanwhile is not overwritten. " +
533
558
  "`force` saves what the check refused and is only for a recording that was itself wrong — never a way past a " +
534
559
  "failing check. A save can be taken back with scenario_undo, which the service checks too: it refuses an undo " +
535
- "that would break a later step, and a recalculation with --discard-edits replaces edits for good.",
560
+ "that would break a later step, and a recalculation with --discard-edits replaces edits for good. " +
561
+ "Instead of `step`, `param` with `kind` changes what a prompt must name: a target must be named or the plan is " +
562
+ "declined (missing_target); a setting uses its recorded value when a prompt names none — for a value that is the " +
563
+ "same every time, such as a database, a host or a working folder. Making one a setting needs `note`.",
536
564
  inputSchema: {
537
565
  type: "object",
538
566
  properties: {
539
567
  target: TARGET,
540
568
  step: STEP,
569
+ param: {
570
+ ...PARAM,
571
+ description: `${PARAM.description} Changes that parameter's kind (with \`kind\`) instead of a step. Not with \`step\`.`,
572
+ },
573
+ kind: {
574
+ type: "string",
575
+ enum: ["target", "setting"],
576
+ description: "With `param`: target (a prompt must name it) or setting (its recorded value is used when a prompt names none; needs `note`).",
577
+ },
541
578
  inputLogic: INPUT_LOGIC,
542
579
  outputLogic: OUTPUT_LOGIC,
543
580
  freeze: {
@@ -549,17 +586,34 @@ const SCENARIO_TOOLS = [
549
586
  type: "boolean",
550
587
  description: "Save although the check refused it. Needs `note`. Only when the recording itself was wrong.",
551
588
  },
552
- note: { type: "string", description: "Why the step is changed; kept in the history. Required with `force`." },
589
+ note: {
590
+ type: "string",
591
+ description: "Why the step (or the parameter's kind) is changed; kept in the history. Required with `force`, and to make a parameter a setting.",
592
+ },
553
593
  revision: {
554
594
  type: "integer",
555
595
  minimum: 1,
556
596
  description: "The chainRevision the change was made against — at the top of a scenario_show answer with `step`; the save is refused if the plan moved on.",
557
597
  },
558
598
  },
559
- required: ["target", "step"],
599
+ // `step`, or `param` with `kind`: which one is checked below, in words.
600
+ required: ["target"],
560
601
  additionalProperties: false,
561
602
  },
562
603
  plan: (a) => {
604
+ const name = param(a);
605
+ if (name !== undefined || given(a.kind)) {
606
+ // A parameter's kind (findoffer.md): nothing runs, so nothing of a step's belongs here.
607
+ if (name === undefined)
608
+ throw new BadArguments("`kind` needs `param`: the parameter to change");
609
+ if (a.kind !== "target" && a.kind !== "setting")
610
+ throw new BadArguments("`kind` must be target or setting");
611
+ for (const key of ["step", "inputLogic", "outputLogic", "freeze", "unfreeze", "force"]) {
612
+ if (given(a[key]))
613
+ throw new BadArguments(`\`param\` changes only a parameter's kind; \`${key}\` belongs to a step`);
614
+ }
615
+ return { argv: ["scenario", "edit", target(a), "--param", name, "--kind", a.kind, ...note(a), ...revision(a)] };
616
+ }
563
617
  if (a.freeze === true && a.unfreeze === true)
564
618
  throw new BadArguments("`freeze` and `unfreeze` ask for opposite things");
565
619
  const argv = [
@@ -571,13 +625,7 @@ const SCENARIO_TOOLS = [
571
625
  ...flag(a, "unfreeze", "--unfreeze"),
572
626
  ...flag(a, "force", "--force"),
573
627
  ];
574
- argv.push(...note(a));
575
- if (a.revision !== undefined && a.revision !== null) {
576
- if (typeof a.revision !== "number" || !Number.isInteger(a.revision) || a.revision < 1) {
577
- throw new BadArguments("`revision` must be a whole number from 1");
578
- }
579
- argv.push("--revision", String(a.revision));
580
- }
628
+ argv.push(...note(a), ...revision(a));
581
629
  return { argv, inputLogic: logic(a, "inputLogic"), outputLogic: logic(a, "outputLogic") };
582
630
  },
583
631
  },
@@ -585,19 +633,27 @@ const SCENARIO_TOOLS = [
585
633
  name: "scenario_undo",
586
634
  changes: true,
587
635
  service: true,
588
- description: "Take back a hand edit of a calculated scenario: its step goes back as it was before that edit. Without `edit`, " +
589
- "the newest edit that can be undone (of `step`, when given); calling it again goes one further back. The service " +
590
- "checks the undo like a save and refuses it when a later step or the final answer would break — `force` with a " +
591
- "`note` undoes it anyway. To redo, undo the undo: pass its sedit_ id as `edit` (the answer names it).",
636
+ description: "Take back a hand edit of a calculated scenario: its step (or a parameter's kind) goes back as it was before that " +
637
+ "edit. Without `edit`, the newest edit that can be undone (of `step` or `param`, when given); calling it again goes " +
638
+ "one further back. The service checks a step's undo like a save and refuses it when a later step or the final " +
639
+ "answer would break — `force` with a `note` undoes it anyway. To redo, undo the undo: pass its id as `edit` (the " +
640
+ "answer names it).",
592
641
  inputSchema: {
593
642
  type: "object",
594
643
  properties: {
595
644
  target: TARGET,
596
- edit: { type: "string", description: "The sedit_ id to undo, as scenario_edits lists it. Not with `step`." },
645
+ edit: {
646
+ type: "string",
647
+ description: "The sedit_ or pedit_ id to undo, as scenario_edits lists it. Not with `step` or `param`.",
648
+ },
597
649
  step: {
598
650
  ...STEP,
599
651
  description: "Undo the newest edit of this step (its stepIndex, as scenario_show prints it). Not with `edit`.",
600
652
  },
653
+ param: {
654
+ ...PARAM,
655
+ description: `${PARAM.description} Undo the newest kind change of this parameter. Not with \`edit\` or \`step\`.`,
656
+ },
601
657
  force: {
602
658
  type: "boolean",
603
659
  description: "Undo although the check refused it (a later step or the answer would break). Needs `note`.",
@@ -609,15 +665,21 @@ const SCENARIO_TOOLS = [
609
665
  },
610
666
  plan: (a) => {
611
667
  const argv = ["scenario", "undo", target(a)];
668
+ const name = param(a);
669
+ if (name !== undefined && (given(a.edit) || given(a.step))) {
670
+ throw new BadArguments("`edit`, `step` and `param` each pick what to undo; give one of them");
671
+ }
612
672
  if (a.edit !== undefined && a.edit !== null) {
613
- if (typeof a.edit !== "string" || !/^sedit_[\w-]+$/.test(a.edit)) {
614
- throw new BadArguments("`edit` must be a sedit_… id");
673
+ if (typeof a.edit !== "string" || !/^(sedit|pedit)_[\w-]+$/.test(a.edit)) {
674
+ throw new BadArguments("`edit` must be a sedit_… or pedit_… id");
615
675
  }
616
676
  if (a.step !== undefined && a.step !== null) {
617
677
  throw new BadArguments("`edit` names one entry and `step` picks one; give one of them");
618
678
  }
619
679
  argv.push("--edit", a.edit);
620
680
  }
681
+ if (name !== undefined)
682
+ argv.push("--param", name);
621
683
  argv.push(...step(a, false), ...flag(a, "force", "--force"), ...note(a));
622
684
  return { argv };
623
685
  },
package/dist/bin/bir.d.ts CHANGED
@@ -15,7 +15,8 @@
15
15
  *
16
16
  * bir scenario list | show <runId|scnId> [--step <n>] | calc <runId|scnId> [--force [--discard-edits]]
17
17
  * bir scenario check|edit <runId|scnId> --step <n> --input-logic <file|-> … ← fix one step
18
- * bir scenario edits | undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
18
+ * bir scenario edit <runId|scnId> --param <name> --kind target|setting … ← what a prompt must name
19
+ * bir scenario edits | undo <runId|scnId> [--edit <id> | --step <n> | --param <name>] [--force --note "why"]
19
20
  * bir scenario editing on|off|status ← may this project's `bir` MCP server change a scenario?
20
21
  * bir scenario replay <scnId> --prompt "…" [--dry] | bir replay status|on|off|allow …
21
22
  * bir investigate [<id>] | list | executions
package/dist/bin/bir.js CHANGED
@@ -15,7 +15,8 @@
15
15
  *
16
16
  * bir scenario list | show <runId|scnId> [--step <n>] | calc <runId|scnId> [--force [--discard-edits]]
17
17
  * bir scenario check|edit <runId|scnId> --step <n> --input-logic <file|-> … ← fix one step
18
- * bir scenario edits | undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
18
+ * bir scenario edit <runId|scnId> --param <name> --kind target|setting … ← what a prompt must name
19
+ * bir scenario edits | undo <runId|scnId> [--edit <id> | --step <n> | --param <name>] [--force --note "why"]
19
20
  * bir scenario editing on|off|status ← may this project's `bir` MCP server change a scenario?
20
21
  * bir scenario replay <scnId> --prompt "…" [--dry] | bir replay status|on|off|allow …
21
22
  * bir investigate [<id>] | list | executions
@@ -109,10 +110,14 @@ Commands:
109
110
  scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
110
111
  [--freeze | --unfreeze] [--note "why"] [--force --note "why"] [--revision <n>]
111
112
  the same check, and save the change if it passes
113
+ scenario edit <runId|scnId> --param <name> --kind target|setting [--note "why"]
114
+ a target must be named by the prompt or the plan is
115
+ declined; a setting uses its recorded value when the
116
+ prompt names none (needs --note)
112
117
  scenario edits <runId|scnId> a scenario's hand edits, newest first
113
- scenario undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
114
- take back the newest edit (of step n); run it again to go
115
- one further back; undo an undo's sedit_ id to redo
118
+ scenario undo <runId|scnId> [--edit <id> | --step <n> | --param <name>] [--force --note "why"]
119
+ take back the newest edit (of step n, or parameter name);
120
+ run it again to go one further back; undo an undo's id to redo
116
121
  scenario editing on|off|status whether this project's bir MCP server offers the tools
117
122
  that change a scenario (restart Claude Code after)
118
123
  scenario replay <scnId> --prompt "…" [--dry]
@@ -156,7 +161,10 @@ Options:
156
161
  --unfreeze scenario check|edit: ask to remove that mark; the check decides
157
162
  --note <text> scenario edit|undo: why, kept in the history (required with --force)
158
163
  --revision <n> scenario edit|undo: only if the plan is still at this chainRevision
159
- --edit <id> scenario undo: the sedit_ id to undo (default: the newest edit that can be)
164
+ --edit <id> scenario undo: the sedit_ or pedit_ id to undo (default: the newest that can be)
165
+ --param <name> scenario edit: the parameter whose kind changes · scenario undo: its newest change
166
+ --kind <kind> scenario edit --param: target (a prompt must name it) or setting (its
167
+ recorded value is used when a prompt names none; needs --note)
160
168
  --discard-edits scenario calc --force: recalculate a plan with hand edits, replacing them
161
169
  --no-browser login: print the link and code, open nothing (SSH, headless)
162
170
  --token <value> login: redeem a one-time setup token from the console (no browser)
@@ -355,6 +363,18 @@ function parseArgs(argv) {
355
363
  case "--discard-edits":
356
364
  args.discardEdits = true;
357
365
  break;
366
+ case "--param":
367
+ // A value flag, like --note: the name is whatever follows, and
368
+ // scenario-edit.ts says what is wrong with it in words.
369
+ args.param = argv[++i];
370
+ if (!args.param)
371
+ usage(2);
372
+ break;
373
+ case "--kind":
374
+ args.kind = argv[++i];
375
+ if (!args.kind)
376
+ usage(2);
377
+ break;
358
378
  case "--here":
359
379
  args.here = true;
360
380
  break;
@@ -3374,6 +3394,8 @@ async function scenarioCommand(args) {
3374
3394
  edit: args.edit,
3375
3395
  revision: args.revision,
3376
3396
  discardEdits: args.discardEdits,
3397
+ param: args.param,
3398
+ kind: args.kind,
3377
3399
  }, {
3378
3400
  cwd: process.cwd(),
3379
3401
  out,
@@ -191,8 +191,8 @@ export const DECLINES = {
191
191
  fix: "enable or recalculate that sub-task, or recalculate this scenario.",
192
192
  },
193
193
  missing_target: {
194
- cause: "the prompt names a target the runner could not find where the recording had it.",
195
- fix: "run from the recording's directory, or name the target as the recording did.",
194
+ cause: "the plan needs a target — a value a prompt must name, such as a file, a table or a region — and the prompt did not name it. A replay never takes a target from the recording.",
195
+ fix: 'name it in the prompt. If the value is the same every time — a database, a host, a working folder — it is a setting, not a target: `bir scenario edit <scnId> --param <name> --kind setting --note "why"` lets the runner use its recorded value when a prompt names none (`bir scenario show <scnId>` lists the parameters and their kinds).',
196
196
  },
197
197
  no_derive_key: {
198
198
  cause: "the scenario has parameters and the runner had no way to derive them: no service session and no ANTHROPIC_API_KEY.",
@@ -203,6 +203,22 @@ export const DECLINES = {
203
203
  fix: "recalculate the scenario.",
204
204
  },
205
205
  };
206
+ /**
207
+ * The `missing_target` fix with what this turn knows put in: the scenario, and
208
+ * the target the gate named (`target database not found`). The code alone once
209
+ * sent a reader after a lookup that failed, when the prompt had simply not
210
+ * named the value.
211
+ */
212
+ function missingTargetFix(fix, turn) {
213
+ let text = fix;
214
+ const scenarioId = turn.matched?.scenarioId ?? turn.plan?.scenarioId;
215
+ if (scenarioId)
216
+ text = text.replaceAll("<scnId>", scenarioId);
217
+ const name = /\btarget (\S+) not found/.exec(turn.decision?.why ?? "")?.[1];
218
+ if (name)
219
+ text = text.replaceAll("<name>", name);
220
+ return text;
221
+ }
206
222
  export const LOW_SAVING_PCT = 0.3;
207
223
  const usd = (v) => `$${v.toFixed(4)}`;
208
224
  const pct = (v) => `${(v * 100).toFixed(1)}%`;
@@ -225,7 +241,7 @@ export function localFindings(turn) {
225
241
  severity: code === "not_ready" && !turn.matched.scenarioId ? "info" : "problem",
226
242
  title: `Matched ${turn.matched.runId} but declined to steer: ${turn.decision.why ?? code}.`,
227
243
  cause: known?.cause ?? "the runner's log line carries the reason text.",
228
- fix: known?.fix ??
244
+ fix: (known && code === "missing_target" ? missingTargetFix(known.fix, turn) : known?.fix) ??
229
245
  (turn.matched.scenarioId
230
246
  ? "read the `replay.decision` line above."
231
247
  : "the matched recording has no scenario yet — calculate it, or let the service pick it up after enough hits."),
@@ -6,8 +6,9 @@
6
6
  * bir scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
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
+ * bir scenario edit <runId|scnId> --param <name> --kind target|setting [--note "why"] [--revision <n>]
9
10
  * bir scenario edits <runId|scnId>
10
- * bir scenario undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
11
+ * bir scenario undo <runId|scnId> [--edit <sedit_id|pedit_id> | --step <n> | --param <name>] [--force --note "why"]
11
12
  * bir scenario calc <runId|scnId> [--force [--discard-edits]]
12
13
  * bir scenario editing on|off|status
13
14
  *
@@ -23,6 +24,12 @@
23
24
  * of quotes, braces and `$`, and every shell mangles a different subset of
24
25
  * them; a file, or stdin with `-`, arrives byte for byte.
25
26
  *
27
+ * A parameter's kind (`--param <name> --kind target|setting`, findoffer.md in
28
+ * the BaseIn repository) is the one change here that runs no code: a target
29
+ * is a value a prompt must name, and the plan is declined without it; a
30
+ * setting takes its recorded value when a prompt names none. Making one a
31
+ * setting needs a note, because it is exactly what a target forbids.
32
+ *
26
33
  * Everything the commands touch is injected ({@link ScenarioEditDeps}), as in
27
34
  * `investigate.ts`, so all of it is tested in-process against a fake service.
28
35
  * The exception is `editing`, which writes the install sidecar — tests point
@@ -107,6 +114,30 @@ export interface EditEntry {
107
114
  /** A recalculation replaced the chain; the entry is history now. */
108
115
  replacedAt: string | null;
109
116
  }
117
+ /** A parameter's kind: a prompt must name a target; a setting takes its recorded value when none does. */
118
+ export type ParamKind = "target" | "setting";
119
+ /** A parameter's kind changed by hand, or such a change undone: the service's `paramEdits` (findoffer.md). */
120
+ export interface ParamEditEntry {
121
+ id: string;
122
+ scenarioId: string;
123
+ param: string;
124
+ kind: "edit" | "revert";
125
+ revertsEditId: string | null;
126
+ note: string | null;
127
+ chainRevisionBefore: number;
128
+ chainRevisionAfter: number;
129
+ /** `null` is no kind, which reads as a target. */
130
+ before: {
131
+ kind: ParamKind | null;
132
+ };
133
+ after: {
134
+ kind: ParamKind | null;
135
+ };
136
+ createdAt: string;
137
+ revertedAt: string | null;
138
+ /** A recalculation dropped the parameter; the entry is history now. */
139
+ replacedAt: string | null;
140
+ }
110
141
  export type ServiceReply = {
111
142
  status: number;
112
143
  body: unknown;
@@ -135,11 +166,15 @@ export interface ScenarioEditArgs {
135
166
  freeze: boolean;
136
167
  unfreeze: boolean;
137
168
  note?: string;
138
- /** `undo --edit`: the `sedit_` id to put back. `undo --step` is {@link step}. */
169
+ /** `undo --edit`: the `sedit_` (or `pedit_`) id to put back. `undo --step` is {@link step}. */
139
170
  edit?: string;
140
171
  /** `--revision`: save only if the plan is still at this `chainRevision`. */
141
172
  revision?: number;
142
173
  discardEdits: boolean;
174
+ /** `edit --param <name>`: the parameter whose kind changes; `undo --param`: its newest change. */
175
+ param?: string;
176
+ /** `edit --kind target|setting`, with `--param`. Checked here, so any word reaches the usage line. */
177
+ kind?: string;
143
178
  }
144
179
  /** The `bir scenario` words this module answers; `list` and `replay` stay in bir.ts. */
145
180
  export declare const SCENARIO_EDIT_SUBCOMMANDS: ReadonlySet<string>;
@@ -202,5 +237,7 @@ export declare function around(text: string, at: number): string;
202
237
  * question, not this list's: it answers `not_latest_edit` when it is not.
203
238
  */
204
239
  export declare function newestUndoable(edits: EditEntry[], step?: number): EditEntry | undefined;
240
+ /** {@link newestUndoable} for kind changes: the newest that stands, of `param` when it is given. */
241
+ export declare function newestUndoableParam(paramEdits: ParamEditEntry[], param?: string): ParamEditEntry | undefined;
205
242
  export {};
206
243
  //# sourceMappingURL=scenario-edit.d.ts.map
@@ -6,8 +6,9 @@
6
6
  * bir scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
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
+ * bir scenario edit <runId|scnId> --param <name> --kind target|setting [--note "why"] [--revision <n>]
9
10
  * bir scenario edits <runId|scnId>
10
- * bir scenario undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"]
11
+ * bir scenario undo <runId|scnId> [--edit <sedit_id|pedit_id> | --step <n> | --param <name>] [--force --note "why"]
11
12
  * bir scenario calc <runId|scnId> [--force [--discard-edits]]
12
13
  * bir scenario editing on|off|status
13
14
  *
@@ -23,6 +24,12 @@
23
24
  * of quotes, braces and `$`, and every shell mangles a different subset of
24
25
  * them; a file, or stdin with `-`, arrives byte for byte.
25
26
  *
27
+ * A parameter's kind (`--param <name> --kind target|setting`, findoffer.md in
28
+ * the BaseIn repository) is the one change here that runs no code: a target
29
+ * is a value a prompt must name, and the plan is declined without it; a
30
+ * setting takes its recorded value when a prompt names none. Making one a
31
+ * setting needs a note, because it is exactly what a target forbids.
32
+ *
26
33
  * Everything the commands touch is injected ({@link ScenarioEditDeps}), as in
27
34
  * `investigate.ts`, so all of it is tested in-process against a fake service.
28
35
  * The exception is `editing`, which writes the install sidecar — tests point
@@ -56,9 +63,10 @@ const USAGE = {
56
63
  show: "usage: bir scenario show <runId|scnId> [--step <n>]",
57
64
  check: "usage: bir scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze] [--json]",
58
65
  edit: "usage: bir scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]\n" +
59
- ' [--freeze | --unfreeze] [--note "why"] [--force --note "why"] [--revision <n>] [--json]',
66
+ ' [--freeze | --unfreeze] [--note "why"] [--force --note "why"] [--revision <n>] [--json]\n' +
67
+ ' bir scenario edit <runId|scnId> --param <name> --kind target|setting [--note "why"] [--revision <n>] [--json]',
60
68
  edits: "usage: bir scenario edits <runId|scnId> [--json]",
61
- undo: 'usage: bir scenario undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "why"] [--json]',
69
+ undo: 'usage: bir scenario undo <runId|scnId> [--edit <sedit_id|pedit_id> | --step <n> | --param <name>] [--force --note "why"] [--json]',
62
70
  calc: "usage: bir scenario calc <runId|scnId> [--force [--discard-edits]]",
63
71
  editing: "usage: bir scenario editing on|off|status",
64
72
  };
@@ -141,8 +149,16 @@ async function checkOrEdit(args, deps, save) {
141
149
  const target = args.positionals[1];
142
150
  if (!target)
143
151
  return usageError(deps, sub);
152
+ if (args.param !== undefined || args.kind !== undefined) {
153
+ if (!save) {
154
+ return usageError(deps, sub, "A parameter's kind is not checked: no code runs. Save it with `bir scenario edit <id> --param <name> --kind target|setting`.");
155
+ }
156
+ return editParam(target, args, deps);
157
+ }
144
158
  if (args.step === undefined) {
145
- return usageError(deps, sub, "Which step? Give --step <n>, the stepIndex `bir scenario show` prints.");
159
+ return usageError(deps, sub, save
160
+ ? "Which step? Give --step <n>, the stepIndex `bir scenario show` prints — or --param <name> --kind target|setting to change a parameter."
161
+ : "Which step? Give --step <n>, the stepIndex `bir scenario show` prints.");
146
162
  }
147
163
  if (!save && (args.freeze || args.force)) {
148
164
  return usageError(deps, sub, "`check` never saves, so --freeze and --force belong to `bir scenario edit`.");
@@ -254,6 +270,90 @@ async function checkOrEdit(args, deps, save) {
254
270
  }
255
271
  return 0;
256
272
  }
273
+ /** A kind in the words the history uses; no kind reads as a target, as it does in the gate. */
274
+ const kindWord = (kind) => (kind ? kind : "no kind (a target)");
275
+ /** What the new kind means for the next prompt, in one line. */
276
+ function kindMeaning(name, kind, sample) {
277
+ if (kind === "setting") {
278
+ const text = JSON.stringify(sample);
279
+ const shown = text === undefined ? "" : ` (${text.length > 60 ? `${text.slice(0, 59)}…` : text})`;
280
+ return `When a prompt names no ${name}, the runner now uses its recorded value${shown}; a prompt that names one still wins.`;
281
+ }
282
+ return `A prompt must now name ${name}, or the runner declines the plan (missing_target).`;
283
+ }
284
+ /**
285
+ * `edit --param <name> --kind target|setting` (findoffer.md). Nothing runs, so
286
+ * there is no check to print: the service answers with the change, and this
287
+ * says what it means for the next prompt. Every flag that belongs to a step is
288
+ * refused before any request, and so is a setting without a note — the one
289
+ * direction that lets a replay use a recorded value.
290
+ */
291
+ async function editParam(target, args, deps) {
292
+ const { out } = deps;
293
+ const name = args.param;
294
+ if (name === undefined || name === "") {
295
+ return usageError(deps, "edit", "--kind needs --param <name>: the parameter to change, as `bir scenario show` lists it.");
296
+ }
297
+ if (args.step !== undefined) {
298
+ return usageError(deps, "edit", "--param changes a parameter and --step a step; give one of them.");
299
+ }
300
+ if (args.inputLogic || args.outputLogic || args.freeze || args.unfreeze || args.force) {
301
+ return usageError(deps, "edit", "--param changes only a parameter's kind; --input-logic, --output-logic, --freeze, --unfreeze and --force belong to a step (--step).");
302
+ }
303
+ if (args.kind !== "target" && args.kind !== "setting") {
304
+ return usageError(deps, "edit", args.kind === undefined ? "Which kind? Give --kind setting or --kind target." : `--kind is target or setting; got ${args.kind}.`);
305
+ }
306
+ const kind = args.kind;
307
+ const note = args.note?.trim() ? args.note : undefined;
308
+ if (kind === "setting" && !note) {
309
+ return usageError(deps, "edit", '--kind setting needs --note "why" (note_required): a setting lets the runner use the recorded value when a prompt names none, and the history keeps why.');
310
+ }
311
+ const resolved = await resolveTarget(target, args, deps);
312
+ if ("code" in resolved)
313
+ return resolved.code;
314
+ const body = { kind };
315
+ if (note)
316
+ body.note = note;
317
+ if (args.revision !== undefined)
318
+ body.expectedRevision = args.revision;
319
+ const reply = await deps.service("PATCH", `/scenarios/${seg(resolved.id)}/params/${seg(name)}`, body);
320
+ if ("error" in reply || reply.status !== 200)
321
+ return explainParam(reply, args, deps, target, "Not saved");
322
+ if (args.json) {
323
+ out(jsonText(reply.body));
324
+ return 0;
325
+ }
326
+ const answer = reply.body;
327
+ const edit = answer?.edit;
328
+ const revision = answer?.chainRevision ?? edit?.chainRevisionAfter;
329
+ out(`Parameter ${name}: ${kindWord(edit?.before.kind)} → ${kindWord(edit?.after.kind ?? kind)} (revision ${revision ?? "?"}).` +
330
+ (edit?.id ? ` Undo: bir scenario undo ${target} --edit ${edit.id}` : ""));
331
+ out(` ${kindMeaning(name, edit?.after.kind ?? kind, answer?.parameter?.sampleValue)}`);
332
+ return 0;
333
+ }
334
+ /**
335
+ * The answers a kind change has words of its own for; everything else as
336
+ * {@link explain} says it. A 404 here is most often a name that is not the
337
+ * scenario's, so it says where the names are.
338
+ */
339
+ function explainParam(reply, args, deps, target, verb) {
340
+ if (!("error" in reply) && !args.json) {
341
+ const code = reply.body?.error;
342
+ if (reply.status === 404) {
343
+ deps.out(`${verb} (not_found): ${target} has no parameter ${args.param}, or it is not yours. \`bir scenario show ${target}\` lists its parameters.`);
344
+ return 1;
345
+ }
346
+ if (code === "nothing_to_change") {
347
+ deps.out(`Nothing to change: ${args.param} is already a ${args.kind}.`);
348
+ return 1;
349
+ }
350
+ if (code === "note_required") {
351
+ deps.out(`${verb} (note_required): making a parameter a setting needs --note "why".`);
352
+ return 1;
353
+ }
354
+ }
355
+ return explain(reply, args, deps, { verb, target, param: args.param });
356
+ }
257
357
  /** What `check` printed, turned into the `edit` that would save it. */
258
358
  function editCommand(target, args) {
259
359
  const parts = ["bir scenario edit", target, "--step", String(args.step)];
@@ -495,12 +595,19 @@ function refusal(out, report, opening, hint) {
495
595
  out(hint);
496
596
  }
497
597
  // ── edits / undo ──────────────────────────────────────────────────────────────
598
+ /** The service's history: step edits, and — from a service that has them — kind changes. */
599
+ function historyOf(body) {
600
+ const b = body;
601
+ return {
602
+ edits: Array.isArray(b?.edits) ? b.edits : [],
603
+ paramEdits: Array.isArray(b?.paramEdits) ? b.paramEdits : [],
604
+ };
605
+ }
498
606
  async function fetchEdits(scenarioId, deps) {
499
607
  const reply = await readWithin(deps.service("GET", `/scenarios/${seg(scenarioId)}/edits`));
500
608
  if ("error" in reply || reply.status !== 200)
501
609
  return { reply };
502
- const edits = reply.body?.edits;
503
- return { edits: Array.isArray(edits) ? edits : [] };
610
+ return historyOf(reply.body);
504
611
  }
505
612
  async function listEdits(args, deps) {
506
613
  const { out } = deps;
@@ -521,21 +628,46 @@ async function listEdits(args, deps) {
521
628
  out(jsonText(reply.body));
522
629
  return 0;
523
630
  }
524
- const edits = reply.body?.edits ?? [];
525
- if (edits.length === 0) {
631
+ const { edits, paramEdits } = historyOf(reply.body);
632
+ if (edits.length === 0 && paramEdits.length === 0) {
526
633
  out(`No hand edits of ${target}.`);
527
634
  return 0;
528
635
  }
529
636
  // Ids are printed whole: they are what `undo --edit` takes. The detail line
530
637
  // is indented rather than aligned under the date, which a 42-character id
531
- // would push past most terminals' width.
532
- for (const e of edits) {
533
- const note = e.note ? ` "${e.note.replace(/\s+/g, " ")}"` : "";
534
- out(`${e.id} ${when(e.createdAt)} step ${e.stepIndex} ${String(e.kind).padEnd(7)} revision ${e.chainRevisionBefore} → ${e.chainRevisionAfter}${note}`);
535
- out(` ${editSummary(e)}`);
638
+ // would push past most terminals' width. One list, newest first: a step's
639
+ // edits and a parameter's kind changes are walked back by the same `undo`.
640
+ const noteOf = (note) => (note ? ` "${note.replace(/\s+/g, " ")}"` : "");
641
+ const rows = [
642
+ ...edits.map((e) => ({
643
+ at: e.createdAt,
644
+ line: `${e.id} ${when(e.createdAt)} step ${e.stepIndex} ${String(e.kind).padEnd(7)} revision ${e.chainRevisionBefore} → ${e.chainRevisionAfter}${noteOf(e.note)}`,
645
+ detail: editSummary(e),
646
+ })),
647
+ ...paramEdits.map((p) => ({
648
+ at: p.createdAt,
649
+ line: `${p.id} ${when(p.createdAt)} param ${p.param} ${String(p.kind).padEnd(7)} revision ${p.chainRevisionBefore} → ${p.chainRevisionAfter}${noteOf(p.note)}`,
650
+ detail: paramSummary(p),
651
+ })),
652
+ ].sort((x, y) => (x.at < y.at ? 1 : x.at > y.at ? -1 : 0));
653
+ for (const r of rows) {
654
+ out(r.line);
655
+ out(` ${r.detail}`);
536
656
  }
537
657
  return 0;
538
658
  }
659
+ /** The second line of a kind change in `edits`: the kinds, and what became of it. */
660
+ function paramSummary(p) {
661
+ const parts = [];
662
+ if (p.kind === "revert" && p.revertsEditId)
663
+ parts.push(`undoes ${p.revertsEditId}`);
664
+ parts.push(`${kindWord(p.before?.kind)} → ${kindWord(p.after?.kind)}`);
665
+ if (p.revertedAt)
666
+ parts.push(`undone ${when(p.revertedAt)}`);
667
+ if (p.replacedAt)
668
+ parts.push(`the parameter was dropped by a recalculation ${when(p.replacedAt)}`);
669
+ return parts.join("; ");
670
+ }
539
671
  /** The second line of an `edits` entry: what the edit changed, and what became of it. */
540
672
  function editSummary(e) {
541
673
  const parts = [];
@@ -584,16 +716,29 @@ export function newestUndoable(edits, step) {
584
716
  !e.replacedAt &&
585
717
  (step === undefined || e.stepIndex === step));
586
718
  }
719
+ /** {@link newestUndoable} for kind changes: the newest that stands, of `param` when it is given. */
720
+ export function newestUndoableParam(paramEdits, param) {
721
+ return [...paramEdits]
722
+ .sort((x, y) => (x.createdAt < y.createdAt ? 1 : x.createdAt > y.createdAt ? -1 : 0))
723
+ .find((e) => e.kind !== "revert" && !e.revertedAt && !e.replacedAt && (param === undefined || e.param === param));
724
+ }
725
+ const isParamEditId = (id) => id.startsWith("pedit_");
587
726
  async function undo(args, deps) {
588
727
  const { out } = deps;
589
728
  const target = args.positionals[1];
590
729
  if (!target)
591
730
  return usageError(deps, "undo");
592
- if (args.edit !== undefined && !/^sedit_[\w-]+$/.test(args.edit)) {
593
- return usageError(deps, "undo", `--edit takes a sedit_… id, as \`bir scenario edits\` prints it; got ${args.edit}.`);
731
+ if (args.edit !== undefined && !/^(sedit|pedit)_[\w-]+$/.test(args.edit)) {
732
+ return usageError(deps, "undo", `--edit takes a sedit_… or pedit_… id, as \`bir scenario edits\` prints it; got ${args.edit}.`);
733
+ }
734
+ const picks = [args.edit, args.step, args.param].filter((v) => v !== undefined).length;
735
+ if (picks > 1) {
736
+ return usageError(deps, "undo", args.param === undefined
737
+ ? "--edit names one entry and --step picks one; give one of them."
738
+ : "--edit names one entry, --step picks a step's newest edit and --param a parameter's; give one of them.");
594
739
  }
595
- if (args.edit !== undefined && args.step !== undefined) {
596
- return usageError(deps, "undo", "--edit names one entry and --step picks one; give one of them.");
740
+ if (args.kind !== undefined) {
741
+ return usageError(deps, "undo", "An undo puts back the kind that was there; --kind belongs to `bir scenario edit --param`.");
597
742
  }
598
743
  const note = args.note?.trim() ? args.note : undefined;
599
744
  if (args.force && !note) {
@@ -609,11 +754,17 @@ async function undo(args, deps) {
609
754
  const listed = await fetchEdits(resolved.id, deps);
610
755
  if ("reply" in listed)
611
756
  return explain(listed.reply, args, deps, { verb: "Could not read the history", target });
612
- const pick = newestUndoable(listed.edits, args.step);
757
+ // Undo walks back across both lists (D9): the newest change of either
758
+ // kind goes first, unless --step or --param narrows it to one.
759
+ const stepPick = args.param === undefined ? newestUndoable(listed.edits, args.step) : undefined;
760
+ const paramPick = args.step === undefined ? newestUndoableParam(listed.paramEdits, args.param) : undefined;
761
+ const pick = !stepPick ? paramPick : !paramPick ? stepPick : paramPick.createdAt > stepPick.createdAt ? paramPick : stepPick;
613
762
  if (!pick) {
614
- return localFailure(args, deps, "nothing_to_undo", args.step === undefined
615
- ? `Nothing to undo: ${target} has no hand edit that can be undone.`
616
- : `Nothing to undo: step ${args.step} of ${target} has no hand edit that can be undone.`);
763
+ return localFailure(args, deps, "nothing_to_undo", args.step !== undefined
764
+ ? `Nothing to undo: step ${args.step} of ${target} has no hand edit that can be undone.`
765
+ : args.param !== undefined
766
+ ? `Nothing to undo: parameter ${args.param} of ${target} has no kind change that can be undone.`
767
+ : `Nothing to undo: ${target} has no hand edit that can be undone.`);
617
768
  }
618
769
  editId = pick.id;
619
770
  }
@@ -633,12 +784,15 @@ async function undo(args, deps) {
633
784
  target,
634
785
  editId,
635
786
  notFound: `Not undone (not_found): ${target} has no edit ${editId}, or it is not yours.`,
787
+ param: args.param,
636
788
  });
637
789
  }
638
790
  if (args.json) {
639
791
  out(jsonText(reply.body));
640
792
  return refused ? 1 : 0;
641
793
  }
794
+ if (isParamEditId(editId))
795
+ return paramUndone(reply.body, editId, target, args, deps);
642
796
  if (answer?.report)
643
797
  renderReport(out, answer.report, { brief: true, undo: true });
644
798
  if (refused && answer?.report) {
@@ -666,6 +820,22 @@ async function undo(args, deps) {
666
820
  }
667
821
  return 0;
668
822
  }
823
+ /**
824
+ * A kind change undone. Nothing ran, so there is no report: the parameter's
825
+ * kind as it is now, what that means for the next prompt, and the way on —
826
+ * redo, or one further back (of that parameter, when one was named).
827
+ */
828
+ function paramUndone(answer, editId, target, args, deps) {
829
+ const entry = answer?.edit;
830
+ const revision = answer?.chainRevision ?? entry?.chainRevisionAfter;
831
+ const name = entry?.param ?? answer?.parameter?.name ?? args.param ?? "?";
832
+ deps.out(`Parameter ${name} is back to ${kindWord(entry?.after.kind)}, as it was before ${entry?.revertsEditId ?? editId} (revision ${revision ?? "?"}).` +
833
+ (entry?.id ? ` Redo: bir scenario undo ${target} --edit ${entry.id}` : ""));
834
+ deps.out(` ${kindMeaning(name, entry?.after.kind, answer?.parameter?.sampleValue)}`);
835
+ const further = args.param !== undefined || args.edit !== undefined ? ` --param ${name}` : "";
836
+ deps.out(` One further back: bir scenario undo ${target}${further}`);
837
+ return 0;
838
+ }
669
839
  // ── calc ──────────────────────────────────────────────────────────────────────
670
840
  /**
671
841
  * `calc` lives here because of one answer: a recalculation rebuilds every
@@ -885,6 +1055,8 @@ function explain(reply, args, deps, e) {
885
1055
  const code = typeof body.error === "string" ? body.error : "";
886
1056
  const target = e.target ?? "<id>";
887
1057
  const stepFlag = e.step !== undefined ? ` --step ${e.step}` : "";
1058
+ // A kind change (findoffer.md) is about a parameter, and nothing in it is checked.
1059
+ const aboutParam = e.param !== undefined || (e.editId?.startsWith("pedit_") ?? false);
888
1060
  if (reply.status === 404) {
889
1061
  out(e.notFound ??
890
1062
  `${e.verb} (not_found): no such scenario, step or edit — or it is not yours. Only a scenario's owner can see or change it.`);
@@ -928,7 +1100,9 @@ function explain(reply, args, deps, e) {
928
1100
  return 1;
929
1101
  case "revision_changed": {
930
1102
  const now = typeof body.chainRevision === "number" ? ` — it is at revision ${body.chainRevision} now` : "";
931
- out(`${e.verb} (revision_changed): the plan changed since it was read${now}. Show it again (\`bir scenario show ${target}${stepFlag}\`), check again, then save again.`);
1103
+ out(aboutParam
1104
+ ? `${e.verb} (revision_changed): the plan changed since it was read${now}. Show it again (\`bir scenario show ${target}\`), then try again.`
1105
+ : `${e.verb} (revision_changed): the plan changed since it was read${now}. Show it again (\`bir scenario show ${target}${stepFlag}\`), check again, then save again.`);
932
1106
  return 1;
933
1107
  }
934
1108
  case "not_latest_edit": {
@@ -936,7 +1110,7 @@ function explain(reply, args, deps, e) {
936
1110
  // newer entry puts the step back as this one left it, and then this one
937
1111
  // can be undone. It used to loop, when undo only toggled (C23).
938
1112
  const latest = typeof body.latestEditId === "string" ? body.latestEditId : "<the newer edit>";
939
- out(`${e.verb} (not_latest_edit): the step has changed since that edit — ${latest} changed it after. Undo the newer one first, then this one:`);
1113
+ out(`${e.verb} (not_latest_edit): the ${aboutParam ? "parameter" : "step"} has changed since that edit — ${latest} changed it after. Undo the newer one first, then this one:`);
940
1114
  out(` bir scenario undo ${target} --edit ${latest}`);
941
1115
  out(` bir scenario undo ${target} --edit ${e.editId ?? "<this edit>"}`);
942
1116
  return 1;
@@ -948,7 +1122,9 @@ function explain(reply, args, deps, e) {
948
1122
  return 1;
949
1123
  }
950
1124
  case "edit_replaced":
951
- out(`${e.verb} (edit_replaced): a recalculation replaced the plan after that edit. It stays in \`bir scenario edits\`, but there is nothing left to undo.`);
1125
+ out(aboutParam
1126
+ ? `${e.verb} (edit_replaced): a recalculation dropped that parameter after the change. It stays in \`bir scenario edits\`, but there is nothing left to undo.`
1127
+ : `${e.verb} (edit_replaced): a recalculation replaced the plan after that edit. It stays in \`bir scenario edits\`, but there is nothing left to undo.`);
952
1128
  return 1;
953
1129
  case "rate_limited": {
954
1130
  const ms = typeof body.retryAfterMs === "number" ? body.retryAfterMs : undefined;
@@ -630,8 +630,9 @@ bir scenario show <runId|scnId> [--step <n>]
630
630
  bir scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
631
631
  bir scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
632
632
  [--freeze | --unfreeze] [--note "<why>"] [--force --note "<why>"] [--revision <n>]
633
+ bir scenario edit <runId|scnId> --param <name> --kind target|setting [--note "<why>"] [--revision <n>]
633
634
  bir scenario edits <runId|scnId>
634
- bir scenario undo <runId|scnId> [--edit <sedit_id> | --step <n>] [--force --note "<why>"]
635
+ bir scenario undo <runId|scnId> [--edit <sedit_id|pedit_id> | --step <n> | --param <name>] [--force --note "<why>"]
635
636
  bir scenario calc <runId|scnId> [--force [--discard-edits]]
636
637
  bir scenario editing on|off|status
637
638
  ```
@@ -666,8 +667,9 @@ never take, is never reached in the check. A `fetch` inside a `try` fails in the
666
667
  code in its own process, with the global scope and only a time limit, where `fetch` and `process`
667
668
  do exist. So read every change before it is saved — yours and Claude's.
668
669
 
669
- **Only a step's code and its mark can be changed.** Not the parameters, not their sample values, not
670
- the logic that builds the final answer. Only tool steps: a sub-task call row belongs to the service.
670
+ **Only a step's code and its mark can be changed — and a parameter's kind (below, *A value no prompt names*).**
671
+ Not the parameters' sample values, not the logic that builds the final answer. Only tool steps: a
672
+ sub-task call row belongs to the service.
671
673
 
672
674
  #### The fix, step by step
673
675
 
@@ -1047,8 +1049,8 @@ The tools, as the `bir` server defines them. **Bold** arguments are required.
1047
1049
  | `scenario_edits` | after `bir scenario editing on`, or with `BIR_SCENARIO_TOOLS=read` | **`target`** | `bir scenario edits` |
1048
1050
  | `investigate` | after `bir scenario editing on`, or with `BIR_SCENARIO_TOOLS=read` | `id` — a `run_`, `scn_` or `sexec_` id; without it, the newest turn here | `bir investigate` |
1049
1051
  | `scenario_check` | after `bir scenario editing on` | **`target`**, **`step`**, `inputLogic`, `outputLogic`, `unfreeze` | `bir scenario check` |
1050
- | `scenario_edit` | after `bir scenario editing on` | **`target`**, **`step`**, `inputLogic`, `outputLogic`, `freeze`, `unfreeze`, `force`, `note`, `revision` | `bir scenario edit` |
1051
- | `scenario_undo` | after `bir scenario editing on` | **`target`**, `edit` or `step` (not both), `force`, `note` | `bir scenario undo` |
1052
+ | `scenario_edit` | after `bir scenario editing on` | **`target`**, and **`step`** with `inputLogic`, `outputLogic`, `freeze`, `unfreeze`, `force` — or **`param`** with **`kind`**; `note`, `revision` | `bir scenario edit` |
1053
+ | `scenario_undo` | after `bir scenario editing on` | **`target`**, one of `edit`, `step` or `param`, `force`, `note` | `bir scenario undo` |
1052
1054
 
1053
1055
  - `target` is a `scn_` id, or the `run_` id of a whole-run scenario. `step` is the `stepIndex`.
1054
1056
  - `inputLogic` and `outputLogic` are the code itself, as text. The server writes them to temporary
@@ -1095,6 +1097,59 @@ For the hiring-year case, Claude's report would be: *step 4's code is a copy of
1095
1097
  so it was marked; the new code builds the query from `summary_table`; the check says it reproduces
1096
1098
  the recording, and the step will run by itself.*
1097
1099
 
1100
+ #### A value no prompt names — `--param <name> --kind setting`
1101
+
1102
+ > Built 2026-09-28, **not yet released**: it needs the next service deploy, then the next
1103
+ > `@basein/runner` version. The design is `findoffer.md` and `editSteps.md` ("Parameter kinds") in
1104
+ > the BaseIn repository.
1105
+
1106
+ Every parameter of a plan is one of two kinds:
1107
+
1108
+ | Kind | Means | When the prompt does not name it |
1109
+ |---|---|---|
1110
+ | **target** | what the task acts on — a file, a table, a region | the plan is declined: `missing_target`. A target is never guessed |
1111
+ | **setting** | a value nobody states, the same every time — a database, a host, a working folder | the recorded value is used |
1112
+
1113
+ A prompt that names a value always wins, whatever its kind. The calculation decides the kinds, and
1114
+ it can get one wrong. The hiring-year plan `scn_09dbd4e6` never ran: its `database` was a target,
1115
+ and the MCPMark prompt never names the database — it is simply the one the agent is connected to.
1116
+ `bir investigate` names such a target: `declined … missing_target (database not named in the
1117
+ prompt)`.
1118
+
1119
+ **Do this.** Make it a setting, and say why:
1120
+
1121
+ ```bash
1122
+ bir scenario edit scn_09dbd4e6-… --param database --kind setting --note "The database is the connection; the prompt never names it."
1123
+ ```
1124
+
1125
+ **You should see:**
1126
+
1127
+ ```
1128
+ Parameter database: target → setting (revision 4). Undo: bir scenario undo scn_09dbd4e6-… --edit pedit_…
1129
+ When a prompt names no database, the runner now uses its recorded value ("hys_trial"); a prompt that names one still wins.
1130
+ ```
1131
+
1132
+ - `--kind setting` needs `--note`: it is the one change that lets a replay use a recorded value.
1133
+ `--kind target` does not.
1134
+ - No code runs, so nothing is checked. The revision goes up by one, as for a step edit.
1135
+ - **A recalculation keeps the kind** while the parameter keeps its name, so unlike a step edit it
1136
+ does not hold a recalculation back. When a recalculation drops the parameter, the change is
1137
+ history.
1138
+ - `bir scenario edits` lists it as `param database`, and `bir investigate` reports it on every run
1139
+ (`param_kind_edited`).
1140
+ - `bir scenario undo … --param database` takes it back. A plain `bir scenario undo` takes back the
1141
+ newest change, a step's or a parameter's.
1142
+ - The `scenario_edit` tool takes `param` and `kind` instead of `step`; `scenario_undo` takes `param`.
1143
+
1144
+ **If it goes wrong:**
1145
+
1146
+ | Symptom | Fix |
1147
+ |---|---|
1148
+ | `--kind setting needs --note "why" (note_required)` | Add `--note "why"` |
1149
+ | `Not saved (not_found): … has no parameter …` | Use a name that `bir scenario show <id>` lists under `parameters` |
1150
+ | `Nothing to change: database is already a setting.` | Nothing to do |
1151
+ | `A parameter's kind is not checked` | `check` runs code, and a kind has none. Use `bir scenario edit` |
1152
+
1098
1153
  #### When it does not work
1099
1154
 
1100
1155
  | Symptom | Cause | Fix |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basein/runner",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "A recording MCP proxy: sits between any MCP client and its MCP servers, executes each call on the client's behalf, and records the run as a reusable BaseIn scenario.",
5
5
  "type": "module",
6
6
  "license": "MIT",