tldr-experts 0.3.0 → 0.3.1

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 (32) hide show
  1. package/CHANGELOG.md +994 -0
  2. package/README.md +26 -7
  3. package/dist/hooks/answer-capture.js +20 -11
  4. package/dist/hooks/budget-gate.js +28 -8
  5. package/dist/hooks/chunk-0z27twdk.js +159 -0
  6. package/dist/hooks/{chunk-0bt6yb2q.js → chunk-39zh2e44.js} +10 -1
  7. package/dist/hooks/{chunk-1zwcxd3f.js → chunk-3g61yg59.js} +1 -1
  8. package/dist/hooks/chunk-3t91gvpp.js +193 -0
  9. package/dist/hooks/{chunk-kw4tffzf.js → chunk-458wgg9j.js} +30 -24
  10. package/dist/hooks/{chunk-j234zf0t.js → chunk-9kkm6q0t.js} +4 -165
  11. package/dist/hooks/{chunk-t8tdv11p.js → chunk-afamdvyn.js} +1 -1
  12. package/dist/hooks/{chunk-a8p2rc94.js → chunk-s5qsb4k6.js} +1 -1
  13. package/dist/hooks/{chunk-g395gk7e.js → chunk-sznsenee.js} +1 -1
  14. package/dist/hooks/{chunk-y0jdr3et.js → chunk-xpxe94qe.js} +23 -10
  15. package/dist/hooks/{chunk-azctppjh.js → chunk-ybacnpxd.js} +4 -1
  16. package/dist/hooks/{chunk-x98qs959.js → chunk-ytvmc5ns.js} +19 -22
  17. package/dist/hooks/chunk-zdxgragg.js +1655 -0
  18. package/dist/hooks/{chunk-p274ckxv.js → chunk-ztczwtj0.js} +6 -1
  19. package/dist/hooks/claim-sources.js +9 -9
  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 +12029 -6330
  25. package/package.json +1 -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/templates/budget.yml +5 -0
  30. package/dist/hooks/chunk-ae6bkfs5.js +0 -0
  31. package/dist/hooks/chunk-sdjnnmzz.js +0 -497
  32. package/dist/hooks/chunk-t56k6146.js +0 -14
package/CHANGELOG.md CHANGED
@@ -1,5 +1,999 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 — 2026-08-31
4
+
5
+ **Unattended mode.** Twelve of the entries below are one feature: a run a **host session**
6
+ drives end to end, and a gate an **agent** may close over a check it wrote down. `attended_by: host`
7
+ stops the framework spawning on a run at all; `economy: host-tokens` stops a ceiling that is not
8
+ dollars from buying a metered spawn; the dispatch-notes slot gives the host the one place to add
9
+ what the bundle cannot know; `next --prepare/--commit --review` makes the Build reviewer the
10
+ second delegable role, so one review is done once; the `fixlist` verdict gives a review that
11
+ SIGNS somewhere to put its findings, for one bounded round that costs no attempt;
12
+ `gates_policy: agent` closes a gate on the seven auto conditions **plus** a boundary check, a
13
+ budget-event check and a validated evidence note, and falls through to a person on a question, a
14
+ moved ceiling, work nobody scoped, or its own refusal — rendered as a decision card rather than
15
+ a dashboard. Three smaller pieces stop a turn being wasted before it starts: `preconditions:`
16
+ on a stage, a story branch fast-forwarded onto its epic before dispatch, and a budget brake that
17
+ counts the work that is LEFT rather than the price the stage was written at. Two measurements
18
+ from 2026-08-30 are the whole argument: **$9.95** of spawns that died on caps a Plan agent had
19
+ priced in host tokens, and a framework reviewer that spawned beside a host already reading the
20
+ same diff. New chapter: `docs/guide/10-unattended-mode.md`. Every part is additive — a run with
21
+ none of these behaves byte-identically to the release before them.
22
+
23
+ ### Added
24
+
25
+ - **A story branch that has fallen behind its epic is fast-forwarded before a developer is
26
+ dispatched onto it.** Measured 2026-08-30 on `260830-tenancy-identity-customers`: S3 was
27
+ reopened, `story reopen` keeps its branch by design, and that branch still sat at the S1-era
28
+ epic tip while the epic had since gained S2 and S5. S3's handlers needed S2's contract, so a
29
+ dispatch on that base would not have compiled. The host fast-forwarded by hand before
30
+ dispatching. That is the one case this automates.
31
+
32
+ ```
33
+ · S3: fast-forwarded `story/260830-tenancy/S3` to `epic/tenancy` — 2 commit(s), b5a2474 → ae9c8dd
34
+ ```
35
+ - **Where.** Inside `openStory`, which is the one place a story worktree is opened, and only
36
+ on the two openings that are about to put a DEVELOPER on the branch: the headless pipeline
37
+ and `tldrx next --prepare`. The review openings (`--prepare --review`, `--commit --review`,
38
+ an errored review re-run) and `--commit` measure nothing and move nothing — a fast-forward
39
+ there would drag other stories' commits onto a branch whose whole meaning is "what this
40
+ story built", for a base nobody is about to compile against.
41
+ - **The requeue case, which fires far more often than the reopen one.** A `changes` verdict
42
+ merges the story into its epic and then hands it a second attempt; before this, attempt 2
43
+ was dispatched onto attempt 1's base. It now starts on the current epic tip.
44
+ - **A diverged branch is warned about, never resolved.** Commits on both sides is the second
45
+ live case — a dead spawn's partial commit on a stale base, where no fast-forward existed
46
+ and the host preserved the partial on a backup branch and re-pointed the story branch by
47
+ hand. Which of two histories survives is a decision, so the framework does not make it: it
48
+ names both counts, both shas, and the two options, changes nothing, and lets the dispatch
49
+ proceed on the old base — saying, in as many words, which base that is.
50
+ - **A dirty worktree is left alone**, whatever the topology says. It is the operator's.
51
+ - **Never a rebase.** Rewriting a branch a developer has already committed to is the class of
52
+ move the run-id-in-branch-name fix (2026-08-29 audit §B) exists to prevent. The only write
53
+ is `git merge --ff-only`, which refuses rather than inventing a merge commit. Measured
54
+ 2026-08-31 against a real repository, which is what the design asked to verify before
55
+ building: blocked by a file in the way it exits non-zero and leaves HEAD and the file
56
+ exactly as they were — atomic-or-nothing, so a failed fast-forward needs no repair, only a
57
+ line saying it did not happen.
58
+ - **`story.base_fastforwarded`** joins the closed §2.9 event set — the only event in it that
59
+ records tldrx moving a ref. It carries `story`, `repo`, `branch`, `base`, `from`, `to` and
60
+ `commits`, `tldrx replay` narrates it, and it is appended ONLY when the ref actually moved:
61
+ a divergent or dirty branch produces a warning and no event, because nothing happened.
62
+ - **An up-to-date branch is silent** and emits nothing, so a run with nothing to say about
63
+ its bases is what it was before.
64
+ - `tldrx story reopen` is unchanged: it still runs no git command, spends nothing and touches
65
+ no branch. The detection belongs where a worktree is being opened anyway and where the
66
+ operator is about to dispatch.
67
+
68
+ - **Decision cards — the shape an interrupt takes when a run stops for a person.** Measured
69
+ 2026-08-30: an unattended run stopped on two owner questions, and the host did NOT show the
70
+ owner the dashboard or the `2 open question(s) in 01-what/questions.md` line the framework
71
+ actually prints. It hand-composed, in chat, the question, the options and a recommendation
72
+ with one line of why. The owner answered both in seconds. The card is what made the
73
+ interrupt cheap; hand-composing it is what the framework was making the host pay for.
74
+
75
+ ```
76
+ DECISION — 260830-tenancy · 01-what/what
77
+ Q2 · Should an existing customer's tenant be inferred or asked for?
78
+ Why asked: no tenant column on the customer aggregate [src: absent:api:src/.../Customer.cs]
79
+ A) infer from the invoice email domain — no new UI, wrong for resellers
80
+ B) ask once at first login — one screen, correct for everyone
81
+ C) other — write it below
82
+ Recommends B — one screen, correct for everyone [src: 01-what/handoff.md:22]
83
+ tldrx answer Q2 "…" --run 260830-tenancy
84
+ ```
85
+ - **Pure rendering of things that already exist.** The question, its `Why asked:` line and
86
+ its lettered options come out of `questions.md` through the **§2.7 parser** — the
87
+ questions grammar is not touched, and a block the parser cannot read does not appear on a
88
+ card any more than it appears anywhere else. The `Recommends` line comes out of the
89
+ evidence note's optional `recommend: [{q, option, why, src}]` array, which the evidence
90
+ grammar already validates.
91
+ - **A question with no recommendation gets no line.** Never a manufactured one and never a
92
+ placeholder: the whole value of that line is that an agent stood behind it with a
93
+ citation.
94
+ - **One renderer, three surfaces.** `tldrx run auto --gate-agent` at the stop;
95
+ `tldrx next`'s agent-gate fallthrough, where the card is **appended** to the fallthrough
96
+ list so nothing that reads those lines today loses a byte; and `tldrx status`, where a run
97
+ waiting on answers now shows the card rather than `open questions: Q1, Q2`.
98
+ - **A card per fallthrough kind.** Budget and boundary get their own card over the same
99
+ frame — the measured fact, then the commands (`widen the scope …` / `approve` / `reject`
100
+ for a boundary; the phase's two numbers plus `budget show` for a budget event) — and every
101
+ other reason an agent gate fell through is carried as one gate card naming its reasons.
102
+ - **`--gate-agent` is rendering only.** It does not upgrade any stage to
103
+ `gates_policy: agent`: a run keeps the policy it was opened with, and a flag that could
104
+ raise one at stop time would make the frozen policy decorative. On an
105
+ `attended_by: host` run it changes nothing — `run auto` is still refused at exit `1`
106
+ before the event log is opened, and nothing spawns.
107
+ - **Nothing else moves.** `tldrx answer`, `questions.md`, the live dashboard and every exit
108
+ code are unchanged, and `run auto` without the flag prints exactly the block it always did.
109
+ - **The budget brake counts the work that is LEFT, not the price the stage was written at.**
110
+ Measured 2026-08-31 on `260830-tenancy-identity-customers`: four of seven stories done, one
111
+ mid-attempt-2, two blocked, and the entire remaining metered cost a developer share and a
112
+ reviewer floor — **$2.50**. The brake compared the phase's remaining dollars against
113
+ `stage.budget_usd`, **$18.00**, a number written before a single story ran and never
114
+ revised. It refused the stage twice and the host ran `budget raise --take-from` twice, for
115
+ money nothing was going to spend.
116
+
117
+ For a Build stage with a plan on disk, `tldrx next`'s refusal, the `budget-gate` hook and
118
+ `tldrx budget show`'s `est.` column now all use one figure computed by one function: `Σ`
119
+ over the unsettled stories of the caps the executor would actually hand out — the
120
+ `03-plan/budget.yml` price through the same scale/share arithmetic, the developer and
121
+ reviewer shares, the `$1.00` reviewer floor, and the attempts each story has left.
122
+ - **The refusal shows its arithmetic**: `remaining work: S4 dev $1.50 + reviewer $1.00 =
123
+ $2.50`, under a line naming how many stories are done and what the stage's static
124
+ estimate was. A number an operator cannot take apart is one they cannot argue with, and
125
+ `$18.00` cited nothing.
126
+ - **`blocked` costs $0.00**, and the blocked ids are named rather than quietly dropped: the
127
+ executor dispatches a blocked story only after `tldrx story reopen`, which is a human
128
+ decision and which legitimately raises the figure again.
129
+ - **A story at `review` has already paid the developer turn under review.** Only a `changes`
130
+ verdict buys another one.
131
+ - **Under `economy: host-tokens` the developer turns are $0.00** — the host session pays for
132
+ them — while the reviewer floors stay, because outside attended mode `reviewAndSettle`
133
+ still spawns a metered reviewer and that floor is real money.
134
+ - **It can only NARROW.** The figure is capped at `stage.budget_usd`, so this brake can
135
+ never refuse more often than it did before; the reviewer floor can otherwise lift a naive
136
+ sum past the ceiling. Asserted in both directions, across a spread of plan shapes, because
137
+ a brake that loosened by accident is the failure to fear here.
138
+ - **`budget.blocked` gains `estimate_basis: plan|static`** and, on the plan basis,
139
+ `static_estimate_usd`, `stories_done` and `stories_total`.
140
+ - **With no plan on disk, and outside Build, every path is byte-identical**, wording
141
+ included: the estimate is `budget_usd` and the message still reads `the stage estimate
142
+ is $X`.
143
+
144
+ - **`preconditions:` on a stage — the check that runs before the money does.** A stage may
145
+ declare operational facts that must hold before it is worth dispatching at all:
146
+
147
+ ```yaml
148
+ preconditions:
149
+ - {id: docker, repo: api, command: "docker compose ps", expect_exit: 0}
150
+ ```
151
+
152
+ The grounding is measured, 2026-08-30: before dispatching a Build story the host checked
153
+ the Docker daemon and the .NET SDK **by hand**, because a story has two attempts, an agent
154
+ cannot debug its way out of a daemon that is down, and the whole turn would have been spent
155
+ proving it. That check took about a second and protected an attempt worth dollars.
156
+ - **Same allowlist rule as a `cmd` check and a story's `` ```dod `` block — and now literally the
157
+ same function.** Only a command byte-equal to one `.tldrx/workspace.yml` declares runs,
158
+ argv-split, never through a shell. The comparison and both refusal sentences moved to
159
+ `schemas/commandAllowlist.ts`, so the three sites can no longer drift into three readings
160
+ of one rule. It is enforced **at load**: a stage naming an undeclared command never
161
+ becomes a runnable stage, so `tldrx run new` over it refuses too.
162
+ - **Red ⇒ refused, exit `2`, having spent nothing.** The id and the command's own exit code
163
+ are named, the stage is left exactly where it was (`ready`), no bundle is written and
164
+ nothing is spawned. The list stops at the first red one.
165
+ - **`--prepare` runs them no less than headless** — a bundle written for a host whose Docker
166
+ is down is the same wasted attempt as a spawn into one. `--commit` never runs them: it
167
+ settles a turn that already happened.
168
+ - **Every run is on the record**: one `check.passed` / `check.failed` event with
169
+ `kind: precondition`, carrying the repo, the command, the exit code and the duration, and
170
+ one operator line — `· precondition: docker compose ps → exit 0 (1.2s)`.
171
+ - **A stage that declares none is byte-identical**: no event, no line, no shipped stage file
172
+ changed. `[assumption]` — per stage, not per story; a per-story precondition is a real
173
+ want and is deliberately not designed here.
174
+ - **The `fixlist` verdict, its artifact and its router — the review that SIGNS and still has
175
+ findings.** Measured 2026-08-31, driving `260830-tenancy-identity-customers` by hand: the
176
+ reviewer signed story S5 — every acceptance criterion met, zero scope violations — and in the
177
+ same breath named three real correctness/security defects the criteria never covered (a
178
+ concurrent double-confirm minting two sessions, a non-atomic confirm, a false security comment
179
+ beside a non-constant-time compare). S1 and S3 went the same way that night. Binary
180
+ `approve`/`changes` has nowhere to put those: `approve` throws them away, `changes` spends the
181
+ story's one requeue on a diff nobody faulted. So all three loops were run in chat — number the
182
+ findings, decide fix-now vs defer-with-log, route them to the author, re-verify — and none of
183
+ it reached a file. This is that loop, as a verdict and an artifact.
184
+ - **`fixlist` settles the story at `review` and spends NO attempt.** The requeue counter counts
185
+ verdicts that FAULTED the diff, and a signature is not one. `04-build/fixlist/<story>-<n>.md`
186
+ is written beside it by the EXECUTOR, never by the reviewer — which holds no write tool, the
187
+ same reason the review log is written there. Numbered `## <n> · <finding> [<severity>]`
188
+ sections, each with `Where:`, `Disposition:` and `Resolved:`.
189
+ - **A disposition ROUTES a finding; `Resolved:` CLOSES it.** Two questions, two fields, because
190
+ one field cannot answer the first once the second is true. `fix-now` · `defer-with-log` ·
191
+ `refuted` · `out-of-scope`, and **`refuted` must carry an `[src: …]`** in its `where` or
192
+ `detail`, through the §2.8 grammar and the §2.8 parser — a reviewer's verdict is a claim like
193
+ every other, and that night's host disproved one by grepping both sides before acting on it.
194
+ A fix list with an uncited `refuted` is refused whole and the verdict falls to `changes`.
195
+ - **The router: `tldrx next --prepare --fixlist <path>`.** The open findings land under
196
+ `## Fix list` in the DEVELOPER's prompt, numbered, with their `Do NOT` lines verbatim — a
197
+ bound the reviewer put on a fix is worth as much as the fix. `pending.json` gains
198
+ `fixlist: {path, round, findings, open}` and `resume_session`, the prior turn's `session_id`,
199
+ so the host can resume that sub-agent rather than pay to rebuild its context. **The framework
200
+ resumes nothing itself** — `spawnAgent` has no `--resume` — so the bundle carries the fix list
201
+ and the merged commit and hands the id back to the party that can act on it. Omit the flag and
202
+ the latest still-open round is carried by itself, the same courtesy `--prepare` already
203
+ extends to a story waiting on a review.
204
+ - **One round per story, and the second is refused out loud.** A free round that could be taken
205
+ twice is a story that never has to settle. A second `fixlist` is read as `changes` — which
206
+ costs the attempt the first one did not — the refusal names the round already on disk, and the
207
+ SECOND reviewer's prompt withdraws the verdict rather than offering one the executor would
208
+ then refuse. `story reopen` resets the count with every other one in the review ledger.
209
+ - **A story cannot settle `done` over an open `fix-now`.** An `approve` there settles `blocked`
210
+ and the reason names the file, the finding's number and its heading, plus the two ways to
211
+ close it. The check reads the FILE, not the envelope that produced it: the file is the state,
212
+ and a host closes a finding by writing `Resolved: yes` in it or re-routing its `Disposition:`.
213
+ That edit is the host's — §B.2's third role — because the author works in a story worktree of
214
+ another repo and its own prompt forbids writing outside it.
215
+ - **Fail-closed, unchanged and asserted.** A `verdict: "fixlist"` whose `fixlist[]` is missing,
216
+ empty or unreadable is `changes`, never a free round and never `approve`. Both economies reach
217
+ the same code: the host writes the envelope into the review bundle, or a spawned reviewer
218
+ returns it — `REVIEW_SCHEMA` gained the verdict and the optional array, and `parseReview`
219
+ narrows both.
220
+ - `defer-with-log` findings are appended to `retro.md`'s `## Build feedback` as the artifact is
221
+ written — the existing second writer with its existing verbatim dedup — so a deferred defect
222
+ reaches the owner through a channel that already exists rather than a new one.
223
+
224
+ - **`tldrx next --prepare --review` / `--commit --review` — the reviewer is the second
225
+ delegable role.** A Build story has two sub-agents and only the developer was ever
226
+ delegable; the reviewer was the FRAMEWORK's spawn in both modes, which on a host-driven
227
+ run buys a second reading of a diff the host is already reading, and a bill nobody
228
+ budgeted. Now it rides the same handshake, one directory down:
229
+ `.agent/<stage>/<story>/review/{prompt.md,pending.json,result.json}` — nested so a
230
+ reviewer bundle can never be read as a developer one.
231
+ - **`--prepare --review` writes the bundle and spawns nothing.** `prompt.md` is what a
232
+ spawned reviewer would have been sent, from the same renderer. `pending.json` carries
233
+ `role: reviewer`, `result_schema` (the reviewer's `--json-schema` envelope, verbatim,
234
+ so the host needs no source to know the shape), and a `review:` block with the diff
235
+ command, the merged commit, the attempt and the **DoD results recovered from
236
+ `events.jsonl`** — the DoD is not re-run, and the prompt says so.
237
+ - **`--commit --review` settles it through the existing seam.** The envelope goes through
238
+ the same `parseReview` with the same fail-closed rule (unreadable ⇒ `changes`, never
239
+ `approve`) and the same `reviewAndSettle`: `approve` ⇒ `done` with its evidence,
240
+ `changes` ⇒ one requeue then `blocked`, `MAX_ATTEMPTS` and the requeue counter
241
+ untouched. A host that never writes `result.json` has produced no verdict and spends
242
+ no attempt.
243
+ - **The trail says whose review it was.** No `agent.spawned`; a `task.started` with
244
+ `role: reviewer, mode: prepare` instead. The `check: review` event carries
245
+ `source: host` (written only for a host review, so the spawned path's payload is
246
+ unchanged byte for byte), and the task row is `cost_usd: null, metered: false` unless
247
+ the envelope declares `cost_usd` / `tokens`.
248
+ - **On `attended_by: host` the framework never calls `spawnReviewer` at all.** Half B
249
+ merges the story and hands the review over, so a full attended story cycle emits zero
250
+ `agent.spawned`. Outside attended mode `--review` is opt-in and the headless reviewer
251
+ is unchanged.
252
+
253
+ ### Changed
254
+
255
+ - **`tldrx next --prepare` on a story awaiting review now writes the reviewer bundle
256
+ instead of spawning a reviewer.** It used to spawn a metered one — which is the single
257
+ thing `--prepare` exists not to do. Measured 2026-08-31 on the live
258
+ `260830-tenancy-identity-customers`: story S3's reviewer died at its $1.00 cap, the
259
+ story parked at `review`, and the `--prepare` that was supposed to rescue it spawned a
260
+ replacement that a two-minute host timeout then killed mid-read. The story is still
261
+ parked at `review` afterwards, its attempt still unspent, and the verdict is the host's
262
+ to write. Headless `tldrx next` still re-runs the review by spawning, unchanged.
263
+ - **`tldrx next`'s `attended_by: host` refusal names `--commit --review`** when the stage
264
+ is holding a reviewer bundle. It named `--commit`, which is the wrong half: that door
265
+ reads the DEVELOPER's `result.json` and re-runs a pipeline that has already merged.
266
+ - **`--discard-pending` bins the reviewer bundle too**, alongside the developer bundles it
267
+ already binned — a stale review `result.json` would otherwise be read by the next
268
+ `--commit --review` as a verdict on work it never saw.
269
+
270
+ - **`tldrx init` says what it is doing while it does it** — a live line per step, in the
271
+ same `--ui scene|compact|plain|off` view family as the agent progress view, on stderr.
272
+ It used to print NOTHING until it was finished. Measured 2026-08-30 on a five-repo
273
+ workspace: **36.0 s of total silence** with the default `--provider auto` against
274
+ **1.3 s** with `--provider static` — so ~97% of the wait is `graphify update` running
275
+ once per repo inside `buildMap`, and the command looked hung for all of it.
276
+ - **Ten steps announce themselves**: detecting repos, building the code map, writing
277
+ `workspace.yml`, planning the interview, seeding experts, reading conventions, writing
278
+ `process.yml`/`facts.yml`, `init-questions.md`, `init-handoff.md`, and the `.gitignore`
279
+ + `CLAUDE.md` blocks. `--mcp` adds an eleventh, because it health-checks every server.
280
+ - **The slow ones name the repo they are inside.** `detecting repos` reports each repo
281
+ with its stack, confidence and default branch as detection finishes it; the map step
282
+ reports `<repo> — 6 documents via graphify` per repo. The wait is now legible instead
283
+ of merely long.
284
+ - **A terminal gets a spinner, colour and an in-place rewrite; a pipe gets plain lines.**
285
+ A finished step is printed once and never touched again, so it survives in scrollback
286
+ after the command exits — a step list is a HISTORY, not a picture of a moment, which is
287
+ why this is not `ui/driver.ts` with a different renderer. In `plain` a step still open
288
+ after 5 s says `still <label> — 12 s`, because there is no spinner there to prove the
289
+ process is alive.
290
+ - **A schoolhouse** (`core/ui/campus.ts`) is painted above the steps in `scene` mode,
291
+ drawn in the same hand as the classroom the agent view renders — `init` is the survey
292
+ that happens before the school opens.
293
+ - **`--quiet`** turns the live view off and keeps the report. **`--ui <mode>`** works on
294
+ `init` exactly as it does on `next`, `run auto` and `expert train`, and a bad value is
295
+ a usage error raised before any work is done.
296
+ - **`core/ui/color.ts`** — the framework's first ANSI palette, resolved per STREAM rather
297
+ than per process. `tldrx init > report.txt` on a terminal has a piped stdout and a TTY
298
+ stderr: the live lines are still worth colouring and the file must still be plain text.
299
+ `palette(false)` is the identity for every ink, so a renderer never branches on colour
300
+ and the uncoloured path stays byte-for-byte deterministic in a test. `FORCE_COLOR` beats
301
+ `NO_COLOR` beats `CI` beats the stream.
302
+
303
+ - **The dispatch-notes slot** — `.agent/<stage>/dispatch-notes.md`, and for a Build story
304
+ `.agent/<stage>/<story>/dispatch-notes.md`: the one place a HOST can add context to a
305
+ prompt the framework generated. Measured over one full run of
306
+ `260830-tenancy-identity-customers`, 2026-08-30, EVERY stage needed host-added context the
307
+ bundle lacked — a deferred decision at What, non-inlined seed docs and a staleness warning
308
+ at How, the owner's answers at Plan, "Docker is up" at Build — and the host had exactly two
309
+ places to put any of it, neither of which is one: `stage.md` is the framework's file,
310
+ shared by every run of that workflow, and an edit to `prompt.md` is thrown away by the next
311
+ `--prepare`.
312
+ - **Rendered under `## Dispatch notes`, between `## Inputs` and `## Previous attempt`.**
313
+ Behind the expert blocks on purpose: the slot is the most volatile thing in the document
314
+ — a human writes it between one cycle and the next — and a per-cycle file ahead of the
315
+ largest stable section would pay the cache-WRITE price on every stage. The same position
316
+ in the Build developer prompt, directly under `## Inputs`, because that is where the
317
+ brief ends and `## Investigate` step 1 tells the developer the files above ARE the brief.
318
+ - **Absent ⇒ nothing changes, byte for byte.** No section, no `dispatch_notes` key in
319
+ `pending.json`, `0 B` in the context ledger. Asserted by adding the file, re-preparing,
320
+ removing it, re-preparing, and comparing the two prompts byte for byte.
321
+ - **Capped at 8 KB, and never free.** The stage's file and the story's file feed ONE slot,
322
+ spent stage-file-first, so neither can quietly double the budget; the overflow is named
323
+ in the prompt, on stdout, and in `pending.json`
324
+ (`dispatch_notes: {bytes, truncated, max_bytes, sources[]}`). The rendered section's
325
+ bytes are charged to the context ledger and count against `prompt_max_bytes` like
326
+ everything else — asserted with a ceiling the prompt clears without notes and breaks
327
+ with them. The byte cut never splits a character in half.
328
+ - **Context, never configuration.** The framework does not parse it, does not substitute
329
+ `{{placeholders}}` in it, does not require `[src: …]` tokens on it, and it cannot change
330
+ a declared input, an output, a check or a cap. The section says all of that to the
331
+ sub-agent in its own preamble, because a note that reads like an instruction is otherwise
332
+ indistinguishable from the stage's own rules.
333
+ - **Survives `--discard-pending`.** The flag bins `pending.json`, `result.json` and
334
+ `result.raw.json`; the notes are an INPUT to the rendering that is about to be redone,
335
+ not an output of the one being binned.
336
+ - **Per-cycle scratch, deliberately.** `.agent/` is gitignored, and that is the whole
337
+ point: a caveat that must outlive the cycle is a FACT, and `.tldrx/memory/facts.yml` is
338
+ the durable channel that already reaches every prompt with attribution behind it. Two
339
+ durable channels for the same thing would make neither authoritative.
340
+ - **`attended_by: host` — a run a host session drives, that the framework never spawns on.**
341
+ One optional top-level key in `run.yml`, set at creation with `tldrx run new --attended-by host`
342
+ or flipped later with `tldrx run attend host` / `tldrx run attend --none`. The finding it is
343
+ built for is one sentence of field notes from 2026-08-30: a bare `tldrx next` on a Build stage
344
+ runs the WHOLE remaining headless pipeline — every wave, every story, as paid spawns — when the
345
+ host wanted one re-review. Six of six of those spawns then died on `Reached maximum budget` at
346
+ caps a Plan agent had authored assuming host-billed sub-agents. $9.95, nothing delivered. The
347
+ affordance was missing at the RUN level: `--prepare`/`--commit` is a decision per invocation,
348
+ and nothing on the run could say "this one is being driven by a host session".
349
+ - **A bare `tldrx next` exits `4` and names the exact command** the stage is waiting for —
350
+ `--prepare` when it is ready, `--commit` when a bundle is already out. Four, not two: the run
351
+ is not refusing the work, it is waiting on the host to take a turn, which is the same shape as
352
+ waiting at a gate and the code `run auto` already stops cleanly on. The refusal is the first
353
+ thing in `runStage` — ahead of the budget gate, ahead of reading an input, ahead of assembling
354
+ a prompt — so nothing is billed and nothing is written.
355
+ - **`--dry-run` is refused with it, and the message says why.** `--dry-run` is `mode: headless`:
356
+ it spawns a real sub-agent and the turn is billed, and only the non-handoff FILES are reverted
357
+ afterwards. That is measured, not read — one `agent.spawned`, one `agent.result`, the cost on
358
+ the ledger. The CLI reference said "Spawns nothing, writes nothing" and a comment in
359
+ `next.ts` said the same; both were wrong and both are corrected here.
360
+ - **`tldrx run auto` is refused at exit `1`**, before the event log is opened, so nothing is
361
+ written. A loop whose whole job is calling `next` headless has nothing to do on a run where
362
+ `next` headless is a refusal.
363
+ - **Three layers, because "nothing spawns" is a promise about money.** `runNext` refuses; every
364
+ executor (`build`, `watch`) refuses a headless context with `refused: true`, so the stage goes
365
+ back to `ready` rather than being marked failed; and `spawnAgent` itself throws while an
366
+ attended run is in flight. The third is what makes "no run path can reach a spawn" a property
367
+ rather than a claim about three `if`s — a fourth call site is always one merge away.
368
+ - **`tldrx run attend` is deliberately small**: it sets one field, appends one `run.attended`
369
+ event carrying the new value and the old, and touches no stage, no output, no branch and no
370
+ money. `--none` REMOVES the key rather than blanking it, because `attended_by: null` is not a
371
+ legal value. A direction is required and never guessed (exit `1`); setting what is already set
372
+ appends nothing, since a decision nobody made does not belong in the log; a `done` or
373
+ `cancelled` run is refused (exit `2`).
374
+ - **`tldrx run status` prints `attended: host`** and the status line gains an `att` marker
375
+ beside `auto:N` / `stale:N`, leading them because it is the one that changes what `tldrx next`
376
+ will do. `--json` gains `attended_by`, appended after `unmetered_tasks` so every existing key
377
+ keeps its position.
378
+ - **Additive, and asserted as such.** Absent — which is every run.yml written before this — the
379
+ framework may spawn and every path is what it was: the two-stage headless fixture's event
380
+ sequence is asserted against the one captured from `main` at `dae1d07`, event for event, and
381
+ an ordinary run.yml never mentions the key. A value the reader does not understand is a schema
382
+ error rather than a silent downgrade to "spawn anyway"; `requireKeys` ignores unknown top-level
383
+ keys, so an older binary reading a run.yml with `attended_by` still validates it — but it will
384
+ DROP the key on its next save, since `emitRunYaml` only writes what it knows.
385
+ - Out of scope on purpose: `tldrx expert train` and `tldrx seed triage --propose` spawn outside a
386
+ run and are untouched. `attended_by` is a property of a run.
387
+ - **`economy: metered-usd | host-tokens` on `budget.yml`** — a price gets a currency, and a
388
+ headless spawn under a ceiling that is not money is refused before it spends. Measured
389
+ 2026-08-30 on `260830-tenancy-identity-customers`: the Plan agent priced the run assuming
390
+ HOST-billed sub-agents — turns the host session pays for, which this process never meters
391
+ and which are ~free to the run — and the executor then enforced those figures as dollar
392
+ ceilings on METERED spawns. Six spawns of six died on `Reached maximum budget`, each
393
+ having spent real money to get there: **$9.95**, for nothing. The money model was a single
394
+ scalar with no unit on it and no way to say *"this number is not dollars."*
395
+ - **One optional key, three places**: the run level of `budget.yml`, any `phases[]` entry
396
+ of it (which overrides the run), and the root of `03-plan/budget.yml`, so a Plan agent
397
+ can say which economy it was pricing in. Resolution is phase-then-run.
398
+ - **`tldrx next` refuses a headless spawn on a `host-tokens` phase — exit 2, above prompt
399
+ assembly, before a byte is written or a cent is spent.** The message names the number,
400
+ the unit, and both ways out (`tldrx next --prepare`, or re-label and re-price the
401
+ phase). `--prepare` / `--commit` are untouched: the in-session handshake is exactly
402
+ where a host-billed turn belongs.
403
+ - **The two are never converted into one another.** There is no exchange rate here and
404
+ inventing one would be a guess about a price. The budget-gate hook does not deny on such
405
+ a phase (there is no dollar ceiling to enforce, and it says so on stderr), the auto
406
+ gate's money condition reads `n/a (host-tokens economy)` rather than comparing a spend
407
+ in dollars to a ceiling in tokens, and `tldrx run estimate` prices the stage in TOKENS
408
+ with no dollar figure at all.
409
+ - **A `03-plan/budget.yml` priced in `host-tokens` contributes no story caps.** Its
410
+ numbers are not dollars, so the Build executor falls back to the uniform share it used
411
+ before plan prices were read at all — and says so on stderr, through the advisory seam
412
+ that already existed for an unusable plan budget.
413
+ - **An unknown value is REFUSED, never defaulted to dollars** — a unit nothing here
414
+ understands is not one it may quietly read as money. An empty `economy:` key, and an
415
+ absent one, both mean `metered-usd`.
416
+ - **`tldrx budget raise` no longer erases what it rewrites past.** The label round-trips
417
+ through the same emitter the raise goes through; a raise that dropped it would turn a
418
+ token budget back into dollars silently, from the one command an operator reaches for
419
+ when a ceiling binds.
420
+ - **Absent label ⇒ byte-identical behaviour**, asserted: an unlabelled `budget.yml` emits
421
+ no `economy:` line, an unlabelled headless stage still spawns, and every existing budget
422
+ test passes untouched.
423
+
424
+ - **`tldrx story reopen <id> --note "<why>"`** — one Build story, given another run of
425
+ developer attempts, by a person. The third verb of the family that landed 2026-08-30 and
426
+ the only one a HUMAN signs: the other two stop the machine reading a transport failure
427
+ as a judgement, and this one is for when the machine read the run correctly and the
428
+ owner disagrees. Found on `260830-tenancy-identity-customers`, where story S3 sat
429
+ `blocked` after two GENUINE `changes` verdicts (its developers ran and committed nothing;
430
+ both reviewers correctly refused an empty diff) — no rescue applied and none should have,
431
+ but S3 gates wave 3 and the owner had decided it ships. The only reopening verb was
432
+ `tldrx reject --stage`, which acts on a STAGE, and hand-editing `run.yml` or a story file
433
+ is forbidden by design.
434
+ - **The note is required** — a reopen with no reason is not actionable — and one
435
+ `story.reopened` is appended carrying the actor, the note, the status the story came
436
+ from, its wave, and how many verdicts the closed run of attempts consumed.
437
+ - **The story goes back to `todo` and its attempt counter restarts at 1 of 2.** Nothing
438
+ is erased to make that true: `story.reopened` is a RESET BOUNDARY that
439
+ `readReviewLedger` reads, so verdicts before it stop counting while every event stays
440
+ in `events.jsonl` for `replay`, `cost` and `retro`. The full reset is the honest
441
+ choice precisely because the history survives it — "you get two more turns" is what
442
+ overruling a block means, and a half-reset would be a number nobody could explain from
443
+ the record.
444
+ - **It runs no agent, spends nothing, deletes nothing and refunds nothing.** The story's
445
+ BRANCH is what carries the last developer's commits forward and it is untouched; the
446
+ worktree is left exactly as the build left it (kept at `review`, already removed for a
447
+ `blocked` story) and is reopened from the branch when the next turn needs it.
448
+ - **It does not make the stage runnable, and does not pretend to.** Sending a stage back
449
+ is `reject`'s own signed decision, so the output names the command that fits where the
450
+ Build stage actually is: `tldrx next` when it is ready, `tldrx reject --note` at a
451
+ pending gate, `tldrx reject --stage` over a signed one.
452
+ - Refuses with exit 2, saying why: an id the plan does not have (naming the ones it
453
+ does); a `done` story, because undoing finished work is a decision about the stage and
454
+ belongs to `reject --stage`; a `todo` story, which is already pending; and a missing
455
+ `--note`. An unknown run id is still exit 3.
456
+ - `tldrx replay` narrates it (`story S3 REOPENED by alan — back to \`todo\` from
457
+ \`blocked\` — "…"`), and the Build stage says so in one line, with the note, when it
458
+ picks the story up. A reopened story is byte-identical on disk to a never-started one,
459
+ and a narrative that showed two `changes` verdicts and then a third developer turn with
460
+ nothing in between would read as the framework losing count.
461
+ - **`--parallel <n>` on `tldrx next` and `tldrx run auto`** — how many stories of ONE
462
+ build wave run at once. `waves.yml` already puts every dependency in an earlier wave, so
463
+ a wave's stories are independent by construction. Also settable per scope as
464
+ `build: {parallel: N}` at the top of a workflow, or per stage as `parallel:` in
465
+ `stage.yml`; the flag beats the workflow, which beats the stage file. **The default is 1
466
+ and at 1 the executor takes exactly the path it always did** — verified byte-identical on
467
+ the event sequence against `main`, not asserted. Above 1 the wave runs in two halves:
468
+ developer + DoD + commit concurrently, then merge + reviewer serially in the wave's
469
+ LISTED order, so the epic branch reads the same whatever order the machine finished in.
470
+ The reviewer half is serial for a reason and not only the merge: a reviewer reads
471
+ `git diff <epic>...<story>`, whose merge base moves every time another story merges into
472
+ that epic. A red story does not cancel its siblings, but the wave ends `failed` and the
473
+ next wave does not start. Ctrl-C/SIGTERM kills every live child, not the first.
474
+ Budgets are untouched: the stage ceiling was already divided by
475
+ `stories x attempts x (developer + reviewer)`, so N at once costs what N in a row cost.
476
+ - The live view gives each running story its own column — `S1 reading … · S2 $ dotnet
477
+ test …` — in the scene, the compact one-liner and `--ui plain`. A lane leaves the line
478
+ when its sub-agent finishes. With nothing parallel the view is what it always was.
479
+
480
+ - **`gates_policy: agent` — a gate an agent may close, over a check it wrote down.** The third
481
+ answer to "who closes a gate", beside `human` (waits) and `auto` (the harness signs when seven
482
+ measured conditions hold). Measured 2026-08-30 on `260830-tenancy-identity-customers`: the host
483
+ ran a defined checklist at every gate and typed it into `approve --note "<free text>"`, where
484
+ nothing validated it, `replay` could not render it, and `run.yml` recorded a person's name for a
485
+ check a sub-agent had made. The evidence note (above) was the artefact; this is the gate.
486
+ - **Strictly stronger than an auto gate, never a cheaper one.** Three things, not one: every
487
+ one of the seven `auto` conditions unchanged and unweakened (including the `boundary`
488
+ condition landed alongside it), PLUS no budget decision in this stage's window, PLUS an
489
+ evidence note that parses, sources every bullet, and whose verdict is `sign`.
490
+ - **The budget requirement is an EVENT, not an arithmetic.** A `budget.raised` or
491
+ `budget.blocked` in `events.jsonl` at or after the stage's `started_at` falls the gate to a
492
+ person even when the spend is comfortably under the ceiling. Condition 3 already compares
493
+ numbers; what it cannot see is that somebody *raised* the ceiling to let this stage through,
494
+ and a decision made to unblock a stage may not then be signed off by the machine that was
495
+ blocked. Asserted in both directions on one fixture: the same gate closes without the event
496
+ and falls through with it, with nothing else changed.
497
+ - **The same door, so the trail reads the same.** A closing agent gate goes through `approve`:
498
+ the checks are re-run off disk, `gate.by` records the note's `by:`, one ordinary
499
+ `gate.approved` is appended, and the cursor advances. `AUTO_GATE_ACTOR` is untouched —
500
+ `by: auto` still means "the facilitator closed it with no note but its own conditions".
501
+ - **The note is COPIED into the run tree**, at `<phase>/gate-evidence/<stage>.md`, and that
502
+ copy is what `gate.evidence.path` points at. `.agent/` is gitignored by spec §1, and a gate
503
+ whose evidence lives only in a gitignored directory is a gate nobody can audit from a clone.
504
+ A copy, not a move: the scratch original stays where the agent left it.
505
+ - **Four fallthroughs are named in their own right**, because a person's next move differs for
506
+ each: `questions` (a decision nobody has made), `budget-event` (a ceiling somebody moved),
507
+ `boundary` (work nobody scoped) and `refusal` (the note's verdict is `refuse` or
508
+ `sign-with-fixlist` — the agent doing its job, not failing at it). Any other failing condition
509
+ reports as `condition`, a missing or broken note as `evidence`. Each is tested in isolation:
510
+ a report that fired three at once would not answer "which of these stopped it", which is the
511
+ first question anybody asks.
512
+ - **`tldrx approve --as-agent [--evidence <path>]`** is the same decision taken by hand, and it
513
+ splits the two refusals apart by exit code. **Exit 2** is "this note is broken" — fix the
514
+ file, nothing was signed. **Exit 4** is "a person decides": the note parsed perfectly and its
515
+ verdict is not `sign`. **Exit 1** is `--as-agent` on a stage whose policy is not `agent`: a
516
+ run keeps the policy it was opened with, and a flag that could upgrade one at approve time
517
+ would make the frozen policy decorative.
518
+ - **A person may always overrule it.** A plain `tldrx approve` on an agent-gated stage works
519
+ exactly as it does anywhere else, is recorded as the person, and writes no `evidence` key. An
520
+ agent gate is one an agent MAY close, never one a person may not.
521
+ - **`tldrx replay` renders the check**, not just the signature: who signed, how many files they
522
+ read, how many citations they spot-checked and what those resolved to, how many touched paths
523
+ they audited and how many were outside the surface, and the path to the note. Rendered from
524
+ the note's FRONT MATTER and from `run.yml` — never from its prose, which would change the
525
+ narrative every time somebody rephrased a sentence. A note that has gone missing is SAID to
526
+ be missing rather than invented, and the counts `run.yml` recorded still stand.
527
+ - **`--gates` gains a qualified form**: `--gates plan:agent,build:agent`. A bare entry still
528
+ means `human`, so every invocation anybody has already typed means exactly what it meant. An
529
+ unknown policy is its own usage error, distinct from an unknown stage.
530
+ - **Additive, and asserted as such.** No shipped scope uses `agent`; it arrives via
531
+ `--gates`, or a fork's own workflow file, and never by default. An absent `gates_policy`
532
+ entry still reads as `human`. A gate with no `evidence` emits no key at all, so every
533
+ `run.yml` written before this round-trips byte-for-byte through a save — asserted by
534
+ comparing the emitter's output against the file on disk. The gate mapping never rejected an
535
+ unknown key (measured against the reader that predates this), so a `run.yml` carrying
536
+ `gate.evidence` still validates on an older binary; a `gates_policy` naming `agent` there
537
+ fails loudly instead, which is the right failure — a policy the reader does not understand is
538
+ not one it may downgrade to "sign it anyway".
539
+ - **The emitter had to be extended, not worked around.** `emitRunYaml`'s `gate()` wrote exactly
540
+ five keys as a flow mapping, so a sixth held in memory would have been dropped, silently, by
541
+ the next save. 47 new tests.
542
+
543
+ - **The gate evidence note** — `.agent/<stage>/evidence.md`, plus `tldrx gate template` to
544
+ write the blank form. This is the artefact half of the `agent` gate (design §A): a third
545
+ answer to "who closes a gate", between `human` (waits) and `auto` (the harness signs when
546
+ seven measured conditions hold). Measured 2026-08-30 on
547
+ `260830-tenancy-identity-customers`: the host ran a defined checklist at every gate and
548
+ typed `approve --note "<evidence>"` by hand, into a free-text field where nothing validated
549
+ it and `replay` could not render it. There was no value meaning *"an agent checked it,
550
+ showed its work, and is accountable for the check"*, so there was nowhere to put the check.
551
+ - **Front matter is the machine half, the body is the human half** — the §2.13 story
552
+ pattern, reused rather than reinvented. Required keys: `version gate role by at verdict
553
+ read citations touches diff_vs_stories`; `caveats` and `recommend` are optional and
554
+ default to `[]`. Four H2 sections in order — `Read` · `Citations checked` ·
555
+ `Touches audited` · `Verdict` — each with at least one list item.
556
+ - **Every bullet goes through the EXISTING §2.8 resolver.** Not a second grammar and not a
557
+ second checker: `srcToken.ts` tokenizes and resolves, and the section rule is
558
+ `handoff.ts`'s, lifted into a shared `validateSections` that `validateHandoff` now calls
559
+ too. Two readers of "is this bullet sourced" drift, and the looser one would win the
560
+ argument at exactly the moment a gate is being signed. A checklist whose own claims are
561
+ unsourced is what `claim-sources` exists to refuse, and an evidence note is a claim about
562
+ a claim.
563
+ - **`unverified` REFUSES here, unlike in a handoff.** A citation nothing could check does
564
+ not fail a stage (spec §2.8) but it is precisely what stops an AUTO gate closing (spec
565
+ §5, condition 5). An agent gate is strictly stronger than an auto gate, never a cheaper
566
+ one, so a `doc:` URL nothing in the workspace names cannot be what a signature rests on.
567
+ - **Seven refusals, each with its own message and its own `kind`**, so a caller routes on
568
+ the reason rather than on a string: unreadable or incomplete front matter · a missing
569
+ section or one holding only prose · a bullet with no `src` token or one that does not
570
+ resolve · `sampled > of` or `resolved + refuted > sampled` · `sampled: 0` with citations
571
+ on record · a verdict that is not `sign` · a `gate:` naming a stage other than the one at
572
+ the cursor. `verdict` is the kind that means "a person decides" rather than "this note is
573
+ broken" — `sign-with-fixlist` and `refuse` fall to a human by design, and the verdict
574
+ space is three because a reviewer can meet every acceptance criterion and still have
575
+ found three real defects nobody wrote a criterion for.
576
+ - **`tldrx gate template` fills what a tool can COUNT and leaves every judgement blank**:
577
+ the gate at the cursor, the time, how many citations the §2.8 resolver found across the
578
+ stage's declared outputs (patterns like `03-plan/stories/<id>.md` included), and how many
579
+ touched paths the plan declares. The blank form deliberately does not validate — a
580
+ template that parsed clean out of the box would be a signature nobody had to earn — and
581
+ it writes no `[src: …]` anywhere, the same rule `questions lint --fix` follows. It spends
582
+ nothing, spawns nothing, approves nothing and moves no cursor; an evidence note already on
583
+ disk is left alone (exit `2`) unless `--force` says otherwise.
584
+ - `validateEvidence(text, srcContext, {gate})` is the function `approve --as-agent` will
585
+ call before it records anything. Nothing in this change signs a gate, reads a
586
+ `gates_policy`, or writes into the run tree: the artefact layer lands first, on purpose.
587
+
588
+ - **Auto-gate condition 7, `boundary` — the stage stayed inside the surface the run declared.**
589
+ The other six ask whether the artefact is sound and whether the work finished. None of them
590
+ asks the question a reviewer asks first: *is this the work we scoped?* Measured 2026-08-30
591
+ on `260830-tenancy-identity-customers` — the host ran this check BY HAND at every gate,
592
+ because the framework ran it nowhere ("touches outside What boundary is NOT checked
593
+ anywhere"), and that run's own S3 review surfaced the shape it was worried about: a
594
+ Platform-layer file edited by a module story.
595
+ - **The surface** is the union of every `file:`-kind `[src: …]` citation in
596
+ `01-what/handoff.md` and `02-how/handoff.md`, and every `touches:` entry of every story
597
+ under `03-plan/stories/` — or of `04-build/implicit-plan.yml` when the scope skipped Plan.
598
+ A directory entry covers everything beneath it, which is how a story declares the files it
599
+ is about to create and the forced companions (a lockfile, a generated client) that come
600
+ with them. The citation half reuses `citedRepoPaths`, the derivation the implicit plan
601
+ already builds `touches:` from — the same §2.8 tokenizer, not a second one.
602
+ - **The measurement** is `git diff --name-only <default_branch>...<epic_branch>`, once per
603
+ repo the plan's epics name, through the Build phase's existing git seam. Nothing is
604
+ checked out, fetched or written, and the epic's own `branch:` is what is diffed — the ref
605
+ `openStory` actually cut, not one re-derived here.
606
+ - **Offending paths are NAMED**, up to eight then `+N more`, prefixed with their repo:
607
+ `boundary=2 changed path(s), 1 outside the surface: app:platform/Auth.cs; work outside the
608
+ declared surface is a boundary change — a human decides whether to widen the scope`.
609
+ "1 path outside the surface" is not something anybody can act on.
610
+ - **A human may still approve over it**, and that is the whole point: widening a boundary is
611
+ a decision, and the framework has no basis for making it. Work nobody scoped is often the
612
+ right work.
613
+ - **It never refuses on an absence.** Outside Build, with no epic branch cut yet, with no
614
+ repo on disk, with no plan, or on a run whose What cited no repo path at all, it is `n/a`
615
+ **with the reason in the note** — a condition that could not measure must not report that
616
+ it measured zero. Same shape as condition 6's `n/a (not a build stage)`.
617
+ - **`tldrx-work/`, `.tldrx/` and `.agent/` are excluded from BOTH sides**, through the same
618
+ `isStatePath` filter the implicit-plan derivation already applies. A handoff cites the
619
+ run's own state as evidence, and in a `root_is_repo: true` workspace the state sits inside
620
+ the product repo — neither is a boundary question.
621
+ - **A bare citation widens the surface rather than shrinking it.** `file := [repo ":"] path
622
+ ":" line` makes the repo prefix optional, and `citedRepoPaths` skips the bare form because
623
+ it feeds a developer prompt, where a wrong guess puts another repo's file in front of an
624
+ agent. Here the risk is inverted — an unattributable citation would manufacture a false
625
+ refusal — so a bare path is admitted to every repo's surface. A check that refuses wrongly
626
+ is a check that gets turned off.
627
+
628
+ ### Changed
629
+
630
+ - **The init report is coloured and carries a roll-up.** Repo names, confidence
631
+ (green/yellow/red), the counts and the `created`/`kept` verbs are inked, and a new
632
+ `files N written · N created · N kept` line answers "how much of this run was
633
+ regenerated, how much is new, and how much was mine and left alone" without reading the
634
+ per-file list. `stripAnsi(coloured) === uncoloured`, asserted.
635
+ - `detectWorkspace` and `buildMap` take optional progress callbacks. Both default to
636
+ doing nothing, so `tldrx map` and every other existing caller behave exactly as before.
637
+ - `tldrx init` now writes progress bytes to stderr like every other long-running command,
638
+ so the two `build.test.ts` cases that spawn it to exercise the node seam pass `--quiet`.
639
+ Their `stderr === ""` assertion is how a REAL warning gets noticed, and it still is.
640
+ - **`tldrx cost` is organised by ECONOMY, and prints no grand total.** `STAGE · ECONOMY ·
641
+ MEASURED · DECLARED`, one footer per economy, and a third line for attempts that reported
642
+ neither — no row spans both columns and nothing adds a dollar to a token. A footer that
643
+ printed `$1.70` under a run which had also burned 1.5M host tokens is the sentence the
644
+ label exists to stop. `--json` carries `economy` on every stage row and the set of
645
+ economies on the run. Attempt lines are unchanged, all four token counters included, and a
646
+ declared `--tokens` figure past a million now reads `~1.2M` rather than `~1200.0k`.
647
+
648
+ - `tldrx doctor` prints where the framework's own files are: a `framework <path>` line
649
+ naming the installed package that ships `stages/`, `workflows/` and `templates/`, and
650
+ saying that a project's overrides live in `.tldrx/stages/` and `.tldrx/workflows/`. The
651
+ `/tldrx` skill says the same in two lines. Measured 2026-08-30: a real session spent
652
+ 1m22s on `find / -name build -type d -path "*stages*"` because nothing printed it.
653
+
654
+ ### Fixed
655
+
656
+ - **A rejected knowledge file gets ONE repair round before the money is thrown away.**
657
+ Measured 2026-08-30 on `~/scavtopia`: `tldrx expert train dotnet-stack --area dotnet --mode
658
+ light` spent **$1.69**, the trainer wrote `knowledge/dotnet.md.partial`, and the validator
659
+ refused it for **two** bullets that asserted an execution (`exit 0`) and cited a file line.
660
+ The file went to `dotnet.rejected.md`, nothing reached `competencies.yml`, the status did not
661
+ move — $1.69 for zero evidence, over a mistake the checker could name in one line and the
662
+ writer could have fixed in one edit. `expert train` now hands those exact problems back to the
663
+ same trainer for one more turn before anything is quarantined.
664
+ - **A fresh spawn carrying the ORIGINAL prompt**, not a resumed session: `spawnAgent` has no
665
+ `--resume` and the session id is captured for the ledger only. Appending to the original
666
+ prompt is what keeps the repair possible at all — the citations to be fixed point into files
667
+ that were inlined in that prompt and nowhere else — and the byte-identical prefix reads the
668
+ cache the first turn paid to create. The appended `# REPAIR ROUND` section carries the
669
+ numbered verdict, the rejected file with a line-number gutter whose numbers are the `L<n>`
670
+ numbers in the verdict, and the reminder that deleting an offending bullet is a legal fix.
671
+ - **One round, and the gate does not move.** The repaired file goes through the same
672
+ `parseKnowledgeFile`, same shape, same scope. A second failure rejects exactly as the first
673
+ used to, quarantines the same way, and returns the same exit `5`. An unsourced claim still
674
+ cannot become evidence; it has simply been told once that it is unsourced.
675
+ - **Paid out of `--max-usd`, never on top of it.** The repair turn's ceiling is
676
+ `min(this sub-agent's share, whatever is left of the run's ceiling)`. Below the `$0.25`
677
+ floor it does not spawn and says so — a cold `claude -p` that dies on
678
+ `error_max_budget_usd` before its first reply costs money and produces nothing.
679
+ - **The operator is told while it happens**, so an extra sub-agent never spends silently:
680
+ `repairing: 3 problem(s) sent back to the trainer — one round, $0.31 of the ceiling left`,
681
+ then either `repaired: the second file validates` or `the repaired file does not validate
682
+ either (2 problem(s)) — one round is all there is`. Both turns land in `training.jsonl`; the
683
+ repair as `task: "code:repair"`, `repair: true`, with the number of problems it was sent.
684
+ - Not on `--commit`: there the sub-agent belongs to the host session and this process spawned
685
+ nothing, so repairing is running `--commit` again. Not for a file that was never written —
686
+ there is no verdict to send back.
687
+
688
+ - **The execution-claim rule is now TAUGHT, with an example and a counter-example.** The same
689
+ $1.69 run is the evidence that stating it once in a paragraph does not work. Both training
690
+ prompts — the spawned one and `--print-prompt` — now give the four literal shapes the checker
691
+ looks for (`exit <n>`, `<n>/<n> passed`, `build is green`, and the bare word `measured` in the
692
+ sentence itself), one conforming line, one refused line, and why the refused one is refused: a
693
+ `workspace.yml` line DECLARES a command and is not a record of running it. Two further gaps
694
+ closed: **not making the claim** is named as the other legal answer (the trainer that failed
695
+ had no command in reach of the sentence it was writing, so "run it and cite it" was not
696
+ actionable), and the `(measured)` **annotation** is explicitly exempted — §2.3 asks for it on
697
+ every bullet, `\bmeasured\b` is one of the patterns, and nothing had ever told a writer that
698
+ `claimCheck` strips the annotation before it looks. The runs-mode prompt never stated the rule
699
+ at all, though the validator has always applied it to both files; it does now.
700
+
701
+ - **`duplicate src` is documented as non-fatal, and the rejection report stops implying
702
+ otherwise.** It always was a warning — one call site, one `severity: "warning"`, one validation
703
+ path for both shapes — but the report did not say which lines were fatal and the headline
704
+ counted warnings as problems, so the same $1.69 run read as "3 problems" when only 2 rejected
705
+ it and the third was a duplicate that costs one bullet its evidence row. `describeKnowledgeIssues`
706
+ now lists errors first, prefixes warnings with `warning:` exactly as `knowledgeWarnings` does,
707
+ and the headline counts errors only (new `knowledgeErrors`). The reason it is a warning is
708
+ recorded where the message is emitted: "earns no second row" is a statement about scoring, not
709
+ about honesty, and throwing away every other finding in a file over a repeated citation is not
710
+ rigour.
711
+
712
+ - **A developer that FAILED is no longer recorded as a consumed attempt.** The
713
+ developer-side sibling of the reviewer fix below, found by the same run on 2026-08-30.
714
+ Five developer spawns on `260830-tenancy-identity-customers` died with
715
+ `Reached maximum budget ($0.30 | $0.40 | $0.50 | $0.90 | $1.50)` before delivering
716
+ anything the pipeline could see, and every one was settled as the story `blocked` —
717
+ terminal in-run — so six of seven stories were reported as tried and failed when five of
718
+ them had never been tried. A failed developer spawn now puts the story back at the status
719
+ it held BEFORE the attempt (`todo`, or `review` when a reviewer had asked for changes),
720
+ keeps its worktree, spends no attempt, and stops the in-process loop rather than buying
721
+ the same error twice. Its `check.failed` carries `check: "developer"`,
722
+ `status: "error"` and the error verbatim as `detail`; the review log, the operator line
723
+ and `retro.md` all say the developer **FAILED**, never that anything was reviewed. The
724
+ next `tldrx next` — headless or `--prepare` — offers the story again as a fresh developer
725
+ run at the **same attempt number**. A developer that RAN and produced work its DoD faulted
726
+ is a different thing entirely and still blocks, unchanged, as do two `changes` verdicts.
727
+ - **Runs recorded by the old code pick those stories back up.** A `blocked` story whose last
728
+ attempt recorded no commit, no check of any kind and no reviewer — the only trace the old
729
+ code left, the error itself having gone to `run.yml` alone — is read as the errored spawn
730
+ it was and offered again, with `S2 was blocked by a developer that FAILED (…) — that was
731
+ never an attempt, so it is offered again`. It is paired with the story's own plan, because
732
+ a story with an empty dod block blocks with exactly the same event shape and that block is
733
+ a plan bug. Measured read-only against the live run: `tldrx next --prepare` now offers
734
+ **S2** and, after it, S5, S4, S6 and S7 — while S3, which was blocked by two genuine
735
+ `changes` verdicts, stays blocked.
736
+ - **The auto gate will not sign a Build stage whose stories are not all `done`.** Its five
737
+ conditions were all about the ARTEFACT — citations, questions, money, status — and none of
738
+ them looked at what the stage was for. On the live run all five held while six of seven
739
+ stories sat `blocked` and the epic branch carried one story's work, and the gate signed the
740
+ stage, then signed it again after a human revoked it. A sixth condition now reads the story
741
+ statuses where they live, refuses with `stories=1 of 7 done — S2:blocked, S3:blocked, …`
742
+ and falls through to the human gate. A person may still approve over blocked stories —
743
+ what is worth shipping is their judgement — and outside the Build phase the condition is
744
+ measured as `n/a` and always holds.
745
+ - **A merge that moved nothing is no longer called "merged".** `git merge --no-ff` of a
746
+ branch that is already an ancestor exits 0 and says "Already up to date", and the handoff's
747
+ Gate section rendered that as landed work: on the live run it read
748
+ `(S1, S3, S5, S4, S7 merged)` when the epic tip carried only S1's commits. The executor now
749
+ counts what the merge is about to move BEFORE it moves it — afterwards it cannot, because a
750
+ merged branch is an ancestor either way — and the Gate line, the story Finding and the
751
+ review log all say “added nothing — identical to `epic/x`” for a count of zero.
752
+ - **A reviewer that FAILED is no longer recorded as a reviewer that asked for changes.**
753
+ Found live 2026-08-30 by the first `feature`-scope run to reach Build: the headless
754
+ reviewer of a 39-file, +1879-line story was given $0.26, died mid-read with
755
+ `Reached maximum budget ($0.26)`, and the executor wrote that transport error down as
756
+ `verdict: "changes"`. That single line spent the story's one requeue, sent a fresh
757
+ developer at code nobody had faulted, and would have blocked the story after a second
758
+ reviewer hit the same wall — with **zero review ever performed**. A failed reviewer now
759
+ settles the story at `review` with `verdict: "error"`: the attempt counter is untouched
760
+ (only a real verdict spends it), the `check.failed` event carries the error as its
761
+ `detail` plus `verdict: "error"` so no ledger counts it as changes-requested, and every
762
+ operator-facing line, the review log and `retro.md` all say the reviewer **FAILED**.
763
+ Fail-closed is unchanged — an unfinished review is still never an approval. Inventing the
764
+ verdict is what stopped.
765
+ - **`tldrx next` on a story whose review errored re-runs only the REVIEW.** The diff is
766
+ already committed and merged and its DoD went green, so there is nothing for a developer
767
+ to redo. Both doors do it: the headless path and `tldrx next --prepare`, which used to
768
+ hand the host session a full "attempt 2" developer bundle. The commit and the DoD results
769
+ come back out of `events.jsonl`, so the resumed reviewer sees the same proof the first one
770
+ did — including on the live run, where a `task.started` for the attempt that was never owed
771
+ 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
772
+ one of the framework's own transport errors is read as the failure it was, including a
773
+ story already left at `in_progress` by a wrongly-prepared attempt 2.
774
+ - **The Build executor reads `03-plan/budget.yml`.** The Plan writes a per-story price map,
775
+ the Plan gate validates it, and until now **nothing read it**: the executor split its
776
+ stage into equal shares, so the story priced at $4.75 and the one priced at $0.75 both got
777
+ $1.03. A priced story now gets `price / (attempts x (developer + reviewer))` as its
778
+ developer ceiling and a quarter of that as its reviewer's; an unpriced one keeps the
779
+ uniform share; prices adding up to more than the stage are scaled down proportionally. A
780
+ `budget.yml` that will not parse or validate is an advisory on stderr, never a refused
781
+ build.
782
+ - **A reviewer is never given less than $1.00.** Whatever the arithmetic says, clamped by
783
+ what the stage has left and by `per_agent_max_usd`. A reviewer that cannot finish reading
784
+ the diff approves nothing and blocks nothing — it converts the entire developer turn
785
+ beside it into a story stuck at `review`, which is what $0.26 did on 2026-08-30.
786
+ - **`tldrx cost` no longer prints `0 in · 0 out · 0 cache write · 0 cache read` for a turn
787
+ the host declared tokens for.** `tldrx next --commit --tokens 342527` writes that number
788
+ onto the task row and the `agent.result` payload, and the cost view ignored it. It now
789
+ renders as `~342.5k declared (host session)`, kept apart from the four measured counters
790
+ rather than folded into them: nobody measured those, and four zeroes claim the turn used
791
+ no tokens.
792
+ - **A stage whose declared outputs are a SHAPE no longer fails while the files sit next to
793
+ the error.** Found live 2026-08-30 by the first `feature`-scope run to reach Plan: the
794
+ stage wrote `03-plan/epics/E1.md` and `03-plan/stories/S1.md`..`S7.md`, and
795
+ `tldrx next --commit` refused it with "`03-plan/epics/<epic>.md` was declared as an output
796
+ but does not exist on disk; `03-plan/stories/<id>.md` was declared as an output but does
797
+ not exist on disk". Plan cannot name its outputs — it does not know how many stories there
798
+ will be until it has written them — so `stage.yml` declares the shape, and every
799
+ filesystem call was asking `existsSync` about a path with a literal `<id>` in it.
800
+ A declared path holding an angle-bracket token is now a **pattern**: it matches any file
801
+ in that directory with the pattern's fixed prefix and suffix, resolved against the run dir
802
+ and then the workspace root, in the same "first base wins" order everything else uses.
803
+ The fix is in `paths.ts`, at the seam, not in the validator: `present`/`missing` count a
804
+ pattern by its matches (so a stage taking `stories/<id>.md` as an INPUT gets past the gap
805
+ check), the prompt is handed the concrete files rather than the shape, the previous-attempt
806
+ inline shows every one of them, and `--dry-run` reverts each file it matched and names it.
807
+ `{repo}` expansion is untouched and still runs first, so `{repo}` and a token compose.
808
+ Plain paths behave exactly as before, down to the wording of their failure.
809
+ - A pattern output that matches nothing now fails honestly — "`03-plan/stories/<id>.md` was
810
+ declared as an output but **no file matches it on disk**", rather than claiming a file
811
+ nobody ever named was looked for and not found. Its `sections:` contract binds **every**
812
+ matched file, and the failure names the concrete file that broke it, not the shape.
813
+ - **`tldrx run estimate` prices cache traffic, which is where the money actually goes.**
814
+ Measured 2026-08-30 on a real workspace: a What stage was estimated at **$0.33** and the
815
+ one comparable real attempt cost **$1.70** — **5x**. That attempt's ledger says why: 56
816
+ input · 29.0k output · **166.3k cache write** · **3,747.1k cache read**. The estimate
817
+ multiplied input and output only, so it was adding up the two columns the money was not in.
818
+ Both cache counters had been on every `agent.result` since wave N and `modelPrices.ts` had
819
+ carried the multipliers the whole time — nothing needed new data, only arithmetic that used
820
+ it. The estimate now prices four terms: measured prompt tokens at the input rate, plus the
821
+ **median** cache write (**1.25x** input), cache read (**0.1x** input) and output of past
822
+ attempts at the same stage id, falling back to attempts at any stage and **naming which
823
+ sample it used**. It prints the breakdown —
824
+ `input ~189 · cache write ~166k · cache read ~3,747k · output ~29k → ~$1.46` — and keeps
825
+ saying "ESTIMATE" in words. With no history the old behaviour stands (it refuses to guess
826
+ the output half) and it now says `cache traffic not modelled — first attempt of this kind`
827
+ rather than pricing a silent zero. The input and cache-write terms overlap on a cold first
828
+ turn, so a first attempt leans high; that is stated in the output's own honesty line, not
829
+ corrected away.
830
+ - **`tldrx cost` shows the cache write / cache read columns on every attempt line**, not only
831
+ on stage and run totals, and no longer hides them on a stage that ran once — previously an
832
+ attempt line carried cost, task and model and nothing about where the money went.
833
+
834
+ - **tldrx state survives the project's own `.gitignore` rules, and `doctor` detects a rule
835
+ that shadows it.** Found by a real user 2026-08-30: their repo carried the stock .NET
836
+ `[Ll]og/` ignore, which swallowed `tldrx-work/<run>/04-build/log/S1.md` — the Build phase's
837
+ per-story review log, which spec §1 marks committed and the handoff cites as
838
+ `[src: 04-build/log/<id>.md:1]`. Nothing errored; the file was written, `git status` stayed
839
+ quiet, and a teammate's clone never got it. `init`'s managed block only ever ADDED ignores,
840
+ so any pre-existing project rule (`log/`, `docs/`, a `*.yml` in a subdir) could hide state
841
+ and nothing noticed. The block now opens with `!tldrx-work/`, `!tldrx-work/**`, `!.tldrx/`
842
+ and `!.tldrx/**` — the bare pair and the `**` pair are both needed, because gitignore cannot
843
+ re-include a file whose parent directory is excluded — and the framework's own ignores follow
844
+ AFTER them, since a later pattern wins. Measured with `git check-ignore -v` against a repo
845
+ carrying `[Ll]og/`, not asserted from memory: the story log comes back not-ignored while the
846
+ product's `Logs/build.log`, `tldrx-work/*/.lock` and `.tldrx/cache/` stay ignored. Re-running
847
+ `init` upgrades a block written before this in place, markers and neighbouring rules kept.
848
+ - **`tldrx doctor` now says when a rule outside that block is still hiding state.** It runs
849
+ `git check-ignore --verbose --no-index -z` over four paths that must be tracked — the newest
850
+ run's `run.yml` and `events.jsonl`, a synthetic `04-build/log/` probe, and
851
+ `.tldrx/memory/facts.yml` — and prints each offender with the rule's own `file:line:pattern`,
852
+ so a `.git/info/exclude` or a nested `.gitignore` is named too. A warning: it never moves the
853
+ exit code, which is about the tools this machine has. `--json` gains `gitignoreShadow`, where
854
+ `null` means no workspace was scanned rather than nothing found.
855
+ - **The `## Inputs` preamble no longer claims files the budget dropped.** Measured on a
856
+ real Build prompt, 2026-08-30: 9 of 15 declared inputs were inlined, the other 6 carried
857
+ "It exists on disk; do not guess at its content" — and the preamble above them still read
858
+ "Their full content is inlined below, so there is nothing to open and nothing else to
859
+ find." The two documents the run existed to edit were among the six. The preamble is now
860
+ conditional in every prompt that has one (stage prompts and the developer prompt share one
861
+ renderer): with everything inlined it is the sentence it always was; with anything dropped
862
+ it is `Inlined below: <n> of <m> declared inputs.` followed by "The rest exist on disk —
863
+ READ them at the listed paths before relying on them; do not guess: <list>".
864
+ - **A touched path the story's worktree cannot read is flagged as such.** The developer works
865
+ in a worktree of the story branch, so a path that exists in the repo but is not committed
866
+ at that branch is unreadable there — and `existsSync(worktree/path)` called it a file the
867
+ story creates. Build now asks git (`git cat-file -e <branch>:<path>`) and marks it `NOT in
868
+ this worktree — its content is only what the handoff quotes`, plus one stderr line per
869
+ path: `warning: input <path> is not committed, so the story worktree cannot read it`. A
870
+ path that exists nowhere is still "does not exist yet — this story creates it".
871
+ - **The story's own goal wins the developer prompt's inline budget.** `touches` was spent in
872
+ list order, so on that same run `AGENTS.md` — cited once in passing — was inlined whole
873
+ and the two documents the goal named were in the dropped tail. Touched paths the story's
874
+ `goal`, acceptance criteria, test plan or title NAME now sort first into the 64 KB; a brief
875
+ that names nothing changes no order at all.
876
+ - **The developer is told to run an acceptance criterion's embedded pattern BEFORE it
877
+ edits.** Found on a real run's second Build of 2026-08-30: a derived criterion carried a
878
+ literal grep (`` Pending `DECISIONS-NEEDED.md` # ``, backticks included) and the markers
879
+ it was meant to count had been written three different ways, so it reported 0 against two
880
+ files that still held five real markers — the in-session driver only caught it by
881
+ measuring the inventory by hand. The developer prompt's `## Investigate` list now carries
882
+ the rule verbatim: validate the pattern against the current tree first; a criterion that
883
+ reports zero while the goal says the work exists is broken, so measure the real inventory,
884
+ use THAT as the completion test, and record the discrepancy in the handoff. The criterion
885
+ text itself stays data the story may not edit. `stages/build/stage.md` says so too.
886
+ - **The implicit story no longer `touches` tldrx's own state.** `touches` is derived from
887
+ every repo path the What handoff cites, and a handoff cites state as evidence: measured
888
+ 2026-08-30, 13 touched paths of which three were `run.yml`, a `.tldrx/triage/**/split.yml`
889
+ and a `.agent/**/prompt.md`. The developer prompt inlines every touched path and calls a
890
+ change outside `touches` a plan deviation, so those three read as permission to rewrite
891
+ the run's own bookkeeping. Anything with `tldrx-work`, `.tldrx` or `.agent` as a path
892
+ segment is now dropped from `touches` and recorded in `notes:` as `excluded <path> from
893
+ touches: tldrx state is never story-writable`. Product documents are untouched.
894
+ - **A document your answer settles now joins the implicit story's `touches`.** Measured on a
895
+ real run, 2026-08-30: the run existed to settle six ADRs, the owner answered all six, and
896
+ the one thing the story could not edit was `ADR-D013-DELIVERY-ZONE-GEOMETRY.md` — the What
897
+ handoff never cited the file, `touches` is built from what the handoff cites, and the
898
+ developer prompt says a change outside `touches` is a plan deviation. The plan's own
899
+ `notes:` said so: "F010 settle no touched document". Build now reads the mapping rule
900
+ backwards as well — a file whose name carries a decision key (`ADR-D013-*.md`,
901
+ `decision-7.md`) that a fact of this run names is added, searched for beside the
902
+ already-touched files first and then across the repo, under the same ≤24 cap — and writes
903
+ `added <path> to touches: settled by F<n>` into `notes:`. A document no fact names is
904
+ never added.
905
+ - **The developer gets the WHOLE answer.** `.tldrx/memory/facts.yml` capped a fact at 300
906
+ chars and `captureAnswers` writes one as `"<question> — <answer>"`, so on that same run all
907
+ six answers were cut and four lost the clause naming the ADR they settle — including the
908
+ words "Accepts ADR-D009 as written." The cap is now 2000 (spec §2.5; the bound only moved
909
+ outwards, so every facts.yml already on disk stays valid), a cut fact ends in ` …` and
910
+ carries `truncated: true`, `01-what/questions.md` is a declared input of the implicit story
911
+ and is inlined into the prompt, and each apply-bullet quotes the full `[Answer]:` text and
912
+ cites both the fact and the line it came from: `[src: F010; 01-what/questions.md:82]`.
913
+ - **The implicit story's goal is the work, not the What's stale scoping.** With answered facts
914
+ the `goal:` list holds nothing but the apply-bullets, and the What handoff's Decisions move
915
+ to a `context:` list rendered under `## Context (from the What stage)` — after the objective,
916
+ labelled background, explicitly not a task. Before this, a run opened to get six decisions
917
+ answered told its developer, as its stated goal, "Out of scope: selecting an answer on the
918
+ owner's behalf … every relevant ADR is status `proposed`". The plan note now names the facts
919
+ the story is for (`… applies the run's answered decisions (F005–F010) …`). With no answers
920
+ nothing moves: the What's decisions are still the goal.
921
+ - **`tldrx next --prepare --discard-pending` re-derives an implicit plan**, instead of
922
+ re-rendering the same story. The flag was handled only for stages with no executor, so on
923
+ Build it did nothing at all: `04-build/implicit-plan.yml` is written once and read forever
924
+ after, and re-preparing could not pick up a fix. It now bins the bundle's `pending.json`,
925
+ `result.json` and `result.raw.json`, derives the plan again from the handoff and the answers
926
+ as they stand, and prepares a fresh bundle — reusing this run's own epic branch and story
927
+ worktree rather than re-cutting or refusing them. It refuses to rewind a plan something has
928
+ been built off (recorded evidence, a settled story, or a commit on `story/<run>/S1` beyond
929
+ the epic) and prints which of those stopped it.
930
+ - **The dirty-tree check ignores tldrx's own state (`tldrx-work/`, `.tldrx/`).** In a
931
+ `root_is_repo: true` workspace the framework's state lives INSIDE the product repo, so
932
+ Build refused the files it had just written itself. Measured 2026-08-30: `tldrx next
933
+ --prepare 260830-decisions-gate` exited 2 with `repo \`aparece-v2\` has 4 uncommitted
934
+ change(s) on \`main\``, and all four were tldrx's — `run.yml` and `events.jsonl` (rewritten
935
+ on every `next`), `.lock` (the run lock) and `04-build/` (the implicit plan written seconds
936
+ earlier). A user's uncommitted answers under `tldrx-work/` blocked it the same way, though
937
+ those are committed on the user's cadence, not as a precondition of Build. Product dirt
938
+ still refuses exactly as before — same message, same fix — and the message now lists only
939
+ product paths; when the only dirt was state, one line says how many files were excused. A
940
+ story commit excludes the same two paths by pathspec: a story worktree is a checkout of the
941
+ same repo, so `git add -A` could otherwise sweep the run folder into the diff a reviewer
942
+ reads (measured: it did). Multi-repo workspaces, whose state is a sibling of the repos
943
+ rather than inside them, are untouched.
944
+ - **A scope that skips the Plan phase can Build.** `docs`, `hotfix`, `performance`,
945
+ `prototype` and `security-patch` all list `build` in `stages:` and `plan` in `skips:`, and
946
+ every one of them was a dead end: `stages/build/stage.yml` declares `03-plan/waves.yml` as
947
+ an input and the executor's first act was to load `03-plan/`, so a real `docs` run parked
948
+ at `04-build (ready)` could only fail its own Build stage with `03-plan/ does not
949
+ validate — stories/: the Plan wrote no stories`. Build now writes the one story that
950
+ decision implies into `04-build/implicit-plan.yml`, deterministically and with no model
951
+ involved: title from `run.yml`, `goal` from `01-what/handoff.md` § Decisions verbatim
952
+ (`[src: …]` tokens kept), `acceptance` from `01-what/success-metrics.md`, `touches` from
953
+ the repo paths that handoff CITES and that exist (≤24, first-cited order, a citation with
954
+ no repo prefix skipped rather than guessed at), `dod` from the commands `workspace.yml`
955
+ declares for the roles the scope calls for, and `budget_usd` from the Build stage ceiling.
956
+ A real `03-plan/` always wins. `tldrx next` prints one line naming the reason, and
957
+ `tldrx run status` prints `plan: implicit (scope skips Plan)` so a synthesised plan never
958
+ reads like one a person approved. The plan carries the work **forward**: bullets whose
959
+ subject is the What stage's own deliverable are dropped on five literal signals
960
+ (`questions.md`, `### Q`, an `01-what/` path, a question id, the run's-questions
961
+ vocabulary) with every drop and its signal recorded in the story's `notes:`, every
962
+ live fact stamped with this run adds
963
+ `Apply <fact> to the touched files [src: F<n>]` to `goal`, and `acceptance` gains a check
964
+ that each document one of those facts settles — the fact's text mentions that file's ADR
965
+ id or decision number — no longer reads `Status: proposed`. A mapping that cannot be
966
+ derived is reported in the story's `notes:` and falls back to "apply every listed fact;
967
+ leave a one-line note per file saying which fact changed it", never to a guess. A fact
968
+ cut at §2.5's 300-char cap is matched against the full `[Answer]:` behind it in
969
+ `01-what/questions.md`: measured on a real run, `captureAnswers` had sliced the ADR
970
+ clause off four of six facts, so 2 of 6 mapped on the stored text and 6 of 6 map with
971
+ the answer. The grep in that criterion is complete or names `notes:` wholesale — a
972
+ `(+1 more)` inside a command is something a person pastes and reads wrong. The
973
+ developer prompt states plainly that Plan was skipped and this story applies the run's
974
+ answered decisions.
975
+ - **`skips:` in a workflow is read rather than decorative.** The schema declared the key and
976
+ the loader dropped it, so nothing could tell "the Plan phase has not run yet" from "no Plan
977
+ phase was ever going to run" — a distinction that cannot be made from disk, since both look
978
+ like an absent `03-plan/`. `WorkflowPreset.skips` now carries it down to `StageSpec`.
979
+ - **A DoD command is looked up by its `workspace.yml` KEY, not by matching the command text.**
980
+ Measured on a real .NET workspace: `lint: dotnet format --verify-no-changes` has no "lint"
981
+ anywhere in the string, so a text match silently found nothing and would have handed a docs
982
+ run an empty Definition of Done. `WorkspaceContext.commandRoles` keeps the keys.
983
+ - Build's declared `03-plan/…` inputs are treated as satisfied when the scope skips Plan, and
984
+ **only** those: every other missing input is still exit 1.
985
+
986
+ ### Verified, not changed
987
+
988
+ - **The walk already skips vendored and generated trees**, and always did: `SKIPPED_DIRS`
989
+ in `detect/walk.ts` covers `node_modules`, `dist`, `build`, `out`, `bin`, `obj`,
990
+ `target`, `.venv`, `Pods`, `.next`, `.expo`, `coverage` and more, plus every
991
+ dot-directory below the root, and it is honoured by `walkFiles` (so by `countCodeFiles`
992
+ and `readSourceTree`) and by `findRepos`. There are now tests that say so: a fixture with
993
+ a `.ts` file planted in each of those trees, and a real git repo inside `node_modules`
994
+ that must not be reported as a workspace member. The slow part of `init` was never the
995
+ walk — it is `graphify update`, once per repo.
996
+
3
997
  ## 0.3.0 — 2026-08-30
4
998
 
5
999
  Every measurement below was taken on a real workspace on 2026-08-29 unless another date is