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 +48 -0
- package/README.md +1 -0
- package/dist/hooks/budget-gate.js +1 -1
- package/dist/hooks/{chunk-53wgfn01.js → chunk-6qnsx1tz.js} +1 -1
- package/dist/hooks/session-start.js +2 -2
- package/dist/hooks/statusline.js +2 -2
- package/dist/tldrx.js +152 -6
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- /package/dist/hooks/{chunk-j5d0nard.js → chunk-f5wkn522.js} +0 -0
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. |
|
|
@@ -20,14 +20,14 @@ import {
|
|
|
20
20
|
runSnapshot,
|
|
21
21
|
statusWithOutcome,
|
|
22
22
|
whatIsWaiting
|
|
23
|
-
} from "./chunk-
|
|
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-
|
|
30
|
+
} from "./chunk-f5wkn522.js";
|
|
31
31
|
import {
|
|
32
32
|
isFinished
|
|
33
33
|
} from "./chunk-6z5rmj0b.js";
|
package/dist/hooks/statusline.js
CHANGED
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
5
|
+
"version": "0.21.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alan Martinez"
|
|
8
8
|
}
|
|
File without changes
|