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 +84 -0
- package/README.md +2 -0
- package/dist/hooks/budget-gate.js +1 -1
- package/dist/hooks/{chunk-j5d0nard.js → chunk-1p5027fn.js} +3 -0
- package/dist/hooks/{chunk-53wgfn01.js → chunk-ah1pdax3.js} +1 -1
- package/dist/hooks/session-start.js +2 -2
- package/dist/hooks/statusline.js +2 -2
- package/dist/tldrx.js +265 -83
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
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. |
|
|
@@ -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 = {
|
|
@@ -20,14 +20,14 @@ import {
|
|
|
20
20
|
runSnapshot,
|
|
21
21
|
statusWithOutcome,
|
|
22
22
|
whatIsWaiting
|
|
23
|
-
} from "./chunk-
|
|
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-
|
|
30
|
+
} from "./chunk-1p5027fn.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) {
|
|
@@ -26466,7 +26477,23 @@ function dodFailureReason(result2, repo) {
|
|
|
26466
26477
|
}
|
|
26467
26478
|
|
|
26468
26479
|
// src/core/build/developerGrants.ts
|
|
26469
|
-
var DEVELOPER_GIT_VERBS = [
|
|
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).
|
|
26598
|
-
"
|
|
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.
|
|
26612
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
5
|
+
"version": "0.22.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alan Martinez"
|
|
8
8
|
}
|