@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 +12 -0
- package/dist/bin/bir-scenario.js +81 -19
- package/dist/bin/bir.d.ts +2 -1
- package/dist/bin/bir.js +27 -5
- package/dist/bin/investigate.js +19 -3
- package/dist/bin/scenario-edit.d.ts +39 -2
- package/dist/bin/scenario-edit.js +200 -24
- package/docs/calculatedReplayGuide.md +60 -5
- package/package.json +1 -1
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
|
package/dist/bin/bir-scenario.js
CHANGED
|
@@ -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
|
|
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: {
|
|
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
|
-
|
|
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
|
|
589
|
-
"the newest edit that can be undone (of `step`, when given); calling it again goes
|
|
590
|
-
"checks
|
|
591
|
-
"`note` undoes it anyway. To redo, undo the undo: pass its
|
|
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: {
|
|
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" || !/^
|
|
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
|
|
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
|
|
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 <
|
|
114
|
-
take back the newest edit (of step n
|
|
115
|
-
one further back; undo an undo's
|
|
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
|
|
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,
|
package/dist/bin/investigate.js
CHANGED
|
@@ -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
|
|
195
|
-
fix:
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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 && !/^
|
|
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.
|
|
596
|
-
return usageError(deps, "undo", "
|
|
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
|
-
|
|
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
|
|
615
|
-
? `Nothing to undo: ${target} has no hand edit that can be undone.`
|
|
616
|
-
:
|
|
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(
|
|
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(
|
|
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
|
|
670
|
-
the logic that builds the final answer. Only tool steps: a
|
|
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
|
|
1051
|
-
| `scenario_undo` | after `bir scenario editing on` | **`target`**, `edit
|
|
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
|
+
"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",
|