tldr-experts 0.23.0 → 0.24.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,132 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.24.0 — 2026-09-14
4
+
5
+ ### Added
6
+
7
+ - **The two-session protocol is mechanical where it was chat (#299).** MEASURED over one
8
+ unattended day (2026-09-13/14, two sessions, ~16 issues): two branches each staged ONE
9
+ unreleased CHANGELOG heading — `0.22.1` and `0.23.0` — for the same next version, and every
10
+ per-branch check passed both because the defect only exists on the merged tree; "is a release
11
+ in flight?" was asked by message four times because `scripts/release.sh` wrote nothing anyone
12
+ could wait on; and a wave's liveness was read off `ps` and a marker's mtime, once wrongly.
13
+ Three things change, none of them in `src/`. `scripts/merge-wave.sh` now refuses a MERGED tree
14
+ carrying more than one `## <v> — unreleased` heading, or one whose version is not above the
15
+ top dated heading, with **exit 12** — the merge commit is rewound, nothing is pushed, both
16
+ headings are named; zero unreleased headings (the state right after a release) still merges.
17
+ `scripts/release.sh` writes `.RELEASE-IN-PROGRESS` at the repo root (pid, host, version,
18
+ started) for its whole span and removes it on every exit path including a red gate and a
19
+ signal; the wave WAITS on that marker exactly as it waits on its own lock — same poll, same
20
+ stale-by-dead-pid rule, same `MW_LOCK_*` budget — and gives up with **exit 13**. And
21
+ `scripts/merge-wave.sh --status` prints one line, exit 0 either way: the live wave's holder,
22
+ branch, phase (`merge|gates|push`, now recorded in the lock) and start time, a live release's
23
+ holder and version, or `idle`. The `maintain` skill gains a §9 for a session that is not the
24
+ driver — announce the file set first (naming the `build.ts` rule), agree the one unreleased
25
+ heading before writing it, announce lock/sha/OK line, re-review after a rebase — citing
26
+ `AGENTS.md` by section; `test/maintain-skill.test.ts` pins the section and its citations.
27
+ Minor by behaviour: a wave that used to pass now refuses.
28
+
29
+ ### Fixed
30
+
31
+ - **A per-story cap now says what it was derived from, and names a lever that moves it (#281).**
32
+ MEASURED on a live unattended run (0.18.2, eight stories): a developer died twice on a **$5.97**
33
+ cap while `03-plan/budget.yml` priced its story at **$14.00** and the 0.18.2 entry below promised
34
+ `max(price × 3, $4.00)` = $42. The `blocked_reason` told the operator to raise the plan price; the
35
+ operator raised every price ×4 and the cap did not move by a cent. The arithmetic is not the
36
+ defect: `priceScale` fits the plan's SUM into the stage's `budget_usd` — $16.20 over a $114.00 plan
37
+ is a scale of 0.1421 — and a uniform raise keeps the ratio and the sum, so the scale absorbs it
38
+ exactly. What the framework SAID was: two comment copies in `caps.ts` and the 0.18.2 bullet wrote
39
+ `price` where the code reads `price × scale`, and that section is dated and immutable, so this
40
+ bullet corrects #277's wording — the ceiling is `max(price × scale × story_cap_multiplier,
41
+ story_cap_floor_usd)`, scale being 1 only while the prices sum inside the stage. Three things
42
+ change, none of them a number. The cap death reason now shows the FORMULA with its inputs, not
43
+ the conclusion — `cap $5.97 = plan price $14.00 × stage scale 0.1421 (stage budget_usd $16.20 over
44
+ $114.00 of plan prices) × story_cap_multiplier 3 = $5.97, above the floor $4.00` — and on a scaled
45
+ plan it says the price cannot move the cap and names the stage's own `budget_usd` with the
46
+ `tldrx budget raise … --stage` command (#244) that lifts the scale to 1; an unscaled plan keeps
47
+ the price as a lever (the old sentence was right exactly there), and an unpriced story names its
48
+ uniform share instead of a plan price it never had. A plan priced past its Build stage still
49
+ passes the `plan` gate — the scale is a deliberate tolerance — but the gate's detail now carries
50
+ the factor (`7.0× what the stage holds`), the scale, the largest story as a worked example and the
51
+ same command; the Build executor prints the same advisory on stderr at entry, before any spawn,
52
+ so a run already in flight is told too. `shortBy` moved from `budget/budgetView.ts` to
53
+ `build/caps.ts` (re-exported where it was): the plan-price shortfall needs the same round-up, and a
54
+ second copy of a rounding rule is what §7 forbids.
55
+ - **A story whose dependency is at `review` now WAITS instead of being written `blocked`, and a
56
+ `blocked` row whose reason names a dependency that has since turned `done` is offered again
57
+ (#280).** Measured on a live unattended run (0.18.2, four stories in four waves, S3 and S4
58
+ `depends_on: [S2]`): S2 came out of its fix round at `review` — verdict recorded, branch merged
59
+ into the epic — and the loop parked both dependents `blocked` with `dependency S2 is \`review\`,
60
+ not \`done\``. Two polls later S2 was `done`; the dependents were still `blocked`, because
61
+ `blocked` is a terminal row the loop never revisits, and a person had to `story reopen` both.
62
+ #260's frontier drew one line — `done` runs, anything else blocks — which is right for a
63
+ dependency that will not land in this loop (`blocked`, or `todo` after its developer died, #263)
64
+ and wrong for `review`/`in_progress`: that is a story mid-pipeline, re-offered by the very next
65
+ invocation, and a terminal row over it confused "not yet" with "never". Now such a dependency is
66
+ a wait — the dependent's row is left untouched at `todo`, no log and no outcome row are written,
67
+ `## Unknowns` names the wait with the story it waits on, and the next invocation asks again. And
68
+ the shape every earlier run left on disk is released the same way a dead developer's block is:
69
+ a `blocked` row whose `## Why it is not done` is a dependency hold, over dependencies that are
70
+ all `done` now, is offered again — nothing attempted it, so nothing about it was judged; a row a
71
+ reviewer blocked keeps its verdict. One leaf, `build/dependencyHold.ts`, writes the sentence and
72
+ reads it back. #263's own pin — a dependency parked `todo` still blocks — is untouched. Visible
73
+ in-session too: after a story settles, a dependent that used to sit `blocked` is now the next
74
+ `--prepare`, so the stage stays `running` and names it instead of reaching the gate.
75
+ - **`--until-done` compares the REFUSAL across attempts, not the last line printed (#297).** The
76
+ guard exists so a loop does not spend its whole relaunch budget hammering a wall, and for whole
77
+ exit families it could not see a wall at all: every stage death ends with the same literal advice
78
+ — `cost is recorded, not refunded — retry with …` — and so did every context refusal, every
79
+ budget refusal, every host-tokens refusal, each of them a string with nothing interpolated in it.
80
+ Reading `lines[lines.length - 1]` there compared a constant to itself, so the SECOND stage death
81
+ of any run was declared a verbatim repeat whether the two deaths were the same refusal, different
82
+ refusals, or measurable progress — and every remaining relaunch was thrown away. Both halves of
83
+ the guard were wrong at once: it spent nothing on the stuck case it was built for, and spent the
84
+ budget on the case that was moving. The fix is not a better index — an index is right only for
85
+ the report shapes that exist the day it is written, and breaks the moment a caller appends
86
+ another note. A report now NAMES its own refusal (`NextOutcome.signature`), the loop compares
87
+ that, and it falls back to the last line only where nothing named one (a throw's message, a
88
+ missing input — reports whose last line already IS their reason; a repeat there is still real
89
+ evidence, and the bound is a backstop that costs money, not a reading). `run.relaunched` carries
90
+ the comparand it will compare against next, and its `reason` — the sentence the stop line and the
91
+ ledger both quote — now names the refusal rather than the advice under it. Executor refusals
92
+ (`refused: true`) are covered at their ONE pass-through rather than producer by producer: eight
93
+ producers across the Build and Watch executors leave through a single `out()` call, which now
94
+ passes the executor's own signature, falling back to `ExecutorOutcome.error` — and four of those
95
+ sentences had to be corrected before the fallback was worth anything. The question is not what a
96
+ sentence interpolates but whether it DISTINGUISHES the states a relaunch can move between, and a
97
+ run comes back to the same repo: naming the repo alone made two different dirty trees one
98
+ refusal, two different stash failures one refusal, and four structurally different foreign-epic
99
+ faults one refusal. So the dirty-tree refusal now names the overlapping paths and why each is
100
+ claimed; the foreign-epic refusal carries one sentence per fault (unreadable claims, an open
101
+ claimant, nobody's leftover, a leftover that could not be moved) instead of one per branch; the
102
+ could-not-be-set-aside refusal carries git's own reason, which its printed lines already had; and
103
+ the red-base refusal names EVERY red command rather than the first in iteration order — measured
104
+ on two base trees that differ only in their second command, where the printed refusal changed and
105
+ the comparand did not move a byte. Watch's branch-incoherence refusal, whose `error` is null and
106
+ whose last line is a literal about `tldrx doctor`, names its faults itself. A ninth producer
107
+ added tomorrow inherits the door instead of being born blind.
108
+
109
+ - **The Build handoff no longer fails its own `claim-sources` check over a command that spans
110
+ lines (#283).** MEASURED twice in one hour on a live unattended run (0.18.2 → 0.18.3): the
111
+ executor wrote `04-build/handoff.md` itself and then refused it — `trailing-position` on a
112
+ blocked story's Findings and Unknowns bullets, then `unsourced` on a `<id>'s developer had …
113
+ refused` bullet — stage exit 5, a `--until-done` relaunch burned each time. The issue's own
114
+ reading, the DoD citation joined mid-line by `; and `, was probed on the same base and passes:
115
+ the reader takes the LAST `[src: …]` on a line. What fails is a NEWLINE. The refused command
116
+ reaches the renderer as the developer typed it (`agentEvents.ts` `toolTarget` returns the Bash
117
+ `command` input verbatim — a wrapped `mv a \` + `b`, a heredoc), `renderBuildHandoff` quoted it
118
+ inside one bullet, and `parseHandoff` ends a bullet at the first column-0 line, so the first
119
+ physical line carried its citation mid-sentence or none at all and the rest was prose nothing
120
+ read. Every element of the document is now made ONE line at the join — the break shown as ` ⏎ `
121
+ rather than erased, so `mv a \ ⏎ b` still says the line was wrapped and nothing is dropped —
122
+ which holds for every field the renderer embeds, not only the two the run hit; the review log
123
+ keeps the command verbatim — and a document that had to draw the mark says so once, under its
124
+ header, where the developer that copies the line reads it. Text the framework composes has to
125
+ satisfy the grammar the framework checks; the rule is one function, `asOneLine`, and the file's
126
+ other writer (`epicRelease.ts`, the `## Epic branch released` section carrying a `run cancel
127
+ --note` verbatim) goes through it too. A document with no newline in any quoted text is
128
+ byte-identical, so the build golden is unchanged.
129
+
3
130
  ## 0.23.0 — 2026-09-14
4
131
 
5
132
  ### Fixed
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.24.0 | 2026-09-14 | `beta` | Five fixes from one unattended night, all measured on live runs. The two-session protocol is mechanical where it was chat: the merge wave refuses a merged tree with two unreleased CHANGELOG headings or one at or below the last release (exit 12), `release.sh` holds a `.RELEASE-IN-PROGRESS` marker the wave waits on (exit 13 when it gives up), `merge-wave.sh --status` reads holder, branch and phase in one line, and the `maintain` skill gains a worker-mode section (#299). A per-story cap says what it was derived from — plan price × stage scale — and names the lever that moves it, and a plan whose prices exceed the stage budget is told so instead of being scaled in silence (#281). A story whose dependency is still at `review` waits in `todo` instead of being written `blocked`, and a dependency block left by an earlier version is released once the dependency is `done` (#280). `--until-done` compares the refusal across attempts, not the advice line printed under it, so a failure that made progress keeps its relaunches (#297). And the Build handoff no longer fails its own `claim-sources` check over a refused command that spans lines: one physical line per element, the break shown as `⏎` and explained once in the document (#283). Minor release: the wave and `--until-done` behave differently in situations that exist today. |
338
339
  | 0.23.0 | 2026-09-14 | `beta` | `tldrx story reopen --as-is` now settles a story whose work is already merged into the epic but whose review never completed (`n-a` or `error` on its last recorded merge): a new, named review-only case that merges nothing and only routes the story to its reviewer, read from an additive ledger field; the existing refusal ("no commit the epic has not got") is untouched and now names the standing verdict when one exists (`changes`, `approve`, `fixlist` still refuse). A fix list whose findings are all deferred settles the story `done` instead of spawning a developer with nothing to fix, measured on a live run that paid for four such developers (#295). And a turn that died on a provider limit no longer records `success` borrowed from the provider's own subtype: the failure record says what the host saw (#296, first half — DETECTING the provider limit as its own non-execution kind is #298 and is NOT in this release). Minor release: `story reopen --as-is` behaves differently in a situation that exists today. |
339
340
  | 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. |
340
341
  | 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`. |
@@ -22,7 +22,7 @@ import {
22
22
  validateRunBudget,
23
23
  wouldExceed,
24
24
  wouldExceedHostTokens
25
- } from "./chunk-1p5027fn.js";
25
+ } from "./chunk-2jb272zj.js";
26
26
  import {
27
27
  EventLog
28
28
  } from "./chunk-6z5rmj0b.js";
@@ -1232,12 +1232,18 @@ function wouldExceedHostTokens(budget, phaseId, spentTokens) {
1232
1232
  };
1233
1233
  }
1234
1234
 
1235
+ // src/core/build/caps.ts
1236
+ var MAX_ATTEMPTS2 = STAGE_TUNING_DEFAULTS.attempts;
1237
+ var REVIEWER_SHARE2 = STAGE_TUNING_DEFAULTS.reviewerShare;
1238
+ var STORY_CAP_MULTIPLIER2 = STAGE_TUNING_DEFAULTS.storyCapMultiplier;
1239
+ var STORY_CAP_FLOOR_USD2 = STAGE_TUNING_DEFAULTS.storyCapFloorUsd;
1240
+ function shortBy(estimate, remaining) {
1241
+ return Math.max(0.01, Math.ceil((estimate - remaining) * 100) / 100);
1242
+ }
1243
+
1235
1244
  // src/core/budget/budgetView.ts
1236
1245
  function raiseCommand(runId, phaseId, amountUsd) {
1237
1246
  return `tldrx budget raise ${phaseId} ${amountUsd.toFixed(2)} --run ${runId}`;
1238
1247
  }
1239
- function shortBy(estimate, remaining) {
1240
- return Math.max(0.01, Math.ceil((estimate - remaining) * 100) / 100);
1241
- }
1242
1248
 
1243
- export { currentActor, nowRfc3339, spentBasis, tallyOf, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ON_GRANT_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, stackExpertNames, BUILD_PHASE2 as BUILD_PHASE, remainingWork, wouldExceed, wouldExceedHostTokens, raiseCommand, shortBy };
1249
+ export { currentActor, nowRfc3339, spentBasis, tallyOf, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ON_GRANT_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, stackExpertNames, BUILD_PHASE2 as BUILD_PHASE, remainingWork, wouldExceed, wouldExceedHostTokens, shortBy, raiseCommand };
@@ -8,7 +8,7 @@ import {
8
8
  spentBasis,
9
9
  tallyOf,
10
10
  validateRunBudget
11
- } from "./chunk-1p5027fn.js";
11
+ } from "./chunk-2jb272zj.js";
12
12
  import {
13
13
  EventLog,
14
14
  OUTCOME_NOT_RECORDED,
@@ -515,10 +515,20 @@ function round(n) {
515
515
  return Math.round(n * 100) / 100;
516
516
  }
517
517
 
518
+ // src/core/facilitator/seedInputs.ts
519
+ var DEFAULT_INPUTS_MAX_BYTES = 256 * 1024;
520
+ var MAX_SEED_INLINE_BYTES = 64 * 1024;
521
+ var MIN_SLICE_BYTES = 2 * 1024;
522
+
523
+ // src/core/facilitator/contextLedger.ts
524
+ var DEFAULT_PROMPT_MAX_BYTES = 400 * 1024;
525
+
518
526
  // src/core/run/runOutcome.ts
519
527
  import { join as join3 } from "node:path";
520
528
 
521
529
  // src/core/build/handoff.ts
530
+ var LINE_BREAK_MARK = "⏎";
531
+ var LINE_BREAK_NOTE = `\`${LINE_BREAK_MARK}\` marks a line break inside a quoted command; the review log beside this file ` + "keeps the command verbatim.";
522
532
  var FINDING_STATUS_RE = new RegExp(`—\\s+(${PLAN_STATUSES.join("|")})\\s+—`);
523
533
  var FINDING_REASON_RE = new RegExp(`—\\s+(?:${PLAN_STATUSES.join("|")})\\s+—\\s+[^:]*:\\s*(\\S.*)$`);
524
534
 
@@ -20,14 +20,14 @@ import {
20
20
  runSnapshot,
21
21
  statusWithOutcome,
22
22
  whatIsWaiting
23
- } from "./chunk-ah1pdax3.js";
23
+ } from "./chunk-4frmcwp3.js";
24
24
  import {
25
25
  expertsDir,
26
26
  loadExperts,
27
27
  pathsIntersect,
28
28
  readExpertDomain,
29
29
  stackExpertNames
30
- } from "./chunk-1p5027fn.js";
30
+ } from "./chunk-2jb272zj.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-ah1pdax3.js";
6
- import"./chunk-1p5027fn.js";
5
+ } from "./chunk-4frmcwp3.js";
6
+ import"./chunk-2jb272zj.js";
7
7
  import"./chunk-6z5rmj0b.js";
8
8
  import"./chunk-m1s8a6s0.js";
9
9
  import"./chunk-yre2scxn.js";