tldr-experts 0.17.0 → 0.18.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,464 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.1 — 2026-09-13
4
+
5
+ ### Added
6
+
7
+ - **Per-story scoped checks, and the full suite once on the epic head (#257).** Measured on a
8
+ field run at 0.16.1: 99 `check.*` events on one 8-story epic — every story's Definition of Done
9
+ ran the whole declared list, the full suite among it, on every attempt and every fix round, and
10
+ the epic head that actually ships was never run at all. The DoD is a delta gate ("this story did
11
+ not break the tree"), proven until now by running everything because nothing narrower existed.
12
+ `workspace.yml` grows `commands.<slot>_scoped: "<cmd> {{paths}}"`, a template beside a declared
13
+ slot: a story's dod line whose command has one runs the template over the story's own paths —
14
+ declared `touches:` ∪ its committed diff ∪ its dirty entries, existing paths only, one derivation
15
+ in `build/scopedPaths.ts` — substituted at the argv level (a path with a space is ONE argument,
16
+ no shell), and the row says `scope: "paths"` with the paths. The moment the epic flips to `done`,
17
+ the deduped full commands of its stories run once in the epic worktree, after `install:` if
18
+ declared, as `scope: "full"` rows on the epic's `lane` attributed to the last story merged; a red
19
+ there blocks that story, so the `stories` gate condition refuses. The template is never a
20
+ command — not in the allowlist, not citable, not handed to the developer, refused at Plan time
21
+ by a sentence naming the suffix; the story's evidence keeps citing the DECLARED command and the
22
+ row's prose names what ran. A repo that declares no template runs, records and cites exactly
23
+ what it did before: the golden is byte-identical, and a `scope` key emitted unconditionally
24
+ reddens it. Pinned by a two-story fake-agent build — exactly 2 `scope: paths` rows with the
25
+ right paths and 1 `scope: full` row with `lane`, a red epic head blocking S2 with the gate
26
+ counting 1 blocked, and a control with no template emitting no `scope` key at all; skipping the
27
+ epic-head run or substituting nothing each reddens exactly one of those.
28
+
29
+ ### Fixed
30
+
31
+ - **A cancelled run's epic branch no longer blocks the next run for the same feature: `run cancel`
32
+ releases it, and Build moves a finished run's leftover aside instead of refusing it as someone
33
+ else's (#272).** Measured 2026-09-13 on a headless proof run at a509dca: run A had cut
34
+ `epic/main-ci-green` and was cancelled; the ordinary retry after a cancelled attempt — same
35
+ feature, id `…-2` — reached Build and was refused, "already exists … did not cut it — refusing to
36
+ stack this run's commits onto someone else's epic", relaunched once under `--until-done`, got the
37
+ same last line and stopped as #252 designed. $3.70 of what/how/plan died on a leftover with ZERO
38
+ commits beyond `main` — the only work of the cancelled run sat on its story branch (#129), which
39
+ nothing touches. Two halves, one rule. `tldrx run cancel` now releases every branch its
40
+ `build.epic_branch` claims, after `closeRun` has taken the run's epic worktrees back: no commit
41
+ beyond the repo's default branch → deleted (it was the base under another name); commits → renamed
42
+ to `epic/<slug>@<run-id>` (one derivation, `build/epicRelease.ts`) so they survive under a name
43
+ that says whose they were; checked out somewhere → left alone and the checkout named; a base git
44
+ cannot count against → left alone, because a miscounted zero there deletes commits. And Build, at
45
+ the refusal, reads WHO owns the branch from the claims under `tldrx-work/`: an owner whose
46
+ `run.yml` is explicitly `cancelled` or `done` — never "no process", since a run killed mid-Build or
47
+ parked at a gate has no process and is still the owner — left a leftover, and Build moves it
48
+ aside by the same rule and cuts its own; an open owner, an unreadable owner, and a branch NO run
49
+ claims keep the refusal verbatim with one line naming which — the unclaimed case is what #262's
50
+ killed-mid-cut run leaves, and its relaunch must find its epic where it left it. Every move is
51
+ written where a person reads it, not only in the branch list: the owner's run.yml
52
+ (`build.epic_released`, additive), its `04-build/handoff.md` when it wrote one, an `epic.released`
53
+ event on its ledger and — from Build — on the new run's ledger and in its handoff's Decisions.
54
+ Pinned against a real repo: delete / rename-with-commits-surviving / worktree-kept for the cancel
55
+ half, and owner-cancelled → renamed + fresh epic + both records, owner-open → verbatim exit 2,
56
+ no-claim → verbatim exit 2 for the Build half; the golden build is byte-identical.
57
+ - **A permission refusal on a story whose tree holds work no longer blocks it before the
58
+ facilitator's own Definition of Done can measure it, and the developer is told why a plumbed
59
+ DoD line is refused (#271).** Measured 2026-09-12 on a headless `run auto --until-done` at
60
+ 0.18.0: the developer finished, committed, and then ran the workspace's DoD command wrapped in
61
+ `> log 2>&1; echo EXIT:$? >> log; tail …` — a line the agent's permission layer splits at every
62
+ separator and refuses because the fragments do not each match a grant (#215). #261's block did
63
+ exactly what it says: `blocked: permission — <that line>` on the gate, one attempt spent,
64
+ `--until-done` correctly parked. And it was the wrong verdict, because the refusal was the
65
+ developer's OWN attempt at verification, the work was already in the tree, and the facilitator
66
+ re-runs the DoD one step later regardless — $8.12 to reach a committed story nothing measured.
67
+ The refusal now stops a story only when the tree holds NO work. "Work" is `workSince`, one
68
+ derivation against the tree the developer was handed: the branch tip's tree differs from it
69
+ (`git diff --quiet`), OR the working copy is dirty (`git status --porcelain` semantics —
70
+ untracked files count, git-ignored files do not), both outside `tldrx-work/` and `.tldrx/`,
71
+ which a `root_is_repo` worktree carries and the framework writes during the turn. Uncommitted
72
+ work counts on purpose: the normal path already takes a dirty tree into the DoD and commits it
73
+ afterwards (`runDod` runs before `commitIfDirty`), and the incident's likeliest shape is a
74
+ developer refused while VERIFYING that never reaches its commit. An empty commit, an untouched
75
+ tree, a turn that wrote only framework state and a turn that wrote only an ignored file all
76
+ still block with #261's exact wording after one attempt. With work the refusal is RECORDED and
77
+ the Definition of Done decides: green, and the story goes on to review with the refused command
78
+ named on its review log (`- Developer: … was refused for approval …`), on `task.done` as an
79
+ additive `permission_refused`, and in the handoff's `## Unknowns`; red, and the story blocks
80
+ with BOTH reasons on one row, the DoD's first. Not blocking never means not recording, and a
81
+ refusal is its own story's and its own attempt's only. The spawned developer prompt also gains
82
+ one rule beside "Done means proven": run each DoD command verbatim and alone, with the
83
+ mechanism stated — `>`, `>>`, `2>&1`, `<`, `|`, `;`, `&&`, `||`, `&`, `$()` split a line into
84
+ subcommands and each must match its own grant, so a compound line is refused even when the
85
+ script is allowed — and that the facilitator re-runs the DoD anyway. That sentence moves the
86
+ frozen developer prompt in `test/build-golden.test.ts` by exactly those lines in six prompt
87
+ files; events, `run.yml` rows and exit codes are byte-identical. Pinned by nine tests: refusal
88
+ after a commit + green DoD → `check.*` ran, `done`, recorded on all three surfaces; uncommitted
89
+ work + green DoD → measured, committed by the facilitator, landed; + red DoD → `blocked` with
90
+ both reasons; empty commit, state-dir-only and ignored-file-only → `blocked: permission — …`
91
+ with no DoD spent, one spawn; two stories with the refusal on one → the other's record clean;
92
+ refused on attempt 1 and clean on attempt 2 → attempt 2's record clean; and the prompt sentence
93
+ once, in `## Rules`. Mutations, each measured: dropping the comparison (every refusal
94
+ continues) reddens #261's own test and the empty-commit, state-only and ignored-only cases; a
95
+ `headSha !== since` proxy reddens the uncommitted and empty-commit cases; dropping the dirty
96
+ half reddens the uncommitted case; dropping the state-dir exclusions reddens state-only;
97
+ counting ignored files (`git status --ignored`) reddens state-only and ignored-only; removing
98
+ the per-attempt reset reddens the attempt-2 case.
99
+ - **The headless refusal under a `host-tokens` ceiling now writes its `budget.blocked` — it was
100
+ the one money-family exit 2 the ledger could not see (#266).** Measured 2026-09-12 on
101
+ `runNext.ts`: `budgetRefusal` and `hostTokensNote` both append a `budget.blocked` before
102
+ returning exit 2; `economyRefusal` — the §E.2 gate that refuses a headless spawn on a phase
103
+ priced in host tokens — returned the same exit with only its CLI lines and no
104
+ `store.append` at all. The `--until-done` supervisor (#252) keys its money guard on a
105
+ `budget.blocked` among the attempt's fresh events, so a refusal that wrote nothing read as
106
+ "a refusal with no money behind it" and was relaunched into the same refusal — harmless to
107
+ the wallet, since the gate fires before any spawn, but a relaunch that could never do
108
+ anything, and a run whose own audit trail never states why it stopped (the same
109
+ blind-instrument shape as #248). The gate now appends one row of the `hostTokensNote` shape
110
+ (`phase`, `economy: host-tokens`, `ceiling_tokens`, a `reason` naming the headless
111
+ invocation) before the exit; the refusal text is byte-for-byte what it was. Pinned by a
112
+ headless run on a host-tokens phase → exit 2 AND exactly one `budget.blocked` on the ledger,
113
+ with `stage.started` still at zero (the row records a refusal, not a stage); removing the
114
+ append reddens exactly that test.
115
+ - **A Plan that priced its stories in a shape nothing reads is refused at the Plan gate, and the
116
+ Plan agent is now told the shape (#264).** Measured on a field run: `03-plan/budget.yml` carried
117
+ `ceiling_usd: 108`, a `stories:` list with an `estimate_usd` and a `why` per story, and
118
+ `total_estimate_usd: 111` — priced, sourced, and unreadable. The only reader, `loadPlanPrices`,
119
+ accepts `validateBudget`'s shape (`run`, `ceiling_usd`, `spent_usd`, `per_phase_usd`) and prices
120
+ only the `per_phase_usd` keys that are story ids, so the file failed validation before the map was
121
+ ever looked for and every story got the uniform cap: $5.40 for the story the plan had priced at
122
+ $28, about a quarter of what the reader's own arithmetic would have handed it. Nothing could have
123
+ gone otherwise — the Plan prompt named `budget.yml` as a filename and never its shape,
124
+ `validatePlan` had zero references to the file, and the one string that named the problem
125
+ (`priceIssue`) was computed at Build time and read by nothing. Two halves. The `## Output
126
+ schemas` section the Plan agent reads carries a fourth artefact, GENERATED like the other three:
127
+ its rule table from `BUDGET_REQUIRED_KEYS`, the list `validateBudget` itself enforces, and its
128
+ example run through that validator by the test that pins the contract — and `tldrx plan schema
129
+ --budget` prints it for a person. And the `plan` check runs `validateBudget` over
130
+ `03-plan/budget.yml` at the gate when the file exists, naming the file and every missing key
131
+ (`budget.yml spent_usd: missing required key \`spent_usd\``), so the shape is refused where it
132
+ costs nothing instead of pricing nothing one stage later. The file stays optional; only its shape
133
+ is not. The Build loader is untouched on purpose: it refuses to load on any `validatePlan` issue,
134
+ and the spec's rule for an invalid budget at Build time — an advisory and the uniform split, never
135
+ a refused build — is what a file edited after its gate still gets, which is why the budget check
136
+ is the gate's own function and not a fourth pass inside `validatePlan` (a guard test pins that
137
+ `validatePlan` does not learn it).
138
+
139
+ ## 0.18.0 — 2026-09-13
140
+
141
+ ### Added
142
+
143
+ - **A run can now end in a pull request toward main, on purpose (#253).** Measured on two
144
+ workspaces on 0.16.1: $89.82 over 28 h and $26.38 over 5.6 h, and neither run could end in a
145
+ PR whatever it spent — every preset ends at `watch`, and `tldrx ship` refused to push the
146
+ branch it opens the PR for, citing a spec sentence that is not in the spec (what
147
+ `docs/spec.md` actually says is that the BUILD phase has no push wrapper). The last mile —
148
+ the only one the owner's sentence names, *"desde que le doy el run, hasta que me entrega un
149
+ PR hacia main"* — was a person typing `git push -u origin epic/<slug>`, then `tldrx ship`,
150
+ then watching the checks. It is now a decision taken once, at `tldrx run new --ship
151
+ <push|pr|merge>`, and frozen into `run.yml` as an additive `ship: {push, pr, auto_merge}`
152
+ block the way `gates_policy` is: not a sixth phase, because shipping has none of a phase's
153
+ properties — it spends nothing, produces a URL, and is gated by what every earlier gate
154
+ already signed. When `run auto` sees the run close it runs the same `tldrx ship` a person
155
+ would type: the epic is pushed through the ONE push wrapper in the codebase (in
156
+ `core/build/git.ts`, beside Build's git seam, with `core/run/ship.ts` pinned as its single
157
+ caller — the Build phase itself still has none), the PR opens with the body it always had,
158
+ and under `merge` the PR is armed with `gh pr merge --auto --merge` so the remote's own
159
+ checks decide. A PR that reports NO check is left open and the record says `merge: absent —
160
+ no checks to wait on` (§7), because GitHub's auto-merge over nothing to wait on is a merge
161
+ now, not a merge when green — the mutation that drops that guard reddens exactly one test.
162
+ `run.yml` gets one record (`pr_urls`, `merge`, per-repo `merges`, `shipped_at`) beside the
163
+ policy, so a loop re-run on a closed run ships nothing twice — while `tldrx ship` typed again
164
+ is the recovery after a partial failure: it re-arms the repo whose merge failed, leaves a
165
+ queued one alone, and never erases a recorded failure (pre-merge review caught the first
166
+ version overwriting it with an empty string and exit 0) — and `run.finished` carries `pr_url`
167
+ and `merge`.
168
+ A ship that is refused is the loop's exit 2 — it was asked for a PR and did not deliver one.
169
+ Absent the flag every run means what it meant: nothing pushed, nothing opened, the refusal
170
+ that names the `git push` command unchanged byte for byte. Every gate is still signed by
171
+ whoever `--gates` says, so an unattended ship is a deliberate `--gates none --ship merge`
172
+ and `run.yml` records both. Found on the way: `run.yml` is emitted key by key, so the
173
+ block was dropped by the first save until the emitter learned it — pinned by a round-trip
174
+ test.
175
+ - **`questions_policy: recommended` — the loop answers a question that carries its own
176
+ `Recommended:` line, and escalates only the ones that do not (#251).** Measured 2026-09-12 on
177
+ two headless runs at 0.16.1, every gate `auto`, `--wait-answers 8h --wait-gates 8h`: $89.82
178
+ and 28 h wall on one, $26.38 and 5.6 h on the other, the agents busy 4.9 h and 56 min of it,
179
+ and **10 owner questions, 9 carrying a `Recommended:` line the asking agent had written**. The
180
+ loop parked on every one, sent `question.raised`, and waited for a person to type
181
+ `tldrx answer` — over a pick the framework already held in a parsed field (`RECOMMENDED_RE`),
182
+ on the decision card and in the notification payload. Nothing acted on it, by design:
183
+ `waitForAnswers` "reads and does nothing else", and `Fact.decided_by` had no honest value for
184
+ a machine's decision. So: `run new --questions <stage,stage|stage:recommended|all|none>` —
185
+ `--gates`' grammar, extracted to ONE parser both flags now call rather than a second copy of
186
+ "split on the first colon" — freezes `questions_policy` into `run.yml` beside `gates_policy`,
187
+ additive and absent by default, so a run opened without the flag is byte-identical to one
188
+ opened before it existed and reads `human` everywhere. Under `recommended` for the cursor
189
+ stage, the moment a stage parks the loop takes each blocking question whose block names one
190
+ of its own options on a `Recommended:` line, through the SAME `writeAnswerSlot` +
191
+ `captureAnswers` that `tldrx answer` is — the footer, the `question.answered` and the
192
+ `fact.added` are a person's bytes — and the fact says the rest: `decided_by: agent-default`
193
+ (a THIRD value beside `owner`/`driver`, because `owner` there would be an audit record lying
194
+ in the dangerous direction), `alternatives` (the options not taken) and `recommended_why`
195
+ (the line's own reason). One `question.auto_answered` per answer reaches the notify hook with
196
+ the pick, the alternatives and the `--supersede` line that reverses it; no `question.raised`
197
+ goes out for a question the loop answered; and the deferred-gate release #247 built is now
198
+ one closure both the human-answer and the loop-answer paths call, so the second cannot form
199
+ a second opinion about when a gate is worth asking about. It never invents a pick: no
200
+ `Recommended:` line, a letter naming no option, or a block tagged with the new additive §2.7
201
+ keys `irreversible: true` / `money: true` is escalated exactly as before — the fixture that
202
+ proves it goes red the moment the `money:` guard is dropped. `tldrx run questions set
203
+ <stage>:<policy> --note "…"` mirrors `run gates set` on a shared engine, one
204
+ `questions.policy_changed` event, every refusal the gates verb has. The close's decided-tally
205
+ counts `agent-default` apart from "not stated" — it IS stated — and names it only when it is
206
+ non-zero, so every close that recorded none reads as it did.
207
+ - **`tldrx run auto --until-done [<n>]` relaunches the loop in-process after an exit it can do
208
+ nothing else with — a stage failure past `--retry-failed`, a thrown error that used to reach
209
+ `fail()` as a bare 1, a refusal whose remedy is mechanical — at most `n` times (default 5),
210
+ writing `run.relaunched` with the exit it recovered from; never over exit 4 (a person's),
211
+ never over a `budget.blocked` (nothing in-process moves the ceiling, #232/#244), and never
212
+ twice over the same last line. Measured 2026-09-12: five hand relaunches and 18 h before a
213
+ story ran (#252).** The loop body is now `runAutoOnce` and `runAuto` a bounded supervisor over
214
+ it — in the same process, because the loop holds no state and re-reads run.yml every
215
+ iteration, which is exactly what makes "run it again" honest. The run id the first attempt
216
+ resolves is pinned for every relaunch, and so is its spend baseline: "spent by this loop" and
217
+ `--max-usd` describe the one command a person typed, and a relaunch that reset the loop's own
218
+ ceiling would have been a ceiling in name only (a test pins it: `[fail, ok, ok]` under
219
+ `--max-usd 0.80` stops at exit 2 after two spawns, not three). One derivation,
220
+ `relaunchVerdict`, decides both the relaunch and whether the run-end notification is held, so
221
+ an attempt that is relaunched sends no `run.failed` and `run.finished` / `run.failed` go out
222
+ once, from the last attempt. A throw is caught only under the flag — without it the loop is
223
+ byte-identical, pinned against `--until-done 0`, and `fail()` still gets its exit 1. The
224
+ epic-claim-at-cut half of the issue is #262 and lives in the Build executor, not here.
225
+
226
+ - **Build entry proves the Definition of Done can run IN A WORKTREE before a developer is
227
+ paid (#254).** Measured on a live workspace, 2026-09-12 at 0.16.1: **18 h from `run auto` to
228
+ the first story that could run**, five relaunches, two of them environment and both invisible
229
+ to Build entry. The repo's `install:` named `./install.sh`, which existed in the human's
230
+ checkout and was never committed — a `git worktree` carries TRACKED FILES ONLY — so every
231
+ story's install failed identically in its own tree, each time after the story was opened and
232
+ immediately before the paid turn, and each time a person fixed it and relaunched. The base
233
+ pre-flight (#41) was green throughout and was right to be: it measures in the checkout, which
234
+ is where the file is. So Build entry now asks the other question once, before `agent.spawned`:
235
+ one throwaway detached worktree at the base sha, the declared `install:` run inside it, then a
236
+ resolution of each declared DoD command's first token in that tree. **Never the suite** — the
237
+ base pre-flight already measures the suite in the checkout and paying twice for it at every
238
+ entry is a cost nobody asked for. A refusal is exit 2, once, naming the exact path or binary.
239
+ Two decisions are the whole design. The `install:` command's own path token is refused by
240
+ `git ls-files` with NOTHING executed, and that is sound only because of WHEN the install runs:
241
+ nothing has installed anything yet, so a relative path the install NAMES cannot be one it
242
+ PRODUCED. A DoD command's path token gets the opposite treatment — tracked-ness decides
243
+ nothing there, because `node_modules/.bin/vitest` is untracked in every repo on earth and is
244
+ present by the time the DoD runs; it is probed AFTER the install, in that tree, and an
245
+ untracked twin in the checkout is rendered as advice, never as the verdict. The same rule has
246
+ a second half: a `dod` list runs in order, so a path named by any command AFTER the first may
247
+ be an artefact an earlier `dod` command builds (`dod: ["npm run build", "dist/check.sh"]`) —
248
+ the probe runs no suite, cannot tell that from "nobody committed it", and therefore writes an
249
+ advisory instead of a refusal. A bare binary name is never downgraded, at any index: nothing
250
+ a `dod` command does can put one on this process's PATH. Refusing what would have worked is
251
+ the expensive direction for an entry gate, so wherever the two readings could not be told
252
+ apart, this refuses less — each half pinned by a control that reddens when it is removed.
253
+ One existing behaviour moved with it: an install that fails deterministically in a fresh tree
254
+ is now refused at entry instead of blocking story 1 (`story-worktree-deps.test.ts` (d) says
255
+ so and says why). The per-story install is untouched and still answers for an install that
256
+ passes on the base and fails on a story's branch. The result is cached beside the base result in
257
+ `04-build/preflight.yml` under an additive `worktree:` key, so a resumed run does not re-pay
258
+ the install, and the cache is narrowed by three things and not two: the base sha, the
259
+ declaration hash (the install, every command probed, and the whole allowlist — the operator's
260
+ fix is an edit to `workspace.yml` and that edit must not be invisible), and AGE. A green gets
261
+ six hours rather than the base green's forever, because this row is a claim about an
262
+ ENVIRONMENT — the host's PATH, the registry the install reached — and none of that is in the
263
+ sha; a red gets the base red's 30 minutes, because #162 is the filed failure where a row
264
+ measured over a broken environment kept refusing after somebody fixed it. The gate costs
265
+ exactly zero where it could learn exactly nothing: a repo with no `install:` whose DoD names
266
+ only bare binaries opens no worktree at all, since `PATH` is the same in both trees and the
267
+ base pre-flight has already RUN the command there.
268
+
269
+ ### Fixed
270
+
271
+ - **One blocked story no longer stops every later wave — the boundary asks per STORY, from
272
+ `depends_on` (#260).** Measured 2026-09-12 on one workspace at 0.16.1: a loop told to build 8
273
+ stories built 5, never started 3, and reported the stage `done`. `03-plan/waves.yml` scheduled
274
+ W1=[S1] W2=[S2,S5,S6,S7,S8] W3=[S3] W4=[S4]; S7 blocked in W2 and the build skipped W3 and W4
275
+ outright, leaving S3 and S4 at `todo` while S2 — the only thing S3 depends on — was `done`. It
276
+ happened twice on the same run, the second time after a `story reopen S7` and a relaunch. The
277
+ mechanism was one line: after each wave the parallel path asked `waveFailed`, which is
278
+ `stories.some(status === "blocked")`, and broke out of the loop — `depends_on` appeared **zero
279
+ times** in `executors/build.ts`, so the one fact that could tell "a story that needs the
280
+ blocked code" from "a story that never did" was never read, though `validatePlan` already
281
+ guarantees every dependency sits in an earlier wave and is therefore settled at that moment.
282
+ The rule the `break` defended is kept, and it is the reason this is a frontier and not a
283
+ deletion: **a story runs when every one of its `depends_on` is `done`**, and a story whose
284
+ dependency did not land is NOT attempted — nothing fans out over code that was not landed,
285
+ which is how one red story becomes N of them. What changes is that the rule stops applying to
286
+ stories that never needed that code. A story held back is now `blocked` WITH the reason,
287
+ `dependency S7 blocked` — absent-with-reason, never a silent `todo` — and that sentence is
288
+ what `blocked_reason` on `gate.requested` and the continue note read, so the gate stops being
289
+ "held by stories" with nothing a person or a turn can act on (the #239 shape, one layer down).
290
+ It is transitive for free: S4 behind a blocked S3 reads `dependency S3 blocked`. The documents
291
+ stop hiding it too — the stage line counts every story `waves.yml` scheduled instead of the
292
+ rows the executor happened to hold (`5 of 8`, never `5 of 5`), the skipped stories are named
293
+ on stdout with the dependency each waits on instead of one line that said the next wave "may"
294
+ have depended on something, and the handoff's `none — every scheduled story reached done` is
295
+ now decided against `waves.yml`: a scheduled story with no outcome at all is named in
296
+ `## Unknowns` rather than dropped, which is the record that was lying in the dangerous
297
+ direction (§7). **`--parallel 1` is untouched**: the sequential path never stopped a later
298
+ story, a story whose dependency blocked is still attempted there, and this frontier replaces
299
+ the parallel path's stop only — turning today's finished stories into blocked ones would be a
300
+ second change, in the opposite direction. Both directions are pinned by tests that are the
301
+ same fixture with one field moved, and the existing test that froze the defect as correct (an
302
+ independent S4 stopped by a blocked S2) was rewritten to give S4 the `depends_on` that makes
303
+ its assertion true for the reason the code's own comment gives — argued in the test, never
304
+ flipped in silence.
305
+
306
+ - **A fix-list finding now declares what it IS, and a documentation defect stops holding a story
307
+ (#255).** Measured 2026-09-12 across two workspaces at 0.16.1: three stories with a green
308
+ definition of done, a merged commit and an APPROVING reviewer settled `blocked` — every one of
309
+ them on a `fix-now` finding whose whole content was a stale docstring or an `[src: …]` line
310
+ that did not resolve. The reviewer was not wrong: the prompt says `fix-now` is "this story's
311
+ own correctness", and a stale docstring in the story's own files honestly reads that way. What
312
+ the record could not say was that the defect was TEXT — a disposition answers "where does this
313
+ go" and nothing answered "what is it" — so a blocked story held the Build gate's `stories`
314
+ condition and three unattended nights ended at a person. `severity` was no help and is not the
315
+ vehicle: it was a free string defaulted to `unrated` that NO consumer read (grep over `src`:
316
+ one render site), so the effect was binary, `fix-now` or nothing. Every finding now carries a
317
+ required `kind` — `correctness` and `security` are behaviour and hold the story exactly as
318
+ before, `docs` and `style` are the way the repo reads and do not — and a `docs`/`style` finding
319
+ submitted `fix-now` is routed to `defer-with-log`, staying in the artefact, in the PR body and
320
+ on `retro.md` with a `Normalised-from: fix-now` line saying what it was submitted as. Nothing
321
+ is lost; it just stops costing a night.
322
+ This makes a gate WEAKER on purpose, so both directions are priced and both are tested.
323
+ Unblocking is not free: declaring a `fix-now` finding `docs` costs the SAME `[src: …]` citation
324
+ `refuted` already costs, through the same §2.8 parser (owner decision, 2026-09-13) — one exit,
325
+ one rule. The sentence that forced it is "the docstring says cents, the code returns dollars":
326
+ its own words do not say which side is wrong, and a one-word field deciding that a money bug is
327
+ a typo is the one failure with no log line to find it by, so the reviewer cites the behaviour
328
+ that makes it harmless or leaves it `correctness`. When in doubt it blocks: a `kind` that is
329
+ absent, unreadable or outside the enum is refused — for FREE, indexed as a fault in the report
330
+ so it costs the story no attempt, because a reviewer that forgot a field did not do bad work.
331
+ Reading is tolerant the other way (§7): a fix list already on disk with no `Kind:` line, or one
332
+ whose `Kind:` does not narrow to the enum, reads as *not stated*, and not stated blocks.
333
+ The four reviewer-prompt goldens MOVED, and that is the change, not an accident: the schema
334
+ alone would have left a reviewer nobody told about the new field, so the prompt's own prose
335
+ explains it, and `test/build-golden.test.ts` froze that prose. The diff is exactly eleven
336
+ inserted lines at one insertion point in each of the four reviewer prompts and nothing else — no
337
+ event, no `run.yml` row, no exit code. The prose teaches only the JUDGEMENT (which of the four
338
+ words is true of the defect) and never spells the field name: gh #133's guard derives that list
339
+ from `REVIEW_SCHEMA` itself, and it caught the first version of this wording.
340
+
341
+ - **The spawned developer can delete a file, and a permission refusal is now a recorded reason
342
+ instead of two burned attempts (#261).** Measured 2026-09-12 on one workspace at 0.16.1: story
343
+ S7, "delete an unused file", was undoable. The developer's allowance carried no verb that
344
+ removes a path — `Write`/`Edit` can empty a file, nothing could unlink it or take it out of
345
+ the index — so every `git rm` and `git mv` it tried came back "This command requires
346
+ approval", which in a headless `-p` run is a prompt nobody is there to answer. Worse was what
347
+ the framework did with that: a refused turn exits 0 and returns an envelope, so the DoD ran
348
+ green on an untouched tree, the empty commit went to review, the reviewer faulted a diff that
349
+ was never written, and the SECOND attempt bought the same wall — the developer's own sentence
350
+ living only in `result.raw.json`, in no event, no story file and no handoff. Two halves, and
351
+ the second is the one that generalises. The allowance grows the file-lifecycle git verbs —
352
+ `Bash(git rm *)`, `Bash(git mv *)`, `Bash(git restore *)`, the space form, never a bare `rm`
353
+ — because they are index operations on the story's OWN tree, undone by exactly the
354
+ `git checkout` that undoes an `Edit`, on a branch that never leaves the machine (`git push`
355
+ stays asserted absent); the 2026-08-29 audit's line — no permission-free shell, only git verbs
356
+ — is the reason the three are git verbs and is unchanged. And a tool call refused for approval
357
+ now BLOCKS the story at once with `` permission — `<command>` `` and why, one string reaching
358
+ the story file, the handoff's `## Unknowns` and `gate.requested`'s `blocked_reason`, so the
359
+ next verb that is missing costs one attempt and says so instead of two and nothing. What the detector
360
+ reads is a structural field first — `tool_result_meta[].non_execution_kind: "user-rejected"`, measured
361
+ on `claude` 2.1.270, present on both refusals measured and absent on every command that ran, including
362
+ one the layer allowed and git itself failed — and the `requires approval` sentence only as a fallback,
363
+ and only on a `Bash` call whose result errored. That fence exists because pre-merge review measured the
364
+ unfenced version reading a plain `Read` of a file CONTAINING the phrase as a refusal: this CHANGELOG is
365
+ one of the files that contains it, and the block happens before the DoD and the commit, so a false
366
+ positive would discard real work and spend the attempt. A refusal carrying neither signal is a miss
367
+ taken on purpose, and the sentence half depends on prose the host writes and can change without notice. What the
368
+ new grant covers, measured against `claude` 2.1.270 with `Bash(git rm *)` as the only rule:
369
+ `git rm -r`, `git rm -rf` and `git rm -r -- .` all pass (a trailing `*` matches the whole
370
+ argument tail, flags included); a path outside the repo is refused by GIT, not by the rule;
371
+ `git -C <elsewhere> rm` is refused by the rule, since the command does not begin `git rm`.
372
+ The open question about the same grammar, **#215** — does an `allow` rule of the
373
+ `Bash(<cmd> *)` form reach a command substitution in its arguments? — was measured alongside
374
+ this, `claude` 2.1.270, with `git rm -n` as a non-destructive instrument: under
375
+ `Bash(git rm *)`, `git rm -n -- "$(echo MARKER.txt)"`, `git rm -n "$(echo MARKER.txt)"` and
376
+ `git rm -n -r "$(echo .)"` were **all denied** ("Contains shell syntax that cannot be
377
+ statically analyzed"), while a bare `git rm MARKER.txt` ran. **The nuance matters more than
378
+ the result: the layer refuses the SYNTAX of a substitution, not the action** — in the same
379
+ measurement the agent rewrote the command with the value already expanded, and then it ran.
380
+ For `git rm <path>` that expanded form is exactly what this grant hands out, so the direction
381
+ is the safe one; nobody should build a guarantee on "substitutions are blocked", because what
382
+ is blocked is a spelling. And all of it is HOST behaviour, in the agent CLI's permission
383
+ layer, which can change without a line of tldrx moving — which is why the argument that holds
384
+ this grant up is the tree-scope one and not the permission layer's manners.
385
+ - **`run.yml` can finally explain its own `cost_usd`, and a Build turn stopped losing its
386
+ accounting entirely (#222).** A field audit across three workspaces measured a row reading
387
+ `input_tokens: 84, output_tokens: 37150, cost_usd: 1.98` for a 124 KB prompt — while the
388
+ provider's own result for that same turn reported `cache_read_input_tokens: 4911750`. 84 plus
389
+ 37,150 tokens do not cost $1.98; the 4.9 M cache reads do, and nothing on the row could say
390
+ so. Across 101 surviving transcripts in one workspace, cache reads were 97.3% of the input
391
+ side and the ledger's column saw 0.002% of it. The counters were parsed on every turn
392
+ (`spawnAgent.ts`) and thrown away one function later. They are now RECORDED: additively on the
393
+ task row, and — the half the issue did not know about — on the `agent.result` of the executor
394
+ path, which emitted **no `usage` at all**, so every Build and Watch turn lost all four
395
+ counters rather than only the two cache ones (measured: `grep -c usage` over the four frozen
396
+ golden `*-events.txt` returned 0, 0, 0, 0 with ten `agent.result` lines among them, and
397
+ `costView.ts`'s `toAttempt` therefore priced a whole Build off `{0,0,0,0}`). Nothing is
398
+ displayed that was not displayed before: this is a fix to the RECORD, and the dashboard and
399
+ `tldrx cost` are byte-identical. Two decisions worth naming. The cache counters are gated one
400
+ at a time rather than under `tokenSplit`'s both-or-nothing rule, because `input`+`output` are
401
+ two halves of one total that `turnTokens` adds and the cache counters are separate quantities
402
+ at separate prices (1.25x and 0.1x an input token) that nothing adds — and the spec's own
403
+ measured pair of calls is the proof that a paired rule would be wrong: the second reported
404
+ `cache_creation: 0` beside 37,059 cache reads, the single most useful number on the row. And
405
+ `turnTokens` names them in its input type while deliberately reading neither, so the exclusion
406
+ is a decision on the record rather than a field nobody has noticed yet: it returns one
407
+ unlabelled `number`, and folding 4.9 M cache reads into an `input_tokens: 84` would make that
408
+ number a total in no currency at all. `ExecutorTask` now carries the provider's `usage` whole
409
+ instead of a hand-copied two-field subset, which is what let the cache counters be dropped at
410
+ that seam in the first place, and the event block has one spelling for all three emitters
411
+ (`envelope.ts`'s `usagePayload`). A row from before this exists reads absent, as it should:
412
+ nothing reconstructs a counter nobody wrote down.
413
+
414
+ - **A gate about to sign itself is no longer sent as a Yes/No the owner cannot answer (#247).**
415
+ #203 holds back the `gate.requested` notification while open questions are the only thing
416
+ holding an `auto` gate, and released it when the answers landed if the gate was still
417
+ `pending`. Under `--wait-gates` that test is true by construction: the only thing that
418
+ self-closes an auto gate mid-wait runs one iteration later, inside the gate wait, after the
419
+ loop has already decided. Measured 2026-09-12 on two live workspaces, 3 of 3 questioned
420
+ stages: the Yes/No went out 600 ms after the last answer and the gate signed itself on the
421
+ very next poll — so the one tap the message invited ran `approve` on an already-approved gate,
422
+ and because the in-wait `gate.approved`/`stage.done` fall between two notify windows nothing
423
+ ever arrived to settle it: the bridge kept the prompt open and re-mentioned its owner every
424
+ escalate tick for two hours. *"esa pregunta está de más, es confusa."* The release now asks
425
+ the gate's CONDITIONS rather than its status — the same `reevaluateAutoGate` the poll uses,
426
+ extracted so the poll and the release read one measurement and neither can form a second
427
+ opinion (§7) — together with the policy that will act on them. Every condition holding and a
428
+ `--wait-gates` to sign it means there is no decision to take: nothing is sent, and the run
429
+ says which stage and why on stdout rather than going quiet. A gate something else still holds
430
+ is notified exactly as #203 promised, and now worded from that one reading: the re-measured
431
+ verdict supplies the summary's `held by:` sentence and the open questions are read in the same
432
+ breath, so a payload can no longer name four open questions beside `holding: "none"` — which
433
+ is what the adapter was handed, measured, and why it drew the default two buttons. #239 fixed
434
+ WHICH command a questions-held gate offers; this is WHEN, and whether, it is offered at all.
435
+ - **A turn that wrote a lot of files could not be RECORDED, and the invocation died owing the
436
+ money it had just spent (#248).** Measured on a live headless run: `tldrx run auto … --retry-failed 2`,
437
+ eight stories, dead at *"refusing to append an invalid event: payload 4135 bytes exceeds the
438
+ 4096 byte cap"*. The field was `outputs` — every run-relative path the turn wrote, 3924 bytes of
439
+ it under a 212-byte envelope — on the `agent.result` that `recordExecutorTasks` appends. What
440
+ that one refused append cost is the reason this is three fixes and not one. The append was
441
+ RAW, so it never reached the capped seam; the seam would not have saved it anyway, because
442
+ `capPayload` knew how to name exactly one absence and it was `detail`, which an `agent.result`
443
+ does not have. The throw landed one line AFTER the try/catch #160 built for precisely this, so
444
+ it escaped `runNext` entirely and came out of the CLI as exit 1 — a code `--retry-failed` does
445
+ not retry, since its branch keys on exit 5. A loop told to survive two failures survived zero.
446
+ And `claimEpicBranches` had put the run's epic branch in memory one line before the throw, with
447
+ the `store.save()` after it: the branch was cut, on disk, and `run.yml` did not say so, so the
448
+ relaunch read its OWN epic as a stranger's and refused to stack onto it — with the only way
449
+ back in being `tldrx next --reuse-epic`, which `run auto` does not expose. So: `capPayload`
450
+ learns `outputs`, and names it by COUNT (`outputs_omitted: 80`) with the full list written
451
+ beside the event at `<phase>/log/overflow/…-agent.result-outputs.txt` — never a truncated list,
452
+ which reads downstream as the whole one. The capped seam moves out of `runExecutor` to module
453
+ scope and all four raw `agent.result` appends route through it, not just the one that was hit:
454
+ all four carry `outputs` (measured), and the other three had simply not been reached yet. A
455
+ throw while recording rows now fails the STAGE with exit 5, in the same family as the executor's
456
+ own throw, and says how many of how many rows reached `run.yml` rather than a boolean that
457
+ would be false for a partial truth. It repaints no row: those turns finished, and only their
458
+ event failed to land. And a claim is saved the moment it is earned. `capPayload` still knows
459
+ its two fields BY NAME and has no general rule for shrinking whatever is biggest — a third
460
+ growable payload field has to be taught to it, deliberately, or it is refused whole.
461
+
3
462
  ## 0.17.0 — 2026-09-12
4
463
 
5
464
  ### Added
package/README.md CHANGED
@@ -316,6 +316,8 @@ back on the registry is 0.3.0.
316
316
 
317
317
  | Version | Date | Status | Contains |
318
318
  |---|---|---|---|
319
+ | 0.18.1 | 2026-09-13 | `beta` | five things a run could not survive on its own, every one measured on the unattended proof runs that followed 0.18.0 rather than reasoned about: a priced plan was never priced — the planner wrote `stories[].estimate_usd` and Build read only `per_phase_usd` + `spent_usd`, so every story got the same $5.40 cap while the plan said $28, and the contract now names the shape and the plan check refuses a budget.yml that does not carry it; a headless refusal under a host-tokens ceiling wrote nothing to the ledger, so `--until-done` relaunched over a refusal it could not see, and now writes its `budget.blocked` first; a developer that had committed its work and was then refused a compound verification command was blocked BEFORE the facilitator ran the Definition of Done, so the DoD now decides when there is work and the refusal is recorded either way, with the prompt saying to run each DoD command verbatim and alone because shell separators split a line into subcommands that each need their own grant; a workspace can declare `<slot>_scoped` DoD commands that run per story on the paths it touched with the full suite once per epic head, after the full suite was measured running three times per story attempt; and a cancelled run no longer leaves an epic branch that refuses the ordinary retry — `run cancel` releases it and Build renames aside an epic whose owner run is closed, while an open or unknown owner still refuses verbatim and an uncounted branch is never deleted |
320
+ | 0.18.0 | 2026-09-13 | `beta` | the release that lets an unattended run survive its own accidents and end somewhere, every item measured on live runs at 0.16.1 rather than reasoned about: a turn that wrote a lot of files could not be RECORDED — an `agent.result` whose `outputs` field held 3924 bytes of run-relative paths blew the 4096-byte event cap through a RAW append that never reached the capped seam, the throw landed one line after the try/catch built for exactly this and came out as exit 1, which `--retry-failed` does not retry since its branch keys on exit 5, so a loop told to survive two failures survived zero, and the epic branch had been cut on disk one line before the throw with the save after it, leaving the relaunch to read its own epic as a stranger's; `outputs` is now named by COUNT with the full list written beside the event rather than truncated, all four raw appends route through the capped seam at module scope, a throw while recording fails the STAGE with exit 5 saying how many of how many rows landed, the claim is saved the moment it is earned, and `capPayload` still knows its two fields BY NAME — a third growable field has to be taught to it deliberately or it is refused whole; Build entry now proves the Definition of Done can run IN A WORKTREE before a developer is paid, after 18 h from `run auto` to the first story that could run on one workspace, five relaunches, because the repo's `install:` named a file that existed in the human's checkout and was never committed and `git worktree` carries TRACKED FILES ONLY, so every story failed identically after the story was opened and immediately before the paid turn while the base pre-flight was green and right to be, measuring in the checkout where the file is — one throwaway worktree at the base sha, the install run inside it, each DoD command's first token resolved there, NEVER the suite, refusal exit 2 naming the exact path or binary, cached under an additive `worktree:` key narrowed by base sha, declaration hash and AGE (six hours for a green, because the row is a claim about an ENVIRONMENT that no sha contains; the base red's 30 minutes for a red), and costing zero where it could learn nothing; where the two readings could not be told apart it deliberately refuses LESS — a path named by a `dod` command after the first may be an artefact an earlier one builds, so that is an advisory and not a verdict; one blocked story no longer stops every later wave, after a loop told to build 8 stories built 5, never started 3, and reported the stage `done`, twice on the same run, because the parallel path asked only "did any story block" and `depends_on` appeared ZERO times in the Build executor — a story now runs when every one of its `depends_on` is `done`, one held back is `blocked` WITH the reason `dependency S7 blocked` (transitive for free) which is what the gate's `blocked_reason` and the continue note read, the stage line counts every story `waves.yml` scheduled rather than the rows the executor happened to hold, and a scheduled story with no outcome at all is named in `## Unknowns` instead of dropped, while `--parallel 1` is untouched on purpose — a story whose dependency blocked is still attempted there, and changing that would be a second change in the opposite direction; the spawned developer can now delete and rename paths in its own tree, after a story that was simply "delete an unused file" proved undoable — nothing in the allowance could unlink a path or take it out of the index, so every `git rm` came back needing approval nobody was there to give, the refused turn exited 0 with an envelope, the DoD ran green on an untouched tree and the reviewer faulted a diff that was never written, twice — the file-lifecycle git verbs are granted as index operations on the story's OWN tree (never a bare `rm`, `git push` still asserted absent), and a tool call refused for approval now BLOCKS the story at once with the command and why, reaching the story file, the handoff and the gate, so the next missing verb costs one attempt instead of two and nothing; that detector reads a structural field first and the English refusal sentence only as a fenced fallback, because pre-merge review measured the unfenced version reading a plain file-read of a document CONTAINING the phrase as a refusal — this very CHANGELOG is one of them — and a refusal carrying neither signal is a miss taken on purpose, with the sentence half depending on prose the host can change without notice; a fix-list finding now declares what it IS, after three stories with a green definition of done, a merged commit and an APPROVING reviewer settled `blocked`, each on a `fix-now` finding whose whole content was a stale docstring or a citation that did not resolve — the reviewer was not wrong and `severity` was never the vehicle (a free string NO consumer read, one render site under `src`), so a required `kind` splits behaviour from text: `correctness` and `security` hold the story exactly as before, a `docs`/`style` finding submitted `fix-now` is routed to `defer-with-log` with a `Normalised-from:` line, and because this makes a gate WEAKER on purpose the unblocking is priced — declaring a finding `docs` costs the SAME `[src: …]` citation that refuting one already costs, a `kind` that is absent or outside the enum is refused for free and indexed as a fault so it costs the story no attempt, a fix list already on disk with no `Kind:` line reads as not stated and not stated blocks, and the four reviewer-prompt goldens MOVED because the schema alone would have left a reviewer nobody told about the field; `run.yml` can finally explain its own `cost_usd` — a row read `input_tokens: 84, output_tokens: 37150, cost_usd: 1.98` for a 124 KB prompt while the provider reported 4,911,750 cache reads for that same turn, and across 101 transcripts in one workspace cache reads were 97.3% of the input side against a column that saw 0.002% of it — the counters were parsed on every turn and thrown away one function later, and the half the issue did not know about is that the executor path's `agent.result` emitted NO `usage` at all, so every Build and Watch turn lost all four counters and the whole Build was priced off zeroes (measured: zero `usage` lines across the four frozen goldens with ten `agent.result` among them); they are recorded additively and gated one at a time rather than both-or-nothing, `turnTokens` names them while deliberately reading neither so the exclusion is a decision on the record, nothing displayed changed — the dashboard and `tldrx cost` are byte-identical — and a row from before this reads absent, since nothing reconstructs a counter nobody wrote down; a gate about to sign itself is no longer sent as a Yes/No the owner cannot answer, after 3 of 3 questioned stages on two live workspaces sent the prompt 600 ms after the last answer and self-signed on the very next poll, so the one tap it invited ran `approve` on an already-approved gate and, because the in-wait approval fell between two notify windows, the bridge kept the prompt open and re-mentioned its owner every escalate tick for two hours — the release now asks the gate's CONDITIONS through the same re-evaluation the poll uses, extracted so neither can form a second opinion, sends nothing when there is no decision to take and says which stage and why on stdout instead of going quiet, and words a genuinely held gate from that one reading so a payload can no longer name four open questions beside `holding: "none"`; and three things a run could not do before: it can end in a pull request toward main on purpose, frozen at `run new --ship` with `push`, `pr` or `merge` into an additive `run.yml` block and not a sixth phase, pushed through the ONE push wrapper in the codebase with a PR that reports NO check left open and recorded as `merge: absent — no checks to wait on` because auto-merge over nothing to wait on is a merge now rather than a merge when green — pre-merge review caught the first version erasing a recorded merge failure with an empty string and exit 0, and the emitter dropped the whole block until it was taught the key; `questions_policy: recommended` answers a blocking question that carries its own `Recommended:` line and escalates the ones that do not, after 10 owner questions across two headless runs ($89.82 over 28 h and $26.38 over 5.6 h, the agents busy 4.9 h and 56 min of it) parked the loop on 9 that already held the pick in a parsed field, writing `decided_by: agent-default` as a THIRD value beside owner and driver because `owner` there would be an audit record lying in the dangerous direction, and never inventing a pick — no line, a letter naming no option, or a block marked irreversible or money escalates exactly as before; and `run auto --until-done [<n>]` relaunches the loop in-process after an exit it can do nothing else with, at most `n` times (default 5), after five hand relaunches and 18 h before a story ran — never over a person's exit 4, never over a budget block since nothing in-process moves a ceiling, never twice over the same last line, with the run id and the spend baseline pinned across relaunches so `--max-usd` still describes the one command a person typed; the epic-claim-at-cut half of that issue lives in the Build executor and is NOT fixed here |
319
321
  | 0.17.0 | 2026-09-12 | `beta` | three things the framework knew and recorded wrongly, each found by reading its own records rather than by using it: a gate notification now carries the condition HOLDING the gate as data instead of leaving it to be guessed from the text of the command it suggests — `holding` says questions, stories or nothing-mechanical, and when a story is blocked with a recorded reason the payload also hands over a ready-made rejection that lets the loop carry on, with the note DERIVED from the blocked story rather than canned, because a rejection's note is fed to the next turn's prompt and a generic one would hand that turn an empty instruction, so when no reason can be derived the offer is absent rather than invented; the emitted command keeps a literal placeholder and never interpolates the reason, which keeps the quoting hazard out of the record and puts it where a substituting client can see it; `run status` stopped printing more money left than it had ceiling — the run ceiling now has ONE live copy, read from `budget.yml` by every live screen, and `run.yml`'s mirror is documented as the creation value and no longer written by a raise, since the break was never that a raise failed to write the mirror but that an ordinary concurrent save carried a stale copy over it, and the fix was to delete the half-sync rather than to build a better one — pre-merge review caught the first version pairing a LIVE spend with that now-frozen ceiling inside `tldrx replay`, which would have reproduced the same impossible line in a different command on every run whose budget had been raised, measured on the real CLI as `$12.00 spent of $10.00 ceiling` with no concurrency required; and a task row now records the ROLE its turn actually ran under, where every Build turn had been filed as the developer including the reviewer's — the role was known at spawn and written to the event stream, and was dropped on the way to the ledger, so the audit record named the wrong actor for work it had itself measured; the new key is additive and written on EVERY row including the developer's, because a role present only on reviewers would make its absence mean developer-or-not-recorded and send a reader back to inferring the role from an absence, which is the guess the change exists to remove |
320
322
  | 0.16.1 | 2026-09-12 | `beta` | five things the framework knew and did not say, or said wrong — four of them found by using it rather than by reading it: a red base pre-flight now KEEPS its output, so a refusal that blocks every story in a Build names the failing test and cites the file, where it used to record only the last line of stdout — measured 2026-09-10, a stage refused with `tail: "Test run completed with non-success exit code: 2"` while the cause, `DockerUnavailableException`, sat on line 12 of 163,702 lines the run had already captured and thrown away, so diagnosing a refusal the framework had itself measured meant re-running the workspace's test command by hand; it now routes through the same seam #211 built for a story's DoD, which had been naming its failing test correctly all along on the same command, the same day, in the same repo — the path with the SMALLER blast radius was the legible one; a gate notification now offers the command that CLEARS it rather than always `tldrx approve` — questions open give `tldrx answer <id>`, unfinished stories give `tldrx run status`, and `approve` is offered only when nothing mechanical is outstanding, after an owner approved a Build gate by mistake twice in one evening over unbuilt stories, each time from a phone, each time needing a revoke, while a ten-minute heartbeat repeated `Run: tldrx approve` seven times under a sentence that correctly named the five open questions holding it; `tldrx reject --and-continue` lets a rejection mean "redo it this way and carry on" instead of ending the run — the loop resumed after an approve and stopped after a reject, so the button meaning "there is still work to do" was the one that stopped the work and only a terminal could revive it; five real rejections that night all meant continue, five cost a manual relaunch, and a bare `tldrx reject` still writes a byte-identical `run.yml` and stops exactly as before; the expert-recompute fixture anchors its evidence dates to a `now` it can move, so `bun test` stops going red by the calendar — pristine `main` was red at the exact sha of the published 0.16.0 with no commit in between, and a clock moved one year forward reddened FIVE cases, not the one that had already fired; and `test/merge-wave.test.ts`'s concurrency failure, documented as a known flake since #115 and carrying a written licence to re-run it, was never one: `merge-guard.sh` rewrote `.git/hooks/reference-transaction` IN PLACE while a sibling wave's `git merge` was exec'ing it — ETXTBSY on Linux at 31% under contention, benign on macOS, which is why it was green locally and red in CI, and why a same-sha re-run failed 2 for 2 rather than passing; the hook is now written to a temp file and RENAMED into place, the refusal that used to borrow `2`/`merge conflict` for a hook abort now says what it was and exits 11, and AGENTS.md §4 withdraws the re-run licence for those two cases while naming the interrupted-merge case (#237) as still open and undiagnosed — because "all real" for a whole file costs the same as "all flake", in the other direction |
321
323
  | 0.16.0 | 2026-09-11 | `beta` | an unattended run can now clear the one kind of failure it was stopping on, and a gate that refuses says why it refused: measured 2026-09-10 on a real unattended `run auto`, the loop drove itself through what → how → plan and signed all three `auto` gates by itself, and still needed a person four times — three of those were content or money decisions a loop must not make, and the fourth was a plan that failed its own check by five characters over a cap, where a person relaunched the same command and the next attempt fixed the two files and passed, so the loop stopped on the one failure it could have cleared; `tldrx run auto --retry-failed <n>` now runs a failed stage again at most `n` times in a row, bounding exit `5` and nothing else — a usage error (`1`), a money refusal (`2`) and an awaiting-human park (`4`) are each attempted ONCE however large `n` is, because a phase ceiling means a human decides about money and a retry would turn that sentence into a delay — only CONSECUTIVE failures count since what is bounded is "this run is stuck" and not "this run has ever failed", a retry SPENDS as a fresh metered stage under the same phase ceiling and the same `--max-usd`, `0` is the default and a default invocation's lines are byte-identical to what they were, and when the bound is spent the loop stops on the failure's own exit `5` and says the count LAST, so the sentence that reaches a phone is what the loop tried and not a bare number; and an `auto` gate that REFUSES now writes down the verdict its note was always designed to carry — a gate sat pending ~40 minutes while `run status` and `--verbose` named no condition at all, and the reason surfaced only when a person guessed at the `tldrx approve` the status line suggested, which is the one route nobody unattended is going to take — recording all seven conditions WITH THEIR VALUES on the still-`pending` gate, since a note that dropped the passing ones would answer "was it the money" with the same silence, and naming the holding ids on the gate row and on the `waiting` line; it writes only over a `pending` gate, so a gate a person has since signed keeps THEIR words, and only when the verdict would change, so a four-hour `--wait-gates` poll writes once per distinct verdict rather than thousands of times — and that test and that write are a compare-and-set under the workspace lock, because pre-merge review reproduced, with two real processes, a check-then-act over an earlier snapshot erasing a concurrent `approve` outright, and the poll runs every two seconds precisely while a person is deciding |
@@ -1,24 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  conflictOf
4
- } from "./chunk-tj66vg1n.js";
4
+ } from "./chunk-1tynpxvm.js";
5
5
  import {
6
6
  FactsStore,
7
7
  formatJaccard
8
- } from "./chunk-54vzevgt.js";
8
+ } from "./chunk-kwaqdq9w.js";
9
9
  import {
10
10
  parseHookInput,
11
11
  readStdin
12
- } from "./chunk-ds9sew2e.js";
12
+ } from "./chunk-pday823f.js";
13
13
  import {
14
14
  EventLog,
15
15
  PHASE_ID_RE
16
- } from "./chunk-jp6jscsd.js";
16
+ } from "./chunk-x64paqbg.js";
17
17
  import {
18
18
  PHASE_IDS
19
- } from "./chunk-d0rp8c68.js";
20
- import"./chunk-k4nqzdw5.js";
21
- import"./chunk-nqwg5ddj.js";
19
+ } from "./chunk-bgxj40rr.js";
20
+ import"./chunk-zg7q0q4y.js";
21
+ import"./chunk-vf992pfw.js";
22
22
  import {
23
23
  ADVISORY_KEY,
24
24
  MAX_FACT_CHARS,
@@ -28,15 +28,17 @@ import {
28
28
  renderQuestionBlock,
29
29
  replaceBlock,
30
30
  serializeQuestions
31
- } from "./chunk-fm141jak.js";
31
+ } from "./chunk-8ak5kwd5.js";
32
32
  import {
33
33
  ITERATION_ONLY_SLOT,
34
34
  PROJECT_FRAMEWORK_DIR,
35
35
  PROJECT_WORK_DIR,
36
36
  commandProbeIssues,
37
37
  factsPath,
38
- parseYaml
39
- } from "./chunk-77y6q9mt.js";
38
+ isScopedTemplate,
39
+ parseYaml,
40
+ scopedSlotOf
41
+ } from "./chunk-c1h38et6.js";
40
42
 
41
43
  // src/hooks/answer-capture.ts
42
44
  import { existsSync as existsSync5 } from "fs";
@@ -151,6 +153,8 @@ function loadWorkspace(root) {
151
153
  const commandRoles = new Map;
152
154
  const commandProbes = new Map;
153
155
  const iterationCommands = new Set;
156
+ const scopedCommands = new Map;
157
+ const scopedTemplates = new Set;
154
158
  const defaultBranches = new Map;
155
159
  let seedTriageThresholdTokens = null;
156
160
  const empty = () => ({
@@ -161,6 +165,8 @@ function loadWorkspace(root) {
161
165
  commandRoles,
162
166
  commandProbes,
163
167
  iterationCommands,
168
+ scopedCommands,
169
+ scopedTemplates,
164
170
  defaultBranches,
165
171
  seedTriageThresholdTokens
166
172
  });
@@ -192,13 +198,24 @@ function loadWorkspace(root) {
192
198
  defaultBranches.set(entry.name, typeof entry.default_branch === "string" && entry.default_branch !== "" ? entry.default_branch : FALLBACK_DEFAULT_BRANCH);
193
199
  const own = [];
194
200
  const roles = declared.get(entry.name) ?? new Map;
195
- for (const value of roles.values()) {
201
+ const scoped = new Map;
202
+ for (const [role, value] of roles) {
203
+ const base = scopedSlotOf(role);
204
+ if (base !== null) {
205
+ const full = roles.get(base);
206
+ if (full !== undefined && scopedSlotOf(base) === null && isScopedTemplate(value)) {
207
+ scoped.set(full, value);
208
+ scopedTemplates.add(value);
209
+ }
210
+ continue;
211
+ }
196
212
  commands.add(value);
197
213
  if (!own.includes(value))
198
214
  own.push(value);
199
215
  }
200
216
  repoCommands.set(entry.name, own);
201
217
  commandRoles.set(entry.name, roles);
218
+ scopedCommands.set(entry.name, scoped);
202
219
  const fast = roles.get(ITERATION_ONLY_SLOT);
203
220
  if (fast !== undefined)
204
221
  iterationCommands.add(fast);
@@ -407,7 +424,11 @@ function answerProvenance(block, overrides, repoNames, fallbackRepos) {
407
424
  return {
408
425
  repos,
409
426
  unresolved: named.unresolved,
410
- source: override?.decidedBy === undefined ? {} : { decided_by: override.decidedBy }
427
+ source: override?.decidedBy === undefined ? {} : { decided_by: override.decidedBy },
428
+ row: {
429
+ ...override?.alternatives === undefined || override.alternatives.length === 0 ? {} : { alternatives: override.alternatives },
430
+ ...override?.recommendedWhy === undefined ? {} : { recommended_why: override.recommendedWhy }
431
+ }
411
432
  };
412
433
  }
413
434
  function unresolvedEntries(captured) {
@@ -445,6 +466,7 @@ function captureAnswers(questionsPath, ctx) {
445
466
  fact: text,
446
467
  ...truncated ? { truncated: true } : {},
447
468
  ...clash === null ? {} : { conflicts_with: [clash.fact.id] },
469
+ ...prov.row,
448
470
  area,
449
471
  repos: prov.repos,
450
472
  kind: "answer",