tldr-experts 0.3.1 → 0.5.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +2396 -0
  2. package/README.md +133 -17
  3. package/dist/hooks/answer-capture.js +7 -8
  4. package/dist/hooks/budget-gate.js +83 -16
  5. package/dist/hooks/{chunk-zdxgragg.js → chunk-14zn51kh.js} +171 -16
  6. package/dist/hooks/{chunk-xpxe94qe.js → chunk-3w55tp71.js} +75 -27
  7. package/dist/hooks/{chunk-afamdvyn.js → chunk-5556vjt5.js} +1 -1
  8. package/dist/hooks/{chunk-ytvmc5ns.js → chunk-889tybxc.js} +26 -28
  9. package/dist/hooks/{chunk-9gb21660.js → chunk-a5dq2dcp.js} +54 -14
  10. package/dist/hooks/{chunk-ztczwtj0.js → chunk-a6rpj2cp.js} +835 -16
  11. package/dist/hooks/{chunk-3g61yg59.js → chunk-bvm6vjrt.js} +1 -1
  12. package/dist/hooks/{chunk-0z27twdk.js → chunk-hcrbr430.js} +10 -5
  13. package/dist/hooks/{chunk-3t91gvpp.js → chunk-nadqsr3w.js} +8 -2
  14. package/dist/hooks/{chunk-458wgg9j.js → chunk-q8d3sff9.js} +86 -11
  15. package/dist/hooks/{chunk-9kkm6q0t.js → chunk-qw73rdbr.js} +35 -3
  16. package/dist/hooks/{chunk-ybacnpxd.js → chunk-sae7sqty.js} +5 -0
  17. package/dist/hooks/{chunk-s5qsb4k6.js → chunk-v1c1hpb8.js} +1 -1
  18. package/dist/hooks/claim-sources.js +32 -21
  19. package/dist/hooks/dod-gate.js +7 -6
  20. package/dist/hooks/no-reask.js +9 -9
  21. package/dist/hooks/session-start.js +46 -21
  22. package/dist/hooks/statusline.js +8 -9
  23. package/dist/tldrx.js +19667 -10978
  24. package/package.json +4 -2
  25. package/plugin/.claude-plugin/plugin.json +2 -2
  26. package/plugin/skills/tldrx/SKILL.md +1 -1
  27. package/stages/build/stage.yml +2 -1
  28. package/stages/plan/stage.md +11 -0
  29. package/stages/watch/stage.md +4 -0
  30. package/templates/expert.md +13 -1
  31. package/templates/watcher.md +8 -1
  32. package/workflows/bugfix.yml +8 -4
  33. package/workflows/docs.yml +8 -4
  34. package/workflows/feature.yml +8 -4
  35. package/workflows/hotfix.yml +8 -4
  36. package/workflows/integration.yml +8 -4
  37. package/workflows/migration.yml +8 -4
  38. package/workflows/performance.yml +8 -4
  39. package/workflows/prototype.yml +8 -4
  40. package/workflows/refactor.yml +8 -4
  41. package/workflows/security-patch.yml +8 -4
  42. package/workflows/spike.yml +8 -4
  43. package/workflows/upgrade.yml +8 -4
  44. package/dist/hooks/chunk-sznsenee.js +0 -503
  45. package/templates/epic.md +0 -38
  46. package/templates/story.md +0 -55
package/CHANGELOG.md CHANGED
@@ -1,5 +1,2328 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 — 2026-09-02
4
+
5
+ ### Added
6
+
7
+ - **The dashboard answers the five questions #85 left open (#93).** Each needed a decision
8
+ rather than a patch, and each was taken the CONSERVATIVE way: render what the files
9
+ already say, invent no interaction the page does not already have, and let nothing on a
10
+ read-only page run anything. Every choice below is a **maintainer call, subject to owner
11
+ review** — they are listed on the issue.
12
+ - **The Watchers tab draws the cards.** `05-watch/watchers/*.md` is read into the model as
13
+ the seven fields `Watcher` really declares — `id`, `epic`, `title`, `stories`, `repos`,
14
+ `status`, plus the optional `owner` (#70) — and the tab prints them instead of printing
15
+ the shape it wished it had. A `draft` card also carries **why**: the `absent:` sources it
16
+ cites under `## Signal`, which is the card's own rule for not being `verified`. The
17
+ reading is deliberately the SMALL one — the model resolves no `[src: …]`, calls no
18
+ `parseWatcherCard` and computes no `CardChecklist`, because that re-checks every citation
19
+ against today's working tree and would make a read-only dashboard the only screen in the
20
+ product that runs something. `tldrx watch check` stays the thing that checks, and the
21
+ page says so. A `verified` stamp over an `absent:` Signal is shown as what it is rather
22
+ than silently corrected: `watch check` re-stamps cards, and a viewer that disagreed with
23
+ the file would be a third opinion.
24
+ - **A `draft` card raises no attention card.** The page's rule — an alert means a run is
25
+ waiting on a PERSON right now, derived once in `waiting.ts` — is unchanged. An
26
+ uninstrumented signal is a fact about coverage that stays true until somebody
27
+ instruments it, and it belongs in a panel the way `budget.blocked` does.
28
+ - **Preflight refusals leave their rows on the page.** `04-build/preflight.yml` is read the
29
+ same way `budget.yml` is: read-only, additive, and through the reader that never throws.
30
+ A new **Base gates** section names each of the workspace's own gate commands, the repo
31
+ and base it ran on, its exit code and its `ok`/`failed`/`unmeasured` status — so a Build
32
+ that refused to start is no longer a stage that went backwards for no visible reason.
33
+ The alternative on the issue was to emit an EVENT instead; that is the bigger, better
34
+ change and it is not this one, because the owner's #85 decision covered the two files
35
+ that already existed and an event is a new write on a refusal path. Reading the file it
36
+ already writes is the same doctrine one step further. Red rows are drawn as rows, not as
37
+ alerts, for the same reason `budget.blocked` is.
38
+ - **`keep_worktrees` is one line on the run detail**, and only when it is set (#16). The
39
+ key is written only when true, so drawing `false` would put a row on every run in the
40
+ workspace saying what all of them do. `RunDocument` projects it tolerantly: anything that
41
+ is not the boolean `true` is not a promise to keep the worktrees.
42
+ - **A cancelled run says who closed it, when, and why.** All three facts already travelled
43
+ on `waiting.message` (#86); the run detail printed the status chip and dropped them. It
44
+ is one `kv` row carrying that sentence — not a second derivation, so the page cannot word
45
+ it differently from `tldrx run status`.
46
+ - **An annotated run wears a marker in the runs list** — one ✎ glyph with the note count in
47
+ its `title`, nothing more. #85 §1 asked for it and the wave declined to invent it. This
48
+ is the smallest thing that is true: no column, no badge count, no new sort key, and the
49
+ notes themselves stay on the run detail where they were. Explicitly **provisional** — a
50
+ count in a row is a design decision, and the first person with a real opinion about that
51
+ list should replace it.
52
+ - **`DASHBOARD_MODEL_VERSION` stays at 3.** `watch`, `preflight` and `keepWorktrees` are
53
+ three additions and nothing was removed; no existing field reads differently than it did
54
+ at v3. The argument for bumping is that `docs/dashboard-model.md` promised, under *What
55
+ is NOT in it*, that two named files were unread, and that promise is now void — but a
56
+ documented absence is not a field, and the number is for fields.
57
+
58
+ - **The dashboard reads `budget.yml` and `events.jsonl` (#85).** The audit that filed the issue
59
+ found five gaps with one root: `buildModel()` read `run.yml`, the phase artefacts, the Plan
60
+ artefacts and the expert files, and nothing else — so five facts a reader went looking for were
61
+ nowhere on the page, each because it lives only in one of those two files. The owner's decision
62
+ (2026-09-02) was that the model may read both, read-only and additive. All five are now drawn.
63
+ - **Operator notes** (`tldrx note`, #46) get their own section on the run detail, with the actor,
64
+ the stage or phase, and the time. **All of them**, not the last three: `tldrx run status` caps
65
+ at three because a terminal has a bottom, and a run detail page does not.
66
+ - **Free review retries** (#78/#79) and the **attempt** each story is on. Both are event-only —
67
+ the story file carries `status` and `evidence` and no counters — so a story that burned both
68
+ attempts and was granted two free re-prompts read on disk exactly like one nobody had touched.
69
+ The plan table gains an `attempts` column (`1 of 2`, against the model's new `maxAttempts`), and
70
+ a **Reopens & retries** section shows the `story.reopened` arcs with their `fix`/`attempts`
71
+ reason and the operator's note. A reopen written before the `reason` key existed reads as
72
+ `attempts`, which is the only kind that existed, rather than as a blank.
73
+ - **`budget.blocked`** occurrences are listed with the phase, both economies' numbers, and — for a
74
+ dollar refusal — the exact `tldrx budget raise` command, short-by rounding included, pinned by a
75
+ test against `raiseCommand` because the renderer is serialised into the page and cannot import
76
+ it. Deliberately **not** an attention card: the page's rule is that an alert means a run is
77
+ waiting on a person NOW, and a refusal in the log is not evidence of that.
78
+ - **The `$0.00 of $25.00` progress bar on a host run is fixed properly.** A run whose `budget.yml`
79
+ says `economy: host-tokens` is not priced in dollars at all, so `ceiling_usd` governs nothing
80
+ and the bar stated a fraction of a denominator that does not apply. Such a run now reads in
81
+ TOKENS on both screens — the runs list and the run detail share one `dashSpendText`, because
82
+ suppressing the bar alone would leave the words `$0.00 of $25.00` making the claim the bar
83
+ was — metered against `ceiling_host_tokens`, the ceiling those tokens really are judged
84
+ against, which exists in no other file. A **Budget** panel carries the per-phase ceilings, `on_exceed`,
85
+ `warn_at_pct`, `on_host_tokens_exceed` and the per-phase economy. An unset phase economy is
86
+ reported as *inherits*, never as a choice somebody made.
87
+ - **`DASHBOARD_MODEL_VERSION` stays at 3**, and the issue asked. Eight fields were added and none
88
+ removed. `spentUsd` was the one with a case to answer — a consumer reading it alone is
89
+ demonstrably wrong about a host-attended run now that the ceiling can sit beside it — but it is
90
+ computed from the same `run.yml` key, holds the same number, and has meant "METERED dollars, a
91
+ lower bound when `unmeteredTasks > 0`" since v3 put `unmeteredTasks` and `hostTokens` next to it.
92
+ A field that gained neighbours did not change meaning.
93
+ - **Neither file is opened by the model**, and the ledger is walked **once per run**. `loadRunResult`
94
+ already parsed both for every run and this file had been discarding them, so the page costs the
95
+ reads it always did; the per-story facts come out of one pass rather than from
96
+ `readReviewLedger`, which re-reads the whole ledger per story and would have made a forty-story
97
+ plan forty passes over a file already in memory.
98
+ - **Absence and damage stay graceful.** No `budget.yml` and no `events.jsonl` renders exactly as
99
+ before, with none of the new sections. An unparseable `budget.yml` costs the panel and nothing
100
+ else. A torn ledger line costs that line and **says so** on the page, because "no operator notes"
101
+ over a damaged ledger is the same lie by omission an unlisted corrupt `run.yml` was.
102
+ - Fixed on the way: an event with **no `payload` key at all** parses fine through the tolerant
103
+ `EventLog.readAll`, and reading `payload.story` off it threw a `TypeError` out of `buildModel`
104
+ and killed the live server for the whole workspace. `TldrxEvent.payload` is typed non-optional;
105
+ the type is a claim about `validateEvent`'s output, not about what is in the file.
106
+
107
+ - **The `tldrx drive` mandate carries its own preflight (#84).** Launching a cold unattended
108
+ session took SIX hand-run commands before the mandate could be pasted at all — `tldrx run
109
+ attend host`, then `tldrx run gates set` five times. Every one of them is a precondition of
110
+ the discipline the mandate exists to transfer, so an owner typing them by hand was doing the
111
+ driver's job for it, and a mandate that assumes its own preconditions only works where
112
+ somebody has already been careful. Both modes now open with a **`## Before anything: the
113
+ preflight`** section, ahead of the roles, that establishes attendedness (`tldrx run status
114
+ <run>`), the gate policy (`--json`, stage by stage) and `budget.yml` — and makes the driver
115
+ **state the ceiling it will honour** before the first turn. Any one it cannot establish is a
116
+ **refusal to start** that names the command that failed, because a driver that starts anyway
117
+ has spent money on a run whose gates it may not close.
118
+ - The two modes differ here exactly as they differ at the gate. `--unattended` may SET what
119
+ it finds wrong: `tldrx run attend host <run>`, and `tldrx run gates set <stage>:agent
120
+ --note "…"` for a stage the owner delegated, over a note **quoting the owner's own
121
+ delegation from the launch message** — so the policy change is signed by the owner's words
122
+ rather than the driver's judgement. `--attended` gets the mirror and may not: gates stay
123
+ human there, so it checks attendedness and the budget, reports a stage that is not `human`
124
+ where it was expected, and moves nothing. A test asserts the attended text contains no
125
+ `gates set <stage>:agent` at all.
126
+ - The `<run>` substitution (#75) covers the new lines like every other, `<stage>` untouched:
127
+ the pinned occurrence count moves from 5/7 to **7 attended / 10 unattended**.
128
+ - Both mandates stay inside `MANDATE_MAX_LINES` (119 unattended, 116 attended), paid for out
129
+ of the header, the driving block and the unattended gate. The sections that make the
130
+ document worth pasting — the three-role spine, evidence discipline, parking, review
131
+ calibration by stakes and budget honesty — are untouched.
132
+
133
+ - **The retro aggregate has consumers: `--json`, the reviewer prompt, and a workspace taxonomy
134
+ (#74).** `tldrx retro --all` (#64) produced exactly the dataset that issue asked for — finding
135
+ class × count × runs × one cited example — and nothing read it. The reader existed; the loop it
136
+ was meant to close was open at the far end. Three seams close it.
137
+ - **`tldrx retro --all --json`** — a stable machine shape: `{version, root, runs, contributed,
138
+ deduped, classes, trends[{cls, count, runs, example{run, kind, text, src}}], findings[]}`. A
139
+ deliberate projection rather than a dump of the internal type, with its key sets asserted
140
+ literally by a test, so adding a field is a visible act and renaming one bumps `version`.
141
+ `--json` belongs to `--all` alone: closing one run WRITES a file and has nothing to parse, so
142
+ `retro --json` on its own is a refusal rather than a stringified sentence.
143
+ - **The adversarial reviewer is fed the workspace's top three classes** before it reads the
144
+ diff, so a review starts from what this team keeps getting wrong instead of rediscovering
145
+ `test-cannot-fail` on its own, run after run. Computed by the same `mineAll` over the same
146
+ workspace, so what the reviewer is told is exactly the top of what `tldrx retro --all` prints.
147
+ Additive and absent-safe in every direction — no runs, no findings, nothing but `other`, or a
148
+ broken taxonomy file, and there is no section at all, not a heading and not a blank line.
149
+ `other` is never offered: it names no defect to look for. Mined once per `tldrx next`
150
+ invocation, so a wave of six stories pays for it once and every reviewer in that invocation
151
+ gets the same prior. The section says out loud that it is a PRIOR and not a checklist — a
152
+ reviewer handed three defect classes and no framing finds three defect classes.
153
+ - **`.tldrx/memory/finding-classes.yml`** adds classes for defects the seven built-in ones do
154
+ not name (`version: 1`, `classes:` of `{name, rules}`, 1–16 each, rules compiled
155
+ case-insensitively). Extensions are tried AFTER every built-in rule and before `other`, so a
156
+ workspace class can only ever claim a finding the built-ins left unclassified — which is what
157
+ keeps an unbounded taxonomy testable, and leaves every shipped fixture immune to whatever a
158
+ workspace writes. A file that will not load is a REFUSAL naming the file, the class and the
159
+ rule — including a rule such as `.*` that matches every text and would swallow the taxonomy —
160
+ never a silent fallback, because a rule its author believes is running and is not would make
161
+ every count a lie. The Build reviewer never fails on it: a refusal costs the prior and prints
162
+ one line, so no story loses an attempt to a YAML typo.
163
+
164
+ - **`CONTRIBUTING.md`, and a section an outside contributor can build a model provider from
165
+ (#27 companion).** #27 (a generic model-provider layer) was closed as parked, not rejected:
166
+ `TLDRX_CLAUDE_BIN` covers today's needs and a provider layer with no second provider is
167
+ speculation. So the seam is written down instead. The new file covers the contribution loop,
168
+ the four gates and what CI actually runs, the red-first rules (including "a test that cannot
169
+ fail is worse than no test", with the shipped example of one that could), and a
170
+ **Contributing a model-provider config** section: what `TLDRX_CLAUDE_BIN` does and does not
171
+ buy, `buildClaudeArgs`' exact command surface, the `stream-json` transcript contract
172
+ `AgentStream` parses and the two behaviours an adapter most easily breaks (the read cap and
173
+ the cost ledger), the result-envelope requirement with its fail-closed rule, and the five
174
+ test files a PR would have to touch. Linked from the README.
175
+
176
+ - **`tldrx questions cards [<run>]` — a parked question, as something to decide (#59).** Measured
177
+ on run `260830-ordering-inventory`, 2026-09-01: the host parked four product questions with
178
+ notes and reported them in its tl;dr. The owner's live words were *"cuales preguntas? no las
179
+ veo? por que no me guió por las preguntas Claude?"* — they were on disk, they were in
180
+ `tldrx questions <run>`, and the count was in the summary. None of that PRESENTED them. Counted
181
+ is not asked, and the gap was never the data; it was the arc from parked to answerable.
182
+ - **Three slots, each a refusal.** Two lines of context (which run and file the question is
183
+ parked in, who asked it, when, in what area) so a card pasted into chat stands up away from
184
+ the terminal. The question's OWN `Why asked:` note verbatim, `[src: …]` included — the slot
185
+ for what the binding docs already decide, quoted rather than summarised; a note that cites
186
+ nothing is FLAGGED as somebody's recollection, and a question parked with no note says so.
187
+ The file's lettered options verbatim, or a loud `NEEDS OPTIONS` marker when it carries none,
188
+ because manufacturing A/B/C would be answering the question in the act of asking it.
189
+ - **A reader, and only a reader** (owner decision 2026-09-01: no interactive loop in v1). It
190
+ opens no run, spawns nothing, records no fact, and a test asserts `questions.md` is
191
+ byte-identical across a render. Answers still flow through `tldrx answer`, and every card
192
+ prints the exact line to type. It does not extend the §2.7 grammar: every field on a card is
193
+ one the existing parser already produces.
194
+ - **No open question is a sentence and an exit 0** — and two silences are told apart, because
195
+ "this run never parked anything" and "everything here is answered" send a reader to different
196
+ places.
197
+
198
+ - **`tldrx watch arm --run <id>` — the merge detector that fires the post-merge checklist (#69).**
199
+ `watch check` (#65) answers *"what do I check now?"*. Nothing answered *"the PR just merged, go
200
+ and check it"* — the half that happens without a human remembering, which matters because the
201
+ failure #65 was filed about IS a memory failure: the owner's own CD gap, a merged branch read as
202
+ a deployed one, cost 19 destroyed records. v1 would not have caught it either, because nobody
203
+ ran it.
204
+ - **A bounded FOREGROUND poller, not a daemon** (owner decision 2026-09-01: no GitHub Actions in
205
+ v1). It reads the branch Build cut from `run.yml` — through the same `pickBranch`/`findRepos`
206
+ `tldrx ship` uses, now exported, so the two verbs cannot disagree about which branch a run
207
+ shipped — asks `gh pr view <branch> --json state,mergedAt` per repo, and prints the same
208
+ checklist the moment every PR for that branch has merged. **Three independent bounds**: a hard
209
+ deadline (`--timeout`, default 3600s, max 86400), a floor on the interval (`--interval`,
210
+ default 60s, under 10 REFUSED rather than quietly raised), and a poll cap that holds even if
211
+ the clock does not move.
212
+ - **Every refusal is a sentence, and they are different sentences.** No epic branch ⇒ `ship`
213
+ cannot have run. No PR for the branch ⇒ either `ship` has not run or the branch was never
214
+ pushed, with both commands printed. A PR `CLOSED` without merging ⇒ stop now, because waiting
215
+ for it could only ever time out. A window that expires ⇒ exit `4` and the command that
216
+ re-arms it.
217
+ - **It never pushes, opens or merges anything**, and `--execute` is deliberately not offered: an
218
+ hour-old poller must not start running the workspace's build commands the instant a merge
219
+ lands. Re-running a recorded command stays an explicit, typed decision.
220
+ - **No test in this suite runs the real `gh` or touches a network.** The unit cases drive a
221
+ recording fake transport; the one end-to-end case puts a stub `gh` first on `PATH`. The clock
222
+ and the sleep are injected, so a test covering a one-hour timeout finishes in milliseconds.
223
+
224
+ - **`tldrx plan schema` — the story/epic/waves contract, printed for a human (#71).** #48 deleted
225
+ `templates/story.md` and `templates/epic.md`, rightly: nothing read them, so nothing kept them
226
+ honest. But they were answering a real question — what shape does a story file take? — and after
227
+ #48 the only reader of the generated contract was `checkContracts.ts`, which splices it into the
228
+ Plan stage's prompt. The shape existed for the agent and not for the person writing a story by
229
+ hand, reviewing one an agent wrote, or debugging a `plan` check refusal.
230
+ - **The same bytes the agent gets.** `plan schema` renders `renderPlanSchemaContract()` verbatim,
231
+ generated from the validators the check runs, so it cannot become the second source of truth
232
+ the templates were. `--story`, `--epic` and `--waves` print one example on its own; at most one
233
+ of them, because two would make the answer ambiguous. A test copies the printed story example
234
+ straight into `validateStoryFile` and asserts it comes back clean.
235
+ - **The one verb in `plan` that resolves nothing.** No workspace, no run, no disk, no spend — the
236
+ question comes before any of that exists, and is often asked from outside a workspace entirely.
237
+
238
+
239
+ - **`tldrx drive [--attended|--unattended]` — the host/driver mandate, shipped (#63).** Every elite
240
+ run of 2026-08-31/09-01 was driven by a session carrying a hand-written playbook, and that playbook
241
+ was the framework's real quality floor. It lived in the owner's chat pastes, so a third party
242
+ inherited the CLI and rediscovered the discipline — or did not. It is now plain text the package
243
+ prints, versioned with the package, for a human to paste into the driving session or read before
244
+ they start.
245
+ - **What it carries**: the three-role protocol (developer sub-agent → a FRESH adversarial reviewer,
246
+ never the author → the host verifying BOTH in the code, not in their reports); evidence
247
+ discipline (measured / inferred / assumed labelled in the same sentence as the claim, exit codes
248
+ never read through a pipe, verification from the source, remote shas via `git ls-remote`);
249
+ parking product questions with what the docs already decide, because an open question is never a
250
+ licence to ship an unguarded write; review calibration by stakes, so a security-bearing story
251
+ gets the strongest reviewer; and budget honesty — declare a turn once, state a floor when the
252
+ records are incomplete.
253
+ - **Two modes, one spine.** The disciplines are identical — they are about how a claim is made,
254
+ not about who is watching. What differs is the GATE (`--unattended` signs over a validated
255
+ evidence note with `tldrx approve --as-agent`; `--attended` never signs and hands the decision
256
+ over) and DRIVING (who spawns). Each mode is bounded at 120 lines, asserted by a test: a mandate
257
+ long enough to skim is one nobody follows.
258
+ - **A mode is required and never guessed** (exit `1`), the same refusal `tldrx run attend` makes.
259
+ Handing an attended session the unattended text tells it to sign gates that were never its to
260
+ sign. It needs no workspace, opens no run, spawns nothing and writes nothing.
261
+
262
+ - **`tldrx retro --all` — cross-run mining of what keeps catching you (#64).** Each run left gold
263
+ nobody aggregated: a reviewer's verdict and findings in `04-build/log/<story>.md`, a fix list with
264
+ a disposition per finding, `retro.md`'s `## Build feedback`, and the reason a person typed when
265
+ they reopened a story. Across the first six real runs the same finding CLASSES kept coming back,
266
+ and nothing read more than one run at a time.
267
+ - **A trends table**: finding class × count × how many runs it appeared in × one example with the
268
+ `[src: …]` that lets a reader go and check it.
269
+ - **Seven classes, in a documented precedence**: `test-cannot-fail`, `missing-negative-control`,
270
+ `unreachable-structure`, `stale-comment`, `authorization-not-widened`, `schema-drift`, `other`.
271
+ Classification is ordered keyword rules over the finding text — no model, no scoring, no
272
+ threshold — so the same tree always produces the same table and a rule that misfires can be
273
+ pointed at. The rules are tested against fixtures in the shape the real artefacts carry.
274
+ - **Zero new state.** It writes nothing anywhere: no `retro.md`, no `practices.md`, no cache.
275
+ A test asserts the workspace is byte-identical across the call.
276
+ - **Absence is never an error.** A run with no Build phase, no retro, no events log or an
277
+ unreadable one contributes what it has and is still counted; an empty workspace is an empty
278
+ answer at exit `0`. A repeat of one finding WITHIN a run is collapsed (`retro.md` quotes the fix
279
+ list verbatim); the same finding in two runs is two occurrences, which is the point. A `refuted`
280
+ fix-list finding is read and dropped — ranking a class by disproven findings would make the
281
+ table a report on the reviewer.
282
+ - `--all` is refused (exit `1`) alongside a `<run-id>` or `--apply`, before a file is opened: each
283
+ asks for the opposite of what `--all` does.
284
+ - **`tldrx watch check` is the post-merge checklist (#65, owner decision 2026-09-01: manual command
285
+ first, `gh` detector later).** `tldrx ship` opens the PR carrying the handoff and the watcher card
286
+ lists the signals that would prove the feature works — and nothing joined the two, so
287
+ "merged ≠ deployed ≠ verified" stayed a thing a person had to remember. Remembered wrongly once, it
288
+ destroyed 19 records. `watch check` now reads the run's cards under `05-watch/watchers/` and prints
289
+ each card's `## Signal` items as a numbered checklist with `## Where`, the baseline, what broken
290
+ looks like, and the Query block, so there is one screen to work through after a merge.
291
+ - **A feature id is now OPTIONAL.** `tldrx watch check` checks every card in the run — the shape a
292
+ CI job wants; `tldrx watch check <feature>` scopes it to one and still prints the citation
293
+ verdict `check` always printed. The citation re-check is unchanged and still exits 1 on a dead
294
+ `[src: …]`.
295
+ - **Owners are DERIVED, never invented.** The watcher schema has no `owner:` key and adding one
296
+ would have been a key nothing writes (the #48 defect again). The owner of a signal here is the
297
+ repo its own citation names — `[src: api:src/Leaderboard.cs:64]` says `api` — and an item that
298
+ cites no repo says so rather than borrowing the card's.
299
+ - **Runnable means the card AND the workspace say so.** Only a `$ <cmd> → exit <n>` source whose
300
+ command `.tldrx/workspace.yml` declares, in a repo the item or the card names unambiguously, is
301
+ offered. `--execute` (off by default) re-runs exactly those, through `runDeclaredCommand` — the
302
+ same allowlist, argv-never-a-shell and timeout the stage `cmd` check uses, extracted rather than
303
+ copied — and reports the exit each gets NOW against the exit the card recorded. That is the one
304
+ thing in the framework that catches a card whose `$ … → exit 0` has quietly become an exit 1:
305
+ `resolveSrc` checks a `cmd` source's MEMBERSHIP and takes the exit code on the agent's word.
306
+ - **A `## Query` block is never runnable.** It is KQL or SQL for the console named under
307
+ `## Where`, which tldrx has neither credentials nor a client for, so it is reproduced with its
308
+ fence language and marked print-only.
309
+ - **Three refusals, told apart because they need different actions.** No `05-watch/` at all ("its
310
+ Watch stage never ran"), a Watch stage that wrote no card ("no story reached `done`"), and a
311
+ `draft` card — which is not a failure but an answer: the card's own `absent:` sources are quoted
312
+ back as what to instrument. The two empty cases exit 3, never 0; a green meaning "I read no
313
+ cards" is the failure this command exists to stop.
314
+
315
+ - **Evals v1 — five golden-transcript evals, one per stage (#26, owner decision 2026-09-01: small
316
+ v1 now).** The suite proved the harness; nothing proved a STAGE. `test/evals/` now runs each of
317
+ What, How, Plan, Build and Watch through the real facilitator against the scripted stand-in agent
318
+ and asserts that stage's output CONTRACT. About 5 seconds on top of `bun test`.
319
+ - **One stage at a time, not a chain.** Stages are sequential — Plan cannot run until How's gate
320
+ is signed — so playing four stages to reach the fifth would make every eval depend on the ones
321
+ before it, and a regression in What would turn all five red without any of them saying why.
322
+ Each eval instead opens its own run on a workflow preset holding exactly one stage, which needs
323
+ no new code: `workflowPath` already prefers `.tldrx/workflows/<scope>.yml` over the shipped
324
+ presets, and `normalisePhase` already takes the phase folder from the stage's own `phase:`
325
+ rather than from its position. Whatever the stage genuinely reads — a plan for Build, a done
326
+ story for Watch — is seeded onto the disk by the scenario.
327
+ - **Built on `learn`'s machinery, not beside it.** The stand-in `claude`
328
+ (`src/core/learn/agentScript.ts`, `learnAgent.ts`) and the toy-repo sandbox are reused as they
329
+ are; the real CLI is unreachable by the same two doors `learn` closes. No production code
330
+ changed for this.
331
+ - **Contract, not snapshot.** Nothing compares bytes. The evals assert what the FRAMEWORK
332
+ computes: the declared outputs read back off `gate.requested`, the `checks:` outcomes and their
333
+ computed detail (`checkPlan`'s branch-model line), the artifacts re-parsed with the framework's
334
+ OWN validators (`validateQuestions`, `parseWatcherCard`), and the side effects a stage exists
335
+ for — a branch cut, the story's `dod` block re-run and its exit code recorded, a `--no-ff` merge
336
+ into the epic branch with `main` untouched, and a watcher card's status COMPUTED as `verified`
337
+ when the scenario's front matter claimed `draft`.
338
+ - **A check that is `skipped` for any reason other than being write-time-only now fails an eval.**
339
+ `runCheck` falls through to `unknown check id '<x>'`, which is what a renamed or deleted check
340
+ looks like from outside — and it would otherwise read as a pass.
341
+ - **`EVALS` is load-bearing, not bookkeeping.** A coverage test asserts it names every stage
342
+ under `stages/`, so a sixth stage shipping without an eval turns the suite red and says which
343
+ one is missing.
344
+ - **Every eval was watched failing before it was trusted.** One sabotage each, listed with its
345
+ symptom in `test/evals/README.md`; the Plan one is the shape to aim for — nothing crashed, every
346
+ count still matched, and dropping a cross-epic `depends_on` was still caught because the derived
347
+ branch model changed. The same file says how to add a sixth eval, and what v1 deliberately does
348
+ not cover: failure paths, the agent gate, attended runs, and prompt QUALITY, which needs a real
349
+ model and a judge.
350
+
351
+ - **`tldrx story reopen <id> --for-fix --note "<defect>"` — a sanctioned fix round on a `done` story
352
+ (#58, owner decision 2026-09-01).** Measured on `260829-scoring-leaderboard`: S11's adversarial
353
+ review found a real defect (linkEmail succeeds, setDisplayName fails, the account is permanently
354
+ linked and the score never claimable). It was accepted, small and well understood — and the story
355
+ was already `done`, so the only choices were rejecting the whole Build stage, which destroys
356
+ fourteen good stories' closure, or fixing it outside the story machinery, which leaves an
357
+ epic-level commit with no story provenance. `done` → fix round was the missing arc.
358
+ - **No attempt is consumed.** The mechanism is the one that was already there: `story.reopened` is a
359
+ reset boundary the review ledger reads, so the approve that closed the story stops counting and
360
+ the fix runs as attempt 1 of 2 — with both turns available to it.
361
+ - **The same DoD and the same reviewer.** The story goes back to `todo` and the Build pipeline picks
362
+ it up unchanged; a fix a reviewer refuses twice blocks, exactly as the original would have.
363
+ Nothing is waved through because a human asked for it.
364
+ - **It cannot relitigate scope.** The `--note` names a defect, and `status:` is the ONLY line the
365
+ verb moves on the story file — the acceptance criteria the reviewer will judge against are the
366
+ ones that were already there.
367
+ - **One open fix round per story.** It opens on a `story.reopened` carrying the new `reason: fix`
368
+ and closes when the story is `done` again; a second `--for-fix` while one is open is refused,
369
+ naming who opened it and with which defect. The bound is read from `events.jsonl`
370
+ (`ReviewLedger.fixRound`), so it holds across processes.
371
+ - Refused when the story is **not** `done` (an unfinished story is the plain verb's job), when
372
+ `--note` is missing, and on that second round. `reason` is written on every `story.reopened` now —
373
+ `fix` or `attempts` — and an event without it predates the key and is an `attempts` reopen, the
374
+ only kind that existed.
375
+ - The plain verb's `done` refusal now points at `--for-fix` beside `reject --stage`.
376
+
377
+ ### Changed
378
+
379
+ - **tldrx is no longer sold as "lightweight" (owner decision, 2026-09-02).** The word survived from
380
+ the concept doc, where it was true of a proposal. What it now describes is five stages, a
381
+ machine-enforced `[src: …]` grammar that refuses a write it cannot resolve, an adversarial
382
+ reviewer that is never the author, a dual-economy budget ledger, and a written trail from `run
383
+ new` to `retro`, held up by a suite that measured 3093 tests across 108 files on 2026-09-02. One line replaces it everywhere, so
384
+ the npm page, the binary's own `--help`, the plugin manifest, the README and both landing pages
385
+ say the same thing: **an evidence-first, file-based AI development framework: five stages, a gate
386
+ on every one, and every claim cited or refused.**
387
+ - Changed in `package.json`, `src/cli/commands/help.ts` (the first line of `tldrx --help`),
388
+ `plugin/.claude-plugin/plugin.json`, `plugin/skills/tldrx/SKILL.md`, `docs/concept.md`,
389
+ `README.md`, and the docs site's `description`, hero and Spanish mirror.
390
+ - **The status tag moved with it, because it was already stale.** 0.4.0 shipped as `beta`
391
+ (`CHANGELOG.md`, `README.md` release table) while the badge, the README preamble, the binary,
392
+ the plugin manifest, the site's "Where this is" section, both site footers and the generated
393
+ release-notes preamble all still said `alpha`. They say `beta` now. The site's beta bar is
394
+ stated as CLEARED rather than as being worked through, which is what the release table has
395
+ claimed since 0.4.0.
396
+ - `docs/RELEASING.md` gains the thing that made this drift invisible: the `status-…` badge is a
397
+ hardcoded shields.io URL and does **not** update on its own, unlike the npm and CI badges
398
+ beside it. The README's own release paragraph now shows `--tag beta`, because `release.sh`
399
+ writes `alpha` when `--tag` is omitted.
400
+
401
+ - **A watcher card may name a HUMAN owner — optional, per item, never invented (#70).**
402
+ `watch check` derived an owner from each item's own citation: `[src: api:src/Leaderboard.cs:64]`
403
+ → `api`. That was right for v1 and stays the fallback, but it answers a different question from
404
+ the one #70 asked — which repo EMITS a signal is not who gets paged when it stops.
405
+ - **Additive, in both halves.** An optional front-matter `owner:` (validated only when present,
406
+ so every card already on disk still validates) and an optional `(owner: <name>)` annotation on
407
+ an individual item, placed BEFORE its `[src: …]` token because §2.8 makes that token the last
408
+ thing on the line. Resolution is item → card → repo-derived, and the printed line says WHICH
409
+ it is showing: `owner: alice (declared on the item)` versus the pre-#70 `owner: api`, left
410
+ byte-identical for every card that declares nothing.
411
+ - **Filled from the ledger that already names owners, or not at all.** `tldrx init` parks
412
+ "Who owns `<repo>`?" as an `ownership` question and the answer lands in
413
+ `.tldrx/memory/facts.yml`. The Watch prompt was inlining `observability` and `deploy` facts
414
+ only, so a sub-agent asked for an owner had no honest source and would have invented one;
415
+ `ownership` is now inlined, and the brief says the name may come from nowhere else.
416
+ - **A lost name is an error, not an absence.** `(owner: )` is a card that TRIED to name somebody
417
+ and lost it, so it is a shape issue on the card rather than a silent fall-through to the repo
418
+ — which is the exact substitution the issue is about.
419
+
420
+ - **`docs/guide/08-cli-reference.md` documents `note` and `ship` (#72).** Every command in `COMMANDS`
421
+ had a `## tldrx <cmd>` heading except three; #55 wrote `plan`'s, and these are the other two —
422
+ `ship` being the command that opens the PR at the end of a run, and `note` how an operator records
423
+ something against a stage. `DOCUMENTED_SUBCOMMANDS` in `test/cli.test.ts` grew to cover them, which
424
+ is what keeps each of these gaps a red test rather than a note. Only `hook` is left out, and
425
+ deliberately: its seven scripts are documented as the one `<script>` slot `USAGE_SPELLINGS` already
426
+ records as a spelling.
427
+
428
+ - **`tldrx watch`'s one-line summary says checklist.** It read "List and re-check the watcher cards a
429
+ run produced", which described half of what `watch check` is for and disagreed with the `--help`
430
+ text #65 updated.
431
+
432
+ - **`tldrx ship` opens one PR PER REPO when the branch is in more than one (#66, owner decision
433
+ 2026-09-01).** Since #57 a chained multi-repo run cuts ONE integration branch, `epic/<run-id>`,
434
+ with the same name in every repo — so `ship` found it in several, every time, by construction, and
435
+ refused with `pass one: --repo <name>`. The last step of every such run was typing the same command
436
+ once per repo and remembering which ones had already gone through.
437
+ - Same handoff as the body of every PR, the repo name in the title, and every URL listed at the
438
+ end. Each PR opens against **that repo's own** `default_branch`.
439
+ - **One repo is byte-identical**, down to the four lines it prints and the number of processes it
440
+ spawns: the `gh pr list` probe below exists for the multi-repo case and never runs when there is
441
+ only one. The single-repo lines are asserted as exact strings, not substrings — "we did not
442
+ change the common case" is not a claim a `toContain` can make.
443
+ - **A partial failure names both sides.** PR 2 of 3 failing still opens PR 3, and the report lists
444
+ the repos that succeeded with their URLs and the repos that failed with the reason (exit `2`).
445
+ Aborting on the first failure would leave a half-shipped run and no statement of which half.
446
+ - **Re-running is safe.** Before creating, each repo is asked whether an open PR for the branch
447
+ already exists (`gh pr list --head`); one that has is skipped and listed. So the fix for a
448
+ partial failure is `tldrx ship` again and nothing else. A `gh` that fails or answers with
449
+ non-JSON is treated as "there is none", so a transient error can never silently turn a real ship
450
+ into a skip.
451
+ - `--repo` still narrows to exactly one, and `ship` still never pushes: an unpushed branch in one
452
+ repo is that repo's failure line, naming its `git push`, while the other repos' PRs still open.
453
+
454
+ ### Fixed
455
+
456
+ - **A training pass with nothing to read is no longer paid for (#101).** `runTraining` had
457
+ exactly one "nothing to work on, refuse before the money" check and it guarded one half of one
458
+ mode: `nothingToMineRefusal` fires only for a ROLE expert with zero minable runs. The CODE pass
459
+ was pushed unconditionally and nothing ever looked at `selection.inlined.length`, so an expert
460
+ whose `## Domain` matched no file on disk spawned a sub-agent, was shown no code at all, and
461
+ wrote a knowledge file about nothing at full price. Measured on the training fixture before the
462
+ fix, with a domain of `src/does-not-exist/`: **`{ code: 0, costUsd: 0.37, wroteKnowledge: true }`**
463
+ — a successful run, priced, from zero input. Live cost, from #94's thread: two near-empty
464
+ trainings at $0.82 each whose "code sweeps found nothing in-domain".
465
+ - **The choice, named: SKIP a dead pass, REFUSE only when none survives.** That is
466
+ `nothingToMineRefusal`'s own idiom — refuse when there is nothing to train from — without
467
+ denying a `--mode full` run that still has one real pass. The skipped pass's reason goes to
468
+ stderr before the money, exactly like the #96 pre-start line, and rides back in
469
+ `TrainOutcome.warnings`; `--prepare` prints it too, and its `N sub-agent(s)` line was already
470
+ derived from the surviving passes so it stays honest by construction.
471
+ - **`roleTraining.ts:79`'s uncovered arm is closed.** Its guard reads
472
+ `if (!isRole || minedFiles > 0) return null`, so a NON-role `--mode full` run against zero
473
+ minable runs was never refused: it spawned a second sub-agent to write
474
+ `- none [src: absent:tldrx-work]` — no evidence, no level, full price. That case is now a
475
+ skip. The role case keeps its own, better-worded refusal untouched, because its runs pass is
476
+ the only pass it has.
477
+ - **It is NOT the #96/#98 preflight and does not touch it.** Underfunded and empty are different
478
+ refusals with different remedies, so they are different checks that never consult each other.
479
+ In particular a skipped pass does not re-divide the ceiling: the share `trainPreflight` priced
480
+ and printed is what the surviving sub-agent gets, and the skipped one is simply not spent.
481
+ - **Exit `1`, deliberately.** `2` is this codebase's MONEY refusal (the `MIN_TRAIN_USD` floor and
482
+ the #96 preflight). Every "you asked for something with nothing behind it" refusal in
483
+ `expert train` is `1` — `missingAreaRefusal`, `lightModeRefusal`, and `nothingToMineRefusal`,
484
+ this check's literal sibling, which `test/training.test.ts` pins as "refused (exit 1)".
485
+ - **One doc correction found while checking that.** `docs/guide/08-cli-reference.md` claimed
486
+ `--mode light` on a role expert exits `2`. It exits `1`: `lightModeRefusal` returns
487
+ `EXIT_USAGE`, and `src/cli/exitCodes.ts` defines `EXIT_USAGE = 1`.
488
+
489
+ - **`expert create` now yields an expert that can actually be trained, and states the `## Domain`
490
+ grammar it will be read under (#94).** Live 2026-09-02: `tldrx expert create discoverer` printed
491
+ *"no areas — every level starts at 0"*, and `tldrx expert train discoverer --area discoverer`
492
+ answered *"has no area (areas: none)"*. Neither sentence said HOW to add one; no flag or
493
+ subcommand existed; the sanctioned path — a block in `competencies.yml` — was discoverable only
494
+ by reading the source. Then the same gap cost money twice more: the hand-written `## Domain`
495
+ bullets came out WORKSPACE-relative (`Scavtopia.Workflows/src/…`) because nothing documents the
496
+ grammar, so **a $2.10 full training earned zero evidence — all 13 of its code citations read
497
+ `outside domain`** — plus two near-empty $0.82 runs whose code sweeps found nothing in domain.
498
+ - **`--area <id>` seeds the first competency area, and `--title <text>` names it.** The title is
499
+ not decoration: light mode greps the words of the area title to choose which files the expert
500
+ is shown (`training/selectFiles.ts`), so `create` prints the default title it chose and names
501
+ `--title`. An area id obeys the same slug rule the expert name does — it is a filename
502
+ (`knowledge/<area>.md`) and half of a copy-pasteable `--area` argument.
503
+ - **Both refusals name the FILE and print the block.** `expert train` on a missing area now
504
+ answers with `.tldrx/experts/<name>/competencies.yml` and the five keys to paste (`id`,
505
+ `title`, `level: 0`, `train_prompt`, `evidence: []`), from one helper both the CLI and
506
+ `runTraining` use, so the two paths cannot drift. `create` with no area says the same thing.
507
+ - **`create` writes the front-matter `repos:`**, read off `.tldrx/workspace.yml` (`repos: []`
508
+ when there is none, rather than a guess). `expert.md` had no such key at all, so a
509
+ hand-created expert declared nothing about which repos its `## Domain` bullets are relative
510
+ to — and that is exactly the thing the bullets are relative to.
511
+ - **The created `expert.md` states the grammar in its own `## Domain` section**: bullets are
512
+ repo-RELATIVE with no repo prefix, a whole-repo claim is `` - repo `api` ``, citations arrive
513
+ as `repo:path:line` so `api:src/Checkout/Cart.cs:12` is matched by `` `src/Checkout/` `` and
514
+ **not** by `` `api/src/Checkout/` ``. It is written as prose, deliberately: a worked example
515
+ written as a BULLET would itself be parsed as a declared domain path, and an expert that
516
+ silently claims `api/src/Checkout` puts every real citation outside its own domain — the bug,
517
+ not the fix. Pinned by a test that the created file still parses to zero domain paths.
518
+ - **The `outside domain` warning names the grammar FIRST** when the cited path would match with
519
+ one leading segment dropped from either side — the bullet carrying a repo prefix it should
520
+ not, or the citation being workspace-relative where a `repo:path:line` was wanted. It says the
521
+ measured fact (`X` without its first segment DOES contain this path), not a diagnosis.
522
+ - **And naming another expert no longer reads as exclusivity.** *"train that expert on it
523
+ instead"* was measured false: overlap between domains is legal, and the hint fires whenever
524
+ this expert's own paths miss. It now reads *"also declares a domain that contains it, so it
525
+ may be the better home for this one (overlap is legal: this fired because none of `<expert>`'s
526
+ own paths match)"*.
527
+ - **`init/loadWorkspaceFile.ts` reads `.tldrx/workspace.yml`'s name from `core/paths.ts`**
528
+ (`PROJECT_WORKSPACE_FILE`, the constant #92 added) instead of from `init/runInit.ts`;
529
+ `runInit` keeps exporting `WORKSPACE_FILE` under that name, aliased to the same string, so no
530
+ import site changed. `loadWorkspaceFile` is the small leaf several commands read the repo list
531
+ through, and taking that one constant off `runInit` dragged the whole of `init` — detection,
532
+ map providers, the MCP probe — into every module that read it: `dist/hooks/session-start.js`
533
+ went 37,937 → 54,467 bytes and blew the 50 KB entry-point cap `test/build.test.ts` enforces.
534
+ It is back to 37,937.
535
+ - Documented in `docs/spec.md` §2.3 and §2.8, `docs/guide/04-experts.md`,
536
+ `docs/guide/08-cli-reference.md`, `docs/guide/09-troubleshooting.md` and both docs-site expert
537
+ guides — the grammar had never been written down anywhere outside `expertDomain.ts`.
538
+
539
+ - **`release.sh` ran the gate AFTER pushing the release commit to `main` (#100).** The order was
540
+ commit → push `main` → `release-check.sh` → tag → push tag, so every red item the gate has —
541
+ tests, typecheck, build, the `Bun.*` seam grep, "tag already exists", "already on npm", "tree
542
+ not clean" — landed a `release: X.Y.Z` commit on `origin/main` carrying a **dated** CHANGELOG
543
+ heading and a **dated** README row, with no tag behind it. That is precisely the half-released
544
+ state checklist item 4 exists to prevent, and it costs a revert commit on `main` or a
545
+ hand-repaired CHANGELOG to undo. The order is now commit → **gate** → push `main` → tag → push
546
+ tag: a red gate leaves `origin/main`, the tags and npm untouched, the whole of the damage is one
547
+ local commit, and the script says so and prints the one command that drops it.
548
+ - **The gate did not have to be weakened to move it.** Of its items only "in sync with
549
+ `origin/main`" assumes the push already happened; run against an unpushed release commit it
550
+ would be permanently red, which is the trap in the naive reorder. `release-check.sh` takes a
551
+ new `--pre-push` that restates that one item as **"`origin/main` is HEAD's parent"** — the same
552
+ assertion (nobody moved `main` under you, nothing but the release commit is unpushed) for a
553
+ tree that has not pushed yet, and one notch stronger, since it also refuses a second unpushed
554
+ commit. Items 1–3 already needed the edits, which exist by then; "working tree clean", "on
555
+ main", "tag does not exist", "not on npm", typecheck, tests, build and the seam grep are
556
+ untouched and all still run. **No flag, no default, no CI path changed**: bare
557
+ `release-check.sh` (the `release-gate-hook.sh` PreToolUse deny) and `--ci` (publish.yml)
558
+ behave exactly as before, and there is a test for each.
559
+ - **`sed -i ''` was macOS-only**, which is why this path had never had a test: measured against
560
+ GNU sed 4.9 in `debian:stable-slim`, `sed -i '' -E …` exits **2** with `sed: can't read
561
+ s/^## …/: No such file or directory`, so `release.sh` could not run on the ubuntu CI runner at
562
+ all. Both substitutions now go through a temp file outside the tree and are `cat` back (inode,
563
+ mode and a clean working tree preserved) — byte-identical output on BSD sed, and it runs on
564
+ GNU sed, which is what lets `test/release-gate-order.test.ts` hold the ordering in CI. The new
565
+ file runs the real two scripts against a real bare "origin" with only `bun` and `npm` stubbed,
566
+ and reads the ordering off the shas the gate actually ran against: pre-fix, HEAD and
567
+ `origin/main` were EQUAL at gate time; now they differ by the release commit.
568
+
569
+ - **The prose was validated against the binary, top to bottom, and a lot of it was false.**
570
+ The last docs QA (3ee3723) was a release ago; `watch check`/`watch arm`, `update` and
571
+ its notice, the `drive` preflight, `plan schema`, `questions cards`, `story reopen --for-fix`,
572
+ the host-token economy and the dashboard's new sources have all landed since. Every claim in
573
+ `README.md`, all thirteen English pages, all thirteen Spanish pages and `CONTRIBUTING.md` was
574
+ cold-read for lies first, then checked against `tldrx <cmd> --help` and the source. What was
575
+ found was not drift at the edges — most of it was load-bearing.
576
+ - **A flag that does not exist.** The README's cost section explained what `--max-budget-usd`
577
+ does. That is the flag tldrx passes DOWN to `claude`, not one a user may type: `tldrx next
578
+ --max-budget-usd 5` answers `unknown flag`. The user-facing flag is `--max-usd`.
579
+ - **"Every run-targeting command exits `2`" was wrong twice**, in the README, the CLI reference
580
+ and the FAQ. `tldrx cost` refuses at exit **1** (`cost.ts` returns `EXIT_USAGE`, and its own
581
+ `--help` never lists a 2), and `tldrx run status` does not refuse at all — it lists every open
582
+ run and exits **0**, which is the whole point of it, since it is the screen you read to find
583
+ the id the others are asking for. All three pages also named five commands as taking a
584
+ positional `<run>`; the real split is that most take either form, `replay` and `retro` take
585
+ the positional only (`--run` there is an unknown flag, measured), and seven take `--run` only.
586
+ - **A documented command that silently creates a directory where you asked for a file.**
587
+ `tldrx dashboard --static --out ./somewhere/page.html` was in the reference. `--out` is a
588
+ directory and the filename is not negotiable (`writeStatic.ts` joins `index.html` and
589
+ `mkdirSync`s the path), so that line makes a directory literally named `page.html`.
590
+ - **A required tool the Quickstart did not name.** `env.yml` declares four `required: true`
591
+ tools; the install page named Node, `git` and `claude` and never mentioned **Bun ≥ 1.3**, so a
592
+ reader who followed it exactly got `tldrx doctor` exit 1 and no explanation.
593
+ - **The `tldrx learn` chapter list was stale AND swapped** on both language Quickstarts: 6 and 7
594
+ are `the agent gate` and `attended`, and the pages had two other titles in the other order —
595
+ so `--chapter 7` sent a reader looking for the agent gate to the wrong chapter.
596
+ - **Counts that had grown.** The `.gitignore` block `init` writes excludes **eight** paths, not
597
+ the five the README listed. The `[src: …]` grammar has **eight** kinds, not the seven the
598
+ evidence table showed — `aidlc:` was missing while the page's own next paragraph said
599
+ "eight". The What, How and Plan stages each declare `questions.md` as an output and Plan also
600
+ declares `budget.yml`; the stages table showed none of them, which matters because the auto
601
+ gate is measured on exactly that file. `tldrx init` seeds **three** kinds of expert — role,
602
+ stack and **domain** — and both expert sections said two.
603
+ - **Twelve `workflows/*.yml` said the auto gate has "the five spec §5 conditions"** and listed
604
+ five. `evaluateAutoGate` has had **seven** since the stories and boundary conditions landed.
605
+ - **The `tldrx drive` mandate's preflight (#84) was absent from every page that enumerates what
606
+ the mandate carries**, in both languages, and the English driving guide still taught the
607
+ three-command recipe the preflight replaced.
608
+ - **`CONTRIBUTING.md` overstated the ref guard and understated the lock.** The guard refuses a
609
+ ref move while ANOTHER invocation holds the lock — the holding run's own git children carry
610
+ `MW_LOCK_TOKEN` and pass, which is what lets the wave commit and push through its own guard;
611
+ as written it would have refused the merge it exists to protect. `merge-wave.sh`'s seven exit
612
+ codes were documented nowhere, in a file whose own rule is to read exit codes. The lock's wait
613
+ is bounded (`MW_LOCK_WAIT_S`, default 3600 s, then exit 6) and `CLAUDE.md` implied it was not.
614
+ - **The Bun/Node seam rule was stated more broadly than it is enforced**, in `CONTRIBUTING.md`,
615
+ `CLAUDE.md` and `docs/RELEASING.md`: the grep scans `src` only, and `scripts/build.ts` calls
616
+ `Bun.build` for a living. A contributor reading it literally files a bug against the build.
617
+ - **`publish.yml` does not "run the same check".** `release-check.sh --ci` skips items 4 and 5
618
+ entirely; the seam check runs in no CI workflow at all. Said plainly now, in both the README
619
+ and `docs/RELEASING.md`.
620
+ - **Four translated strings that had to stay English** in the Spanish mirror: two `--note`
621
+ samples, the `## Qn · Title` heading the §2.7 parser matches on, and `$0.00 of $25.00`, which
622
+ is what the dashboard actually renders.
623
+ - Fixed on the way: the `0.5.0` CHANGELOG section had grown **two `### Fixed` groups** and had
624
+ two additions (Evals v1, `story reopen --for-fix`) filed under `### Removed` — an artefact of
625
+ unioning sibling branches. One group per kind now, in the order `0.3.0` used, with every entry
626
+ byte-identical and only the duplicate heading gone.
627
+ - Two defects found while checking and **filed rather than fixed**, both outside this pass:
628
+ [#99](https://github.com/ederwii/tldr-experts/issues/99) `test/merge-wave.test.ts` asserts over
629
+ a shared `TMPDIR`, so a sibling process fails it (measured: red under load, green alone, and
630
+ green for the full suite with no sibling), and
631
+ [#100](https://github.com/ederwii/tldr-experts/issues/100) `release.sh` pushes the release
632
+ commit to `main` before it runs `release-check.sh`.
633
+
634
+ - **`expert train` says what it is about to spend, on which model, BEFORE it spends it (#96).**
635
+ Live 2026-09-02: `tldrx expert train discoverer --area discoverer --mode full` inherited the
636
+ claude CLI's last-used model — `fable-5`, a premium tier — and ran against the default ceiling.
637
+ Full mode splits that ceiling between its two sub-agents, so the code pass was handed $1.00. It
638
+ died with `Reached maximum budget ($1)` at 54 s, **$1.31 spent and recorded, nothing written to
639
+ `competencies.yml`.** Three defaults compounded and not one of them was said out loud.
640
+ - **A pre-start line names the model, its tier, and where the name came from** — `--model`,
641
+ `$ANTHROPIC_MODEL`, or a `model:` key in `.claude/settings.local.json` /
642
+ `.claude/settings.json` / `~/.claude/settings.json`: `model claude-fable-5[1m] (premium,
643
+ inherited from your claude CLI via ~/.claude/settings.json) — pass --model to override ·
644
+ --mode full · $3.00 across 2 sub-agent(s), $1.50 each`. When nothing on the box says which
645
+ model the CLI will pick, the line says THAT rather than inventing a tier.
646
+ - **A refusal — exit `2`, nothing spawned, nothing spent — when the share cannot fit and the
647
+ ceiling is the DEFAULT one.** The test is arithmetic, not a category: does the money one
648
+ sub-agent gets reach what one pass on that tier costs? Measured full trainings run
649
+ **$1.21–$1.60 end to end on a mid model** (two `training.jsonl` lines on `aparece-platform`,
650
+ `docs/audits/2026-08-29/experts-knowledge.md` §E, plus the top of the band in #96), so one
651
+ pass is ~$0.70 mid and ~$1.76 premium (opus lists at 2.5x sonnet). Full mode's $1.50 a pass
652
+ does not reach it. The refusal names both remedies — `--model sonnet`, or `--max-usd <n>`.
653
+ - **An explicit `--max-usd` is never refused.** The operator looked at the number; that is the
654
+ whole decision this check exists to ask for. It warns in one line and proceeds.
655
+ - **The full-mode default `--max-usd` is now $3.00**, up from the $2.00 light mode still uses,
656
+ because full mode pays for two sub-agent passes and the one repair round a rejected knowledge
657
+ file earns comes out of the same share. $1.50 a pass is ~2x the measured per-pass midpoint.
658
+ Deliberately not scaled higher for a premium model: `--max-budget-usd` is a stop after the
659
+ turn, not a cap (spec §2.6.1 — a $1.50 ceiling has realised $5.15), so a bigger default cannot
660
+ make a premium turn affordable, only more expensive to lose. `budget-gate` prices `--mode
661
+ full` at $3.00 to match.
662
+ - Salvaging the partial evidence a budget death throws away is a design call and was NOT built;
663
+ the question is asked on #96.
664
+
665
+ - **`expert train --prepare` carries that check into the bundle (#98).** The check above was wired
666
+ to the spawning path only, and `--prepare` spawns nothing — but it writes the ceiling into
667
+ `pending.json` (`max_budget_usd: 1.50`) and into the prompt text (`Ceiling for this sub-agent:
668
+ $1.50`), and a host session then spends against it. Measured on `b5d59c5`: a full `--prepare` with
669
+ a premium model inherited from `~/.claude/settings.json` printed `$1.50 ceiling each`, returned
670
+ `preflight: null`, and wrote two bundles saying `model: null` — the same trap, one command later,
671
+ on the host's money.
672
+ - The model line and any warning now go to **stdout with the prepared block** and into an optional
673
+ **`preflight`** key on each bundle's `pending.json`. Absent when there is no warning, so an
674
+ unremarkable bundle stays byte-identical to the one this command has always written.
675
+ - **What it may CLAIM differs by half.** Headless, tldrx spawns `claude` with no `--model`, so the
676
+ CLI's default is a prediction about a process this code starts: the refusal stands. On
677
+ `--prepare` an **explicit `--model`** is an instruction written into the bundle and is refused
678
+ the same way (exit `2`, no bundle written); an **inherited** model only warns, naming both
679
+ remedies — refusing a bundle over a settings-file key, for a session tldrx does not control,
680
+ would be asserting more than is known. `--commit` says nothing: that money is already spent.
681
+
682
+ - **`test/merge-wave.test.ts` no longer fails a wave over another wave's log directory (#95, #97).**
683
+ `merge-wave.sh` writes its logs to `${TMPDIR:-/tmp}/mw-$$`, and the test that asserts a green run
684
+ cleans up after itself diffed a listing of the machine's SHARED tmpdir. Every wave on the box
685
+ writes there — this file's other tests, every sibling agent's wave, and the real merge wave whose
686
+ `bun test` is running this very file — so the assertion was about ONE run's cleanup and measured
687
+ the whole machine. It went red twice on trees whose diff touches neither the script nor its tests:
688
+ **#95**, on a sibling's DELIBERATELY KEPT red-wave log (`mw-35458`, carrying another run's
689
+ `poison.txt` merge — merge-wave keeps a failed run's logs on purpose, "every FAIL above names the
690
+ directory it kept"); and **#97**, on a CONCURRENT invocation's LIVE `mw-15412`, twelve minutes into
691
+ #90's wave, with `main` left at an unpushed merge commit. Both cost a full re-gate, and the same
692
+ trees passed standalone.
693
+ - **Every invocation now gets a private `$TMPDIR` inside its own sandbox**, so "did THIS run clean
694
+ up after itself" has an answer that does not depend on what else the box is doing. Per
695
+ invocation, not per sandbox: this file deliberately runs two waves at once. The scan is read only
696
+ after the run has exited, so anything left in that root is a genuine leftover and no liveness
697
+ rule has to be guessed at — a concurrent wave's live `mw-<pid>` is, correctly, invisible from
698
+ there.
699
+ - **What the test asserts about merge-wave is unchanged.** A green run that stops removing its log
700
+ directory still fails it, and a red run that stops keeping the one it names still fails it — both
701
+ re-measured by mutating `scripts/merge-wave.sh` and watching the assertions fire.
702
+ - **A red run's kept logs now leave with the sandbox.** They were being written to the machine's
703
+ tmpdir and never removed — 1300 `mw-*` directories had accumulated there by the time #95 was
704
+ filed, including the one it tripped on.
705
+
706
+ - **A story's first attempt is dispatched at what the plan priced it, not at that figure halved
707
+ again (#91).** `03-plan/budget.yml` has been read since 2026-08-30, but the price it carries was
708
+ divided by the worst case ONE story can be asked for — `MAX_ATTEMPTS × (1 + REVIEWER_SHARE)` =
709
+ 2.5 — before a single attempt had run. Measured on run `260901-leaderboard-v2` (finding F-4):
710
+ Delivery priced S2 at $2.10 of a $3.85 Build stage and the developer was dispatched under
711
+ **$0.84**. A deliberately-atomic large story starved on the one attempt that mattered while
712
+ trivial ones carried slack, and the plan's own measured pricing — the whole point of writing the
713
+ file — was thrown away in the arithmetic that read it.
714
+ - **`developerPriceDivisor(attempt)`** now decides it. Attempt 1 is the pass Delivery priced and
715
+ gets `price / (1 + REVIEWER_SHARE)` — the whole price less the reviewer's derived quarter, so
716
+ S2's ceiling goes $0.84 → **$1.68**. Attempt 2 is a contingency nobody priced and keeps the
717
+ pre-#91 figure, `price / (MAX_ATTEMPTS × (1 + REVIEWER_SHARE))`. No attempt is ever handed less
718
+ than it was handed before.
719
+ - **The even split is untouched.** A plan with no `budget.yml`, one that does not validate, one
720
+ priced in `host-tokens`, and any story the plan did not name all still get
721
+ `stage / (stories × attempts × 1.25)`, pinned by tests that were green before this change and
722
+ are green after it.
723
+ - **What this costs, stated.** The worst case one PRICED story can be asked for goes from
724
+ `0.8 × price` to `1.2 × price`. The phase ceiling is metered once, at stage entry —
725
+ `runNext.runExecutor` skips the brake while a stage is `running` — so nothing re-checks the
726
+ envelope between two spawns of the same headless `runAll`; that is the window
727
+ `REVIEWER_FLOOR_USD` already opens by design. `priceScale` still holds the sum of the declared
728
+ prices inside the stage, and `remainingWork` still clamps the brake's estimate to the stage's
729
+ own price, so the brake can never refuse more often than it used to. The measured REMAINDER of
730
+ a story's price would be tighter on the second attempt, and is recoverable (`agent.result`
731
+ carries `key` = the story id and a row-level `cost_usd`) — it is not read, because the
732
+ budget-gate hook's `remainingWork` would have to read the same ledger on its hot path to stay
733
+ in step, and the worst case is `0.8 + 0.4` either way.
734
+ - **`remainingWork` mirrors the schedule, turn by turn.** `RemainingStory` gains
735
+ `developerCapsUsd` — one cap per turn still to dispatch, in the order they run — because
736
+ `cap × turns` stopped being the truth. The turns still to run are the LAST of the story's run
737
+ of attempts, so a story with one attempt behind it is priced as the attempt 2 it is about to
738
+ become. `renderRemainingWork` prints `dev $1.20 ×2` while the turns cost the same and
739
+ `dev $3.60 + $1.80` once they do not, so the total on the line still adds up. A test asserts
740
+ the mirrored divisor equals the executor's own, as it already did for the three constants.
741
+
742
+ - **The merge lock now has something to say about raw git in the shared checkout (#89).** The
743
+ lock serialises merge-wave INVOCATIONS; it never serialised git. Measured 2026-09-02: while
744
+ agent A's gates were running, agent B typed `git reset --hard origin/main` into the same
745
+ shared checkout — reflog `reset: moving to origin/main` — and A's merge commit stopped being
746
+ reachable from `main` mid-gate. #44's gated-HEAD assertion fired for the first time in anger
747
+ and refused to push. The aftermath was caught; the damage was not prevented.
748
+ - **`scripts/merge-guard.sh`, installed as a `reference-transaction` hook on every
749
+ merge-wave run.** That is the only hook git will let abort work in progress: it fires for
750
+ every reference update and, in its `prepared` state alone, aborts the whole transaction on
751
+ a non-zero exit. Measured on git 2.50.1 with the guard refusing — `git reset --hard`,
752
+ `git checkout -B main`, `git merge --no-ff`, `git commit` (including `--no-verify`, which
753
+ does **not** bypass it) and `git update-ref` all exit 128 with `fatal: ref updates aborted
754
+ by hook`, and **the ref does not move**. There is no pre-reset, pre-checkout or
755
+ pre-merge-anything hook; this is the strongest mechanism git offers without a daemon.
756
+ - **Where the line honestly lies, because it is not where it looks.** `git reset --hard`
757
+ writes the WORKING TREE before it opens any ref transaction — measured: the file content
758
+ had already changed to the target commit's while HEAD still pointed at the old one. So the
759
+ guard saves the **commit**, which is what #89 lost, and cannot save the checked-out files;
760
+ a wave whose worktree is clobbered mid-gate still gates a tree nobody meant it to, and its
761
+ gated-HEAD assertion will not fire, because HEAD is exactly where it left it. `git merge`
762
+ is the better case — refused before the worktree is touched at all. And a hook in a
763
+ checkout is bypassable by anyone willing to: `git -c core.hooksPath=…`, deleting the hook,
764
+ or exporting `MW_LOCK_TOKEN`. It is built against accidents, not intent.
765
+ - **The lock is the sentinel; the marker is what that lock looks like to a human.**
766
+ `merge-wave.sh` writes `.MERGE-WAVE-IN-PROGRESS` at the root of the shared checkout naming
767
+ the branch, the owning pid and host, and the way back in. Gitignored, so a wave cannot trip
768
+ its own dirty-tree guard; removed by `release()` on every path out, INT and TERM traps
769
+ included. The guard keys on the LOCK rather than on the marker, because only the lock
770
+ carries an owner and only an owner can be tested for death — a SIGKILLed wave must not
771
+ leave a file behind that wedges the checkout.
772
+ - **Scope, so an agent's own work is never in the way.** In the shared checkout every ref
773
+ update is refused while a foreign wave holds the lock. From a linked worktree only
774
+ `refs/heads/main` is — refs live in the common dir, so `git update-ref refs/heads/main`
775
+ typed in a worktree destroys the wave just as thoroughly (measured: it succeeds, unguarded).
776
+ The holder's own git is recognised by an exported token written inside the lock, so
777
+ merge-wave cannot deadlock itself, and a lock whose owner is dead is ignored exactly as the
778
+ waiting loop already ignores it — `scripts/merge-lock.sh` is that one vocabulary, shared by
779
+ writer and guard so the two cannot drift apart.
780
+ - **The state name git passes is not portable, and assuming it was shipped a red CI.** macOS
781
+ git 2.50.1 calls the abortable state `prepared`; the Linux runner's git calls it something
782
+ else (`fatal: in 'preparing' phase, update aborted by the reference-transaction hook`, run
783
+ 33589554234 on `e0e76a2`). The guard's `*)` arm printed a usage error and exited 2, so on
784
+ that machine every ref update in every sandbox was refused and the whole merge-wave suite
785
+ went red at once while macOS stayed green — #49's failure shape exactly. It now recognises
786
+ `committed` and `aborted` and treats **everything else** as the prepare state, which cannot
787
+ repeat the blanket refusal: git honours the exit code in the prepare state only, so an
788
+ unrecognised name is either handled correctly or ignored. The test asks git which states it
789
+ passes rather than hardcoding them, and additionally probes the names this machine does NOT
790
+ use — the only way the portability property is testable anywhere.
791
+
792
+ - **And the convention it only approximates is now written down**, verbatim and identically,
793
+ in `CONTRIBUTING.md` and `CLAUDE.md`: agents touch the shared checkout ONLY through
794
+ `scripts/merge-wave.sh`, every other piece of work happens in their own worktree, and never
795
+ `git reset --hard` or `git checkout -B main` in the shared checkout. A test asserts both
796
+ files still say it.
797
+
798
+ - **A cancelled run is no longer told to retry itself (#86).** `tldrx run cancel` is the way to
799
+ close a run whose stage FAILED — that is the case `run.cancelled` exists for, and it is a
800
+ run-level field precisely so the stages keep their failure, "which is history, not state".
801
+ `deriveRunStatus` read that decision first; `waitingFor` could not read it at all. It is typed
802
+ against `WaitingRun`, the smallest shape both `RunFile` and the dashboard's tolerantly-read
803
+ `RunDocument` satisfy, and that shape had `cursor` and `phases` and nothing else — so the answer
804
+ came from the status of the stage at the cursor, which a cancellation deliberately leaves
805
+ `failed`. A run somebody had closed reported `waiting.kind: failed` and
806
+ `"… FAILED — retry: \`tldrx next\`"`, counted as movable, and could wear `← next` on the
807
+ dashboard. Both screens, because both have read the same derivation since #60.
808
+ - `WaitingRun` grows an optional `cancelled`, `RunDocument` projects it, and `waitingFor` reads
809
+ it FIRST — before the cursor checks, in the same order and for the same reason
810
+ `deriveRunStatus` does. A ninth waiting kind, `cancelled`, joins `WAITING_KINDS`; it is not in
811
+ `MOVABLE_KINDS`, so `tldrx status` stops offering the run and the dashboard raises no card.
812
+ - The message names **who closed it, when, and their note** — three facts that were dropped
813
+ everywhere, on the one screen that exists to say what a run needs. It offers no command:
814
+ `tldrx next` on a cancelled run already advances nothing, and printing it is what made an
815
+ operator think there was something left to do.
816
+
817
+ - **An out-of-order `--commit` no longer leaves a `stage.started` behind (#87).** `runExecutor`
818
+ stamped the stage `running` and appended `stage.started` on its way in, before the executor had
819
+ had a chance to say anything. #82 restores the stage's STATUS when that executor turns out to be
820
+ refusing a sequencing mistake, so `run.yml` comes back byte for byte — but `events.jsonl` is
821
+ append-only truth and the line could not be unwritten. The live run `260901-leaderboard-v2` left
822
+ one at 2026-09-02T00:36:37Z: a `stage.started` with no matching `stage.done` or `stage.failed`,
823
+ which `renderReplay` and anything counting starts read as a start. A `--commit` never STARTS a
824
+ stage — it settles a cycle a `--prepare` started, and on the ordinary commit the stage is
825
+ already `running` and no event was emitted anyway — so it no longer emits one at all. The
826
+ `markRunning` STAMP stays, keeping the original `started_at`, so a `--commit` that does settle a
827
+ cycle on a stage something demoted still finishes through. The refusal now precedes the event by
828
+ construction: there is no event. Pinned by measuring the WHOLE log, not a `stage.started` count.
829
+
830
+ - **An unreadable `result.json` is refused like an absent one, loudly (#88).** Owner decision,
831
+ 2026-09-02. #82 split `PendingError` into `absent` (the host has not written the file) and
832
+ `unreadable` (the host wrote it and got it wrong), and left the second failing the stage. But
833
+ nothing was attempted there either — no sub-agent ran, no cent moved, no branch changed — and
834
+ the fix is the same single command: rewrite the file, run `tldrx next --commit` again. Failing
835
+ it actively OBSTRUCTED that fix, because it demoted the stage out of `running`, and the
836
+ phase-budget gate is skipped exactly when a stage is `running` — which is how #82's live run
837
+ took a `budget.blocked` it had not earned. A host that fat-fingers its JSON paid the same tax.
838
+ It is #79's model — FORM never costs an attempt, CONTENT/WORK always does — applied to run
839
+ state.
840
+ - Corruption does not pass silently. Both call sites in `build.ts` now go through one door that
841
+ appends a typed **`result.unreadable`** event naming the run-dir-relative path, the parser's
842
+ own message, the role (`developer` or `reviewer`) and the story, and the refusal line says
843
+ which file to rewrite and which command to run. It is the only thing a sequencing refusal ever
844
+ writes; `run.yml` still comes back byte for byte, and `tldrx replay` renders the line.
845
+ - `parseReview`'s fail-closed rule is untouched and is a different, harsher contract: it governs
846
+ an envelope that PARSES and is not a valid verdict (unreadable ⇒ `changes`, never `approve`).
847
+ A file that does not parse at all never reaches it.
848
+
849
+ - **Watch READS the branch it diffs off `run.yml`; it used to derive one (#90).** The watch stage
850
+ diffed `feature.epic?.branch` — the `branch:` an epic file DECLARES, written at Plan time before a
851
+ line of code exists. Under the integration branch model (#57) the Build executor deliberately
852
+ ignores that value: every epic's stories merge into one `epic/<run-slug>` branch and the epic
853
+ stays in the plan as a label. So on `260901-leaderboard-v2` the prompt told BOTH watchers,
854
+ verbatim, that `epic/leaderboard-v2-api` *"does not resolve in scavtopia-workflows. Treat this
855
+ feature's code as UNSEEN — cite `absent:` rather than guessing at what it emits"* — about a branch
856
+ nothing had ever cut, while `build.epic_branch` recorded the real one three lines away in the same
857
+ run. An obedient watcher would have written an all-`absent:` card, and **that card PASSES
858
+ `claim-sources`**, because an `absent:` citation resolves by construction. Confident, validated,
859
+ useless coverage; caught only because the host's brief carried the real branch independently.
860
+ - **The branch now comes from the run's record and nowhere else** (`core/watch/recordedBranch.ts`).
861
+ `branch_model: integration` → the run's one recorded branch, for every feature. `per-epic`, and
862
+ every run written before `branch_model` existed → the recorded LIST, with the epic's declaration
863
+ used *only as a key into it*. A declaration is never returned as a branch: either it is in the
864
+ record and the record's entry is what is returned, or the answer is "unrecorded".
865
+ - **A branch the record CLAIMS and the repo cannot find is now a refusal**, not an instruction.
866
+ It is the run contradicting itself, so the stage refuses before anything spawns (`refused`, so
867
+ the stage goes back to `ready`), naming the recorded value, the feature and the repo, and saying
868
+ what fixes it. The treat-as-UNSEEN instruction survives for the one honest case — the record
869
+ names no branch at all — where the prompt now cites `build.epic_branch` rather than a name it
870
+ made up. The two absences used to render as the same sentence.
871
+ - Thirteen tests, red first (6 red / 6 green before, 13 green after): the derivation reproduced
872
+ under both branch models (the prompt's branch lines asserted byte for byte against
873
+ `build.epic_branch`), the loud path in both `headless` and `--prepare`, `--commit` deliberately
874
+ NOT refusing a turn that is already paid for, and the honest absence.
875
+
876
+ - **A recorded `default_branch` that does not resolve is incoherent state too (#92).** #90 taught
877
+ Watch to tell a run that recorded NO branch (honest) from a branch its own `build.epic_branch`
878
+ claims and the repo cannot find (incoherent, refused). It left the third case alone: the BASE of
879
+ the same diff. `.tldrx/workspace.yml` declares `default_branch: main` for a repo where `main` does
880
+ not resolve, and the prompt rendered *"`main`, the `default_branch` of api, does not resolve
881
+ there. Treat this feature's code as UNSEEN — cite `absent:` rather than guessing at what it
882
+ emits"* — inviting the same all-`absent:` card that passes `claim-sources` and covers nothing.
883
+ Narrower blast radius than #90 (a misdetected or renamed default branch, not an every-run
884
+ derivation), same shape: **a value the workspace RECORDS that the repo cannot find.**
885
+ - **Watch refuses at `--prepare` and headless**, naming the repo, the recorded value and
886
+ `.tldrx/workspace.yml`, and saying that the value was DETECTED from that repo — so a repo with
887
+ no such branch means the record has gone stale (a rename, a fresh clone with only remote
888
+ branches, a misdetection). `epicDiff` carries it as `baseMissing`, the sibling of #90's
889
+ `branchMissing`, and checks the base FIRST: a base that is not there voids every diff in that
890
+ repo rather than emptying one, so there is nothing to learn by asking about the branch. On
891
+ `--commit` it stays tolerant, the same call #90 made — those cards are already paid for.
892
+ - **`tldrx doctor` reports it**, which is where the issue asked whether it belonged, and the
893
+ answer is yes: this is a workspace RECORDING something false, and `doctor` is the one command
894
+ whose job is what this machine and this workspace actually have. One
895
+ `git rev-parse --verify --quiet` per declared repo, nothing fetched or written. A **warning**
896
+ that never moves the exit code — `healthy` is about the TOOLS this machine has, and a repo can
897
+ legitimately be mid-clone or mid-rename on a developer's box; a `doctor` that exits 1 for that
898
+ is a `doctor` people stop running. A repo that is not on disk, has no `default_branch` recorded,
899
+ or is not a git repo is reported as SKIPPED with its reason, never folded into "all resolve".
900
+ - **`boundary` has the gap and deliberately keeps its verdict — measured, not assumed.** It does
901
+ diff `<default_branch>...<epic_branch>` (`boundary.ts`, `base:` read from `workspace.yml`;
902
+ changing the record from `main` to `trunk` changes the target's `base` byte for byte), and an
903
+ unresolvable base came back `{ok: true, detail: "n/a (nothing could be diffed: \`trunk\` does not
904
+ resolve in app)"}` — the gate passes GREEN and stops measuring for as long as the record is
905
+ wrong. It is **not** made to refuse: it spawns nothing and writes nothing, so the fault costs it
906
+ a measurement rather than producing a false one, and `boundary.ts`'s own contract is that it
907
+ "must not refuse a gate for a reason that has nothing to do with the boundary" — one stale
908
+ record would otherwise brick every Build gate in the workspace. What changed is the WORDING: its
909
+ two absences used to be the same sentence (`` `<ref>` does not resolve in <repo> ``), so a
910
+ merged-and-deleted epic branch (nothing to fix) read exactly like a stale `default_branch`
911
+ (`n/a` until someone fixes it). The base's reason now names the record and points at
912
+ `tldrx doctor`; the branch's is untouched.
913
+ - `.tldrx/workspace.yml` is spelled once, in `paths.ts`, now that three operator-facing messages
914
+ tell someone to go and edit it.
915
+ - Fifteen tests, red first (8 red / 7 green before, 15 green after), including the verbatim
916
+ pre-fix string captured off the real prompt, the `--commit` tolerance, doctor's warning level
917
+ asserted through `runDoctor` end to end, and the boundary determination pinned on all four
918
+ counts — that it reads `default_branch`, that it stays `ok`, that its reason now names the
919
+ record, and that a missing epic branch keeps its own wording.
920
+
921
+ - **The dashboard says the framework's CURRENT vocabulary, not 0.2.0's.** The live page
922
+ shipped in 0.2.0 and has had one change since; the framework has had a great many. An audit of
923
+ `src/core/dashboard/` against today's `run.yml` and `waiting.ts` found seven words the files use
924
+ and the page dropped. A page that drops a word does not look wrong — it looks finished, which is
925
+ worse, and every one of these was measured on a rendered fixture before it was fixed.
926
+ - **`prepared` and `running` rendered as "nothing".** `waitingFor` has had eight kinds since the
927
+ 2026-08-29 audit; the WAITING ON column named five and fell through to `nothing — <status>` for
928
+ the rest. So a host-attended run with a `--prepare` bundle on disk — the entire attended loop —
929
+ said it was waiting on nothing while `isMovable` had it wearing `← next` on the same row. Every
930
+ kind now prints `waiting.message`, which is the sentence the CLI already prints, and `prepared`
931
+ joins gate/answer/failed as a card: it is a run waiting on a person.
932
+ - **An `agent` gate was counted as a human one.** The execution-path eyebrow read
933
+ `N human, M auto`, so a run opened `--gates what:agent,plan:agent,build:agent` reported as
934
+ all-human — the exact opposite of what it was set up to do, in the one number that eyebrow
935
+ exists to give. It now counts all three, the same arithmetic `renderGates` does.
936
+ - **Nothing showed what an agent gate was signed over.** `run.yml` records the verdict, the
937
+ sample, what resolved and what was refuted, and the run-relative path of the COMMITTED evidence
938
+ note; the model dropped the whole block, so `agent by reviewer` and `human by alan` read as the
939
+ same kind of fact. A human signature is a name and a person accountable for it. An agent's is a
940
+ name and nothing, unless what it checked is beside it. Now `path[].gateEvidence`, printed under
941
+ the signer — the path as text, never a link.
942
+ - **`$0.00 spent` on a run whose turns a host session paid for.** `spentUsd` is metered dollars
943
+ and the meter drew it as the whole story: the exact failure `unmeteredNote` exists to stop the
944
+ CLI making, made in a progress bar. `attendedBy`, `unmeteredTasks` and `hostTokens` now ride the
945
+ model, the card prints `attended: host` in the CLI's own words, and the two currencies are shown
946
+ side by side and never added — there is no exchange rate between a metered dollar and a host token.
947
+ - **`build.branch_model` (#57) and the epic branches were invisible** in a section headed
948
+ *Plan & build*. A chained plan on one integration branch and independent per-epic branches drew
949
+ the identical table. A null model is reported as unrecorded, never guessed at as `per-epic`.
950
+ - **A `stale` stage looked finished.** A stage left behind by `tldrx reject --stage` wears
951
+ `done` with its outputs still on disk, derived from a decision that has been withdrawn. It now
952
+ wears a `stale` chip too.
953
+ - **The page claimed it read `events.jsonl`.** It never has (measured: nothing in `model.ts` or
954
+ `loadPhaseArtefacts` opens the ledger). That sentence is why a reader could not tell an empty
955
+ ledger from an unread one, so the *How to use it* tab now says which files it reads, that the
956
+ ledger is not among them, and which commands do read it. The Watchers tab, which existed
957
+ precisely to be honest about a gap, printed an invented card shape whose field names matched
958
+ none of the seven `Watcher` actually carries; it now prints the real one and names
959
+ `tldrx watch list`.
960
+ - **`review` was grey.** A `PLAN_STATUSES` value the page's tone function had never heard of, so
961
+ a story in review landed in the same colour as one nobody had started.
962
+ - **The test that should have caught all of this asserted the opposite.** `WAITING_KINDS` is now a
963
+ value rather than a bare type union, and the chain fixture's coverage test names the three kinds
964
+ it cannot hold instead of claiming it covers every kind — the claim that let `prepared` go
965
+ unrendered for months. `modelVersion` stays at `3`: every model change here is an ADDITION, and
966
+ the rule in `model.ts` is that only a removal or a change of meaning bumps it.
967
+ - **What the same audit found and did NOT fix is #85**, because each of it is a decision rather
968
+ than a patch: reading `events.jsonl` (operator notes, story attempts and reopens), reading
969
+ `budget.yml` (per-phase ceilings, `on_exceed`, whether `next` is affordable), reading the
970
+ watcher cards, and surfacing a preflight refusal. #86 is a separate bug the audit turned up in
971
+ `run/waiting.ts` — a cancelled run is offered a retry — which both screens share.
972
+
973
+ - **The #80 guard's kind sweep could not tell a regex literal in CODE from one quoted in a
974
+ COMMENT (#83).** The marker half of that guard has a discriminator and says why: a `[` inside a
975
+ regex opens a character class, so code that matches the `[src:` marker must spell it `\[src`
976
+ while prose writes `[src: …]` bare. The kind half had no equivalent, because prose quoting a
977
+ regex LITERAL copies it character for character — there is no signature inside `/^F\d{3,6}$/`
978
+ that a real production has and a quoted one lacks. #81 paid the tax: the doc comment explaining
979
+ which two literals had just been DELETED tripped the sweep in the very file that had stopped
980
+ classifying, and had to be reworded to name the shapes without writing them.
981
+ - **The discriminator is the line's ROLE, not its characters.** A line that is comment prose —
982
+ a whole-line `//`, a block opener, or one of its `*` continuation lines — is not a
983
+ classification, and the sweep now reads the file with those lines removed.
984
+ - **Line-local, holding no state, and that is the design rather than a shortcut.** #80's own
985
+ first attempt reached for a lexer and was bitten twice: eleven offenders that were all doc
986
+ comments, then five more that were template literals nested inside interpolations once
987
+ comments and strings were stripped. This predicate never looks inside a string, so an
988
+ unclosed `/**` in a prompt cannot unbalance it and blind the sweep to the code below —
989
+ asserted, not assumed. What it cannot lex it keeps: a shape quoted in a trailing comment
990
+ after code is still read as code, so this half still fails CLOSED.
991
+ - **Measured in both directions on the real tree, not only on fixtures.** Re-injecting #81's
992
+ exact doc comment into `validateFactsFile.ts` leaves the sweep green; injecting two real
993
+ productions as CODE into the same file turns it red with `validateFactsFile.ts classifies
994
+ answer, fact` — the identical message #81 saw for prose. The sweep is also no longer
995
+ vacuous by assumption: a test pins the kinds the canonical grammar itself trips
996
+ (`answer`, `cmd`, `fact`) and the six productions the deleted `core/map/srcToken.ts`
997
+ carried, so a production that stops matching anything becomes visible rather than silent.
998
+ - **What it removed, measured over the tree.** 28 files matched at least one production
999
+ before, 20 after; the 8 that stopped matching were doc comments mentioning `→ exit` and
1000
+ nothing else — each one production away from #81's surprise red. No file lost a
1001
+ classification: the most kinds any file decides is still 3, and it is still `srcToken.ts`.
1002
+
1003
+ - **A handshake called in the wrong ORDER no longer fails the stage and the run (#82).** On the
1004
+ live run `260901-leaderboard-v2` (2026-09-02T00:36Z) the driver ran `tldrx next --commit
1005
+ --review` while no reviewer bundle was out. The framework said exactly the right thing — "no
1006
+ reviewer bundle is out — run `tldrx next --prepare --review` first" — and then emitted
1007
+ `stage.failed` twice and flipped the run to `status: failed`, so a mistyped command needed an
1008
+ explicit recovery. Nothing had been attempted: no spawn, no cent, no branch, no story moved.
1009
+ Same family as #78/#79 (form vs work), applied to state transitions instead of attempts.
1010
+ - **Five refusals are reclassified**, all of them the handshake asked for by the wrong end:
1011
+ `--commit --review` with no reviewer bundle out; `--commit` with no story `in_progress`;
1012
+ `--prepare --review` over a story whose developer half has not run; and either `--commit`
1013
+ before its `result.json` has been written. Each now exits `1`, names the command that fixes
1014
+ it, and leaves `run.yml` byte for byte as it was — no `stage.failed`, no status moved, no
1015
+ task recorded, no cent metered.
1016
+ - **This is the behaviour the framework already had one layer over.** A single-agent stage sent
1017
+ `--commit` before its `--prepare` has always returned exit `1` and touched nothing
1018
+ (`commitStage`). Build was the outlier because Build owns its own middle, so its refusals came
1019
+ back as `ok: false` and `runNext` could only read that as a failed stage. The fix gives Build
1020
+ the same door rather than inventing a second contract.
1021
+ - **`sequencing` is a second flag, not a widening of `refused`.** `refused` sends the stage back
1022
+ to `ready`, which is right for a precondition an operator must go and fix — a dirty repo, a
1023
+ red base tree — because the cycle cannot continue. A sequencing refusal is a cycle that is
1024
+ perfectly fine, held by the wrong end: the bundle is still out and sending the stage back to
1025
+ `ready` would throw away the state the next command needs. Honoured only when the outcome
1026
+ carries no tasks, no cost and no epic claim, so a refusal that spent something still records
1027
+ it; defaulting to "record it" is the direction a mistake here is recoverable in.
1028
+ - **The bug had a price, and it was the budget brake.** `runExecutor` skips the phase-budget
1029
+ gate exactly when a stage is already `running`, because a Build stage hands out one story per
1030
+ `--prepare`/`--commit` cycle. Failing the stage demoted it out of `running`, so the next
1031
+ `--prepare` was priced as a fresh stage start: on the live run that was a `budget.blocked`
1032
+ ten seconds after the refusal, $2.66 left against a re-charged $4.32 estimate, for money
1033
+ already partly spent. Reproduced in the fixture and pinned.
1034
+ - **`PendingError` carries a typed `kind`** — `absent` (the file was never written; a step of
1035
+ the handshake) vs `unreadable` (somebody wrote it and got it wrong; still a failure). Typed
1036
+ rather than matched on the message, for the reason #79 gave: a caller reading the words breaks
1037
+ the moment the message improves.
1038
+ - Genuine failures are untouched and pinned: a red Definition of Done still blocks its story, a
1039
+ reviewer that dies is still recorded as a failed check, and a plan that cannot be loaded is
1040
+ still `stage.failed` with a `failed` run. Two existing pins moved deliberately, both of them
1041
+ asserting the old exit `5` of a pure sequencing refusal.
1042
+
1043
+ - **`facts.yml` ids and citable ids were two spellings of one shape; now they are one constant
1044
+ (#81).** `src/core/facts/validateFactsFile.ts` defined its own `F` and `Q` id patterns, agreeing
1045
+ with `SRC_PATTERNS.fact` / `.answer` character for character, with nothing asserting that they
1046
+ must. Found while fixing #80 and filed rather than fixed there, because it is not #80's defect:
1047
+ the `[src: …]` token grammar has exactly one reader now, and what was duplicated here is the id
1048
+ SHAPE that grammar happens to share with the facts file, reached by a different reader answering
1049
+ a different question — which is why #80's kind sweep allowlisted the file rather than folding it in.
1050
+ - **They must agree, and the DIRECTION is what makes it load-bearing.** One string makes one
1051
+ trip: `formatFactId` mints `F102`, `validateFactsFile` admits it into `.tldrx/memory/facts.yml`,
1052
+ and `classifySrc` reads that same `F102` back out of a `[src: F102]` token for `knowledgeFile`
1053
+ to resolve against the store. So every id the facts file accepts must be citable, or a fact
1054
+ exists that cannot be cited and is invisible to every reader downstream of it. Nothing in that
1055
+ pipeline ever runs both readers on the same string, so the drift would have been silent. (The
1056
+ other direction is merely untidy: a citable id no facts file holds is caught later, as a
1057
+ citation that resolves to nothing.)
1058
+ - **Derived, not merely asserted.** `validateFactsFile.ts` imports `SRC_PATTERNS.fact` and
1059
+ `.answer` — the same publication surface `srcGrammarContract.ts` already generates the
1060
+ documented grammar from — so there is one spelling rather than two that match. The #80
1061
+ allowlist entry is deleted along with the need for it, and that sweep now covers the file like
1062
+ any other; an exemption that has stopped being needed is one that will some day cover something
1063
+ it was never granted for.
1064
+ - **A behavioural guard too, because sharing a constant only holds while nobody re-types a
1065
+ literal.** The kind sweep needs TWO productions in one file before it fires, so a single
1066
+ respelled `F` shape slips straight past it — measured by sabotage, not assumed.
1067
+ `test/map-citations.test.ts` now runs both readers over a table of ids spanning both digit
1068
+ boundaries and compares their verdicts; loosening either shape by one digit turns it red with
1069
+ `F12: facts.yml holds it but the grammar refuses it`.
1070
+ - **One drift had already happened, in prose.** The `source.q` refusal read `expected ^Q\d+$ or
1071
+ null` while the reader ran `^Q\d{1,6}$` — the shape's third spelling, telling an author a
1072
+ seven-digit question id was acceptable and then refusing it. Both refusal messages are now
1073
+ generated from the pattern they enforce, so neither can drift from its reader again.
1074
+ - **The docs site said 0.3.1, named a flag that does not exist, and told readers to hand-write
1075
+ a mandate the CLI now prints.** A cold read of every page against `tldrx <cmd> --help`, EN and
1076
+ ES. Three claims were false rather than merely stale: the version, printed on six pages, was two
1077
+ releases behind what `npm i -g tldr-experts` installs; `--max-budget-usd` was named as a `tldrx
1078
+ next` flag in both budget pages, but it is what tldrx passes down to `claude` and `tldrx next
1079
+ --max-budget-usd 3` exits `1` on an unknown flag; and `guides/driving` said "there is no keyword
1080
+ for the third part — the mandate is prose you write" three weeks after `tldrx drive
1081
+ --attended|--unattended` shipped to print exactly that, versioned with the package. Also
1082
+ corrected: the hero claimed a gate you own at the end of *each* stage, where `feature` ships two
1083
+ `auto` ones; the quickstart said "five role experts" one line under output reading `experts 6
1084
+ seeded`, and reported `$0.31 of $4.00` on a run opened at `$5.00` without saying the second
1085
+ figure is the stage's ceiling.
1086
+ - **The site covered none of the last release's commands, and the Spanish mirror had fallen a
1087
+ release behind the English one.** Added across both languages: `story reopen --for-fix`,
1088
+ `questions lint`, `retro --all --json`, `watch check --execute`, `update_check: off`, `seed
1089
+ answer`, exit `130`, `budget.yml`'s `ceiling_host_tokens` / `on_host_tokens_exceed` (the host
1090
+ economy was documented as unboundable), the integration-branch model for chained epics (#57),
1091
+ a FAQ entry for `tldrx update` and the version notice, and a link to `CONTRIBUTING.md`. The ES
1092
+ CLI reference was additionally missing `update`, `drive`, `retro --all` and `plan schema`
1093
+ outright. `reference/cli` no longer pins a version number that goes stale every release. The
1094
+ Spanish pages' code comments were half-translated; terminal output stays English, the docs
1095
+ author's own `#` comments do not.
1096
+
1097
+ - **`tldrx map --check` ran a SECOND, divergent `[src: …]` grammar; there is one grammar now
1098
+ (#80).** `src/core/map/srcToken.ts` was not a thin wrapper over the claim-sources reader — it was
1099
+ a parallel set of regexes, and they disagreed on five axes: its `file` pattern
1100
+ (`[^\s:]+`) refused a path containing a colon, its answer pattern (`^Q\d+$`) had no digit cap,
1101
+ its token pattern was **global** so a citation written mid-sentence counted as one, it stripped no
1102
+ trailing backtick or full stop before matching, and it had never heard of `aidlc:`. So a citation
1103
+ `claim-sources` accepted could be reported by `map --check` as a problem, and one it refused could
1104
+ be reported as fine — the "two readers of one question drift, and the looser one wins the argument
1105
+ at the wrong moment" hazard `core/text/handoff.ts` is written against. It also carried #77's
1106
+ defect on its own path, printing the symptoms `bullet has no [src: …] token` and
1107
+ `unparseable src token` and nothing else.
1108
+ - **The parser is deleted, not wrapped.** `checkCitations` calls `parseSrcToken`,
1109
+ `diagnoseSrcToken` and `describeSrcFailure` from `src/core/text/srcToken.ts`, so every `map
1110
+ --check` grammar failure gets the #77 treatment by construction: the rule id, the rule in its
1111
+ own words, the line as written, and a line that would have passed. `CitationProblem` gained a
1112
+ `rule` field, so a caller can tell "you wrote it wrong" from "it has drifted" without parsing
1113
+ prose. `srcToken(srcs)` — the BUILDER, which had no equivalent on the canonical side — moved
1114
+ across rather than went, and now joins on the same `SRC_SEPARATOR` the reader splits on.
1115
+ `isBullet` stayed behind in `checkCitations.ts`: it is about DOCUMENTS, and mixing the two is
1116
+ what the deleted file got wrong.
1117
+ - **The migration hazard was measured before the switch, not asserted after it.** Both grammars
1118
+ were run over every `.tldrx/map/**` document and init handoff in the two real workspaces
1119
+ available — **39 documents, 692 lines, 435 of them carrying a citation** — and they disagreed on
1120
+ **zero** lines: no multi-token line, no non-bullet citation, and not one token that failed to end
1121
+ its line. The axes where they *would* differ are pinned as a table in
1122
+ `test/map-citations.test.ts`. Unification is **stricter** on six shapes (a mid-line token, a
1123
+ `..` in a path, a 7-digit `Q`, a 4-digit exit code, a backwards line range, a line number of
1124
+ zero) and **looser** on five that the old reader wrongly refused (a path containing a colon, an
1125
+ `aidlc:…#Q<n>` src, a `cmd` whose command holds a backtick, an `absent:` path with a space, a
1126
+ token wrapped in backticks). An `aidlc:` citation used to be misread as repo `aidlc` and
1127
+ reported as an unknown repo; it is a kind now.
1128
+ - **A guard makes re-adding a copy go red** (#48's lesson: deleting one file does not stop a
1129
+ second). Two halves, both on the SHAPE rather than the file name — no file outside the canonical
1130
+ grammar and two documented non-parsers may hold a regex that matches the `[src:` marker, and no
1131
+ file outside it may classify two or more src KINDS. The guard is proven against the deleted
1132
+ file's own two regexes rather than assumed. It also turned up a real second copy of the fact and
1133
+ question id shapes in `validateFactsFile.ts`, filed as #81 rather than fixed here.
1134
+ - **`map --check`'s summary counts the two failures separately.** A citation that does not parse
1135
+ never reached the filesystem, so folding it into "N of M citations do not resolve" reported a
1136
+ denominator it was never in — and, when every problem was a grammar one, printed "3 of 0".
1137
+
1138
+ - **A `[src: …]` rejection now states the RULE it enforced, and the grammar is published where the
1139
+ writers read it (#77).** Run `260830-ordering-inventory` lost **three story attempts** to one
1140
+ message. Three review envelopes were refused with "no `[src: …]`" — the SYMPTOM — while all three
1141
+ carried a citation. The host guessed the grammar twice, got it wrong twice, and finally opened
1142
+ `dist/tldrx.js` to extract three rules that were written down nowhere a writer could read them:
1143
+ the token must END its line (`TRAILING_TOKEN_RE` is anchored), a `]` **inside** the token
1144
+ truncates the match (`[^\]]*`), and a `cmd` source needs the real `→`, never ASCII `->`.
1145
+ - **Every rejection on the path names its rule, quotes the line, and shows a corrected one.**
1146
+ `SRC_RULES` (`src/core/text/srcToken.ts`) is fifteen rules, each with the pattern that enforces
1147
+ it and a worked `bad`/`good` pair; `diagnoseSrcToken` maps a failure onto exactly one. The hook
1148
+ denies, the gate's `claim-sources` detail, `parseFixFindings`' `refuted` refusal and every
1149
+ `classifySrc` error now carry the id. The deny messages take the document text and quote the
1150
+ offending line back — naming `L14` and stopping there is what sent the host looking.
1151
+ - **The grammar is GENERATED and spliced into the prompt** (`renderSrcGrammarContract`), following
1152
+ the #35 precedent: kinds from `SRC_KINDS`, patterns from `SRC_PATTERNS` printed `.source` and
1153
+ all, rules and examples from `SRC_RULES`, the four-section rules from the same constants
1154
+ `handoff.ts` enforces. It reaches the writers through the check-contracts registry (every stage
1155
+ declaring `claim-sources` over a `.md` output), the Watch executor — which read `stage.md`
1156
+ raw and so was the one writer never given any contract — and the reviewer prompt, whose
1157
+ `refuted` verdict is held to this grammar and was never told what it is.
1158
+ - **The trap is behavioural, not textual.** `test/src-grammar.test.ts` pushes every documented
1159
+ `bad` back through `diagnoseSrcToken` and every `good` through `parseSrcToken`. Loosen a regex
1160
+ without updating its rule and the suite goes red; a doc that has stopped being true cannot ship
1161
+ quietly. It caught one bug on its first run: `RegExp.source` re-escapes non-ASCII, so
1162
+ `CMD_RE.source` spells the arrow as a `\u`-escape — the contract would have documented "use the
1163
+ real `→`" with the arrow itself written as an escape sequence. `readableSource` decodes it.
1164
+ - Not changed: what the parser ACCEPTS. Tolerating `->` is a product decision and stays open
1165
+ as #77's item 3.
1166
+ - **A grammar-rejected review envelope no longer consumes a story ATTEMPT (#78).** Measured on run
1167
+ `260830-ordering-inventory` (2026-09-01): stories S2, S3 and S5 each recorded
1168
+ `check: review · verdict: changes · attempt: 1` over a summary beginning *"I would sign this:
1169
+ every named acceptance criterion is met"*. Those were `fixlist` envelopes refused by the
1170
+ **claim-sources** check — a `refuted` finding whose `[src: …]` sat mid-line, where §2.8's
1171
+ end-anchored parser cannot see it — and each was charged to the story as a failure of its WORK.
1172
+ Three of the run's attempts went on formatting. A malformed envelope is a fault in the reviewer's
1173
+ *report*; conflating the instrument with the result is what this fixes.
1174
+ - **The framework asks again, for free.** A refusal the claim-sources check raised re-prompts the
1175
+ same reviewer for a corrected envelope, carrying what was refused verbatim under
1176
+ `## Your previous envelope was REFUSED`. **Bounded at two** per envelope round (owner decision,
1177
+ 2026-09-01): the third refusal is recorded as the ordinary `changes` and costs the attempt, so a
1178
+ reviewer that cannot write the grammar at all still settles instead of looping free. The bound
1179
+ resets when a verdict is finally counted — it is per envelope round, not per story.
1180
+ - **Both doors, one rule.** A spawned reviewer re-prompts itself in-process;
1181
+ `tldrx next --commit --review` leaves the bundle out with the refusal spliced into its
1182
+ `prompt.md`, bins the refused `result.json` and settles nothing. Attempt accounting must not
1183
+ depend on which door a verdict came through, so both go through the same predicate.
1184
+ - **Auditable, because it is bookkeeping.** Each free round appends one `story.review_retried`
1185
+ (§2.9) carrying the story, the attempt it did **not** spend, which retry it was, the bound, and
1186
+ the refusal. `readReviewLedger` counts them, which is how the bound survives a fresh
1187
+ `tldrx next` and the one-envelope-per-process host handshake alike. Each re-prompt is a real
1188
+ metered turn and gets its own task row: it costs the story no attempt, never no money.
1189
+ - **Scope guard, pinned by test.** Only the claim-sources grammar — *widened to every
1190
+ envelope-FORMAT refusal by #79 below, in the same release.* A verdict's CONTENT and a red DoD
1191
+ keep exactly the cost they had.
1192
+ - **The message is #77's, inherited rather than copied.** The re-prompt carries the refusals
1193
+ verbatim and points at the `Citation grammar` section #77 splices into the
1194
+ same prompt, so the reviewer is told which rule it broke, on which line, with a corrected
1195
+ example — and there is no second copy of the grammar to keep in step. The classifier
1196
+ is a typed INDEX over `problems`, not a second list, precisely so #78 could not end up
1197
+ string-matching the text #77 was rewriting.
1198
+
1199
+ - **Every envelope-FORMAT refusal gets that same free round, not just the citation (#79).** #78 drew
1200
+ its scope at the claim-sources check because that is what the evidence named, and filed the rest.
1201
+ But `parseFixFindings` refuses an envelope for five other shapes — a `fixlist` that is missing,
1202
+ not an array or empty; a row that is not an object; a row with no `finding` text; a row with no
1203
+ valid `disposition` — and `parseReview` refuses a sixth, a verdict WORD outside the enum (#36).
1204
+ Every one of them is a fault in how the reviewer wrote its *report*, exactly the argument #78 made,
1205
+ and every one of them still cost the story an attempt. Owner decision (2026-09-01, on the issue):
1206
+ all of it, **one mental model — FORM never costs an attempt, CONTENT/WORK always does.**
1207
+ - **Nothing about the mechanism changed.** Same bound (two per envelope round, the third is the
1208
+ ordinary `changes`), same counter, same `story.review_retried` event, same two doors, same
1209
+ per-turn metering. #79 widened *what earns a correction* and touched nothing else.
1210
+ - **The verdict WORD (#36) is now free too, and #36's message is unchanged.** A reviewer that
1211
+ writes `sign` — the gate vocabulary — said nothing wrong about the diff; it reached for a word
1212
+ the story enum does not have. It is told so, by name, and asked again. `Review.formatProblems`
1213
+ is the union of the fix-list index and that verdict fault, because it is also what the corrected
1214
+ envelope's prompt is rendered from: a refusal missing from it is one the reviewer is never told
1215
+ about.
1216
+ - **The guard is the INDEX, which is what survives the next widening.** `ParsedFixlist.format`
1217
+ stays a typed subset of `problems`, built one push site at a time through a single
1218
+ `refuseFormat` helper — never a second list of strings, and never "everything `parseFixFindings`
1219
+ said". The free round is granted only when the index claims **every** reason the envelope was
1220
+ refused, so a refusal about the WORK added later costs the attempt until somebody deliberately
1221
+ indexes it as form. Defaulting to *costs* is the direction a mistake is recoverable in.
1222
+ - **A non-empty `findings[]` is deliberately NOT the content signal**, though it is the obvious
1223
+ candidate. Measured across the nine `aparece-v2` runs (2026-09-01): all 25 recorded review logs
1224
+ carry a non-empty `findings[]` and all 25 are `approve` — the one verdict whose own prompt line
1225
+ says *"Empty on `approve`"*. Reviewers use it as a narrative evidence log whatever the verdict,
1226
+ so gating on it would have made the free round almost never fire and quietly narrowed #78 as
1227
+ well. A judgement about the work is caught where it is actually stated: a declared `changes`
1228
+ raises no format refusal at all, so it costs its attempt.
1229
+ - **Nine tests red first**, each newly-free class proven to re-prompt without spending an attempt
1230
+ and to record the event, plus the bound re-proven on a shape refusal. #78's CONTENT and DoD pins
1231
+ are unchanged and still green.
1232
+
1233
+ - **`merge-wave.sh` no longer leaves a conflicted tree behind, wedging every queued sibling (#76).**
1234
+ On a merge conflict the script exited `2` **without** `git merge --abort`, and the `EXIT` trap
1235
+ then released the lock. The conflicted index survived that handover, so the next queued
1236
+ invocation acquired the lock, failed its dirty-tree guard and exited `1` `FAIL dirty tree` having
1237
+ merged nothing — and so did every one after it, until a human ran the abort by hand. Observed
1238
+ live 2026-09-01 by two agents: a sibling's abandoned merge left `UU CHANGELOG.md` plus 14 staged
1239
+ paths in the shared checkout, and cluster L's first merge-wave returned `1` with nothing merged.
1240
+ Under the concurrent multi-cluster pattern, one conflict wedged every other cluster.
1241
+ - **Collect, then abort** — the order `mergeNoFf` already uses one directory over
1242
+ (`src/core/build/git.ts:314-326`). The agent still learns exactly which files conflicted and
1243
+ still has to rebase and retry; the checkout it hands back is the one it was given. This is the
1244
+ same class of hazard the lock was written for: state from one invocation leaking into the next.
1245
+ - **The refusal now names what is dirty.** `FAIL dirty tree` alone accused the caller of leaving
1246
+ junk in their own checkout when, inside the lock, the likeliest cause is another run's residue
1247
+ in the shared one. It now says so and lists the paths.
1248
+ - Proved by a repro that runs the real script against a real conflicting merge and asserts
1249
+ `git status --porcelain` is empty afterwards, `MERGE_HEAD` is gone, `HEAD` has not moved, the
1250
+ lock is released, and a second invocation merges instead of being refused.
1251
+
1252
+ - **The built-CLI dashboard test no longer gates a stale `dist/` (#73).** `beforeAll` read
1253
+ `if (existsSync(DIST)) return`, so the tests ran against whatever `dist/tldrx.js` happened to be
1254
+ lying around — locally, a build from before the working-tree changes the run was checking.
1255
+ Measured 2026-09-01 while fixing #60: the guard served a binary built at 14:43, and the model
1256
+ version assertion read `Expected: 3, Received: 2`. That is the lucky direction; the same staleness
1257
+ hiding a regression is silent, and `bun run build` is a separate later step in both
1258
+ `scripts/merge-wave.sh` and CI. CI was safe only by accident — a fresh checkout has no `dist/` —
1259
+ which means the guard only ever applied where it did harm.
1260
+ - **Always rebuild, and the number is the reason.** `bun scripts/build.ts` costs 199 ms cold and
1261
+ 55–59 ms warm on the reference machine (3 runs, 2026-09-01) against a ~420 s suite: under 0.05%.
1262
+ A stamp of `src/` would cost more to keep honest than it saves, and `test/build.test.ts:78`
1263
+ already built unconditionally — this file was the exception.
1264
+ - A new assertion pins the property rather than the mechanism: `dist/tldrx.js` may not predate the
1265
+ newest file under `src/`, `scripts/build.ts` or `package.json`.
1266
+
1267
+ - **`tldrx drive` fills the mandate's `<run>` in (#75).** The mandate's every command read
1268
+ `tldrx next --prepare <run>`, and the header told the reader to find-replace — 7 occurrences
1269
+ unattended, 5 attended (measured; the issue estimated ~8), by hand, at the exact moment somebody
1270
+ is trying to start a run. One occurrence missed sends a session at the wrong run.
1271
+ - **An id, or the one open run.** `tldrx drive --unattended <run>` or `--run <id>` (the positional
1272
+ wins, `ship`'s order) substitutes **textually and never validates** — an id naming no run is the
1273
+ operator's typo to notice, and the command stays the one thing in the CLI that runs anywhere.
1274
+ With no id, the ONE open run of the current workspace is used.
1275
+ - **It refuses to guess between two.** Where `RunStore.resolve` would call it ambiguous, drive
1276
+ declines to substitute, leaves `<run>` standing and names the ids on stderr — a mandate silently
1277
+ aimed at the wrong run is the bug being fixed, not a smaller version of it. No workspace, no
1278
+ runs, an unreadable `tldrx-work/`: all keep the placeholder and still exit `0`.
1279
+
1280
+ - **`tldrx status` no longer calls a RUNNING run "cannot start yet" (#60).** Verbatim from
1281
+ aparece-v2, 2026-09-01: `run 260830-ordering-inventory (…) cannot start yet — it was proposed to
1282
+ follow money-and-payments` / `at 04-build / build · run status running · waiting: prepared` /
1283
+ `blocked by money-and-payments — it is pending`. The run was building, with S1 verified minutes
1284
+ earlier. `triage.depends_on` is an order a split PROPOSED before either run existed, and it was
1285
+ out-ranking what the run was observably doing.
1286
+ - **Observed state outranks proposed order.** One rule, in the resolver both screens read
1287
+ (`src/core/run/dependencies.ts`): a run that has left `pending` has started, and a proposal
1288
+ cannot un-start it. `hasStarted` is the one definition — `pending` is the only status a run that
1289
+ has never run a stage can wear, because runs are created with every stage `pending` and every
1290
+ path to any other value goes through a stage that was `running`.
1291
+ - **The proposal becomes a footnote, not the headline.** A started run renders its own cursor and
1292
+ waiting kind, gains back the command it was denied, and carries
1293
+ `proposed to follow <run> — started anyway` as a secondary line. `blocked by` is now reserved
1294
+ for a run that really cannot move — one that has not started. A run that has NOT started is
1295
+ unchanged: same words, same withheld command.
1296
+ - **It gets the `← next` slot back.** The ordering hint had demoted the only run with work in
1297
+ flight and pointed the owner at the sibling that had not begun. `runnable` is now
1298
+ `movable && (started || nothing outstanding)`.
1299
+ - **`prepared` and `running` get their own summary lines.** Both used to fall through to
1300
+ "is blocked at <phase>/<stage>" — the same wrong word, one layer down.
1301
+ - **The dashboard was making the identical claim** about the same shape and is fixed with it:
1302
+ fixture run `charlie` is `awaiting_gate` behind a `pending` sibling, and the page said
1303
+ "blocked by bravo" while suppressing the gate alert for a signature a person could give right
1304
+ then. `DASHBOARD_MODEL_VERSION` → **3**: `blockedBy` is unchanged and still records the
1305
+ proposal, the new `runs[].started` says whether it still holds anything back, and `runnable`
1306
+ reads `true` for that one shape where it read `false`.
1307
+
1308
+ - **A cost/token declaration on a story commit now attaches to the build task (#68).** Measured on
1309
+ two live runs: the leaderboard host declared $2.25 on a story commit and `tldrx cost` kept
1310
+ 04-build at $0.00 — "the declaration didn't attach to the build task the way it did for
1311
+ what/how/plan" — and ordering 260830 showed the same shape. The run's recorded build spend was a
1312
+ floor, budget arithmetic ran blind on the most expensive phase, and ~8M host tokens were invisible.
1313
+ - **The seam, not the accounting.** `ExecutorContext` never carried `--cost-usd`/`--tokens`, so
1314
+ Build's `commit()` could only read the envelope's own `cost_usd` and wrote
1315
+ `round2(result.cost_usd ?? 0)` — a METERED `$0.00`. It now resolves
1316
+ `options.costUsd ?? result.cost_usd`, which is exactly what `commitStage` does for what/how/plan,
1317
+ and the declared tokens ride onto the task row and the `agent.result` payload the cost report
1318
+ reads. The reviewer half (`--commit --review`) takes the same precedence.
1319
+ - **Nothing declared is UNMETERED, not $0.00.** `cost_usd: null` + `metered: false`, the spelling
1320
+ every other host turn already gets, because `$0.00` is a measurement and a false one.
1321
+ - **Backward-safe.** A declaration is never allowed to overwrite a measurement — an envelope that
1322
+ reports its own cost still wins over nothing declared — and no recorded zero is rewritten. Only
1323
+ new declarations attach.
1324
+
1325
+ - **`onStderrLine` on the runtime seam, so progress prints before the verdict it produced (#67).**
1326
+ `SpawnOptions` had `onStdoutLine` and nothing for the other stream, so stderr — where every tldrx
1327
+ UI writes its progress — could only be handed back as one string at exit. `tldrx learn` printed
1328
+ `01-what/what done — $0.31 of $4.00` and only THEN the `[00:00] writing …` lines that produced it.
1329
+ - Implemented in **both** `bunRuntime.ts` and `nodeRuntime.ts`, with the same contract
1330
+ `onStdoutLine` already has: the full text still accumulates in `SpawnResult.stderr`, a trailing
1331
+ partial line is delivered at close, and omitting the callback leaves the buffered path
1332
+ byte-for-byte as it was. Node gets its own `LineSplitter` per stream — one shared buffer would
1333
+ splice a half-written stdout line onto the front of a stderr line.
1334
+ - **The learn workaround is deleted**, per the instructions it carried since phase 1:
1335
+ `realStepRunner` passes the callback and `playChapter` no longer writes `result.stderr` after the
1336
+ step. Measured by playing chapter 2 by hand on both checkouts: the summary was at line 107 with
1337
+ its progress lines at 109-124, and is now at 123 with them at 105-121.
1338
+ - The tests turn on TIMING, not content: a buffered implementation could hand the same lines to the
1339
+ same callback by splitting at exit, so the line is required to arrive while the spawn's promise
1340
+ is still pending.
1341
+
1342
+ - **`tldrx plan` has a section in the CLI reference (#55).** `docs/guide/08-cli-reference.md` had no
1343
+ `## tldrx plan` heading at all, so `sync-dod` — the one mechanical repair for stories whose dod
1344
+ block a `workspace.yml` edit orphaned — was undocumented. The section states the four per-line
1345
+ outcomes, the git-history ancestry, the `.md.bak`, and that it runs no agent and moves no cursor.
1346
+ The #54 docs-coverage test is widened from `run` to `run` and `plan`, and now also asserts the
1347
+ heading rather than a passing mention. It is still a scoped list rather than a generalisation over
1348
+ every command: `hook`'s seven scripts are a deliberate spelling, and `note` and `ship` have the
1349
+ same gap #55 was about (filed separately).
1350
+
1351
+ - **`tldrx update`, and a one-line notice when a newer version exists (#62, owner decision
1352
+ 2026-09-01: on by default, with an opt-out).** The owner installed 0.4.0 on a second machine and
1353
+ found there was no way to ask the tool to update itself, and no way to be told a newer one existed.
1354
+ - **`tldrx update` is `npm i -g tldr-experts@latest`, run for you**, plus the part a wrapper does
1355
+ not give you: the CHANGELOG between the version you had and the version you now have.
1356
+ - **The new version is READ BACK from what npm installed** —
1357
+ `$(npm root -g)/tldr-experts/package.json`, with the delta taken from the `CHANGELOG.md` beside
1358
+ it. The process printing that line is the OLD build and cannot know what the new one is; when the
1359
+ read-back fails it says so and prints no changelog rather than inventing one.
1360
+ - **The notice never touches the network on the hot path.** A command reads
1361
+ `~/.tldrx/version-check.json` and nothing else. The registry call happens in a DETACHED child
1362
+ (`stdio: "ignore"`, `unref()`) spawned after the output is written, and its answer is for the
1363
+ NEXT invocation. Cached for 24 h; silent on any network failure, on a body that is not the JSON
1364
+ it asked for, and on a home directory it cannot write.
1365
+ - **Never in `--json`, never during a hook, never off a terminal.** `tldrx hook` and
1366
+ `tldrx statusline` are suppressed by name (spec §0: a hook is deterministic), `--json` anywhere
1367
+ in argv is suppressed, a non-TTY stdout is suppressed, and so is CI. The line itself goes to
1368
+ stderr, so no command's stdout changes shape.
1369
+ - One line, in the issue's own wording: `tldr-experts 0.5.0 available (you have 0.4.0) — tldrx
1370
+ update`. Asserted as an exact string, because "roughly this sentence" is how one line becomes
1371
+ three.
1372
+ - **Opt out** with `TLDRX_UPDATE_CHECK=off` for a shell (spelled like `TLDRX_UI` and
1373
+ `TLDRX_CLAUDE_BIN`; `0`, `false`, `no` and `never` also work), or `update_check: off` in
1374
+ `~/.tldrx/config.yml` for the machine. A config file that does not parse is not an opt-out and
1375
+ not a crash.
1376
+
1377
+ - **`budget.yml` no longer adds host tokens to dollars (#61, owner decision 2026-09-01).**
1378
+ `validateRunBudget` summed EVERY phase ceiling and compared the total to `ceiling_usd` — but since
1379
+ the `economy:` label landed, a phase priced in `host-tokens` carries a host-session token allowance
1380
+ and not money. A realistic allowance therefore made a valid file invalid
1381
+ (`phase ceilings sum to 200018 > ceiling_usd 25`), and it did so in the one check that fails
1382
+ **closed**: `RunStore.open` threw, and the budget-gate hook then denied every spawn on the run.
1383
+ - **Separate ceilings, per economy.** `ceiling_usd` stays dollars-only and a new optional
1384
+ `ceiling_host_tokens` — at the run level and per phase — is the host-token allowance. The two are
1385
+ never added and never converted: there is no exchange rate here, and inventing one would be a
1386
+ guess about a price, which is the whole reason the label exists.
1387
+ - **The sum runs once per economy.** Σ the `metered-usd` phases against `ceiling_usd`; Σ the
1388
+ `host-tokens` phases against `ceiling_host_tokens`. The dollar half is byte-identical arithmetic
1389
+ with the same refusal in the same words.
1390
+ - **Additive, and absence changes nothing.** With no `ceiling_host_tokens` declared, the token sum
1391
+ has nothing to compare against and is not checked — deliberately the lax side, since the only
1392
+ other number on the run is dollars. The compat bar is the live `260830-ordering-inventory`
1393
+ budget.yml, mid-Build in another workspace while this was written and asserted verbatim in
1394
+ `test/economy.test.ts`: it validates, its phase sum of exactly 62.00 against a 62.00 ceiling still
1395
+ passes, one cent more is still refused in the same words, and every phase still reads
1396
+ `metered-usd` with no token ceiling.
1397
+ - `hostTokenCeiling` (f353d8d, #22b) prefers the new field and **still falls back to `ceiling_usd`**,
1398
+ because the files written before it existed put the allowance in that one unlabelled scalar and
1399
+ they are still on disk. The emitter round-trips both, so `budget raise` cannot erase a token
1400
+ ceiling it rewrites past.
1401
+ - The workaround the issue documented is gone: `test/hooks.test.ts` and `test/economy.test.ts` no
1402
+ longer raise the run ceiling to `60000`/`100000` — dollar figures that meant nothing — to let a
1403
+ token phase validate.
1404
+ - **`RunStore.ceilingsToWrite` now carries a phase's `economy` and `ceiling_host_tokens` from disk**
1405
+ beside its `ceiling_usd`. That seam re-reads CEILINGS before every save so a raise landing
1406
+ mid-stage is not clobbered by the copy the process opened with — and a phase's ceiling is three
1407
+ fields, not one. Preserving the number without the label would have been the worse half of both:
1408
+ a token allowance kept on a phase this reader had been told is priced in dollars, where
1409
+ `hostTokenCeiling` can no longer see it. For a file with no labels and no token ceilings — every
1410
+ budget.yml on disk today, the live one included — all three are identical on both sides and the
1411
+ save writes exactly what it always wrote.
1412
+
1413
+ ### Removed
1414
+
1415
+ - **`templates/story.md` and `templates/epic.md` are deleted (#48, owner decision 2026-09-01,
1416
+ option (a)).** They stated the Plan front-matter schema, shipped in the npm package, and
1417
+ `grep -rn 'story\.md' src/` found nothing that read either one. Since 3ae0ce9 the live copy is
1418
+ generated: `src/core/plan/schemaContract.ts` builds the story, the epic and `waves.yml` from
1419
+ `STORY_KEYS` / `EPIC_KEYS` / `PLAN_STATUSES` / the `MAX_*` constants and splices them into the Plan
1420
+ prompt. The drift guard 7ac298c added held the two files to that contract; deleting them removes
1421
+ the second copy instead of maintaining it.
1422
+ - **The consumers were tests, and they now generate.** `test/plan.test.ts` and
1423
+ `test/plan-schema-contract.test.ts` were the only readers; both take the story and the epic from
1424
+ `planContractExamples()`. Nothing in `src/`, `stages/`, `workflows/`, `plugin/` or `docs/` read
1425
+ either file, and `run new` never copied them.
1426
+ - **`templates` stays in `package.json` → `files`.** The directory still ships eleven templates
1427
+ that ARE read at runtime — `templates/expert.md` and `templates/experts/<role>.md` are read by
1428
+ `createExpert.ts` and `roleExperts.ts` in an installed package — so removing the entry would
1429
+ break `tldrx expert create` to delete two files that no longer exist.
1430
+ - **The drift guard changed meaning and kept its teeth.** It now asserts the GENERATED story and
1431
+ epic validate through `validateStoryFile` / `validateEpicFile` with keys equal to `STORY_KEYS` /
1432
+ `EPIC_KEYS` in order, that neither file is back on disk, and that no OTHER shipped template has
1433
+ grown the same front matter under a new name. Proven, not assumed: setting the example's
1434
+ `status:` to `wip` turns 4 tests red, and renaming `test_plan` in the generator turns 6 red.
1435
+
1436
+ ## 0.4.0 — 2026-09-01
1437
+
1438
+ ### Changed
1439
+
1440
+ - **Dependent epics share ONE integration branch (#57, owner decision 2026-09-01, option (a)).**
1441
+ One-branch-per-epic assumes the epics are independent. Run `260829-scoring-leaderboard` planned
1442
+ E2 (the API) with E3 and E4 (mobile) consuming it, and a downstream story's base — cut from its
1443
+ own epic branch, itself cut from `main` — could not see the upstream epic's merged work. It broke
1444
+ twice, and both times the host fast-forwarded the EPIC branches by hand: cross-epic surgery done
1445
+ with a feature built for stale STORY bases (design §F.2), which collapses branches the owner may
1446
+ have meant to merge separately. A run whose epics form a dependency chain now cuts a single
1447
+ integration branch and the epics become labels.
1448
+ - **Detected at PLAN time, from what the plan already says.** A story whose `depends_on` names a
1449
+ story in another epic IS the chain. `validatePlan` reports the cross-epic edges it read
1450
+ (`PlanReport.epicChain`, deduplicated per epic pair), and the `plan` gate check states the branch
1451
+ model in its passing detail — `epics form a chain (E3→E2, E4→E2) → single integration branch
1452
+ \`epic/<run-id>\`` or `independent epics → one branch each`. The owner never discovers it
1453
+ mid-Build, which is the half of #57 that was not about branches at all.
1454
+ - **`epic/<run-id>`, not a new namespace.** `EPIC_BRANCH_RE`, `watch`'s feature-slug extraction,
1455
+ `ship`, `boundary` and the `--reuse-epic` guard are all keyed on the `epic/` prefix; an
1456
+ `integration/…` branch would have changed every one of them to buy a word. The run id IS in the
1457
+ name — unlike an ordinary epic branch, which is deliberately unscoped because an epic is the unit
1458
+ a team merges — because an integration branch belongs to one run by definition.
1459
+ - **One run-scoped epic worktree**, `_epic-<run>-integration`, because git will not check one branch
1460
+ out in two worktrees. It is picked up by the `_epic-<run>-` prefix that the §2.8 src resolver and
1461
+ the run-close cleanup (#16) already enumerate, so the lifetime decision from that change holds.
1462
+ - **Backward-safe by an ABSENT key.** The Build executor records what it used in `run.yml`
1463
+ (`build.branch_model: per-epic | integration`, additive and optional). A run.yml that names
1464
+ branches and no model predates the key — the three closed runs, and any run mid-flight — and stays
1465
+ `per-epic`, so it resumes on the branches it already cut rather than being re-pointed at one that
1466
+ was never cut. A model, once written, is never rewritten.
1467
+ - **Nothing else moved.** The dirty-tree refusal, the foreign-epic refusal and `--reuse-epic`, the
1468
+ gated HEAD, `git merge --no-ff` into the epic worktree, and the story-base fast-forward all behave
1469
+ exactly as before; only WHICH branch they name changes. `tldrx ship` needed no change: a chained
1470
+ run claims one branch, so it opens one PR with no `--branch`, and an unchained multi-epic run still
1471
+ asks which.
1472
+ - The acceptance test is the leaderboard shape, passing: S1 in E1 writes a file, S2 in E2 depends on
1473
+ it, and S2's worktree holds that file with **no `story.base_fastforwarded` event** — the base was
1474
+ right when the developer was dispatched, not repaired afterwards. Red before the change (`Expected:
1475
+ true, Received: false`).
1476
+
1477
+ - **An epic worktree now lives for the RUN's lifetime, not the Build stage's (#16, owner decision
1478
+ 2026-09-01, option (a)).** The shipped half of #16 made a `file` src resolve against
1479
+ `.tldrx/worktrees/<repo>/_epic-<run>-<epic>` before the working tree — and then `BuildSession.finish()`
1480
+ removed that directory before the Build handoff was even written, so unless the operator had typed
1481
+ `--keep-worktrees` a later Watch stage had nothing to resolve against and the fix bit only under a flag.
1482
+ Cleanup moves to run CLOSE, which is what the checkout actually belongs to.
1483
+ - **Watch citing epic-only code works by default.** Proved end to end rather than by construction:
1484
+ `test/build-executor.test.ts` runs the real pipeline, then resolves a `[src: app:s1.txt:1]` against a
1485
+ file the test first shows is committed on `epic/e1` and absent from the working tree.
1486
+ - **Every close path takes them**, because a run does not only close one way: `tldrx next` closing the
1487
+ last stage, `tldrx approve` signing the last gate, and `tldrx run cancel`. Without the last two the
1488
+ change would have traded one leak for another — a cancelled run's checkouts used to be gone already,
1489
+ because Build removed them on the way past.
1490
+ - **`--keep-worktrees` keeps its meaning, one scope wider**: survive even the run close. It is
1491
+ remembered on the run as `keep_worktrees:` (additive and optional; absent means clean up, which is
1492
+ what every existing run.yml meant) because the flag is typed on the `tldrx next` that BUILDS and the
1493
+ run is usually closed by a different command in a different process.
1494
+ - Story worktrees are untouched: still removed the moment a story reaches `done` or `blocked`.
1495
+
1496
+ - **The budget gate's three open policy questions are answered (#22, owner decision 2026-09-01).**
1497
+ bb6204b wired the DATA — both economies, `attended_by`, `runSpend` — and deliberately changed no
1498
+ verdict. These are the verdicts.
1499
+ - **(a) An `attended_by: host` run is INFORMED, never DENIED, on metered dollars.** `tldrx next` on
1500
+ such a run spawns nothing, so the estimate the gate was refusing against is spend that provably will
1501
+ not happen. Both the PreToolUse hook and `tldrx next`'s own brake now say every number they would
1502
+ have refused with, plus both economies, and allow. The event is `budget.warned`, not `budget.blocked`:
1503
+ nothing was blocked, and recording a block that did not happen is the exact failure #22 was filed
1504
+ about.
1505
+ - **(b) A `host-tokens` ceiling is soft-enforced.** Under that economy the ceiling NUMBER is a
1506
+ host-session token allowance, so accumulated declared `tokens:` against it is the one comparison in
1507
+ the gate whose two sides share a unit. Crossing it WARNS and still allows. It stops only under the
1508
+ explicit opt-in **`on_host_tokens_exceed: block`** in `budget.yml` — an enum beside `on_exceed`,
1509
+ defaulting to `warn`, so every file written before the key existed keeps the behaviour it had. The
1510
+ refusal never offers `tldrx budget raise`, which moves dollars and would send the operator at the
1511
+ wrong number. (a) beats (b): the opt-in still never denies an attended run.
1512
+ - **(c) `remainingWork` zeroes the developer share on an attended run**, mirroring `economy:
1513
+ host-tokens`, because it is the same fact — the host session pays for those turns. Attendedness and
1514
+ the phase economy were independent, so an attended run on a `metered-usd` phase still counted
1515
+ developer turns against money this framework will never spend, on the brake and in `run estimate`
1516
+ alike. Reviewer floors are untouched in both cases.
1517
+
1518
+ ### Added
1519
+
1520
+ - **The documentation site speaks Spanish (`docs-site/es/`, phase 2).** All twelve English pages are
1521
+ now mirrored under `/es/` at the same paths and translated into es-MX developer Spanish, and
1522
+ `locales.es.themeConfig` carries the Spanish sidebar, nav, edit link and page chrome (outline,
1523
+ prev/next, search modal, footer) rather than English chrome around Spanish prose. The placeholder
1524
+ that phase 1 left at `/es/` is replaced by the translated landing page.
1525
+ - **tldrx's own vocabulary stays in English where it is an identifier.** The stage names
1526
+ (`What → How → Plan → Build → Watch`), `run`, `story`, `DoD`, `scope`, `handoff` and `workspace`
1527
+ are the things you type or the files on disk, so they are not translated; each is glossed once in
1528
+ Spanish where it first appears. `gate` is the one exception — it renders as **compuerta**, because
1529
+ the phase-1 placeholder had already shipped that word to the live site.
1530
+ - **Code blocks and command output are verbatim English** — a translated transcript would be a
1531
+ transcript of a command nobody ran. The narration around them is translated.
1532
+ - **Anchors were verified against the rendered HTML, not assumed**, because dead-link checking does
1533
+ not see them. That mattered here: VitePress's slugifier strips accents but *keeps* `¿`, so
1534
+ `## ¿Cómo lo detengo?` becomes `id="¿como-lo-detengo"`. The one heading that is linked to carries
1535
+ an explicit `{#puedo-manejarlo-desde-claude-code}`. A sweep of the built site resolves all 5
1536
+ anchor links across its 26 pages, 0 broken.
1537
+ - **Release notes are deliberately not translated**: the page is generated from `CHANGELOG.md` at
1538
+ build time, so a Spanish copy would drift. The `/es/` sidebar links the English page and says so.
1539
+
1540
+ - **A public documentation site, written for people who have never seen tldrx (`docs-site/`, phase 1).**
1541
+ A VitePress site deployed to GitHub Pages by `.github/workflows/docs.yml` on any push to `main` that
1542
+ touches `docs-site/` or `CHANGELOG.md`. Twelve short English pages — a landing page, a Quickstart, one
1543
+ page per concept (the five stages, files-as-state, gates, evidence, budgets), four guides and a
1544
+ condensed CLI overview — plus a generated changelog and a Spanish placeholder. None of them is pasted
1545
+ from `docs/`, which stays the agent-facing reference. Every command
1546
+ and every block of output on the Quickstart was produced by running the real binary; nothing on the
1547
+ site documents a flag that `--help` does not.
1548
+ - **The changelog page is generated, never copied.** `docs-site/scripts/gen-changelog.ts` reads this
1549
+ file at build time and emits one line per entry, so a release note reaches the website without
1550
+ anybody maintaining a second copy of it. The generated page is gitignored for the same reason.
1551
+ - VitePress dead-link checking is left ON and the build is green with it — proven by a probe, not
1552
+ assumed: a deliberate link to a missing page failed the build with `1 dead link(s) found`. (Anchors
1553
+ are NOT checked by it, so `#fragment` targets were verified against the rendered HTML by hand.)
1554
+ - i18n is wired now, with the English content at the root and a Spanish placeholder under `/es/`, so
1555
+ phase 2 is a matter of adding files rather than restructuring the site.
1556
+ - `docs-site/` is excluded from the npm package (it is not in `files:`) and from `tsc --noEmit` (the
1557
+ root tsconfig includes only `bin`, `src`, `test`) — measured: `npm pack --dry-run` still lists 52
1558
+ files and none of them is under `docs-site/`.
1559
+
1560
+ - **`tldrx learn` chapters 3-8 — the whole loop, played (#30, phase 2).** The tutorial now runs end to
1561
+ end in about five seconds of real commands: **3** the gate (`approve --note`, and the record it writes
1562
+ in `run.yml`), **4** one story built for real (How's `auto` gate closing itself over its seven
1563
+ conditions, Plan's human gate, then a Build that cuts `epic/bulk-pricing`, spawns a developer in a
1564
+ worktree, re-runs the story's `npm run test` DoD, commits, merges and spawns a reviewer), **5** a
1565
+ genuinely red DoD and the three commands back from it (`story reopen`, `reject`, `budget raise`),
1566
+ **6** an `agent` gate closed by `approve --as-agent` over a structured evidence note, **7**
1567
+ `run attend host`, the refusal a bare `next` then gives, and the `next --prepare` / `next --commit`
1568
+ pair actually run, **8** `cost --all`, `run estimate`, and the budget brake refusing a stage the
1569
+ phase can no longer afford.
1570
+ - Chapter 4's DoD is real, chapter 5's failure is real: the story's test script is `exit 0` until a
1571
+ developer replaces it with a `node` test that then catches a wrong number — so the tutorial teaches
1572
+ "a green DoD over an empty test proves nothing" by letting it happen rather than by saying it.
1573
+ - Chapter 5 opens a second run, so `{run}` and `{runDir}` now expand in a step's `command` as well as
1574
+ in a turn's writes, and mean **the newest run that is still open** — the same set `resolveRun` picks
1575
+ from, so the placeholder and the CLI cannot disagree about which run a command means.
1576
+ - Chapter 4's `prepare()` commits what `init` left untracked, because the Build executor refuses to
1577
+ cut a branch from a dirty tree (measured: `?? .gitignore, ?? CLAUDE.md`, exit 2) — and the narration
1578
+ teaches that refusal instead of hiding it.
1579
+ - **Fixed: the toy repo now carries its own git identity.** Chapter 4's Build commits through the
1580
+ framework's own executor, with whatever identity the machine has — so on a box with no global
1581
+ `user.email` (a fresh laptop, a container, `ubuntu-latest`) `git commit` failed with `Author identity
1582
+ unknown` and the chapter died three commands in. `makeSandbox` writes `user.email`, `user.name` and
1583
+ `commit.gpgsign=false` into the sandbox repo's own config, on every open, so an older sandbox is
1584
+ repaired too. The test pins it with `user.useConfigOnly` rather than an empty global config: git
1585
+ guesses an identity from gecos and hostname and only fails where it cannot, so an empty config
1586
+ passes on a laptop and fails in a container — which is exactly how this reached CI.
1587
+
1588
+ - **`tldrx learn` — a playable sandbox tutorial that runs the REAL commands (#30, phase 1 of 3).**
1589
+ A tutorial that runs the shipped binary can never drift from the shipped behaviour: every output the
1590
+ learner reads is produced by the code, not written down by a doc author. `tldrx learn` scaffolds a
1591
+ throwaway workspace (a four-file git repo with a `test` script that exits 0), then narrates, shows the
1592
+ exact command, waits for Enter and RUNS it — `tldrx init`, `run new`, `next`, `answer` are the real
1593
+ ones, against that sandbox.
1594
+ - **Chapters 1 and 2 ship playable**: init (read the `workspace.yml` detection actually produced) and
1595
+ the What stage (a question comes back, `tldrx answer` records it, it becomes `F001` in
1596
+ `.tldrx/memory/facts.yml`). Chapters 3-8 from the issue are phase 2 and are DATA plus one
1597
+ `assert()` — see the contract in `src/core/learn/Chapter.ts`.
1598
+ - **It cannot spend money, by construction rather than by convention.** The sandbox writes its own
1599
+ `claude` stand-in, names it in `TLDRX_CLAUDE_BIN` and puts it first on the child `PATH`, so neither
1600
+ the spawn seam nor a bare `claude` on `PATH` can reach the real CLI. `test/learn.test.ts` proves it
1601
+ the only way worth proving: it plants a booby-trapped `claude` that writes a marker file, plays both
1602
+ chapters, and asserts the marker is absent AND the chapters completed — because a tutorial that
1603
+ spawned nothing would pass a marker check for the wrong reason.
1604
+ - **It cannot touch your work.** Everything is written under the sandbox directory (`~/.tldrx-learn`
1605
+ by default, `--sandbox` to move it), and a sandbox that would sit inside a real tldrx workspace is
1606
+ refused before a byte is written.
1607
+ - **Files as state, like everything else**: `progress.json` in the sandbox is what makes a bare
1608
+ `tldrx learn` resume, `--chapter <n>` jump (playing an unfinished prerequisite first), and `--reset`
1609
+ start over. With no terminal on stdin the chapters play straight through rather than hanging at the
1610
+ first prompt.
1611
+ - The stand-in agent is scripted per chapter and is **fail-closed**: a prompt no turn matches exits 1
1612
+ and names the turns it did have, so a hole in the tutorial is a loud failure rather than an
1613
+ improvised answer. The `stream-json` writer moved from `test/fixtures/fakeStream.ts` to
1614
+ `src/core/facilitator/fakeTranscript.ts` — beside `agentEvents.ts`, which reads that format — so the
1615
+ tutorial's stand-in and the four test fakes cannot drift apart. The fixture re-exports it.
1616
+
1617
+ - **`tldrx note <run> [--stage <id>] "text"` — an honest carrier for an operator annotation, at the
1618
+ moment it happened (#46).** Measured on `260829-scoring-leaderboard` (2026-09-01): a host performed an
1619
+ owner-delegated mechanical resync of eight story dod blocks, was asked to note it in the run log, and
1620
+ could not — `events.jsonl` is append-only and tool-owned, so the only carriers were a FUTURE gate note
1621
+ (late, and keyed to a decision the note is not about) or a `reject` (destructive). The session ended up
1622
+ hanging the context off an unrelated `story.reopened`.
1623
+ - **One event, and nothing else.** It appends a single `operator_note` line carrying actor, timestamp,
1624
+ optional stage and the text. It does not go through `RunStore.save()`, which would rewrite `updated_at`
1625
+ and re-derive every status for an annotation that changed no state: `test/operator-note.test.ts`
1626
+ compares `run.yml` and `budget.yml` **byte for byte** across the call, because "safe to reach for
1627
+ mid-run" is the whole of what makes the verb usable.
1628
+ - **Every refusal writes nothing.** An unknown run (exit 3), a stage this run does not have (exit 2), an
1629
+ empty note (exit 1) and a lone argument that turns out to name a run all refuse before the log is
1630
+ opened. The last one is the trap worth naming: `tldrx note 260829-x` is a half-typed command, not a
1631
+ note whose entire content is a run id, and recording it would be the one outcome nobody wanted.
1632
+ - **Visible afterwards.** `tldrx run status` prints the last three (with `tldrx replay` named when there
1633
+ are more), `--json` carries them as `operator_notes`, and `tldrx replay` narrates every one in place.
1634
+
1635
+ - **`tldrx run gates set <stage>:<policy> --note "…"` — the signed upgrade path for a frozen
1636
+ `gates_policy` (#14).** The policy is resolved at `run new` and frozen into `run.yml` by design, and that
1637
+ default is not taken back here. What it left with no door at all, found on the 2026-08-30/31 unattended
1638
+ pilots: a run created BEFORE the `agent` policy existed can never use `approve --as-agent`, and `run.yml`
1639
+ is hand-edit-forbidden (spec §1) — so the only move was to abandon the run.
1640
+ - **Built like `story reopen`, because it is the same kind of act:** a person overruling state the machine
1641
+ is holding. A `--note` is required; ONE stage per invocation (a list would let a second change ride on
1642
+ the first one's signature); the entry must be QUALIFIED, because under `--gates` a bare `plan` means
1643
+ `human` and a signature must not rest on a default; and a no-op is refused rather than recorded.
1644
+ - **One `gate.policy_changed` event** carries the phase, who signed it, the old and the new value, and the
1645
+ note. A run with no `gates_policy:` at all gets the FULL map written — every stage explicitly, with the
1646
+ one change applied — because a partial map would quietly claim its other stages had been decided too.
1647
+ - It changes who may CLOSE a gate from then on. Gates already signed are untouched, and nothing re-reads
1648
+ the policy of a closed one.
1649
+
1650
+ - **`tldrx ship` — open a PR from the run's epic branch, with the handoff as the body (#15).** The loop
1651
+ ended at "merge by hand": a finished epic sat on `epic/<slug>`, the document explaining it sat in
1652
+ `<run>/<phase>/handoff.md`, and nothing carried either one to a PR.
1653
+ - **It never pushes.** `core/build/git.ts` has no `git push` wrapper on purpose (spec §5), and this verb
1654
+ keeps that rule rather than being the exception to it: a branch the remote has not seen is a refusal
1655
+ that names the exact `git -C … push -u` command. Publishing a branch is a decision, and it stays the
1656
+ operator's.
1657
+ - **It never writes to the run** — no event, no gate, no cursor, no money — and it does not mirror
1658
+ tickets: `tldrx tickets sync` already is that verb, holds the `process.yml` contract and appends
1659
+ `ticket.synced`. A second, thinner mirror inside `ship` would give the workspace two answers to "is this
1660
+ story mirrored", so `ship` names it as the next step instead.
1661
+ - **Clean refusals, in a sentence:** no epic branch, no handoff, no `gh`, no remote, an unpushed branch,
1662
+ several epic branches with no `--branch`, a branch the run did not cut. The body goes to `gh` as
1663
+ `--body-file`, never as an argument, so a long handoff cannot overflow an argv limit.
1664
+ - Both external binaries go through one narrow transport that takes a cwd — the same idea as
1665
+ `adapters/transport.ts`, and the only way to ASSERT the argument shape of a command the suite must not
1666
+ run. The unit tests drive a recording fake; the one end-to-end test puts a STUB `gh` first on PATH in a
1667
+ throwaway workspace with a throwaway bare `origin`. The real `gh` is never invoked by a test.
1668
+
1669
+ - **`TLDRX_CLAUDE_BIN` — point the sub-agent spawn at a different binary (#27, minimal slice).**
1670
+ `spawnAgent` hardcoded `claude`, so a pinned install, a wrapper that adds a proxy or credentials,
1671
+ and a stand-in in a sandbox all required patching source. The variable replaces the executable
1672
+ NAME and nothing else — the argv is still Claude Code's, so what it points at has to speak
1673
+ `-p --output-format stream-json --json-schema` — and blank or whitespace counts as unset. Honoured
1674
+ everywhere the CLI is spawned: `spawnAgent`, the `--dry-run` command line (`describeSpawn`) and
1675
+ `claude mcp list` (`McpProbe`). `tldrx doctor` deliberately still checks `claude --version`,
1676
+ because `env.yml` declares that string. Documented under **Environment variables** in the CLI
1677
+ reference. This is not the provider abstraction #27 asks for; #27 stays open for it.
1678
+
1679
+ - **A drift guard on `templates/story.md` and `templates/epic.md` (#48).**
1680
+ Both ship in the npm package, both state the Plan front-matter schema, and **nothing in `src/`
1681
+ reads either one** — a second copy of a contract whose first copy is computed from `STORY_KEYS`
1682
+ and `EPIC_KEYS`. Add a required key and `schemaContract.ts` stops compiling while the templates
1683
+ say nothing; a human then opens one, writes a story the check refuses, and the framework looks
1684
+ broken. They now go through `validateStoryFile` / `validateEpicFile` — the very checks the stage
1685
+ gates on — with their key sets asserted equal to `STORY_KEYS` / `EPIC_KEYS` in order, and the
1686
+ status enum each spells out in a comment asserted equal to `PLAN_STATUSES`. Proven to have teeth:
1687
+ renaming one key and staling one enum comment turns three tests red. Whether the files should be
1688
+ generated or deleted is a packaging decision and is left open on #48.
1689
+
1690
+ - **The merge-wave sandbox is built under a hostile `init.defaultBranch` (#49).**
1691
+ `test/merge-wave.test.ts` names every repo `main`, and CI run 33459567355 failed in the test's
1692
+ own setup — `git push -q origin main` → `src refspec main does not match any` — on a runner whose
1693
+ default branch is not `main`. `f1ffe56` had already fixed it (`-b main` on both inits,
1694
+ `--branch main` on the clone), but nothing EXERCISED the fix: on a `main`-defaulting host,
1695
+ removing the treatment changes nothing. The sandbox now pins `init.defaultBranch: trunk` for
1696
+ every git command it builds itself with, the clone asserts it is on `main` rather than
1697
+ discovering it five lines later, and two tests pin the mechanism — untreated reproduces the CI
1698
+ error verbatim, treated does not.
1699
+
1700
+ - **The Plan prompt now STATES the schema the `plan` check enforces, generated from the check itself (#35, #38).**
1701
+ `stages/plan/stage.md` named the output filenames — `stories/<id>.md`, `epics/<epic>.md`, `waves.yml` — and
1702
+ said nothing about their shape, so a fresh agent learned it by having a paid attempt refused. Measured twice
1703
+ in two days: on `260831-hardening-d1` the plan sub-agent followed the rendered bundle faithfully and wrote
1704
+ seven stories as plain markdown (`no YAML front matter — the file must open with ---`), and on
1705
+ `260829-scoring-leaderboard` it wrote a 1,009-character acceptance item against a `MAX_ITEM_CHARS = 512`
1706
+ cap that appeared in no file it could read. Both attempts were consumed, correctly and uselessly.
1707
+ - **Generated, not copied.** `src/core/plan/schemaContract.ts` renders a `## Output schemas` section from
1708
+ `STORY_KEYS`, `EPIC_KEYS`, `PLAN_STATUSES` and the six `MAX_*` constants — the same definitions
1709
+ `validateStory`, `validateEpic` and `validateWaves` read. `Record<StoryKey, Field>` is load-bearing:
1710
+ add a key to a schema and the file stops compiling until the new key has a value and a rule. The worked
1711
+ examples it ships are run through `validatePlan` itself in the tests, so the contract the prompt states
1712
+ and the contract the check enforces are provably the same one. This repo already had the other kind:
1713
+ `templates/story.md` carries the schema correctly and `grep -rn 'story\.md' src/` finds nothing that
1714
+ reads it.
1715
+ - **Only where the check runs.** `applyCheckContracts` splices it under an H2 the framework owns, for a
1716
+ stage that declares `checks: [plan]` AND writes `waves.yml` — the same predicate `checkPlan` skips on,
1717
+ now shared (`writesPlanArtefacts`). A What or How prompt is byte-identical to before. It goes into
1718
+ `stage.md` rather than after the inputs because `prompt.ts` orders the document most-stable-first for
1719
+ the prompt cache, and a section computed from constants is exactly as stable as the stage body.
1720
+ - **~4.2 KB against a $4 stage.** The alternative it replaces is a refused attempt per fresh workspace.
1721
+
1722
+ - **`tldrx plan sync-dod` — the mechanical repair for dod blocks an edited `workspace.yml` orphaned (#42).**
1723
+ A story's ```dod block may only name commands `workspace.yml` declares, byte for byte, and that rule is not
1724
+ relaxed by a byte here — it is what stops a data file from running an arbitrary command as you. What it
1725
+ lacked was an inverse. Measured live 2026-08-31 on `260829-scoring-leaderboard`: fixing `workspace.yml`
1726
+ (a filtered `test:`, `lint:` deleted) instantly invalidated the dod blocks of **8 approved stories**, and
1727
+ the only recoveries were hand-editing agent-approved artefacts or re-running the whole Plan stage — a paid
1728
+ turn to change two lines in eight files, churning thirteen correct stories on the way.
1729
+ - **Evidence, not similarity.** The ancestry comes from git's history of `.tldrx/workspace.yml`: a line a
1730
+ previous version declared under a role the current file still has becomes that role's current command; a
1731
+ line whose role is gone is dropped; a line the current file already declares is left alone; and a line no
1732
+ version ever declared — or one two roles once shared and now disagree on — is **flagged, its story left
1733
+ byte-identical**, and the command exits `2`. Guessing at a rename by string shape is the one thing this
1734
+ must not do. In a workspace with no git history there are no ancestors, so everything non-current is
1735
+ flagged.
1736
+ - **It touches nothing else.** Front matter, prose, blank lines and the fences come back byte-identical;
1737
+ only the command lines inside the fence move. The previous version is kept at `<story>.md.bak`
1738
+ (`writeAtomic`), the summary is a per-story diff, `--dry-run` prints it and writes nothing, and the result
1739
+ is validated by the same plan check the drift came from. Stories that CAN be synced still are — one
1740
+ undecidable line is not a veto on the other seven files.
1741
+ - **The drift message now names the remedy.** "`<cmd>` is not one of .tldrx/workspace.yml's commands — a
1742
+ story may not invent one" gained a second sentence pointing at `tldrx plan sync-dod`. Only for a STORY:
1743
+ a stage's `cmd:` is a line a human wrote, and `sync-dod` does not touch stage files.
1744
+
1745
+ - **`tldrx answer <Qn> "…" --supersede` — the verb for reversing a decision already on record.**
1746
+ Found live 2026-08-31: an owner reversed an answered decision after the risk behind it was
1747
+ refuted, and `tldrx answer` refused ("Q1 is not an open question") because an answer is
1748
+ recorded once. `superseded_by` had been in the §2.5 schema since the first draft with **no
1749
+ command that wrote it**, so the only route was a hand edit of `facts.yml` — and a hand edit
1750
+ that left `superseded_by: null` left the reversed decision inside `FactsStore.active`, which
1751
+ every stage reads as never-re-ask truth. The next run would have reinstated the call the owner
1752
+ had just taken back.
1753
+ - **The verb.** Valid only on an ANSWERED question (on an open one it exits `1` and says to
1754
+ answer it normally; without the flag an answered one still exits `3`, now naming
1755
+ `--supersede`). It appends a new fact carrying the whole new answer with the same `area` and
1756
+ `repos` and ordinary provenance, sets the old fact's `superseded_by` and the new one's
1757
+ `supersedes` — both halves, through `FactsStore.supersede`, under the workspace lock, so the
1758
+ reciprocity rule cannot be broken — and never edits a byte of the old fact's text. Reversing
1759
+ twice supersedes the SECOND fact: the chain is walked to its head, so it stays single-link.
1760
+ - **The questions block is appended to, not rewritten.** The original `[Answer]:` line and its
1761
+ footer stand; a superseding answer line and a `reanswered_by | reanswered_at | fact |
1762
+ supersedes` footer go under them. `status:` stays `answered`, because it is.
1763
+ - **Every reader that feeds a decision now skips a superseded fact.** This was half the work
1764
+ and none of it was new code: `superseded_by` had a writer for the first time, and six readers
1765
+ had been filtering on retirement alone. `isLive` (`core/facts/Fact.ts`) is now the one
1766
+ predicate behind `FactsStore.active` (no-re-ask, `tldrx run new --from` de-duplication),
1767
+ `findDuplicate`, `renderFacts` (the `{{facts}}` section of **every** prepared prompt),
1768
+ `renderWatchFacts`, `runFacts` (the implicit plan's "this run's answers") and `relevantFacts`
1769
+ (the training miner). One test had pinned the old behaviour in words — "a
1770
+ superseded-but-not-retired row stays visible" — and that was the bug, not the rule.
1771
+ - **History readers still show it, labelled.** `tldrx replay` renders the new `fact.superseded`
1772
+ event as its own line — the one moment the workspace's durable memory changes its mind was
1773
+ the one moment replay could not narrate — and `tldrx retro` lists the old fact with
1774
+ `(superseded by F<n>)` beside it.
1775
+
1776
+ ### Fixed
1777
+
1778
+ - **`tldrx learn` — the cold-player QA round (#30).** A first-time player played all eight chapters
1779
+ and returned SHIP-with-fixlist. Everything they found is fixed or recorded:
1780
+ - **Chapter 8 no longer lies about the brake.** It said "the phase has already spent its Watch
1781
+ money" while the tool printed `$1.89 left … estimate is $2.00`. The real mechanism is that a
1782
+ re-run is priced at the stage's WHOLE declared `budget_usd`, never at what a second attempt might
1783
+ add — so a stage that has spent anything can no longer afford itself. The chapter now says that,
1784
+ quotes both figures, and `assert()`s them against the `budget.blocked` event so the numbers cannot
1785
+ drift away from the sentence.
1786
+ - **Chapter 1 no longer promises something chapter 2 does not deliver.** `--no-interview` skips
1787
+ *init's* setup interview, which no chapter covers; the forward reference is gone and the debrief
1788
+ now sends the learner to `.tldrx/init-handoff.md`, where measured/inferred/assumed and
1789
+ `[src: …]` / `absent:` actually live.
1790
+ - **`tldrx learn --chapter <n>` refuses a chapter that is already played**, up front and by name,
1791
+ instead of narrating it and then dying mid-chapter on `run new: … already exists` (exit 1,
1792
+ measured). The refusal names `--reset` and the chapter a bare `learn` would resume at.
1793
+ - **The tutorial has a door out.** The ending now names the first four commands to type on a real
1794
+ repo — `tldrx init` (with the warning that it runs an interview by default), `run new --scope
1795
+ hotfix`, `next`, and `tldrx ship`.
1796
+ - **Chapter 7 RUNS `next --prepare` and `next --commit`** against the feature run's Watch stage
1797
+ instead of describing them in a debrief. Chapters 6 and 7 swapped for it: the attended chapter
1798
+ addresses the feature run through `{run}`, so the hotfix run has to be signed off first.
1799
+ - **Every non-zero exit code is printed** (`→ exit 4`), so the code chapter 2 teaches is a thing
1800
+ the learner reads rather than a thing they are told. Chapter 5 now also demonstrates the exit-2
1801
+ refusal a bare `next` gives with two runs open, and names the two run-id spellings.
1802
+ - Jargon defined at first use — expert, level 0, the `claim-sources` / `no-reask` / `budget-gate`
1803
+ bracket, `boundary`, `[src: …]`, `absent:`, economy, §2.11 — and the `expert … has no evidence`
1804
+ nudge explained once instead of repeating unexplained nine times.
1805
+ - Known and NOT fixed: a step's stderr (where the agent stream lives) is buffered and printed after
1806
+ its stdout, so a summary can appear before the stream that produced it. Interleaving needs an
1807
+ `onStderrLine` on the runtime seam and in both implementations; documented in `engine.ts`.
1808
+
1809
+ - **`tldrx cost` no longer claims "two economies" over one (#56).** The `(no total: two economies, no
1810
+ exchange rate)` footnote was unconditional, so a run whose every attempt was metered in dollars was
1811
+ told no total could be printed. It is printed only when both economies are actually present.
1812
+
1813
+ - **The README's "Not on npm yet" warning was false and told readers not to run the install line
1814
+ directly underneath it.** The package IS published: `npm view tldr-experts version` → `0.3.1`,
1815
+ exit 0. The warning is removed rather than re-dated — the npm badge at the top of the README
1816
+ already shows the live version, so nothing in its place can go stale the same way. The
1817
+ `npm i -g tldr-experts` line it was contradicting is unchanged.
1818
+
1819
+ - **The site's own home page linked an anchor that does not exist.** The hero's "Try it offline,
1820
+ free" button pointed at `/quickstart#try-the-whole-thing-first-for-free`, but the heading renders
1821
+ as `id="first-try-it-for-free"` — verified against the LIVE page, not just a local build. Dead-link
1822
+ checking never saw it because VitePress does not check fragments. Repointed, and a sweep of the
1823
+ built site now resolves every anchor link it emits.
1824
+
1825
+ - **`tickets sync`, `tickets status` and `budget show` took a run id as a positional that neither
1826
+ their `usage` nor their `--help` declared (#53).** Measured at `7ac298c`:
1827
+ `tldrx tickets status zzz-positional-probe` and `tldrx budget show zzz-positional-probe` both reach the
1828
+ run resolver and exit `3` with `no run 'zzz-positional-probe'`, so both forms have always been
1829
+ supported. The mechanism is the same in each: the subcommand word is consumed by the dispatcher
1830
+ (`tickets.ts:53`, `budget.ts:32`) before `stringFlag(args, "run") ?? args.positionals[0]` runs
1831
+ (`tickets.ts:246`, `budget.ts:48`), so `positionals[0]` is a run id by then.
1832
+ - **The capability is DECLARED, not removed.** `usage` now reads `tldrx tickets sync [<run>] …`,
1833
+ `tldrx tickets status [<run>] …` and `tldrx budget show [<run>] …`, and both help entries gain the
1834
+ `[<run>]` arg every other run-scoped command already carries. Nothing about what the CLI accepts
1835
+ changed.
1836
+ - **This is the axis #51's guard cannot see.** That guard compares the registry to the usage; here the
1837
+ registry itself was narrower than the code, and where both are silent both are green. The new check
1838
+ in `test/cli.test.ts` is a hand-written list — nothing derives a positional from source — but its
1839
+ BEHAVIOURAL half spawns the real CLI against a throwaway workspace, so it also goes red if the
1840
+ capability is ever removed, which is the direction a tidy-up of the arg parsing would break it in.
1841
+
1842
+ - **`tldrx run gates set` was documented nowhere in `docs/` (#54).** `grep -rn "gates set" docs/` returned
1843
+ nothing at `7ac298c`, so the CLI reference — the page a reader lands on from the README — described
1844
+ seven of `tldrx run`'s eight subcommands. It matters more than an ordinary docs gap because `gates set`
1845
+ is the ONLY sanctioned way to move a `gates_policy` that `run new` froze, and the situation it exists for
1846
+ (a run opened before the `agent` policy existed, which can otherwise never use `approve --as-agent`) is
1847
+ one an operator hits mid-run and searches the docs for. What they found was "abandon the run".
1848
+ - Documented in all three places the question gets asked from: the `tldrx run` usage block and a new
1849
+ prose entry in `docs/guide/08-cli-reference.md`; a **Moving a frozen policy** section in
1850
+ `docs/guide/03-runs-and-gates.md`, right under the paragraph that explains the freeze, with the
1851
+ `--gate-agent` disclaimer further down now linking to it; and `docs/spec.md`, both in §2.2's
1852
+ `gates_policy` row and as its own §CLI row (exits `0,1,2,3`, each measured).
1853
+ - Every copy carries the two facts a usage line cannot: **`--note` is mandatory**, and the change
1854
+ appends one **`gate.policy_changed`** event with the actor, the moment, the note and the old→new
1855
+ value — the whole audit trail for a mutation nobody would otherwise go looking for.
1856
+ - Guarded: `test/cli.test.ts` now asserts the CLI reference names every subcommand in
1857
+ `runCommand.subcommands`, plus `gate.policy_changed` by name. Scoped to `run` on purpose —
1858
+ `plan sync-dod` has no section on that page at all (a separate gap, unfiled), and `hook`'s seven
1859
+ scripts are deliberately documented as one `<script>` slot.
1860
+
1861
+ - **The epic file duplicated every story's status, and nothing ever updated the copy (#50).**
1862
+ Measured on `260829-scoring-leaderboard` (2026-09-01): `03-plan/epics/E1.md` listed S1, S2 and S3 as
1863
+ `todo` in its `## Stories` table while `03-plan/stories/S1.md` said `done` (merged at `0a50660`,
1864
+ `task.done` in `events.jsonl` at 23:46:11Z) and S2 said `in_progress`. Nothing had lied — nothing had
1865
+ written, either. Fixed by **removing the copy**, not by adding a second writer, and the repo already
1866
+ drew that line for the one field it does maintain: the epic's front-matter `status:` is DERIVED and
1867
+ written by `BuildExecutor.updateEpicStatus` (`build.ts:2552`) from the story files. A copy with a
1868
+ writer is a cache; a copy without one is a lie waiting to be read.
1869
+ - **Nothing parsed the table.** `validateEpicFile` is front matter only ("the front matter is the whole
1870
+ schema", `schemas/epic.ts:69`), `adapters/body.ts:50` mirrors an epic to a ticket as a bare list of
1871
+ ids with no status, and the dashboard reads the front matter. A writer would have been maintaining a
1872
+ document with no reader.
1873
+ - **Both copies of the shape are fixed, not just the visible one.** `templates/epic.md` now points at
1874
+ `03-plan/stories/<id>.md` instead of tabulating it, and the GENERATED contract the Plan sub-agent
1875
+ reads (`schemaContract.ts`, spliced into the stage prompt) now says "Do NOT restate a story's status,
1876
+ repo or `depends_on`" — without that, the next Plan agent invents the table again, which is how it
1877
+ got there.
1878
+ - `test/plan-schema-contract.test.ts` runs the issue's acceptance: build a plan from the shipped
1879
+ templates, flip S1 to `done` through `updateStoryFront` (the writer the Build executor uses), then
1880
+ grep the epic. Any `S<n>` + status word on one line of the epic body is a claim, and the claim set
1881
+ must be empty.
1882
+
1883
+ - **Seven `usage` strings were narrower than the same command's `--help` (#51, after #25).**
1884
+ `usage` is what a BAD invocation prints — `run.ts:85`, `questions.ts:38`, `tickets.ts:60`, `gate.ts:44`
1885
+ and three more write `<cmd>.usage` to stderr — so it is the string an operator reads at the exact
1886
+ moment they got the invocation wrong, and it was hiding flags the code accepts. Widened: `run attend`,
1887
+ `run status`, `run estimate`, `run auto`, `run unlock` and `run cancel` now show `[--run <id>]`
1888
+ (all six read `args.positionals[0] ?? stringFlag(args, "run")`); `tldrx next` shows it too
1889
+ (`next.ts:48`); and `tldrx questions lint` shows the `[<run>]` positional it has always taken
1890
+ (`questions.ts:49`, and `docs/guide/08-cli-reference.md` had been documenting it for longer than the
1891
+ CLI admitted it).
1892
+ - **The guard is subcommand-aware, and that is not gold-plating.** A plain
1893
+ `usage.includes("--run")` calls `run` CLEAN, because the new `run gates set` line names `--run` —
1894
+ measured, the naive check saw **one of run's seven gaps**. `test/cli.test.ts` scopes a flag that
1895
+ declares a `sub:` to that subcommand's block of the usage, and falls back to the whole string for a
1896
+ `sub:` that is a MODE rather than a word in argv (`dashboard --out {sub: "static"}`, which the first
1897
+ draft reported as a gap it is not).
1898
+ - **Three of the seven were spelling, not gaps, and are allowlisted with the reason:** `seed`'s
1899
+ `<Qid> "<text>"`, `watch`'s `check <feature>` and `hook`'s enumerated script names all say the same
1900
+ thing more specifically than the registry's general name. A fourth, `run`'s
1901
+ `<stage>:<human|auto|agent>`, is the same case. The allowlist is itself checked: every entry must
1902
+ still name a declared flag or arg, so a rename turns an exemption red instead of silent.
1903
+ - **`tickets --dry-run` was left OUT on purpose.** `tickets sync` previews by default and `--apply` is
1904
+ the write; advertising `--dry-run` would imply the opposite, and `test/money-safety.test.ts:319`
1905
+ asserts its absence. Recorded in the allowlist as a decision rather than papered over as a gap.
1906
+
1907
+ - **A literal ESC byte in `McpProbe.ts`'s ANSI regex (#52).** `src/core/doctor/McpProbe.ts:11` wrote a raw
1908
+ `0x1b` where `\x1b` was meant, so the source read `/<ESC>\[[0-9;]*m/g` and a reader — in a diff, in a
1909
+ review, in a terminal, in most editors — saw `/\[[0-9;]*m/g`, a different and wrong-looking regex that
1910
+ someone tidying is one keystroke from breaking `tldrx doctor --mcp` with. **Not the #47 hazard**, and
1911
+ worth saying: ESC does not trip the binary-file heuristic, and the file was always visible to a grep.
1912
+ Behaviour is byte-identical, and `.source` is the wrong instrument for proving that (it returns the
1913
+ literal as written, so the two spellings differ there while compiling to the same matcher) — so
1914
+ `test/doctor.test.ts` compares the shipped `stripAnsi` against a reference rebuilt from the old
1915
+ literal-ESC form over a nine-line corpus, 4 of which change. `test/source-hygiene.test.ts` now flags a
1916
+ raw ESC as well as a NUL: measured across all 479 `.ts` files under `src/`, `test/`, `bin/` and
1917
+ `scripts/`, `McpProbe.ts` held the only one, so the check has no false positives to trade against.
1918
+
1919
+ - **A stray NUL byte made two source files invisible to every grep-based sweep (#47).**
1920
+ `test/cli.test.ts` carried one literal `0x00`, so `file(1)` called it `data` and `grep -I` —
1921
+ ripgrep and ugrep too — dropped it SILENTLY, exit 0, no message. Measured on `origin/main`:
1922
+ `grep -lI -E 'node:child_process|Bun\.spawn' test/*.ts` returned **36 files with `cli.test.ts`
1923
+ absent**, though it calls `Bun.spawn` on line 32. That is how it missed #43's load-aware timeout
1924
+ and then timed out at 5004 ms on the very merge that was fixing timeouts. Writing the guard found
1925
+ a **second** one nobody had reported — `src/core/text/srcToken.ts:711`, a NUL used as a cache-key
1926
+ separator, which hid that file from every `src/` sweep (367 of 368 `.ts` files visible). Both are
1927
+ now the two-character escape `\0`: identical at runtime, ordinary text on disk. Post-fix the same
1928
+ sweep finds `cli.test.ts` and all 368 `src/` files. `test/source-hygiene.test.ts` walks `src/`,
1929
+ `test/`, `bin/` and `scripts/` and fails on any NUL, with the offender named at `path:line`.
1930
+
1931
+ - **The five wave-5 docs-pass nits, each a sentence nothing was checking (#25).**
1932
+ - **`boundary.ts` promised an exclusion is "never silent" and dropped state paths without a
1933
+ word** — `BoundarySurface.excluded` was populated and read by nothing. Every verdict that has a
1934
+ surface now names what was excluded, green and red alike, including the case where the
1935
+ exclusion was ALL there was and the run therefore reported "declares no surface".
1936
+ - **The precondition refusal asserted "the stage is still `ready`" without looking.** It reports
1937
+ the status `run.yml` actually holds. Bigger than filed: on a FRESH run the stage at the cursor
1938
+ is `pending`, not `ready`, so the old sentence was wrong in the ordinary case as well as on the
1939
+ retry of a `failed` stage.
1940
+ - **The agent-gate fallthrough printed its label twice** — `boundary: boundary=…`, because every
1941
+ condition detail was prefixed with its own id including the two that have a trigger of their
1942
+ own. Only the generic `condition` trigger keeps the prefix; alone it names nothing.
1943
+ - **`dispatchNotes.ts` documented `.agent/04-build/build/S5/…`**, one phase segment more than
1944
+ `dispatchNotesPath` builds. The example is now asserted equal to the path the code produces.
1945
+ - **Two usage strings were narrower than their own `--help`**: `tldrx gate template` omitted the
1946
+ positional `[<run>]` it accepts, and `run new` spelled `--gates <a,b|all|none>` where the help
1947
+ says `<a,b|a:agent|all|none>`.
1948
+
1949
+ - **`tldrx next --dry-run` spawns nothing. It used to cost $0.42 a go (#17).** The flag ran the
1950
+ stage for real — one `claude -p`, one `agent.spawned`, one `agent.result`, the cost on the
1951
+ ledger — and only reverted the non-handoff FILES afterwards. Measured on the 2026-08-30 pilot;
1952
+ `tldrx next --help` had said "Spawns nothing and writes nothing" the whole time, so this is the
1953
+ code catching up to the promise rather than the promise being watered down to the code.
1954
+ - **What it does now.** It assembles the prompt, prices it, and stops: the expert bundle, the
1955
+ context ledger, the prompt size, the declared outputs, and the **exact `claude -p` argv** it
1956
+ would have run (with the `--json-schema` blob elided as `<envelope-schema>`, rendered from
1957
+ `buildClaudeArgs` itself so the printed command cannot drift from the real one). Then the two
1958
+ commands that would actually dispatch it. Exit `0`.
1959
+ - **Nothing is written either.** No prompt bundle and no `pending.json`, so a dry run cannot
1960
+ leave a `--commit` looking at a turn that never happened; the stage keeps its status and the
1961
+ ledger keeps its zero. `dry_run_allowed: false` still refuses (Build sets it: a stage that cuts
1962
+ branches and fans out per-story sub-agents has no ONE dispatch to describe).
1963
+ - **On an attended run it is still refused at exit `4`** — but for the right reason now. It costs
1964
+ nothing; it describes a dispatch the framework never makes there, and `--prepare` writes the
1965
+ bundle the host is going to carry. The message said "it spawns a real sub-agent" and no longer
1966
+ lies.
1967
+
1968
+ - **A precondition gets its own clock, not the stage's 900–1800 s (#20).** `preconditions:`
1969
+ inherited `timeout_s`, so one hung command — `docker info` against a dead daemon is the measured
1970
+ case — could hold a run for half an hour: exactly the waste the feature exists to prevent, taken
1971
+ by the guard instead of by the attempt. Each precondition now gets **60 s** by default
1972
+ (`PRECONDITION_TIMEOUT_S`), overridable per entry with `timeout_s: <n>`, refused at load if that
1973
+ is not a number `> 0`. A timeout is a red precondition like any other — exit `2`, nothing
1974
+ written, nothing spawned, the stage where it was — and its message names the precondition, its
1975
+ own timeout and the knob that changes it, rather than the stage's. `CommandRun` gained
1976
+ `timedOut` so a timeout can be told from a refusal or a wrong exit code without reading prose.
1977
+
1978
+ - **The budget gate can see host-token spend and attendedness (#22).** The tolerant reader the
1979
+ `budget-gate` hook and the status line share (`hooks/lib/runFile.ts`) skipped `tasks[]` and
1980
+ `attended_by:` entirely. So a run whose turns a host session paid for reported `$0.00` metered
1981
+ and nothing else, and `runSnapshot`'s tolerant path hard-coded `attendedByHost: false` with a
1982
+ comment admitting it meant "cannot see". `RunView` now carries `attended_by` and each task's
1983
+ `cost_usd` / `metered` / `tokens`; `runSpend` derives the metered dollars, the declared host
1984
+ tokens and the uncosted turns; `renderRunEconomies` renders the one line that says a dollar
1985
+ figure is a lower bound. The gate appends it to a `host-tokens` phase's stderr note and to a
1986
+ refusal, and `budget.blocked` records `economy`, `attended_by`, `metered_usd`, `host_tokens` and
1987
+ `unmetered_tasks`. **No verdict changed**: a dollar ceiling still governs dollars, the two
1988
+ currencies are still never converted, and a plain metered run's refusal is byte-identical.
1989
+
1990
+ - **The `max_reads` flake was a real race, not a slow test (#24).** A chunk boundary is not a line
1991
+ boundary: `LineSplitter` hands every complete line in one chunk to the read counter
1992
+ synchronously, so when the OS coalesced the sub-agent's writes — which is what a loaded CI box
1993
+ does — reads 4..20 were counted in the same tick as read 3, long before the `SIGKILL` just
1994
+ ordered could land. `agent.result.payload.reads` was therefore a function of scheduling, and the
1995
+ assertion pinning it to the cap cost two retries in one night. The counter now stops the moment
1996
+ the cap fires, so what is recorded is the number of reads the cap ALLOWED. Pinned by a fixture
1997
+ that makes the coalescing deterministic (`FAKE_CLAUDE_READS_BURST=1` — every read pair in one
1998
+ write): pre-fix that reported 20 reads against a cap of 3.
1999
+
2000
+ - **One over-cap list item no longer cascades into false "S<id> has no file" errors (#37).**
2001
+ `validatePlan` resolves cross-file references out of the set of stories that PARSED, so a story file that
2002
+ failed its own validation was indistinguishable from one that was never written. Measured on the
2003
+ `260829-scoring-leaderboard` session: `acceptance[3]` in `S8.md` was 1,009 characters against the 512 cap,
2004
+ and the check reported three errors of which one was real — the other two said `S8 has no file in stories/`
2005
+ about a file that was 5,794 bytes on disk. The operator only avoided a wasted pass by re-deriving the cause
2006
+ from the validator source; an agent reading that message goes hunting for a missing file or rewrites
2007
+ `waves.yml`.
2008
+ - A reference to a story or epic whose FILE EXISTS is never reported as missing. It now reads
2009
+ `S8 is unresolved because stories/S8.md failed validation — that file exists; fix the errors reported
2010
+ against it and this one goes with them`, carries `cascade: true` on the `PlanIssue`, and covers the
2011
+ id-mismatch case (`stories/S8.md declares id \`S9\``) as well as the invalid-file case. The epic side —
2012
+ a story pointing at an epic whose own file did not validate — had the identical bug and the identical fix.
2013
+ - `describePlanIssues` orders root violations ahead of cascades. Its window is three issues wide, so one
2014
+ real defect cascading into four references could otherwise spend the whole window on consequences and
2015
+ never name the cause.
2016
+ - A story that is genuinely absent still reports `has no file`, with no cascade flag.
2017
+
2018
+ - **A refused list value now names the cap it broke, at the cap's current value (#38).**
2019
+ The constants were interpolated already but the sentence was not self-describing: `513 characters exceeds
2020
+ the 512 cap` did not say the cap is per-item or that splitting the item is the fix. Now
2021
+ `513 characters exceeds the 512-character cap on one list item — split it into several items` and
2022
+ `65 items exceeds the 64-item cap`, both still derived from `MAX_ITEM_CHARS` / the list's own `max`, and
2023
+ the same constants are what the Plan prompt states up front.
2024
+
2025
+ - **The merge itself is now serialised, and a gate can no longer describe a tree it is not pushing
2026
+ (#44).** `scripts/merge-wave.sh` merges, gates and pushes in ONE shared checkout and took no lock.
2027
+ Measured on the pre-fix script with two concurrent invocations against a real sandbox repo: run A
2028
+ gated `7afcc0e` (its own merge) at `typecheck` and `fadc923` (the OTHER run's merge, landed
2029
+ mid-gate) at `build`, then printed `OK fadc923 … pushed`. Both runs reported the same sha and
2030
+ both exited 0 — a green report over a tree neither had finished gating. With a red change in the
2031
+ other branch the same interleaving hands agent A a `FAIL build=1` for code it never wrote, which
2032
+ is what was actually observed live 2026-08-31 (`2184` tests counted where the branch had `2181`).
2033
+ - **A lock, held from before the dirty-tree check through the push.** `mkdir` on
2034
+ `.git/merge-wave.lock` — atomic on macOS and Linux, where `flock(1)` is not on stock macOS, and
2035
+ in `git rev-parse --git-common-dir` so the lock can never be dirt in the tree it guards — and
2036
+ not `$R/.git`, which in a linked worktree is a FILE that `mkdir` can never turn into a lock. A second invocation WAITS,
2037
+ saying so on stderr (`merge-wave: waiting for another merge in this checkout (owner: …)`) so the
2038
+ single summary line on stdout stays a single line. Waiting is bounded (`MW_LOCK_WAIT_S`, default
2039
+ 3600 → exit `6`), and a lock whose owner is a dead pid on this host, or older than
2040
+ `MW_LOCK_STALE_S`, is broken open — after re-reading the owner line, so two waiters cannot tear
2041
+ down a lock a third has just taken. An interrupted run hands the lock back on its way out: an
2042
+ untrapped signal kills bash WITHOUT running its `EXIT` trap, so `INT` and `TERM` are trapped too. No
2043
+ path through the wait loop is free of the budget, including the break-open one: a lock that
2044
+ cannot be created or removed now fails in under a second instead of spinning forever.
2045
+ - **And an assertion that does not depend on the lock.** Between the last gate and the push, HEAD
2046
+ must still be the commit the gates ran against; if it moved, nothing is pushed and the script
2047
+ exits `5` saying which sha it gated and which one is there now. The lock prevents the race; this
2048
+ makes pushing an ungated HEAD impossible even for someone who bypasses the lock. The pre-fix
2049
+ script, given the same mid-gate commit, pushed it and reported `OK`.
2050
+ - **It pushes the commit it gated, not the `main` ref.** `git push origin main` publishes
2051
+ `refs/heads/main` whatever HEAD is — and a red gate leaves `main` sitting on an ungated merge
2052
+ commit by design, so the next run from a detached or repaired HEAD would have published THAT.
2053
+ The push is `HEAD:main` now, and a pre-flight refuses (exit `7`) when the gated commit is not a
2054
+ fast-forward of `origin/main` rather than letting the server's rejection be the first news.
2055
+ - Gate logs moved from the fixed `/tmp/mw-*.log` to a per-invocation `${TMPDIR}/mw-<pid>/`, and the
2056
+ FAIL lines name the directory. Two runs in two clones on one box shared those files.
2057
+
2058
+ - **The test suite no longer goes red because the machine was busy (#43).** On an untouched
2059
+ `origin/main`, `bun test` reported `2155 pass · 5 fail` while the same two files alone reported
2060
+ `91 pass · 0 fail`: four tests that spawn a REAL `git` expiring on bun's 5000 ms default, and one
2061
+ 50 ms performance budget measured at 66.4 ms, with three `tldrx` runs and two other agents sharing
2062
+ the box. Because `merge-wave.sh` refuses to push on any test failure, that red is indistinguishable
2063
+ from a regression at the exact moment a merge is decided, and the natural response — re-run until
2064
+ green — is how a real regression eventually gets pushed.
2065
+ - **The clock moved; no assertion did.** `test/fixtures/machineLoad.ts` measures the machine
2066
+ (1-minute run-queue per core, floored at 1 and capped at 8) and hands out budgets from it. All
2067
+ **42** test files that spawn a real process — `git`, `bun`, the CLI — now open with
2068
+ `setDefaultTimeout(spawnTestTimeout())`: 30 s idle, scaled by load, still a hang detector. How
2069
+ long a process takes to start is a property of the machine, not of the code, so a fixed budget on
2070
+ such a test measures the box. A test enumerates those files and fails if a new one skips the
2071
+ budget. Serialising the suite would not have helped: `bun test` already runs files sequentially
2072
+ in one process (verified — a `setDefaultTimeout` in one file does not reach the next, and a 5.5 s
2073
+ test in that next file still expired at 5000 ms). The contention is other processes on the box,
2074
+ which only a load-aware budget can see.
2075
+ - **The first version of that list was a `grep -l`, and it lied.** It returned 14 files and silently
2076
+ omitted `cli.test.ts`, whose "every command's help lists an exit table" then timed out at 5004 ms
2077
+ on the merge that was fixing timeouts. Cause: one stray NUL byte at `test/cli.test.ts:366` makes
2078
+ the file `data` to `file(1)`, and grep drops binary files under `-I` without a word. The list is
2079
+ built by READING every file now, and `cli.test.ts` is asserted to be in it. Filed as #47.
2080
+ - **The one real performance budget keeps its teeth.** `handoff` on 256 KB is now the floor of
2081
+ three runs against `perfBudgetMs(50)`, which on an idle machine is 50 — the identical assertion.
2082
+ A stall inflates some runs and never the floor, and a function that genuinely takes 120 ms still
2083
+ fails, which is itself a test.
2084
+
2085
+ - **`npm pack` output no longer refuses the next agent's merge (#45).** `tldr-experts-<version>.tgz`
2086
+ was not ignored, and the dirty-tree guard refuses on ANY porcelain line, untracked included — so a
2087
+ pack artifact left by a release check blocked the merge of whoever came next, someone who did not
2088
+ create the file and could not know whether deleting it was safe. `*.tgz` is ignored now. The guard
2089
+ is deliberately unchanged: an untracked file is still dirt, and a test holds it to that.
2090
+
2091
+ - **`claim-sources` reports every problem it found, over every declared `.md` output — and a
2092
+ `file` src resolves against this run's epic worktree.** Four issues, one code path (#33, #34,
2093
+ #23, #16), all four measured on the 2026-08-30/31 unattended pilot runs.
2094
+ - **It reported ONE problem** (#33). `checkClaimSources` returned on the first file, the first
2095
+ category and `unresolved[0]`, so a 226-bullet cap breach sat invisible behind a single bad
2096
+ file path: fixing the visible one and re-running would have bought the next one at the price
2097
+ of a full paid pass. Every file and every category is reported now, as a per-file summary
2098
+ (`<file>: 3 unsourced bullet(s) on line(s) …; 2 unresolvable source(s) — …`), with file-level
2099
+ problems such as the cap breach listed FIRST so 200 line numbers cannot bury them. Up to six
2100
+ of a category are named and the rest become `(+N more)` — the same convention
2101
+ `describeKnowledgeIssues` uses, and necessary because a check's `detail` is rendered inside
2102
+ one-line summaries (`autoGate`, `next`).
2103
+ - **It looked at ONE file** (#34). The filter was `endsWith("handoff.md")`, so the identical
2104
+ violation refused the stage when it was written in `handoff.md` and passed in silence when it
2105
+ was written in `design.md`, `contracts.md` or `scope.md` beside it — the pilot's pass-3
2106
+ violation was caught only because it happened to be in the handoff. Every declared `.md`
2107
+ output is read now, by both the gate check and the write-time hook: the four-section rule for
2108
+ the files that ARE handoffs, and `validateCitations` for the ones that are not. That second
2109
+ rule is deliberately narrower — a bullet with no citation is prose, but a `[src: …]` that WAS
2110
+ written must parse, must resolve, and must obey `$ … → exit n` belonging only to an
2111
+ `Evidence ledger`. A declared non-handoff output that was never written is still not a
2112
+ failure; that is the `--commit` gap check's job.
2113
+ - **The execution-claim validator reads the verb** (#23). `\bexit \d` missed "exits 0", which
2114
+ is how a trainer writing normal English says it, so the claim slipped through the grammar the
2115
+ rule exists to enforce while "exit 0" three words away was refused. Conjugation, an optional
2116
+ "with", and the `code`/`status` spellings all match now; the digit is still required, so "the
2117
+ exit path is documented" and "the exchange refuses an empty code" stay prose.
2118
+ - **A `file` src resolves against this run's epic worktree** (#16). The Build phase commits
2119
+ onto an epic branch and deliberately does not merge it, so a Watch-stage handoff ABOUT that
2120
+ work had every `repo:src/…` citation refused for naming code the working tree does not have
2121
+ yet — the stage's own evidence was rejected for being true. `.tldrx/worktrees/<repo>/_epic-<run>-<epic>`
2122
+ is now a resolution base, tried before the working tree, for both the hook and the gate; the
2123
+ path convention has one home (`core/paths.ts`) that the Build executor writes and the §2.8
2124
+ resolver reads. Resolution also no longer stops at the first base where the file EXISTS but
2125
+ is too short — a file truncated on the epic branch would otherwise deny a claim about the
2126
+ line it still has on `main`. **Still open** (commented on #16): the epic worktree is removed
2127
+ at the end of Build unless `--keep-worktrees`, so the default Watch stage has no tree on disk
2128
+ to resolve against. Closing that means reading blobs out of the epic branch inside a hook
2129
+ whose budget is 50 ms, or keeping epic worktrees for the life of the run — a design call, not
2130
+ a mechanical one.
2131
+
2132
+ - **The Build DoD is a DELTA gate again: the base tree is checked before any story is charged (#41).**
2133
+ A dod block proves one thing — *this story did not break the tree* — and nothing checked that the tree was
2134
+ unbroken to begin with. Measured live 2026-08-31 on `260829-scoring-leaderboard`: of the three commands
2135
+ `workspace.yml` declared, **two already failed on pristine main** — a bare `dotnet test` ran two `Live`-trait
2136
+ tests that call paid Azure AI and that the repo's own CI excludes, and `dotnet format --verify-no-changes`
2137
+ flagged 336 files in a repo whose CI never gates format at all. All 15 stories in the plan would have blocked
2138
+ identically, each having spent a developer turn, and each told the operator the STORY was red.
2139
+ - **Pre-flight at Build entry.** After the dirty-tree and foreign-epic refusals and before anything is
2140
+ dispatched, every dod command the pending stories name is run once against the untouched base tree. A
2141
+ non-zero exit refuses the stage (exit `2`, back to `ready`) naming the command, its exit code and the repo,
2142
+ with no attempt spent and nothing charged.
2143
+ - **In the repo's own checkout, not a fresh worktree.** That is the tree a human means by "the base": it has
2144
+ the installed dependencies and tool state that make the command mean what the team thinks it means. A
2145
+ pristine worktree would fail half the world's repos for want of `node_modules` and turn a safety net into
2146
+ an outage.
2147
+ - **Paid for once.** Results go to `04-build/preflight.yml` — files are the state — keyed by repo, command and
2148
+ the base sha, and are read back by every later invocation of the run. A missing or unreadable cache is a
2149
+ question, never a fault: a run that entered Build on an older binary measures lazily rather than erroring.
2150
+ - **Attribution.** When a story's DoD does go red, the cached base result decides whose fault it is. A command
2151
+ red on the base too halts the build with the same workspace-config error rather than blocking the story and
2152
+ consuming its attempt. A command the gate declined to run is recorded `unmeasured` and excuses nothing.
2153
+
2154
+ - **The review handshake no longer swallows an unrecognized verdict, nor drops structured
2155
+ findings.** Measured on `260831-hardening-d1` / S1 (2026-08-31). Two verdict grammars coexist
2156
+ — gate evidence is `sign | sign-with-fixlist | refuse`, a story review is
2157
+ `approve | fixlist | changes` — and the host-facing hint named neither, saying only "write
2158
+ {verdict, summary, findings}". The host wrote `sign`. `parseReview` fail-closed it to `changes`,
2159
+ correctly and **silently**: a clean fix-list verification round read as a second `changes`, the
2160
+ story went `blocked`, and a `story reopen` cycle was the only way to record the verdict that
2161
+ had been meant all along. Separately, `findings` was filtered with `typeof f === "string"`, so
2162
+ the attempt-1 adversarial reviewer's seven `{severity, file, line, claim, evidence, fix}`
2163
+ objects were dropped whole — the verdict survived, the evidence it rested on did not.
2164
+ - **The contract is now stated where the host reads it.** Both `--commit --review` hints name
2165
+ the enum: `verdict is one of approve | fixlist | changes, NOT the `sign`/`refuse` gate
2166
+ vocabulary`.
2167
+ - **The downgrade is announced.** Fail-closed is unchanged — an unreadable verdict is still
2168
+ `changes`, never `approve` — but a verdict outside the enum now comes back on
2169
+ `Review.verdictProblem` ("the reviewer's verdict `sign` is not approve|fixlist|changes —
2170
+ recorded as `changes`"), is printed by the executor on the one path both doors pass through,
2171
+ and rides in `findings` so the review log and the next attempt's `## Previous attempt` both
2172
+ carry it. A DECLARED `fixlist` that fell short is untouched: `fixlistProblems` already says
2173
+ that one out loud, and two sentences for one downgrade would read as two faults.
2174
+ - **Structured findings are rendered, never dropped.** An object becomes
2175
+ `[severity] file:line — claim · evidence: … · fix: …`; a shape nothing recognizes is kept as
2176
+ JSON; a `findings` that is not an array is kept as one finding. An unreadable finding in the
2177
+ log beats a finding that is not in the log.
2178
+
2179
+ - **A project stage override that supplies only `stage.yml` no longer swaps the stage body for an
2180
+ empty one.** Reported from the 260829-scoring-leaderboard driver session (2026-08-31) and
2181
+ reproduced here: `stage.md` was resolved by string-substituting `stage.yml` in the path the
2182
+ preset had already picked, so creating `.tldrx/stages/plan/stage.yml` to tune one key moved the
2183
+ BODY lookup into a directory that had none — and the miss was read as an empty string. The
2184
+ context ledger printed `stage 1 B` where it had been 4.9 KB; the sub-agent would have been
2185
+ dispatched with the inputs, the experts and the rejection note and **zero** stage instructions,
2186
+ and nothing refused. `stageMdPath` now resolves per FILE, not per directory: the override's own
2187
+ `stage.md` wins, else the packaged one is inherited, and a stage with no body anywhere is a
2188
+ named `StageBodyError` rather than a silent empty prompt. Both readers — `next --prepare` and
2189
+ the Watch executor — go through it.
2190
+
2191
+ - **`approve --as-agent`'s refusal now names the route that works on the run in front of you.**
2192
+ It pointed only at `--gates <stage>:agent`, which is chosen at `run new` and frozen there — so
2193
+ the one suggestion meant recreating a run already in flight. It now leads with the delegated
2194
+ approve: read the agent's evidence note yourself and sign as you, `tldrx approve --note
2195
+ "delegated: <agent> reviewed this, evidence at <path>"`, which keeps the gate's policy and puts
2196
+ the provenance on the record. Found across the 2026-08-30/31 unattended pilots.
2197
+
2198
+ - **`budget raise <phase> <usd>` help said `<usd>` was "the new ceiling"; the source adds it.**
2199
+ `raiseBudget` computes `ceiling_usd + amount` (`raiseBudget.ts:83`), so an operator following
2200
+ the help over-raised — measured live on the scavtopia leaderboard run, a "$5.40 new ceiling"
2201
+ command would have set $8.00. The arithmetic is what live runs depend on and is untouched; the
2202
+ words move. `<usd>` is now "how much to ADD to that phase's ceiling — a delta, not a new
2203
+ ceiling", with a note spelling out the $10 + $25 = $35 case and pointing at `budget show`, which
2204
+ already prints the correctly-sized command.
2205
+
2206
+ - **`tldrx run new --from` stores an imported answer's own words, not a letter pointing at a file
2207
+ it does not own.** AI-DLC records a chosen option as `[Answer]: C`, and the import stored
2208
+ "<question> — C" verbatim; two facts became unreadable once aidlc was uninstalled and the source
2209
+ file went with it (2026-08-30/31 pilots). The interview flow has always resolved a letter to the
2210
+ option's text before recording (`interview/reply.ts:32-37`), and the import now does the same:
2211
+ `parseAidlcQuestions` reads the lettered options (uppercase, and a space required after the
2212
+ punctuation, so `- E.g. …` stays prose) and `answerText` resolves the answer against them; a
2213
+ letter with no option behind it is stored as typed rather than invented. Conflict detection is
2214
+ unchanged by the longer text: it keys on the QUESTION, as `hooks/no-reask.ts:54` already does,
2215
+ because `findDuplicate` is Jaccard over tokens and therefore length-sensitive — the same
2216
+ contradiction scored 0.78 against a bare letter and ~0.22 against the answer written out.
2217
+
2218
+ - **A second run's stories no longer merge into ANOTHER run's epic branch.** Measured live
2219
+ 2026-08-31 on two concurrent runs: `260831-hardening-d1` reported S1, S2 and S6 all
2220
+ "merged into `epic/hardening-d1` (N commits carried)", and `epic/hardening-d1` was still
2221
+ sitting at its base with **zero** story commits — all three merges had landed on
2222
+ `epic/d1-tenancy-identity-customers`, a CLOSED previous run's branch. Nothing failed; the
2223
+ run closed green with an empty epic, and it surfaced only because a later story measured
2224
+ `git merge-base` and found the dependency it had been promised was missing.
2225
+ - **The cause was one missing run id.** `openEpicWorktree` built the epic worktree's disk
2226
+ path as `_epic-<epic id>`, and every plan names its first epic `E1`. The second run's
2227
+ `existsSync` therefore hit the FIRST run's directory, `addWorktree` was skipped, and
2228
+ `git merge --no-ff` ran inside a checkout of a foreign epic branch. The in-memory map was
2229
+ keyed correctly (`repo:epicBranch`) — only the path collided, and only across processes.
2230
+ Every progress line renders `story.epicBranch`, so the messages were right about where the
2231
+ merge was *meant* to go for as long as the bytes went somewhere else.
2232
+ - **The path now carries the run**: `_epic-<run id>-<epic id>`, the same shape the STORY
2233
+ worktree was given after the 2026-08-29 audit found the identical class of bug one level
2234
+ down. That fix never reached the epic worktree, which is the worse half — a story worktree
2235
+ collision means two sub-agents editing one file, an epic worktree collision is a merge.
2236
+ - **And a reuse on the wrong branch now refuses.** Every reuse of an epic worktree — the
2237
+ remembered path and the one found on disk — asserts its checked-out branch is the story's
2238
+ epic branch first (`assertWorktreeOn`, `core/build/git.ts`). A mismatch throws
2239
+ `WorktreeBranchMismatchError` naming both branches and the directory, and fails the stage.
2240
+ It never re-points the worktree and never merges anyway. Path scoping makes the collision
2241
+ impossible; this makes it impossible to repeat *silently*.
2242
+
2243
+ - **A `--note` with a blank line in it no longer destroys `run.yml`.** Measured 2026-08-31 on the
2244
+ live `260829-scoring-leaderboard` run: `tldrx reject --note "<two paragraphs>"` wrote the note
2245
+ into the gate's flow mapping with LITERAL newlines inside a double-quoted scalar, which is not
2246
+ YAML — the `yaml` package answered `Missing closing " quote at line 57` and Bun's parser
2247
+ `Unexpected character` — and from that moment **every** command on the run failed. There was no
2248
+ repair verb, so the operator had to hand-edit a file the docs forbid editing, and the next save
2249
+ re-emitted the same string and broke it again at the same line, taking `run.yml.bak` with it.
2250
+ Four changes, each closing one part of the loop:
2251
+ - **The emitter escapes, at the one place every field goes through.** `yamlScalar`
2252
+ (`core/facts/emitFactsYaml.ts`) escaped `\` and `"` and nothing else; it now emits via
2253
+ `JSON.stringify`, whose string grammar is a strict subset of YAML 1.2's double-quoted scalar —
2254
+ the escaping this repo already trusted in `adapters/external.ts` and `build/storyFile.ts`.
2255
+ Because every YAML this framework hand-emits routes strings through that one helper, the fix
2256
+ reaches **all** of them at once: gate notes (`approve`, `reject`, `revoke`), `cancelled.note`,
2257
+ task `error` and `stopped_by`, gate `evidence`, run `title`/`scope`, `facts.yml` fact text and
2258
+ retirement reasons, and `split.yml` goals, claims, questions and answers. Verified against
2259
+ **both** parsers behind the runtime seam. Existing files do not churn: over every code point
2260
+ from U+0020 to U+FFFF the new escaping and the old produce identical bytes (63,456 checked, 0
2261
+ differ), so only the values that were already corrupt change shape.
2262
+ - **A file already broken this way heals itself on load.** `parseYamlRepairing` (`core/yaml.ts`)
2263
+ re-escapes raw control characters trapped inside a double-quoted scalar, re-parses, and accepts
2264
+ the result only if it parses — otherwise the parser's ORIGINAL error is thrown, because a
2265
+ repair that cannot be verified is not offered. `RunStore.open` then rewrites the mended file
2266
+ through the fixed emitter and says so on stderr. A one-time hand repair was never enough: the
2267
+ old emitter re-corrupted the file on the next save, so `emit(load(x))` had to be made stable.
2268
+ - **Every state write keeps one step back.** `RunStore` and `FactsStore` had grown a
2269
+ byte-identical private copy of temp-plus-rename each; both now call one
2270
+ `core/fs/writeAtomic.ts`, which additionally copies the version it is about to replace to
2271
+ `<file>.bak`. Atomicity only ever guaranteed a WHOLE file, never a good one. The copy is taken
2272
+ before the rename, so the live file is never absent for an instant and a torn backup can only
2273
+ ever cost a backup. `tldrx init` now adds `tldrx-work/*/*.bak` and `.tldrx/memory/*.bak` to the
2274
+ managed `.gitignore` block.
2275
+ - **A `run.yml` beyond mechanical repair fails honestly, and takes nothing else down with it.**
2276
+ The error names the file, quotes the parser verbatim, says that every command on the run reads
2277
+ that file first, and points at `run.yml.bak` — while stating plainly that using it is a MANUAL
2278
+ decision tldrx will not make. Separately, one corrupt `run.yml` used to throw a raw
2279
+ `YAMLParseError` out of `buildModel` and kill `tldrx dashboard` for the whole workspace;
2280
+ `loadRunResult` (`core/replay/loadRun.ts`) now distinguishes missing from unreadable, and the
2281
+ dashboard lists the run as **unreadable** with the parse error beside it and renders every
2282
+ other run as normal.
2283
+
2284
+ ### Changed
2285
+
2286
+ - **The docs now say, at the top of both places a reader starts, that `run attend host` is a LOCK
2287
+ and `run auto` is an ENGINE.** Grounding: on 2026-08-31 the framework's own author — who had read
2288
+ the chapter — ran `tldrx run attend host <run>` expecting it to drive the whole run by itself,
2289
+ and then asked whether `attend` and `auto` compose. They do not, and the code has always said so
2290
+ (`runAuto.ts:108` refuses `run auto` on an attended run at exit `1`, before the event log is
2291
+ opened; `runNext.ts:659` exits `4` on a bare `next` and names the `--prepare` command). The docs
2292
+ took too long to say it.
2293
+ - **README gains "Trying it: three ways to run"**, immediately after Quick start: a three-row
2294
+ table of who executes each turn, what a turn costs, and where each mode stops; one scenario
2295
+ line each (`run auto` for a small run you would watch anyway and for CI/cron — the only mode
2296
+ with no session behind it; `attend host` when a session is already open and cost or quality
2297
+ matters; `attend host` + a mandate for overnight); and the two-command recipe with a verbatim
2298
+ example **mandate prompt**.
2299
+ - **`docs/guide/10-unattended-mode.md` leads with the same disambiguation** — a blockquoted
2300
+ lock-vs-engine table above the chapter's opening paragraph, so a skimmer cannot make that
2301
+ mistake — and gains a `### The mandate` section carrying the prompt verbatim, tying its four
2302
+ legitimate interrupts back to the `questions` / `budget-event` / `boundary` fallthroughs the
2303
+ framework already enforces, and to the fact that no `git push` wrapper exists in the Build
2304
+ executor (`src/core/build/git.ts:13`) and the developer prompt says "Do not push"
2305
+ (`src/core/build/prompts.ts:180`).
2306
+ - **`tldrx run --help` says it too.** `run` had notes for `status`, `estimate`, `unlock` and
2307
+ `cancel` and none for the pair that actually confuses people. It now leads with one note per
2308
+ mode — "a LOCK, not an engine" / "an ENGINE, not a lock", each naming the other's refusal — and
2309
+ the `<host|--none>` argument line says the framework will not spawn on the run again. Help text
2310
+ only; no behaviour, no flag and no exit code moved.
2311
+
2312
+ - **`tldrx run estimate` is remaining-work aware (#21).** It priced the next stage from token medians while
2313
+ the budget brake separately computed what that stage still had to pay for — two models, one question, and
2314
+ the one people read was the one that never shrank. A Build stage with five of six stories done was still
2315
+ quoted the number the Plan wrote before any of them ran, which is the figure that made a pilot operator
2316
+ move money twice for work the run could already afford.
2317
+ - It now calls **the same `remainingWork()`** the brake and `budget show`'s `est.` column call, with the
2318
+ same inputs, and reports it beside the token estimate: done stories excluded, blocked ones named, the
2319
+ arithmetic shown. A test asserts the two numbers are identical rather than merely similar.
2320
+ - It also rolls the run up: `still to run: N stage(s) … $X priced`, with terminal stages excluded and the
2321
+ cursor stage narrowed by the plan when the plan knew better. `--json` carries both as `remaining` and
2322
+ `runRemaining`.
2323
+ - The token half is untouched. The input side is still measured off the same assembly `next` builds, and
2324
+ the cache/output medians still say which sample they came from — that half was never the complaint.
2325
+
3
2326
  ## 0.3.1 — 2026-08-31
4
2327
 
5
2328
  **Unattended mode.** Twelve of the entries below are one feature: a run a **host session**
@@ -653,6 +2976,66 @@ none of these behaves byte-identically to the release before them.
653
2976
 
654
2977
  ### Fixed
655
2978
 
2979
+ - **A trainer that `cd`s no longer writes its knowledge file into a different git repo.**
2980
+ Measured 2026-08-31 on `~/scavtopia` (five repos, ten `expert train --mode light` runs): the
2981
+ `mcp` run was rejected with `mcp.md.partial was never written`, and the file had been written —
2982
+ 46 lines, 9,567 bytes, complete and usable — to
2983
+ `whiteboard/.tldrx/experts/mcp/knowledge/mcp.md.partial`. The sub-agent ran
2984
+ `cd <workspace>/whiteboard` to execute that repo's declared gate command, then wrote the
2985
+ RELATIVE path the prompt had given it, and the path resolved against the repo it had `cd`'d
2986
+ into. Three costs from the one bug: **$1.23 charged for work that was finished and then
2987
+ orphaned**, a parasitic `.tldrx/` tree left inside an unrelated git repo (`git -C whiteboard
2988
+ status` → `?? .tldrx/`), and **no repair round possible** — the missing-file branch returns
2989
+ before the repair check, so this failure mode was unrecoverable by construction even with
2990
+ budget left. Fixed at both ends.
2991
+ - **Prevention: the prompt now states an ABSOLUTE output path**, workspace-root-resolved, and
2992
+ says why — "If you `cd` into a repo to run its gate command, a relative path then resolves
2993
+ against THAT repo … and throws the whole paid run away. That is measured, not hypothetical."
2994
+ Both training prompts carry it, and so does the repair round's target, for the same reason.
2995
+ - **Recovery: when the file is missing, the declared repo roots are probed** for the stray
2996
+ relative write before "never written" is said. A file found there is moved back and validated
2997
+ exactly as if it had landed correctly — recovery is not a pass, the same `parseKnowledgeFile`
2998
+ still judges it, and a recovered file that fails can still be repaired because the probe runs
2999
+ ABOVE the repair round.
3000
+ - **The note is honest and names the mess.** `recovered: the trainer wrote to
3001
+ whiteboard/.tldrx/… , inside the `whiteboard` repo — a relative `.tldrx/…` path resolves
3002
+ against whatever directory it had `cd`'d into.` The empty parasitic directories are removed
3003
+ on the way out; a directory holding anything else is **left in place and named**, with the
3004
+ `git -C <repo> status` to run, because a tool that deletes inside a repo it was never asked
3005
+ to touch is a worse bug than the one it is fixing. A repo carrying its own
3006
+ `.tldrx/workspace.yml` is skipped entirely — that file may belong to a nested workspace, and
3007
+ taking it would be theft rather than recovery.
3008
+ - When no stray is found the verdict is unchanged and now says where it looked.
3009
+
3010
+ - **A rejected training run records WHICH problems, not just how many.**
3011
+ Measured 2026-08-31: `components` failed with 12 problems for $1.02, and `training.jsonl` — the
3012
+ durable record — held only the string `"…does not validate — 12 problem(s)"`. The twelve went
3013
+ to stdout, where five of them were printed and the rest elided as `(+7 more)`. Anyone who had
3014
+ not captured stdout, which is anyone running this normally, could not tell why a $1.02 run
3015
+ failed. The list is now persisted twice.
3016
+ - **On the ledger**: `check.failed.payload` carries `problems` (the rendered per-problem
3017
+ lines), `problems_total`, `errors`, and `task`. The list is fitted to the record's 4 KB
3018
+ payload cap and reports `problems_omitted` when it does not fit — an append that THROWS on
3019
+ an oversize payload would take the cost line down with the reasons, which is the opposite of
3020
+ the point. The repair round's own `check.failed` carries what it sent back, so "what did the
3021
+ repair actually fix" is answerable later.
3022
+ - **In the file**: `<area>.rejected.md` now opens with a `# REJECTED` header — expert/area,
3023
+ mode, timestamp, dollars spent, error and warning counts, and every problem, uncapped —
3024
+ above the trainer's bytes exactly as written, separated by a rule. A quarantine with no
3025
+ verdict (a sub-agent that died, a rollback) gets no header: there were no reasons to state
3026
+ and inventing them would be inventing the reason.
3027
+
3028
+ - **`## Sources` is now taught as prose with the refused shape shown.** Same batch: four of the
3029
+ five problems the `components` report printed are one mistake four times — `L34 Sources: no
3030
+ [src: …] token`, `L35`, `L36`, `L37`. The trainer had written the recap as a bulleted list.
3031
+ The prompt already said "**Sources** — prose", and a writer who reads that as a style note
3032
+ writes bullets, because bullets are what the other four sections take. The rule it collides
3033
+ with is genuinely file-wide — `parseKnowledgeFile` requires a `[src: …]` token on EVERY list
3034
+ item in every declared section, recap included, and an unsourced one is an error that rejects
3035
+ the file whole. Both prompts now show the accepted prose next to the refused bullets, the same
3036
+ move the execution-claim rule makes. Whether an unsourced recap bullet should be a warning
3037
+ rather than an error is a real question and is deliberately NOT settled here.
3038
+
656
3039
  - **A rejected knowledge file gets ONE repair round before the money is thrown away.**
657
3040
  Measured 2026-08-30 on `~/scavtopia`: `tldrx expert train dotnet-stack --area dotnet --mode
658
3041
  light` spent **$1.69**, the trainer wrote `knowledge/dotnet.md.partial`, and the validator
@@ -985,6 +3368,19 @@ none of these behaves byte-identically to the release before them.
985
3368
 
986
3369
  ### Verified, not changed
987
3370
 
3371
+ - **`tldrx expert train` already exits nonzero when a training fails.** The 2026-08-31 batch
3372
+ report measured shell `EXIT=0` on all ten invocations, including the three that failed their
3373
+ check — but nine of those ten ran on a build that predates this one (`dist/tldrx.js` was
3374
+ rewritten mid-batch at 05:40Z). On the current source the code path is intact:
3375
+ `runTraining` returns `EXIT_AGENT_FAILED` (5), `expert train` returns `outcome.code`,
3376
+ `dispatch` returns it, and `bin/tldrx.ts` does `process.exit(await dispatch(...))`. Now pinned
3377
+ by three tests that drive the REAL CLI as a subprocess with a fake `claude` on PATH and assert
3378
+ the PROCESS exit code — one for a file that does not validate, one for a file that was never
3379
+ written, one for the passing case — because "`runTraining` returns 5" and "the process exits
3380
+ 5" are two different claims. Falsified before being trusted: making `expert train` return
3381
+ `EXIT_OK` breaks two of the three.
3382
+
3383
+
988
3384
  - **The walk already skips vendored and generated trees**, and always did: `SKIPPED_DIRS`
989
3385
  in `detect/walk.ts` covers `node_modules`, `dist`, `build`, `out`, `bin`, `obj`,
990
3386
  `target`, `.venv`, `Pods`, `.next`, `.expo`, `coverage` and more, plus every