tldr-experts 0.3.0 → 0.4.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 (36) hide show
  1. package/CHANGELOG.md +1957 -0
  2. package/README.md +99 -12
  3. package/dist/hooks/answer-capture.js +21 -12
  4. package/dist/hooks/budget-gate.js +95 -11
  5. package/dist/hooks/{chunk-0bt6yb2q.js → chunk-39zh2e44.js} +10 -1
  6. package/dist/hooks/chunk-4cp363kv.js +1766 -0
  7. package/dist/hooks/{chunk-t8tdv11p.js → chunk-7y2dq0pj.js} +1 -1
  8. package/dist/hooks/chunk-9zsqxr6y.js +213 -0
  9. package/dist/hooks/{chunk-a8p2rc94.js → chunk-b8kxzna2.js} +1 -1
  10. package/dist/hooks/{chunk-j234zf0t.js → chunk-c6t5nx0r.js} +37 -166
  11. package/dist/hooks/{chunk-g395gk7e.js → chunk-m3mewgnw.js} +36 -17
  12. package/dist/hooks/chunk-phmdk72a.js +164 -0
  13. package/dist/hooks/chunk-rpcxsqh3.js +199 -0
  14. package/dist/hooks/{chunk-azctppjh.js → chunk-rz541e2b.js} +7 -1
  15. package/dist/hooks/{chunk-y0jdr3et.js → chunk-s1c5h7yx.js} +71 -29
  16. package/dist/hooks/{chunk-p274ckxv.js → chunk-sq44k6g2.js} +155 -12
  17. package/dist/hooks/{chunk-x98qs959.js → chunk-t1ywrfr4.js} +35 -42
  18. package/dist/hooks/{chunk-1zwcxd3f.js → chunk-tzzwddct.js} +1 -1
  19. package/dist/hooks/claim-sources.js +31 -23
  20. package/dist/hooks/dod-gate.js +12 -37
  21. package/dist/hooks/no-reask.js +9 -9
  22. package/dist/hooks/session-start.js +176 -422
  23. package/dist/hooks/statusline.js +14 -11
  24. package/dist/tldrx.js +22798 -12309
  25. package/package.json +3 -1
  26. package/plugin/.claude-plugin/plugin.json +1 -1
  27. package/plugin/skills/tldrx/SKILL.md +16 -3
  28. package/stages/build/stage.md +5 -0
  29. package/stages/build/stage.yml +2 -1
  30. package/stages/plan/stage.md +11 -0
  31. package/templates/budget.yml +5 -0
  32. package/templates/epic.md +5 -3
  33. package/dist/hooks/chunk-ae6bkfs5.js +0 -0
  34. package/dist/hooks/chunk-kw4tffzf.js +0 -139
  35. package/dist/hooks/chunk-sdjnnmzz.js +0 -497
  36. package/dist/hooks/chunk-t56k6146.js +0 -14
package/CHANGELOG.md CHANGED
@@ -1,5 +1,1962 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-09-01
4
+
5
+ ### Changed
6
+
7
+ - **Dependent epics share ONE integration branch (#57, owner decision 2026-09-01, option (a)).**
8
+ One-branch-per-epic assumes the epics are independent. Run `260829-scoring-leaderboard` planned
9
+ E2 (the API) with E3 and E4 (mobile) consuming it, and a downstream story's base — cut from its
10
+ own epic branch, itself cut from `main` — could not see the upstream epic's merged work. It broke
11
+ twice, and both times the host fast-forwarded the EPIC branches by hand: cross-epic surgery done
12
+ with a feature built for stale STORY bases (design §F.2), which collapses branches the owner may
13
+ have meant to merge separately. A run whose epics form a dependency chain now cuts a single
14
+ integration branch and the epics become labels.
15
+ - **Detected at PLAN time, from what the plan already says.** A story whose `depends_on` names a
16
+ story in another epic IS the chain. `validatePlan` reports the cross-epic edges it read
17
+ (`PlanReport.epicChain`, deduplicated per epic pair), and the `plan` gate check states the branch
18
+ model in its passing detail — `epics form a chain (E3→E2, E4→E2) → single integration branch
19
+ \`epic/<run-id>\`` or `independent epics → one branch each`. The owner never discovers it
20
+ mid-Build, which is the half of #57 that was not about branches at all.
21
+ - **`epic/<run-id>`, not a new namespace.** `EPIC_BRANCH_RE`, `watch`'s feature-slug extraction,
22
+ `ship`, `boundary` and the `--reuse-epic` guard are all keyed on the `epic/` prefix; an
23
+ `integration/…` branch would have changed every one of them to buy a word. The run id IS in the
24
+ name — unlike an ordinary epic branch, which is deliberately unscoped because an epic is the unit
25
+ a team merges — because an integration branch belongs to one run by definition.
26
+ - **One run-scoped epic worktree**, `_epic-<run>-integration`, because git will not check one branch
27
+ out in two worktrees. It is picked up by the `_epic-<run>-` prefix that the §2.8 src resolver and
28
+ the run-close cleanup (#16) already enumerate, so the lifetime decision from that change holds.
29
+ - **Backward-safe by an ABSENT key.** The Build executor records what it used in `run.yml`
30
+ (`build.branch_model: per-epic | integration`, additive and optional). A run.yml that names
31
+ branches and no model predates the key — the three closed runs, and any run mid-flight — and stays
32
+ `per-epic`, so it resumes on the branches it already cut rather than being re-pointed at one that
33
+ was never cut. A model, once written, is never rewritten.
34
+ - **Nothing else moved.** The dirty-tree refusal, the foreign-epic refusal and `--reuse-epic`, the
35
+ gated HEAD, `git merge --no-ff` into the epic worktree, and the story-base fast-forward all behave
36
+ exactly as before; only WHICH branch they name changes. `tldrx ship` needed no change: a chained
37
+ run claims one branch, so it opens one PR with no `--branch`, and an unchained multi-epic run still
38
+ asks which.
39
+ - The acceptance test is the leaderboard shape, passing: S1 in E1 writes a file, S2 in E2 depends on
40
+ it, and S2's worktree holds that file with **no `story.base_fastforwarded` event** — the base was
41
+ right when the developer was dispatched, not repaired afterwards. Red before the change (`Expected:
42
+ true, Received: false`).
43
+
44
+ - **An epic worktree now lives for the RUN's lifetime, not the Build stage's (#16, owner decision
45
+ 2026-09-01, option (a)).** The shipped half of #16 made a `file` src resolve against
46
+ `.tldrx/worktrees/<repo>/_epic-<run>-<epic>` before the working tree — and then `BuildSession.finish()`
47
+ removed that directory before the Build handoff was even written, so unless the operator had typed
48
+ `--keep-worktrees` a later Watch stage had nothing to resolve against and the fix bit only under a flag.
49
+ Cleanup moves to run CLOSE, which is what the checkout actually belongs to.
50
+ - **Watch citing epic-only code works by default.** Proved end to end rather than by construction:
51
+ `test/build-executor.test.ts` runs the real pipeline, then resolves a `[src: app:s1.txt:1]` against a
52
+ file the test first shows is committed on `epic/e1` and absent from the working tree.
53
+ - **Every close path takes them**, because a run does not only close one way: `tldrx next` closing the
54
+ last stage, `tldrx approve` signing the last gate, and `tldrx run cancel`. Without the last two the
55
+ change would have traded one leak for another — a cancelled run's checkouts used to be gone already,
56
+ because Build removed them on the way past.
57
+ - **`--keep-worktrees` keeps its meaning, one scope wider**: survive even the run close. It is
58
+ remembered on the run as `keep_worktrees:` (additive and optional; absent means clean up, which is
59
+ what every existing run.yml meant) because the flag is typed on the `tldrx next` that BUILDS and the
60
+ run is usually closed by a different command in a different process.
61
+ - Story worktrees are untouched: still removed the moment a story reaches `done` or `blocked`.
62
+
63
+ - **The budget gate's three open policy questions are answered (#22, owner decision 2026-09-01).**
64
+ bb6204b wired the DATA — both economies, `attended_by`, `runSpend` — and deliberately changed no
65
+ verdict. These are the verdicts.
66
+ - **(a) An `attended_by: host` run is INFORMED, never DENIED, on metered dollars.** `tldrx next` on
67
+ such a run spawns nothing, so the estimate the gate was refusing against is spend that provably will
68
+ not happen. Both the PreToolUse hook and `tldrx next`'s own brake now say every number they would
69
+ have refused with, plus both economies, and allow. The event is `budget.warned`, not `budget.blocked`:
70
+ nothing was blocked, and recording a block that did not happen is the exact failure #22 was filed
71
+ about.
72
+ - **(b) A `host-tokens` ceiling is soft-enforced.** Under that economy the ceiling NUMBER is a
73
+ host-session token allowance, so accumulated declared `tokens:` against it is the one comparison in
74
+ the gate whose two sides share a unit. Crossing it WARNS and still allows. It stops only under the
75
+ explicit opt-in **`on_host_tokens_exceed: block`** in `budget.yml` — an enum beside `on_exceed`,
76
+ defaulting to `warn`, so every file written before the key existed keeps the behaviour it had. The
77
+ refusal never offers `tldrx budget raise`, which moves dollars and would send the operator at the
78
+ wrong number. (a) beats (b): the opt-in still never denies an attended run.
79
+ - **(c) `remainingWork` zeroes the developer share on an attended run**, mirroring `economy:
80
+ host-tokens`, because it is the same fact — the host session pays for those turns. Attendedness and
81
+ the phase economy were independent, so an attended run on a `metered-usd` phase still counted
82
+ developer turns against money this framework will never spend, on the brake and in `run estimate`
83
+ alike. Reviewer floors are untouched in both cases.
84
+
85
+ ### Added
86
+
87
+ - **The documentation site speaks Spanish (`docs-site/es/`, phase 2).** All twelve English pages are
88
+ now mirrored under `/es/` at the same paths and translated into es-MX developer Spanish, and
89
+ `locales.es.themeConfig` carries the Spanish sidebar, nav, edit link and page chrome (outline,
90
+ prev/next, search modal, footer) rather than English chrome around Spanish prose. The placeholder
91
+ that phase 1 left at `/es/` is replaced by the translated landing page.
92
+ - **tldrx's own vocabulary stays in English where it is an identifier.** The stage names
93
+ (`What → How → Plan → Build → Watch`), `run`, `story`, `DoD`, `scope`, `handoff` and `workspace`
94
+ are the things you type or the files on disk, so they are not translated; each is glossed once in
95
+ Spanish where it first appears. `gate` is the one exception — it renders as **compuerta**, because
96
+ the phase-1 placeholder had already shipped that word to the live site.
97
+ - **Code blocks and command output are verbatim English** — a translated transcript would be a
98
+ transcript of a command nobody ran. The narration around them is translated.
99
+ - **Anchors were verified against the rendered HTML, not assumed**, because dead-link checking does
100
+ not see them. That mattered here: VitePress's slugifier strips accents but *keeps* `¿`, so
101
+ `## ¿Cómo lo detengo?` becomes `id="¿como-lo-detengo"`. The one heading that is linked to carries
102
+ an explicit `{#puedo-manejarlo-desde-claude-code}`. A sweep of the built site resolves all 5
103
+ anchor links across its 26 pages, 0 broken.
104
+ - **Release notes are deliberately not translated**: the page is generated from `CHANGELOG.md` at
105
+ build time, so a Spanish copy would drift. The `/es/` sidebar links the English page and says so.
106
+
107
+ - **A public documentation site, written for people who have never seen tldrx (`docs-site/`, phase 1).**
108
+ A VitePress site deployed to GitHub Pages by `.github/workflows/docs.yml` on any push to `main` that
109
+ touches `docs-site/` or `CHANGELOG.md`. Twelve short English pages — a landing page, a Quickstart, one
110
+ page per concept (the five stages, files-as-state, gates, evidence, budgets), four guides and a
111
+ condensed CLI overview — plus a generated changelog and a Spanish placeholder. None of them is pasted
112
+ from `docs/`, which stays the agent-facing reference. Every command
113
+ and every block of output on the Quickstart was produced by running the real binary; nothing on the
114
+ site documents a flag that `--help` does not.
115
+ - **The changelog page is generated, never copied.** `docs-site/scripts/gen-changelog.ts` reads this
116
+ file at build time and emits one line per entry, so a release note reaches the website without
117
+ anybody maintaining a second copy of it. The generated page is gitignored for the same reason.
118
+ - VitePress dead-link checking is left ON and the build is green with it — proven by a probe, not
119
+ assumed: a deliberate link to a missing page failed the build with `1 dead link(s) found`. (Anchors
120
+ are NOT checked by it, so `#fragment` targets were verified against the rendered HTML by hand.)
121
+ - i18n is wired now, with the English content at the root and a Spanish placeholder under `/es/`, so
122
+ phase 2 is a matter of adding files rather than restructuring the site.
123
+ - `docs-site/` is excluded from the npm package (it is not in `files:`) and from `tsc --noEmit` (the
124
+ root tsconfig includes only `bin`, `src`, `test`) — measured: `npm pack --dry-run` still lists 52
125
+ files and none of them is under `docs-site/`.
126
+
127
+ - **`tldrx learn` chapters 3-8 — the whole loop, played (#30, phase 2).** The tutorial now runs end to
128
+ end in about five seconds of real commands: **3** the gate (`approve --note`, and the record it writes
129
+ in `run.yml`), **4** one story built for real (How's `auto` gate closing itself over its seven
130
+ conditions, Plan's human gate, then a Build that cuts `epic/bulk-pricing`, spawns a developer in a
131
+ worktree, re-runs the story's `npm run test` DoD, commits, merges and spawns a reviewer), **5** a
132
+ genuinely red DoD and the three commands back from it (`story reopen`, `reject`, `budget raise`),
133
+ **6** an `agent` gate closed by `approve --as-agent` over a structured evidence note, **7**
134
+ `run attend host`, the refusal a bare `next` then gives, and the `next --prepare` / `next --commit`
135
+ pair actually run, **8** `cost --all`, `run estimate`, and the budget brake refusing a stage the
136
+ phase can no longer afford.
137
+ - Chapter 4's DoD is real, chapter 5's failure is real: the story's test script is `exit 0` until a
138
+ developer replaces it with a `node` test that then catches a wrong number — so the tutorial teaches
139
+ "a green DoD over an empty test proves nothing" by letting it happen rather than by saying it.
140
+ - Chapter 5 opens a second run, so `{run}` and `{runDir}` now expand in a step's `command` as well as
141
+ in a turn's writes, and mean **the newest run that is still open** — the same set `resolveRun` picks
142
+ from, so the placeholder and the CLI cannot disagree about which run a command means.
143
+ - Chapter 4's `prepare()` commits what `init` left untracked, because the Build executor refuses to
144
+ cut a branch from a dirty tree (measured: `?? .gitignore, ?? CLAUDE.md`, exit 2) — and the narration
145
+ teaches that refusal instead of hiding it.
146
+ - **Fixed: the toy repo now carries its own git identity.** Chapter 4's Build commits through the
147
+ framework's own executor, with whatever identity the machine has — so on a box with no global
148
+ `user.email` (a fresh laptop, a container, `ubuntu-latest`) `git commit` failed with `Author identity
149
+ unknown` and the chapter died three commands in. `makeSandbox` writes `user.email`, `user.name` and
150
+ `commit.gpgsign=false` into the sandbox repo's own config, on every open, so an older sandbox is
151
+ repaired too. The test pins it with `user.useConfigOnly` rather than an empty global config: git
152
+ guesses an identity from gecos and hostname and only fails where it cannot, so an empty config
153
+ passes on a laptop and fails in a container — which is exactly how this reached CI.
154
+
155
+ - **`tldrx learn` — a playable sandbox tutorial that runs the REAL commands (#30, phase 1 of 3).**
156
+ A tutorial that runs the shipped binary can never drift from the shipped behaviour: every output the
157
+ learner reads is produced by the code, not written down by a doc author. `tldrx learn` scaffolds a
158
+ throwaway workspace (a four-file git repo with a `test` script that exits 0), then narrates, shows the
159
+ exact command, waits for Enter and RUNS it — `tldrx init`, `run new`, `next`, `answer` are the real
160
+ ones, against that sandbox.
161
+ - **Chapters 1 and 2 ship playable**: init (read the `workspace.yml` detection actually produced) and
162
+ the What stage (a question comes back, `tldrx answer` records it, it becomes `F001` in
163
+ `.tldrx/memory/facts.yml`). Chapters 3-8 from the issue are phase 2 and are DATA plus one
164
+ `assert()` — see the contract in `src/core/learn/Chapter.ts`.
165
+ - **It cannot spend money, by construction rather than by convention.** The sandbox writes its own
166
+ `claude` stand-in, names it in `TLDRX_CLAUDE_BIN` and puts it first on the child `PATH`, so neither
167
+ the spawn seam nor a bare `claude` on `PATH` can reach the real CLI. `test/learn.test.ts` proves it
168
+ the only way worth proving: it plants a booby-trapped `claude` that writes a marker file, plays both
169
+ chapters, and asserts the marker is absent AND the chapters completed — because a tutorial that
170
+ spawned nothing would pass a marker check for the wrong reason.
171
+ - **It cannot touch your work.** Everything is written under the sandbox directory (`~/.tldrx-learn`
172
+ by default, `--sandbox` to move it), and a sandbox that would sit inside a real tldrx workspace is
173
+ refused before a byte is written.
174
+ - **Files as state, like everything else**: `progress.json` in the sandbox is what makes a bare
175
+ `tldrx learn` resume, `--chapter <n>` jump (playing an unfinished prerequisite first), and `--reset`
176
+ start over. With no terminal on stdin the chapters play straight through rather than hanging at the
177
+ first prompt.
178
+ - The stand-in agent is scripted per chapter and is **fail-closed**: a prompt no turn matches exits 1
179
+ and names the turns it did have, so a hole in the tutorial is a loud failure rather than an
180
+ improvised answer. The `stream-json` writer moved from `test/fixtures/fakeStream.ts` to
181
+ `src/core/facilitator/fakeTranscript.ts` — beside `agentEvents.ts`, which reads that format — so the
182
+ tutorial's stand-in and the four test fakes cannot drift apart. The fixture re-exports it.
183
+
184
+ - **`tldrx note <run> [--stage <id>] "text"` — an honest carrier for an operator annotation, at the
185
+ moment it happened (#46).** Measured on `260829-scoring-leaderboard` (2026-09-01): a host performed an
186
+ owner-delegated mechanical resync of eight story dod blocks, was asked to note it in the run log, and
187
+ could not — `events.jsonl` is append-only and tool-owned, so the only carriers were a FUTURE gate note
188
+ (late, and keyed to a decision the note is not about) or a `reject` (destructive). The session ended up
189
+ hanging the context off an unrelated `story.reopened`.
190
+ - **One event, and nothing else.** It appends a single `operator_note` line carrying actor, timestamp,
191
+ optional stage and the text. It does not go through `RunStore.save()`, which would rewrite `updated_at`
192
+ and re-derive every status for an annotation that changed no state: `test/operator-note.test.ts`
193
+ compares `run.yml` and `budget.yml` **byte for byte** across the call, because "safe to reach for
194
+ mid-run" is the whole of what makes the verb usable.
195
+ - **Every refusal writes nothing.** An unknown run (exit 3), a stage this run does not have (exit 2), an
196
+ empty note (exit 1) and a lone argument that turns out to name a run all refuse before the log is
197
+ opened. The last one is the trap worth naming: `tldrx note 260829-x` is a half-typed command, not a
198
+ note whose entire content is a run id, and recording it would be the one outcome nobody wanted.
199
+ - **Visible afterwards.** `tldrx run status` prints the last three (with `tldrx replay` named when there
200
+ are more), `--json` carries them as `operator_notes`, and `tldrx replay` narrates every one in place.
201
+
202
+ - **`tldrx run gates set <stage>:<policy> --note "…"` — the signed upgrade path for a frozen
203
+ `gates_policy` (#14).** The policy is resolved at `run new` and frozen into `run.yml` by design, and that
204
+ default is not taken back here. What it left with no door at all, found on the 2026-08-30/31 unattended
205
+ pilots: a run created BEFORE the `agent` policy existed can never use `approve --as-agent`, and `run.yml`
206
+ is hand-edit-forbidden (spec §1) — so the only move was to abandon the run.
207
+ - **Built like `story reopen`, because it is the same kind of act:** a person overruling state the machine
208
+ is holding. A `--note` is required; ONE stage per invocation (a list would let a second change ride on
209
+ the first one's signature); the entry must be QUALIFIED, because under `--gates` a bare `plan` means
210
+ `human` and a signature must not rest on a default; and a no-op is refused rather than recorded.
211
+ - **One `gate.policy_changed` event** carries the phase, who signed it, the old and the new value, and the
212
+ note. A run with no `gates_policy:` at all gets the FULL map written — every stage explicitly, with the
213
+ one change applied — because a partial map would quietly claim its other stages had been decided too.
214
+ - It changes who may CLOSE a gate from then on. Gates already signed are untouched, and nothing re-reads
215
+ the policy of a closed one.
216
+
217
+ - **`tldrx ship` — open a PR from the run's epic branch, with the handoff as the body (#15).** The loop
218
+ ended at "merge by hand": a finished epic sat on `epic/<slug>`, the document explaining it sat in
219
+ `<run>/<phase>/handoff.md`, and nothing carried either one to a PR.
220
+ - **It never pushes.** `core/build/git.ts` has no `git push` wrapper on purpose (spec §5), and this verb
221
+ keeps that rule rather than being the exception to it: a branch the remote has not seen is a refusal
222
+ that names the exact `git -C … push -u` command. Publishing a branch is a decision, and it stays the
223
+ operator's.
224
+ - **It never writes to the run** — no event, no gate, no cursor, no money — and it does not mirror
225
+ tickets: `tldrx tickets sync` already is that verb, holds the `process.yml` contract and appends
226
+ `ticket.synced`. A second, thinner mirror inside `ship` would give the workspace two answers to "is this
227
+ story mirrored", so `ship` names it as the next step instead.
228
+ - **Clean refusals, in a sentence:** no epic branch, no handoff, no `gh`, no remote, an unpushed branch,
229
+ several epic branches with no `--branch`, a branch the run did not cut. The body goes to `gh` as
230
+ `--body-file`, never as an argument, so a long handoff cannot overflow an argv limit.
231
+ - Both external binaries go through one narrow transport that takes a cwd — the same idea as
232
+ `adapters/transport.ts`, and the only way to ASSERT the argument shape of a command the suite must not
233
+ run. The unit tests drive a recording fake; the one end-to-end test puts a STUB `gh` first on PATH in a
234
+ throwaway workspace with a throwaway bare `origin`. The real `gh` is never invoked by a test.
235
+
236
+ - **`TLDRX_CLAUDE_BIN` — point the sub-agent spawn at a different binary (#27, minimal slice).**
237
+ `spawnAgent` hardcoded `claude`, so a pinned install, a wrapper that adds a proxy or credentials,
238
+ and a stand-in in a sandbox all required patching source. The variable replaces the executable
239
+ NAME and nothing else — the argv is still Claude Code's, so what it points at has to speak
240
+ `-p --output-format stream-json --json-schema` — and blank or whitespace counts as unset. Honoured
241
+ everywhere the CLI is spawned: `spawnAgent`, the `--dry-run` command line (`describeSpawn`) and
242
+ `claude mcp list` (`McpProbe`). `tldrx doctor` deliberately still checks `claude --version`,
243
+ because `env.yml` declares that string. Documented under **Environment variables** in the CLI
244
+ reference. This is not the provider abstraction #27 asks for; #27 stays open for it.
245
+
246
+ - **A drift guard on `templates/story.md` and `templates/epic.md` (#48).**
247
+ Both ship in the npm package, both state the Plan front-matter schema, and **nothing in `src/`
248
+ reads either one** — a second copy of a contract whose first copy is computed from `STORY_KEYS`
249
+ and `EPIC_KEYS`. Add a required key and `schemaContract.ts` stops compiling while the templates
250
+ say nothing; a human then opens one, writes a story the check refuses, and the framework looks
251
+ broken. They now go through `validateStoryFile` / `validateEpicFile` — the very checks the stage
252
+ gates on — with their key sets asserted equal to `STORY_KEYS` / `EPIC_KEYS` in order, and the
253
+ status enum each spells out in a comment asserted equal to `PLAN_STATUSES`. Proven to have teeth:
254
+ renaming one key and staling one enum comment turns three tests red. Whether the files should be
255
+ generated or deleted is a packaging decision and is left open on #48.
256
+
257
+ - **The merge-wave sandbox is built under a hostile `init.defaultBranch` (#49).**
258
+ `test/merge-wave.test.ts` names every repo `main`, and CI run 33459567355 failed in the test's
259
+ own setup — `git push -q origin main` → `src refspec main does not match any` — on a runner whose
260
+ default branch is not `main`. `f1ffe56` had already fixed it (`-b main` on both inits,
261
+ `--branch main` on the clone), but nothing EXERCISED the fix: on a `main`-defaulting host,
262
+ removing the treatment changes nothing. The sandbox now pins `init.defaultBranch: trunk` for
263
+ every git command it builds itself with, the clone asserts it is on `main` rather than
264
+ discovering it five lines later, and two tests pin the mechanism — untreated reproduces the CI
265
+ error verbatim, treated does not.
266
+
267
+ - **The Plan prompt now STATES the schema the `plan` check enforces, generated from the check itself (#35, #38).**
268
+ `stages/plan/stage.md` named the output filenames — `stories/<id>.md`, `epics/<epic>.md`, `waves.yml` — and
269
+ said nothing about their shape, so a fresh agent learned it by having a paid attempt refused. Measured twice
270
+ in two days: on `260831-hardening-d1` the plan sub-agent followed the rendered bundle faithfully and wrote
271
+ seven stories as plain markdown (`no YAML front matter — the file must open with ---`), and on
272
+ `260829-scoring-leaderboard` it wrote a 1,009-character acceptance item against a `MAX_ITEM_CHARS = 512`
273
+ cap that appeared in no file it could read. Both attempts were consumed, correctly and uselessly.
274
+ - **Generated, not copied.** `src/core/plan/schemaContract.ts` renders a `## Output schemas` section from
275
+ `STORY_KEYS`, `EPIC_KEYS`, `PLAN_STATUSES` and the six `MAX_*` constants — the same definitions
276
+ `validateStory`, `validateEpic` and `validateWaves` read. `Record<StoryKey, Field>` is load-bearing:
277
+ add a key to a schema and the file stops compiling until the new key has a value and a rule. The worked
278
+ examples it ships are run through `validatePlan` itself in the tests, so the contract the prompt states
279
+ and the contract the check enforces are provably the same one. This repo already had the other kind:
280
+ `templates/story.md` carries the schema correctly and `grep -rn 'story\.md' src/` finds nothing that
281
+ reads it.
282
+ - **Only where the check runs.** `applyCheckContracts` splices it under an H2 the framework owns, for a
283
+ stage that declares `checks: [plan]` AND writes `waves.yml` — the same predicate `checkPlan` skips on,
284
+ now shared (`writesPlanArtefacts`). A What or How prompt is byte-identical to before. It goes into
285
+ `stage.md` rather than after the inputs because `prompt.ts` orders the document most-stable-first for
286
+ the prompt cache, and a section computed from constants is exactly as stable as the stage body.
287
+ - **~4.2 KB against a $4 stage.** The alternative it replaces is a refused attempt per fresh workspace.
288
+
289
+ - **`tldrx plan sync-dod` — the mechanical repair for dod blocks an edited `workspace.yml` orphaned (#42).**
290
+ A story's ```dod block may only name commands `workspace.yml` declares, byte for byte, and that rule is not
291
+ relaxed by a byte here — it is what stops a data file from running an arbitrary command as you. What it
292
+ lacked was an inverse. Measured live 2026-08-31 on `260829-scoring-leaderboard`: fixing `workspace.yml`
293
+ (a filtered `test:`, `lint:` deleted) instantly invalidated the dod blocks of **8 approved stories**, and
294
+ the only recoveries were hand-editing agent-approved artefacts or re-running the whole Plan stage — a paid
295
+ turn to change two lines in eight files, churning thirteen correct stories on the way.
296
+ - **Evidence, not similarity.** The ancestry comes from git's history of `.tldrx/workspace.yml`: a line a
297
+ previous version declared under a role the current file still has becomes that role's current command; a
298
+ line whose role is gone is dropped; a line the current file already declares is left alone; and a line no
299
+ version ever declared — or one two roles once shared and now disagree on — is **flagged, its story left
300
+ byte-identical**, and the command exits `2`. Guessing at a rename by string shape is the one thing this
301
+ must not do. In a workspace with no git history there are no ancestors, so everything non-current is
302
+ flagged.
303
+ - **It touches nothing else.** Front matter, prose, blank lines and the fences come back byte-identical;
304
+ only the command lines inside the fence move. The previous version is kept at `<story>.md.bak`
305
+ (`writeAtomic`), the summary is a per-story diff, `--dry-run` prints it and writes nothing, and the result
306
+ is validated by the same plan check the drift came from. Stories that CAN be synced still are — one
307
+ undecidable line is not a veto on the other seven files.
308
+ - **The drift message now names the remedy.** "`<cmd>` is not one of .tldrx/workspace.yml's commands — a
309
+ story may not invent one" gained a second sentence pointing at `tldrx plan sync-dod`. Only for a STORY:
310
+ a stage's `cmd:` is a line a human wrote, and `sync-dod` does not touch stage files.
311
+
312
+ - **`tldrx answer <Qn> "…" --supersede` — the verb for reversing a decision already on record.**
313
+ Found live 2026-08-31: an owner reversed an answered decision after the risk behind it was
314
+ refuted, and `tldrx answer` refused ("Q1 is not an open question") because an answer is
315
+ recorded once. `superseded_by` had been in the §2.5 schema since the first draft with **no
316
+ command that wrote it**, so the only route was a hand edit of `facts.yml` — and a hand edit
317
+ that left `superseded_by: null` left the reversed decision inside `FactsStore.active`, which
318
+ every stage reads as never-re-ask truth. The next run would have reinstated the call the owner
319
+ had just taken back.
320
+ - **The verb.** Valid only on an ANSWERED question (on an open one it exits `1` and says to
321
+ answer it normally; without the flag an answered one still exits `3`, now naming
322
+ `--supersede`). It appends a new fact carrying the whole new answer with the same `area` and
323
+ `repos` and ordinary provenance, sets the old fact's `superseded_by` and the new one's
324
+ `supersedes` — both halves, through `FactsStore.supersede`, under the workspace lock, so the
325
+ reciprocity rule cannot be broken — and never edits a byte of the old fact's text. Reversing
326
+ twice supersedes the SECOND fact: the chain is walked to its head, so it stays single-link.
327
+ - **The questions block is appended to, not rewritten.** The original `[Answer]:` line and its
328
+ footer stand; a superseding answer line and a `reanswered_by | reanswered_at | fact |
329
+ supersedes` footer go under them. `status:` stays `answered`, because it is.
330
+ - **Every reader that feeds a decision now skips a superseded fact.** This was half the work
331
+ and none of it was new code: `superseded_by` had a writer for the first time, and six readers
332
+ had been filtering on retirement alone. `isLive` (`core/facts/Fact.ts`) is now the one
333
+ predicate behind `FactsStore.active` (no-re-ask, `tldrx run new --from` de-duplication),
334
+ `findDuplicate`, `renderFacts` (the `{{facts}}` section of **every** prepared prompt),
335
+ `renderWatchFacts`, `runFacts` (the implicit plan's "this run's answers") and `relevantFacts`
336
+ (the training miner). One test had pinned the old behaviour in words — "a
337
+ superseded-but-not-retired row stays visible" — and that was the bug, not the rule.
338
+ - **History readers still show it, labelled.** `tldrx replay` renders the new `fact.superseded`
339
+ event as its own line — the one moment the workspace's durable memory changes its mind was
340
+ the one moment replay could not narrate — and `tldrx retro` lists the old fact with
341
+ `(superseded by F<n>)` beside it.
342
+
343
+ ### Fixed
344
+
345
+ - **`tldrx learn` — the cold-player QA round (#30).** A first-time player played all eight chapters
346
+ and returned SHIP-with-fixlist. Everything they found is fixed or recorded:
347
+ - **Chapter 8 no longer lies about the brake.** It said "the phase has already spent its Watch
348
+ money" while the tool printed `$1.89 left … estimate is $2.00`. The real mechanism is that a
349
+ re-run is priced at the stage's WHOLE declared `budget_usd`, never at what a second attempt might
350
+ add — so a stage that has spent anything can no longer afford itself. The chapter now says that,
351
+ quotes both figures, and `assert()`s them against the `budget.blocked` event so the numbers cannot
352
+ drift away from the sentence.
353
+ - **Chapter 1 no longer promises something chapter 2 does not deliver.** `--no-interview` skips
354
+ *init's* setup interview, which no chapter covers; the forward reference is gone and the debrief
355
+ now sends the learner to `.tldrx/init-handoff.md`, where measured/inferred/assumed and
356
+ `[src: …]` / `absent:` actually live.
357
+ - **`tldrx learn --chapter <n>` refuses a chapter that is already played**, up front and by name,
358
+ instead of narrating it and then dying mid-chapter on `run new: … already exists` (exit 1,
359
+ measured). The refusal names `--reset` and the chapter a bare `learn` would resume at.
360
+ - **The tutorial has a door out.** The ending now names the first four commands to type on a real
361
+ repo — `tldrx init` (with the warning that it runs an interview by default), `run new --scope
362
+ hotfix`, `next`, and `tldrx ship`.
363
+ - **Chapter 7 RUNS `next --prepare` and `next --commit`** against the feature run's Watch stage
364
+ instead of describing them in a debrief. Chapters 6 and 7 swapped for it: the attended chapter
365
+ addresses the feature run through `{run}`, so the hotfix run has to be signed off first.
366
+ - **Every non-zero exit code is printed** (`→ exit 4`), so the code chapter 2 teaches is a thing
367
+ the learner reads rather than a thing they are told. Chapter 5 now also demonstrates the exit-2
368
+ refusal a bare `next` gives with two runs open, and names the two run-id spellings.
369
+ - Jargon defined at first use — expert, level 0, the `claim-sources` / `no-reask` / `budget-gate`
370
+ bracket, `boundary`, `[src: …]`, `absent:`, economy, §2.11 — and the `expert … has no evidence`
371
+ nudge explained once instead of repeating unexplained nine times.
372
+ - Known and NOT fixed: a step's stderr (where the agent stream lives) is buffered and printed after
373
+ its stdout, so a summary can appear before the stream that produced it. Interleaving needs an
374
+ `onStderrLine` on the runtime seam and in both implementations; documented in `engine.ts`.
375
+
376
+ - **`tldrx cost` no longer claims "two economies" over one (#56).** The `(no total: two economies, no
377
+ exchange rate)` footnote was unconditional, so a run whose every attempt was metered in dollars was
378
+ told no total could be printed. It is printed only when both economies are actually present.
379
+
380
+ - **The README's "Not on npm yet" warning was false and told readers not to run the install line
381
+ directly underneath it.** The package IS published: `npm view tldr-experts version` → `0.3.1`,
382
+ exit 0. The warning is removed rather than re-dated — the npm badge at the top of the README
383
+ already shows the live version, so nothing in its place can go stale the same way. The
384
+ `npm i -g tldr-experts` line it was contradicting is unchanged.
385
+
386
+ - **The site's own home page linked an anchor that does not exist.** The hero's "Try it offline,
387
+ free" button pointed at `/quickstart#try-the-whole-thing-first-for-free`, but the heading renders
388
+ as `id="first-try-it-for-free"` — verified against the LIVE page, not just a local build. Dead-link
389
+ checking never saw it because VitePress does not check fragments. Repointed, and a sweep of the
390
+ built site now resolves every anchor link it emits.
391
+
392
+ - **`tickets sync`, `tickets status` and `budget show` took a run id as a positional that neither
393
+ their `usage` nor their `--help` declared (#53).** Measured at `7ac298c`:
394
+ `tldrx tickets status zzz-positional-probe` and `tldrx budget show zzz-positional-probe` both reach the
395
+ run resolver and exit `3` with `no run 'zzz-positional-probe'`, so both forms have always been
396
+ supported. The mechanism is the same in each: the subcommand word is consumed by the dispatcher
397
+ (`tickets.ts:53`, `budget.ts:32`) before `stringFlag(args, "run") ?? args.positionals[0]` runs
398
+ (`tickets.ts:246`, `budget.ts:48`), so `positionals[0]` is a run id by then.
399
+ - **The capability is DECLARED, not removed.** `usage` now reads `tldrx tickets sync [<run>] …`,
400
+ `tldrx tickets status [<run>] …` and `tldrx budget show [<run>] …`, and both help entries gain the
401
+ `[<run>]` arg every other run-scoped command already carries. Nothing about what the CLI accepts
402
+ changed.
403
+ - **This is the axis #51's guard cannot see.** That guard compares the registry to the usage; here the
404
+ registry itself was narrower than the code, and where both are silent both are green. The new check
405
+ in `test/cli.test.ts` is a hand-written list — nothing derives a positional from source — but its
406
+ BEHAVIOURAL half spawns the real CLI against a throwaway workspace, so it also goes red if the
407
+ capability is ever removed, which is the direction a tidy-up of the arg parsing would break it in.
408
+
409
+ - **`tldrx run gates set` was documented nowhere in `docs/` (#54).** `grep -rn "gates set" docs/` returned
410
+ nothing at `7ac298c`, so the CLI reference — the page a reader lands on from the README — described
411
+ seven of `tldrx run`'s eight subcommands. It matters more than an ordinary docs gap because `gates set`
412
+ is the ONLY sanctioned way to move a `gates_policy` that `run new` froze, and the situation it exists for
413
+ (a run opened before the `agent` policy existed, which can otherwise never use `approve --as-agent`) is
414
+ one an operator hits mid-run and searches the docs for. What they found was "abandon the run".
415
+ - Documented in all three places the question gets asked from: the `tldrx run` usage block and a new
416
+ prose entry in `docs/guide/08-cli-reference.md`; a **Moving a frozen policy** section in
417
+ `docs/guide/03-runs-and-gates.md`, right under the paragraph that explains the freeze, with the
418
+ `--gate-agent` disclaimer further down now linking to it; and `docs/spec.md`, both in §2.2's
419
+ `gates_policy` row and as its own §CLI row (exits `0,1,2,3`, each measured).
420
+ - Every copy carries the two facts a usage line cannot: **`--note` is mandatory**, and the change
421
+ appends one **`gate.policy_changed`** event with the actor, the moment, the note and the old→new
422
+ value — the whole audit trail for a mutation nobody would otherwise go looking for.
423
+ - Guarded: `test/cli.test.ts` now asserts the CLI reference names every subcommand in
424
+ `runCommand.subcommands`, plus `gate.policy_changed` by name. Scoped to `run` on purpose —
425
+ `plan sync-dod` has no section on that page at all (a separate gap, unfiled), and `hook`'s seven
426
+ scripts are deliberately documented as one `<script>` slot.
427
+
428
+ - **The epic file duplicated every story's status, and nothing ever updated the copy (#50).**
429
+ Measured on `260829-scoring-leaderboard` (2026-09-01): `03-plan/epics/E1.md` listed S1, S2 and S3 as
430
+ `todo` in its `## Stories` table while `03-plan/stories/S1.md` said `done` (merged at `0a50660`,
431
+ `task.done` in `events.jsonl` at 23:46:11Z) and S2 said `in_progress`. Nothing had lied — nothing had
432
+ written, either. Fixed by **removing the copy**, not by adding a second writer, and the repo already
433
+ drew that line for the one field it does maintain: the epic's front-matter `status:` is DERIVED and
434
+ written by `BuildExecutor.updateEpicStatus` (`build.ts:2552`) from the story files. A copy with a
435
+ writer is a cache; a copy without one is a lie waiting to be read.
436
+ - **Nothing parsed the table.** `validateEpicFile` is front matter only ("the front matter is the whole
437
+ schema", `schemas/epic.ts:69`), `adapters/body.ts:50` mirrors an epic to a ticket as a bare list of
438
+ ids with no status, and the dashboard reads the front matter. A writer would have been maintaining a
439
+ document with no reader.
440
+ - **Both copies of the shape are fixed, not just the visible one.** `templates/epic.md` now points at
441
+ `03-plan/stories/<id>.md` instead of tabulating it, and the GENERATED contract the Plan sub-agent
442
+ reads (`schemaContract.ts`, spliced into the stage prompt) now says "Do NOT restate a story's status,
443
+ repo or `depends_on`" — without that, the next Plan agent invents the table again, which is how it
444
+ got there.
445
+ - `test/plan-schema-contract.test.ts` runs the issue's acceptance: build a plan from the shipped
446
+ templates, flip S1 to `done` through `updateStoryFront` (the writer the Build executor uses), then
447
+ grep the epic. Any `S<n>` + status word on one line of the epic body is a claim, and the claim set
448
+ must be empty.
449
+
450
+ - **Seven `usage` strings were narrower than the same command's `--help` (#51, after #25).**
451
+ `usage` is what a BAD invocation prints — `run.ts:85`, `questions.ts:38`, `tickets.ts:60`, `gate.ts:44`
452
+ and three more write `<cmd>.usage` to stderr — so it is the string an operator reads at the exact
453
+ moment they got the invocation wrong, and it was hiding flags the code accepts. Widened: `run attend`,
454
+ `run status`, `run estimate`, `run auto`, `run unlock` and `run cancel` now show `[--run <id>]`
455
+ (all six read `args.positionals[0] ?? stringFlag(args, "run")`); `tldrx next` shows it too
456
+ (`next.ts:48`); and `tldrx questions lint` shows the `[<run>]` positional it has always taken
457
+ (`questions.ts:49`, and `docs/guide/08-cli-reference.md` had been documenting it for longer than the
458
+ CLI admitted it).
459
+ - **The guard is subcommand-aware, and that is not gold-plating.** A plain
460
+ `usage.includes("--run")` calls `run` CLEAN, because the new `run gates set` line names `--run` —
461
+ measured, the naive check saw **one of run's seven gaps**. `test/cli.test.ts` scopes a flag that
462
+ declares a `sub:` to that subcommand's block of the usage, and falls back to the whole string for a
463
+ `sub:` that is a MODE rather than a word in argv (`dashboard --out {sub: "static"}`, which the first
464
+ draft reported as a gap it is not).
465
+ - **Three of the seven were spelling, not gaps, and are allowlisted with the reason:** `seed`'s
466
+ `<Qid> "<text>"`, `watch`'s `check <feature>` and `hook`'s enumerated script names all say the same
467
+ thing more specifically than the registry's general name. A fourth, `run`'s
468
+ `<stage>:<human|auto|agent>`, is the same case. The allowlist is itself checked: every entry must
469
+ still name a declared flag or arg, so a rename turns an exemption red instead of silent.
470
+ - **`tickets --dry-run` was left OUT on purpose.** `tickets sync` previews by default and `--apply` is
471
+ the write; advertising `--dry-run` would imply the opposite, and `test/money-safety.test.ts:319`
472
+ asserts its absence. Recorded in the allowlist as a decision rather than papered over as a gap.
473
+
474
+ - **A literal ESC byte in `McpProbe.ts`'s ANSI regex (#52).** `src/core/doctor/McpProbe.ts:11` wrote a raw
475
+ `0x1b` where `\x1b` was meant, so the source read `/<ESC>\[[0-9;]*m/g` and a reader — in a diff, in a
476
+ review, in a terminal, in most editors — saw `/\[[0-9;]*m/g`, a different and wrong-looking regex that
477
+ someone tidying is one keystroke from breaking `tldrx doctor --mcp` with. **Not the #47 hazard**, and
478
+ worth saying: ESC does not trip the binary-file heuristic, and the file was always visible to a grep.
479
+ Behaviour is byte-identical, and `.source` is the wrong instrument for proving that (it returns the
480
+ literal as written, so the two spellings differ there while compiling to the same matcher) — so
481
+ `test/doctor.test.ts` compares the shipped `stripAnsi` against a reference rebuilt from the old
482
+ literal-ESC form over a nine-line corpus, 4 of which change. `test/source-hygiene.test.ts` now flags a
483
+ raw ESC as well as a NUL: measured across all 479 `.ts` files under `src/`, `test/`, `bin/` and
484
+ `scripts/`, `McpProbe.ts` held the only one, so the check has no false positives to trade against.
485
+
486
+ - **A stray NUL byte made two source files invisible to every grep-based sweep (#47).**
487
+ `test/cli.test.ts` carried one literal `0x00`, so `file(1)` called it `data` and `grep -I` —
488
+ ripgrep and ugrep too — dropped it SILENTLY, exit 0, no message. Measured on `origin/main`:
489
+ `grep -lI -E 'node:child_process|Bun\.spawn' test/*.ts` returned **36 files with `cli.test.ts`
490
+ absent**, though it calls `Bun.spawn` on line 32. That is how it missed #43's load-aware timeout
491
+ and then timed out at 5004 ms on the very merge that was fixing timeouts. Writing the guard found
492
+ a **second** one nobody had reported — `src/core/text/srcToken.ts:711`, a NUL used as a cache-key
493
+ separator, which hid that file from every `src/` sweep (367 of 368 `.ts` files visible). Both are
494
+ now the two-character escape `\0`: identical at runtime, ordinary text on disk. Post-fix the same
495
+ sweep finds `cli.test.ts` and all 368 `src/` files. `test/source-hygiene.test.ts` walks `src/`,
496
+ `test/`, `bin/` and `scripts/` and fails on any NUL, with the offender named at `path:line`.
497
+
498
+ - **The five wave-5 docs-pass nits, each a sentence nothing was checking (#25).**
499
+ - **`boundary.ts` promised an exclusion is "never silent" and dropped state paths without a
500
+ word** — `BoundarySurface.excluded` was populated and read by nothing. Every verdict that has a
501
+ surface now names what was excluded, green and red alike, including the case where the
502
+ exclusion was ALL there was and the run therefore reported "declares no surface".
503
+ - **The precondition refusal asserted "the stage is still `ready`" without looking.** It reports
504
+ the status `run.yml` actually holds. Bigger than filed: on a FRESH run the stage at the cursor
505
+ is `pending`, not `ready`, so the old sentence was wrong in the ordinary case as well as on the
506
+ retry of a `failed` stage.
507
+ - **The agent-gate fallthrough printed its label twice** — `boundary: boundary=…`, because every
508
+ condition detail was prefixed with its own id including the two that have a trigger of their
509
+ own. Only the generic `condition` trigger keeps the prefix; alone it names nothing.
510
+ - **`dispatchNotes.ts` documented `.agent/04-build/build/S5/…`**, one phase segment more than
511
+ `dispatchNotesPath` builds. The example is now asserted equal to the path the code produces.
512
+ - **Two usage strings were narrower than their own `--help`**: `tldrx gate template` omitted the
513
+ positional `[<run>]` it accepts, and `run new` spelled `--gates <a,b|all|none>` where the help
514
+ says `<a,b|a:agent|all|none>`.
515
+
516
+ - **`tldrx next --dry-run` spawns nothing. It used to cost $0.42 a go (#17).** The flag ran the
517
+ stage for real — one `claude -p`, one `agent.spawned`, one `agent.result`, the cost on the
518
+ ledger — and only reverted the non-handoff FILES afterwards. Measured on the 2026-08-30 pilot;
519
+ `tldrx next --help` had said "Spawns nothing and writes nothing" the whole time, so this is the
520
+ code catching up to the promise rather than the promise being watered down to the code.
521
+ - **What it does now.** It assembles the prompt, prices it, and stops: the expert bundle, the
522
+ context ledger, the prompt size, the declared outputs, and the **exact `claude -p` argv** it
523
+ would have run (with the `--json-schema` blob elided as `<envelope-schema>`, rendered from
524
+ `buildClaudeArgs` itself so the printed command cannot drift from the real one). Then the two
525
+ commands that would actually dispatch it. Exit `0`.
526
+ - **Nothing is written either.** No prompt bundle and no `pending.json`, so a dry run cannot
527
+ leave a `--commit` looking at a turn that never happened; the stage keeps its status and the
528
+ ledger keeps its zero. `dry_run_allowed: false` still refuses (Build sets it: a stage that cuts
529
+ branches and fans out per-story sub-agents has no ONE dispatch to describe).
530
+ - **On an attended run it is still refused at exit `4`** — but for the right reason now. It costs
531
+ nothing; it describes a dispatch the framework never makes there, and `--prepare` writes the
532
+ bundle the host is going to carry. The message said "it spawns a real sub-agent" and no longer
533
+ lies.
534
+
535
+ - **A precondition gets its own clock, not the stage's 900–1800 s (#20).** `preconditions:`
536
+ inherited `timeout_s`, so one hung command — `docker info` against a dead daemon is the measured
537
+ case — could hold a run for half an hour: exactly the waste the feature exists to prevent, taken
538
+ by the guard instead of by the attempt. Each precondition now gets **60 s** by default
539
+ (`PRECONDITION_TIMEOUT_S`), overridable per entry with `timeout_s: <n>`, refused at load if that
540
+ is not a number `> 0`. A timeout is a red precondition like any other — exit `2`, nothing
541
+ written, nothing spawned, the stage where it was — and its message names the precondition, its
542
+ own timeout and the knob that changes it, rather than the stage's. `CommandRun` gained
543
+ `timedOut` so a timeout can be told from a refusal or a wrong exit code without reading prose.
544
+
545
+ - **The budget gate can see host-token spend and attendedness (#22).** The tolerant reader the
546
+ `budget-gate` hook and the status line share (`hooks/lib/runFile.ts`) skipped `tasks[]` and
547
+ `attended_by:` entirely. So a run whose turns a host session paid for reported `$0.00` metered
548
+ and nothing else, and `runSnapshot`'s tolerant path hard-coded `attendedByHost: false` with a
549
+ comment admitting it meant "cannot see". `RunView` now carries `attended_by` and each task's
550
+ `cost_usd` / `metered` / `tokens`; `runSpend` derives the metered dollars, the declared host
551
+ tokens and the uncosted turns; `renderRunEconomies` renders the one line that says a dollar
552
+ figure is a lower bound. The gate appends it to a `host-tokens` phase's stderr note and to a
553
+ refusal, and `budget.blocked` records `economy`, `attended_by`, `metered_usd`, `host_tokens` and
554
+ `unmetered_tasks`. **No verdict changed**: a dollar ceiling still governs dollars, the two
555
+ currencies are still never converted, and a plain metered run's refusal is byte-identical.
556
+
557
+ - **The `max_reads` flake was a real race, not a slow test (#24).** A chunk boundary is not a line
558
+ boundary: `LineSplitter` hands every complete line in one chunk to the read counter
559
+ synchronously, so when the OS coalesced the sub-agent's writes — which is what a loaded CI box
560
+ does — reads 4..20 were counted in the same tick as read 3, long before the `SIGKILL` just
561
+ ordered could land. `agent.result.payload.reads` was therefore a function of scheduling, and the
562
+ assertion pinning it to the cap cost two retries in one night. The counter now stops the moment
563
+ the cap fires, so what is recorded is the number of reads the cap ALLOWED. Pinned by a fixture
564
+ that makes the coalescing deterministic (`FAKE_CLAUDE_READS_BURST=1` — every read pair in one
565
+ write): pre-fix that reported 20 reads against a cap of 3.
566
+
567
+ - **One over-cap list item no longer cascades into false "S<id> has no file" errors (#37).**
568
+ `validatePlan` resolves cross-file references out of the set of stories that PARSED, so a story file that
569
+ failed its own validation was indistinguishable from one that was never written. Measured on the
570
+ `260829-scoring-leaderboard` session: `acceptance[3]` in `S8.md` was 1,009 characters against the 512 cap,
571
+ and the check reported three errors of which one was real — the other two said `S8 has no file in stories/`
572
+ about a file that was 5,794 bytes on disk. The operator only avoided a wasted pass by re-deriving the cause
573
+ from the validator source; an agent reading that message goes hunting for a missing file or rewrites
574
+ `waves.yml`.
575
+ - A reference to a story or epic whose FILE EXISTS is never reported as missing. It now reads
576
+ `S8 is unresolved because stories/S8.md failed validation — that file exists; fix the errors reported
577
+ against it and this one goes with them`, carries `cascade: true` on the `PlanIssue`, and covers the
578
+ id-mismatch case (`stories/S8.md declares id \`S9\``) as well as the invalid-file case. The epic side —
579
+ a story pointing at an epic whose own file did not validate — had the identical bug and the identical fix.
580
+ - `describePlanIssues` orders root violations ahead of cascades. Its window is three issues wide, so one
581
+ real defect cascading into four references could otherwise spend the whole window on consequences and
582
+ never name the cause.
583
+ - A story that is genuinely absent still reports `has no file`, with no cascade flag.
584
+
585
+ - **A refused list value now names the cap it broke, at the cap's current value (#38).**
586
+ The constants were interpolated already but the sentence was not self-describing: `513 characters exceeds
587
+ the 512 cap` did not say the cap is per-item or that splitting the item is the fix. Now
588
+ `513 characters exceeds the 512-character cap on one list item — split it into several items` and
589
+ `65 items exceeds the 64-item cap`, both still derived from `MAX_ITEM_CHARS` / the list's own `max`, and
590
+ the same constants are what the Plan prompt states up front.
591
+
592
+ - **The merge itself is now serialised, and a gate can no longer describe a tree it is not pushing
593
+ (#44).** `scripts/merge-wave.sh` merges, gates and pushes in ONE shared checkout and took no lock.
594
+ Measured on the pre-fix script with two concurrent invocations against a real sandbox repo: run A
595
+ gated `7afcc0e` (its own merge) at `typecheck` and `fadc923` (the OTHER run's merge, landed
596
+ mid-gate) at `build`, then printed `OK fadc923 … pushed`. Both runs reported the same sha and
597
+ both exited 0 — a green report over a tree neither had finished gating. With a red change in the
598
+ other branch the same interleaving hands agent A a `FAIL build=1` for code it never wrote, which
599
+ is what was actually observed live 2026-08-31 (`2184` tests counted where the branch had `2181`).
600
+ - **A lock, held from before the dirty-tree check through the push.** `mkdir` on
601
+ `.git/merge-wave.lock` — atomic on macOS and Linux, where `flock(1)` is not on stock macOS, and
602
+ in `git rev-parse --git-common-dir` so the lock can never be dirt in the tree it guards — and
603
+ not `$R/.git`, which in a linked worktree is a FILE that `mkdir` can never turn into a lock. A second invocation WAITS,
604
+ saying so on stderr (`merge-wave: waiting for another merge in this checkout (owner: …)`) so the
605
+ single summary line on stdout stays a single line. Waiting is bounded (`MW_LOCK_WAIT_S`, default
606
+ 3600 → exit `6`), and a lock whose owner is a dead pid on this host, or older than
607
+ `MW_LOCK_STALE_S`, is broken open — after re-reading the owner line, so two waiters cannot tear
608
+ down a lock a third has just taken. An interrupted run hands the lock back on its way out: an
609
+ untrapped signal kills bash WITHOUT running its `EXIT` trap, so `INT` and `TERM` are trapped too. No
610
+ path through the wait loop is free of the budget, including the break-open one: a lock that
611
+ cannot be created or removed now fails in under a second instead of spinning forever.
612
+ - **And an assertion that does not depend on the lock.** Between the last gate and the push, HEAD
613
+ must still be the commit the gates ran against; if it moved, nothing is pushed and the script
614
+ exits `5` saying which sha it gated and which one is there now. The lock prevents the race; this
615
+ makes pushing an ungated HEAD impossible even for someone who bypasses the lock. The pre-fix
616
+ script, given the same mid-gate commit, pushed it and reported `OK`.
617
+ - **It pushes the commit it gated, not the `main` ref.** `git push origin main` publishes
618
+ `refs/heads/main` whatever HEAD is — and a red gate leaves `main` sitting on an ungated merge
619
+ commit by design, so the next run from a detached or repaired HEAD would have published THAT.
620
+ The push is `HEAD:main` now, and a pre-flight refuses (exit `7`) when the gated commit is not a
621
+ fast-forward of `origin/main` rather than letting the server's rejection be the first news.
622
+ - Gate logs moved from the fixed `/tmp/mw-*.log` to a per-invocation `${TMPDIR}/mw-<pid>/`, and the
623
+ FAIL lines name the directory. Two runs in two clones on one box shared those files.
624
+
625
+ - **The test suite no longer goes red because the machine was busy (#43).** On an untouched
626
+ `origin/main`, `bun test` reported `2155 pass · 5 fail` while the same two files alone reported
627
+ `91 pass · 0 fail`: four tests that spawn a REAL `git` expiring on bun's 5000 ms default, and one
628
+ 50 ms performance budget measured at 66.4 ms, with three `tldrx` runs and two other agents sharing
629
+ the box. Because `merge-wave.sh` refuses to push on any test failure, that red is indistinguishable
630
+ from a regression at the exact moment a merge is decided, and the natural response — re-run until
631
+ green — is how a real regression eventually gets pushed.
632
+ - **The clock moved; no assertion did.** `test/fixtures/machineLoad.ts` measures the machine
633
+ (1-minute run-queue per core, floored at 1 and capped at 8) and hands out budgets from it. All
634
+ **42** test files that spawn a real process — `git`, `bun`, the CLI — now open with
635
+ `setDefaultTimeout(spawnTestTimeout())`: 30 s idle, scaled by load, still a hang detector. How
636
+ long a process takes to start is a property of the machine, not of the code, so a fixed budget on
637
+ such a test measures the box. A test enumerates those files and fails if a new one skips the
638
+ budget. Serialising the suite would not have helped: `bun test` already runs files sequentially
639
+ in one process (verified — a `setDefaultTimeout` in one file does not reach the next, and a 5.5 s
640
+ test in that next file still expired at 5000 ms). The contention is other processes on the box,
641
+ which only a load-aware budget can see.
642
+ - **The first version of that list was a `grep -l`, and it lied.** It returned 14 files and silently
643
+ omitted `cli.test.ts`, whose "every command's help lists an exit table" then timed out at 5004 ms
644
+ on the merge that was fixing timeouts. Cause: one stray NUL byte at `test/cli.test.ts:366` makes
645
+ the file `data` to `file(1)`, and grep drops binary files under `-I` without a word. The list is
646
+ built by READING every file now, and `cli.test.ts` is asserted to be in it. Filed as #47.
647
+ - **The one real performance budget keeps its teeth.** `handoff` on 256 KB is now the floor of
648
+ three runs against `perfBudgetMs(50)`, which on an idle machine is 50 — the identical assertion.
649
+ A stall inflates some runs and never the floor, and a function that genuinely takes 120 ms still
650
+ fails, which is itself a test.
651
+
652
+ - **`npm pack` output no longer refuses the next agent's merge (#45).** `tldr-experts-<version>.tgz`
653
+ was not ignored, and the dirty-tree guard refuses on ANY porcelain line, untracked included — so a
654
+ pack artifact left by a release check blocked the merge of whoever came next, someone who did not
655
+ create the file and could not know whether deleting it was safe. `*.tgz` is ignored now. The guard
656
+ is deliberately unchanged: an untracked file is still dirt, and a test holds it to that.
657
+
658
+ - **`claim-sources` reports every problem it found, over every declared `.md` output — and a
659
+ `file` src resolves against this run's epic worktree.** Four issues, one code path (#33, #34,
660
+ #23, #16), all four measured on the 2026-08-30/31 unattended pilot runs.
661
+ - **It reported ONE problem** (#33). `checkClaimSources` returned on the first file, the first
662
+ category and `unresolved[0]`, so a 226-bullet cap breach sat invisible behind a single bad
663
+ file path: fixing the visible one and re-running would have bought the next one at the price
664
+ of a full paid pass. Every file and every category is reported now, as a per-file summary
665
+ (`<file>: 3 unsourced bullet(s) on line(s) …; 2 unresolvable source(s) — …`), with file-level
666
+ problems such as the cap breach listed FIRST so 200 line numbers cannot bury them. Up to six
667
+ of a category are named and the rest become `(+N more)` — the same convention
668
+ `describeKnowledgeIssues` uses, and necessary because a check's `detail` is rendered inside
669
+ one-line summaries (`autoGate`, `next`).
670
+ - **It looked at ONE file** (#34). The filter was `endsWith("handoff.md")`, so the identical
671
+ violation refused the stage when it was written in `handoff.md` and passed in silence when it
672
+ was written in `design.md`, `contracts.md` or `scope.md` beside it — the pilot's pass-3
673
+ violation was caught only because it happened to be in the handoff. Every declared `.md`
674
+ output is read now, by both the gate check and the write-time hook: the four-section rule for
675
+ the files that ARE handoffs, and `validateCitations` for the ones that are not. That second
676
+ rule is deliberately narrower — a bullet with no citation is prose, but a `[src: …]` that WAS
677
+ written must parse, must resolve, and must obey `$ … → exit n` belonging only to an
678
+ `Evidence ledger`. A declared non-handoff output that was never written is still not a
679
+ failure; that is the `--commit` gap check's job.
680
+ - **The execution-claim validator reads the verb** (#23). `\bexit \d` missed "exits 0", which
681
+ is how a trainer writing normal English says it, so the claim slipped through the grammar the
682
+ rule exists to enforce while "exit 0" three words away was refused. Conjugation, an optional
683
+ "with", and the `code`/`status` spellings all match now; the digit is still required, so "the
684
+ exit path is documented" and "the exchange refuses an empty code" stay prose.
685
+ - **A `file` src resolves against this run's epic worktree** (#16). The Build phase commits
686
+ onto an epic branch and deliberately does not merge it, so a Watch-stage handoff ABOUT that
687
+ work had every `repo:src/…` citation refused for naming code the working tree does not have
688
+ yet — the stage's own evidence was rejected for being true. `.tldrx/worktrees/<repo>/_epic-<run>-<epic>`
689
+ is now a resolution base, tried before the working tree, for both the hook and the gate; the
690
+ path convention has one home (`core/paths.ts`) that the Build executor writes and the §2.8
691
+ resolver reads. Resolution also no longer stops at the first base where the file EXISTS but
692
+ is too short — a file truncated on the epic branch would otherwise deny a claim about the
693
+ line it still has on `main`. **Still open** (commented on #16): the epic worktree is removed
694
+ at the end of Build unless `--keep-worktrees`, so the default Watch stage has no tree on disk
695
+ to resolve against. Closing that means reading blobs out of the epic branch inside a hook
696
+ whose budget is 50 ms, or keeping epic worktrees for the life of the run — a design call, not
697
+ a mechanical one.
698
+
699
+ - **The Build DoD is a DELTA gate again: the base tree is checked before any story is charged (#41).**
700
+ A dod block proves one thing — *this story did not break the tree* — and nothing checked that the tree was
701
+ unbroken to begin with. Measured live 2026-08-31 on `260829-scoring-leaderboard`: of the three commands
702
+ `workspace.yml` declared, **two already failed on pristine main** — a bare `dotnet test` ran two `Live`-trait
703
+ tests that call paid Azure AI and that the repo's own CI excludes, and `dotnet format --verify-no-changes`
704
+ flagged 336 files in a repo whose CI never gates format at all. All 15 stories in the plan would have blocked
705
+ identically, each having spent a developer turn, and each told the operator the STORY was red.
706
+ - **Pre-flight at Build entry.** After the dirty-tree and foreign-epic refusals and before anything is
707
+ dispatched, every dod command the pending stories name is run once against the untouched base tree. A
708
+ non-zero exit refuses the stage (exit `2`, back to `ready`) naming the command, its exit code and the repo,
709
+ with no attempt spent and nothing charged.
710
+ - **In the repo's own checkout, not a fresh worktree.** That is the tree a human means by "the base": it has
711
+ the installed dependencies and tool state that make the command mean what the team thinks it means. A
712
+ pristine worktree would fail half the world's repos for want of `node_modules` and turn a safety net into
713
+ an outage.
714
+ - **Paid for once.** Results go to `04-build/preflight.yml` — files are the state — keyed by repo, command and
715
+ the base sha, and are read back by every later invocation of the run. A missing or unreadable cache is a
716
+ question, never a fault: a run that entered Build on an older binary measures lazily rather than erroring.
717
+ - **Attribution.** When a story's DoD does go red, the cached base result decides whose fault it is. A command
718
+ red on the base too halts the build with the same workspace-config error rather than blocking the story and
719
+ consuming its attempt. A command the gate declined to run is recorded `unmeasured` and excuses nothing.
720
+
721
+ - **The review handshake no longer swallows an unrecognized verdict, nor drops structured
722
+ findings.** Measured on `260831-hardening-d1` / S1 (2026-08-31). Two verdict grammars coexist
723
+ — gate evidence is `sign | sign-with-fixlist | refuse`, a story review is
724
+ `approve | fixlist | changes` — and the host-facing hint named neither, saying only "write
725
+ {verdict, summary, findings}". The host wrote `sign`. `parseReview` fail-closed it to `changes`,
726
+ correctly and **silently**: a clean fix-list verification round read as a second `changes`, the
727
+ story went `blocked`, and a `story reopen` cycle was the only way to record the verdict that
728
+ had been meant all along. Separately, `findings` was filtered with `typeof f === "string"`, so
729
+ the attempt-1 adversarial reviewer's seven `{severity, file, line, claim, evidence, fix}`
730
+ objects were dropped whole — the verdict survived, the evidence it rested on did not.
731
+ - **The contract is now stated where the host reads it.** Both `--commit --review` hints name
732
+ the enum: `verdict is one of approve | fixlist | changes, NOT the `sign`/`refuse` gate
733
+ vocabulary`.
734
+ - **The downgrade is announced.** Fail-closed is unchanged — an unreadable verdict is still
735
+ `changes`, never `approve` — but a verdict outside the enum now comes back on
736
+ `Review.verdictProblem` ("the reviewer's verdict `sign` is not approve|fixlist|changes —
737
+ recorded as `changes`"), is printed by the executor on the one path both doors pass through,
738
+ and rides in `findings` so the review log and the next attempt's `## Previous attempt` both
739
+ carry it. A DECLARED `fixlist` that fell short is untouched: `fixlistProblems` already says
740
+ that one out loud, and two sentences for one downgrade would read as two faults.
741
+ - **Structured findings are rendered, never dropped.** An object becomes
742
+ `[severity] file:line — claim · evidence: … · fix: …`; a shape nothing recognizes is kept as
743
+ JSON; a `findings` that is not an array is kept as one finding. An unreadable finding in the
744
+ log beats a finding that is not in the log.
745
+
746
+ - **A project stage override that supplies only `stage.yml` no longer swaps the stage body for an
747
+ empty one.** Reported from the 260829-scoring-leaderboard driver session (2026-08-31) and
748
+ reproduced here: `stage.md` was resolved by string-substituting `stage.yml` in the path the
749
+ preset had already picked, so creating `.tldrx/stages/plan/stage.yml` to tune one key moved the
750
+ BODY lookup into a directory that had none — and the miss was read as an empty string. The
751
+ context ledger printed `stage 1 B` where it had been 4.9 KB; the sub-agent would have been
752
+ dispatched with the inputs, the experts and the rejection note and **zero** stage instructions,
753
+ and nothing refused. `stageMdPath` now resolves per FILE, not per directory: the override's own
754
+ `stage.md` wins, else the packaged one is inherited, and a stage with no body anywhere is a
755
+ named `StageBodyError` rather than a silent empty prompt. Both readers — `next --prepare` and
756
+ the Watch executor — go through it.
757
+
758
+ - **`approve --as-agent`'s refusal now names the route that works on the run in front of you.**
759
+ It pointed only at `--gates <stage>:agent`, which is chosen at `run new` and frozen there — so
760
+ the one suggestion meant recreating a run already in flight. It now leads with the delegated
761
+ approve: read the agent's evidence note yourself and sign as you, `tldrx approve --note
762
+ "delegated: <agent> reviewed this, evidence at <path>"`, which keeps the gate's policy and puts
763
+ the provenance on the record. Found across the 2026-08-30/31 unattended pilots.
764
+
765
+ - **`budget raise <phase> <usd>` help said `<usd>` was "the new ceiling"; the source adds it.**
766
+ `raiseBudget` computes `ceiling_usd + amount` (`raiseBudget.ts:83`), so an operator following
767
+ the help over-raised — measured live on the scavtopia leaderboard run, a "$5.40 new ceiling"
768
+ command would have set $8.00. The arithmetic is what live runs depend on and is untouched; the
769
+ words move. `<usd>` is now "how much to ADD to that phase's ceiling — a delta, not a new
770
+ ceiling", with a note spelling out the $10 + $25 = $35 case and pointing at `budget show`, which
771
+ already prints the correctly-sized command.
772
+
773
+ - **`tldrx run new --from` stores an imported answer's own words, not a letter pointing at a file
774
+ it does not own.** AI-DLC records a chosen option as `[Answer]: C`, and the import stored
775
+ "<question> — C" verbatim; two facts became unreadable once aidlc was uninstalled and the source
776
+ file went with it (2026-08-30/31 pilots). The interview flow has always resolved a letter to the
777
+ option's text before recording (`interview/reply.ts:32-37`), and the import now does the same:
778
+ `parseAidlcQuestions` reads the lettered options (uppercase, and a space required after the
779
+ punctuation, so `- E.g. …` stays prose) and `answerText` resolves the answer against them; a
780
+ letter with no option behind it is stored as typed rather than invented. Conflict detection is
781
+ unchanged by the longer text: it keys on the QUESTION, as `hooks/no-reask.ts:54` already does,
782
+ because `findDuplicate` is Jaccard over tokens and therefore length-sensitive — the same
783
+ contradiction scored 0.78 against a bare letter and ~0.22 against the answer written out.
784
+
785
+ - **A second run's stories no longer merge into ANOTHER run's epic branch.** Measured live
786
+ 2026-08-31 on two concurrent runs: `260831-hardening-d1` reported S1, S2 and S6 all
787
+ "merged into `epic/hardening-d1` (N commits carried)", and `epic/hardening-d1` was still
788
+ sitting at its base with **zero** story commits — all three merges had landed on
789
+ `epic/d1-tenancy-identity-customers`, a CLOSED previous run's branch. Nothing failed; the
790
+ run closed green with an empty epic, and it surfaced only because a later story measured
791
+ `git merge-base` and found the dependency it had been promised was missing.
792
+ - **The cause was one missing run id.** `openEpicWorktree` built the epic worktree's disk
793
+ path as `_epic-<epic id>`, and every plan names its first epic `E1`. The second run's
794
+ `existsSync` therefore hit the FIRST run's directory, `addWorktree` was skipped, and
795
+ `git merge --no-ff` ran inside a checkout of a foreign epic branch. The in-memory map was
796
+ keyed correctly (`repo:epicBranch`) — only the path collided, and only across processes.
797
+ Every progress line renders `story.epicBranch`, so the messages were right about where the
798
+ merge was *meant* to go for as long as the bytes went somewhere else.
799
+ - **The path now carries the run**: `_epic-<run id>-<epic id>`, the same shape the STORY
800
+ worktree was given after the 2026-08-29 audit found the identical class of bug one level
801
+ down. That fix never reached the epic worktree, which is the worse half — a story worktree
802
+ collision means two sub-agents editing one file, an epic worktree collision is a merge.
803
+ - **And a reuse on the wrong branch now refuses.** Every reuse of an epic worktree — the
804
+ remembered path and the one found on disk — asserts its checked-out branch is the story's
805
+ epic branch first (`assertWorktreeOn`, `core/build/git.ts`). A mismatch throws
806
+ `WorktreeBranchMismatchError` naming both branches and the directory, and fails the stage.
807
+ It never re-points the worktree and never merges anyway. Path scoping makes the collision
808
+ impossible; this makes it impossible to repeat *silently*.
809
+
810
+ - **A `--note` with a blank line in it no longer destroys `run.yml`.** Measured 2026-08-31 on the
811
+ live `260829-scoring-leaderboard` run: `tldrx reject --note "<two paragraphs>"` wrote the note
812
+ into the gate's flow mapping with LITERAL newlines inside a double-quoted scalar, which is not
813
+ YAML — the `yaml` package answered `Missing closing " quote at line 57` and Bun's parser
814
+ `Unexpected character` — and from that moment **every** command on the run failed. There was no
815
+ repair verb, so the operator had to hand-edit a file the docs forbid editing, and the next save
816
+ re-emitted the same string and broke it again at the same line, taking `run.yml.bak` with it.
817
+ Four changes, each closing one part of the loop:
818
+ - **The emitter escapes, at the one place every field goes through.** `yamlScalar`
819
+ (`core/facts/emitFactsYaml.ts`) escaped `\` and `"` and nothing else; it now emits via
820
+ `JSON.stringify`, whose string grammar is a strict subset of YAML 1.2's double-quoted scalar —
821
+ the escaping this repo already trusted in `adapters/external.ts` and `build/storyFile.ts`.
822
+ Because every YAML this framework hand-emits routes strings through that one helper, the fix
823
+ reaches **all** of them at once: gate notes (`approve`, `reject`, `revoke`), `cancelled.note`,
824
+ task `error` and `stopped_by`, gate `evidence`, run `title`/`scope`, `facts.yml` fact text and
825
+ retirement reasons, and `split.yml` goals, claims, questions and answers. Verified against
826
+ **both** parsers behind the runtime seam. Existing files do not churn: over every code point
827
+ from U+0020 to U+FFFF the new escaping and the old produce identical bytes (63,456 checked, 0
828
+ differ), so only the values that were already corrupt change shape.
829
+ - **A file already broken this way heals itself on load.** `parseYamlRepairing` (`core/yaml.ts`)
830
+ re-escapes raw control characters trapped inside a double-quoted scalar, re-parses, and accepts
831
+ the result only if it parses — otherwise the parser's ORIGINAL error is thrown, because a
832
+ repair that cannot be verified is not offered. `RunStore.open` then rewrites the mended file
833
+ through the fixed emitter and says so on stderr. A one-time hand repair was never enough: the
834
+ old emitter re-corrupted the file on the next save, so `emit(load(x))` had to be made stable.
835
+ - **Every state write keeps one step back.** `RunStore` and `FactsStore` had grown a
836
+ byte-identical private copy of temp-plus-rename each; both now call one
837
+ `core/fs/writeAtomic.ts`, which additionally copies the version it is about to replace to
838
+ `<file>.bak`. Atomicity only ever guaranteed a WHOLE file, never a good one. The copy is taken
839
+ before the rename, so the live file is never absent for an instant and a torn backup can only
840
+ ever cost a backup. `tldrx init` now adds `tldrx-work/*/*.bak` and `.tldrx/memory/*.bak` to the
841
+ managed `.gitignore` block.
842
+ - **A `run.yml` beyond mechanical repair fails honestly, and takes nothing else down with it.**
843
+ The error names the file, quotes the parser verbatim, says that every command on the run reads
844
+ that file first, and points at `run.yml.bak` — while stating plainly that using it is a MANUAL
845
+ decision tldrx will not make. Separately, one corrupt `run.yml` used to throw a raw
846
+ `YAMLParseError` out of `buildModel` and kill `tldrx dashboard` for the whole workspace;
847
+ `loadRunResult` (`core/replay/loadRun.ts`) now distinguishes missing from unreadable, and the
848
+ dashboard lists the run as **unreadable** with the parse error beside it and renders every
849
+ other run as normal.
850
+
851
+ ### Changed
852
+
853
+ - **The docs now say, at the top of both places a reader starts, that `run attend host` is a LOCK
854
+ and `run auto` is an ENGINE.** Grounding: on 2026-08-31 the framework's own author — who had read
855
+ the chapter — ran `tldrx run attend host <run>` expecting it to drive the whole run by itself,
856
+ and then asked whether `attend` and `auto` compose. They do not, and the code has always said so
857
+ (`runAuto.ts:108` refuses `run auto` on an attended run at exit `1`, before the event log is
858
+ opened; `runNext.ts:659` exits `4` on a bare `next` and names the `--prepare` command). The docs
859
+ took too long to say it.
860
+ - **README gains "Trying it: three ways to run"**, immediately after Quick start: a three-row
861
+ table of who executes each turn, what a turn costs, and where each mode stops; one scenario
862
+ line each (`run auto` for a small run you would watch anyway and for CI/cron — the only mode
863
+ with no session behind it; `attend host` when a session is already open and cost or quality
864
+ matters; `attend host` + a mandate for overnight); and the two-command recipe with a verbatim
865
+ example **mandate prompt**.
866
+ - **`docs/guide/10-unattended-mode.md` leads with the same disambiguation** — a blockquoted
867
+ lock-vs-engine table above the chapter's opening paragraph, so a skimmer cannot make that
868
+ mistake — and gains a `### The mandate` section carrying the prompt verbatim, tying its four
869
+ legitimate interrupts back to the `questions` / `budget-event` / `boundary` fallthroughs the
870
+ framework already enforces, and to the fact that no `git push` wrapper exists in the Build
871
+ executor (`src/core/build/git.ts:13`) and the developer prompt says "Do not push"
872
+ (`src/core/build/prompts.ts:180`).
873
+ - **`tldrx run --help` says it too.** `run` had notes for `status`, `estimate`, `unlock` and
874
+ `cancel` and none for the pair that actually confuses people. It now leads with one note per
875
+ mode — "a LOCK, not an engine" / "an ENGINE, not a lock", each naming the other's refusal — and
876
+ the `<host|--none>` argument line says the framework will not spawn on the run again. Help text
877
+ only; no behaviour, no flag and no exit code moved.
878
+
879
+ - **`tldrx run estimate` is remaining-work aware (#21).** It priced the next stage from token medians while
880
+ the budget brake separately computed what that stage still had to pay for — two models, one question, and
881
+ the one people read was the one that never shrank. A Build stage with five of six stories done was still
882
+ quoted the number the Plan wrote before any of them ran, which is the figure that made a pilot operator
883
+ move money twice for work the run could already afford.
884
+ - It now calls **the same `remainingWork()`** the brake and `budget show`'s `est.` column call, with the
885
+ same inputs, and reports it beside the token estimate: done stories excluded, blocked ones named, the
886
+ arithmetic shown. A test asserts the two numbers are identical rather than merely similar.
887
+ - It also rolls the run up: `still to run: N stage(s) … $X priced`, with terminal stages excluded and the
888
+ cursor stage narrowed by the plan when the plan knew better. `--json` carries both as `remaining` and
889
+ `runRemaining`.
890
+ - The token half is untouched. The input side is still measured off the same assembly `next` builds, and
891
+ the cache/output medians still say which sample they came from — that half was never the complaint.
892
+
893
+ ## 0.3.1 — 2026-08-31
894
+
895
+ **Unattended mode.** Twelve of the entries below are one feature: a run a **host session**
896
+ drives end to end, and a gate an **agent** may close over a check it wrote down. `attended_by: host`
897
+ stops the framework spawning on a run at all; `economy: host-tokens` stops a ceiling that is not
898
+ dollars from buying a metered spawn; the dispatch-notes slot gives the host the one place to add
899
+ what the bundle cannot know; `next --prepare/--commit --review` makes the Build reviewer the
900
+ second delegable role, so one review is done once; the `fixlist` verdict gives a review that
901
+ SIGNS somewhere to put its findings, for one bounded round that costs no attempt;
902
+ `gates_policy: agent` closes a gate on the seven auto conditions **plus** a boundary check, a
903
+ budget-event check and a validated evidence note, and falls through to a person on a question, a
904
+ moved ceiling, work nobody scoped, or its own refusal — rendered as a decision card rather than
905
+ a dashboard. Three smaller pieces stop a turn being wasted before it starts: `preconditions:`
906
+ on a stage, a story branch fast-forwarded onto its epic before dispatch, and a budget brake that
907
+ counts the work that is LEFT rather than the price the stage was written at. Two measurements
908
+ from 2026-08-30 are the whole argument: **$9.95** of spawns that died on caps a Plan agent had
909
+ priced in host tokens, and a framework reviewer that spawned beside a host already reading the
910
+ same diff. New chapter: `docs/guide/10-unattended-mode.md`. Every part is additive — a run with
911
+ none of these behaves byte-identically to the release before them.
912
+
913
+ ### Added
914
+
915
+ - **A story branch that has fallen behind its epic is fast-forwarded before a developer is
916
+ dispatched onto it.** Measured 2026-08-30 on `260830-tenancy-identity-customers`: S3 was
917
+ reopened, `story reopen` keeps its branch by design, and that branch still sat at the S1-era
918
+ epic tip while the epic had since gained S2 and S5. S3's handlers needed S2's contract, so a
919
+ dispatch on that base would not have compiled. The host fast-forwarded by hand before
920
+ dispatching. That is the one case this automates.
921
+
922
+ ```
923
+ · S3: fast-forwarded `story/260830-tenancy/S3` to `epic/tenancy` — 2 commit(s), b5a2474 → ae9c8dd
924
+ ```
925
+ - **Where.** Inside `openStory`, which is the one place a story worktree is opened, and only
926
+ on the two openings that are about to put a DEVELOPER on the branch: the headless pipeline
927
+ and `tldrx next --prepare`. The review openings (`--prepare --review`, `--commit --review`,
928
+ an errored review re-run) and `--commit` measure nothing and move nothing — a fast-forward
929
+ there would drag other stories' commits onto a branch whose whole meaning is "what this
930
+ story built", for a base nobody is about to compile against.
931
+ - **The requeue case, which fires far more often than the reopen one.** A `changes` verdict
932
+ merges the story into its epic and then hands it a second attempt; before this, attempt 2
933
+ was dispatched onto attempt 1's base. It now starts on the current epic tip.
934
+ - **A diverged branch is warned about, never resolved.** Commits on both sides is the second
935
+ live case — a dead spawn's partial commit on a stale base, where no fast-forward existed
936
+ and the host preserved the partial on a backup branch and re-pointed the story branch by
937
+ hand. Which of two histories survives is a decision, so the framework does not make it: it
938
+ names both counts, both shas, and the two options, changes nothing, and lets the dispatch
939
+ proceed on the old base — saying, in as many words, which base that is.
940
+ - **A dirty worktree is left alone**, whatever the topology says. It is the operator's.
941
+ - **Never a rebase.** Rewriting a branch a developer has already committed to is the class of
942
+ move the run-id-in-branch-name fix (2026-08-29 audit §B) exists to prevent. The only write
943
+ is `git merge --ff-only`, which refuses rather than inventing a merge commit. Measured
944
+ 2026-08-31 against a real repository, which is what the design asked to verify before
945
+ building: blocked by a file in the way it exits non-zero and leaves HEAD and the file
946
+ exactly as they were — atomic-or-nothing, so a failed fast-forward needs no repair, only a
947
+ line saying it did not happen.
948
+ - **`story.base_fastforwarded`** joins the closed §2.9 event set — the only event in it that
949
+ records tldrx moving a ref. It carries `story`, `repo`, `branch`, `base`, `from`, `to` and
950
+ `commits`, `tldrx replay` narrates it, and it is appended ONLY when the ref actually moved:
951
+ a divergent or dirty branch produces a warning and no event, because nothing happened.
952
+ - **An up-to-date branch is silent** and emits nothing, so a run with nothing to say about
953
+ its bases is what it was before.
954
+ - `tldrx story reopen` is unchanged: it still runs no git command, spends nothing and touches
955
+ no branch. The detection belongs where a worktree is being opened anyway and where the
956
+ operator is about to dispatch.
957
+
958
+ - **Decision cards — the shape an interrupt takes when a run stops for a person.** Measured
959
+ 2026-08-30: an unattended run stopped on two owner questions, and the host did NOT show the
960
+ owner the dashboard or the `2 open question(s) in 01-what/questions.md` line the framework
961
+ actually prints. It hand-composed, in chat, the question, the options and a recommendation
962
+ with one line of why. The owner answered both in seconds. The card is what made the
963
+ interrupt cheap; hand-composing it is what the framework was making the host pay for.
964
+
965
+ ```
966
+ DECISION — 260830-tenancy · 01-what/what
967
+ Q2 · Should an existing customer's tenant be inferred or asked for?
968
+ Why asked: no tenant column on the customer aggregate [src: absent:api:src/.../Customer.cs]
969
+ A) infer from the invoice email domain — no new UI, wrong for resellers
970
+ B) ask once at first login — one screen, correct for everyone
971
+ C) other — write it below
972
+ Recommends B — one screen, correct for everyone [src: 01-what/handoff.md:22]
973
+ tldrx answer Q2 "…" --run 260830-tenancy
974
+ ```
975
+ - **Pure rendering of things that already exist.** The question, its `Why asked:` line and
976
+ its lettered options come out of `questions.md` through the **§2.7 parser** — the
977
+ questions grammar is not touched, and a block the parser cannot read does not appear on a
978
+ card any more than it appears anywhere else. The `Recommends` line comes out of the
979
+ evidence note's optional `recommend: [{q, option, why, src}]` array, which the evidence
980
+ grammar already validates.
981
+ - **A question with no recommendation gets no line.** Never a manufactured one and never a
982
+ placeholder: the whole value of that line is that an agent stood behind it with a
983
+ citation.
984
+ - **One renderer, three surfaces.** `tldrx run auto --gate-agent` at the stop;
985
+ `tldrx next`'s agent-gate fallthrough, where the card is **appended** to the fallthrough
986
+ list so nothing that reads those lines today loses a byte; and `tldrx status`, where a run
987
+ waiting on answers now shows the card rather than `open questions: Q1, Q2`.
988
+ - **A card per fallthrough kind.** Budget and boundary get their own card over the same
989
+ frame — the measured fact, then the commands (`widen the scope …` / `approve` / `reject`
990
+ for a boundary; the phase's two numbers plus `budget show` for a budget event) — and every
991
+ other reason an agent gate fell through is carried as one gate card naming its reasons.
992
+ - **`--gate-agent` is rendering only.** It does not upgrade any stage to
993
+ `gates_policy: agent`: a run keeps the policy it was opened with, and a flag that could
994
+ raise one at stop time would make the frozen policy decorative. On an
995
+ `attended_by: host` run it changes nothing — `run auto` is still refused at exit `1`
996
+ before the event log is opened, and nothing spawns.
997
+ - **Nothing else moves.** `tldrx answer`, `questions.md`, the live dashboard and every exit
998
+ code are unchanged, and `run auto` without the flag prints exactly the block it always did.
999
+ - **The budget brake counts the work that is LEFT, not the price the stage was written at.**
1000
+ Measured 2026-08-31 on `260830-tenancy-identity-customers`: four of seven stories done, one
1001
+ mid-attempt-2, two blocked, and the entire remaining metered cost a developer share and a
1002
+ reviewer floor — **$2.50**. The brake compared the phase's remaining dollars against
1003
+ `stage.budget_usd`, **$18.00**, a number written before a single story ran and never
1004
+ revised. It refused the stage twice and the host ran `budget raise --take-from` twice, for
1005
+ money nothing was going to spend.
1006
+
1007
+ For a Build stage with a plan on disk, `tldrx next`'s refusal, the `budget-gate` hook and
1008
+ `tldrx budget show`'s `est.` column now all use one figure computed by one function: `Σ`
1009
+ over the unsettled stories of the caps the executor would actually hand out — the
1010
+ `03-plan/budget.yml` price through the same scale/share arithmetic, the developer and
1011
+ reviewer shares, the `$1.00` reviewer floor, and the attempts each story has left.
1012
+ - **The refusal shows its arithmetic**: `remaining work: S4 dev $1.50 + reviewer $1.00 =
1013
+ $2.50`, under a line naming how many stories are done and what the stage's static
1014
+ estimate was. A number an operator cannot take apart is one they cannot argue with, and
1015
+ `$18.00` cited nothing.
1016
+ - **`blocked` costs $0.00**, and the blocked ids are named rather than quietly dropped: the
1017
+ executor dispatches a blocked story only after `tldrx story reopen`, which is a human
1018
+ decision and which legitimately raises the figure again.
1019
+ - **A story at `review` has already paid the developer turn under review.** Only a `changes`
1020
+ verdict buys another one.
1021
+ - **Under `economy: host-tokens` the developer turns are $0.00** — the host session pays for
1022
+ them — while the reviewer floors stay, because outside attended mode `reviewAndSettle`
1023
+ still spawns a metered reviewer and that floor is real money.
1024
+ - **It can only NARROW.** The figure is capped at `stage.budget_usd`, so this brake can
1025
+ never refuse more often than it did before; the reviewer floor can otherwise lift a naive
1026
+ sum past the ceiling. Asserted in both directions, across a spread of plan shapes, because
1027
+ a brake that loosened by accident is the failure to fear here.
1028
+ - **`budget.blocked` gains `estimate_basis: plan|static`** and, on the plan basis,
1029
+ `static_estimate_usd`, `stories_done` and `stories_total`.
1030
+ - **With no plan on disk, and outside Build, every path is byte-identical**, wording
1031
+ included: the estimate is `budget_usd` and the message still reads `the stage estimate
1032
+ is $X`.
1033
+
1034
+ - **`preconditions:` on a stage — the check that runs before the money does.** A stage may
1035
+ declare operational facts that must hold before it is worth dispatching at all:
1036
+
1037
+ ```yaml
1038
+ preconditions:
1039
+ - {id: docker, repo: api, command: "docker compose ps", expect_exit: 0}
1040
+ ```
1041
+
1042
+ The grounding is measured, 2026-08-30: before dispatching a Build story the host checked
1043
+ the Docker daemon and the .NET SDK **by hand**, because a story has two attempts, an agent
1044
+ cannot debug its way out of a daemon that is down, and the whole turn would have been spent
1045
+ proving it. That check took about a second and protected an attempt worth dollars.
1046
+ - **Same allowlist rule as a `cmd` check and a story's `` ```dod `` block — and now literally the
1047
+ same function.** Only a command byte-equal to one `.tldrx/workspace.yml` declares runs,
1048
+ argv-split, never through a shell. The comparison and both refusal sentences moved to
1049
+ `schemas/commandAllowlist.ts`, so the three sites can no longer drift into three readings
1050
+ of one rule. It is enforced **at load**: a stage naming an undeclared command never
1051
+ becomes a runnable stage, so `tldrx run new` over it refuses too.
1052
+ - **Red ⇒ refused, exit `2`, having spent nothing.** The id and the command's own exit code
1053
+ are named, the stage is left exactly where it was (`ready`), no bundle is written and
1054
+ nothing is spawned. The list stops at the first red one.
1055
+ - **`--prepare` runs them no less than headless** — a bundle written for a host whose Docker
1056
+ is down is the same wasted attempt as a spawn into one. `--commit` never runs them: it
1057
+ settles a turn that already happened.
1058
+ - **Every run is on the record**: one `check.passed` / `check.failed` event with
1059
+ `kind: precondition`, carrying the repo, the command, the exit code and the duration, and
1060
+ one operator line — `· precondition: docker compose ps → exit 0 (1.2s)`.
1061
+ - **A stage that declares none is byte-identical**: no event, no line, no shipped stage file
1062
+ changed. `[assumption]` — per stage, not per story; a per-story precondition is a real
1063
+ want and is deliberately not designed here.
1064
+ - **The `fixlist` verdict, its artifact and its router — the review that SIGNS and still has
1065
+ findings.** Measured 2026-08-31, driving `260830-tenancy-identity-customers` by hand: the
1066
+ reviewer signed story S5 — every acceptance criterion met, zero scope violations — and in the
1067
+ same breath named three real correctness/security defects the criteria never covered (a
1068
+ concurrent double-confirm minting two sessions, a non-atomic confirm, a false security comment
1069
+ beside a non-constant-time compare). S1 and S3 went the same way that night. Binary
1070
+ `approve`/`changes` has nowhere to put those: `approve` throws them away, `changes` spends the
1071
+ story's one requeue on a diff nobody faulted. So all three loops were run in chat — number the
1072
+ findings, decide fix-now vs defer-with-log, route them to the author, re-verify — and none of
1073
+ it reached a file. This is that loop, as a verdict and an artifact.
1074
+ - **`fixlist` settles the story at `review` and spends NO attempt.** The requeue counter counts
1075
+ verdicts that FAULTED the diff, and a signature is not one. `04-build/fixlist/<story>-<n>.md`
1076
+ is written beside it by the EXECUTOR, never by the reviewer — which holds no write tool, the
1077
+ same reason the review log is written there. Numbered `## <n> · <finding> [<severity>]`
1078
+ sections, each with `Where:`, `Disposition:` and `Resolved:`.
1079
+ - **A disposition ROUTES a finding; `Resolved:` CLOSES it.** Two questions, two fields, because
1080
+ one field cannot answer the first once the second is true. `fix-now` · `defer-with-log` ·
1081
+ `refuted` · `out-of-scope`, and **`refuted` must carry an `[src: …]`** in its `where` or
1082
+ `detail`, through the §2.8 grammar and the §2.8 parser — a reviewer's verdict is a claim like
1083
+ every other, and that night's host disproved one by grepping both sides before acting on it.
1084
+ A fix list with an uncited `refuted` is refused whole and the verdict falls to `changes`.
1085
+ - **The router: `tldrx next --prepare --fixlist <path>`.** The open findings land under
1086
+ `## Fix list` in the DEVELOPER's prompt, numbered, with their `Do NOT` lines verbatim — a
1087
+ bound the reviewer put on a fix is worth as much as the fix. `pending.json` gains
1088
+ `fixlist: {path, round, findings, open}` and `resume_session`, the prior turn's `session_id`,
1089
+ so the host can resume that sub-agent rather than pay to rebuild its context. **The framework
1090
+ resumes nothing itself** — `spawnAgent` has no `--resume` — so the bundle carries the fix list
1091
+ and the merged commit and hands the id back to the party that can act on it. Omit the flag and
1092
+ the latest still-open round is carried by itself, the same courtesy `--prepare` already
1093
+ extends to a story waiting on a review.
1094
+ - **One round per story, and the second is refused out loud.** A free round that could be taken
1095
+ twice is a story that never has to settle. A second `fixlist` is read as `changes` — which
1096
+ costs the attempt the first one did not — the refusal names the round already on disk, and the
1097
+ SECOND reviewer's prompt withdraws the verdict rather than offering one the executor would
1098
+ then refuse. `story reopen` resets the count with every other one in the review ledger.
1099
+ - **A story cannot settle `done` over an open `fix-now`.** An `approve` there settles `blocked`
1100
+ and the reason names the file, the finding's number and its heading, plus the two ways to
1101
+ close it. The check reads the FILE, not the envelope that produced it: the file is the state,
1102
+ and a host closes a finding by writing `Resolved: yes` in it or re-routing its `Disposition:`.
1103
+ That edit is the host's — §B.2's third role — because the author works in a story worktree of
1104
+ another repo and its own prompt forbids writing outside it.
1105
+ - **Fail-closed, unchanged and asserted.** A `verdict: "fixlist"` whose `fixlist[]` is missing,
1106
+ empty or unreadable is `changes`, never a free round and never `approve`. Both economies reach
1107
+ the same code: the host writes the envelope into the review bundle, or a spawned reviewer
1108
+ returns it — `REVIEW_SCHEMA` gained the verdict and the optional array, and `parseReview`
1109
+ narrows both.
1110
+ - `defer-with-log` findings are appended to `retro.md`'s `## Build feedback` as the artifact is
1111
+ written — the existing second writer with its existing verbatim dedup — so a deferred defect
1112
+ reaches the owner through a channel that already exists rather than a new one.
1113
+
1114
+ - **`tldrx next --prepare --review` / `--commit --review` — the reviewer is the second
1115
+ delegable role.** A Build story has two sub-agents and only the developer was ever
1116
+ delegable; the reviewer was the FRAMEWORK's spawn in both modes, which on a host-driven
1117
+ run buys a second reading of a diff the host is already reading, and a bill nobody
1118
+ budgeted. Now it rides the same handshake, one directory down:
1119
+ `.agent/<stage>/<story>/review/{prompt.md,pending.json,result.json}` — nested so a
1120
+ reviewer bundle can never be read as a developer one.
1121
+ - **`--prepare --review` writes the bundle and spawns nothing.** `prompt.md` is what a
1122
+ spawned reviewer would have been sent, from the same renderer. `pending.json` carries
1123
+ `role: reviewer`, `result_schema` (the reviewer's `--json-schema` envelope, verbatim,
1124
+ so the host needs no source to know the shape), and a `review:` block with the diff
1125
+ command, the merged commit, the attempt and the **DoD results recovered from
1126
+ `events.jsonl`** — the DoD is not re-run, and the prompt says so.
1127
+ - **`--commit --review` settles it through the existing seam.** The envelope goes through
1128
+ the same `parseReview` with the same fail-closed rule (unreadable ⇒ `changes`, never
1129
+ `approve`) and the same `reviewAndSettle`: `approve` ⇒ `done` with its evidence,
1130
+ `changes` ⇒ one requeue then `blocked`, `MAX_ATTEMPTS` and the requeue counter
1131
+ untouched. A host that never writes `result.json` has produced no verdict and spends
1132
+ no attempt.
1133
+ - **The trail says whose review it was.** No `agent.spawned`; a `task.started` with
1134
+ `role: reviewer, mode: prepare` instead. The `check: review` event carries
1135
+ `source: host` (written only for a host review, so the spawned path's payload is
1136
+ unchanged byte for byte), and the task row is `cost_usd: null, metered: false` unless
1137
+ the envelope declares `cost_usd` / `tokens`.
1138
+ - **On `attended_by: host` the framework never calls `spawnReviewer` at all.** Half B
1139
+ merges the story and hands the review over, so a full attended story cycle emits zero
1140
+ `agent.spawned`. Outside attended mode `--review` is opt-in and the headless reviewer
1141
+ is unchanged.
1142
+
1143
+ ### Changed
1144
+
1145
+ - **`tldrx next --prepare` on a story awaiting review now writes the reviewer bundle
1146
+ instead of spawning a reviewer.** It used to spawn a metered one — which is the single
1147
+ thing `--prepare` exists not to do. Measured 2026-08-31 on the live
1148
+ `260830-tenancy-identity-customers`: story S3's reviewer died at its $1.00 cap, the
1149
+ story parked at `review`, and the `--prepare` that was supposed to rescue it spawned a
1150
+ replacement that a two-minute host timeout then killed mid-read. The story is still
1151
+ parked at `review` afterwards, its attempt still unspent, and the verdict is the host's
1152
+ to write. Headless `tldrx next` still re-runs the review by spawning, unchanged.
1153
+ - **`tldrx next`'s `attended_by: host` refusal names `--commit --review`** when the stage
1154
+ is holding a reviewer bundle. It named `--commit`, which is the wrong half: that door
1155
+ reads the DEVELOPER's `result.json` and re-runs a pipeline that has already merged.
1156
+ - **`--discard-pending` bins the reviewer bundle too**, alongside the developer bundles it
1157
+ already binned — a stale review `result.json` would otherwise be read by the next
1158
+ `--commit --review` as a verdict on work it never saw.
1159
+
1160
+ - **`tldrx init` says what it is doing while it does it** — a live line per step, in the
1161
+ same `--ui scene|compact|plain|off` view family as the agent progress view, on stderr.
1162
+ It used to print NOTHING until it was finished. Measured 2026-08-30 on a five-repo
1163
+ workspace: **36.0 s of total silence** with the default `--provider auto` against
1164
+ **1.3 s** with `--provider static` — so ~97% of the wait is `graphify update` running
1165
+ once per repo inside `buildMap`, and the command looked hung for all of it.
1166
+ - **Ten steps announce themselves**: detecting repos, building the code map, writing
1167
+ `workspace.yml`, planning the interview, seeding experts, reading conventions, writing
1168
+ `process.yml`/`facts.yml`, `init-questions.md`, `init-handoff.md`, and the `.gitignore`
1169
+ + `CLAUDE.md` blocks. `--mcp` adds an eleventh, because it health-checks every server.
1170
+ - **The slow ones name the repo they are inside.** `detecting repos` reports each repo
1171
+ with its stack, confidence and default branch as detection finishes it; the map step
1172
+ reports `<repo> — 6 documents via graphify` per repo. The wait is now legible instead
1173
+ of merely long.
1174
+ - **A terminal gets a spinner, colour and an in-place rewrite; a pipe gets plain lines.**
1175
+ A finished step is printed once and never touched again, so it survives in scrollback
1176
+ after the command exits — a step list is a HISTORY, not a picture of a moment, which is
1177
+ why this is not `ui/driver.ts` with a different renderer. In `plain` a step still open
1178
+ after 5 s says `still <label> — 12 s`, because there is no spinner there to prove the
1179
+ process is alive.
1180
+ - **A schoolhouse** (`core/ui/campus.ts`) is painted above the steps in `scene` mode,
1181
+ drawn in the same hand as the classroom the agent view renders — `init` is the survey
1182
+ that happens before the school opens.
1183
+ - **`--quiet`** turns the live view off and keeps the report. **`--ui <mode>`** works on
1184
+ `init` exactly as it does on `next`, `run auto` and `expert train`, and a bad value is
1185
+ a usage error raised before any work is done.
1186
+ - **`core/ui/color.ts`** — the framework's first ANSI palette, resolved per STREAM rather
1187
+ than per process. `tldrx init > report.txt` on a terminal has a piped stdout and a TTY
1188
+ stderr: the live lines are still worth colouring and the file must still be plain text.
1189
+ `palette(false)` is the identity for every ink, so a renderer never branches on colour
1190
+ and the uncoloured path stays byte-for-byte deterministic in a test. `FORCE_COLOR` beats
1191
+ `NO_COLOR` beats `CI` beats the stream.
1192
+
1193
+ - **The dispatch-notes slot** — `.agent/<stage>/dispatch-notes.md`, and for a Build story
1194
+ `.agent/<stage>/<story>/dispatch-notes.md`: the one place a HOST can add context to a
1195
+ prompt the framework generated. Measured over one full run of
1196
+ `260830-tenancy-identity-customers`, 2026-08-30, EVERY stage needed host-added context the
1197
+ bundle lacked — a deferred decision at What, non-inlined seed docs and a staleness warning
1198
+ at How, the owner's answers at Plan, "Docker is up" at Build — and the host had exactly two
1199
+ places to put any of it, neither of which is one: `stage.md` is the framework's file,
1200
+ shared by every run of that workflow, and an edit to `prompt.md` is thrown away by the next
1201
+ `--prepare`.
1202
+ - **Rendered under `## Dispatch notes`, between `## Inputs` and `## Previous attempt`.**
1203
+ Behind the expert blocks on purpose: the slot is the most volatile thing in the document
1204
+ — a human writes it between one cycle and the next — and a per-cycle file ahead of the
1205
+ largest stable section would pay the cache-WRITE price on every stage. The same position
1206
+ in the Build developer prompt, directly under `## Inputs`, because that is where the
1207
+ brief ends and `## Investigate` step 1 tells the developer the files above ARE the brief.
1208
+ - **Absent ⇒ nothing changes, byte for byte.** No section, no `dispatch_notes` key in
1209
+ `pending.json`, `0 B` in the context ledger. Asserted by adding the file, re-preparing,
1210
+ removing it, re-preparing, and comparing the two prompts byte for byte.
1211
+ - **Capped at 8 KB, and never free.** The stage's file and the story's file feed ONE slot,
1212
+ spent stage-file-first, so neither can quietly double the budget; the overflow is named
1213
+ in the prompt, on stdout, and in `pending.json`
1214
+ (`dispatch_notes: {bytes, truncated, max_bytes, sources[]}`). The rendered section's
1215
+ bytes are charged to the context ledger and count against `prompt_max_bytes` like
1216
+ everything else — asserted with a ceiling the prompt clears without notes and breaks
1217
+ with them. The byte cut never splits a character in half.
1218
+ - **Context, never configuration.** The framework does not parse it, does not substitute
1219
+ `{{placeholders}}` in it, does not require `[src: …]` tokens on it, and it cannot change
1220
+ a declared input, an output, a check or a cap. The section says all of that to the
1221
+ sub-agent in its own preamble, because a note that reads like an instruction is otherwise
1222
+ indistinguishable from the stage's own rules.
1223
+ - **Survives `--discard-pending`.** The flag bins `pending.json`, `result.json` and
1224
+ `result.raw.json`; the notes are an INPUT to the rendering that is about to be redone,
1225
+ not an output of the one being binned.
1226
+ - **Per-cycle scratch, deliberately.** `.agent/` is gitignored, and that is the whole
1227
+ point: a caveat that must outlive the cycle is a FACT, and `.tldrx/memory/facts.yml` is
1228
+ the durable channel that already reaches every prompt with attribution behind it. Two
1229
+ durable channels for the same thing would make neither authoritative.
1230
+ - **`attended_by: host` — a run a host session drives, that the framework never spawns on.**
1231
+ One optional top-level key in `run.yml`, set at creation with `tldrx run new --attended-by host`
1232
+ or flipped later with `tldrx run attend host` / `tldrx run attend --none`. The finding it is
1233
+ built for is one sentence of field notes from 2026-08-30: a bare `tldrx next` on a Build stage
1234
+ runs the WHOLE remaining headless pipeline — every wave, every story, as paid spawns — when the
1235
+ host wanted one re-review. Six of six of those spawns then died on `Reached maximum budget` at
1236
+ caps a Plan agent had authored assuming host-billed sub-agents. $9.95, nothing delivered. The
1237
+ affordance was missing at the RUN level: `--prepare`/`--commit` is a decision per invocation,
1238
+ and nothing on the run could say "this one is being driven by a host session".
1239
+ - **A bare `tldrx next` exits `4` and names the exact command** the stage is waiting for —
1240
+ `--prepare` when it is ready, `--commit` when a bundle is already out. Four, not two: the run
1241
+ is not refusing the work, it is waiting on the host to take a turn, which is the same shape as
1242
+ waiting at a gate and the code `run auto` already stops cleanly on. The refusal is the first
1243
+ thing in `runStage` — ahead of the budget gate, ahead of reading an input, ahead of assembling
1244
+ a prompt — so nothing is billed and nothing is written.
1245
+ - **`--dry-run` is refused with it, and the message says why.** `--dry-run` is `mode: headless`:
1246
+ it spawns a real sub-agent and the turn is billed, and only the non-handoff FILES are reverted
1247
+ afterwards. That is measured, not read — one `agent.spawned`, one `agent.result`, the cost on
1248
+ the ledger. The CLI reference said "Spawns nothing, writes nothing" and a comment in
1249
+ `next.ts` said the same; both were wrong and both are corrected here.
1250
+ - **`tldrx run auto` is refused at exit `1`**, before the event log is opened, so nothing is
1251
+ written. A loop whose whole job is calling `next` headless has nothing to do on a run where
1252
+ `next` headless is a refusal.
1253
+ - **Three layers, because "nothing spawns" is a promise about money.** `runNext` refuses; every
1254
+ executor (`build`, `watch`) refuses a headless context with `refused: true`, so the stage goes
1255
+ back to `ready` rather than being marked failed; and `spawnAgent` itself throws while an
1256
+ attended run is in flight. The third is what makes "no run path can reach a spawn" a property
1257
+ rather than a claim about three `if`s — a fourth call site is always one merge away.
1258
+ - **`tldrx run attend` is deliberately small**: it sets one field, appends one `run.attended`
1259
+ event carrying the new value and the old, and touches no stage, no output, no branch and no
1260
+ money. `--none` REMOVES the key rather than blanking it, because `attended_by: null` is not a
1261
+ legal value. A direction is required and never guessed (exit `1`); setting what is already set
1262
+ appends nothing, since a decision nobody made does not belong in the log; a `done` or
1263
+ `cancelled` run is refused (exit `2`).
1264
+ - **`tldrx run status` prints `attended: host`** and the status line gains an `att` marker
1265
+ beside `auto:N` / `stale:N`, leading them because it is the one that changes what `tldrx next`
1266
+ will do. `--json` gains `attended_by`, appended after `unmetered_tasks` so every existing key
1267
+ keeps its position.
1268
+ - **Additive, and asserted as such.** Absent — which is every run.yml written before this — the
1269
+ framework may spawn and every path is what it was: the two-stage headless fixture's event
1270
+ sequence is asserted against the one captured from `main` at `dae1d07`, event for event, and
1271
+ an ordinary run.yml never mentions the key. A value the reader does not understand is a schema
1272
+ error rather than a silent downgrade to "spawn anyway"; `requireKeys` ignores unknown top-level
1273
+ keys, so an older binary reading a run.yml with `attended_by` still validates it — but it will
1274
+ DROP the key on its next save, since `emitRunYaml` only writes what it knows.
1275
+ - Out of scope on purpose: `tldrx expert train` and `tldrx seed triage --propose` spawn outside a
1276
+ run and are untouched. `attended_by` is a property of a run.
1277
+ - **`economy: metered-usd | host-tokens` on `budget.yml`** — a price gets a currency, and a
1278
+ headless spawn under a ceiling that is not money is refused before it spends. Measured
1279
+ 2026-08-30 on `260830-tenancy-identity-customers`: the Plan agent priced the run assuming
1280
+ HOST-billed sub-agents — turns the host session pays for, which this process never meters
1281
+ and which are ~free to the run — and the executor then enforced those figures as dollar
1282
+ ceilings on METERED spawns. Six spawns of six died on `Reached maximum budget`, each
1283
+ having spent real money to get there: **$9.95**, for nothing. The money model was a single
1284
+ scalar with no unit on it and no way to say *"this number is not dollars."*
1285
+ - **One optional key, three places**: the run level of `budget.yml`, any `phases[]` entry
1286
+ of it (which overrides the run), and the root of `03-plan/budget.yml`, so a Plan agent
1287
+ can say which economy it was pricing in. Resolution is phase-then-run.
1288
+ - **`tldrx next` refuses a headless spawn on a `host-tokens` phase — exit 2, above prompt
1289
+ assembly, before a byte is written or a cent is spent.** The message names the number,
1290
+ the unit, and both ways out (`tldrx next --prepare`, or re-label and re-price the
1291
+ phase). `--prepare` / `--commit` are untouched: the in-session handshake is exactly
1292
+ where a host-billed turn belongs.
1293
+ - **The two are never converted into one another.** There is no exchange rate here and
1294
+ inventing one would be a guess about a price. The budget-gate hook does not deny on such
1295
+ a phase (there is no dollar ceiling to enforce, and it says so on stderr), the auto
1296
+ gate's money condition reads `n/a (host-tokens economy)` rather than comparing a spend
1297
+ in dollars to a ceiling in tokens, and `tldrx run estimate` prices the stage in TOKENS
1298
+ with no dollar figure at all.
1299
+ - **A `03-plan/budget.yml` priced in `host-tokens` contributes no story caps.** Its
1300
+ numbers are not dollars, so the Build executor falls back to the uniform share it used
1301
+ before plan prices were read at all — and says so on stderr, through the advisory seam
1302
+ that already existed for an unusable plan budget.
1303
+ - **An unknown value is REFUSED, never defaulted to dollars** — a unit nothing here
1304
+ understands is not one it may quietly read as money. An empty `economy:` key, and an
1305
+ absent one, both mean `metered-usd`.
1306
+ - **`tldrx budget raise` no longer erases what it rewrites past.** The label round-trips
1307
+ through the same emitter the raise goes through; a raise that dropped it would turn a
1308
+ token budget back into dollars silently, from the one command an operator reaches for
1309
+ when a ceiling binds.
1310
+ - **Absent label ⇒ byte-identical behaviour**, asserted: an unlabelled `budget.yml` emits
1311
+ no `economy:` line, an unlabelled headless stage still spawns, and every existing budget
1312
+ test passes untouched.
1313
+
1314
+ - **`tldrx story reopen <id> --note "<why>"`** — one Build story, given another run of
1315
+ developer attempts, by a person. The third verb of the family that landed 2026-08-30 and
1316
+ the only one a HUMAN signs: the other two stop the machine reading a transport failure
1317
+ as a judgement, and this one is for when the machine read the run correctly and the
1318
+ owner disagrees. Found on `260830-tenancy-identity-customers`, where story S3 sat
1319
+ `blocked` after two GENUINE `changes` verdicts (its developers ran and committed nothing;
1320
+ both reviewers correctly refused an empty diff) — no rescue applied and none should have,
1321
+ but S3 gates wave 3 and the owner had decided it ships. The only reopening verb was
1322
+ `tldrx reject --stage`, which acts on a STAGE, and hand-editing `run.yml` or a story file
1323
+ is forbidden by design.
1324
+ - **The note is required** — a reopen with no reason is not actionable — and one
1325
+ `story.reopened` is appended carrying the actor, the note, the status the story came
1326
+ from, its wave, and how many verdicts the closed run of attempts consumed.
1327
+ - **The story goes back to `todo` and its attempt counter restarts at 1 of 2.** Nothing
1328
+ is erased to make that true: `story.reopened` is a RESET BOUNDARY that
1329
+ `readReviewLedger` reads, so verdicts before it stop counting while every event stays
1330
+ in `events.jsonl` for `replay`, `cost` and `retro`. The full reset is the honest
1331
+ choice precisely because the history survives it — "you get two more turns" is what
1332
+ overruling a block means, and a half-reset would be a number nobody could explain from
1333
+ the record.
1334
+ - **It runs no agent, spends nothing, deletes nothing and refunds nothing.** The story's
1335
+ BRANCH is what carries the last developer's commits forward and it is untouched; the
1336
+ worktree is left exactly as the build left it (kept at `review`, already removed for a
1337
+ `blocked` story) and is reopened from the branch when the next turn needs it.
1338
+ - **It does not make the stage runnable, and does not pretend to.** Sending a stage back
1339
+ is `reject`'s own signed decision, so the output names the command that fits where the
1340
+ Build stage actually is: `tldrx next` when it is ready, `tldrx reject --note` at a
1341
+ pending gate, `tldrx reject --stage` over a signed one.
1342
+ - Refuses with exit 2, saying why: an id the plan does not have (naming the ones it
1343
+ does); a `done` story, because undoing finished work is a decision about the stage and
1344
+ belongs to `reject --stage`; a `todo` story, which is already pending; and a missing
1345
+ `--note`. An unknown run id is still exit 3.
1346
+ - `tldrx replay` narrates it (`story S3 REOPENED by alan — back to \`todo\` from
1347
+ \`blocked\` — "…"`), and the Build stage says so in one line, with the note, when it
1348
+ picks the story up. A reopened story is byte-identical on disk to a never-started one,
1349
+ and a narrative that showed two `changes` verdicts and then a third developer turn with
1350
+ nothing in between would read as the framework losing count.
1351
+ - **`--parallel <n>` on `tldrx next` and `tldrx run auto`** — how many stories of ONE
1352
+ build wave run at once. `waves.yml` already puts every dependency in an earlier wave, so
1353
+ a wave's stories are independent by construction. Also settable per scope as
1354
+ `build: {parallel: N}` at the top of a workflow, or per stage as `parallel:` in
1355
+ `stage.yml`; the flag beats the workflow, which beats the stage file. **The default is 1
1356
+ and at 1 the executor takes exactly the path it always did** — verified byte-identical on
1357
+ the event sequence against `main`, not asserted. Above 1 the wave runs in two halves:
1358
+ developer + DoD + commit concurrently, then merge + reviewer serially in the wave's
1359
+ LISTED order, so the epic branch reads the same whatever order the machine finished in.
1360
+ The reviewer half is serial for a reason and not only the merge: a reviewer reads
1361
+ `git diff <epic>...<story>`, whose merge base moves every time another story merges into
1362
+ that epic. A red story does not cancel its siblings, but the wave ends `failed` and the
1363
+ next wave does not start. Ctrl-C/SIGTERM kills every live child, not the first.
1364
+ Budgets are untouched: the stage ceiling was already divided by
1365
+ `stories x attempts x (developer + reviewer)`, so N at once costs what N in a row cost.
1366
+ - The live view gives each running story its own column — `S1 reading … · S2 $ dotnet
1367
+ test …` — in the scene, the compact one-liner and `--ui plain`. A lane leaves the line
1368
+ when its sub-agent finishes. With nothing parallel the view is what it always was.
1369
+
1370
+ - **`gates_policy: agent` — a gate an agent may close, over a check it wrote down.** The third
1371
+ answer to "who closes a gate", beside `human` (waits) and `auto` (the harness signs when seven
1372
+ measured conditions hold). Measured 2026-08-30 on `260830-tenancy-identity-customers`: the host
1373
+ ran a defined checklist at every gate and typed it into `approve --note "<free text>"`, where
1374
+ nothing validated it, `replay` could not render it, and `run.yml` recorded a person's name for a
1375
+ check a sub-agent had made. The evidence note (above) was the artefact; this is the gate.
1376
+ - **Strictly stronger than an auto gate, never a cheaper one.** Three things, not one: every
1377
+ one of the seven `auto` conditions unchanged and unweakened (including the `boundary`
1378
+ condition landed alongside it), PLUS no budget decision in this stage's window, PLUS an
1379
+ evidence note that parses, sources every bullet, and whose verdict is `sign`.
1380
+ - **The budget requirement is an EVENT, not an arithmetic.** A `budget.raised` or
1381
+ `budget.blocked` in `events.jsonl` at or after the stage's `started_at` falls the gate to a
1382
+ person even when the spend is comfortably under the ceiling. Condition 3 already compares
1383
+ numbers; what it cannot see is that somebody *raised* the ceiling to let this stage through,
1384
+ and a decision made to unblock a stage may not then be signed off by the machine that was
1385
+ blocked. Asserted in both directions on one fixture: the same gate closes without the event
1386
+ and falls through with it, with nothing else changed.
1387
+ - **The same door, so the trail reads the same.** A closing agent gate goes through `approve`:
1388
+ the checks are re-run off disk, `gate.by` records the note's `by:`, one ordinary
1389
+ `gate.approved` is appended, and the cursor advances. `AUTO_GATE_ACTOR` is untouched —
1390
+ `by: auto` still means "the facilitator closed it with no note but its own conditions".
1391
+ - **The note is COPIED into the run tree**, at `<phase>/gate-evidence/<stage>.md`, and that
1392
+ copy is what `gate.evidence.path` points at. `.agent/` is gitignored by spec §1, and a gate
1393
+ whose evidence lives only in a gitignored directory is a gate nobody can audit from a clone.
1394
+ A copy, not a move: the scratch original stays where the agent left it.
1395
+ - **Four fallthroughs are named in their own right**, because a person's next move differs for
1396
+ each: `questions` (a decision nobody has made), `budget-event` (a ceiling somebody moved),
1397
+ `boundary` (work nobody scoped) and `refusal` (the note's verdict is `refuse` or
1398
+ `sign-with-fixlist` — the agent doing its job, not failing at it). Any other failing condition
1399
+ reports as `condition`, a missing or broken note as `evidence`. Each is tested in isolation:
1400
+ a report that fired three at once would not answer "which of these stopped it", which is the
1401
+ first question anybody asks.
1402
+ - **`tldrx approve --as-agent [--evidence <path>]`** is the same decision taken by hand, and it
1403
+ splits the two refusals apart by exit code. **Exit 2** is "this note is broken" — fix the
1404
+ file, nothing was signed. **Exit 4** is "a person decides": the note parsed perfectly and its
1405
+ verdict is not `sign`. **Exit 1** is `--as-agent` on a stage whose policy is not `agent`: a
1406
+ run keeps the policy it was opened with, and a flag that could upgrade one at approve time
1407
+ would make the frozen policy decorative.
1408
+ - **A person may always overrule it.** A plain `tldrx approve` on an agent-gated stage works
1409
+ exactly as it does anywhere else, is recorded as the person, and writes no `evidence` key. An
1410
+ agent gate is one an agent MAY close, never one a person may not.
1411
+ - **`tldrx replay` renders the check**, not just the signature: who signed, how many files they
1412
+ read, how many citations they spot-checked and what those resolved to, how many touched paths
1413
+ they audited and how many were outside the surface, and the path to the note. Rendered from
1414
+ the note's FRONT MATTER and from `run.yml` — never from its prose, which would change the
1415
+ narrative every time somebody rephrased a sentence. A note that has gone missing is SAID to
1416
+ be missing rather than invented, and the counts `run.yml` recorded still stand.
1417
+ - **`--gates` gains a qualified form**: `--gates plan:agent,build:agent`. A bare entry still
1418
+ means `human`, so every invocation anybody has already typed means exactly what it meant. An
1419
+ unknown policy is its own usage error, distinct from an unknown stage.
1420
+ - **Additive, and asserted as such.** No shipped scope uses `agent`; it arrives via
1421
+ `--gates`, or a fork's own workflow file, and never by default. An absent `gates_policy`
1422
+ entry still reads as `human`. A gate with no `evidence` emits no key at all, so every
1423
+ `run.yml` written before this round-trips byte-for-byte through a save — asserted by
1424
+ comparing the emitter's output against the file on disk. The gate mapping never rejected an
1425
+ unknown key (measured against the reader that predates this), so a `run.yml` carrying
1426
+ `gate.evidence` still validates on an older binary; a `gates_policy` naming `agent` there
1427
+ fails loudly instead, which is the right failure — a policy the reader does not understand is
1428
+ not one it may downgrade to "sign it anyway".
1429
+ - **The emitter had to be extended, not worked around.** `emitRunYaml`'s `gate()` wrote exactly
1430
+ five keys as a flow mapping, so a sixth held in memory would have been dropped, silently, by
1431
+ the next save. 47 new tests.
1432
+
1433
+ - **The gate evidence note** — `.agent/<stage>/evidence.md`, plus `tldrx gate template` to
1434
+ write the blank form. This is the artefact half of the `agent` gate (design §A): a third
1435
+ answer to "who closes a gate", between `human` (waits) and `auto` (the harness signs when
1436
+ seven measured conditions hold). Measured 2026-08-30 on
1437
+ `260830-tenancy-identity-customers`: the host ran a defined checklist at every gate and
1438
+ typed `approve --note "<evidence>"` by hand, into a free-text field where nothing validated
1439
+ it and `replay` could not render it. There was no value meaning *"an agent checked it,
1440
+ showed its work, and is accountable for the check"*, so there was nowhere to put the check.
1441
+ - **Front matter is the machine half, the body is the human half** — the §2.13 story
1442
+ pattern, reused rather than reinvented. Required keys: `version gate role by at verdict
1443
+ read citations touches diff_vs_stories`; `caveats` and `recommend` are optional and
1444
+ default to `[]`. Four H2 sections in order — `Read` · `Citations checked` ·
1445
+ `Touches audited` · `Verdict` — each with at least one list item.
1446
+ - **Every bullet goes through the EXISTING §2.8 resolver.** Not a second grammar and not a
1447
+ second checker: `srcToken.ts` tokenizes and resolves, and the section rule is
1448
+ `handoff.ts`'s, lifted into a shared `validateSections` that `validateHandoff` now calls
1449
+ too. Two readers of "is this bullet sourced" drift, and the looser one would win the
1450
+ argument at exactly the moment a gate is being signed. A checklist whose own claims are
1451
+ unsourced is what `claim-sources` exists to refuse, and an evidence note is a claim about
1452
+ a claim.
1453
+ - **`unverified` REFUSES here, unlike in a handoff.** A citation nothing could check does
1454
+ not fail a stage (spec §2.8) but it is precisely what stops an AUTO gate closing (spec
1455
+ §5, condition 5). An agent gate is strictly stronger than an auto gate, never a cheaper
1456
+ one, so a `doc:` URL nothing in the workspace names cannot be what a signature rests on.
1457
+ - **Seven refusals, each with its own message and its own `kind`**, so a caller routes on
1458
+ the reason rather than on a string: unreadable or incomplete front matter · a missing
1459
+ section or one holding only prose · a bullet with no `src` token or one that does not
1460
+ resolve · `sampled > of` or `resolved + refuted > sampled` · `sampled: 0` with citations
1461
+ on record · a verdict that is not `sign` · a `gate:` naming a stage other than the one at
1462
+ the cursor. `verdict` is the kind that means "a person decides" rather than "this note is
1463
+ broken" — `sign-with-fixlist` and `refuse` fall to a human by design, and the verdict
1464
+ space is three because a reviewer can meet every acceptance criterion and still have
1465
+ found three real defects nobody wrote a criterion for.
1466
+ - **`tldrx gate template` fills what a tool can COUNT and leaves every judgement blank**:
1467
+ the gate at the cursor, the time, how many citations the §2.8 resolver found across the
1468
+ stage's declared outputs (patterns like `03-plan/stories/<id>.md` included), and how many
1469
+ touched paths the plan declares. The blank form deliberately does not validate — a
1470
+ template that parsed clean out of the box would be a signature nobody had to earn — and
1471
+ it writes no `[src: …]` anywhere, the same rule `questions lint --fix` follows. It spends
1472
+ nothing, spawns nothing, approves nothing and moves no cursor; an evidence note already on
1473
+ disk is left alone (exit `2`) unless `--force` says otherwise.
1474
+ - `validateEvidence(text, srcContext, {gate})` is the function `approve --as-agent` will
1475
+ call before it records anything. Nothing in this change signs a gate, reads a
1476
+ `gates_policy`, or writes into the run tree: the artefact layer lands first, on purpose.
1477
+
1478
+ - **Auto-gate condition 7, `boundary` — the stage stayed inside the surface the run declared.**
1479
+ The other six ask whether the artefact is sound and whether the work finished. None of them
1480
+ asks the question a reviewer asks first: *is this the work we scoped?* Measured 2026-08-30
1481
+ on `260830-tenancy-identity-customers` — the host ran this check BY HAND at every gate,
1482
+ because the framework ran it nowhere ("touches outside What boundary is NOT checked
1483
+ anywhere"), and that run's own S3 review surfaced the shape it was worried about: a
1484
+ Platform-layer file edited by a module story.
1485
+ - **The surface** is the union of every `file:`-kind `[src: …]` citation in
1486
+ `01-what/handoff.md` and `02-how/handoff.md`, and every `touches:` entry of every story
1487
+ under `03-plan/stories/` — or of `04-build/implicit-plan.yml` when the scope skipped Plan.
1488
+ A directory entry covers everything beneath it, which is how a story declares the files it
1489
+ is about to create and the forced companions (a lockfile, a generated client) that come
1490
+ with them. The citation half reuses `citedRepoPaths`, the derivation the implicit plan
1491
+ already builds `touches:` from — the same §2.8 tokenizer, not a second one.
1492
+ - **The measurement** is `git diff --name-only <default_branch>...<epic_branch>`, once per
1493
+ repo the plan's epics name, through the Build phase's existing git seam. Nothing is
1494
+ checked out, fetched or written, and the epic's own `branch:` is what is diffed — the ref
1495
+ `openStory` actually cut, not one re-derived here.
1496
+ - **Offending paths are NAMED**, up to eight then `+N more`, prefixed with their repo:
1497
+ `boundary=2 changed path(s), 1 outside the surface: app:platform/Auth.cs; work outside the
1498
+ declared surface is a boundary change — a human decides whether to widen the scope`.
1499
+ "1 path outside the surface" is not something anybody can act on.
1500
+ - **A human may still approve over it**, and that is the whole point: widening a boundary is
1501
+ a decision, and the framework has no basis for making it. Work nobody scoped is often the
1502
+ right work.
1503
+ - **It never refuses on an absence.** Outside Build, with no epic branch cut yet, with no
1504
+ repo on disk, with no plan, or on a run whose What cited no repo path at all, it is `n/a`
1505
+ **with the reason in the note** — a condition that could not measure must not report that
1506
+ it measured zero. Same shape as condition 6's `n/a (not a build stage)`.
1507
+ - **`tldrx-work/`, `.tldrx/` and `.agent/` are excluded from BOTH sides**, through the same
1508
+ `isStatePath` filter the implicit-plan derivation already applies. A handoff cites the
1509
+ run's own state as evidence, and in a `root_is_repo: true` workspace the state sits inside
1510
+ the product repo — neither is a boundary question.
1511
+ - **A bare citation widens the surface rather than shrinking it.** `file := [repo ":"] path
1512
+ ":" line` makes the repo prefix optional, and `citedRepoPaths` skips the bare form because
1513
+ it feeds a developer prompt, where a wrong guess puts another repo's file in front of an
1514
+ agent. Here the risk is inverted — an unattributable citation would manufacture a false
1515
+ refusal — so a bare path is admitted to every repo's surface. A check that refuses wrongly
1516
+ is a check that gets turned off.
1517
+
1518
+ ### Changed
1519
+
1520
+ - **The init report is coloured and carries a roll-up.** Repo names, confidence
1521
+ (green/yellow/red), the counts and the `created`/`kept` verbs are inked, and a new
1522
+ `files N written · N created · N kept` line answers "how much of this run was
1523
+ regenerated, how much is new, and how much was mine and left alone" without reading the
1524
+ per-file list. `stripAnsi(coloured) === uncoloured`, asserted.
1525
+ - `detectWorkspace` and `buildMap` take optional progress callbacks. Both default to
1526
+ doing nothing, so `tldrx map` and every other existing caller behave exactly as before.
1527
+ - `tldrx init` now writes progress bytes to stderr like every other long-running command,
1528
+ so the two `build.test.ts` cases that spawn it to exercise the node seam pass `--quiet`.
1529
+ Their `stderr === ""` assertion is how a REAL warning gets noticed, and it still is.
1530
+ - **`tldrx cost` is organised by ECONOMY, and prints no grand total.** `STAGE · ECONOMY ·
1531
+ MEASURED · DECLARED`, one footer per economy, and a third line for attempts that reported
1532
+ neither — no row spans both columns and nothing adds a dollar to a token. A footer that
1533
+ printed `$1.70` under a run which had also burned 1.5M host tokens is the sentence the
1534
+ label exists to stop. `--json` carries `economy` on every stage row and the set of
1535
+ economies on the run. Attempt lines are unchanged, all four token counters included, and a
1536
+ declared `--tokens` figure past a million now reads `~1.2M` rather than `~1200.0k`.
1537
+
1538
+ - `tldrx doctor` prints where the framework's own files are: a `framework <path>` line
1539
+ naming the installed package that ships `stages/`, `workflows/` and `templates/`, and
1540
+ saying that a project's overrides live in `.tldrx/stages/` and `.tldrx/workflows/`. The
1541
+ `/tldrx` skill says the same in two lines. Measured 2026-08-30: a real session spent
1542
+ 1m22s on `find / -name build -type d -path "*stages*"` because nothing printed it.
1543
+
1544
+ ### Fixed
1545
+
1546
+ - **A trainer that `cd`s no longer writes its knowledge file into a different git repo.**
1547
+ Measured 2026-08-31 on `~/scavtopia` (five repos, ten `expert train --mode light` runs): the
1548
+ `mcp` run was rejected with `mcp.md.partial was never written`, and the file had been written —
1549
+ 46 lines, 9,567 bytes, complete and usable — to
1550
+ `whiteboard/.tldrx/experts/mcp/knowledge/mcp.md.partial`. The sub-agent ran
1551
+ `cd <workspace>/whiteboard` to execute that repo's declared gate command, then wrote the
1552
+ RELATIVE path the prompt had given it, and the path resolved against the repo it had `cd`'d
1553
+ into. Three costs from the one bug: **$1.23 charged for work that was finished and then
1554
+ orphaned**, a parasitic `.tldrx/` tree left inside an unrelated git repo (`git -C whiteboard
1555
+ status` → `?? .tldrx/`), and **no repair round possible** — the missing-file branch returns
1556
+ before the repair check, so this failure mode was unrecoverable by construction even with
1557
+ budget left. Fixed at both ends.
1558
+ - **Prevention: the prompt now states an ABSOLUTE output path**, workspace-root-resolved, and
1559
+ says why — "If you `cd` into a repo to run its gate command, a relative path then resolves
1560
+ against THAT repo … and throws the whole paid run away. That is measured, not hypothetical."
1561
+ Both training prompts carry it, and so does the repair round's target, for the same reason.
1562
+ - **Recovery: when the file is missing, the declared repo roots are probed** for the stray
1563
+ relative write before "never written" is said. A file found there is moved back and validated
1564
+ exactly as if it had landed correctly — recovery is not a pass, the same `parseKnowledgeFile`
1565
+ still judges it, and a recovered file that fails can still be repaired because the probe runs
1566
+ ABOVE the repair round.
1567
+ - **The note is honest and names the mess.** `recovered: the trainer wrote to
1568
+ whiteboard/.tldrx/… , inside the `whiteboard` repo — a relative `.tldrx/…` path resolves
1569
+ against whatever directory it had `cd`'d into.` The empty parasitic directories are removed
1570
+ on the way out; a directory holding anything else is **left in place and named**, with the
1571
+ `git -C <repo> status` to run, because a tool that deletes inside a repo it was never asked
1572
+ to touch is a worse bug than the one it is fixing. A repo carrying its own
1573
+ `.tldrx/workspace.yml` is skipped entirely — that file may belong to a nested workspace, and
1574
+ taking it would be theft rather than recovery.
1575
+ - When no stray is found the verdict is unchanged and now says where it looked.
1576
+
1577
+ - **A rejected training run records WHICH problems, not just how many.**
1578
+ Measured 2026-08-31: `components` failed with 12 problems for $1.02, and `training.jsonl` — the
1579
+ durable record — held only the string `"…does not validate — 12 problem(s)"`. The twelve went
1580
+ to stdout, where five of them were printed and the rest elided as `(+7 more)`. Anyone who had
1581
+ not captured stdout, which is anyone running this normally, could not tell why a $1.02 run
1582
+ failed. The list is now persisted twice.
1583
+ - **On the ledger**: `check.failed.payload` carries `problems` (the rendered per-problem
1584
+ lines), `problems_total`, `errors`, and `task`. The list is fitted to the record's 4 KB
1585
+ payload cap and reports `problems_omitted` when it does not fit — an append that THROWS on
1586
+ an oversize payload would take the cost line down with the reasons, which is the opposite of
1587
+ the point. The repair round's own `check.failed` carries what it sent back, so "what did the
1588
+ repair actually fix" is answerable later.
1589
+ - **In the file**: `<area>.rejected.md` now opens with a `# REJECTED` header — expert/area,
1590
+ mode, timestamp, dollars spent, error and warning counts, and every problem, uncapped —
1591
+ above the trainer's bytes exactly as written, separated by a rule. A quarantine with no
1592
+ verdict (a sub-agent that died, a rollback) gets no header: there were no reasons to state
1593
+ and inventing them would be inventing the reason.
1594
+
1595
+ - **`## Sources` is now taught as prose with the refused shape shown.** Same batch: four of the
1596
+ five problems the `components` report printed are one mistake four times — `L34 Sources: no
1597
+ [src: …] token`, `L35`, `L36`, `L37`. The trainer had written the recap as a bulleted list.
1598
+ The prompt already said "**Sources** — prose", and a writer who reads that as a style note
1599
+ writes bullets, because bullets are what the other four sections take. The rule it collides
1600
+ with is genuinely file-wide — `parseKnowledgeFile` requires a `[src: …]` token on EVERY list
1601
+ item in every declared section, recap included, and an unsourced one is an error that rejects
1602
+ the file whole. Both prompts now show the accepted prose next to the refused bullets, the same
1603
+ move the execution-claim rule makes. Whether an unsourced recap bullet should be a warning
1604
+ rather than an error is a real question and is deliberately NOT settled here.
1605
+
1606
+ - **A rejected knowledge file gets ONE repair round before the money is thrown away.**
1607
+ Measured 2026-08-30 on `~/scavtopia`: `tldrx expert train dotnet-stack --area dotnet --mode
1608
+ light` spent **$1.69**, the trainer wrote `knowledge/dotnet.md.partial`, and the validator
1609
+ refused it for **two** bullets that asserted an execution (`exit 0`) and cited a file line.
1610
+ The file went to `dotnet.rejected.md`, nothing reached `competencies.yml`, the status did not
1611
+ move — $1.69 for zero evidence, over a mistake the checker could name in one line and the
1612
+ writer could have fixed in one edit. `expert train` now hands those exact problems back to the
1613
+ same trainer for one more turn before anything is quarantined.
1614
+ - **A fresh spawn carrying the ORIGINAL prompt**, not a resumed session: `spawnAgent` has no
1615
+ `--resume` and the session id is captured for the ledger only. Appending to the original
1616
+ prompt is what keeps the repair possible at all — the citations to be fixed point into files
1617
+ that were inlined in that prompt and nowhere else — and the byte-identical prefix reads the
1618
+ cache the first turn paid to create. The appended `# REPAIR ROUND` section carries the
1619
+ numbered verdict, the rejected file with a line-number gutter whose numbers are the `L<n>`
1620
+ numbers in the verdict, and the reminder that deleting an offending bullet is a legal fix.
1621
+ - **One round, and the gate does not move.** The repaired file goes through the same
1622
+ `parseKnowledgeFile`, same shape, same scope. A second failure rejects exactly as the first
1623
+ used to, quarantines the same way, and returns the same exit `5`. An unsourced claim still
1624
+ cannot become evidence; it has simply been told once that it is unsourced.
1625
+ - **Paid out of `--max-usd`, never on top of it.** The repair turn's ceiling is
1626
+ `min(this sub-agent's share, whatever is left of the run's ceiling)`. Below the `$0.25`
1627
+ floor it does not spawn and says so — a cold `claude -p` that dies on
1628
+ `error_max_budget_usd` before its first reply costs money and produces nothing.
1629
+ - **The operator is told while it happens**, so an extra sub-agent never spends silently:
1630
+ `repairing: 3 problem(s) sent back to the trainer — one round, $0.31 of the ceiling left`,
1631
+ then either `repaired: the second file validates` or `the repaired file does not validate
1632
+ either (2 problem(s)) — one round is all there is`. Both turns land in `training.jsonl`; the
1633
+ repair as `task: "code:repair"`, `repair: true`, with the number of problems it was sent.
1634
+ - Not on `--commit`: there the sub-agent belongs to the host session and this process spawned
1635
+ nothing, so repairing is running `--commit` again. Not for a file that was never written —
1636
+ there is no verdict to send back.
1637
+
1638
+ - **The execution-claim rule is now TAUGHT, with an example and a counter-example.** The same
1639
+ $1.69 run is the evidence that stating it once in a paragraph does not work. Both training
1640
+ prompts — the spawned one and `--print-prompt` — now give the four literal shapes the checker
1641
+ looks for (`exit <n>`, `<n>/<n> passed`, `build is green`, and the bare word `measured` in the
1642
+ sentence itself), one conforming line, one refused line, and why the refused one is refused: a
1643
+ `workspace.yml` line DECLARES a command and is not a record of running it. Two further gaps
1644
+ closed: **not making the claim** is named as the other legal answer (the trainer that failed
1645
+ had no command in reach of the sentence it was writing, so "run it and cite it" was not
1646
+ actionable), and the `(measured)` **annotation** is explicitly exempted — §2.3 asks for it on
1647
+ every bullet, `\bmeasured\b` is one of the patterns, and nothing had ever told a writer that
1648
+ `claimCheck` strips the annotation before it looks. The runs-mode prompt never stated the rule
1649
+ at all, though the validator has always applied it to both files; it does now.
1650
+
1651
+ - **`duplicate src` is documented as non-fatal, and the rejection report stops implying
1652
+ otherwise.** It always was a warning — one call site, one `severity: "warning"`, one validation
1653
+ path for both shapes — but the report did not say which lines were fatal and the headline
1654
+ counted warnings as problems, so the same $1.69 run read as "3 problems" when only 2 rejected
1655
+ it and the third was a duplicate that costs one bullet its evidence row. `describeKnowledgeIssues`
1656
+ now lists errors first, prefixes warnings with `warning:` exactly as `knowledgeWarnings` does,
1657
+ and the headline counts errors only (new `knowledgeErrors`). The reason it is a warning is
1658
+ recorded where the message is emitted: "earns no second row" is a statement about scoring, not
1659
+ about honesty, and throwing away every other finding in a file over a repeated citation is not
1660
+ rigour.
1661
+
1662
+ - **A developer that FAILED is no longer recorded as a consumed attempt.** The
1663
+ developer-side sibling of the reviewer fix below, found by the same run on 2026-08-30.
1664
+ Five developer spawns on `260830-tenancy-identity-customers` died with
1665
+ `Reached maximum budget ($0.30 | $0.40 | $0.50 | $0.90 | $1.50)` before delivering
1666
+ anything the pipeline could see, and every one was settled as the story `blocked` —
1667
+ terminal in-run — so six of seven stories were reported as tried and failed when five of
1668
+ them had never been tried. A failed developer spawn now puts the story back at the status
1669
+ it held BEFORE the attempt (`todo`, or `review` when a reviewer had asked for changes),
1670
+ keeps its worktree, spends no attempt, and stops the in-process loop rather than buying
1671
+ the same error twice. Its `check.failed` carries `check: "developer"`,
1672
+ `status: "error"` and the error verbatim as `detail`; the review log, the operator line
1673
+ and `retro.md` all say the developer **FAILED**, never that anything was reviewed. The
1674
+ next `tldrx next` — headless or `--prepare` — offers the story again as a fresh developer
1675
+ run at the **same attempt number**. A developer that RAN and produced work its DoD faulted
1676
+ is a different thing entirely and still blocks, unchanged, as do two `changes` verdicts.
1677
+ - **Runs recorded by the old code pick those stories back up.** A `blocked` story whose last
1678
+ attempt recorded no commit, no check of any kind and no reviewer — the only trace the old
1679
+ code left, the error itself having gone to `run.yml` alone — is read as the errored spawn
1680
+ it was and offered again, with `S2 was blocked by a developer that FAILED (…) — that was
1681
+ never an attempt, so it is offered again`. It is paired with the story's own plan, because
1682
+ a story with an empty dod block blocks with exactly the same event shape and that block is
1683
+ a plan bug. Measured read-only against the live run: `tldrx next --prepare` now offers
1684
+ **S2** and, after it, S5, S4, S6 and S7 — while S3, which was blocked by two genuine
1685
+ `changes` verdicts, stays blocked.
1686
+ - **The auto gate will not sign a Build stage whose stories are not all `done`.** Its five
1687
+ conditions were all about the ARTEFACT — citations, questions, money, status — and none of
1688
+ them looked at what the stage was for. On the live run all five held while six of seven
1689
+ stories sat `blocked` and the epic branch carried one story's work, and the gate signed the
1690
+ stage, then signed it again after a human revoked it. A sixth condition now reads the story
1691
+ statuses where they live, refuses with `stories=1 of 7 done — S2:blocked, S3:blocked, …`
1692
+ and falls through to the human gate. A person may still approve over blocked stories —
1693
+ what is worth shipping is their judgement — and outside the Build phase the condition is
1694
+ measured as `n/a` and always holds.
1695
+ - **A merge that moved nothing is no longer called "merged".** `git merge --no-ff` of a
1696
+ branch that is already an ancestor exits 0 and says "Already up to date", and the handoff's
1697
+ Gate section rendered that as landed work: on the live run it read
1698
+ `(S1, S3, S5, S4, S7 merged)` when the epic tip carried only S1's commits. The executor now
1699
+ counts what the merge is about to move BEFORE it moves it — afterwards it cannot, because a
1700
+ merged branch is an ancestor either way — and the Gate line, the story Finding and the
1701
+ review log all say “added nothing — identical to `epic/x`” for a count of zero.
1702
+ - **A reviewer that FAILED is no longer recorded as a reviewer that asked for changes.**
1703
+ Found live 2026-08-30 by the first `feature`-scope run to reach Build: the headless
1704
+ reviewer of a 39-file, +1879-line story was given $0.26, died mid-read with
1705
+ `Reached maximum budget ($0.26)`, and the executor wrote that transport error down as
1706
+ `verdict: "changes"`. That single line spent the story's one requeue, sent a fresh
1707
+ developer at code nobody had faulted, and would have blocked the story after a second
1708
+ reviewer hit the same wall — with **zero review ever performed**. A failed reviewer now
1709
+ settles the story at `review` with `verdict: "error"`: the attempt counter is untouched
1710
+ (only a real verdict spends it), the `check.failed` event carries the error as its
1711
+ `detail` plus `verdict: "error"` so no ledger counts it as changes-requested, and every
1712
+ operator-facing line, the review log and `retro.md` all say the reviewer **FAILED**.
1713
+ Fail-closed is unchanged — an unfinished review is still never an approval. Inventing the
1714
+ verdict is what stopped.
1715
+ - **`tldrx next` on a story whose review errored re-runs only the REVIEW.** The diff is
1716
+ already committed and merged and its DoD went green, so there is nothing for a developer
1717
+ to redo. Both doors do it: the headless path and `tldrx next --prepare`, which used to
1718
+ hand the host session a full "attempt 2" developer bundle. The commit and the DoD results
1719
+ come back out of `events.jsonl`, so the resumed reviewer sees the same proof the first one
1720
+ did — including on the live run, where a `task.started` for the attempt that was never owed
1721
+ sits AFTER the DoD it did not run, and must not erase it. Runs recorded by the OLD code resume too — a `verdict: "changes"` whose `detail` is
1722
+ one of the framework's own transport errors is read as the failure it was, including a
1723
+ story already left at `in_progress` by a wrongly-prepared attempt 2.
1724
+ - **The Build executor reads `03-plan/budget.yml`.** The Plan writes a per-story price map,
1725
+ the Plan gate validates it, and until now **nothing read it**: the executor split its
1726
+ stage into equal shares, so the story priced at $4.75 and the one priced at $0.75 both got
1727
+ $1.03. A priced story now gets `price / (attempts x (developer + reviewer))` as its
1728
+ developer ceiling and a quarter of that as its reviewer's; an unpriced one keeps the
1729
+ uniform share; prices adding up to more than the stage are scaled down proportionally. A
1730
+ `budget.yml` that will not parse or validate is an advisory on stderr, never a refused
1731
+ build.
1732
+ - **A reviewer is never given less than $1.00.** Whatever the arithmetic says, clamped by
1733
+ what the stage has left and by `per_agent_max_usd`. A reviewer that cannot finish reading
1734
+ the diff approves nothing and blocks nothing — it converts the entire developer turn
1735
+ beside it into a story stuck at `review`, which is what $0.26 did on 2026-08-30.
1736
+ - **`tldrx cost` no longer prints `0 in · 0 out · 0 cache write · 0 cache read` for a turn
1737
+ the host declared tokens for.** `tldrx next --commit --tokens 342527` writes that number
1738
+ onto the task row and the `agent.result` payload, and the cost view ignored it. It now
1739
+ renders as `~342.5k declared (host session)`, kept apart from the four measured counters
1740
+ rather than folded into them: nobody measured those, and four zeroes claim the turn used
1741
+ no tokens.
1742
+ - **A stage whose declared outputs are a SHAPE no longer fails while the files sit next to
1743
+ the error.** Found live 2026-08-30 by the first `feature`-scope run to reach Plan: the
1744
+ stage wrote `03-plan/epics/E1.md` and `03-plan/stories/S1.md`..`S7.md`, and
1745
+ `tldrx next --commit` refused it with "`03-plan/epics/<epic>.md` was declared as an output
1746
+ but does not exist on disk; `03-plan/stories/<id>.md` was declared as an output but does
1747
+ not exist on disk". Plan cannot name its outputs — it does not know how many stories there
1748
+ will be until it has written them — so `stage.yml` declares the shape, and every
1749
+ filesystem call was asking `existsSync` about a path with a literal `<id>` in it.
1750
+ A declared path holding an angle-bracket token is now a **pattern**: it matches any file
1751
+ in that directory with the pattern's fixed prefix and suffix, resolved against the run dir
1752
+ and then the workspace root, in the same "first base wins" order everything else uses.
1753
+ The fix is in `paths.ts`, at the seam, not in the validator: `present`/`missing` count a
1754
+ pattern by its matches (so a stage taking `stories/<id>.md` as an INPUT gets past the gap
1755
+ check), the prompt is handed the concrete files rather than the shape, the previous-attempt
1756
+ inline shows every one of them, and `--dry-run` reverts each file it matched and names it.
1757
+ `{repo}` expansion is untouched and still runs first, so `{repo}` and a token compose.
1758
+ Plain paths behave exactly as before, down to the wording of their failure.
1759
+ - A pattern output that matches nothing now fails honestly — "`03-plan/stories/<id>.md` was
1760
+ declared as an output but **no file matches it on disk**", rather than claiming a file
1761
+ nobody ever named was looked for and not found. Its `sections:` contract binds **every**
1762
+ matched file, and the failure names the concrete file that broke it, not the shape.
1763
+ - **`tldrx run estimate` prices cache traffic, which is where the money actually goes.**
1764
+ Measured 2026-08-30 on a real workspace: a What stage was estimated at **$0.33** and the
1765
+ one comparable real attempt cost **$1.70** — **5x**. That attempt's ledger says why: 56
1766
+ input · 29.0k output · **166.3k cache write** · **3,747.1k cache read**. The estimate
1767
+ multiplied input and output only, so it was adding up the two columns the money was not in.
1768
+ Both cache counters had been on every `agent.result` since wave N and `modelPrices.ts` had
1769
+ carried the multipliers the whole time — nothing needed new data, only arithmetic that used
1770
+ it. The estimate now prices four terms: measured prompt tokens at the input rate, plus the
1771
+ **median** cache write (**1.25x** input), cache read (**0.1x** input) and output of past
1772
+ attempts at the same stage id, falling back to attempts at any stage and **naming which
1773
+ sample it used**. It prints the breakdown —
1774
+ `input ~189 · cache write ~166k · cache read ~3,747k · output ~29k → ~$1.46` — and keeps
1775
+ saying "ESTIMATE" in words. With no history the old behaviour stands (it refuses to guess
1776
+ the output half) and it now says `cache traffic not modelled — first attempt of this kind`
1777
+ rather than pricing a silent zero. The input and cache-write terms overlap on a cold first
1778
+ turn, so a first attempt leans high; that is stated in the output's own honesty line, not
1779
+ corrected away.
1780
+ - **`tldrx cost` shows the cache write / cache read columns on every attempt line**, not only
1781
+ on stage and run totals, and no longer hides them on a stage that ran once — previously an
1782
+ attempt line carried cost, task and model and nothing about where the money went.
1783
+
1784
+ - **tldrx state survives the project's own `.gitignore` rules, and `doctor` detects a rule
1785
+ that shadows it.** Found by a real user 2026-08-30: their repo carried the stock .NET
1786
+ `[Ll]og/` ignore, which swallowed `tldrx-work/<run>/04-build/log/S1.md` — the Build phase's
1787
+ per-story review log, which spec §1 marks committed and the handoff cites as
1788
+ `[src: 04-build/log/<id>.md:1]`. Nothing errored; the file was written, `git status` stayed
1789
+ quiet, and a teammate's clone never got it. `init`'s managed block only ever ADDED ignores,
1790
+ so any pre-existing project rule (`log/`, `docs/`, a `*.yml` in a subdir) could hide state
1791
+ and nothing noticed. The block now opens with `!tldrx-work/`, `!tldrx-work/**`, `!.tldrx/`
1792
+ and `!.tldrx/**` — the bare pair and the `**` pair are both needed, because gitignore cannot
1793
+ re-include a file whose parent directory is excluded — and the framework's own ignores follow
1794
+ AFTER them, since a later pattern wins. Measured with `git check-ignore -v` against a repo
1795
+ carrying `[Ll]og/`, not asserted from memory: the story log comes back not-ignored while the
1796
+ product's `Logs/build.log`, `tldrx-work/*/.lock` and `.tldrx/cache/` stay ignored. Re-running
1797
+ `init` upgrades a block written before this in place, markers and neighbouring rules kept.
1798
+ - **`tldrx doctor` now says when a rule outside that block is still hiding state.** It runs
1799
+ `git check-ignore --verbose --no-index -z` over four paths that must be tracked — the newest
1800
+ run's `run.yml` and `events.jsonl`, a synthetic `04-build/log/` probe, and
1801
+ `.tldrx/memory/facts.yml` — and prints each offender with the rule's own `file:line:pattern`,
1802
+ so a `.git/info/exclude` or a nested `.gitignore` is named too. A warning: it never moves the
1803
+ exit code, which is about the tools this machine has. `--json` gains `gitignoreShadow`, where
1804
+ `null` means no workspace was scanned rather than nothing found.
1805
+ - **The `## Inputs` preamble no longer claims files the budget dropped.** Measured on a
1806
+ real Build prompt, 2026-08-30: 9 of 15 declared inputs were inlined, the other 6 carried
1807
+ "It exists on disk; do not guess at its content" — and the preamble above them still read
1808
+ "Their full content is inlined below, so there is nothing to open and nothing else to
1809
+ find." The two documents the run existed to edit were among the six. The preamble is now
1810
+ conditional in every prompt that has one (stage prompts and the developer prompt share one
1811
+ renderer): with everything inlined it is the sentence it always was; with anything dropped
1812
+ it is `Inlined below: <n> of <m> declared inputs.` followed by "The rest exist on disk —
1813
+ READ them at the listed paths before relying on them; do not guess: <list>".
1814
+ - **A touched path the story's worktree cannot read is flagged as such.** The developer works
1815
+ in a worktree of the story branch, so a path that exists in the repo but is not committed
1816
+ at that branch is unreadable there — and `existsSync(worktree/path)` called it a file the
1817
+ story creates. Build now asks git (`git cat-file -e <branch>:<path>`) and marks it `NOT in
1818
+ this worktree — its content is only what the handoff quotes`, plus one stderr line per
1819
+ path: `warning: input <path> is not committed, so the story worktree cannot read it`. A
1820
+ path that exists nowhere is still "does not exist yet — this story creates it".
1821
+ - **The story's own goal wins the developer prompt's inline budget.** `touches` was spent in
1822
+ list order, so on that same run `AGENTS.md` — cited once in passing — was inlined whole
1823
+ and the two documents the goal named were in the dropped tail. Touched paths the story's
1824
+ `goal`, acceptance criteria, test plan or title NAME now sort first into the 64 KB; a brief
1825
+ that names nothing changes no order at all.
1826
+ - **The developer is told to run an acceptance criterion's embedded pattern BEFORE it
1827
+ edits.** Found on a real run's second Build of 2026-08-30: a derived criterion carried a
1828
+ literal grep (`` Pending `DECISIONS-NEEDED.md` # ``, backticks included) and the markers
1829
+ it was meant to count had been written three different ways, so it reported 0 against two
1830
+ files that still held five real markers — the in-session driver only caught it by
1831
+ measuring the inventory by hand. The developer prompt's `## Investigate` list now carries
1832
+ the rule verbatim: validate the pattern against the current tree first; a criterion that
1833
+ reports zero while the goal says the work exists is broken, so measure the real inventory,
1834
+ use THAT as the completion test, and record the discrepancy in the handoff. The criterion
1835
+ text itself stays data the story may not edit. `stages/build/stage.md` says so too.
1836
+ - **The implicit story no longer `touches` tldrx's own state.** `touches` is derived from
1837
+ every repo path the What handoff cites, and a handoff cites state as evidence: measured
1838
+ 2026-08-30, 13 touched paths of which three were `run.yml`, a `.tldrx/triage/**/split.yml`
1839
+ and a `.agent/**/prompt.md`. The developer prompt inlines every touched path and calls a
1840
+ change outside `touches` a plan deviation, so those three read as permission to rewrite
1841
+ the run's own bookkeeping. Anything with `tldrx-work`, `.tldrx` or `.agent` as a path
1842
+ segment is now dropped from `touches` and recorded in `notes:` as `excluded <path> from
1843
+ touches: tldrx state is never story-writable`. Product documents are untouched.
1844
+ - **A document your answer settles now joins the implicit story's `touches`.** Measured on a
1845
+ real run, 2026-08-30: the run existed to settle six ADRs, the owner answered all six, and
1846
+ the one thing the story could not edit was `ADR-D013-DELIVERY-ZONE-GEOMETRY.md` — the What
1847
+ handoff never cited the file, `touches` is built from what the handoff cites, and the
1848
+ developer prompt says a change outside `touches` is a plan deviation. The plan's own
1849
+ `notes:` said so: "F010 settle no touched document". Build now reads the mapping rule
1850
+ backwards as well — a file whose name carries a decision key (`ADR-D013-*.md`,
1851
+ `decision-7.md`) that a fact of this run names is added, searched for beside the
1852
+ already-touched files first and then across the repo, under the same ≤24 cap — and writes
1853
+ `added <path> to touches: settled by F<n>` into `notes:`. A document no fact names is
1854
+ never added.
1855
+ - **The developer gets the WHOLE answer.** `.tldrx/memory/facts.yml` capped a fact at 300
1856
+ chars and `captureAnswers` writes one as `"<question> — <answer>"`, so on that same run all
1857
+ six answers were cut and four lost the clause naming the ADR they settle — including the
1858
+ words "Accepts ADR-D009 as written." The cap is now 2000 (spec §2.5; the bound only moved
1859
+ outwards, so every facts.yml already on disk stays valid), a cut fact ends in ` …` and
1860
+ carries `truncated: true`, `01-what/questions.md` is a declared input of the implicit story
1861
+ and is inlined into the prompt, and each apply-bullet quotes the full `[Answer]:` text and
1862
+ cites both the fact and the line it came from: `[src: F010; 01-what/questions.md:82]`.
1863
+ - **The implicit story's goal is the work, not the What's stale scoping.** With answered facts
1864
+ the `goal:` list holds nothing but the apply-bullets, and the What handoff's Decisions move
1865
+ to a `context:` list rendered under `## Context (from the What stage)` — after the objective,
1866
+ labelled background, explicitly not a task. Before this, a run opened to get six decisions
1867
+ answered told its developer, as its stated goal, "Out of scope: selecting an answer on the
1868
+ owner's behalf … every relevant ADR is status `proposed`". The plan note now names the facts
1869
+ the story is for (`… applies the run's answered decisions (F005–F010) …`). With no answers
1870
+ nothing moves: the What's decisions are still the goal.
1871
+ - **`tldrx next --prepare --discard-pending` re-derives an implicit plan**, instead of
1872
+ re-rendering the same story. The flag was handled only for stages with no executor, so on
1873
+ Build it did nothing at all: `04-build/implicit-plan.yml` is written once and read forever
1874
+ after, and re-preparing could not pick up a fix. It now bins the bundle's `pending.json`,
1875
+ `result.json` and `result.raw.json`, derives the plan again from the handoff and the answers
1876
+ as they stand, and prepares a fresh bundle — reusing this run's own epic branch and story
1877
+ worktree rather than re-cutting or refusing them. It refuses to rewind a plan something has
1878
+ been built off (recorded evidence, a settled story, or a commit on `story/<run>/S1` beyond
1879
+ the epic) and prints which of those stopped it.
1880
+ - **The dirty-tree check ignores tldrx's own state (`tldrx-work/`, `.tldrx/`).** In a
1881
+ `root_is_repo: true` workspace the framework's state lives INSIDE the product repo, so
1882
+ Build refused the files it had just written itself. Measured 2026-08-30: `tldrx next
1883
+ --prepare 260830-decisions-gate` exited 2 with `repo \`aparece-v2\` has 4 uncommitted
1884
+ change(s) on \`main\``, and all four were tldrx's — `run.yml` and `events.jsonl` (rewritten
1885
+ on every `next`), `.lock` (the run lock) and `04-build/` (the implicit plan written seconds
1886
+ earlier). A user's uncommitted answers under `tldrx-work/` blocked it the same way, though
1887
+ those are committed on the user's cadence, not as a precondition of Build. Product dirt
1888
+ still refuses exactly as before — same message, same fix — and the message now lists only
1889
+ product paths; when the only dirt was state, one line says how many files were excused. A
1890
+ story commit excludes the same two paths by pathspec: a story worktree is a checkout of the
1891
+ same repo, so `git add -A` could otherwise sweep the run folder into the diff a reviewer
1892
+ reads (measured: it did). Multi-repo workspaces, whose state is a sibling of the repos
1893
+ rather than inside them, are untouched.
1894
+ - **A scope that skips the Plan phase can Build.** `docs`, `hotfix`, `performance`,
1895
+ `prototype` and `security-patch` all list `build` in `stages:` and `plan` in `skips:`, and
1896
+ every one of them was a dead end: `stages/build/stage.yml` declares `03-plan/waves.yml` as
1897
+ an input and the executor's first act was to load `03-plan/`, so a real `docs` run parked
1898
+ at `04-build (ready)` could only fail its own Build stage with `03-plan/ does not
1899
+ validate — stories/: the Plan wrote no stories`. Build now writes the one story that
1900
+ decision implies into `04-build/implicit-plan.yml`, deterministically and with no model
1901
+ involved: title from `run.yml`, `goal` from `01-what/handoff.md` § Decisions verbatim
1902
+ (`[src: …]` tokens kept), `acceptance` from `01-what/success-metrics.md`, `touches` from
1903
+ the repo paths that handoff CITES and that exist (≤24, first-cited order, a citation with
1904
+ no repo prefix skipped rather than guessed at), `dod` from the commands `workspace.yml`
1905
+ declares for the roles the scope calls for, and `budget_usd` from the Build stage ceiling.
1906
+ A real `03-plan/` always wins. `tldrx next` prints one line naming the reason, and
1907
+ `tldrx run status` prints `plan: implicit (scope skips Plan)` so a synthesised plan never
1908
+ reads like one a person approved. The plan carries the work **forward**: bullets whose
1909
+ subject is the What stage's own deliverable are dropped on five literal signals
1910
+ (`questions.md`, `### Q`, an `01-what/` path, a question id, the run's-questions
1911
+ vocabulary) with every drop and its signal recorded in the story's `notes:`, every
1912
+ live fact stamped with this run adds
1913
+ `Apply <fact> to the touched files [src: F<n>]` to `goal`, and `acceptance` gains a check
1914
+ that each document one of those facts settles — the fact's text mentions that file's ADR
1915
+ id or decision number — no longer reads `Status: proposed`. A mapping that cannot be
1916
+ derived is reported in the story's `notes:` and falls back to "apply every listed fact;
1917
+ leave a one-line note per file saying which fact changed it", never to a guess. A fact
1918
+ cut at §2.5's 300-char cap is matched against the full `[Answer]:` behind it in
1919
+ `01-what/questions.md`: measured on a real run, `captureAnswers` had sliced the ADR
1920
+ clause off four of six facts, so 2 of 6 mapped on the stored text and 6 of 6 map with
1921
+ the answer. The grep in that criterion is complete or names `notes:` wholesale — a
1922
+ `(+1 more)` inside a command is something a person pastes and reads wrong. The
1923
+ developer prompt states plainly that Plan was skipped and this story applies the run's
1924
+ answered decisions.
1925
+ - **`skips:` in a workflow is read rather than decorative.** The schema declared the key and
1926
+ the loader dropped it, so nothing could tell "the Plan phase has not run yet" from "no Plan
1927
+ phase was ever going to run" — a distinction that cannot be made from disk, since both look
1928
+ like an absent `03-plan/`. `WorkflowPreset.skips` now carries it down to `StageSpec`.
1929
+ - **A DoD command is looked up by its `workspace.yml` KEY, not by matching the command text.**
1930
+ Measured on a real .NET workspace: `lint: dotnet format --verify-no-changes` has no "lint"
1931
+ anywhere in the string, so a text match silently found nothing and would have handed a docs
1932
+ run an empty Definition of Done. `WorkspaceContext.commandRoles` keeps the keys.
1933
+ - Build's declared `03-plan/…` inputs are treated as satisfied when the scope skips Plan, and
1934
+ **only** those: every other missing input is still exit 1.
1935
+
1936
+ ### Verified, not changed
1937
+
1938
+ - **`tldrx expert train` already exits nonzero when a training fails.** The 2026-08-31 batch
1939
+ report measured shell `EXIT=0` on all ten invocations, including the three that failed their
1940
+ check — but nine of those ten ran on a build that predates this one (`dist/tldrx.js` was
1941
+ rewritten mid-batch at 05:40Z). On the current source the code path is intact:
1942
+ `runTraining` returns `EXIT_AGENT_FAILED` (5), `expert train` returns `outcome.code`,
1943
+ `dispatch` returns it, and `bin/tldrx.ts` does `process.exit(await dispatch(...))`. Now pinned
1944
+ by three tests that drive the REAL CLI as a subprocess with a fake `claude` on PATH and assert
1945
+ the PROCESS exit code — one for a file that does not validate, one for a file that was never
1946
+ written, one for the passing case — because "`runTraining` returns 5" and "the process exits
1947
+ 5" are two different claims. Falsified before being trusted: making `expert train` return
1948
+ `EXIT_OK` breaks two of the three.
1949
+
1950
+
1951
+ - **The walk already skips vendored and generated trees**, and always did: `SKIPPED_DIRS`
1952
+ in `detect/walk.ts` covers `node_modules`, `dist`, `build`, `out`, `bin`, `obj`,
1953
+ `target`, `.venv`, `Pods`, `.next`, `.expo`, `coverage` and more, plus every
1954
+ dot-directory below the root, and it is honoured by `walkFiles` (so by `countCodeFiles`
1955
+ and `readSourceTree`) and by `findRepos`. There are now tests that say so: a fixture with
1956
+ a `.ts` file planted in each of those trees, and a real git repo inside `node_modules`
1957
+ that must not be reported as a workspace member. The slow part of `init` was never the
1958
+ walk — it is `graphify update`, once per repo.
1959
+
3
1960
  ## 0.3.0 — 2026-08-30
4
1961
 
5
1962
  Every measurement below was taken on a real workspace on 2026-08-29 unless another date is