tldr-experts 0.20.0 → 0.21.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,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0 — 2026-09-13
4
+
5
+ ### Fixed
6
+
7
+ - **The raise an operator is told to make now moves a ceiling, and a stage that cannot fund a
8
+ review stops buying one (#244, #289).** One function, one question — how much money is there,
9
+ and who decides when there is not enough — so the two were done together. The model, measured
10
+ case by case by the session running two live unattended runs (their measurement, not this
11
+ file's): the three money figures are three knobs with three different jobs, not three copies of
12
+ one number. The STAGE's `budget_usd` decides the price scale and therefore every per-story
13
+ developer ceiling and the reviewer's; the PHASE ceiling only takes part in the economy refusal
14
+ ("remaining work > what is left") and caps no spawn at all; `per_agent_max_usd` only trims a
15
+ ceiling from above. Their two controls: raising only the stage figure, 16.20 → 60, moved a
16
+ developer ceiling 5.97 → 22.11 on the next spawn, and raising `per_agent_max_usd` and the phase
17
+ ceiling *without* touching the stage moved the ceiling by nothing. So `tldrx budget raise
18
+ <phase> <usd>` — the command every refusal names — moved the one knob that caps nothing a
19
+ sub-agent is dispatched under: the operator raised, re-ran, died on the identical cap, and each
20
+ `reject --and-continue` bought exactly one more turn. It now takes `--stage <id>`, which adds
21
+ the same amount to that stage's own `budget_usd` in `run.yml` (additively, like the phase
22
+ ceiling; recorded on `budget.raised` as `stage`/`stage_budget_before`/`stage_budget_after`, and
23
+ an unknown stage id is a usage refusal that writes neither file), and a raise that names no
24
+ stage now SAYS in its own output that no spawn ceiling moved and names the flag that would
25
+ move one.
26
+ - **A reviewer the stage cannot fund is not spawned, and the refusal is not recorded as a verdict
27
+ (#289).** Measured on a live unattended run (`--budget 60`, `--gates none --questions none
28
+ --ship merge`): story S1's developer went DoD 3/3 green at $10.64, the reviewer right after it
29
+ was handed **$0.43**, died with `Reached maximum budget ($0.43)` before reading a line of the
30
+ diff, and the `verdict: error` that recorded its death parked S1 at `review` with both
31
+ dependent stories blocked behind it — the loop stopped after $17.23 of a $60 run.
32
+ `REVIEWER_FLOOR_USD` ($1.00) already existed and did not save it, because the floor YIELDS to
33
+ the stage remainder (`min(REVIEWER_FLOOR_USD, budget − spent)`): a nearly-exhausted stage buys a
34
+ turn that provably cannot finish. The fix is not a bigger floor. When the stage has less left
35
+ than a review costs the executor refuses BEFORE the spawn — no `agent.spawned`, no task row, not
36
+ a cent — and the story parks at `review` exactly where an unjudged story parks, with its diff
37
+ merged and its one attempt unspent. What it writes down is **not a verdict**: `verdict: "n-a"`
38
+ ("no reviewer ran for this story") beside a `reviewer_unfunded:` reason on the story's
39
+ `task.done`, its review log and the retro — the reviewer-side sibling of the `developerError`
40
+ and `budget_death:` records #271 and #277 shipped, and for the same reason. An agent that never
41
+ started formed no opinion, and `error` already means "it died mid-read"; writing a death nobody
42
+ chose as a review outcome is an audit record lying in the dangerous direction. The reason names
43
+ the lever that actually moves the ceiling — `tldrx budget raise <phase> <usd> --stage <id>`,
44
+ sized to the shortfall — rather than the phase ceiling that caps no spawn. The review stays
45
+ owed, so the next invocation's review-only path picks it up instead of re-running the developer,
46
+ and under an unchanged remainder it refuses again for free. `reviewerCap`'s arithmetic is
47
+ deliberately untouched: it is mirrored on the budget gate's hot path, and the brake's estimate
48
+ must keep reading the schedule the executor would have spent under. A HOST review is never
49
+ refused this way — it costs the stage nothing.
50
+
3
51
  ## 0.20.0 — 2026-09-13
4
52
 
5
53
  ### Added
package/README.md CHANGED
@@ -335,6 +335,7 @@ back on the registry is 0.3.0.
335
335
 
336
336
  | Version | Date | Status | Contains |
337
337
  |---|---|---|---|
338
+ | 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
339
  | 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
340
  | 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
341
  | 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-f5wkn522.js";
26
26
  import {
27
27
  EventLog
28
28
  } from "./chunk-6z5rmj0b.js";
@@ -8,7 +8,7 @@ import {
8
8
  spentBasis,
9
9
  tallyOf,
10
10
  validateRunBudget
11
- } from "./chunk-j5d0nard.js";
11
+ } from "./chunk-f5wkn522.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-6qnsx1tz.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-f5wkn522.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-6qnsx1tz.js";
6
+ import"./chunk-f5wkn522.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) {
@@ -28893,6 +28904,21 @@ function text2(value) {
28893
28904
  return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
28894
28905
  }
28895
28906
  var REVIEWER_FAILED = "the reviewer sub-agent failed";
28907
+ var REVIEWER_UNFUNDED_MARK = "no reviewer was spawned: the stage cannot fund one";
28908
+ function reviewerUnfundedReason(parts) {
28909
+ 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}`;
28910
+ }
28911
+ function reviewerUnfunded(reason) {
28912
+ return {
28913
+ verdict: "n-a",
28914
+ summary: reason,
28915
+ findings: [],
28916
+ fixlist: [],
28917
+ fixlistProblems: [],
28918
+ formatProblems: [],
28919
+ verdictProblem: null
28920
+ };
28921
+ }
28896
28922
  function reviewerFailed(error) {
28897
28923
  const said2 = (error ?? "").trim();
28898
28924
  return {
@@ -29011,6 +29037,14 @@ function summaryLines(outcome) {
29011
29037
  `> ${outcome.developerError}`
29012
29038
  ];
29013
29039
  }
29040
+ if ((outcome.reviewerUnfunded ?? null) !== null) {
29041
+ return [
29042
+ "**No reviewer was spawned: the stage could not fund one.** This is not a verdict and not",
29043
+ "a failure of the diff — nothing read it, and nothing was spent trying. Why:",
29044
+ "",
29045
+ `> ${outcome.reviewerUnfunded ?? ""}`
29046
+ ];
29047
+ }
29014
29048
  if (outcome.verdict === "error") {
29015
29049
  return [
29016
29050
  "**The reviewer FAILED and returned no verdict.** This is not a request for changes:",
@@ -29374,6 +29408,9 @@ function unmeteredNote(count) {
29374
29408
  function raiseCommand(runId, phaseId, amountUsd) {
29375
29409
  return `tldrx budget raise ${phaseId} ${amountUsd.toFixed(2)} --run ${runId}`;
29376
29410
  }
29411
+ function stageRaiseCommand(runId, phaseId, stageId, amountUsd) {
29412
+ return `${raiseCommand(runId, phaseId, amountUsd)} --stage ${stageId}`;
29413
+ }
29377
29414
  function shortBy(estimate, remaining2) {
29378
29415
  return Math.max(0.01, Math.ceil((estimate - remaining2) * 100) / 100);
29379
29416
  }
@@ -36486,6 +36523,15 @@ function reviewerCap(parts, spentUsd, storyId) {
36486
36523
  const floor = Math.min(REVIEWER_FLOOR_USD2, Math.max(parts.budgetUsd - spentUsd, 0));
36487
36524
  return round25(Math.min(Math.max(derived, floor), parts.maxBudgetUsd));
36488
36525
  }
36526
+ function stageRemainderUsd(parts, spentUsd) {
36527
+ if (!(parts.budgetUsd > 0))
36528
+ return null;
36529
+ return Math.max(round25(parts.budgetUsd - spentUsd), 0);
36530
+ }
36531
+ function reviewerUnderfunded(parts, spentUsd) {
36532
+ const left = stageRemainderUsd(parts, spentUsd);
36533
+ return left !== null && left < REVIEWER_FLOOR_USD2;
36534
+ }
36489
36535
  function priceOf(parts, storyId) {
36490
36536
  if (storyId === undefined)
36491
36537
  return null;
@@ -36686,6 +36732,8 @@ function readReviewLedger(runDir, storyId) {
36686
36732
  };
36687
36733
  }
36688
36734
  function reviewEventErrored2(payload) {
36735
+ if (typeof payload.unfunded_usd === "number")
36736
+ return true;
36689
36737
  if (payload.verdict === "error")
36690
36738
  return true;
36691
36739
  if (payload.verdict !== "changes")
@@ -36821,7 +36869,7 @@ function awaitingReview(runDir, stageId, pending) {
36821
36869
  function resumableReview(runDir, storyId, status, fresh) {
36822
36870
  if (status !== "review" && status !== "in_progress")
36823
36871
  return null;
36824
- if (fresh !== undefined && fresh.verdict !== "error")
36872
+ if (fresh !== undefined && !reviewStillOwed(fresh))
36825
36873
  return null;
36826
36874
  const ledger4 = readReviewLedger(runDir, storyId);
36827
36875
  if (ledger4.erroredWith === null || ledger4.commit === null)
@@ -37194,6 +37242,9 @@ function storyRetroLines(outcome, runId) {
37194
37242
  if (outcome.developerError !== null) {
37195
37243
  lines.push(`- \`${outcome.id}\` — the developer FAILED and produced no work on attempt ` + `${String(outcome.attempts)}: ${oneLine5(outcome.developerError)} ${src}`);
37196
37244
  }
37245
+ if ((outcome.reviewerUnfunded ?? null) !== null) {
37246
+ lines.push(`- \`${outcome.id}\` — no reviewer was spawned on attempt ` + `${String(outcome.attempts)}: ${oneLine5(outcome.reviewerUnfunded ?? "")} ${src}`);
37247
+ }
37197
37248
  if (outcome.verdict === "error") {
37198
37249
  lines.push(`- \`${outcome.id}\` — the reviewer FAILED and returned no verdict on attempt ` + `${String(outcome.attempts)}: ${oneLine5(outcome.reviewSummary)} ${src}`);
37199
37250
  }
@@ -38910,6 +38961,7 @@ class BuildSession {
38910
38961
  outcomes = new Map;
38911
38962
  refusals = new Map;
38912
38963
  capDeaths = new Map;
38964
+ unfundedReviews = new Map;
38913
38965
  asIsSettlements = new Map;
38914
38966
  epics = new EpicState;
38915
38967
  lines = [];
@@ -39253,7 +39305,7 @@ class BuildSession {
39253
39305
  return;
39254
39306
  if (outcome?.status !== "review")
39255
39307
  return;
39256
- if (outcome.verdict === "error")
39308
+ if (reviewStillOwed(outcome))
39257
39309
  return;
39258
39310
  if (outcome.verdict === "fixlist")
39259
39311
  return;
@@ -39313,6 +39365,7 @@ class BuildSession {
39313
39365
  const before = this.statusOf(planned);
39314
39366
  this.refusals.delete(id);
39315
39367
  this.capDeaths.delete(id);
39368
+ this.unfundedReviews.delete(id);
39316
39369
  this.asIsSettlements.set(id, asIs);
39317
39370
  const story = await this.writes.run(() => this.openStory(planned));
39318
39371
  await this.writes.run(() => {
@@ -39386,6 +39439,7 @@ class BuildSession {
39386
39439
  const before = this.statusOf(planned);
39387
39440
  this.refusals.delete(planned.story.id);
39388
39441
  this.capDeaths.delete(planned.story.id);
39442
+ this.unfundedReviews.delete(planned.story.id);
39389
39443
  this.asIsSettlements.delete(planned.story.id);
39390
39444
  const story = await this.writes.run(() => this.openStory(planned, true));
39391
39445
  await this.writes.run(() => {
@@ -39565,6 +39619,21 @@ class BuildSession {
39565
39619
  await this.handOffReview(story, dod, commit2, priorCost, carried, epicBase);
39566
39620
  return "handed-off";
39567
39621
  }
39622
+ if (supplied === undefined && reviewerUnderfunded(this.capParts, this.spent())) {
39623
+ const review2 = this.refuseUnfundedReview(story);
39624
+ await this.settle(story, "review", {
39625
+ dod,
39626
+ commit: commit2,
39627
+ merged: true,
39628
+ carried,
39629
+ epicBase,
39630
+ verdict: "n-a",
39631
+ review: review2,
39632
+ cost: round25(priorCost),
39633
+ reason: review2.summary
39634
+ });
39635
+ return "settled";
39636
+ }
39568
39637
  const outcome = supplied === undefined ? await this.spawnReviewer(story, dod, epicBase) : { review: supplied, cost: 0 };
39569
39638
  const review = outcome.review;
39570
39639
  const cost = round25(priorCost + outcome.cost);
@@ -39769,7 +39838,8 @@ class BuildSession {
39769
39838
  });
39770
39839
  });
39771
39840
  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}\``);
39841
+ 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})`;
39842
+ this.lines.push(` · ${planned.story.id}: ${failed3} — ` + `re-running the REVIEW only; \`${resume.commit}\` is already merged into \`${story.epicBranch}\``);
39773
39843
  this.noteUnrecordedBase(planned.story.id, story.epicBranch, resume.epicBase);
39774
39844
  await this.reviewAndSettle(story, resume.dod, resume.commit, 0, null, resume.epicBase ?? null);
39775
39845
  }
@@ -40107,6 +40177,28 @@ class BuildSession {
40107
40177
  storyTitle: story.planned.story.title
40108
40178
  });
40109
40179
  }
40180
+ refuseUnfundedReview(story) {
40181
+ const id = story.planned.story.id;
40182
+ const left = stageRemainderUsd(this.capParts, this.spent()) ?? 0;
40183
+ const review = reviewerUnfunded(reviewerUnfundedReason({
40184
+ remainingUsd: left,
40185
+ floorUsd: REVIEWER_FLOOR_USD2,
40186
+ fix: `\`${stageRaiseCommand(this.ctx.runId, this.ctx.phaseId, this.ctx.stageId, shortBy(REVIEWER_FLOOR_USD2, left))}\``
40187
+ }));
40188
+ this.unfundedReviews.set(id, review.summary);
40189
+ this.ctx.emit("check.failed", {
40190
+ phase: this.ctx.phaseId,
40191
+ check: "review",
40192
+ story: id,
40193
+ verdict: review.verdict,
40194
+ attempt: story.attempt,
40195
+ unfunded_usd: left,
40196
+ floor_usd: REVIEWER_FLOOR_USD2,
40197
+ detail: review.summary
40198
+ });
40199
+ this.reviewers.set(id, null);
40200
+ return review;
40201
+ }
40110
40202
  async spawnReviewer(story, dod, epicBase) {
40111
40203
  const id = story.planned.story.id;
40112
40204
  let refusal = null;
@@ -40330,6 +40422,7 @@ class BuildSession {
40330
40422
  permissionRefused: this.refusals.get(id) ?? null,
40331
40423
  declaredCommands: this.repoCommands(story.planned.story.repo),
40332
40424
  budgetDeath: this.capDeaths.get(id) ?? null,
40425
+ reviewerUnfunded: this.unfundedReviews.get(id) ?? null,
40333
40426
  asIs: this.asIsSettlements.get(id) ?? null,
40334
40427
  rescued,
40335
40428
  cost_usd: parts.cost
@@ -40362,6 +40455,7 @@ class BuildSession {
40362
40455
  ...parts.epicBase === null || parts.epicBase === undefined || parts.epicBase === "" ? {} : { epic_base: parts.epicBase },
40363
40456
  ...outcome.permissionRefused == null ? {} : { permission_refused: outcome.permissionRefused },
40364
40457
  ...outcome.budgetDeath == null ? {} : { budget_death: outcome.budgetDeath },
40458
+ ...outcome.reviewerUnfunded == null ? {} : { reviewer_unfunded: outcome.reviewerUnfunded },
40365
40459
  ...outcome.asIs == null ? {} : { as_is: true, as_is_by: outcome.asIs.actor, as_is_note: outcome.asIs.note }
40366
40460
  });
40367
40461
  if (pruning && (rescued === null || rescued.worktree === null)) {
@@ -48520,12 +48614,12 @@ function wouldExceedGrant(budget, phaseId, resultingCeilingUsd) {
48520
48614
  }
48521
48615
 
48522
48616
  // src/cli/commands/budget.ts
48523
- var VALUE_FLAGS3 = ["run", "root", "take-from", "note", "fact", "phase", "on-exceed"];
48617
+ var VALUE_FLAGS3 = ["run", "root", "take-from", "note", "fact", "phase", "on-exceed", "stage"];
48524
48618
  var budgetCommand = {
48525
48619
  name: "budget",
48526
48620
  summary: "Show what the run may still spend, raise a phase ceiling, or record what the owner authorized",
48527
48621
  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>]
48622
+ ` + ` tldrx budget raise <phase> <usd> [--stage <id>] [--run <id>] [--take-from <phase>] [--note <text>] [--root <path>]
48529
48623
  ` + " tldrx budget grant <usd> --fact <F> [--phase <p>] [--on-exceed <warn|block>] [--note <text>] [--run <id>] [--root <path>]",
48530
48624
  implemented: true,
48531
48625
  async run(argv) {
@@ -48546,6 +48640,14 @@ ${budgetCommand.usage}
48546
48640
  }
48547
48641
  };
48548
48642
  var GRANT_ONLY_FLAGS = ["fact", "phase", "on-exceed"];
48643
+ var RAISE_ONLY_FLAGS = ["stage"];
48644
+ function refuseRaiseFlags(args, verb) {
48645
+ for (const flag of RAISE_ONLY_FLAGS) {
48646
+ if (!boolFlag(args, flag))
48647
+ continue;
48648
+ 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>`");
48649
+ }
48650
+ }
48549
48651
  function refuseGrantFlags(args, verb) {
48550
48652
  for (const flag of GRANT_ONLY_FLAGS) {
48551
48653
  if (!boolFlag(args, flag))
@@ -48557,6 +48659,7 @@ function budgetShow(argv) {
48557
48659
  try {
48558
48660
  const args = parseArgs(argv, VALUE_FLAGS3);
48559
48661
  refuseGrantFlags(args, "show");
48662
+ refuseRaiseFlags(args, "show");
48560
48663
  const resolved = openRun(args.positionals[0] ?? stringFlag(args, "run"), workspaceRootFrom(args));
48561
48664
  if (!isResolved(resolved))
48562
48665
  return resolved.exit;
@@ -48587,6 +48690,9 @@ function budgetRaise(argv) {
48587
48690
  if (!isResolved(resolved))
48588
48691
  return resolved.exit;
48589
48692
  const store = resolved.store;
48693
+ const stageId = stringFlag(args, "stage") ?? null;
48694
+ if (stageId !== null)
48695
+ requireStageBudget(store.run, phaseId, stageId);
48590
48696
  const outcome = raiseBudget(store.budget, {
48591
48697
  phaseId,
48592
48698
  amountUsd,
@@ -48600,6 +48706,19 @@ function budgetRaise(argv) {
48600
48706
  return EXIT_GATE_REFUSED;
48601
48707
  }
48602
48708
  store.mutateBudget(() => outcome.budget);
48709
+ const stageBefore = stageId === null ? null : requireStageBudget(store.run, phaseId, stageId);
48710
+ if (stageId !== null) {
48711
+ store.mutate((run2) => ({
48712
+ ...run2,
48713
+ phases: run2.phases.map((phase) => phase.id !== phaseId ? phase : {
48714
+ ...phase,
48715
+ stages: phase.stages.map((stage2) => stage2.id !== stageId ? stage2 : {
48716
+ ...stage2,
48717
+ budget_usd: round25(stage2.budget_usd + amountUsd)
48718
+ })
48719
+ })
48720
+ }));
48721
+ }
48603
48722
  store.append({
48604
48723
  ts: nowRfc3339(),
48605
48724
  run: store.runId,
@@ -48615,12 +48734,17 @@ function budgetRaise(argv) {
48615
48734
  phase_ceiling_after: outcome.phaseCeilingAfter,
48616
48735
  run_ceiling_before: outcome.runCeilingBefore,
48617
48736
  run_ceiling_after: outcome.runCeilingAfter,
48737
+ ...stageId === null || stageBefore === null ? {} : {
48738
+ stage: stageId,
48739
+ stage_budget_before: stageBefore,
48740
+ stage_budget_after: round25(stageBefore + amountUsd)
48741
+ },
48618
48742
  note: stringFlag(args, "note") ?? ""
48619
48743
  }
48620
48744
  });
48621
48745
  store.save();
48622
48746
  const view = buildBudgetView(store.run, store.budget, store.runDir);
48623
- const lines = [describeRaise(outcome)];
48747
+ const lines = [describeRaise(outcome), ...stageRaiseLines(phaseId, stageId, stageBefore, amountUsd)];
48624
48748
  if (verdict2.sentence !== null)
48625
48749
  lines.push(verdict2.sentence);
48626
48750
  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 +48759,7 @@ function budgetRaise(argv) {
48635
48759
  function budgetGrant(argv) {
48636
48760
  try {
48637
48761
  const args = parseArgs(argv, VALUE_FLAGS3);
48762
+ refuseRaiseFlags(args, "grant");
48638
48763
  const amountText = args.positionals[0];
48639
48764
  if (amountText === undefined) {
48640
48765
  throw new UsageError("budget grant needs an amount: `tldrx budget grant 20 --fact F031`");
@@ -48720,6 +48845,27 @@ function money4(amount) {
48720
48845
  function openRun(wanted, root2) {
48721
48846
  return resolveRunOrExplain("tldrx budget", root2, wanted);
48722
48847
  }
48848
+ function requireStageBudget(run2, phaseId, stageId) {
48849
+ const phase = run2.phases.find((entry) => entry.id === phaseId);
48850
+ if (phase === undefined)
48851
+ throw new UsageError(`no phase \`${phaseId}\` in this run`);
48852
+ const stage2 = phase.stages.find((entry) => entry.id === stageId);
48853
+ if (stage2 === undefined) {
48854
+ const known = phase.stages.map((entry) => entry.id).join(", ");
48855
+ throw new UsageError(`no stage \`${stageId}\` in ${phaseId} — this phase has: ${known === "" ? "none" : known}`);
48856
+ }
48857
+ return stage2.budget_usd;
48858
+ }
48859
+ function stageRaiseLines(phaseId, stageId, before, amountUsd) {
48860
+ if (stageId === null || before === null) {
48861
+ return [
48862
+ "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).`
48863
+ ];
48864
+ }
48865
+ return [
48866
+ `${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.`
48867
+ ];
48868
+ }
48723
48869
 
48724
48870
  // src/cli/commands/cost.ts
48725
48871
  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.21.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.21.0",
6
6
  "author": {
7
7
  "name": "Alan Martinez"
8
8
  }