tldr-experts 0.11.1 → 0.13.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 (31) hide show
  1. package/CHANGELOG.md +251 -0
  2. package/README.md +2 -0
  3. package/dist/hooks/answer-capture.js +7 -7
  4. package/dist/hooks/budget-gate.js +7 -7
  5. package/dist/hooks/{chunk-h23db062.js → chunk-1knfmj6j.js} +1 -1
  6. package/dist/hooks/{chunk-5hyd1rk1.js → chunk-6fywd8xd.js} +1 -1
  7. package/dist/hooks/{chunk-z6n9551r.js → chunk-cg8v4mz4.js} +1 -1
  8. package/dist/hooks/{chunk-rw3n3z6t.js → chunk-f1w9sy07.js} +19 -1
  9. package/dist/hooks/{chunk-jmypgf6s.js → chunk-fsgsn8vb.js} +31 -3
  10. package/dist/hooks/{chunk-qn8qnrgw.js → chunk-fyr7f9t6.js} +3 -3
  11. package/dist/hooks/{chunk-8d0t69pk.js → chunk-mvz7jwt8.js} +1 -1
  12. package/dist/hooks/{chunk-gj1ckg0c.js → chunk-nwzevx0j.js} +3 -3
  13. package/dist/hooks/{chunk-zs84arc1.js → chunk-qefqmgjn.js} +1 -1
  14. package/dist/hooks/{chunk-hx0jgrmh.js → chunk-v2xzr4qd.js} +1 -1
  15. package/dist/hooks/{chunk-wbkwjpq2.js → chunk-w9r9p155.js} +62 -32
  16. package/dist/hooks/{chunk-b26rvgjd.js → chunk-yvgkw0gm.js} +1 -1
  17. package/dist/hooks/claim-sources.js +5 -5
  18. package/dist/hooks/dod-gate.js +5 -5
  19. package/dist/hooks/no-reask.js +8 -8
  20. package/dist/hooks/session-start.js +10 -10
  21. package/dist/hooks/statusline.js +6 -6
  22. package/dist/tldrx.js +1571 -765
  23. package/package.json +1 -1
  24. package/plugin/.claude-plugin/plugin.json +1 -1
  25. package/stages/build/stage.yml +24 -0
  26. package/templates/experts/architect.md +1 -1
  27. package/templates/experts/operations.md +1 -1
  28. package/templates/experts/stack/dotnet.md +3 -4
  29. package/templates/experts/stack/javascript.md +3 -4
  30. package/templates/experts/stack/python.md +3 -4
  31. package/templates/experts/stack/typescript.md +3 -4
package/CHANGELOG.md CHANGED
@@ -1,6 +1,257 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## 0.13.0 — 2026-09-08
5
+
6
+ ### Added
7
+
8
+ - **`tldrx expert rescore [<name>] [--area <a>]` — score the knowledge you already paid for.**
9
+ Its sibling `recompute` is arithmetic over the evidence rows already in `competencies.yml`;
10
+ `rescore` RE-READS `knowledge/*.md` and derives their evidence again under today's rules. It
11
+ exists because the gate fix below changes what the NEXT training run earns and can do nothing
12
+ for the files already on disk: for every expert the bug hit, `evidence:` is `[]`, so the only
13
+ other recovery is to buy the same readings a second time — **$9.47** of them in the measured
14
+ case. Like `recompute` it reads no code, spawns nothing, spends nothing, and leaves `status`
15
+ and `last_trained` exactly as it found them.
16
+ Rows are dated by the knowledge file's own `trained_at`, or by the expert's `last_trained`
17
+ when it has none — **never by the clock**, because §2.6 weighs recency and a reading taken in
18
+ August is not evidence gathered today. When neither date exists the file is skipped with that
19
+ as the reason, and so is a file that no longer validates: rescoring is a re-read under
20
+ today's rules, and keeping rows an older tldrx once accepted is the drift this refuses.
21
+ **A rescored row carries two dates, and the rescore writes itself into the ledger** — because
22
+ the workspaces this command exists to rescue are exactly the ones whose `training.jsonl`
23
+ already asserts `check.passed` with `evidence_added: 0`. Built and measured on the first cut:
24
+ a $0 rescore turned that ledger's own August date into real August-dated rows and moved the
25
+ level 0 → 1, leaving `training.jsonl` byte-identical — two files that contradict each other,
26
+ with nothing on disk saying a free re-derivation weeks later is what wrote the rows. So
27
+ `areas[].evidence[].rescored_at` is additive and optional beside `at`, and its ABSENCE keeps
28
+ the meaning every row already had: a paid training turn earned it. `at` still says when the
29
+ CLAIM was read and is still the only date the level formula weighs; `rescored_at` says when
30
+ the SCORING happened. And `training.jsonl` gains one `evidence.rescored` line per file whose
31
+ rescore actually moved something — `cost_usd: 0`, `spawned: 0`, dated when it RAN and never
32
+ backdated to match the rows, carrying `dated_at`/`dated_by` so the two clocks in one record
33
+ can never be read as one. A rescore that changes nothing writes no line: it contradicts
34
+ nothing, and a ledger that grows on every idempotent re-run is one nobody reads.
35
+
36
+ - **`scripts/merge-wave.sh` now refuses a branch that carries no review record (#192).** Every
37
+ other invariant this script protects got a mechanism — the lock (#44), the HEAD assertion, the
38
+ ref guard (#89), the unwind and orphan refusal (#116), the self-snapshot (#117). The pre-merge
39
+ review was still a paragraph, and prose loses to recency: measured over twelve maintenance
40
+ waves (2026-09-06 → 2026-09-07), **4 of the 12** pre-merge reviews found a real Important
41
+ defect the implementer then fixed before the branch merged, and the one wave that reviewed
42
+ *after* merging found one too — which then sat on `main` for ~2 hours, because once a commit is
43
+ published the only remedies are a follow-up merge or a revert and both wait for the next lock
44
+ window. That wave did not decide to skip the review; it simply did not remember at the moment
45
+ the branch was green, and nothing in the merge path could tell the difference afterwards.
46
+ So the record is now a FILE on the branch, `.review/<branch>.md`, asserted in the same preamble
47
+ block as the dirty-tree and orphan refusals: `verdict: merge` on the first line (anything else,
48
+ `verdict: fixes required` included, refuses), `reviewed-by:` naming who read it, and `against:`
49
+ naming the sha they read. A file rather than a commit trailer because it lands in the merge
50
+ commit's tree — `main` keeps answering "was this reviewed, by whom, against which diff" long
51
+ after the branch is deleted, which a trailer could not. **A stale record refuses rather than
52
+ warns**, and the refusal names both shas: a review of a different diff is exactly the hole the
53
+ honour system already allowed, and a warning would let it through. Staleness is measured as
54
+ "the code moved", not "the sha differs" — the named sha must be an ancestor of the branch head
55
+ with no path outside `.review/` changed since — because a record can only ever name the commit
56
+ the reviewer READ, and committing the record moves the head past exactly that sha, so a literal
57
+ sha-equals-head rule would be unsatisfiable by construction. A rebase does invalidate a record,
58
+ correctly: it is a different diff. The exit code is **10**, a new one: that script's table is
59
+ its own namespace with one code per condition, `2` there is already "merge conflict", and the
60
+ CLI refusal families in `src/cli/exitCodes.ts` — where 2 IS the gate refusal — do not reach a
61
+ shell script that never imports them. Every refusal prints the file path and the three
62
+ lines it wants, so a session that trips the gate never has to read the script to satisfy it.
63
+ There is **no escape hatch** — no flag, no env var — on the issue's own argument that a
64
+ documented one is a hole the moment it exists. Scoped to the MERGE PATH alone:
65
+ `scripts/release.sh` commits on `main` directly, waves nothing and names `merge-wave.sh`
66
+ nowhere (measured — `grep -n 'merge-wave' scripts/release.sh scripts/release-check.sh
67
+ docs/RELEASING.md` exits 1 with no output), so releasing is untouched and `docs/RELEASING.md`
68
+ needed no change. `AGENTS.md` §2 carries the rule and the `maintain` skill cites it rather
69
+ than restating it.
70
+
71
+ ### Fixed
72
+
73
+ - **The mutation check is asked of the developer, which can run it, instead of the reviewer,
74
+ which holds no pen.** Every stack pack's `## Checks` carried "Can each new test fail? verify:
75
+ change the line under test, re-run only that test's file … and confirm it goes red", and those
76
+ Checks are rendered verbatim into the reviewer prompt under `## Stack checks` — a hundred lines
77
+ above a Rule in that same prompt reading "You have no write tool." That was not a wording slip
78
+ in one of the two places: `REVIEWER_TOOLS` is `Read`, `Grep`, `Glob`, `Bash(git diff *)`, so the
79
+ role being asked held neither the pen the mutation needs nor a way to run a test. Measured over
80
+ a week of unattended runs on three real workspaces, hosts resolved the contradiction by hand
81
+ every time and nobody reported the check as unaskable — which is the worse outcome, because a
82
+ reviewer that cannot perform a check still answers it, from reading alone, and calls that an
83
+ answer. It is a producer's obligation, so it goes to the producer: the developer's contract now
84
+ carries it as step 5 of `## Investigate` (`MUTATION_PROOF_RULE`), naming the same instrument the
85
+ packs named — the one test file while iterating, the declared command once, at the Definition of
86
+ Done — and asking for the record it leaves. What is left in the packs is the reviewer's half,
87
+ and it is a read: *did the developer record, beside each new test, that it was seen to fail?* A
88
+ new test carrying none is a finding with a cited file. Where the record goes is the part that
89
+ had to be decided rather than assumed — the reviewer's allowance holds no `git log`, so a commit
90
+ message is a surface it cannot read, and beside the test, in the test file, is the one place
91
+ that is both the developer's to write and the reviewer's to cite. The developer prompt's bytes
92
+ change and `test/build-golden.test.ts` moves with them; the reviewer prompt's own bytes do not.
93
+ - **A role expert could never earn evidence — `--mode full` mines the run record and the domain
94
+ gate refused it** (gh #154). `roleTraining.ts` states the premise: full mode's pre-pass reads
95
+ `tldrx-work/<run>/**/{handoff,retro}.md`, "the record of how this workflow actually ran, which
96
+ IS a role's domain". `outsideDomain` then judged every one of those citations against the
97
+ `## Domain` bullets of `expert.md`, which name folders of CODE — so 100% of what the runs pass
98
+ produced was discarded as `outside domain`, in every workspace, every time.
99
+ Measured on a real workspace at 0.8.0: four role experts, `--mode full`, **$9.47 spent and one
100
+ evidence row** — and that row is a fact token, the one citation kind the gate never inspects.
101
+ And it was not fixable from the workspace: `domainPaths()` drops any single-segment bullet, so
102
+ `tldrx-work` — the only spelling the matcher would match — cannot be declared, while every
103
+ spelling that DOES register (`tldrx-work/**`, `tldrx-work/<run>`) is a literal the
104
+ no-globbing matcher can never reach. The two sets are disjoint.
105
+ Re-measured on 0.12.0 before the fix: `domainPaths()` over the five shipped role templates
106
+ still yields `.tldrx/map/**`, `.tldrx/process.yml` + `.tldrx/map/workspace.md`,
107
+ `.tldrx/conventions/shared.md`, `.tldrx/map/{repo}/gotchas.md` and `.tldrx/memory/facts.yml`,
108
+ and `pathsIntersect` is `false` against every one of them for a citation to
109
+ `tldrx-work/<run>/03-build/handoff.md` — so no role expert could earn a row from the runs
110
+ pass at 0.12.0 either.
111
+ The gate now treats the run record as in-domain for the file mined FROM it, scoped to the pass
112
+ and not to the expert's `kind:` — a light file citing a handoff is still out of domain, and
113
+ still says so.
114
+ - **Two shipped role templates declared a `## Domain` path that matched nothing at all.**
115
+ `pathsIntersect` is segment-prefix matching with no globbing, so architect's `.tldrx/map/**`
116
+ and operations' `.tldrx/map/{repo}/gotchas.md` were literal strings: even a citation to the
117
+ map itself was refused. Both are now spelled `.tldrx/map/`, which the matcher reaches; the
118
+ nuance each carried stays in the bullet's prose. Globs and `{repo}` placeholders remain
119
+ unsupported in a `## Domain` bullet — a test now refuses one in any shipped role template
120
+ rather than letting it look like it works.
121
+ - **A paid training pass that earned nothing said so nowhere durable.** The ledger recorded
122
+ `problems` only on the failure path, so a file that validated, cost $1.61 and added zero
123
+ evidence rows was written down as `check.passed` with `evidence_added: 0` and no reason beside
124
+ it — the operator's next signal being `tldrx status` telling them to train it again. The
125
+ `check.passed` record now carries its `warnings`, and the run prints `the level did not move —
126
+ $X.XX bought 0 evidence row(s)` with the reasons under it. It stays exit `0` and the file is
127
+ still kept: a warning is a way of being worth nothing, not a lie, and quarantining an honest
128
+ file would destroy the one thing that was paid for. (The sibling of #101 on the output side —
129
+ that one refuses an empty INPUT before the money; nothing asked the question after.)
130
+
131
+ ## 0.12.0 — 2026-09-08
132
+
133
+ ### Added
134
+
135
+ - **The reviewer can run on a different model than the developer, and every verdict now says
136
+ which model produced it.** Measured this week across three real workspaces and 168 Build
137
+ stories: `model:` and `effort:` in `stage.yml` are per STAGE, one accessor served both
138
+ spawns, and all 168 stories ran the same model at the same effort for the developer AND the
139
+ reviewer that judged its diff. Zero reviewers ran on anything stronger — so *does a stronger
140
+ reviewer find more* was not a question the data could answer, while hosts were already
141
+ answering it by hand, upgrading the reviewer on stories whose own text said they were
142
+ security-bearing. Three additive keys close that: `reviewer: {model?, effort?}` in
143
+ `stage.yml` is the reviewer role's own pin; `reviewer_by_stakes:` is the same thing keyed on
144
+ a story's new optional `stakes:` field (`security | money | data | correctness | routine`, a
145
+ closed enum the Plan agent emits and nothing infers from prose); and both resolve FIELD BY
146
+ FIELD under `--model`/`--effort`, which stay the operator's word. Absent everywhere ⇒ the
147
+ reviewer runs on the stage's own two lines, byte for byte. **No opus default ships**:
148
+ `stages/build/stage.yml` declares `reviewer_by_stakes: {}` and a commented example, because
149
+ there is no evidence yet that a stronger reviewer finds more and shipping the answer as a
150
+ default would spend money asserting the thing these keys exist to measure. What ships is the
151
+ record that makes the evidence accumulate — a spawned reviewer's arguments were always on
152
+ its `agent.spawned`, a host review's are now read off `--model`/`--effort` on `tldrx next
153
+ --commit --review` and written as `basis: host-declared`, `04-build/log/<story>.md` carries
154
+ a `- Reviewer:` line, and `tldrx replay` names the model on every review round. A host that
155
+ declared nothing reads `not recorded` rather than the bundle's suggested model: that
156
+ suggestion is one tldrx made, and quoting it back would be a record lying in the dangerous
157
+ direction. (#178)
158
+ - **`touches` was declared and never measured, so the boundary audit was answering against a
159
+ forecast.** Measured on three real workspaces: one story changed 21 files and 18 of them fell
160
+ outside its declared `touches`, and the other two stories of the same run under-declared as
161
+ well — none of it a mistake anybody could point at, because the 18 were the Application-layer
162
+ files the work genuinely required and no compiler runs at Plan time to predict them. The
163
+ consequence is the part worth fixing: the check that exists to say "this epic changed something
164
+ no story claimed" was comparing a diff against a guess, which is the shape of check that gets
165
+ switched off. Build now takes the second reading, at the moment a story's work is final and the
166
+ answer is free: it diffs the story over exactly the range the reviewer was shown (the
167
+ `epic_base` recorded on `task.done` since 0.10.0 — one definition of "the story's diff", not a
168
+ second `git diff` derivation) and, when paths landed outside the declared list, appends ONE
169
+ `story.touches_widened` with `basis: "measured"` and `actor: framework`, naming the paths and
170
+ the count. It is the SAME event `tldrx story widen` writes, deliberately: "the surface grew" is
171
+ one fact, and a sibling type would make every reader ask two questions to learn it. `basis` is
172
+ additive and **absent means `declared`**, so every row written before it existed keeps its
173
+ meaning and reads unchanged. It is advisory and cannot refuse — nothing is written when the
174
+ work stayed inside the forecast, an unreadable diff is simply no measurement, and the story's
175
+ own `touches:` is never rewritten, because declaring is the operator's verb and a framework
176
+ that back-dated a declaration would make the plan claim it declared a path it did not. Both
177
+ bases render labelled in the Build handoff, in the `tldrx ship` PR body that embeds it, and in
178
+ `tldrx replay`, so the paths outside BOTH readings are the ones a human is pointed at.
179
+
180
+
181
+ - **A run now records which tldrx wrote it (#183).** `run.yml`'s `version: 1` is the FILE
182
+ FORMAT's number; nothing anywhere carried the framework's, so across 23 unattended runs on
183
+ three real workspaces no record could be attributed to the release that produced it — in a
184
+ week that shipped ten of them and moved behaviour in three (the DoD refusal record grew
185
+ `status`, the reviewer's diff base moved, a mandate check was reworded). `run.yml` gains
186
+ two additive keys beside the format's: `created_with`, stamped once at `run new` and never
187
+ rewritten, and `last_written_by`, rewritten by every save — so a run that outlived an
188
+ upgrade carries both ends of the range that drove it. `agent.spawned` and `agent.result`
189
+ carry `tldrx_version` for the same reason at the turn level: a spawn that never produced a
190
+ result is still a turn a release is answerable for. All of it reads from the one source
191
+ `tldrx --version` reads, so a record and the CLI cannot disagree. `run status` prints both
192
+ stamps; a run.yml written before the keys reads `not recorded`, which is a different fact
193
+ from a guess at what happened to be installed that day.
194
+
195
+ - **A task row now carries a duration it actually measured, and says which span it is
196
+ (#184).** `started_at` is the INVOCATION's clock and `ended_at` is the instant the row was
197
+ written, so every task of one parallel Build shares a start and subtracting the two yields
198
+ close to the whole invocation for each of them — three unattended runs measured 34.5 h,
199
+ 43.2 h and 36.8 h of span that no record could attribute to a phase or a sub-agent. Those
200
+ two fields keep their meaning; `duration_ms` is the new one, and it never travels without
201
+ `duration_basis`. `spawned` is the wall clock around the sub-agent's own process, measured
202
+ in `spawnAgent.ts` and written on the task row and on `agent.result`. `prepare-to-commit`
203
+ is the gap between a `--prepare` bundle's `prepared_at` and the `--commit` that recorded
204
+ the turn: the only span the framework can see for an in-session turn, and one that INCLUDES
205
+ the host's own time — a ceiling on the sub-agent's span the way `spent_usd` is a floor on
206
+ the money, and the spec says so rather than letting the number be read as the sub-agent's.
207
+ `tldrx cost` gains a duration column per attempt and a per-stage sum that names a mixed
208
+ basis instead of adding two different quantities, and counts the attempts it could not
209
+ time. A row from before this reads `not recorded`, never `0s`: "it took no time" and
210
+ "nobody timed it" are different facts and only one is a measurement.
211
+
212
+ - **A `maintain` skill turns a session into this repo's maintainer, and the circuit it encodes
213
+ is the one that was measured, not the one that sounds right.** Twelve maintenance waves ran
214
+ over two days by hand, and three of their lessons were costing time every cycle. First,
215
+ issues go stale in days: one asked for a fix to sequential Build execution while `--parallel`
216
+ had already shipped, so the skill reproduces an issue's measurement on current `origin/main`
217
+ BEFORE anything is touched, and a measured refutation closes it. Second, a fresh reviewer who
218
+ did not write the code found a real Important defect in four of those waves, each one
219
+ checkable from `git log`: `2a6413f` fixed a defect review found in `cbd5c4b`, and the fixes
220
+ that pre-merge review forced landed inside `28a987e` (the B0 mandate reword), `674049a` (the
221
+ heartbeat fix) and `103ff96` (the `runAuto` $0.00 fix). The one wave that reviewed AFTER
222
+ merging left its defect on `main` for two hours. So review is a mandatory step BEFORE
223
+ `scripts/merge-wave.sh`, run by a separate sub-agent on a mid-tier model, which is where the
224
+ defects were actually found. Third, concurrency: at most 3 issues a
225
+ cycle with non-overlapping file sets and at most 2 implementers at once, because the flaky
226
+ tests this repo already knows about redden under machine load and a red gate you caused
227
+ yourself costs a full re-run to disprove. The skill is a workflow and cites `AGENTS.md` and
228
+ `docs/RELEASING.md` by section rather than restating them; `test/maintain-skill.test.ts`
229
+ holds that premise mechanically — every `tldrx` command and `scripts/*.sh` it names must
230
+ exist in the real surface, every `§N` it cites must resolve to a real heading, and no private
231
+ workspace name or chat-product name may appear anywhere in the skill tree (#191). The review
232
+ step itself is still prose, which is the one thing the skill cannot fix about itself — #192
233
+ proposes making `scripts/merge-wave.sh` refuse a branch that carries no review record.
234
+
235
+ ### Fixed
236
+
237
+ - **No surface prints a bare `$0.00` over work nobody metered.** Measured across 23 real runs:
238
+ 45 % of 845 task rows are in-session turns recorded `cost_usd: null, metered: false`, which
239
+ every sum treats as contributing nothing — the only honest arithmetic there is. Two of those
240
+ runs then RENDERED `spent_usd: 0.00` against $3,000 and $200 ceilings after 30 and 9
241
+ stories. The counting and the caveat already existed (#103, #139); what did not was a rule
242
+ for the FIGURE, so each screen printed `$${n.toFixed(2)}` in its own words and three printed
243
+ it with no caveat at all. One implementation now writes it everywhere: `≥ $12.40 (7 tasks
244
+ unmetered)` when some of the work was metered, and `not measured: 9 in-session tasks, 0
245
+ metered` when none of it was and there is no floor worth printing. `run status`, `budget
246
+ show`, the dashboard's row and its hero, `replay`, `run auto`'s per-loop lines, the Build
247
+ handoff's `Cost:` header (and so the ship PR body, which embeds it), the `budget.warned`
248
+ note and every notification payload all read from it. `budget.yml` gains `unmetered_tasks`
249
+ and `spent_basis: lower-bound | complete` so an archived file alone still tells the truth —
250
+ emitted only when there IS something unmetered, so a fully metered run and every budget.yml
251
+ written before them stay byte-identical. `spent_usd` is unchanged and still what the ceiling
252
+ arithmetic enforces; a run that really did meter everything keeps its plain figure, because
253
+ a caveat on every screen is a caveat nobody reads.
254
+
4
255
  ## 0.11.1 — 2026-09-08
5
256
 
6
257
  ### Changed
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.13.0 | 2026-09-08 | `beta` | evidence a role expert can actually earn, and a review that leaves a record: `--mode full`'s runs pass mines `tldrx-work/**` while the domain gate judged every citation it produced against folders of code — measured at four role experts, **$9.47 and one evidence row**, and unfixable from the workspace because the single spelling the matcher would reach is the one `domainPaths()` drops — so the gate now treats the run record as in-domain for the file mined FROM it, scoped to the pass and never to the expert's `kind:` (a light file citing a handoff is still out of domain and still says so), two shipped role templates stop declaring `.tldrx/map/**` and `.tldrx/map/{repo}/gotchas.md` paths that matched nothing at all, and a pass that validated, spent money and earned zero rows now prints `the level did not move — $X.XX bought 0 evidence row(s)` with its reasons carried into `check.passed` instead of a silent ledger; `tldrx expert rescore` recovers what was already bought for $0 by re-reading `knowledge/*.md` under today's rules, dating rows by the knowledge file's own `trained_at` and never by the clock, with `rescored_at` additive beside `at` — its ABSENCE keeping the meaning every existing row had — and one `evidence.rescored` line per file it actually moved, so a free re-derivation can never be read as a paid turn; `scripts/merge-wave.sh` refuses a branch carrying no `.review/<branch>.md` with **exit 10**, its own code because `2` in that script is already "merge conflict", a stale record refusing rather than warning and staleness measured as "the code moved" rather than "the sha differs", since committing the record moves the head past exactly the sha it names; and the mutation check moves from the reviewer, whose allowance is `Read`/`Grep`/`Glob`/`Bash(git diff *)` and holds no pen, to the developer's contract that can run it, leaving the reviewer the read it can actually perform |
320
+ | 0.12.0 | 2026-09-08 | `beta` | records that can be attributed and spans that were actually measured: the reviewer can be pinned to its own model and effort per role (`reviewer:`) and per story stakes (`reviewer_by_stakes:`, keyed on a story's new optional `stakes:` enum), resolved field by field under `--model`/`--effort` and shipping NO opus default — because there is no evidence yet that a stronger reviewer finds more, only the record that lets the evidence accumulate: every verdict now names the model that produced it, a host review reading `basis: host-declared` off its own flags and a host that declared nothing reading `not recorded` rather than the bundle's suggestion; `run.yml` gains `created_with` and `last_written_by` beside the file format's own `version: 1`, and `agent.spawned`/`agent.result` carry `tldrx_version`, so a run that outlived an upgrade carries both ends of the range that drove it; a task row carries `duration_ms` that never travels without `duration_basis` — `spawned` is the wall clock around the sub-agent's process, `prepare-to-commit` is a ceiling that includes the host's own time — and `tldrx cost` shows a duration per attempt and a per-stage sum that names a mixed basis instead of adding two different quantities, with a pre-existing row reading `not recorded` and never `0s`; no surface prints a bare `$0.00` over work nobody metered — one implementation writes `≥ $12.40 (7 tasks unmetered)` or `not measured: 9 in-session tasks, 0 metered` across `run status`, `budget show`, the dashboard, `replay`, `run auto`, the Build handoff and every notification, and `budget.yml` gains `unmetered_tasks` and `spent_basis`; the surface a story actually changed is measured off its own diff over the range the reviewer was shown and appended as one `story.touches_widened` with `basis: "measured"` beside what was declared, advisory and never rewriting the operator's `touches:`; and a `maintain` skill encodes the maintenance circuit twelve hand-run waves actually measured — reproduce an issue on current `origin/main` before touching anything, a fresh reviewer before `merge-wave.sh` and not after, at most 3 issues and 2 implementers a cycle — pinned by a test that every command and `§N` it cites resolves |
319
321
  | 0.11.1 | 2026-09-08 | `beta` | operating a run when nobody is watching: the drive mandate now names the host's own context as the costliest instrument and tells the driver what to READ back — a sub-agent's outcome from its `result.json` and the ledger, never its transcript, each sub-agent briefed to report its turn in ten lines, and a cited file re-opened to VERIFY at a gate rather than re-read for a claim already made — paid for inside the unchanged line budgets rather than appended; `.tldrx/workspace.yml` takes one optional `notify:` block whose command is handed a `version: 1` JSON object on stdin at each moment a person is needed (an open question with its options, its recommendation and the literal `tldrx answer` line; a gate with its approve line; a finished or failed run with what its exit code's family means; a budget warning with both numbers), split to argv and never shelled, with a notifier that will not split, is not there, exits non-zero or hangs recorded as `notify.failed` instead of failing the run; `tldrx run auto --wait-answers <duration>` polls the question files and resumes when the answer lands instead of exiting 4 the moment a stage parks, and `--notify-every <duration>` sends a timer-driven heartbeat that reminds rather than reassures while a run is parked; and a new EN+ES guide page walks the whole unattended loop — host mode versus the engine, the payload keys, a dependency-free Node adapter, a first-run checklist and the four ways it silently does nothing — naming no messaging service, by the same reasoning the mandate has always given |
320
322
  | 0.11.0 | 2026-09-08 | `beta` | governance that leaves a record: every answer says who decided it and what repo it binds, an advisory contradiction check raises a question that never stops a run and states its own limit, the close and the Build handoff count decided vs not-stated; `tldrx story widen` is the sanctioned way to grow a story's surface (a done story needs `reopen --for-fix` first) and a defect no story owns is named in the handoff, the PR body and the boundary card instead of absorbed; a budget grant is a recorded number in `budget.yml` that survives every writer, `budget raise` warns or refuses against it, presets are labelled as the assumptions they are, `triage.budget_basis` says where a figure came from and `tldrx cost --stories` measures each story against the ceiling its spawn was given, honest about lower bounds |
321
323
  | 0.10.0 | 2026-09-07 | `beta` | the mechanical blockers measured in real runs, fixed: a refused DoD command is recorded as refused (never a fabricated exit 126) in the event, the handoff, the review and retro logs, and the readers that used to recover it as green; the dirty-tree refusal prints the exact stash and pop commands and its true reason; the reviewer diffs the epic as it was before the merge (`epic_base` recorded on the story and the bundle) instead of an empty range; `tldrx init` probes each declared command once and records `command_probes` beside a `commands:` allowlist it never guesses; `tldrx ship` opens a PR whose body describes the change and its known defects, with the handoff folded underneath, and a state refusal that honours settled stories' declared touches; reviewer turns carry their token split and the spend basis reads it, so a whole provider no longer reads as absent |
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  conflictOf
4
- } from "./chunk-qn8qnrgw.js";
4
+ } from "./chunk-fyr7f9t6.js";
5
5
  import {
6
6
  FactsStore,
7
7
  formatJaccard
8
- } from "./chunk-gj1ckg0c.js";
8
+ } from "./chunk-nwzevx0j.js";
9
9
  import {
10
10
  parseHookInput,
11
11
  readStdin
12
- } from "./chunk-8d0t69pk.js";
12
+ } from "./chunk-mvz7jwt8.js";
13
13
  import {
14
14
  EventLog,
15
15
  PHASE_ID_RE
16
- } from "./chunk-rw3n3z6t.js";
17
- import"./chunk-b26rvgjd.js";
16
+ } from "./chunk-f1w9sy07.js";
17
+ import"./chunk-yvgkw0gm.js";
18
18
  import {
19
19
  ADVISORY_KEY,
20
20
  MAX_FACT_CHARS,
@@ -24,7 +24,7 @@ import {
24
24
  renderQuestionBlock,
25
25
  replaceBlock,
26
26
  serializeQuestions
27
- } from "./chunk-h23db062.js";
27
+ } from "./chunk-1knfmj6j.js";
28
28
  import {
29
29
  ITERATION_ONLY_SLOT,
30
30
  PROJECT_FRAMEWORK_DIR,
@@ -32,7 +32,7 @@ import {
32
32
  commandProbeIssues,
33
33
  factsPath,
34
34
  parseYaml
35
- } from "./chunk-5hyd1rk1.js";
35
+ } from "./chunk-6fywd8xd.js";
36
36
 
37
37
  // src/hooks/answer-capture.ts
38
38
  import { existsSync as existsSync5 } from "fs";
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-zs84arc1.js";
4
+ } from "./chunk-qefqmgjn.js";
5
5
  import {
6
6
  allow,
7
7
  deny,
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-hx0jgrmh.js";
12
- import"./chunk-8d0t69pk.js";
11
+ } from "./chunk-v2xzr4qd.js";
12
+ import"./chunk-mvz7jwt8.js";
13
13
  import {
14
14
  asRunBudget,
15
15
  currentActor,
@@ -29,20 +29,20 @@ import {
29
29
  validateRunBudget,
30
30
  wouldExceed,
31
31
  wouldExceedHostTokens
32
- } from "./chunk-jmypgf6s.js";
32
+ } from "./chunk-fsgsn8vb.js";
33
33
  import {
34
34
  EventLog
35
- } from "./chunk-rw3n3z6t.js";
35
+ } from "./chunk-f1w9sy07.js";
36
36
  import {
37
37
  noteDeprecations
38
- } from "./chunk-h23db062.js";
38
+ } from "./chunk-1knfmj6j.js";
39
39
  import {
40
40
  PROJECT_WORK_DIR,
41
41
  findWorkspaceRoot,
42
42
  locateWork,
43
43
  parseYaml,
44
44
  stageYamlPath
45
- } from "./chunk-5hyd1rk1.js";
45
+ } from "./chunk-6fywd8xd.js";
46
46
 
47
47
  // src/hooks/budget-gate.ts
48
48
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseSrcToken
3
- } from "./chunk-5hyd1rk1.js";
3
+ } from "./chunk-6fywd8xd.js";
4
4
 
5
5
  // src/core/text/questions.ts
6
6
  var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
@@ -9122,4 +9122,4 @@ function noneBullet(lookedAt) {
9122
9122
  return `- none [src: absent:${lookedAt}]`;
9123
9123
  }
9124
9124
 
9125
- export { runtime, parseYaml, parseYamlRepairing, STAGES_DIR, TEMPLATES_DIR, PROJECT_FRAMEWORK_DIR, PROJECT_WORK_DIR, result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, requireRecord, asDocument, STORY_ID_RE, WAVE_ID_RE, MAX_WAVES, MAX_STORIES_PER_WAVE, MAX_PLAN_STORIES, requireStringList, requirePattern, requireVersion1, parseFrontMatter, ITERATION_ONLY_SLOT, readStory, DodCommandRefused, runDodCommand, commandProbeIssues, readableSource, SRC_PATTERNS, srcRule, parseSrcToken, classifySrc, locateWork, findWorkspaceRoot, loadWorkspace, toSrcContext, repoPath, factsPath, stageYamlPath, listRunDirs, parseHandoff, isHandoff, BULLET_RULE, EMPTY_SECTION_RULE, validateHandoff, validateCitations, noneBullet };
9125
+ export { runtime, parseYaml, parseYamlRepairing, FRAMEWORK_ROOT, STAGES_DIR, TEMPLATES_DIR, PROJECT_FRAMEWORK_DIR, PROJECT_WORK_DIR, result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, requireRecord, asDocument, STORY_ID_RE, WAVE_ID_RE, MAX_WAVES, MAX_STORIES_PER_WAVE, MAX_PLAN_STORIES, requireStringList, requirePattern, requireVersion1, parseFrontMatter, ITERATION_ONLY_SLOT, readStory, DodCommandRefused, runDodCommand, commandProbeIssues, readableSource, SRC_PATTERNS, srcRule, parseSrcToken, classifySrc, locateWork, findWorkspaceRoot, loadWorkspace, toSrcContext, repoPath, factsPath, stageYamlPath, listRunDirs, parseHandoff, isHandoff, BULLET_RULE, EMPTY_SECTION_RULE, validateHandoff, validateCitations, noneBullet };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  toolInput
3
- } from "./chunk-hx0jgrmh.js";
3
+ } from "./chunk-v2xzr4qd.js";
4
4
 
5
5
  // src/hooks/lib/wouldBe.ts
6
6
  import { existsSync, readFileSync } from "node:fs";
@@ -10,7 +10,7 @@ import {
10
10
  requireString,
11
11
  requireVersion,
12
12
  result
13
- } from "./chunk-5hyd1rk1.js";
13
+ } from "./chunk-6fywd8xd.js";
14
14
 
15
15
  // src/core/events/EventLog.ts
16
16
  import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -448,6 +448,9 @@ function describe(value) {
448
448
  return typeof value === "string" ? `\`${value}\`` : String(value);
449
449
  }
450
450
 
451
+ // src/core/run/duration.ts
452
+ var DURATION_BASES = ["spawned", "prepare-to-commit"];
453
+
451
454
  // src/core/run/RunFile.ts
452
455
  var STAGE_STATUSES = [
453
456
  "pending",
@@ -596,6 +599,10 @@ function validateRunFile(input) {
596
599
  }
597
600
  if (doc.attended_by !== undefined)
598
601
  requireEnum(doc.attended_by, ATTENDED_BY, "attended_by", issues);
602
+ for (const key of ["created_with", "last_written_by"]) {
603
+ if (doc[key] !== undefined)
604
+ requireString(doc[key], key, issues);
605
+ }
599
606
  if (doc.keep_worktrees !== undefined && typeof doc.keep_worktrees !== "boolean") {
600
607
  issues.push({ path: "keep_worktrees", message: `expected a boolean, got ${typeof doc.keep_worktrees}` });
601
608
  }
@@ -717,6 +724,17 @@ function validateRunFile(input) {
717
724
  issues.push({ path: `${tp}.${key}`, message: "expected a number >= 0" });
718
725
  }
719
726
  }
727
+ if (task.duration_ms !== undefined) {
728
+ if (typeof task.duration_ms !== "number" || !Number.isFinite(task.duration_ms) || task.duration_ms < 0) {
729
+ issues.push({ path: `${tp}.duration_ms`, message: "expected a number >= 0" });
730
+ }
731
+ requireEnum(task.duration_basis, DURATION_BASES, `${tp}.duration_basis`, issues);
732
+ } else if (task.duration_basis !== undefined) {
733
+ issues.push({
734
+ path: `${tp}.duration_basis`,
735
+ message: "a duration_basis with no duration_ms names the basis of nothing"
736
+ });
737
+ }
720
738
  if (typeof task.cost_usd === "number")
721
739
  spentFromTasks += task.cost_usd;
722
740
  checkOrder(task.started_at, task.ended_at, tp, issues);
@@ -23,7 +23,7 @@ import {
23
23
  requireVersion,
24
24
  requireVersion1,
25
25
  result
26
- } from "./chunk-5hyd1rk1.js";
26
+ } from "./chunk-6fywd8xd.js";
27
27
 
28
28
  // src/hooks/lib/runFile.ts
29
29
  import { existsSync, readFileSync } from "node:fs";
@@ -176,6 +176,24 @@ function nowRfc3339() {
176
176
  return `${new Date().toISOString().slice(0, 19)}Z`;
177
177
  }
178
178
 
179
+ // src/core/budget/spentFigure.ts
180
+ var SPENT_BASES = ["lower-bound", "complete"];
181
+ function spentBasis(unmetered) {
182
+ return unmetered > 0 ? "lower-bound" : "complete";
183
+ }
184
+ function tallyOf(tasks) {
185
+ let usd = 0;
186
+ let unmetered = 0;
187
+ for (const task of tasks) {
188
+ if (task.metered === false) {
189
+ unmetered += 1;
190
+ continue;
191
+ }
192
+ usd += task.cost_usd ?? 0;
193
+ }
194
+ return { usd: Math.round(usd * 100) / 100, unmetered, metered: tasks.length - unmetered };
195
+ }
196
+
179
197
  // src/core/budget/RunBudget.ts
180
198
  var ON_EXCEED = ["block", "warn"];
181
199
  var ON_HOST_TOKENS_EXCEED = ["warn", "block"];
@@ -230,6 +248,12 @@ function validateRunBudget(input) {
230
248
  if (doc.ceiling_host_tokens !== undefined && doc.ceiling_host_tokens !== null) {
231
249
  requireNumber(doc.ceiling_host_tokens, "ceiling_host_tokens", issues);
232
250
  }
251
+ if (doc.unmetered_tasks !== undefined && doc.unmetered_tasks !== null) {
252
+ requireNumber(doc.unmetered_tasks, "unmetered_tasks", issues);
253
+ }
254
+ if (doc.spent_basis !== undefined && doc.spent_basis !== null) {
255
+ requireEnum(doc.spent_basis, SPENT_BASES, "spent_basis", issues);
256
+ }
233
257
  if (doc.authorized_usd !== undefined && doc.authorized_usd !== null) {
234
258
  requireNumber(doc.authorized_usd, "authorized_usd", issues);
235
259
  requirePositiveGrant(doc.authorized_usd, "authorized_usd", issues);
@@ -315,6 +339,8 @@ function asRunBudget(input) {
315
339
  authorized_by: doc.authorized_by ?? null,
316
340
  authorized_at: doc.authorized_at ?? null,
317
341
  on_grant_exceed: doc.on_grant_exceed ?? DEFAULT_ON_GRANT_EXCEED,
342
+ unmetered_tasks: doc.unmetered_tasks ?? 0,
343
+ spent_basis: doc.spent_basis ?? spentBasis(doc.unmetered_tasks ?? 0),
318
344
  phases: (doc.phases ?? []).map((phase) => ({
319
345
  id: phase.id,
320
346
  ceiling_usd: phase.ceiling_usd,
@@ -569,12 +595,14 @@ function readEvidenceRows(input) {
569
595
  continue;
570
596
  }
571
597
  const confidence = str2(row.confidence);
598
+ const rescoredAt = str2(row.rescored_at);
572
599
  evidence.push({
573
600
  kind,
574
601
  src,
575
602
  at,
576
603
  ...row.cross === true ? { cross: true } : {},
577
- ...isEvidenceConfidence(confidence) ? { confidence } : {}
604
+ ...isEvidenceConfidence(confidence) ? { confidence } : {},
605
+ ...rescoredAt === "" ? {} : { rescored_at: rescoredAt }
578
606
  });
579
607
  }
580
608
  return { evidence, ignored: [...counts.values()] };
@@ -1297,4 +1325,4 @@ function shortBy(estimate, remaining) {
1297
1325
  return Math.max(0.01, Math.ceil((estimate - remaining) * 100) / 100);
1298
1326
  }
1299
1327
 
1300
- export { loadRunView, isAttendedByHostView, runSpend, hostTokensIn, renderRunEconomies, cursorStage, openRunViews, newestActiveRun, currentActor, nowRfc3339, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ON_GRANT_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, stackExpertNames, remainingWork, wouldExceed, wouldExceedHostTokens, raiseCommand, shortBy };
1328
+ export { loadRunView, isAttendedByHostView, runSpend, hostTokensIn, renderRunEconomies, cursorStage, openRunViews, newestActiveRun, currentActor, nowRfc3339, spentBasis, tallyOf, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ON_GRANT_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, stackExpertNames, remainingWork, wouldExceed, wouldExceedHostTokens, raiseCommand, shortBy };
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  findDuplicate
3
- } from "./chunk-gj1ckg0c.js";
3
+ } from "./chunk-nwzevx0j.js";
4
4
  import {
5
5
  evidencePath,
6
6
  gateEvidencePath,
7
7
  parseEvidence
8
- } from "./chunk-rw3n3z6t.js";
8
+ } from "./chunk-f1w9sy07.js";
9
9
  import {
10
10
  openBlocks,
11
11
  parseQuestions
12
- } from "./chunk-h23db062.js";
12
+ } from "./chunk-1knfmj6j.js";
13
13
 
14
14
  // src/core/distill/distill.ts
15
15
  var CONFLICT_THRESHOLD = 0.6;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runtime
3
- } from "./chunk-5hyd1rk1.js";
3
+ } from "./chunk-6fywd8xd.js";
4
4
 
5
5
  // src/core/hooks/passthrough.ts
6
6
  async function readStdin() {
@@ -3,7 +3,7 @@ import {
3
3
  withWorkspaceLock,
4
4
  workspaceRootOfFactsPath,
5
5
  writeAtomic
6
- } from "./chunk-b26rvgjd.js";
6
+ } from "./chunk-yvgkw0gm.js";
7
7
  import {
8
8
  FACT_CONFIDENCES,
9
9
  FACT_KINDS,
@@ -14,7 +14,7 @@ import {
14
14
  isLive,
15
15
  isRetired,
16
16
  noteDeprecations
17
- } from "./chunk-h23db062.js";
17
+ } from "./chunk-1knfmj6j.js";
18
18
  import {
19
19
  SRC_PATTERNS,
20
20
  asDocument,
@@ -27,7 +27,7 @@ import {
27
27
  requireString,
28
28
  requireVersion,
29
29
  result
30
- } from "./chunk-5hyd1rk1.js";
30
+ } from "./chunk-6fywd8xd.js";
31
31
 
32
32
  // src/core/facts/FactsStore.ts
33
33
  import { existsSync, readFileSync } from "node:fs";
@@ -3,7 +3,7 @@ import {
3
3
  EMPTY_SECTION_RULE,
4
4
  noneBullet,
5
5
  srcRule
6
- } from "./chunk-5hyd1rk1.js";
6
+ } from "./chunk-6fywd8xd.js";
7
7
 
8
8
  // src/hooks/lib/messages.ts
9
9
  var GRAMMAR = "[src: <repo:path:line> | https://… | Q<n> | F<n> | $ <cmd> → exit <n> | graph:<node> | absent:<path>]";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parseHookInput,
3
3
  readStdin
4
- } from "./chunk-8d0t69pk.js";
4
+ } from "./chunk-mvz7jwt8.js";
5
5
 
6
6
  // src/hooks/lib/decide.ts
7
7
  function deny(reason) {