tldr-experts 0.20.0 → 0.22.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,89 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.0 — 2026-09-14
4
+
5
+ ### Fixed
6
+
7
+ - **A developer may now READ its own tree, `git -C` is refused on purpose, and every cure says
8
+ WHY it is a cure (#287, #294).** Two issues, one edit, because they are the same defect seen
9
+ twice. The developer's git allowance held `add`, `commit`, `rm`, `mv`, `restore` and **no read
10
+ verb at all** — it could write its worktree and never look at it. Measured on a live
11
+ unattended run: a developer ran `git -C <worktree> log --oneline -5`, was refused, was
12
+ re-spawned with the cure, was refused again, and the story died — ~$5 for a command that
13
+ changes nothing. `status`, `log`, `diff` and `show` join the ONE constant the grant, the
14
+ prompt's list and the refusal classifier all read (`build/developerGrants.ts`), so all three
15
+ gained them in the same edit. **`-C <path>` (and `--git-dir`, `--work-tree`) stays ungranted
16
+ as a decision, and the code now says so**: `-C` points git at ANY directory — the epic
17
+ worktree, another story's worktree, the shared checkout — so granting it would turn a read
18
+ verb into a read of trees the story does not own, and a mutating verb into a write there. It
19
+ is its own refusal kind (`elsewhere`) whose cure is "drop `-C <path>` and run git from your
20
+ own worktree", never "ask for `-C`". The second half is the reason these shipped together:
21
+ the three cures we had — "run each command alone" (#278), "run `git log` from your own
22
+ worktree" (#287), "don't append `echo`" (#294) — all said WHAT and none said WHY, and an agent
23
+ that does not know why finds another way to do the same thing. #294 is the proof: three
24
+ consecutive developers on ONE story appended `; echo "EXIT:$?"` to a DoD command, each was
25
+ refused, and the cure kept telling them to separate the commands — but from their side the
26
+ `echo` was not an extra command, it was HOW you read an exit code, so they re-added it. A
27
+ `separator` classification now carries `capturing` (the chain exists only to capture the
28
+ outcome: an `echo` naming `$?`, or a redirect of the command's own output), and that cure, the
29
+ retry prefix and the developer prompt all append the same one clause, from one constant: the
30
+ facilitator re-runs the Definition of Done after you and records each command's exit code, so
31
+ the number the `echo` would print is written down whether you capture it or not. That
32
+ clause says only what this repo measures (`build/dodRunner.ts`) and only what holds for every
33
+ provider: whether an agent's own execution tool hands the exit code back to the model is a
34
+ property of the provider's CLI — measured for Claude Code's `Bash` tool, established nowhere
35
+ for `codex exec` — and the sentence goes into the prompt BOTH providers read, so asserting it
36
+ would have been this very bug one level up, a plausible explanation instead of a true one. The
37
+ developer prompt goldens moved for exactly those two paragraphs.
38
+
39
+ ## 0.21.0 — 2026-09-13
40
+
41
+ ### Fixed
42
+
43
+ - **The raise an operator is told to make now moves a ceiling, and a stage that cannot fund a
44
+ review stops buying one (#244, #289).** One function, one question — how much money is there,
45
+ and who decides when there is not enough — so the two were done together. The model, measured
46
+ case by case by the session running two live unattended runs (their measurement, not this
47
+ file's): the three money figures are three knobs with three different jobs, not three copies of
48
+ one number. The STAGE's `budget_usd` decides the price scale and therefore every per-story
49
+ developer ceiling and the reviewer's; the PHASE ceiling only takes part in the economy refusal
50
+ ("remaining work > what is left") and caps no spawn at all; `per_agent_max_usd` only trims a
51
+ ceiling from above. Their two controls: raising only the stage figure, 16.20 → 60, moved a
52
+ developer ceiling 5.97 → 22.11 on the next spawn, and raising `per_agent_max_usd` and the phase
53
+ ceiling *without* touching the stage moved the ceiling by nothing. So `tldrx budget raise
54
+ <phase> <usd>` — the command every refusal names — moved the one knob that caps nothing a
55
+ sub-agent is dispatched under: the operator raised, re-ran, died on the identical cap, and each
56
+ `reject --and-continue` bought exactly one more turn. It now takes `--stage <id>`, which adds
57
+ the same amount to that stage's own `budget_usd` in `run.yml` (additively, like the phase
58
+ ceiling; recorded on `budget.raised` as `stage`/`stage_budget_before`/`stage_budget_after`, and
59
+ an unknown stage id is a usage refusal that writes neither file), and a raise that names no
60
+ stage now SAYS in its own output that no spawn ceiling moved and names the flag that would
61
+ move one.
62
+ - **A reviewer the stage cannot fund is not spawned, and the refusal is not recorded as a verdict
63
+ (#289).** Measured on a live unattended run (`--budget 60`, `--gates none --questions none
64
+ --ship merge`): story S1's developer went DoD 3/3 green at $10.64, the reviewer right after it
65
+ was handed **$0.43**, died with `Reached maximum budget ($0.43)` before reading a line of the
66
+ diff, and the `verdict: error` that recorded its death parked S1 at `review` with both
67
+ dependent stories blocked behind it — the loop stopped after $17.23 of a $60 run.
68
+ `REVIEWER_FLOOR_USD` ($1.00) already existed and did not save it, because the floor YIELDS to
69
+ the stage remainder (`min(REVIEWER_FLOOR_USD, budget − spent)`): a nearly-exhausted stage buys a
70
+ turn that provably cannot finish. The fix is not a bigger floor. When the stage has less left
71
+ than a review costs the executor refuses BEFORE the spawn — no `agent.spawned`, no task row, not
72
+ a cent — and the story parks at `review` exactly where an unjudged story parks, with its diff
73
+ merged and its one attempt unspent. What it writes down is **not a verdict**: `verdict: "n-a"`
74
+ ("no reviewer ran for this story") beside a `reviewer_unfunded:` reason on the story's
75
+ `task.done`, its review log and the retro — the reviewer-side sibling of the `developerError`
76
+ and `budget_death:` records #271 and #277 shipped, and for the same reason. An agent that never
77
+ started formed no opinion, and `error` already means "it died mid-read"; writing a death nobody
78
+ chose as a review outcome is an audit record lying in the dangerous direction. The reason names
79
+ the lever that actually moves the ceiling — `tldrx budget raise <phase> <usd> --stage <id>`,
80
+ sized to the shortfall — rather than the phase ceiling that caps no spawn. The review stays
81
+ owed, so the next invocation's review-only path picks it up instead of re-running the developer,
82
+ and under an unchanged remainder it refuses again for free. `reviewerCap`'s arithmetic is
83
+ deliberately untouched: it is mirrored on the budget gate's hot path, and the brake's estimate
84
+ must keep reading the schedule the executor would have spent under. A HOST review is never
85
+ refused this way — it costs the stage nothing.
86
+
3
87
  ## 0.20.0 — 2026-09-13
4
88
 
5
89
  ### Added
package/README.md CHANGED
@@ -335,6 +335,8 @@ back on the registry is 0.3.0.
335
335
 
336
336
  | Version | Date | Status | Contains |
337
337
  |---|---|---|---|
338
+ | 0.22.0 | 2026-09-14 | `beta` | A developer may now READ its own tree — `git status`, `log`, `diff` and `show` join the one constant the grant, the developer prompt and the refusal classifier all read; measured on a live unattended run where a developer was refused `git -C <worktree> log` twice and the story died, ~$5 for a command that changes nothing. `-C <path>` (and `--git-dir`, `--work-tree`) stays ungranted as a decision with its own refusal kind, `elsewhere`, because it points git at trees the story does not own. And every refusal cure now says WHY it is a cure: three consecutive developers on one story re-appended `; echo "EXIT:$?"` to a DoD command because the cure said what to drop and never that the facilitator re-runs the Definition of Done itself and records each exit code (#287, #294). Minor release: the developer's git allowance grew and a new refusal kind was added. |
339
+ | 0.21.0 | 2026-09-13 | `beta` | `tldrx budget raise <phase> <usd> --stage <id>` now moves the stage's own `budget_usd` — the figure that actually sets a developer's and a reviewer's spawn ceiling — instead of the phase figure, which caps no spawn at all: measured on a live unattended run where raising the plan's per-story price and the phase ceiling moved a developer's cap by nothing, and only raising the stage figure moved it; a raise naming no `--stage` now says outright that it moved no spawn ceiling. And a reviewer a nearly-exhausted stage cannot fund is refused before it is spawned — the run that surfaced this handed a reviewer **$0.43**, which died before reading a line of diff and recorded `verdict: error`, parking the story with its dependents blocked; the refusal now costs $0 and records `verdict: n-a`, not an error the reviewer never formed (#244, #289). Minor release: a new flag, `--stage`, on `budget raise`. |
338
340
  | 0.20.0 | 2026-09-13 | `beta` | `tldrx seed check <file\|dir>` runs a hand-written seed through the same importer chain `run new --seed` runs, plus the authoring rules four unattended runs paid to learn — bullets under 200 characters, `[src:]` citations that resolve, `dod` lines byte-equal to a declared command with no shell separator, `depends_on` wherever two stories touch one file, a `Recommended:` on every open question — and prints the budget split and per-story caps a run would set, creating no run and spending nothing; `tldrx install --claude` now writes a second managed skill, `tldrx-plan`, that reads `workspace.yml` and the repo tree and turns "I want X" into seeds that pass the same check, naming only declared tools (#291). And a permission refusal over a command the workspace's `commands:` could have granted now names the operator's cure on the first dead developer instead of the fourth, measured on a live unattended run where four developers died and two review rounds ran before the missing command turned out to be one `workspace.yml` line away (#285). Minor release: a new CLI verb, `tldrx seed check`. |
339
341
  | 0.19.0 | 2026-09-13 | `beta` | `tldrx story reopen <id> --as-is --note "…"` settles a story a person finished by hand from its branch as it stands, spawning no developer — measured on a live unattended run where a hand-rebased branch had a green DoD and a clean merge-tree but no way to land, blocked by #271's since-the-spawn rule from a developer with nothing to do (#279). Everything after the missing developer is the ordinary path: the epic merges in first if it moved, the same DoD and reviewer judge the diff, the same merge lands it. The record never lies about who did the work — `story.reopened` carries `reason: as_is` with the actor and note, `task.done` carries `as_is`/`as_is_by`/`as_is_note`, and the review log names the branch and the signer. Two refusals are structural with no flag to skip either: a branch with no commit ahead of its epic, and a red DoD. The signature clears the moment a real developer attempt starts, so a requeued story after a `changes` verdict records a developer, not an as-is. Minor release: a new flag, `--as-is`, on `story reopen`. |
340
342
  | 0.18.3 | 2026-09-13 | `beta` | four fixes to the unattended build path, all measured on live runs: a headless permission refusal is now classified — a chained shell command, a git verb outside the developer's allowance, or neither — so a chained line gets one retry with the cure stated first, an ungranted verb names its granted equivalent, and the developer prompt now lists the git verbs it holds from the one constant the grant is built from, after six refusals in one day left a ledger that recorded the symptom and not the cause; a wave no longer merges a story into its epic from the base it was cut from once a sibling has moved that epic — the story is measured against the epic tip immediately before merge, the epic is merged into the story and its Definition of Done re-run only when the epic actually moved, and a conflict blocks the story naming the files rather than merging half of one; a seed bullet over the claim cap no longer has its citation cut in half, with the clip moving back to the citation's start instead; and a failed commit count is no longer read as zero, so `--discard-pending` can no longer delete a plan just because git could not answer. |
@@ -22,7 +22,7 @@ import {
22
22
  validateRunBudget,
23
23
  wouldExceed,
24
24
  wouldExceedHostTokens
25
- } from "./chunk-j5d0nard.js";
25
+ } from "./chunk-1p5027fn.js";
26
26
  import {
27
27
  EventLog
28
28
  } from "./chunk-6z5rmj0b.js";
@@ -728,6 +728,9 @@ var KINDS = {
728
728
  var MAX_FIXLIST_ROUNDS = STAGE_TUNING_DEFAULTS.fixlistRounds;
729
729
  var FINDING_KINDS = ["correctness", "security", "docs", "style"];
730
730
 
731
+ // src/core/build/refusalKind.ts
732
+ var OUTCOME_ALREADY_REPORTED = "the facilitator re-runs the Definition of Done after you and records each command's exit " + "code, so the number the `echo` would print is measured and written down whether you " + "capture it or not";
733
+
731
734
  // src/core/build/prompts.ts
732
735
  var MAX_TOUCHED_BYTES = 64 * 1024;
733
736
  var REVIEW_SCHEMA = {
@@ -8,7 +8,7 @@ import {
8
8
  spentBasis,
9
9
  tallyOf,
10
10
  validateRunBudget
11
- } from "./chunk-j5d0nard.js";
11
+ } from "./chunk-1p5027fn.js";
12
12
  import {
13
13
  EventLog,
14
14
  OUTCOME_NOT_RECORDED,
@@ -20,14 +20,14 @@ import {
20
20
  runSnapshot,
21
21
  statusWithOutcome,
22
22
  whatIsWaiting
23
- } from "./chunk-53wgfn01.js";
23
+ } from "./chunk-ah1pdax3.js";
24
24
  import {
25
25
  expertsDir,
26
26
  loadExperts,
27
27
  pathsIntersect,
28
28
  readExpertDomain,
29
29
  stackExpertNames
30
- } from "./chunk-j5d0nard.js";
30
+ } from "./chunk-1p5027fn.js";
31
31
  import {
32
32
  isFinished
33
33
  } from "./chunk-6z5rmj0b.js";
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  bar,
4
4
  runSnapshot
5
- } from "./chunk-53wgfn01.js";
6
- import"./chunk-j5d0nard.js";
5
+ } from "./chunk-ah1pdax3.js";
6
+ import"./chunk-1p5027fn.js";
7
7
  import"./chunk-6z5rmj0b.js";
8
8
  import"./chunk-m1s8a6s0.js";
9
9
  import"./chunk-yre2scxn.js";
package/dist/tldrx.js CHANGED
@@ -16639,6 +16639,12 @@ var ENTRIES = [
16639
16639
  runFlag(),
16640
16640
  json("the budget view", "show"),
16641
16641
  { name: "take-from", arg: "<phase>", meaning: "Move the money out of this phase instead of raising the run's total.", sub: "raise" },
16642
+ {
16643
+ name: "stage",
16644
+ arg: "<id>",
16645
+ meaning: "ALSO add the same amount to that stage's own budget_usd in run.yml — the one knob a sub-agent's ceiling is derived from (#244). Without it a raise moves the phase ceiling, which decides only whether a stage may START, and every per-story and reviewer cap stays exactly where it was.",
16646
+ sub: "raise"
16647
+ },
16642
16648
  { name: "note", arg: "<text>", meaning: "Why the ceiling moved. Recorded on the budget.raised event beside the before/after and the actor.", sub: "raise" },
16643
16649
  { name: "note", arg: "<text>", meaning: "Why the grant was recorded. Kept on the budget.granted event beside the amount, the fact and the actor.", sub: "grant" },
16644
16650
  {
@@ -16665,10 +16671,12 @@ var ENTRIES = [
16665
16671
  examples: [
16666
16672
  "tldrx budget show",
16667
16673
  "tldrx budget raise 04-build 25 --take-from 02-how",
16674
+ "tldrx budget raise 04-build 25 --stage build",
16668
16675
  "tldrx budget grant 20 --fact F031 --on-exceed block"
16669
16676
  ],
16670
16677
  exits: [EXIT_OK, EXIT_USAGE, EXIT_GATE_REFUSED, EXIT_NOT_FOUND],
16671
16678
  notes: [
16679
+ "Three knobs, three jobs, and only one of them caps a spawn. The STAGE's `budget_usd` (run.yml) decides the price scale, every per-story developer ceiling and the reviewer's — `--stage` is the sanctioned way to move it (#244). The PHASE ceiling (budget.yml) decides only the economy refusal, `remaining work > what is left`. `per_agent_max_usd` only caps a ceiling from above. Measured on two live unattended runs: the stage figure alone, 16.20 to 60, moved a developer ceiling 5.97 to 22.11 on the next spawn; raising the other two without it moved nothing.",
16672
16680
  "`raise` ADDS. `raise 04-build 25` on a phase already ceilinged at $10 leaves it at $35, not $25 — the amount is a delta, and the run ceiling grows with it unless --take-from moves the money. `budget show` prints the exact command, already sized to the shortfall, when a stage is blocked; pasting that is the way to raise without doing the arithmetic.",
16673
16681
  "`grant` RECORDS, it does not spend: it writes authorized_usd, authorized_by, authorized_at and on_grant_exceed into budget.yml and appends a budget.granted event. No ceiling moves, and a grant the current ceiling already exceeds is still recorded — the money is committed, there is nothing left to refuse. `raise` then measures the ceiling it is about to write against it: a PHASE grant against the phase ceiling, the RUN grant against the run ceiling.",
16674
16682
  "Two exit families, two conditions. A bad amount, an unknown phase, an unknown --on-exceed value, or a --fact naming no live fact is a USAGE error: exit 1, nothing written. A ceiling above the recorded grant under on_grant_exceed: block is a GATE refusal: exit 2, budget.yml byte-identical. Under the default warn the ceiling is written and one sentence names the grant, the fact and the figure."
@@ -26439,6 +26447,9 @@ function dodRefused(result2) {
26439
26447
  }
26440
26448
  var DOD_REFUSAL_FALLBACK = "the gate declined to run it";
26441
26449
  var DEVELOPER_FAILED = "the developer sub-agent failed";
26450
+ function reviewStillOwed(outcome) {
26451
+ return outcome.verdict === "error" || (outcome.reviewerUnfunded ?? null) !== null;
26452
+ }
26442
26453
  var AS_IS_MARK = "the branch was taken AS IT STANDS — no developer was spawned for it";
26443
26454
  var AS_IS_NOT_AHEAD_MARK = "nothing to take as it stands";
26444
26455
  function asIsNotAheadReason(branch, epicBranch, ahead) {
@@ -26466,7 +26477,23 @@ function dodFailureReason(result2, repo) {
26466
26477
  }
26467
26478
 
26468
26479
  // src/core/build/developerGrants.ts
26469
- var DEVELOPER_GIT_VERBS = ["add", "commit", "rm", "mv", "restore"];
26480
+ var DEVELOPER_GIT_VERBS = [
26481
+ "add",
26482
+ "commit",
26483
+ "rm",
26484
+ "mv",
26485
+ "restore",
26486
+ "status",
26487
+ "log",
26488
+ "diff",
26489
+ "show"
26490
+ ];
26491
+ var GIT_ELSEWHERE_OPTIONS = ["-C", "--git-dir", "--work-tree"];
26492
+ function gitElsewhereOption(argv) {
26493
+ const word = argv[1] ?? "";
26494
+ const flag = word.includes("=") ? word.slice(0, word.indexOf("=")) : word;
26495
+ return GIT_ELSEWHERE_OPTIONS.includes(flag) ? flag : null;
26496
+ }
26470
26497
  function developerGitGrants() {
26471
26498
  return DEVELOPER_GIT_VERBS.map((verb) => `Bash(git ${verb} *)`);
26472
26499
  }
@@ -26491,6 +26518,94 @@ function slotForCommand(line) {
26491
26518
  return SUBCOMMAND_RE.test(second) ? `${head} ${second}` : head;
26492
26519
  }
26493
26520
 
26521
+ // src/core/build/refusalKind.ts
26522
+ var MAX_SEPARATOR_RETRIES = 1;
26523
+ function classifyRefusal(command2, declared) {
26524
+ const separator = unquotedShellSeparator(command2);
26525
+ if (separator !== null) {
26526
+ return { kind: "separator", separator, capturing: capturesOutcome(command2, separator) };
26527
+ }
26528
+ const argv = command2.trim().split(/\s+/);
26529
+ if (argv[0] !== "git")
26530
+ return undeclaredKind(command2, declared);
26531
+ const verb = argv[1] ?? "";
26532
+ const elsewhere = gitElsewhereOption(argv);
26533
+ if (elsewhere !== null)
26534
+ return { kind: "elsewhere", option: elsewhere };
26535
+ if (verb === "" || verb.startsWith("-"))
26536
+ return { kind: "unknown" };
26537
+ if (isDeveloperGitVerb(verb))
26538
+ return { kind: "unknown" };
26539
+ return { kind: "verb", verb, equivalent: grantedEquivalent(verb, argv.slice(2)) };
26540
+ }
26541
+ function undeclaredKind(command2, declared) {
26542
+ if (declared === undefined)
26543
+ return { kind: "unknown" };
26544
+ if (grantsCommand(declared, command2))
26545
+ return { kind: "unknown" };
26546
+ const slot = slotForCommand(command2);
26547
+ return slot === "" ? { kind: "unknown" } : { kind: "undeclared", slot };
26548
+ }
26549
+ function grantedEquivalent(verb, args) {
26550
+ if (verb === "checkout") {
26551
+ const switching = args.some((a) => a === "-b" || a === "-B" || a === "--orphan" || a === "-t" || a === "--track");
26552
+ if (switching)
26553
+ return null;
26554
+ const hasTarget = args.some((a) => !a.startsWith("-")) || args.includes("--");
26555
+ return hasTarget ? "git restore <path>" : null;
26556
+ }
26557
+ if (verb === "reset") {
26558
+ const mode = args.some((a) => /^--(hard|soft|mixed|merge|keep)$/.test(a));
26559
+ if (mode)
26560
+ return null;
26561
+ if (args.includes("--"))
26562
+ return "git restore --staged <path>";
26563
+ const [first, second] = args.filter((a) => !a.startsWith("-"));
26564
+ if (first === "HEAD" && second !== undefined)
26565
+ return "git restore --staged <path>";
26566
+ return null;
26567
+ }
26568
+ return null;
26569
+ }
26570
+ var EXIT_CODE_ECHO_RE = /(^|[;&|])\s*echo\b[^;&|]*\$\?/;
26571
+ function capturesOutcome(command2, separator) {
26572
+ if (EXIT_CODE_ECHO_RE.test(command2))
26573
+ return true;
26574
+ return separator === ">" || separator === ">>" || separator === "2>&1";
26575
+ }
26576
+ var OUTCOME_ALREADY_REPORTED = "the facilitator re-runs the Definition of Done after you and records each command's exit " + "code, so the number the `echo` would print is measured and written down whether you " + "capture it or not";
26577
+ function refusalCure(kind) {
26578
+ switch (kind.kind) {
26579
+ case "separator":
26580
+ return "run each command alone — shell separators split a line into subcommands that each need their own grant" + (kind.capturing ? `. The exit code is not lost by dropping it: ${OUTCOME_ALREADY_REPORTED}` : "");
26581
+ case "elsewhere":
26582
+ return `drop \`${kind.option} <path>\` and run git from your own worktree — \`${kind.option}\` ` + "points git at another directory, so it is refused on purpose rather than missing from the " + "allowance: it would reach trees this story does not own. Your working directory already is " + "the worktree, and the read verbs run there";
26583
+ case "verb":
26584
+ return kind.equivalent === null ? `\`git ${kind.verb}\` is not granted` : `\`git ${kind.verb}\` is not granted; use \`${kind.equivalent}\``;
26585
+ case "undeclared":
26586
+ return `nothing in .tldrx/workspace.yml's \`commands:\` grants \`${kind.slot}\` — a developer's ` + "grant is built from the declared commands, so no re-run and no reopen note can make this " + `line runnable. The operator's cure: add a \`commands:\` slot whose value is exactly ` + `\`${kind.slot}\` (a slot grants that string plus any arguments), or a longer prefix of the ` + "refused line if less should be granted";
26587
+ case "unknown":
26588
+ return "";
26589
+ }
26590
+ }
26591
+ function withCure(sentence, command2, declared) {
26592
+ const cure = refusalCure(classifyRefusal(command2, declared));
26593
+ return cure === "" ? sentence : `${sentence}. The cure: ${cure}`;
26594
+ }
26595
+ function separatorCurePrefix(command2) {
26596
+ const separator = unquotedShellSeparator(command2);
26597
+ const capturing = separator !== null && capturesOutcome(command2, separator);
26598
+ return [
26599
+ `Your previous command \`${command2}\` was refused because it chains commands. Run each command alone.`,
26600
+ "The permission layer splits a line at every shell separator (`&&`, `;`, `|`, `>`, `2>&1`, `$()`)",
26601
+ "and each fragment must match its own grant, so a compound line is refused even when every",
26602
+ "command in it is allowed on its own. This is the one retry: a second refusal blocks the story.",
26603
+ ...capturing ? [`The exit code is not lost by dropping it: ${OUTCOME_ALREADY_REPORTED}.`] : [],
26604
+ ""
26605
+ ].join(`
26606
+ `);
26607
+ }
26608
+
26494
26609
  // src/core/build/prompts.ts
26495
26610
  var MAX_TOUCHED_BYTES = 64 * 1024;
26496
26611
  var MAX_TOUCHED_FILES = 24;
@@ -26594,8 +26709,11 @@ function buildDeveloperPrompt(parts) {
26594
26709
  "",
26595
26710
  ...parts.testFast === undefined ? [] : [...testFastRule(parts.testFast.fast, parts.testFast.full), ""],
26596
26711
  `Commit with \`git add\` and \`git commit\`. The git verbs you hold are exactly ${gitVerbList()}.`,
26597
- "`git restore <path>` is how to put a file back (there is no checkout in that list). Nothing",
26598
- "else about git is yours to do.",
26712
+ "`git restore <path>` is how to put a file back (there is no checkout in that list). The read",
26713
+ "verbs are yours so you can look at your own tree before you commit — run them from your working",
26714
+ "directory, which already IS this worktree. Never `-C <path>`: it points git at another directory,",
26715
+ "which is not yours to read or write, so it is refused on purpose. Nothing else about git is",
26716
+ "yours to do.",
26599
26717
  "",
26600
26718
  "## Rules",
26601
26719
  "",
@@ -26608,8 +26726,9 @@ function buildDeveloperPrompt(parts) {
26608
26726
  "- Run each Definition of Done command verbatim and alone: no redirection, pipes or chaining.",
26609
26727
  " Shell separators (`>`, `>>`, `2>&1`, `<`, `|`, `;`, `&&`, `||`, `&`, `$()`) split a line into",
26610
26728
  " subcommands, and each subcommand must match its own grant, so a compound line is refused",
26611
- " even when the script itself is allowed. The facilitator re-runs the Definition of Done",
26612
- " after you anyway.",
26729
+ " even when the script itself is allowed.",
26730
+ `- Do not append \`; echo $?\` or redirect a command's output to a file to capture its outcome.`,
26731
+ ` The exit code is not lost by dropping it: ${OUTCOME_ALREADY_REPORTED}.`,
26613
26732
  "",
26614
26733
  "### Conventions",
26615
26734
  "",
@@ -28704,78 +28823,6 @@ function verdictReviewer(checkPayload, lastSpawn) {
28704
28823
  return lastSpawn;
28705
28824
  }
28706
28825
 
28707
- // src/core/build/refusalKind.ts
28708
- var MAX_SEPARATOR_RETRIES = 1;
28709
- function classifyRefusal(command2, declared) {
28710
- const separator = unquotedShellSeparator(command2);
28711
- if (separator !== null)
28712
- return { kind: "separator", separator };
28713
- const argv = command2.trim().split(/\s+/);
28714
- if (argv[0] !== "git")
28715
- return undeclaredKind(command2, declared);
28716
- const verb = argv[1] ?? "";
28717
- if (verb === "" || verb.startsWith("-"))
28718
- return { kind: "unknown" };
28719
- if (isDeveloperGitVerb(verb))
28720
- return { kind: "unknown" };
28721
- return { kind: "verb", verb, equivalent: grantedEquivalent(verb, argv.slice(2)) };
28722
- }
28723
- function undeclaredKind(command2, declared) {
28724
- if (declared === undefined)
28725
- return { kind: "unknown" };
28726
- if (grantsCommand(declared, command2))
28727
- return { kind: "unknown" };
28728
- const slot = slotForCommand(command2);
28729
- return slot === "" ? { kind: "unknown" } : { kind: "undeclared", slot };
28730
- }
28731
- function grantedEquivalent(verb, args) {
28732
- if (verb === "checkout") {
28733
- const switching = args.some((a) => a === "-b" || a === "-B" || a === "--orphan" || a === "-t" || a === "--track");
28734
- if (switching)
28735
- return null;
28736
- const hasTarget = args.some((a) => !a.startsWith("-")) || args.includes("--");
28737
- return hasTarget ? "git restore <path>" : null;
28738
- }
28739
- if (verb === "reset") {
28740
- const mode = args.some((a) => /^--(hard|soft|mixed|merge|keep)$/.test(a));
28741
- if (mode)
28742
- return null;
28743
- if (args.includes("--"))
28744
- return "git restore --staged <path>";
28745
- const [first2, second] = args.filter((a) => !a.startsWith("-"));
28746
- if (first2 === "HEAD" && second !== undefined)
28747
- return "git restore --staged <path>";
28748
- return null;
28749
- }
28750
- return null;
28751
- }
28752
- function refusalCure(kind) {
28753
- switch (kind.kind) {
28754
- case "separator":
28755
- return "run each command alone — shell separators split a line into subcommands that each need their own grant";
28756
- case "verb":
28757
- return kind.equivalent === null ? `\`git ${kind.verb}\` is not granted` : `\`git ${kind.verb}\` is not granted; use \`${kind.equivalent}\``;
28758
- case "undeclared":
28759
- return `nothing in .tldrx/workspace.yml's \`commands:\` grants \`${kind.slot}\` — a developer's ` + "grant is built from the declared commands, so no re-run and no reopen note can make this " + `line runnable. The operator's cure: add a \`commands:\` slot whose value is exactly ` + `\`${kind.slot}\` (a slot grants that string plus any arguments), or a longer prefix of the ` + "refused line if less should be granted";
28760
- case "unknown":
28761
- return "";
28762
- }
28763
- }
28764
- function withCure(sentence, command2, declared) {
28765
- const cure = refusalCure(classifyRefusal(command2, declared));
28766
- return cure === "" ? sentence : `${sentence}. The cure: ${cure}`;
28767
- }
28768
- function separatorCurePrefix(command2) {
28769
- return [
28770
- `Your previous command \`${command2}\` was refused because it chains commands. Run each command alone.`,
28771
- "The permission layer splits a line at every shell separator (`&&`, `;`, `|`, `>`, `2>&1`, `$()`)",
28772
- "and each fragment must match its own grant, so a compound line is refused even when every",
28773
- "command in it is allowed on its own. This is the one retry: a second refusal blocks the story.",
28774
- ""
28775
- ].join(`
28776
- `);
28777
- }
28778
-
28779
28826
  // src/core/build/review.ts
28780
28827
  var VERDICT_WORDS = ["approve", "fixlist", "changes"];
28781
28828
  var VERDICT_ENUM = VERDICT_WORDS.join("|");
@@ -28893,6 +28940,21 @@ function text2(value) {
28893
28940
  return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
28894
28941
  }
28895
28942
  var REVIEWER_FAILED = "the reviewer sub-agent failed";
28943
+ var REVIEWER_UNFUNDED_MARK = "no reviewer was spawned: the stage cannot fund one";
28944
+ function reviewerUnfundedReason(parts) {
28945
+ return `${REVIEWER_UNFUNDED_MARK} — $${parts.remainingUsd.toFixed(2)} left, and a review ` + `costs at least $${parts.floorUsd.toFixed(2)}. Nothing was spent on a turn that could not ` + `read the diff. Raise the stage's own budget_usd — the phase ceiling caps no spawn: ${parts.fix}`;
28946
+ }
28947
+ function reviewerUnfunded(reason) {
28948
+ return {
28949
+ verdict: "n-a",
28950
+ summary: reason,
28951
+ findings: [],
28952
+ fixlist: [],
28953
+ fixlistProblems: [],
28954
+ formatProblems: [],
28955
+ verdictProblem: null
28956
+ };
28957
+ }
28896
28958
  function reviewerFailed(error) {
28897
28959
  const said2 = (error ?? "").trim();
28898
28960
  return {
@@ -29011,6 +29073,14 @@ function summaryLines(outcome) {
29011
29073
  `> ${outcome.developerError}`
29012
29074
  ];
29013
29075
  }
29076
+ if ((outcome.reviewerUnfunded ?? null) !== null) {
29077
+ return [
29078
+ "**No reviewer was spawned: the stage could not fund one.** This is not a verdict and not",
29079
+ "a failure of the diff — nothing read it, and nothing was spent trying. Why:",
29080
+ "",
29081
+ `> ${outcome.reviewerUnfunded ?? ""}`
29082
+ ];
29083
+ }
29014
29084
  if (outcome.verdict === "error") {
29015
29085
  return [
29016
29086
  "**The reviewer FAILED and returned no verdict.** This is not a request for changes:",
@@ -29374,6 +29444,9 @@ function unmeteredNote(count) {
29374
29444
  function raiseCommand(runId, phaseId, amountUsd) {
29375
29445
  return `tldrx budget raise ${phaseId} ${amountUsd.toFixed(2)} --run ${runId}`;
29376
29446
  }
29447
+ function stageRaiseCommand(runId, phaseId, stageId, amountUsd) {
29448
+ return `${raiseCommand(runId, phaseId, amountUsd)} --stage ${stageId}`;
29449
+ }
29377
29450
  function shortBy(estimate, remaining2) {
29378
29451
  return Math.max(0.01, Math.ceil((estimate - remaining2) * 100) / 100);
29379
29452
  }
@@ -36486,6 +36559,15 @@ function reviewerCap(parts, spentUsd, storyId) {
36486
36559
  const floor = Math.min(REVIEWER_FLOOR_USD2, Math.max(parts.budgetUsd - spentUsd, 0));
36487
36560
  return round25(Math.min(Math.max(derived, floor), parts.maxBudgetUsd));
36488
36561
  }
36562
+ function stageRemainderUsd(parts, spentUsd) {
36563
+ if (!(parts.budgetUsd > 0))
36564
+ return null;
36565
+ return Math.max(round25(parts.budgetUsd - spentUsd), 0);
36566
+ }
36567
+ function reviewerUnderfunded(parts, spentUsd) {
36568
+ const left = stageRemainderUsd(parts, spentUsd);
36569
+ return left !== null && left < REVIEWER_FLOOR_USD2;
36570
+ }
36489
36571
  function priceOf(parts, storyId) {
36490
36572
  if (storyId === undefined)
36491
36573
  return null;
@@ -36686,6 +36768,8 @@ function readReviewLedger(runDir, storyId) {
36686
36768
  };
36687
36769
  }
36688
36770
  function reviewEventErrored2(payload) {
36771
+ if (typeof payload.unfunded_usd === "number")
36772
+ return true;
36689
36773
  if (payload.verdict === "error")
36690
36774
  return true;
36691
36775
  if (payload.verdict !== "changes")
@@ -36821,7 +36905,7 @@ function awaitingReview(runDir, stageId, pending) {
36821
36905
  function resumableReview(runDir, storyId, status, fresh) {
36822
36906
  if (status !== "review" && status !== "in_progress")
36823
36907
  return null;
36824
- if (fresh !== undefined && fresh.verdict !== "error")
36908
+ if (fresh !== undefined && !reviewStillOwed(fresh))
36825
36909
  return null;
36826
36910
  const ledger4 = readReviewLedger(runDir, storyId);
36827
36911
  if (ledger4.erroredWith === null || ledger4.commit === null)
@@ -37194,6 +37278,9 @@ function storyRetroLines(outcome, runId) {
37194
37278
  if (outcome.developerError !== null) {
37195
37279
  lines.push(`- \`${outcome.id}\` — the developer FAILED and produced no work on attempt ` + `${String(outcome.attempts)}: ${oneLine5(outcome.developerError)} ${src}`);
37196
37280
  }
37281
+ if ((outcome.reviewerUnfunded ?? null) !== null) {
37282
+ lines.push(`- \`${outcome.id}\` — no reviewer was spawned on attempt ` + `${String(outcome.attempts)}: ${oneLine5(outcome.reviewerUnfunded ?? "")} ${src}`);
37283
+ }
37197
37284
  if (outcome.verdict === "error") {
37198
37285
  lines.push(`- \`${outcome.id}\` — the reviewer FAILED and returned no verdict on attempt ` + `${String(outcome.attempts)}: ${oneLine5(outcome.reviewSummary)} ${src}`);
37199
37286
  }
@@ -38910,6 +38997,7 @@ class BuildSession {
38910
38997
  outcomes = new Map;
38911
38998
  refusals = new Map;
38912
38999
  capDeaths = new Map;
39000
+ unfundedReviews = new Map;
38913
39001
  asIsSettlements = new Map;
38914
39002
  epics = new EpicState;
38915
39003
  lines = [];
@@ -39253,7 +39341,7 @@ class BuildSession {
39253
39341
  return;
39254
39342
  if (outcome?.status !== "review")
39255
39343
  return;
39256
- if (outcome.verdict === "error")
39344
+ if (reviewStillOwed(outcome))
39257
39345
  return;
39258
39346
  if (outcome.verdict === "fixlist")
39259
39347
  return;
@@ -39313,6 +39401,7 @@ class BuildSession {
39313
39401
  const before = this.statusOf(planned);
39314
39402
  this.refusals.delete(id);
39315
39403
  this.capDeaths.delete(id);
39404
+ this.unfundedReviews.delete(id);
39316
39405
  this.asIsSettlements.set(id, asIs);
39317
39406
  const story = await this.writes.run(() => this.openStory(planned));
39318
39407
  await this.writes.run(() => {
@@ -39386,6 +39475,7 @@ class BuildSession {
39386
39475
  const before = this.statusOf(planned);
39387
39476
  this.refusals.delete(planned.story.id);
39388
39477
  this.capDeaths.delete(planned.story.id);
39478
+ this.unfundedReviews.delete(planned.story.id);
39389
39479
  this.asIsSettlements.delete(planned.story.id);
39390
39480
  const story = await this.writes.run(() => this.openStory(planned, true));
39391
39481
  await this.writes.run(() => {
@@ -39565,6 +39655,21 @@ class BuildSession {
39565
39655
  await this.handOffReview(story, dod, commit2, priorCost, carried, epicBase);
39566
39656
  return "handed-off";
39567
39657
  }
39658
+ if (supplied === undefined && reviewerUnderfunded(this.capParts, this.spent())) {
39659
+ const review2 = this.refuseUnfundedReview(story);
39660
+ await this.settle(story, "review", {
39661
+ dod,
39662
+ commit: commit2,
39663
+ merged: true,
39664
+ carried,
39665
+ epicBase,
39666
+ verdict: "n-a",
39667
+ review: review2,
39668
+ cost: round25(priorCost),
39669
+ reason: review2.summary
39670
+ });
39671
+ return "settled";
39672
+ }
39568
39673
  const outcome = supplied === undefined ? await this.spawnReviewer(story, dod, epicBase) : { review: supplied, cost: 0 };
39569
39674
  const review = outcome.review;
39570
39675
  const cost = round25(priorCost + outcome.cost);
@@ -39769,7 +39874,8 @@ class BuildSession {
39769
39874
  });
39770
39875
  });
39771
39876
  this.noteMerged(story, null);
39772
- this.lines.push(` · ${planned.story.id}: the previous reviewer FAILED (${resume.error}) — ` + `re-running the REVIEW only; \`${resume.commit}\` is already merged into \`${story.epicBranch}\``);
39877
+ const failed3 = resume.error.includes(REVIEWER_UNFUNDED_MARK) ? `the previous review was refused for want of money (${resume.error})` : `the previous reviewer FAILED (${resume.error})`;
39878
+ this.lines.push(` · ${planned.story.id}: ${failed3} — ` + `re-running the REVIEW only; \`${resume.commit}\` is already merged into \`${story.epicBranch}\``);
39773
39879
  this.noteUnrecordedBase(planned.story.id, story.epicBranch, resume.epicBase);
39774
39880
  await this.reviewAndSettle(story, resume.dod, resume.commit, 0, null, resume.epicBase ?? null);
39775
39881
  }
@@ -40107,6 +40213,28 @@ class BuildSession {
40107
40213
  storyTitle: story.planned.story.title
40108
40214
  });
40109
40215
  }
40216
+ refuseUnfundedReview(story) {
40217
+ const id = story.planned.story.id;
40218
+ const left = stageRemainderUsd(this.capParts, this.spent()) ?? 0;
40219
+ const review = reviewerUnfunded(reviewerUnfundedReason({
40220
+ remainingUsd: left,
40221
+ floorUsd: REVIEWER_FLOOR_USD2,
40222
+ fix: `\`${stageRaiseCommand(this.ctx.runId, this.ctx.phaseId, this.ctx.stageId, shortBy(REVIEWER_FLOOR_USD2, left))}\``
40223
+ }));
40224
+ this.unfundedReviews.set(id, review.summary);
40225
+ this.ctx.emit("check.failed", {
40226
+ phase: this.ctx.phaseId,
40227
+ check: "review",
40228
+ story: id,
40229
+ verdict: review.verdict,
40230
+ attempt: story.attempt,
40231
+ unfunded_usd: left,
40232
+ floor_usd: REVIEWER_FLOOR_USD2,
40233
+ detail: review.summary
40234
+ });
40235
+ this.reviewers.set(id, null);
40236
+ return review;
40237
+ }
40110
40238
  async spawnReviewer(story, dod, epicBase) {
40111
40239
  const id = story.planned.story.id;
40112
40240
  let refusal = null;
@@ -40330,6 +40458,7 @@ class BuildSession {
40330
40458
  permissionRefused: this.refusals.get(id) ?? null,
40331
40459
  declaredCommands: this.repoCommands(story.planned.story.repo),
40332
40460
  budgetDeath: this.capDeaths.get(id) ?? null,
40461
+ reviewerUnfunded: this.unfundedReviews.get(id) ?? null,
40333
40462
  asIs: this.asIsSettlements.get(id) ?? null,
40334
40463
  rescued,
40335
40464
  cost_usd: parts.cost
@@ -40362,6 +40491,7 @@ class BuildSession {
40362
40491
  ...parts.epicBase === null || parts.epicBase === undefined || parts.epicBase === "" ? {} : { epic_base: parts.epicBase },
40363
40492
  ...outcome.permissionRefused == null ? {} : { permission_refused: outcome.permissionRefused },
40364
40493
  ...outcome.budgetDeath == null ? {} : { budget_death: outcome.budgetDeath },
40494
+ ...outcome.reviewerUnfunded == null ? {} : { reviewer_unfunded: outcome.reviewerUnfunded },
40365
40495
  ...outcome.asIs == null ? {} : { as_is: true, as_is_by: outcome.asIs.actor, as_is_note: outcome.asIs.note }
40366
40496
  });
40367
40497
  if (pruning && (rescued === null || rescued.worktree === null)) {
@@ -48520,12 +48650,12 @@ function wouldExceedGrant(budget, phaseId, resultingCeilingUsd) {
48520
48650
  }
48521
48651
 
48522
48652
  // src/cli/commands/budget.ts
48523
- var VALUE_FLAGS3 = ["run", "root", "take-from", "note", "fact", "phase", "on-exceed"];
48653
+ var VALUE_FLAGS3 = ["run", "root", "take-from", "note", "fact", "phase", "on-exceed", "stage"];
48524
48654
  var budgetCommand = {
48525
48655
  name: "budget",
48526
48656
  summary: "Show what the run may still spend, raise a phase ceiling, or record what the owner authorized",
48527
48657
  usage: `tldrx budget show [<run>] [--run <id>] [--json] [--root <path>]
48528
- ` + ` tldrx budget raise <phase> <usd> [--run <id>] [--take-from <phase>] [--note <text>] [--root <path>]
48658
+ ` + ` tldrx budget raise <phase> <usd> [--stage <id>] [--run <id>] [--take-from <phase>] [--note <text>] [--root <path>]
48529
48659
  ` + " tldrx budget grant <usd> --fact <F> [--phase <p>] [--on-exceed <warn|block>] [--note <text>] [--run <id>] [--root <path>]",
48530
48660
  implemented: true,
48531
48661
  async run(argv) {
@@ -48546,6 +48676,14 @@ ${budgetCommand.usage}
48546
48676
  }
48547
48677
  };
48548
48678
  var GRANT_ONLY_FLAGS = ["fact", "phase", "on-exceed"];
48679
+ var RAISE_ONLY_FLAGS = ["stage"];
48680
+ function refuseRaiseFlags(args, verb) {
48681
+ for (const flag of RAISE_ONLY_FLAGS) {
48682
+ if (!boolFlag(args, flag))
48683
+ continue;
48684
+ throw new UsageError(`--${flag} is \`budget raise\`'s flag, not \`budget ${verb}\`'s — ` + "raise a stage's own ceiling with `tldrx budget raise <phase> <usd> --stage <id>`");
48685
+ }
48686
+ }
48549
48687
  function refuseGrantFlags(args, verb) {
48550
48688
  for (const flag of GRANT_ONLY_FLAGS) {
48551
48689
  if (!boolFlag(args, flag))
@@ -48557,6 +48695,7 @@ function budgetShow(argv) {
48557
48695
  try {
48558
48696
  const args = parseArgs(argv, VALUE_FLAGS3);
48559
48697
  refuseGrantFlags(args, "show");
48698
+ refuseRaiseFlags(args, "show");
48560
48699
  const resolved = openRun(args.positionals[0] ?? stringFlag(args, "run"), workspaceRootFrom(args));
48561
48700
  if (!isResolved(resolved))
48562
48701
  return resolved.exit;
@@ -48587,6 +48726,9 @@ function budgetRaise(argv) {
48587
48726
  if (!isResolved(resolved))
48588
48727
  return resolved.exit;
48589
48728
  const store = resolved.store;
48729
+ const stageId = stringFlag(args, "stage") ?? null;
48730
+ if (stageId !== null)
48731
+ requireStageBudget(store.run, phaseId, stageId);
48590
48732
  const outcome = raiseBudget(store.budget, {
48591
48733
  phaseId,
48592
48734
  amountUsd,
@@ -48600,6 +48742,19 @@ function budgetRaise(argv) {
48600
48742
  return EXIT_GATE_REFUSED;
48601
48743
  }
48602
48744
  store.mutateBudget(() => outcome.budget);
48745
+ const stageBefore = stageId === null ? null : requireStageBudget(store.run, phaseId, stageId);
48746
+ if (stageId !== null) {
48747
+ store.mutate((run2) => ({
48748
+ ...run2,
48749
+ phases: run2.phases.map((phase) => phase.id !== phaseId ? phase : {
48750
+ ...phase,
48751
+ stages: phase.stages.map((stage2) => stage2.id !== stageId ? stage2 : {
48752
+ ...stage2,
48753
+ budget_usd: round25(stage2.budget_usd + amountUsd)
48754
+ })
48755
+ })
48756
+ }));
48757
+ }
48603
48758
  store.append({
48604
48759
  ts: nowRfc3339(),
48605
48760
  run: store.runId,
@@ -48615,12 +48770,17 @@ function budgetRaise(argv) {
48615
48770
  phase_ceiling_after: outcome.phaseCeilingAfter,
48616
48771
  run_ceiling_before: outcome.runCeilingBefore,
48617
48772
  run_ceiling_after: outcome.runCeilingAfter,
48773
+ ...stageId === null || stageBefore === null ? {} : {
48774
+ stage: stageId,
48775
+ stage_budget_before: stageBefore,
48776
+ stage_budget_after: round25(stageBefore + amountUsd)
48777
+ },
48618
48778
  note: stringFlag(args, "note") ?? ""
48619
48779
  }
48620
48780
  });
48621
48781
  store.save();
48622
48782
  const view = buildBudgetView(store.run, store.budget, store.runDir);
48623
- const lines = [describeRaise(outcome)];
48783
+ const lines = [describeRaise(outcome), ...stageRaiseLines(phaseId, stageId, stageBefore, amountUsd)];
48624
48784
  if (verdict2.sentence !== null)
48625
48785
  lines.push(verdict2.sentence);
48626
48786
  lines.push(view.blocked === null ? "`tldrx next` is now affordable in every phase that still has a stage to run." : `still BLOCKED in ${view.blocked.id} — ${view.fix_command ?? ""}`);
@@ -48635,6 +48795,7 @@ function budgetRaise(argv) {
48635
48795
  function budgetGrant(argv) {
48636
48796
  try {
48637
48797
  const args = parseArgs(argv, VALUE_FLAGS3);
48798
+ refuseRaiseFlags(args, "grant");
48638
48799
  const amountText = args.positionals[0];
48639
48800
  if (amountText === undefined) {
48640
48801
  throw new UsageError("budget grant needs an amount: `tldrx budget grant 20 --fact F031`");
@@ -48720,6 +48881,27 @@ function money4(amount) {
48720
48881
  function openRun(wanted, root2) {
48721
48882
  return resolveRunOrExplain("tldrx budget", root2, wanted);
48722
48883
  }
48884
+ function requireStageBudget(run2, phaseId, stageId) {
48885
+ const phase = run2.phases.find((entry) => entry.id === phaseId);
48886
+ if (phase === undefined)
48887
+ throw new UsageError(`no phase \`${phaseId}\` in this run`);
48888
+ const stage2 = phase.stages.find((entry) => entry.id === stageId);
48889
+ if (stage2 === undefined) {
48890
+ const known = phase.stages.map((entry) => entry.id).join(", ");
48891
+ throw new UsageError(`no stage \`${stageId}\` in ${phaseId} — this phase has: ${known === "" ? "none" : known}`);
48892
+ }
48893
+ return stage2.budget_usd;
48894
+ }
48895
+ function stageRaiseLines(phaseId, stageId, before, amountUsd) {
48896
+ if (stageId === null || before === null) {
48897
+ return [
48898
+ "No spawn ceiling moved: per-story and reviewer caps come from the STAGE's own " + `budget_usd, not from this phase ceiling. Move that too with \`--stage <id>\` ` + `(\`tldrx budget show\` names the stage ${phaseId} would run next).`
48899
+ ];
48900
+ }
48901
+ return [
48902
+ `${phaseId}/${stageId} budget_usd $${before.toFixed(2)} → $${round25(before + amountUsd).toFixed(2)} ` + `(+$${amountUsd.toFixed(2)}) — every per-story and reviewer cap is derived from this figure.`
48903
+ ];
48904
+ }
48723
48905
 
48724
48906
  // src/cli/commands/cost.ts
48725
48907
  var VALUE_FLAGS4 = ["run", "root"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tldr-experts",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "tldr-experts: an evidence-first, file-based AI development framework - five stages, a gate on every one, and every claim cited or refused. Installs the `tldrx` (and `tldr-experts`) command. Beta.",
5
5
  "license": "MIT",
6
6
  "author": "Alan Martinez",
@@ -2,7 +2,7 @@
2
2
  "$doc": "Shape verified from https://code.claude.com/docs/en/plugins.md (Quickstart > Create the plugin manifest). Fields used here: name, description, version, author.name. Only plugin.json goes inside .claude-plugin/; skills/, agents/ and hooks/ live at the plugin root.",
3
3
  "name": "tldrx",
4
4
  "description": "tldr-experts: an evidence-first, file-based AI development framework. Five stages, a gate on every one, every claim cited or refused. Beta.",
5
- "version": "0.20.0",
5
+ "version": "0.22.0",
6
6
  "author": {
7
7
  "name": "Alan Martinez"
8
8
  }