tldr-experts 0.14.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +339 -0
  2. package/README.md +2 -0
  3. package/dist/hooks/answer-capture.js +11 -10
  4. package/dist/hooks/budget-gate.js +17 -14
  5. package/dist/hooks/{chunk-fefnr71h.js → chunk-3kmx3dmz.js} +84 -211
  6. package/dist/hooks/chunk-4mjxyfp9.js +148 -0
  7. package/dist/hooks/{chunk-mzwqb08p.js → chunk-54vzevgt.js} +3 -3
  8. package/dist/hooks/{chunk-wwwmkey4.js → chunk-77y6q9mt.js} +18 -3
  9. package/dist/hooks/chunk-d0rp8c68.js +278 -0
  10. package/dist/hooks/{chunk-tpz0zb0t.js → chunk-ds9sew2e.js} +1 -1
  11. package/dist/hooks/{chunk-bqmstq21.js → chunk-fm141jak.js} +1 -1
  12. package/dist/hooks/chunk-k4nqzdw5.js +93 -0
  13. package/dist/hooks/{chunk-gvz0rzb9.js → chunk-msswet35.js} +1 -1
  14. package/dist/hooks/{chunk-db7wf3hy.js → chunk-nqwg5ddj.js} +1 -1
  15. package/dist/hooks/{chunk-r95h9ynf.js → chunk-qdvwnts3.js} +1 -1
  16. package/dist/hooks/{chunk-mt8f2czb.js → chunk-rz0qr006.js} +25 -31
  17. package/dist/hooks/{chunk-dxk7eaqn.js → chunk-vmhfw2r1.js} +1 -1
  18. package/dist/hooks/{chunk-ppgbs5ag.js → chunk-vvr4rk82.js} +79 -21
  19. package/dist/hooks/{chunk-taj1vrbe.js → chunk-z0hnthw4.js} +3 -3
  20. package/dist/hooks/claim-sources.js +5 -5
  21. package/dist/hooks/dod-gate.js +18 -8
  22. package/dist/hooks/no-reask.js +8 -8
  23. package/dist/hooks/session-start.js +21 -13
  24. package/dist/hooks/statusline.js +9 -7
  25. package/dist/tldrx.js +2119 -1204
  26. package/package.json +1 -1
  27. package/plugin/.claude-plugin/plugin.json +1 -1
  28. package/stages/build/stage.yml +5 -1
  29. package/stages/watch/stage.yml +4 -1
  30. package/templates/budget.yml +13 -6
  31. package/templates/watcher.md +11 -0
  32. package/templates/workspace.yml +6 -0
  33. package/workflows/bugfix.yml +5 -1
  34. package/workflows/docs.yml +5 -1
  35. package/workflows/feature.yml +5 -1
  36. package/workflows/hotfix.yml +5 -1
  37. package/workflows/integration.yml +5 -1
  38. package/workflows/migration.yml +5 -1
  39. package/workflows/performance.yml +5 -1
  40. package/workflows/prototype.yml +5 -1
  41. package/workflows/refactor.yml +5 -1
  42. package/workflows/retro.yml +5 -1
  43. package/workflows/security-patch.yml +5 -1
  44. package/workflows/spike.yml +5 -1
  45. package/workflows/upgrade.yml +5 -1
  46. package/dist/hooks/chunk-dzkg8c68.js +0 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,344 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0 — 2026-09-11
4
+
5
+ ### Added
6
+
7
+ - **`tldrx run auto --retry-failed <n>` — a bounded retry on a failed stage (#233).** Measured
8
+ on a real unattended run at 0.15.0: the loop drove itself through what → how → plan and
9
+ signed all three `auto` gates by itself, and still needed a person four times. Three of the
10
+ four were content decisions or money, which a loop must not make. The fourth was a plan that
11
+ failed its `plan` check by five characters over a cap — a person relaunched `run auto`, the
12
+ next attempt fixed the two files and passed, and nothing else happened. So the loop stopped
13
+ on the one failure it could have cleared, and it stopped because `run auto` returned on every
14
+ non-zero exit except `4`. Now `--retry-failed <n>` lets it run that stage again, at most `n`
15
+ times in a row. It bounds exit `5` and nothing else: a usage error (`1`), a money refusal
16
+ (`2`) and an awaiting-human park (`4`) are each attempted ONCE however large `n` is, because
17
+ a phase ceiling means *a human decides about money* and a retry would turn that sentence into
18
+ a delay. Only CONSECUTIVE failures count — any other outcome puts the count back to zero,
19
+ since what is bounded is "this run is stuck", not "this run has ever failed". A retry SPENDS:
20
+ it is a fresh metered stage under the same phase ceiling and the same `--max-usd`, which is
21
+ what stops it running up a bill. `0` is the default and a default invocation's lines are
22
+ byte-identical to what they were; anything outside `0..3` is refused by name with exit `1`,
23
+ from the one constant the loop and the flag parser share. When the bound is spent the loop
24
+ stops on the failure's own exit `5` and SAYS the count last — `3 consecutive stage failures
25
+ at 03-plan/plan …` — so the sentence that reaches a phone is what the loop tried, not a bare
26
+ `5`.
27
+
28
+ ### Fixed
29
+
30
+ - **An `auto` gate that REFUSES now writes the verdict down, so `run status` says which of the
31
+ seven conditions is holding it (#230).** The note has always been the designed answer to
32
+ "which of the seven stopped it" — and it was written only by a gate that CLOSED, so the one
33
+ record built for that question was `note: ""` in exactly the case it exists for. Measured
34
+ 2026-09-10 on an unattended `run auto`: a gate sat pending ~40 minutes, `run status` and
35
+ `run status --verbose` named no condition, and the reason (`claim-sources`, one unresolvable
36
+ source) surfaced only when a person guessed at the `tldrx approve` the status line suggested
37
+ — the one route nobody unattended is going to take. Each re-measure that refuses now records
38
+ `auto-gate refused — held by: <ids> · <all seven with their values>` on the gate that is
39
+ still `pending`, and `run status` names the ids on the gate row and on the `waiting` line
40
+ (`--verbose` still quotes the whole note). All seven values, not just the failures: a note
41
+ that dropped `budget=$0.30 of $6.00` would answer "was it the money" with the same silence.
42
+ It writes only a `pending` gate — a gate a person has since signed keeps THEIR words — and
43
+ only when the note would change, so a four-hour `--wait-gates` poll writes `run.yml` once per
44
+ distinct verdict rather than thousands of times. That `pending` test and the write are a
45
+ **compare-and-set under the workspace lock**, over a `run.yml` read inside it: a check-then-act
46
+ over a snapshot taken a moment earlier erased a concurrent `approve` outright — `save()` writes
47
+ the whole snapshot and re-reads only `budget.yml`'s ceilings, so the gate fell back to
48
+ `pending`, `by`/`at` to null and the person's words were gone. Caught in pre-merge review and
49
+ reproduced with two real processes, which is now the test: the poll runs every two seconds
50
+ precisely while a person is deciding, so an audit record that destroys the evidence of a human
51
+ decision is not a theoretical interleaving. A gate carrying a refusal note is no longer
52
+ counted among the "signed gates carry a note" rows, because nobody closed it.
53
+
54
+ ## 0.15.0 — 2026-09-10
55
+
56
+ ### Added
57
+
58
+ - **Four Build calibrations became optional `stage.yml` keys: `attempts` (default 2),
59
+ `fixlist_rounds` (1), `reviewer_share` (0.25), `gate_signer_share` (0.25).** Each was a code
60
+ constant, and each is a calibration rather than an invariant — "two attempts" is the
61
+ framework's opinion about how many developer turns a story deserves before a human should
62
+ look at it, and a hardening wave over a legacy repo holds a different one. Until now the only
63
+ way to hold it was a fork. `parallel:` made exactly this move already and the argument is the
64
+ same. Absent ⇒ today's constant, byte for byte, which is what the golden proves; present and
65
+ out of range (`attempts` 1..5, `fixlist_rounds` 0..3, the two shares 0..1) is REFUSED by name
66
+ at `validateStage` rather than clamped, because a clamp lets an operator write a number, pay
67
+ for something else, and never be told which. One derivation
68
+ (`src/core/schemas/stageTuning.ts`): `caps.ts`, `budget/remainingWork.ts`, `gateSigner.ts` and
69
+ `build/fixlist.ts` keep the constants they always exported — the prose that justifies a number
70
+ lives with the code it governs — but each is now defined as its field there, so the default
71
+ cannot move in one file and not the other.
72
+ - **`tldrx run auto --prompt-max-bytes <n>` and `--max-reads <n>` (#208).** Passed to every
73
+ `next` the loop makes, with the same precedence they have there: the flag beats the stage
74
+ file. Until now the only way to raise either for an *unattended* run was to edit
75
+ `.tldrx/stages/<id>/stage.yml` — a file change, in the workspace, to get past one refusal.
76
+
77
+ - **`install:` is a slot the framework runs, in every fresh story worktree, before the
78
+ developer (#209).** It has been in `templates/workspace.yml` since the beginning and no code
79
+ read it: `git grep` over `src/` found zero readers outside the unrelated Claude-Code
80
+ installer. Now, when a repo declares one, Build runs it in the story's worktree the moment
81
+ that worktree is created — through the same allowlist-and-argv runner every other declared
82
+ command goes through, so an installer is not a privileged string — and records it as its own
83
+ `check: "install"` with an exit code, a `duration_ms` and `tree: "worktree"`. The duration is
84
+ the point as much as the exit code: this work used to be folded into the story's paid turn or
85
+ absent from the record entirely. An install that FAILS blocks the story with what the
86
+ installer printed and dispatches no developer at all — a tree whose dependencies did not
87
+ install cannot prove anything, and paying a turn to discover that is the cost this whole
88
+ change exists to stop.
89
+ - **A watcher card can now say "there is nothing to query", and be believed (#212).** Measured
90
+ on 0.14.2, on a real workspace: the Watch stage read a code path that sends a verification
91
+ code and returns — no log line, no metric, no span anywhere in it. It said so correctly under
92
+ `## Signal`, `## Where` and `## Looks broken when`, each with an `absent:` source, and noted
93
+ that the only signal that exists is a customer reporting a missing code. Then it reached
94
+ `## Query`, wrote the same truth in prose, and the validator refused the whole card for it —
95
+ "`## Query` holds no fenced block — the query has to be copy-pasteable, not described". The
96
+ stage failed and the cost was spent on the honest answer. `Query` was the one checked section
97
+ with no absent form — every other section had `absent:` and it had nothing. It now accepts one line as well as a fenced block:
98
+ `Query: none — <reason> [src: …]`, where the reason ends with a §2.8 token read by the *same*
99
+ parser `claim-sources` denies a handoff bullet with, so an unsourced or unresolvable `none` is
100
+ refused exactly like any unsourced item. A line rather than a fence tagged `none` because the
101
+ `[src: …]` grammar is line-terminal: inside a fence the reason could only be sourced by a
102
+ second reader of that grammar, and a card has exactly one.
103
+ A `none` is EARNED, not asserted: it is refused unless the card's own `## Signal` cites `absent:`
104
+ and the reason's source is `absent:` too — found by a pre-merge reviewer's probe, where a card
105
+ naming a live, resolving emitting line took `Query: none` and validated, because the "only when
106
+ nothing is instrumented" rule lived in the stage prompt and nowhere a card had to pass.
107
+ `watch check`, `watch arm`'s post-merge screen and every other view print the form as
108
+ `unobservable — <reason>` with its source, through one renderer; the Watch handoff lists the
109
+ card under an `**unobservable**` line rather than quietly omitting it. Prose under `## Query` is still refused, in the same words as before
110
+ — the new form is a shape a reader can recognise, not permission to describe a query.
111
+ - **`run.yml` grows an additive `outcome:`, so a run can no longer read `done` over nothing
112
+ delivered (#210).** Run STATUS is a roll-up of the execution path and nothing else — every
113
+ stage of a run whose stories all blocked is terminal, so `deriveRunStatus` calls it `done`,
114
+ correctly, and until now that was the only word anyone got. `outcome:` is written ONCE when the
115
+ run closes, by all three commands that close one (`tldrx next` closing the last stage,
116
+ `tldrx approve` signing the last gate, and `tldrx run cancel`), and carries
117
+ `kind: delivered | partial | nothing-delivered | n/a` with the counts and the first blocked
118
+ story. It is rendered by `tldrx run status` (`status done — nothing delivered: 0 of 3 stories;
119
+ S1 — …`), by `tldrx status`, by the `run.finished` notification, by the dashboard model
120
+ (additive; `DASHBOARD_MODEL_VERSION` stays 3) and by the `ship` PR body — one renderer, six
121
+ surfaces, so none of them can disagree about what a run delivered. The two absences are named
122
+ rather than invented (§7): a `run.yml` written before the field reads `not recorded` with the
123
+ reason in it, and a run with no Build phase — a docs-scope run — reads `n/a` with `why`, never
124
+ a confident `0 of 0`.
125
+ - **A truncated input is now told to the OWNER, not only to the sub-agent (#207).** The inputs
126
+ budget has always written a "truncated inputs: …" line into the prompt itself, so the agent
127
+ knew; the person paying did not. Measured on a real workspace: a 168,873 B `facts.yml` reached
128
+ an `effort: high` design turn as 86,571 B under the 98,304-byte `inputs_max_bytes` of the day
129
+ (0.14.1; the default is 256 KB since #208), that
130
+ turn ran to its `timeout_s`, was killed, and booked nothing — and the only record that half the
131
+ fact ledger had been dropped lived in `.agent/how/prompt.md`, a file nobody opens until after
132
+ the failure it explains. The facilitator now appends one `input.truncated` event per cut input
133
+ AT SPAWN, carrying `stage`, `path`, `bytes`, `inlined_bytes` and `cap` — all four numbers, so a
134
+ reader never has to reconstruct one. `tldrx run status --verbose` lists them under the stage
135
+ the run is parked on, and the `stage.done` / `run.failed` / `status` notification summaries end
136
+ with one sentence naming them (*"1 input truncated: facts.yml 169 KB → 87 KB (cap 96 KB)."*).
137
+ **No tenth notify kind**: a truncation is a caveat on a moment already being announced, and a
138
+ new kind would have to be handled by every adapter already written against the nine. Absent
139
+ when nothing was cut, so an ordinary run's log and summaries are byte-identical to before.
140
+ - **A turn killed on `timeout_s` keeps the usage the provider had already streamed (#207).**
141
+ Before this, fifteen minutes of real `effort: high` compute booked an all-zero `usage` block —
142
+ honestly labelled `metered: false`, and still throwing away token counts that had already
143
+ crossed the wire. `agent.result` now carries an additive `usage_basis`, present only on a
144
+ killed turn: `partial-before-kill` means the counters beside it are the last frame the provider
145
+ streamed — a floor on the turn, never its total — and `absent` means not one frame arrived,
146
+ with `unmetered_reason` saying so in words rather than leaving a confident zero to be read as a
147
+ measurement. `tldrx cost` prints the basis under the attempt. **No dollar figure is invented in
148
+ either case**: measured against `claude` 2.1.251, the stream reports tokens per assistant
149
+ message and dollars only on the final `result` line, which a killed process never reaches, so a
150
+ USD number here could only be one this framework computed itself. `metered` stays `false` and
151
+ nothing sums these tokens into a spend.
152
+
153
+ ### Changed
154
+
155
+ - **A stage's `timeout_s` now bounds a turn a real model can finish: 900 s → 7200 s
156
+ (#207).** It has always been a PER-TURN clock — a Build stage that dispatches six stories
157
+ gives each spawn its own — and 900 s was set before anyone had watched a week of unattended
158
+ runs. Measured 2026-09-07/09 across three real workspaces: Opus turns on real repositories
159
+ run 15-50 minutes, so a quarter of an hour sat under the MEDIAN of the work this framework
160
+ dispatches. It was not a safety net; it was the most common way a turn ended, and it killed
161
+ a `how` turn and two Build developer turns in a single day — each one a paid turn thrown
162
+ away with its output. Two hours is above the longest turn observed, so the next turn to hit
163
+ it is genuinely stuck rather than merely large. The shipped stage files that set the key
164
+ moved with it (`build` 1800, `watch` 900), and `PRECONDITION_TIMEOUT_S` is untouched at 60:
165
+ a precondition is a liveness question, and letting a hung `docker info` borrow a Build
166
+ stage's clock is exactly the waste that key exists to prevent.
167
+ - **The context ceilings stop refusing prompts the model would have read: `prompt_max_bytes`
168
+ 163840 → 409600, `inputs_max_bytes` 98304 → 262144.** Both were sized just above the largest
169
+ thing this repo had SEEN in 2026-08, which is a reasonable way to set a ceiling once and a
170
+ bad way to keep one. Measured on the same three workspaces: a `how` stage built a 202 KB
171
+ prompt and was refused by a ceiling whose own message called that prompt "29% of a 200k
172
+ window"; and a 169 KB `facts.yml` was sliced to 96 KB — named, as a truncation always is,
173
+ but naming a loss the operator cannot afford is not the same as not taking it. Neither is
174
+ unbounded: 400 KB is still well under the smallest window in `budget/modelPrices.ts`, so the
175
+ first prompt past it is still one worth stopping. `knowledge_max_bytes` is deliberately
176
+ unchanged at 49152 — trained knowledge is the section nobody asked for, and the one the
177
+ refusal tells you to cut first.
178
+ - **The context line no longer quotes a window it cannot source.** `contextTokensFor` fell
179
+ back to a DOCUMENTED 200 000-token default for a model the price table has never heard of,
180
+ and the ledger printed that as if it were the model's own window. On the 1M-window models
181
+ these runs use, it understated the window by 5x — inside a refusal, which is how a ceiling
182
+ ends up arguing against itself. A window with no evidence behind it is now simply absent:
183
+ the line prints the token estimate and stops, with no percentage and no figure, the same
184
+ rule `priceFor` has always followed for prices. `knownContextTokensFor` is the null-returning
185
+ half; `contextTokensFor` is that plus the documented fallback, so there is still one table
186
+ and one derivation.
187
+ - **A phase ceiling now holds every attempt its stages may take, so a retry is not refused by
188
+ arithmetic (#170).** `run new` sized a phase for exactly ONE attempt while the framework
189
+ granted two, so the first retry of a stage that had spent anything at all was refused for
190
+ money the run had been given. Measured: a `what` stage that died with $0.29 spent left
191
+ $18.91 of its $19.20 phase, and `next` refused with "phase 01-what has $18.91 left and the
192
+ stage estimate is $19.20" — the estimate is the whole stage, and part of it had already been
193
+ charged. The operator's only move was to raise money for work paid for once. `planBudget`
194
+ now splits the run ceiling by `attempts × budget_usd`, so the STAGE figure stays what one
195
+ attempt costs — the figure the brake is checked against — and the PHASE holds all of them,
196
+ the way `worstCaseShares` has always sized Build stories. The shipped `default_budget_usd`
197
+ figures and `templates/budget.yml` doubled at the same commit, so the per-stage dollars an
198
+ existing workspace sees are unchanged. `on_exceed` and the grant/ceiling reconciliation are
199
+ untouched.
200
+
201
+ - **`warn_at_pct` still fires at the same real dollars.** A phase now holds every
202
+ attempt its stages may take, so measuring 80% against the raw ceiling would need
203
+ roughly twice the real spend on a run that never retries — the warning would
204
+ arrive after the money it was warning about had gone, which is a warning that has
205
+ stopped being one. `wouldExceed` measures it against **one attempt's share**
206
+ (`ceiling ÷ attempts`) and carries that figure as `warnBasis`. The refusal is
207
+ untouched: `exceeds`, `remaining` and `on_exceed` still answer for the whole
208
+ phase, because the phase may spend all of it. Owner decision 2026-09-09.
209
+ - **Everything that prints "attempt N of M" reads the stage that dispatched the
210
+ story.** The dashboard printed it off the global constant for every story of
211
+ every run (`RunModel.maxAttempts` is additive, so `DASHBOARD_MODEL_VERSION` does
212
+ not move — §7's rule is that additions do not bump it), and `story reopen
213
+ --for-fix` said "attempt 1 of 2" whatever the stage declared. One resolver,
214
+ `buildStageDefaults`, and it is tolerant: an unreadable workflow gives the
215
+ shipped pair rather than throwing on a page render.
216
+ - **The maintain skill says which sha a review record's `against:` must name (#207).** Both
217
+ SKILL.md §3 and the reviewer brief now state it once: the **code head** — the branch's last
218
+ commit that is not itself a `.review/` commit — never the record commit's own sha, and never an
219
+ amended one. Measured 2026-09-09: amending a record commit replaces its sha, and
220
+ `scripts/merge-wave.sh` then refuses the branch with exit 10, correctly. The rule cost a wave
221
+ and was written down nowhere an agent reads before it happens; `test/maintain-skill.test.ts`
222
+ now pins the literal phrase in both documents.
223
+
224
+ ### Fixed
225
+
226
+ - **The DoD-gate hook re-runs a story's commands on the STAGE's clock, not on a
227
+ private 900 s constant.** `src/hooks/dod-gate.ts` carried
228
+ `const DEFAULT_TIMEOUT_S = 900` with a comment citing spec §2.3, and never opened
229
+ a stage file — so a workspace that had deliberately given its Build stage a
230
+ different `timeout_s` got 900 s in the gate and something else in the turn, and
231
+ this hook re-runs the very commands that turn ran. Measured RED: under a stage
232
+ declaring `timeout_s: 2`, a `sleep 30` in a `dod` block ran to completion and the
233
+ gate ALLOWED — "no deny decision (exit 0)". It now resolves story `timeout_s:` →
234
+ the Build stage's → the shipped default, which is the order §2.3 and §7's
235
+ DoD-gate row have always described.
236
+
237
+ - **A Build gate now says what the stage DELIVERED, on every gate policy — the counting existed
238
+ and ran for `auto` gates alone (#210).** Measured on tldrx 0.14.2 across two real workspaces,
239
+ 2026-09-09: two engine-driven runs reached the end, both printed `run <id> is done`, both had
240
+ their Build gate approved by the owner from his phone, and both delivered ZERO stories. The
241
+ whole of what he had to decide on was
242
+ `{"phase":"04-build","cost_usd":1.78,"outputs":["04-build/handoff.md"],"checks":["claim-sources:passed"]}`
243
+ — a dollar figure, one output and one green check. The information existed on disk at that
244
+ instant: the same run's `04-build/handoff.md` `## Findings` read
245
+ `S1 · … — blocked — … npm run test exited 127 …`. `storiesCondition` in `autoGate.ts` computed
246
+ exactly the missing view — `N of M done`, with the unfinished ones named — but `evaluateAutoGate`
247
+ is called only when `policy === "auto"`, and both of these gates were `human`, so nothing counted
248
+ and nothing carried it. The derivation is lifted into one leaf (`core/run/runOutcome.ts`, §7 one
249
+ implementation per derivation) and `gate.requested` for a Build stage now carries
250
+ `stories: {total, done, in_progress, review, blocked, todo}` plus `blocked_story` and
251
+ `blocked_reason` — the FIRST blocked story's own words, read out of the handoff's `## Findings`
252
+ through the parser that lives beside the renderer that wrote it. The same sentence goes in the
253
+ notification's **summary** and not only its detail (the argument #203 already won for `held`: the
254
+ summary is the half that reaches a lock screen), on the terminal line under `<phase>/<stage> done`,
255
+ and at the head of the decision card. All four keys are absent on a non-Build stage and on a run
256
+ with no plan — a `stories` block of zeroes there would say a plan was read and found empty.
257
+ - **`tldrx ship` refuses a run that delivered no story instead of opening a PR over nothing
258
+ (#210).** It already knew: `shipBody.ts` reads the handoff's `## Findings`, keeps only the
259
+ bullets that say `done`, and with none of them writes ``- (nothing settled `done` in this run)``
260
+ — into a pull request it created anyway. That is a review request for a diff that does not
261
+ exist. It now refuses with exit `1` (the "nothing behind it" family, not `2`: no gate said no,
262
+ there is simply nothing to open a PR from), naming the counts and the first blocked story's
263
+ reason, and `--dry-run` is refused in the same words. A run that delivered at least one story
264
+ ships exactly as it did, and its PR body gains an **Outcome** header line so a reviewer reads
265
+ the ratio before the list. The refusal sits AFTER the no-handoff check, because the handoff is
266
+ the document the reason is read out of.
267
+ - **A story's Definition of Done exiting `127` is reported as an absent binary, not as a red
268
+ test (#209).** Measured on two real workspaces at 0.14.2: the Build-entry pre-flight recorded
269
+ `npm run test` → exit 0, `Ran all test suites.` in the human's checkout, and minutes later the
270
+ same command in the story's fresh worktree recorded `exit_code: 127`,
271
+ `detail: "sh: jest: command not found"`. The story blocked, verdict `n-a`, no reviewer, no
272
+ commit — with a full developer turn already paid for. Nothing about the story was wrong: 127
273
+ is `command not found`, the base tree has `node_modules` and a `git worktree` is a fresh
274
+ checkout of tracked files that has none. `dodRunner.ts` already documented that trap for the
275
+ base tree in as many words ("a pristine worktree would fail half the world's repos for want of
276
+ `node_modules`") while the story's tree ran under it untouched. A 127 now carries the reason
277
+ with it: the test command's binary is absent in the worktree, named where the shell named it,
278
+ with the `install:` slot given as the fix. Nothing is guessed in the process — not `npm ci`
279
+ from a lockfile, and not a symlink of the base tree's `node_modules`, which the message offers
280
+ as an option and explicitly does not take, because hoisting and cache layout are per-tree facts
281
+ a shared tree can be silently wrong about. Absent-with-reason, applied to an exit code.
282
+ - **A developer may run its declared commands WITH ARGUMENTS (#209).** From the same run's
283
+ transcript: the developer never ran its own Definition of Done either. Every attempt —
284
+ `npx jest …`, `npm run test -- app/dev/__tests__/…`, three times bare — came back
285
+ `"This command requires approval to run"`, so the 127 above was first seen by the gate, after
286
+ the turn had been paid for, by an agent that could not have seen it sooner. The cause was the
287
+ allowance: one `Bash(<declared command>)` per command, and Claude Code's permission grammar
288
+ makes that rule EXACT — its own docs say `Bash(npm run build)` "Doesn't match
289
+ `npm run build --watch`". Every declared command now emits both that exact grant and the
290
+ trailing-argument one (`Bash(<command> *)`, which the docs also spell `Bash(<command>:*)`),
291
+ from one derivation both tool lists share. The reviewer's allowance is untouched and still
292
+ read-only: `Read`, `Grep`, `Glob`, `Bash(git diff *)` — already the trailing-wildcard grammar.
293
+ - **A story's DoD check says which tree it ran in (#209).** `tree: "worktree"` on every
294
+ `check: "dod"` payload, and on the new install check. The pre-flight and the story ran the same
295
+ command and wrote the same shape of record, and nothing in either said they were different
296
+ trees — so a green pre-flight beside a story's 127 read as a contradiction instead of as the
297
+ environment gap it was. A `127` whose tail names the binary also carries `absent_binary`.
298
+
299
+ - **A red Definition-of-Done command's records name the FAILURE, not the last warning on
300
+ stderr (#211).** `check.failed`'s `detail`, `04-build/log/<story>.md` under both
301
+ `## Definition of done` and `## Why it is not done`, the handoff Finding and the notify
302
+ summary all carried ONE sentence, and that sentence was
303
+ `lastMeaningfulLine(stdout + "\n" + stderr)` — the last non-empty line of the two streams
304
+ concatenated, which is the last line of stderr whenever stderr wrote anything at all.
305
+ Measured on a real workspace: a story's gate script exited 1 and the only thing any record
306
+ kept was `sys:1: DeprecationWarning: builtin type swigvarlink has no __module__ attribute`.
307
+ Which test failed was in no record, the worktree had been deleted, and the owner could not
308
+ tell what broke without re-running the suite. A host had raised the same thing in prose a
309
+ week earlier.
310
+ Now: the last 200 lines (or 16 KB, whichever is smaller) of the command's combined output go
311
+ to `04-build/log/dod-output/<story>-<n>.txt`, one file per red check, written before the
312
+ worktree is removed. `check.failed` gains `output_path` and `output_bytes`, and its `detail`
313
+ becomes up to 5 failure-looking lines — `FAIL`, `Error`, `assert`, `Traceback`, `not ok` and
314
+ their siblings, the last 5 lines when none match, never the first line of stderr — bounded at
315
+ 1024 bytes so this event, of all events, can never be the one whose `detail` §2.9's
316
+ 4096-byte cap strips (#160). The story log quotes the excerpt in a fenced block and cites the
317
+ file `[src: …:1]`; the blocked-story reason and its handoff Finding carry the same citation;
318
+ and a story a person reopens now hands its next developer the log under `## Previous
319
+ attempt`, so the agent reads the real failure instead of rediscovering it in a worktree that
320
+ no longer exists. One heuristic in one helper (`failureExcerpt` in
321
+ `src/core/build/dodOutput.ts`), one path derivation (`dodOutputRel`), and a green command
322
+ still writes no file and emits the byte-identical event it always did. `not found` is in the
323
+ heuristic for a measured reason: a shell's `sh: <bin>: command not found` is the only line an
324
+ exit 127 has, and #209 reads the absent binary's name off `tail` — without it `absent_binary`
325
+ went from `dodbin` to `""` on the rebase. The citation resolves
326
+ through the repo's existing `lineOf()` to the line the excerpt starts on, never a constant
327
+ `:1`, and the heuristic reads bun's own `(fail)` summary line as well as the capitalised
328
+ spellings.
329
+ Two consequences worth stating in their own right. **The kept output may carry secrets** — it
330
+ is raw stdout and stderr, so an `env` dump, a token in a connection string or a stack trace
331
+ with a credential all land in it — and `tldrx-work/` is state this framework tells owners to
332
+ COMMIT. So `tldrx init`'s managed `.gitignore` block now excludes
333
+ `tldrx-work/**/04-build/log/dod-output/`, after the `!tldrx-work/**` re-include so it wins:
334
+ the tails stay on the machine that measured them, and the bounded excerpt inside
335
+ `04-build/log/<story>.md` is what goes into history. Delete that one line to opt in, and read
336
+ a tail before sharing it. And **the `## Previous attempt` header now says which kind of
337
+ attempt it was**: it was unconditionally "A reviewer read your last attempt … and asked for
338
+ changes", which over a DoD-only block would have sat above a log reading
339
+ `Verdict: n-a · Reviewer: not recorded` — a prompt asserting a review that never happened.
340
+ One renderer, two headers, chosen by data the executor already has.
341
+
3
342
 
4
343
  ## 0.14.3 — 2026-09-10
5
344
 
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.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 |
320
+ | 0.15.0 | 2026-09-10 | `beta` | defaults for the models actually running today, and records that name what happened: measured 2026-09-07/09 across three real workspaces, the first engine-driven run of each was ended by a calibration rather than by the work — a `how` turn and two Build developer turns killed at a 900 s per-turn clock while Opus turns on real repositories run 15-50 minutes, a 202 KB prompt refused by a ceiling whose own message called it "29% of a 200k window", and a 169 KB `facts.yml` sliced to 96 KB on its way into a design turn that then died. So a turn gets two hours (`timeout_s` 900 → 7200), a prompt 400 KB and inputs 256 KB, a phase ceiling holds every attempt its stages may take so the first retry of a stage that spent anything is no longer refused by arithmetic — `warn_at_pct` still measured against one attempt's share, so the warning still arrives before the money — and the four numbers that were calibrations rather than invariants (`attempts`, `fixlist_rounds`, `reviewer_share`, `gate_signer_share`) became optional `stage.yml` keys, refused by name out of range instead of clamped, absent meaning today's constant byte for byte, with `tldrx run auto --prompt-max-bytes` and `--max-reads` for the unattended run that would otherwise need a file edit to get past one refusal; a story's Definition of Done now runs with its dependencies installed — the `install:` slot has sat unread in `templates/workspace.yml` since the beginning and now runs in every fresh story worktree through the same allowlist-and-argv runner, recorded with its own exit code and duration, blocking the story rather than paying a turn to discover it — an exit 127 is reported as a named absent binary and not as a red test, a declared command may be run WITH ARGUMENTS (the exact `Bash(npm run test)` grant matched nothing the developer actually typed, so its own 127 was first seen by the gate, after the turn was paid for), and every DoD check says which tree it ran in; the Build gate now names story outcomes on every policy and not only `auto` — two runs approved from a phone printed `run is done` over zero stories delivered — `run.yml` records an additive `outcome:` written once by all three commands that close a run and rendered by six surfaces, and `tldrx ship` refuses with exit 1 instead of opening a PR over nothing; a red DoD keeps its real failure — the last 200 lines on disk (gitignored, since a tail can carry a secret), up to five failure-looking lines as the detail rather than the last `DeprecationWarning` on stderr, the failing line cited at the line it starts on, and the next attempt told it was the check and not a reviewer; a watcher card may honestly say `Query: none — <reason> [src: …]`, earned only over a card whose own `## Signal` cites `absent:` and refused like any unsourced item otherwise, after a stage spent real money writing the honest answer and was refused for it; a truncated input is told to the OWNER at spawn and not only to the sub-agent, a turn killed on timeout keeps the usage it had already streamed and never a price; and the maintain skill says which sha a review record must cite — the code head — a rule that cost a wave and was written down nowhere an agent reads |
319
321
  | 0.14.3 | 2026-09-10 | `beta` | foreign uncommitted work no longer stops a Build, and the dashboard flake that blocked four merges in two days has a root cause: the dirty-tree guard used to count every `git status --porcelain` entry and refuse, offering only "commit it" or "stash it" — neither of which an agent may take with another person's files — and measured across three real workspaces on 0.14.2, every first engine-driven run reaching Build stopped at `04-build`, over seed docs, a data export and one untracked note; the dirt is now classified, `own` and `overlapping` refusing or passing exactly as before while everything `foreign` is set aside with a pathspec-limited `git stash push` as the LAST step before the epic branch is cut, recorded as `worktree.foreign_work_aside` and given back with `--index` on every exit path, success or failure, nothing ever deleted and nothing force-popped, a repo mid-merge, rebase, cherry-pick or bisect refused outright because that state has no clean undo, and a pop git refuses said as the stage's last line and carried into the handoff and the notification; the refusal's printed remedy is now the SAME string the engine runs, limited to the paths it listed and relaunching by mode, after an owner ran the pathspec-less line exactly as printed and it swept the run's own records under `tldrx-work/<run>/` into the stash until `tldrx next` answered `no run`; every path handed to git for a write is `:(literal)` and `git status` is read with `-z`, since a glob pathspec moved the neighbouring `x.txt` for a file called `[x].txt`; and the dashboard's live tests stop racing a typed millisecond — five consecutive runs of the two files went red 3 times, at 5084.27 / 5108.01 / 5256.49 ms against a hard-coded 5000 under load averages 65–107 on 14 cores — every deadline now deriving from one `eventWaitMs()` helper that scales like every other budget, with `test/machine-load.test.ts` refusing a hard-coded deadline in either file so it cannot come back at somebody's merge, while that measurement surfaced the product half: `watchWorkspace` armed its mtime sweep only in `poll` mode, so a dropped FSEvents notification left a live dashboard silently stale for the life of the process — measured with `fseventsd` at 98–115% CPU, directory events that never arrived AT ALL at 82,556 ms and 113,942 ms — and the sweep now runs in watch mode too, at 2 s, so a dropped notification is bounded rather than fatal |
320
322
  | 0.14.2 | 2026-09-09 | `beta` | every citation check starts from a fresh view of `questions.md` and `facts.yml`: `srcToken.ts` memoised both indexes at module scope and nothing outside `test/` ever dropped them, so in `tldrx run auto` — one Node process for a whole run — the FIRST citation resolved anywhere froze the view every later stage was then judged against; measured on three real unattended workspaces at 0.14.1, a `how` stage refused with `no such question Q2 … declared: Q1` over questions it had itself written minutes earlier, and another refused over `145 live fact(s)` when `facts.yml` held 148, the three extra written by the owner's answers two seconds before the stage started — roughly $11 of paid turns thrown away for ids that were real the whole time; the indexes are now refreshed inside `toSrcContext`, the one place a citation context is built and a place every caller reaches exactly once per check, gate or hook, so a document's forty citations still read `facts.yml` once while nothing survives the check that read it |
321
323
  | 0.14.1 | 2026-09-09 | `beta` | an auto gate that says what holds it and closes itself, and three derivations cut to one: measured on a real workspace at 0.14.0, the first `run auto --wait-answers 4h --wait-gates 4h` with a notify hook finished its What stage for $1.98 with every declared check green and told the owner *"waiting at a auto gate that did not close by itself — a person signs it"* — no reason, because `evaluateAutoGate` had computed exactly that sentence into a stdout line nobody was watching while `gate.requested` was appended a hundred lines BEFORE the verdict existed; the verdict is now taken one statement earlier and `why`/`held_by` ride the event to the phone (present only for an `auto` policy, since `held_by: []` on a `human` gate would read as "checked, and nothing held it"), the questions that hold a gate are notified BEFORE the gate downstream of them so the owner stops being told to sign a thing before being told what it is, and an `auto` gate whose only blocker was an open question stops permanently degrading into a `human` one — each poll re-runs the seven conditions off disk and signs through the same `approve` door `next` uses, never for `human` and never for `agent`, a person's `approve` or `reject` still landing first; a question can carry its own `Recommended: <letter> — <why> [src: …]`, parsed tolerantly so an older note reads as not recorded rather than refusing; `tldrx facts add --repo <name>` is now checked against `workspace.yml` through the ONE leaf `answer --repo` already refused on, because the unvalidated flag wrote `repos: [ghost]` at exit 0 and every prompt or filter keyed on a real repo name was then silently blind to that fact; the five phase ids are written out in one file instead of three — `PHASE_IDS`, `QUESTION_PHASES`, and an inline literal walked by `questions lint` that `QUESTION_PHASES`' own doc comment claimed could not exist — pinned from both ends, because an identical second copy and a copy that has already drifted redden different guards and neither alone is enough; a training test stops running on bun's fixed 5000 ms after a spawn probe, validated first against a file the heuristic already claimed (87 spawns recorded, so the instrument can see the thing), found it spawning three real children unclaimed by any marker, with the 14 further unclaimed files filed rather than fixed; and `release-check.sh` refuses a rewritten released section — every dated heading whose tag is present must equal `git show vX.Y.Z:CHANGELOG.md` byte for byte, three sections having drifted with nothing checking, and an amendment recorded in `CHANGELOG.amendments` must still contain the tag's section as an ordered subsequence and may only add lines that exist verbatim in its source sha, after a reviewer proved the first version of that check happily passed an invented bullet |
@@ -1,23 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  conflictOf
4
- } from "./chunk-taj1vrbe.js";
4
+ } from "./chunk-z0hnthw4.js";
5
5
  import {
6
6
  FactsStore,
7
7
  formatJaccard
8
- } from "./chunk-mzwqb08p.js";
8
+ } from "./chunk-54vzevgt.js";
9
9
  import {
10
10
  parseHookInput,
11
11
  readStdin
12
- } from "./chunk-tpz0zb0t.js";
13
- import {
14
- PHASE_IDS
15
- } from "./chunk-dzkg8c68.js";
12
+ } from "./chunk-ds9sew2e.js";
16
13
  import {
17
14
  EventLog,
18
15
  PHASE_ID_RE
19
- } from "./chunk-mt8f2czb.js";
20
- import"./chunk-db7wf3hy.js";
16
+ } from "./chunk-rz0qr006.js";
17
+ import {
18
+ PHASE_IDS
19
+ } from "./chunk-d0rp8c68.js";
20
+ import"./chunk-k4nqzdw5.js";
21
+ import"./chunk-nqwg5ddj.js";
21
22
  import {
22
23
  ADVISORY_KEY,
23
24
  MAX_FACT_CHARS,
@@ -27,7 +28,7 @@ import {
27
28
  renderQuestionBlock,
28
29
  replaceBlock,
29
30
  serializeQuestions
30
- } from "./chunk-bqmstq21.js";
31
+ } from "./chunk-fm141jak.js";
31
32
  import {
32
33
  ITERATION_ONLY_SLOT,
33
34
  PROJECT_FRAMEWORK_DIR,
@@ -35,7 +36,7 @@ import {
35
36
  commandProbeIssues,
36
37
  factsPath,
37
38
  parseYaml
38
- } from "./chunk-wwwmkey4.js";
39
+ } from "./chunk-77y6q9mt.js";
39
40
 
40
41
  // src/hooks/answer-capture.ts
41
42
  import { existsSync as existsSync5 } from "fs";
@@ -1,48 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-dxk7eaqn.js";
4
+ } from "./chunk-vmhfw2r1.js";
5
5
  import {
6
6
  allow,
7
7
  deny,
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-gvz0rzb9.js";
12
- import"./chunk-tpz0zb0t.js";
11
+ } from "./chunk-msswet35.js";
12
+ import"./chunk-ds9sew2e.js";
13
13
  import {
14
14
  asRunBudget,
15
15
  currentActor,
16
- cursorStage,
17
16
  economyFor,
18
- hostTokensIn,
19
- isAttendedByHostView,
20
17
  isHostTokens,
21
- loadRunView,
22
- newestActiveRun,
23
18
  nowRfc3339,
24
19
  raiseCommand,
25
20
  remainingWork,
26
- renderRunEconomies,
27
- runSpend,
28
21
  shortBy,
29
22
  validateRunBudget,
30
23
  wouldExceed,
31
24
  wouldExceedHostTokens
32
- } from "./chunk-fefnr71h.js";
25
+ } from "./chunk-3kmx3dmz.js";
33
26
  import {
34
27
  EventLog
35
- } from "./chunk-mt8f2czb.js";
28
+ } from "./chunk-rz0qr006.js";
29
+ import {
30
+ cursorStage,
31
+ hostTokensIn,
32
+ isAttendedByHostView,
33
+ loadRunView,
34
+ newestActiveRun,
35
+ renderRunEconomies,
36
+ runSpend
37
+ } from "./chunk-4mjxyfp9.js";
38
+ import"./chunk-k4nqzdw5.js";
36
39
  import {
37
40
  noteDeprecations
38
- } from "./chunk-bqmstq21.js";
41
+ } from "./chunk-fm141jak.js";
39
42
  import {
40
43
  PROJECT_WORK_DIR,
41
44
  findWorkspaceRoot,
42
45
  locateWork,
43
46
  parseYaml,
44
47
  stageYamlPath
45
- } from "./chunk-wwwmkey4.js";
48
+ } from "./chunk-77y6q9mt.js";
46
49
 
47
50
  // src/hooks/budget-gate.ts
48
51
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";