tldr-experts 0.32.0 → 0.34.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,291 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.34.0 — 2026-09-17
4
+
5
+ ### Fixed
6
+
7
+ - **`budget show`, `run estimate` and the `budget-gate` hook now price a Build stage's
8
+ `reviewer_share`, `story_cap_multiplier` and `story_cap_floor_usd` the same way `tldrx next`'s
9
+ brake does, instead of the shipped defaults (see #333).** #214 fixed exactly one of the four
10
+ `stage.yml` calibrations `remainingWork` takes — `attempts` — leaving these three still
11
+ defaulted by the three readers while the brake resolved all four through `spec.tuning`.
12
+ Measured against the Build fixture (three stories priced 2.00/2.50/4.50, two done, stage
13
+ ceiling $60): a stage that writes `story_cap_multiplier: 1` ("take the plan's price
14
+ literally") was quoted $24.25 by `budget show`/`run estimate`/the hook against the $10.75 the
15
+ brake would actually price and dispatch — 2.3x — with the phase ceiling able to sit between
16
+ the two, so `budget show` said BLOCKED and the hook denied a `tldrx next` the brake itself
17
+ allows. `buildStageDefaults` (`core/run/workflowPreset.ts`) now resolves and returns the
18
+ stage's full `StageTuning`, not just `attempts` and `timeout_s`, by re-reading the same
19
+ `stage.yml` `facilitator/stageSpec.ts`'s `overlay` already reads — tolerant exactly as before,
20
+ so an unreadable stage file still falls back to the shipped defaults. `run estimate` already
21
+ held the full tuning via `loadStageSpec`; it was just not forwarding three of the four fields
22
+ to `remainingWork`.
23
+ - **`round2` and the spec §3 exit-code table are each defined in exactly one place, with a shape
24
+ test refusing a second copy (see #174).** `round2` (`Math.round(n * 100) / 100`) was
25
+ independently restated in nine files; the spec §3 exit codes (`cli/exitCodes.ts`) were
26
+ independently restated, under a mix of matching and diverging names (`EXIT_REFUSED` for
27
+ `EXIT_GATE_REFUSED`, `EXIT_AWAITING` for `EXIT_AWAITING_HUMAN`), in fourteen more — wider than
28
+ the issue's own grep found, which only named `runNext.ts`/`runAuto.ts`; the broader set was
29
+ measured here (`grep -rlE '^const EXIT_[A-Z_]+\s*=\s*[0-9]+;' src/`) and folded into the same
30
+ fix rather than filed separately, since it is the identical defect the issue is about. Every
31
+ local copy now imports the canonical one (`core/build/caps.ts`'s exported `round2`,
32
+ `cli/exitCodes.ts`'s table, aliased where a file's own name differed) instead of restating it.
33
+ Mechanism this was hiding: a golden-guard mutation of `cli/exitCodes.ts` left
34
+ `test/build-golden.test.ts` byte-identical, because the Build path never read that file's
35
+ constants at all — it read `runNext.ts`'s and `runAuto.ts`'s own private copies. The new
36
+ `test/round2-exitcodes-one-derivation.test.ts` asserts, on the SHAPE (a local, unexported
37
+ `function round2(`/`const EXIT_*` declaration) rather than on the measured names, that no file
38
+ under `src/` besides the two canonical ones defines either — on the precedent of
39
+ `test/phase-ids-one-derivation.test.ts` (#187): deleting today's copies does not stop a third
40
+ one from coming back under a name this file never mentions.
41
+ - **`merge-wave.sh --status` now says why it could not look instead of answering `idle` (see
42
+ #332).** AGENTS.md §2 calls this command the authoritative instrument for "is a wave alive" —
43
+ the reference-transaction guard and the merge lock exist precisely so nobody has to `ps` plus
44
+ a marker's mtime. Measured on the first autonomous run against this repo (2026-09-17): run
45
+ from outside a git repository, or with its `merge-lock.sh` sibling missing, it answered `idle`
46
+ and exit 0 — a false all-clear that could start a wave on top of one already live. It now
47
+ prints `unknown: <reason>` and exits **15**, its own code (`14` stays release.sh's — see
48
+ AGENTS.md §2 and docs/RELEASING.md), so a caller polling stdout can no longer read "could not
49
+ check" as "nothing is happening".
50
+ - **`merge-wave.sh` now diffs a released, dated CHANGELOG section's content, not just its
51
+ heading, so a rebase-then-merge across a release can no longer land a bullet inside it
52
+ silently.** The exit-12 gate only ever counted unreleased headings; a branch written before
53
+ a release and rebased onto main after that release merged CLEANLY — no conflict, no heading
54
+ touched — and its bullet landed inside the now-dated, supposedly immutable section, caught
55
+ only by a human reading the diff or much later by `release-check.sh`'s own #200 check. The
56
+ wave now compares every dated section against what main had immediately before the merge and
57
+ refuses with its own exit **16**, naming the section and the first difference (see #336).
58
+ - **A spawning test run whose `$TMPDIR` sits inside a git work tree can no longer commit its
59
+ scratch paths onto the enclosing branch (#335).** Fixtures resolve their sandbox through
60
+ `os.tmpdir()`/`$TMPDIR` and run real git inside it; when that path is inside a worktree,
61
+ git's own upward repo search walks out of the fixture and finds the WORKTREE's repo, so
62
+ `git add`/`git commit` there land on whatever branch is checked out — #335 measured 960
63
+ tracked scratch paths and dozens of foreign commits on a real branch this way, and the same
64
+ underlying mechanism reproduced here on `490f637` from a fresh throwaway worktree as the same
65
+ two test failures the issue reported (`test/gitignore-shadow.test.ts`,
66
+ `test/install.test.ts`). The `bun test` preload `test/preload/tmpdirGuard.ts` now refuses,
67
+ named, before any test runs, when the resolved `$TMPDIR` is inside a git work tree — opt out
68
+ only with `TLDRX_ALLOW_TMPDIR_IN_WORKTREE=1`, once the isolation is verified some other way.
69
+ - **A turn killed mid-flight by SIGINT/SIGTERM now banks its cost as absent, not a measured
70
+ `$0.00` (see #355).** `interrupt.ts`'s `recordPartialResult` wrote `cost_usd: 0` with no
71
+ `metered` key on both the task row and the `agent.result` payload — legal by `RunFile.ts`'s
72
+ validator, which only ever checked the converse (a `null` needs `metered: false`), so it
73
+ passed silently while `tallyOf` summed the row into the metered bucket as a real, measured
74
+ zero and `spendBasis` called the run's total complete. It now writes `cost_usd: null,
75
+ metered: false` on the row and the payload, the same idiom every other unknown-cost turn in
76
+ this codebase already uses, so a Ctrl-C mid-turn reads as a stated lower bound.
77
+ - **A killed parent's abandoned Build-story turn now leaves a row in `run.yml`, not nothing (see
78
+ #337).** The headless half of this shipped earlier as the orphan case #337 was filed for; the
79
+ fix deliberately skipped every Build-executor phase, reasoning the per-story ceiling ledger
80
+ already covered it — true for `tldrx cost`'s per-story view, but that ledger has no `run.yml`
81
+ task row to show for it, so `tallyOf`/`spendBasis` (and so `budget.spent_usd`) never saw the
82
+ abandoned attempt at all, not even as unmetered. `demoteStaleRunning` now reads which of a
83
+ stuck Build stage's `agent.spawned` turns no `agent.result` ever answered
84
+ (`build/orphanedTurns.ts`) and records one `cost_usd: null, metered: false` row per abandoned
85
+ story, same as the headless case.
86
+ - **The boundary gate now diffs the branch Build actually cut under `branch_model: integration`,
87
+ instead of an epic file's unused per-epic `branch:` label (see #225).** `epicTargets` read
88
+ `03-plan/epics/*.md`'s own `branch:` as the ref to diff — right under `per-epic`, where Build
89
+ cuts exactly that name, and silently wrong under `integration`, where every epic's stories
90
+ merge into ONE recorded branch and the epic's `branch:` is never cut at all. A run with eight
91
+ such epics measured `boundary=n/a (nothing could be diffed: … does not resolve …)` and
92
+ auto-signed a +31,465/−237-line PR across 117 files with the condition having diffed nothing.
93
+ The branch now comes from `run.yml`'s `build.epic_branch`, through the same `recordedEpicBranch`
94
+ derivation `watch` and `ship` already use (#90) — the epic file supplies repos and stories,
95
+ never the ref. A run that recorded an epic branch and still cannot diff it now HOLDS the gate
96
+ instead of reading `n/a`; `n/a` stays reserved for a run that named no branch to diff at all.
97
+ - **The boundary gate reads a run's stories through the same validated, all-phases walk `ship`
98
+ and carried-findings ownership already share, instead of its own `03-plan/`-only, schema-less
99
+ reader (see #189).** `boundary.ts`'s `storyTouches` was a third `stories/*.md` reader: it
100
+ looked only under `03-plan/` (missing a story filed under `04-build/`, the phase a carried fix
101
+ list's own story is written under), parsed front matter with no schema (taking a story a real
102
+ reader would report as unreadable), and dropped an unparseable file silently. It now calls the
103
+ shared `scanStories`/`phaseDirsOf` leaf (extracted from `build/carriedRows.ts` into
104
+ `build/storyScan.ts` so `boundary.ts` — which `carriedRows.ts` already imports from — does not
105
+ import it back), closing a divergence `carriedRows.ts` had documented as "somebody else's
106
+ issue" since #171.
107
+ - **An agent gate can no longer sign over evidence dated before the stage it is closing even
108
+ started (see #144).** A field evaluation of a delegated `watch` gate found its evidence note
109
+ timestamped 7h before its stage began, asserting facts already false by the time it signed,
110
+ with `refuted: 0` — the resolution half of that bug was #140; this is the remaining half
111
+ AGENTS.md §7 names: "a gate's evidence must not predate what it signs". `validateEvidence`
112
+ refuses a note whose `at:` is before the CURRENT attempt's own start — the new, additive
113
+ `RunStage.attempt_started_at`, which `markRunning` overwrites on every run, never the frozen
114
+ `started_at` it sits beside (§7: existing fields only grow, never change meaning; `started_at`
115
+ is set once, at the stage's first run, and stays that forever) — on both the engine's own
116
+ signer and `approve --as-agent`, one derivation, both doors. Two more holes pre-merge review
117
+ found in the same check: `at` was only confirmed non-empty, so a raw string compare read a
118
+ date-only `at` as earlier than a same-day, full-precision stage start; `at` is now confirmed a
119
+ real RFC3339 instant first, and the two timestamps are compared as instants, not strings. And
120
+ the full test gate caught the new check's own collateral: `tldrx learn`'s chapter 6 planted its
121
+ stand-in evidence note with a fixed past date, which the real wall clock had by now walked
122
+ past — its `at:` is stamped off `Date.now()` (with a 5-minute forward buffer, since the note is
123
+ written before the step that starts the stage it signs) rather than a date fixed at authoring
124
+ time.
125
+ - **`tldrx reject` now archives a stage's scratch evidence note instead of leaving it for the
126
+ next attempt to sign over (see #199).** A reject sent a stage back to `ready` but never
127
+ touched `.agent/<stage>/evidence.md`, so a note that had already said `verdict: sign` over one
128
+ attempt's outputs was still the first (and only) thing either signer found after the stage
129
+ re-ran with different outputs — and could sign again over an attempt it never read. The note
130
+ is renamed to `evidence.rejected-<at>.md` in the same scratch directory (moved, not deleted:
131
+ it is still the record of a real check somebody made), so the re-run meets the ordinary "no
132
+ evidence note at …" state instead. `reject` also advances the stage's `attempt_started_at`
133
+ anchor to its own moment (see #144's entry above) — closing a gap pre-merge review found:
134
+ without it, `approve --evidence <path>` pointed straight at the just-archived note still
135
+ signed, since nothing yet had moved the anchor past it. Archiving is collision-safe too
136
+ (`-2`, `-3`, … suffixes): two rejections landing in the same clock second used to let the
137
+ second silently overwrite the first's archive.
138
+ - **`reject --stage` (i.e. a revoke) no longer strands a later stage `running` with no verb that
139
+ owns it, and `run unlock`'s stranded-run note no longer promises a demotion `next` does not
140
+ make (see #228).** A revoke marked every later stage `stale: true` but left `status: running`
141
+ untouched; `next` only demotes a `running` stage behind a DEAD-PID lock, and a revoke leaves no
142
+ lock behind at all, so the cursor could walk back into that stranded stage mid-flight once the
143
+ revoked stage was re-approved. A later `running` stage with nothing holding it (no `--prepare`
144
+ bundle waiting) now goes back to `ready` alongside going stale, named on the outcome and on the
145
+ additive `gate.revoked` event; one holding a bundle is a sub-agent turn already paid for, so
146
+ the revoke refuses instead and names it. `run unlock` had the same false promise for a
147
+ lockless `running` stage ("`tldrx next` demotes it") — it now performs that demotion itself
148
+ when nothing is holding the run, rather than describing a move nothing makes. A revoke also
149
+ advances `attempt_started_at` (see #144, #199) on the stage it revokes and on every stage it
150
+ demotes, for the same reason `reject` does.
151
+ - **A mutating generator command in a story's dod block can no longer be run by the base-tree
152
+ pre-flight as a routine measurement (see #362).** Measured live, tldrx 0.31.1: a `.NET`
153
+ story's dod block declared `dotnet ef migrations add …` beside ordinary build/test commands,
154
+ and the base-tree probe ran it against the UNTOUCHED base tree — a DELTA-gate measurement is
155
+ not supposed to mutate the tree it is measuring, and it would have written a migration triple
156
+ into pristine `main` as a side effect of asking a question. `isGeneratorCommand`
157
+ (`schemas/commandAllowlist.ts`) detects a generator BY NAME — the command text, not a
158
+ workspace-declared slot key, since a team names its migration-add slot whatever it likes — and
159
+ two doors read that one derivation: `validateStoryDod` now refuses a generator command at Plan
160
+ time, so a plan is never written this way, and `dodRunner.ts`'s `measureBaseCommand` refuses
161
+ to run one against either base tree it measures, as defense in depth for a story file that
162
+ reached Build some other way. Neither door touches a story's own fresh worktree, where a
163
+ generator legitimately runs once, by the developer.
164
+ - **`tldrx next` now self-closes a parked `auto` gate the moment every condition it was held on
165
+ clears, `--wait-gates` or not (see #342).** Before this, the only door that ever re-measured an
166
+ already-`awaiting_gate` stage was `run auto --wait-gates`'s own poll (`waitForGate` →
167
+ `selfCloseAutoGate`); without that flag, `run auto` answering its own blocking questions (or a
168
+ person answering them by hand) left the gate `awaiting_gate` forever, and the next `next` call
169
+ reported it as terminal-for-this-call with a generic `gate pending: tldrx approve` — never
170
+ noticing the questions it was parked on had since been answered. `next` now re-runs the same
171
+ seven conditions (`reevaluateAutoGate`) on every `awaiting_gate` stage whose policy is `auto`
172
+ before giving up, and signs it through the same `approve` door a freshly-closed gate uses —
173
+ `human` and `agent` gates are untouched, and a verdict that still fails falls through to the
174
+ ordinary human-gate report exactly as before.
175
+ - **The default Build budget gate now parks at a story boundary instead of stopping the stage
176
+ incomplete with no boundary the operator chose (see #354).** `runNext.ts`'s `budgetRefusal`
177
+ meters the phase ceiling exactly once, at stage entry — the Build executor itself never
178
+ re-checked the envelope between two dispatches of one headless run, so a phase that could
179
+ afford its entry estimate but ran short partway through kept dispatching until the money was
180
+ gone, and the only way the gate fired again was an all-or-nothing refusal on a RETRY of a
181
+ failed stage. Modeled directly on #298's rate-limit park: before starting a new story's
182
+ developer, the executor now checks whether the stage's remainder still covers that story's
183
+ floor and, if not, lets whatever is already running finish and settle, then stops — recording
184
+ one `budget.parked` event (mirroring `agent.rate_limited`'s shape) and naming the shortfall as
185
+ the reason in both the operator line and the handoff's `## Unknowns`. `run auto --wait-gates`
186
+ resumes the parked stage itself once an operator's `tldrx budget raise --stage` (or
187
+ `--rebalance-finished`) actually raises the STAGE's own `budget_usd` — checked fresh on every
188
+ poll against the phase's real remainder, never a clock — and `run status` names the park
189
+ instead of reading as a decision waiting on a person.
190
+
191
+ ## 0.33.0 — 2026-09-17
192
+
193
+ ### Added
194
+
195
+ - **A workspace can opt its base pre-flight probe into a story-shaped worktree, so an
196
+ environmental red that only appears there is caught before the first story (#371).** #363
197
+ shipped `tree:` provenance on every base-pre-flight row and declined, by measured decision, to
198
+ move the probe itself into a worktree by default — doing so on every run would pay for the
199
+ declared command a second time and reopen the `node_modules`-less outage the checkout-only
200
+ design exists to avoid. `probe_in_worktree: true`, workspace-wide and additive, is the opt-in
201
+ for a workspace that has actually seen the gap #363 named: a command green in the checkout and
202
+ red only in a story's own worktree shape (a `.git` FILE vs. a `.git` directory is the measured
203
+ case). On, every declared base command is ALSO measured, once per repo, in a fresh detached
204
+ worktree of the base sha — kept beside the checkout row, never instead of it, under the same
205
+ `tree:` field (`checkout` vs. `worktree`) rather than a third tree constant. Off (the default,
206
+ and every workspace before this key existed) leaves the base pre-flight measuring the checkout
207
+ only, exactly as before. Pre-merge review measurement: a session that calls the base pre-flight
208
+ more than once (a `prepare()` followed by the real call) paid for a `git worktree add`/`remove`
209
+ on EVERY call, even when every declared command already had a fresh `tree: worktree` reading and
210
+ nothing was spawned there — fixed by checking every command's cache hit before touching git at
211
+ all, so a worktree is opened only when at least one command still needs a fresh measurement.
212
+
213
+ ### Fixed
214
+
215
+ - **The plan sequencing rule (#365) could false-positive two unrelated stories that happened to
216
+ share a generic single-transition camelCase token (#370).** `fieldsIn`
217
+ (`src/core/plan/planShape.ts`) captured ANY bare camelCase/PascalCase token with one inner
218
+ capital as a field candidate, so a story enforcing an invariant near a word like `toString` or
219
+ `getId` collided with an unrelated story populating a same-named `toString` or `getId`
220
+ elsewhere, refusing an honest plan for sharing nothing but an incidental identifier. A bare
221
+ camelCase/PascalCase token with at least two internal capital transitions
222
+ (`DeliveryAddressText`) still always counts; a one-transition token (`toString`, `getId`) now
223
+ counts only when the SAME normalized field is ALSO spelled `snake_case` or backtick-quoted
224
+ somewhere else in the plan — dropping one-transition tokens outright, the first cut at this
225
+ fix, reopened #365 for a genuine two-word field spelled bare PascalCase on one side and
226
+ `snake_case` on the other (`order_total` enforced in one story, bare `OrderTotal` populated in
227
+ another still refuses; `toString` shared by two unrelated stories, with no `snake_case` or
228
+ backtick sibling anywhere in the plan, still does not). `snake_case` and backtick-quoted
229
+ identifiers are unaffected either way, and the originating #365 case still refuses.
230
+ - **`task.done`'s `as_is_note` had no size bound, unlike its two siblings on the same event
231
+ (#368).** `permission_refused` and `budget_death` were clamped by #359 after a ~110-line
232
+ refused heredoc alone pushed `task.done` past the §2.9 4096-byte payload cap and failed the
233
+ whole build STAGE, not just the story. `as_is_note` — the operator-typed `--note` on `tldrx
234
+ story reopen <id> --as-is` — was never given the same treatment, even though nothing stops a
235
+ long, scripted or pasted note from reaching the exact same shape. It is now passed through the
236
+ same `clampAgentText` call, the same `TASK_DONE_FIELD_MAX_BYTES` constant and the same
237
+ `[clamped: N bytes exceeds the M byte cap]` marker convention #359 already set, so an
238
+ oversized as-is note can no longer fail the build stage.
239
+ - **The Build-entry probe's throwaway worktree ran a repo's `install:` but not its
240
+ `tool_restore:`, so a command whose resolution depended on the restore having run could be
241
+ refused there even though the checkout door — which does run `tool_restore:` since #363 —
242
+ would have been fine (part of #371).** `entryProbe.ts`'s per-repo declaration now also reads
243
+ `tool_restore:` (`toolRestoreCommandFor`, the same slot `prepBaseTree` already runs at the
244
+ checkout door) and runs it in the fresh worktree before that worktree's `install:` and before
245
+ any Definition-of-Done command's binary is resolved there, refusing Build by name on a
246
+ nonzero exit — the same shape the checkout door already uses, never a second copy of the
247
+ restore logic.
248
+
249
+ ### Changed
250
+
251
+ - **`settleAskedNoDiff` and `settleRedDod` now share one requeue-settle helper instead of each
252
+ hand-building the same shape (#369).** Both settled a "requeue this attempt, attempts remain"
253
+ turn by constructing an identical eight-field n-a-review object and `keepWorktree: true` by
254
+ hand — copied verbatim in two places, the AGENTS.md §12 duplication the file's own
255
+ decomposition precedent exists to avoid. `settleRequeue` now holds that shape once, taking the
256
+ fields that actually vary (`dod`, `reason`, `askedNoDiff`) as data; both call sites are
257
+ unchanged in behavior — `test/build-golden.test.ts` stays byte-identical.
258
+ - **AGENTS.md §11 now names the negated-closing-verb trap and widens its own verification
259
+ grep to catch it.** Measured 2026-09-16: commit `d8f90f9`'s sentence `reported not fixed:
260
+ #359` auto-closed #359 anyway — GitHub's closing-keyword parser matches `fixed` (any
261
+ inflection) followed by an optional colon/punctuation and `#N`, and does not parse the
262
+ negation three tokens earlier; #359 needed a reopen before its real fix (`24f1324`) closed
263
+ it a second time, legitimately. §11's own documented verification command,
264
+ `grep -iE '(clos|fix|resolv)[a-z]* #'`, had the identical blind spot — it does not allow a
265
+ colon between the verb and the number, so it read `fixed: #359` as a non-match (measured
266
+ exit 1) and would have let this exact sentence through. The grep is widened to
267
+ `(clos|fix|resolv)[a-z]*[[:punct:]]* #` (measured exit 0 on the same sentence); the real
268
+ discipline stays "never write a closing-shaped phrase near a `#N` that should not close" —
269
+ the grep is a tripwire, not the fix (closes #372).
270
+ - **`ci.yml` now keeps `test/merge-wave.test.ts`'s tmp sandbox as a build artifact when the
271
+ job fails.** This is an INSTRUMENT change, not a fix for the still-undiagnosed #237 flake
272
+ (interrupted-merge case expects a SIGTERM exit of 143, sometimes sees 0 after ~92s): every
273
+ #237 data point so far recorded the same second wall on top of the first — "no merge.log is
274
+ available from the CI sandbox" — because the sandbox that file plants was discarded with
275
+ the runner. The `bun test` step's `TMPDIR` now points at `${{ runner.temp }}/mw` instead of
276
+ the OS default, and a new `if: failure()` step uploads a scoped subset of it on failure
277
+ (`actions/upload-artifact@v4`, `retention-days: 14`). Review caught a second wall in the
278
+ first cut (closes #372's own class of bug, an instrument that cannot observe the thing):
279
+ `afterEach` `rmSync`s every sandbox unconditionally, pass or fail, INSIDE the bun process,
280
+ before that upload step ever runs — so the artifact would have been empty on the exact run
281
+ it exists for. `test/merge-wave.test.ts` now mkdtemps its sandboxes under the recognisable
282
+ `mw-sandbox-` prefix and skips its own cleanup when `MERGE_WAVE_KEEP_SANDBOX` is set (the
283
+ decision lives in one small, unit-tested function, `keepSandbox`/`cleanupSandbox` in
284
+ `test/fixtures/mergeWaveSandbox.ts` — local runs stay hermetic and self-cleaning per §8
285
+ unless that var is set); ci.yml sets it on the `bun test` step and scopes the upload's
286
+ `path:` to exactly that prefix, so the next red leaves a `merge.log` and lock state an
287
+ agent can actually read (see #237, closes #373).
288
+
3
289
  ## 0.32.0 — 2026-09-16
4
290
 
5
291
  ### Fixed
package/README.md CHANGED
@@ -335,6 +335,8 @@ back on the registry is 0.3.0.
335
335
 
336
336
  | Version | Date | Status | Contains |
337
337
  |---|---|---|---|
338
+ | 0.34.0 | 2026-09-17 | `beta` | Four waves of the audited backlog, every branch reviewed before main: run auto no longer stops itself (parked auto gates self-close, Build parks at a story boundary on budget), gates cannot sign stale or backdated evidence, interrupted turns bank their cost as absent instead of a measured zero, the boundary condition diffs the branch the run recorded, mutating generators are refused from dod blocks, merge-wave refuses edits to released CHANGELOG sections and answers `unknown` instead of `idle`, and the test harness refuses a TMPDIR inside a work tree — 14 issues, the first of them shipped end-to-end by a tldrx run against this repo |
339
+ | 0.33.0 | 2026-09-17 | `beta` | Six follow-ups from the 0.32.0 cycle, each measured on the second unattended run (run #42) or on the review that shipped it. The plan sequencing rule counts a bare camelCase field only when it is unambiguous or the plan spells it elsewhere as snake_case or in backticks, so unrelated stories sharing a word like `toString` no longer refuse each other (#370); `task.done` clamps an oversized `as_is_note` the way it clamps `permission_refused` (#368); the asked-no-diff and red-DoD requeues settle through one helper that takes data (#369); a workspace can opt its base pre-flight probe into a story-shaped worktree with `probe_in_worktree: true`, and the Build-entry probe runs `tool_restore:` first (#371, the remainder of #363); AGENTS §11 names the negated closing verb that still auto-closes and widens the pre-wave grep (#372); and CI keeps merge-wave's sandboxes on failure so the next #237 red carries its `merge.log` (#373). Minor release: a new workspace field and a new plan refusal shape. |
338
340
  | 0.32.0 | 2026-09-16 | `beta` | Seven framework defects measured on the second unattended run (run #42), where every one of nine operator stalls was the framework's, not the code's. A refused compound shell line is a rework note for the developer instead of a human stop (#360); a dependency hold is re-evaluated the moment the dependency lands (#361) and a rescued story is written back to `todo` the instant the release is decided (#366); an event payload over the size cap is clamped to a bounded head instead of failing the whole build stage (#359); a headless developer is told it is unattended, that `## Inputs` is where to start and not an allowlist, and a turn that asks instead of deciding is a named `asked_no_diff` failure (#364); the rate-limit park waits for 90% of the window, not the provider's first warning at half of it (#367); the plan refuses a story that enforces an invariant on a field before the story that populates it, matching the field across spellings (#365); and the preflight restores workspace tools before its first probe and names the tree it ran in (#363, in part). Minor release: a new plan refusal, a changed park threshold and a new failure kind. |
339
341
  | 0.31.1 | 2026-09-16 | `beta` | One fix, measured on the first unattended run under 0.31.0: a seed placed at the documented `.tldrx/seeds/` location was invisible to the seed-solution detector, because the filter that drops the `what` stage's own framework inputs dropped every `.tldrx/` path, so `how` ran at full price for exactly the seeds #346 was written to skip (#358). |
340
342
  | 0.31.0 | 2026-09-16 | `beta` | Ten efficiency adjustments, ranked from a measured audit of 48 runs, so an unattended run spends less and stalls less. `how` is skipped when the seed already declares its own solution, and runs on `sonnet` when it does run (#346); `risks.md` is no longer generated (#350); `watch` follows `build`'s gate policy, so an all-auto run signs itself (#349); a Watch card's `[src:]` punctuation slip is repaired locally instead of costing a turn (#345); a fix list with zero open findings settles the round, an unreadable one holds it by name (#218); `run status` warns NO-RETRY before a retry the budget refuses (#232); a failed headless turn carries a named `failure_kind`, so the loop stops buying the same retry for every death (#348); an orphaned turn is banked as unmetered instead of a confident zero (#337); a relaunch resumes its own epic branch instead of restarting it (#347); the base-tree DoD refusal names the workspace `install:` (#343) and `run auto` names `--wait-gates` (#342). |
@@ -1,24 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  conflictOf
4
- } from "./chunk-04kpbhvk.js";
4
+ } from "./chunk-9kzevq39.js";
5
5
  import {
6
6
  FactsStore,
7
7
  formatJaccard
8
- } from "./chunk-f9d2efhb.js";
8
+ } from "./chunk-bfvcebn6.js";
9
9
  import {
10
10
  parseHookInput,
11
11
  readStdin
12
- } from "./chunk-zyz0fbz5.js";
12
+ } from "./chunk-jfaarcb1.js";
13
13
  import"./chunk-ae6bkfs5.js";
14
14
  import {
15
15
  EventLog,
16
16
  PHASE_ID_RE
17
- } from "./chunk-akemhezh.js";
17
+ } from "./chunk-4d22xm3c.js";
18
18
  import {
19
19
  PHASE_IDS
20
- } from "./chunk-yyhwf3bp.js";
21
- import"./chunk-5kv2vtha.js";
20
+ } from "./chunk-kcrphpwc.js";
21
+ import"./chunk-7ypyp8xk.js";
22
22
  import {
23
23
  ADVISORY_KEY,
24
24
  MAX_FACT_CHARS,
@@ -28,7 +28,7 @@ import {
28
28
  renderQuestionBlock,
29
29
  replaceBlock,
30
30
  serializeQuestions
31
- } from "./chunk-x1f4dxwk.js";
31
+ } from "./chunk-sjykymar.js";
32
32
  import {
33
33
  ITERATION_ONLY_SLOT,
34
34
  PROJECT_FRAMEWORK_DIR,
@@ -38,7 +38,7 @@ import {
38
38
  isScopedTemplate,
39
39
  parseYaml,
40
40
  scopedSlotOf
41
- } from "./chunk-9bd6b3xf.js";
41
+ } from "./chunk-0a3b8w1c.js";
42
42
 
43
43
  // src/hooks/answer-capture.ts
44
44
  import { existsSync as existsSync5 } from "fs";
@@ -157,6 +157,7 @@ function loadWorkspace(root) {
157
157
  const scopedTemplates = new Set;
158
158
  const defaultBranches = new Map;
159
159
  let seedTriageThresholdTokens = null;
160
+ let probeInWorktree = false;
160
161
  const empty = () => ({
161
162
  root,
162
163
  repos,
@@ -168,7 +169,8 @@ function loadWorkspace(root) {
168
169
  scopedCommands,
169
170
  scopedTemplates,
170
171
  defaultBranches,
171
- seedTriageThresholdTokens
172
+ seedTriageThresholdTokens,
173
+ probeInWorktree
172
174
  });
173
175
  const path = join(root, PROJECT_FRAMEWORK_DIR, "workspace.yml");
174
176
  if (!existsSync(path))
@@ -186,6 +188,8 @@ function loadWorkspace(root) {
186
188
  seedTriageThresholdTokens = tokens;
187
189
  }
188
190
  }
191
+ if (doc?.probe_in_worktree === true)
192
+ probeInWorktree = true;
189
193
  const list = doc?.repos;
190
194
  if (!Array.isArray(list))
191
195
  return empty();
@@ -1,24 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-q1ge3nex.js";
4
+ } from "./chunk-vhn2vz83.js";
5
5
  import {
6
6
  allow,
7
7
  deny,
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-27v2rwa9.js";
12
- import"./chunk-zyz0fbz5.js";
11
+ } from "./chunk-d3jrf6yy.js";
12
+ import"./chunk-jfaarcb1.js";
13
13
  import {
14
14
  REBALANCE_SOURCE,
15
15
  applyRebalance,
16
16
  asRunBudget,
17
- currentActor,
18
17
  describeRebalance,
19
18
  economyFor,
20
19
  isHostTokens,
21
- nowRfc3339,
22
20
  planRebalance,
23
21
  raiseCommand,
24
22
  raiseGrantVerdict,
@@ -27,10 +25,12 @@ import {
27
25
  validateRunBudget,
28
26
  wouldExceed,
29
27
  wouldExceedHostTokens
30
- } from "./chunk-5at77jx8.js";
28
+ } from "./chunk-dw8gggv9.js";
31
29
  import {
32
- EventLog
33
- } from "./chunk-akemhezh.js";
30
+ EventLog,
31
+ currentActor,
32
+ nowRfc3339
33
+ } from "./chunk-4d22xm3c.js";
34
34
  import {
35
35
  cursorStage,
36
36
  hostTokensIn,
@@ -39,20 +39,20 @@ import {
39
39
  newestActiveRun,
40
40
  renderRunEconomies,
41
41
  runSpend
42
- } from "./chunk-8ds2ret8.js";
42
+ } from "./chunk-57s4mdhy.js";
43
43
  import {
44
44
  buildStageDefaults
45
- } from "./chunk-yyhwf3bp.js";
45
+ } from "./chunk-kcrphpwc.js";
46
46
  import {
47
47
  noteDeprecations
48
- } from "./chunk-x1f4dxwk.js";
48
+ } from "./chunk-sjykymar.js";
49
49
  import {
50
50
  PROJECT_WORK_DIR,
51
51
  findWorkspaceRoot,
52
52
  locateWork,
53
53
  parseYaml,
54
54
  stageYamlPath
55
- } from "./chunk-9bd6b3xf.js";
55
+ } from "./chunk-0a3b8w1c.js";
56
56
 
57
57
  // src/hooks/budget-gate.ts
58
58
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -125,7 +125,8 @@ await runHook("budget-gate", async () => {
125
125
  const attended = isAttendedByHostView(view);
126
126
  const stage = cursorStage(view);
127
127
  const declared = stage?.budget_usd ?? stageBudgetFromLibrary(root, view.cursor.stage);
128
- const attempts = buildStageDefaults(root, view.scope, view.cursor.stage).attempts;
128
+ const tuning = buildStageDefaults(root, view.scope, view.cursor.stage);
129
+ const attempts = tuning.attempts;
129
130
  const work = declared === null ? null : remainingWork({
130
131
  runDir: view.dir,
131
132
  phaseId: view.cursor.phase,
@@ -135,7 +136,10 @@ await runHook("budget-gate", async () => {
135
136
  maxUsd: null,
136
137
  economy: economyFor(budget, view.cursor.phase),
137
138
  attended,
138
- attempts
139
+ attempts,
140
+ reviewerShare: tuning.reviewerShare,
141
+ storyCapMultiplier: tuning.storyCapMultiplier,
142
+ storyCapFloorUsd: tuning.storyCapFloorUsd
139
143
  });
140
144
  const estimate = estimateFor(command, work === null ? null : work.usd);
141
145
  if (estimate <= 0)
@@ -8818,6 +8818,7 @@ function loadWorkspace(root) {
8818
8818
  const scopedTemplates = new Set;
8819
8819
  const defaultBranches = new Map;
8820
8820
  let seedTriageThresholdTokens = null;
8821
+ let probeInWorktree = false;
8821
8822
  const empty = () => ({
8822
8823
  root,
8823
8824
  repos,
@@ -8829,7 +8830,8 @@ function loadWorkspace(root) {
8829
8830
  scopedCommands,
8830
8831
  scopedTemplates,
8831
8832
  defaultBranches,
8832
- seedTriageThresholdTokens
8833
+ seedTriageThresholdTokens,
8834
+ probeInWorktree
8833
8835
  });
8834
8836
  const path = join3(root, PROJECT_FRAMEWORK_DIR, "workspace.yml");
8835
8837
  if (!existsSync3(path))
@@ -8847,6 +8849,8 @@ function loadWorkspace(root) {
8847
8849
  seedTriageThresholdTokens = tokens;
8848
8850
  }
8849
8851
  }
8852
+ if (doc?.probe_in_worktree === true)
8853
+ probeInWorktree = true;
8850
8854
  const list = doc?.repos;
8851
8855
  if (!Array.isArray(list))
8852
8856
  return empty();
@@ -2,7 +2,7 @@ import {
2
2
  GATE_POLICIES,
3
3
  validateGatesPolicy,
4
4
  validateStagePolicy
5
- } from "./chunk-yyhwf3bp.js";
5
+ } from "./chunk-kcrphpwc.js";
6
6
  import {
7
7
  asDocument,
8
8
  isRecord,
@@ -15,7 +15,7 @@ import {
15
15
  requireString,
16
16
  requireVersion,
17
17
  result
18
- } from "./chunk-9bd6b3xf.js";
18
+ } from "./chunk-0a3b8w1c.js";
19
19
 
20
20
  // src/core/events/EventLog.ts
21
21
  import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -69,6 +69,7 @@ var EVENT_TYPES = [
69
69
  "budget.blocked",
70
70
  "budget.raised",
71
71
  "budget.granted",
72
+ "budget.parked",
72
73
  "fact.added",
73
74
  "fact.retired",
74
75
  "fact.superseded",
@@ -231,6 +232,19 @@ function skippedNote(skipped) {
231
232
  }
232
233
  var WARNED = new Set;
233
234
 
235
+ // src/hooks/lib/actor.ts
236
+ function currentActor() {
237
+ const user = process.env.USER ?? process.env.USERNAME ?? "";
238
+ return user.trim() === "" ? "unknown" : user.trim();
239
+ }
240
+ function nowRfc3339() {
241
+ return `${new Date().toISOString().slice(0, 19)}Z`;
242
+ }
243
+ var RFC3339_RE = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
244
+ function isRfc3339(value) {
245
+ return RFC3339_RE.test(value) && !Number.isNaN(Date.parse(value));
246
+ }
247
+
234
248
  // src/core/run/questionsPolicy.ts
235
249
  var QUESTION_POLICIES = ["human", "recommended"];
236
250
  function validateQuestionsPolicy(value, stageIds, issues) {
@@ -300,6 +314,9 @@ function parseEvidence(text) {
300
314
  const role = requireEnum2(doc.role, EVIDENCE_ROLES, "role", issues);
301
315
  const by = requireText(doc.by, "by", issues);
302
316
  const at = requireText(doc.at, "at", issues);
317
+ if (at !== "" && !isRfc3339(at)) {
318
+ issues.push(front(0, `\`at\` must be a valid RFC3339 timestamp, got \`${at}\``));
319
+ }
303
320
  const verdict = requireEnum2(doc.verdict, EVIDENCE_VERDICTS, "verdict", issues);
304
321
  const read = requireStrings(doc.read, "read", issues);
305
322
  const citations = requireCitations(doc.citations, issues);
@@ -728,6 +745,9 @@ function validateRunFile(input) {
728
745
  if (stage.stale !== undefined && typeof stage.stale !== "boolean") {
729
746
  issues.push({ path: `${path}.stale`, message: "expected true or false" });
730
747
  }
748
+ if (stage.attempt_started_at !== undefined && stage.attempt_started_at !== null && typeof stage.attempt_started_at !== "string") {
749
+ issues.push({ path: `${path}.attempt_started_at`, message: "expected a string or null" });
750
+ }
731
751
  checkOrder(stage.started_at, stage.ended_at, path, issues);
732
752
  if (cursor !== null && cursor.phase === phaseId && cursor.stage === stageId)
733
753
  cursorResolves = true;
@@ -918,4 +938,4 @@ function gateEvidencePath(runDir, phaseId, stageId) {
918
938
  return join2(runDir, ...gateEvidenceRelPath(phaseId, stageId).split("/"));
919
939
  }
920
940
 
921
- export { EventLog, parseEvidence, PHASE_ID_RE, OUTCOME_NOT_RECORDED, isTerminal, isFinished, isAttendedByHost, flatten, stageAt, deriveRunStatus, derivePhaseStatus, validateRunFile, asRunFile, evidencePath, gateEvidencePath };
941
+ export { EventLog, currentActor, nowRfc3339, parseEvidence, PHASE_ID_RE, OUTCOME_NOT_RECORDED, isTerminal, isFinished, isAttendedByHost, flatten, stageAt, deriveRunStatus, derivePhaseStatus, validateRunFile, asRunFile, evidencePath, gateEvidencePath };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  listRunDirs,
3
3
  parseYaml
4
- } from "./chunk-9bd6b3xf.js";
4
+ } from "./chunk-0a3b8w1c.js";
5
5
 
6
6
  // src/hooks/lib/runFile.ts
7
7
  import { existsSync, readFileSync } from "node:fs";