tldr-experts 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1113 -0
- package/README.md +20 -5
- package/dist/hooks/answer-capture.js +136 -12
- package/dist/hooks/budget-gate.js +9 -9
- package/dist/hooks/{chunk-889tybxc.js → chunk-17gv74sr.js} +3 -3
- package/dist/hooks/{chunk-nadqsr3w.js → chunk-97ncnegd.js} +1 -1
- package/dist/hooks/{chunk-a6rpj2cp.js → chunk-9w64mxdh.js} +345 -22
- package/dist/hooks/chunk-dgct01h0.js +804 -0
- package/dist/hooks/{chunk-a5dq2dcp.js → chunk-dhbxzjfs.js} +2 -4
- package/dist/hooks/{chunk-hcrbr430.js → chunk-g4db8rbs.js} +2 -30
- package/dist/hooks/{chunk-5556vjt5.js → chunk-ka6bkb64.js} +1 -1
- package/dist/hooks/{chunk-14zn51kh.js → chunk-pstax1g9.js} +28 -569
- package/dist/hooks/chunk-rrkdfk7s.js +30 -0
- package/dist/hooks/{chunk-qw73rdbr.js → chunk-v4ay50nq.js} +1 -1
- package/dist/hooks/{chunk-v1c1hpb8.js → chunk-vrhczzxs.js} +1 -1
- package/dist/hooks/{chunk-bvm6vjrt.js → chunk-x6n8b5fp.js} +1 -1
- package/dist/hooks/{chunk-3w55tp71.js → chunk-yvp9ayxs.js} +51 -17
- package/dist/hooks/claim-sources.js +8 -10
- package/dist/hooks/dod-gate.js +7 -7
- package/dist/hooks/no-reask.js +8 -9
- package/dist/hooks/session-start.js +23 -18
- package/dist/hooks/statusline.js +10 -10
- package/dist/tldrx.js +3045 -1228
- package/env.yml +9 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/tldrx/SKILL.md +8 -1
- package/stages/how/stage.yml +9 -2
- package/stages/plan/stage.yml +26 -0
- package/templates/experts/delivery.md +3 -0
- package/templates/questions.md +8 -0
- package/dist/hooks/chunk-q8d3sff9.js +0 -220
- package/dist/hooks/chunk-sae7sqty.js +0 -206
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1118 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.1 — 2026-09-03
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The Build handoff's `Cost:` header was invocation-scoped, so a re-entered stage reported
|
|
8
|
+
`$0.00` for a phase that had spent `$0.44` (#138).** The sibling of #137's two sections, one
|
|
9
|
+
line higher up, on a document whose own docstring says it "describes the phase, not the
|
|
10
|
+
invocation". `writeHandoff` fed the header `this.spent()` — the sum of the tasks THIS process
|
|
11
|
+
spawned — so a `tldrx next` → `tldrx reject` → `tldrx next` rewrote the same file's
|
|
12
|
+
`Cost: $0.44 of $200.00 ceiling` as `Cost: $0.00`: the second invocation settled nothing,
|
|
13
|
+
spent nothing, and said so about the whole phase. Measured on `f5936d2` with the `TWO_WAVES`
|
|
14
|
+
fixture at `FAKE_BUILD_COST=0.11`.
|
|
15
|
+
- **`FAKE_BUILD_COST=0` is why nobody saw it.** Every other re-entry test in
|
|
16
|
+
`test/build-executor.test.ts` pins the cost to zero, where both writes read `$0.00` and
|
|
17
|
+
agree. The new block does not, and buys the headroom that needs with a $200 stage ceiling —
|
|
18
|
+
the budget gate refuses to RESTART a stage whose estimate no longer fits, so a fixture whose
|
|
19
|
+
stage estimate IS the phase ceiling cannot re-enter once a cent is recorded. Raising the
|
|
20
|
+
default in the shared fixture instead was rejected: it would have rewritten the pinned
|
|
21
|
+
expectations of every re-entry test in the file for a defect one block now covers head-on.
|
|
22
|
+
- **The durable source is `run.yml`'s `stage.cost_usd`**, plus what this invocation has spent
|
|
23
|
+
and not yet handed back (`phaseCostToDate`, exported from `executors/build.ts`). Chosen over
|
|
24
|
+
the `agent.result` events because it is the ledger the budget is derived from AND it
|
|
25
|
+
validates its own arithmetic: `rollUp` recomputes it from `stage.tasks` on every save,
|
|
26
|
+
`rollUpBudget` mirrors it into `budget.yml`, `run status` and the dashboard read it, and
|
|
27
|
+
`validateRunFile` refuses a `run.yml` whose `budget.spent_usd` drifts from the sum of its
|
|
28
|
+
task rows by more than a cent. The events carry the same numbers — every `recordTask` is
|
|
29
|
+
paired with an `agent.result` written from the same task in the same loop — but nothing
|
|
30
|
+
checks that they still do.
|
|
31
|
+
- **Three properties it rests on, verified rather than assumed.** `tldrx reject` does not touch
|
|
32
|
+
the number (it rewrites `status`, `ended_at` and `gate` and nothing else), which answers the
|
|
33
|
+
open question in the issue: a re-run reports the earlier spend, because the money was spent.
|
|
34
|
+
This invocation is not in `run.yml` yet, because `recordExecutorTasks` runs after the
|
|
35
|
+
executor returns — so adding the two cannot double-count, and a re-entry that DOES spend is
|
|
36
|
+
pinned at `$0.22 + $0.11 = $0.33`. And opening the store mid-stage is the shape the executor
|
|
37
|
+
already uses twice, not a new coupling.
|
|
38
|
+
- **`ExecutorOutcome.costUsd` stays invocation-scoped.** It is what the facilitator adds to the
|
|
39
|
+
run budget; a phase-to-date figure there would double-count on every re-entry.
|
|
40
|
+
- **Unreadable is not zero.** No `run.yml`, one that fails schema validation, or a stage id
|
|
41
|
+
that does not resolve gives this invocation's own spend with the reason in brackets — never
|
|
42
|
+
a confident total. A stage that has genuinely spent nothing still reads `$0.00` with no note.
|
|
43
|
+
|
|
44
|
+
- **That same header was a LOWER BOUND whenever a turn ran in-session, and nothing on the line
|
|
45
|
+
said so (#139).** A host session driving `--prepare` / `--commit` without `--cost-usd` is
|
|
46
|
+
recorded as `cost_usd: null` + `metered: false`, and `rollUp` sums it as nothing — so the phase
|
|
47
|
+
figure is what the METERED turns cost, not what the phase cost. The dashboard has marked
|
|
48
|
+
exactly this case as a `lower bound` since #103 and `tldrx budget show` prints it in words; the
|
|
49
|
+
handoff header was the one cost surface that stayed silent, on the FIRST write as well as a
|
|
50
|
+
re-entry. Filed as *inferred* and **measured before it was fixed**, which is what the issue
|
|
51
|
+
asked for: a fixture driving the real host path — `--prepare`, a `result.json` with no
|
|
52
|
+
`cost_usd`, `--commit` — put one unmetered turn beside one `$0.11` spawn and wrote
|
|
53
|
+
`Cost: $0.11 of $200.00 ceiling`, a bare figure indistinguishable from a fully metered phase.
|
|
54
|
+
- **One derivation, three surfaces.** The counting and the sentence moved out of
|
|
55
|
+
`dashboard/model.ts` into `core/budget/spendBasis.ts`, and the header prints what it returns.
|
|
56
|
+
The dashboard's model imports the page renderer, so the executor could not import IT — and
|
|
57
|
+
fixing the wording in place is how two spellings of one caveat get born, which the issue
|
|
58
|
+
named as the reason not to. Only the SUBJECT differs: a stage-scoped header says "the stage".
|
|
59
|
+
Dashboard output is unchanged.
|
|
60
|
+
- **The counts come from the same rows the sum does** — `run.yml`'s `stage.tasks`, plus this
|
|
61
|
+
invocation's, for the same reason `invocationUsd` is added to the total: `recordExecutorTasks`
|
|
62
|
+
runs after the executor returns, so counting `run.yml` alone would report the first write of a
|
|
63
|
+
host-driven handoff as fully metered.
|
|
64
|
+
- **A fully metered stage keeps its clean line.** `measured` is the one basis with nothing to
|
|
65
|
+
caveat. A turn counts as having produced no dollars if it is `metered: false` OR a metered
|
|
66
|
+
`cost_usd` of exactly `0` — the wider of the two readings, inherited unchanged from #103, so
|
|
67
|
+
the two surfaces cannot classify the same turn differently. It is also the conservative
|
|
68
|
+
direction: #138 and #139 both flattered the number.
|
|
69
|
+
- **Both caveats when both apply.** An unreadable `run.yml` (#138) and an unmetered turn (#139)
|
|
70
|
+
are different facts about the same figure, and the note now carries both rather than one.
|
|
71
|
+
|
|
72
|
+
- **`test/attempt-cost.test.ts` proved "carries no format refusal" with the bare word
|
|
73
|
+
`REFUSED`, and unrelated prompt prose turned it red (#135).** The thing it means to detect
|
|
74
|
+
is `renderFormatRefusal`'s heading; what it detected was an eight-letter English word,
|
|
75
|
+
anywhere in a ~14 KB document. #133 added one sentence elsewhere in the same prompt that
|
|
76
|
+
happened to use it and the assertion went red over prose that has nothing to do with a
|
|
77
|
+
refusal — a wrong-instrument failure, not a behaviour change.
|
|
78
|
+
- The heading is now exported as `FORMAT_REFUSAL_HEADING` from `src/core/build/review.ts`
|
|
79
|
+
and asserted in place of the word, so the renderer and its test cannot spell it
|
|
80
|
+
differently. Same idiom, and same reason, as `SRC_GRAMMAR_HEADING` and
|
|
81
|
+
`REVIEWER_FOCUS_HEADING`. A test pins that the constant IS the rendered first line.
|
|
82
|
+
- The workaround went with it: `payloadCapLines`' docstring told the next author not to use
|
|
83
|
+
the word. A comment asking people to avoid an English word was never a guard, and the
|
|
84
|
+
prose is free to say `REFUSED` again.
|
|
85
|
+
- **03-plan had NO map at all on a single-repo workspace (#136).** Its one map declaration was
|
|
86
|
+
`.tldrx/map/workspace.md`, which `buildMap.ts:83-88` writes only in multi-repo mode — so on
|
|
87
|
+
a single-repo workspace Plan's entire map input resolved to nothing, while all six
|
|
88
|
+
`MAP_DOCS` sat unread under `.tldrx/map/<repo>/`. Measured on a single-repo fixture with a
|
|
89
|
+
real map: `map PRESENT: []`, and the prompt carried no map content whatsoever.
|
|
90
|
+
- Plan now also declares `.tldrx/map/{repo}/commands.md`. This is a GATE, not a preference:
|
|
91
|
+
Plan writes each story's `dod.commands`, the `plan` check validates every one against the
|
|
92
|
+
workspace allowlist and refuses the story when it cannot ("an empty allowlist is not a
|
|
93
|
+
permit", `src/core/schemas/commandAllowlist.ts:33`) — and Plan was shown that allowlist
|
|
94
|
+
nowhere. `.tldrx/workspace.yml` is not one of its inputs, the generic stage prompt renders
|
|
95
|
+
no commands section (only the DEVELOPER prompt does, `prompts.ts:175`), and multi-repo's
|
|
96
|
+
`workspace.md` carries repo name, path, stack, branch and confidence, not commands. The
|
|
97
|
+
map document that mirrors the allowlist (`renderMap.ts:27`) was declared by no stage.
|
|
98
|
+
- **One document, not six.** Plan decomposes a design 02-how has already placed on real
|
|
99
|
+
paths, so architecture stays upstream; six documents per repo on a stage whose job is
|
|
100
|
+
splitting and ordering is the context nobody asked for that the wave-N lesson in
|
|
101
|
+
`seedInputs.ts` is about. Measured cost: +120 B on a 20,777 B prompt, against budgets of
|
|
102
|
+
98,304 (`inputs_max_bytes`) and 163,840 (`prompt_max_bytes`).
|
|
103
|
+
- **Multi-repo semantics are unchanged** — `workspace.md` stays, and stays without `{repo}`.
|
|
104
|
+
Its absence on a single-repo workspace is still SAID rather than performed (#131): there
|
|
105
|
+
is no cross-repo view of one repo, and the `absent:` block reports exactly that.
|
|
106
|
+
|
|
107
|
+
- **A re-entered Build stage OVERWROTE its own handoff with a degraded reconstruction (#137).**
|
|
108
|
+
`04-build/handoff.md` is rewritten by every `tldrx next` that reaches the end of the stage, and
|
|
109
|
+
its own docstring says it "describes the phase, not the invocation" — but two of its sections
|
|
110
|
+
were fed from what THIS process did. So the second write over a re-entered stage (`tldrx reject`
|
|
111
|
+
then `tldrx next`, or any run whose stories did not all settle at once) replaced true statements
|
|
112
|
+
with false ones. Measured on `340fb91`: an Evidence ledger of two green DoD rows became
|
|
113
|
+
`- no Definition of Done ran [src: absent:03-plan/stories]`, and a Gate row reading
|
|
114
|
+
`(S1, S2 merged)` became `(no story merged)` while `git log epic/e1` carried both merge commits
|
|
115
|
+
throughout. The Gate section is the one a human reads before merging an epic by hand, so this is
|
|
116
|
+
the 2026-08-30 empty-merge defect arriving from the other side; a third section degraded quietly
|
|
117
|
+
with them, Findings losing each story's merged sha to `at (no commit)`.
|
|
118
|
+
- **Reconstructed where the truth is durable.** The row `fromDisk` builds for a story an earlier
|
|
119
|
+
invocation settled now READS its DoD results and its merged commit from `events.jsonl` via
|
|
120
|
+
`readReviewLedger` — the same reader `rereview` already trusts when it declines to re-run a
|
|
121
|
+
DoD, so nothing new is being asserted. The Gate section learns what is on an epic branch from
|
|
122
|
+
both sources instead of one: this process's own merges, plus the stories disk says reached
|
|
123
|
+
`done`, which in this pipeline is a status only a merged story reaches.
|
|
124
|
+
- **Said as absent, with its reason, where it is not.** What a merge CARRIED is counted before
|
|
125
|
+
the merge and stored nowhere, and afterwards cannot be measured at all — a merged story branch
|
|
126
|
+
is an ancestor of the epic either way. Those stories get a third list of their own and a row
|
|
127
|
+
that names the gap and the command that closes it — *S1, S2 merged by an earlier `tldrx next`
|
|
128
|
+
— what each carried was not re-measured here, run `git log epic/e1`*. Folding them into
|
|
129
|
+
`merged` would overclaim; dropping them is what printed `no story merged`. A declared dod
|
|
130
|
+
command whose result is in no event is named the same way against its own log, and the negative
|
|
131
|
+
`- no Definition of Done ran` is now written only when the stories declare no commands.
|
|
132
|
+
- The degraded handoff PASSED the claim-sources check the whole time — it reads citations, not
|
|
133
|
+
truth — so nothing downstream was ever going to catch this. Five tests now pin it.
|
|
134
|
+
- Answered and filed, not fixed: the header's `Cost:` line is invocation-scoped too, so the
|
|
135
|
+
second write reports `$0.00` for a stage that spent `$0.44` (measured). Different seam — the
|
|
136
|
+
number is not in the ledger this fix reads — so it is #138.
|
|
137
|
+
|
|
138
|
+
- **The `feature` preset declared map inputs without `{repo}`, so 02-how and 03-plan ran with
|
|
139
|
+
no map at all (#131).** `stages/how/stage.yml` asked for `.tldrx/map/architecture.md` and
|
|
140
|
+
`.tldrx/map/conventions.md`. `tldrx init` writes the map PER REPO —
|
|
141
|
+
`.tldrx/map/<repo>/architecture.md`, one folder per repo, six `MAP_DOCS` documents each
|
|
142
|
+
(`src/core/map/buildMap.ts:74-79`) — so nothing on disk could ever answer either
|
|
143
|
+
declaration. They are OPTIONAL inputs, `declaredInputsOf` drops an optional input that is
|
|
144
|
+
not present, and the two stages the map exists FOR were dispatched without it on every
|
|
145
|
+
feature-scope run. 01-what and 05-watch carried the token the whole time, which is why two
|
|
146
|
+
stages of five went wrong quietly for as long as they did.
|
|
147
|
+
- Both paths gain `{repo}`. `03-plan`'s `.tldrx/map/workspace.md` deliberately does NOT:
|
|
148
|
+
it is the one map document written at the map ROOT, and only in multi-repo mode
|
|
149
|
+
(`buildMap.ts:83-88`), so the token would point it at a file that never exists. The
|
|
150
|
+
reasoning is now a comment in the file so the next sweep leaves it alone too.
|
|
151
|
+
- **Swept all thirteen scope presets.** `workflows/*.yml` compose the same five stage files
|
|
152
|
+
and declare no `inputs:` of their own, so `how` was the only offender — and both facts are
|
|
153
|
+
now pinned: one test asserts no shipped stage declares a `MAP_DOCS` document without
|
|
154
|
+
`{repo}` (derived from `MAP_DOCS`, so a seventh document is covered the day it is added),
|
|
155
|
+
another asserts no preset grows stage inputs that would bypass it.
|
|
156
|
+
- **And an absence is now SAID rather than performed.** A declared input that resolves to
|
|
157
|
+
nothing is named twice: as a `### Declared, but not on disk` block in the prompt's
|
|
158
|
+
`## Inputs`, carrying that path's own `[src: absent:<path>]` token so the stage's handoff
|
|
159
|
+
can source a negative claim on it, and as one line on stdout. This is the half that turns
|
|
160
|
+
the next such typo into a first-run report instead of a live incident — a sub-agent that is
|
|
161
|
+
not told what is missing cannot tell "the map says nothing about this" from "I was never
|
|
162
|
+
shown the map", and its handoff records the second as the first.
|
|
163
|
+
|
|
164
|
+
- **The reviewer prompt described the result envelope in prose beside the schema that defines
|
|
165
|
+
it (#133).** `REVIEW_SCHEMA` is handed to `claude --json-schema` on the spawned path and
|
|
166
|
+
written verbatim into the bundle as `pending.json` → `result_schema` on the host path, so
|
|
167
|
+
both halves of the handshake already answered the same question — and the prompt then
|
|
168
|
+
described the same envelope again, key by key, in the one document a model reads most
|
|
169
|
+
carefully. Two live reviews lost cycles to it, one because the host dictated the shape from
|
|
170
|
+
memory, which is exactly what a prose paraphrase invites.
|
|
171
|
+
- `## Produce` now points at `result_schema` (and at `--json-schema` for a spawn) as the
|
|
172
|
+
single authority and states no field of it. The verdicts moved to their own `## Verdict`
|
|
173
|
+
section, because WHICH verdict to return is judgement and judgement is what the prompt is
|
|
174
|
+
for; #77's `refuted`-needs-a-citation contract is judgement too and is untouched.
|
|
175
|
+
- **The 4096-byte payload cap is now named.** The verdict's prose is copied into a
|
|
176
|
+
`check.passed`/`check.failed` payload, `validateEvent` refuses any payload over
|
|
177
|
+
`MAX_PAYLOAD_BYTES` (`src/core/events/Event.ts:121`) and `EventLog.append` THROWS rather
|
|
178
|
+
than writing a shortened line — so an essay-length verdict does not arrive trimmed, it
|
|
179
|
+
takes the ledger entry down with it. The number is imported into the prompt, never typed,
|
|
180
|
+
so changing the cap changes the promise.
|
|
181
|
+
- Pinned by a test that asserts the prompt REFERENCES rather than RESTATES, off the schema
|
|
182
|
+
itself: every field name in `REVIEW_SCHEMA` — top level and inside a `fixlist` row, minus
|
|
183
|
+
the verdict words — must not appear in the rendered prompt. A field added to the schema is
|
|
184
|
+
covered without anyone remembering to add it here.
|
|
185
|
+
- **Pruning a blocked story's worktree destroyed the work inside it (#129).** Measured live
|
|
186
|
+
2026-09-02 on run `260830-money-and-payments` (aparece-v2), reported by the unattended
|
|
187
|
+
driver: a story's Definition of Done failed, the executor settled it `blocked`, and
|
|
188
|
+
`cleanUp` ran `git worktree remove --force` over a tree that still held the developer's
|
|
189
|
+
fix, uncommitted. There was no branch, no stash and no reflog to get it back from. `blocked`
|
|
190
|
+
is precisely the state a human is going to want to inspect, and it was the one state that
|
|
191
|
+
destroyed the evidence first.
|
|
192
|
+
- The invariant, and it has no exceptions in it: **the framework never deletes a worktree
|
|
193
|
+
holding changes that reached no ref.** Before any prune, `settle` commits whatever the
|
|
194
|
+
tree still holds to the STORY branch as
|
|
195
|
+
`wip(<id>): rescued from a story that settled \`blocked\`` — an honest subject naming the
|
|
196
|
+
verdict, the reason in the body, and "nothing reviewed this and nothing merged it" said
|
|
197
|
+
out loud, because a rescue commit that read like a delivery would be #130 in another file.
|
|
198
|
+
- Commit-then-prune rather than never-prune, because *recoverable* has to mean recoverable
|
|
199
|
+
**by sha**: a kept directory survives until the next `run close` or temp sweep, a commit
|
|
200
|
+
on the story branch survives a year. If the commit cannot be made, the worktree is KEPT
|
|
201
|
+
instead and the review log names its path.
|
|
202
|
+
- The sha is recorded where a human looks, not only where a terminal scrolls: a
|
|
203
|
+
`## Uncommitted work rescued` section in `04-build/log/<id>.md`, and a new
|
|
204
|
+
`story.work_rescued` event (§2.9) carrying story, repo, branch, sha and the settled
|
|
205
|
+
status. It is the second event in the enum that records tldrx touching git on the
|
|
206
|
+
operator's behalf, and it is appended only when a commit was really made.
|
|
207
|
+
- `--keep-worktrees`, a story parked at `review`, and a parked developer failure are
|
|
208
|
+
untouched: nothing is about to be deleted on any of those paths, so there is nothing to
|
|
209
|
+
rescue from. A green story rescues nothing either — `commitIfDirty` has already put every
|
|
210
|
+
byte on the branch.
|
|
211
|
+
- Red first, in `test/blocked-prune.test.ts`: on `14f01ec` a blocked story's `s1.txt`
|
|
212
|
+
existed nowhere — `git cat-file -e story/<run>/S1:s1.txt` exited non-zero and the tip of
|
|
213
|
+
the story branch was still `chore: fixture repo`. Five tests, including the
|
|
214
|
+
commit-cannot-be-made path (a `pre-commit` hook that exits 1), which asserts the tree is
|
|
215
|
+
still there with the file in it.
|
|
216
|
+
|
|
217
|
+
- **The fix list recorded `Resolved: yes` over a fix that did not exist (#130).** Same
|
|
218
|
+
incident, and the driver called it the most dangerous of the four because it fails silently
|
|
219
|
+
and in the wrong direction: `04-build/fixlist/S4-1.md` ended with **`Resolved: yes`** and a
|
|
220
|
+
`result.json` describing the fix in detail, while the code did not contain it — the worktree
|
|
221
|
+
holding it had been pruned (#129) before anything reached a ref. The audit trail said a
|
|
222
|
+
defect was closed while it was alive, and one approval away from carrying the story to
|
|
223
|
+
`done`. Root: the accounting was written from an agent's REPORT rather than from a verified
|
|
224
|
+
code state, which is the one thing this framework refuses everywhere else and had never
|
|
225
|
+
applied to its own bookkeeping.
|
|
226
|
+
- **A close now carries the sha the fix landed as**: the line is `Resolved: yes <sha>`, and
|
|
227
|
+
the sha is CHECKED before a story may settle — it must resolve to a commit in the story's
|
|
228
|
+
repo (`git rev-parse --verify <sha>^{commit}`) and be reachable from the story branch
|
|
229
|
+
(`git merge-base --is-ancestor`). A bare `yes` closes nothing; `isOpen` treats an
|
|
230
|
+
unevidenced claim as an open finding.
|
|
231
|
+
- **The record stops lying about itself.** A claim that does not check out is rewritten in
|
|
232
|
+
place to `Resolved: claimed-unverified — <why>`, keeping the fact that somebody reported a
|
|
233
|
+
fix while withdrawing the assertion that it landed. Three refusals, each named: no commit
|
|
234
|
+
to point at, a sha that is not a commit in the repo, a commit that is not on the branch.
|
|
235
|
+
- One-directional by construction: verification can only move a finding from closed to
|
|
236
|
+
OPEN. Nothing here closes one, and a `Resolved: no` is never touched. Every `Resolved: yes`
|
|
237
|
+
is checked whatever its disposition — a `defer-with-log` claim over a fix that does not
|
|
238
|
+
exist is a smaller problem and the same lie — while only a `fix-now` gates `done`.
|
|
239
|
+
- The artifact teaches the form: its preamble now says `Resolved: yes <sha>`, says a bare
|
|
240
|
+
`yes` closes nothing, and says the sha is checked. So do the router's lines in a
|
|
241
|
+
`--prepare --fixlist` bundle and the block message on a refused `done`.
|
|
242
|
+
- Red first, in `test/fixlist.test.ts`: on `14f01ec` a fix list edited to `Resolved: yes`
|
|
243
|
+
with nothing landed settled the story `status: done`, evidence `commit 3380cca`, over two
|
|
244
|
+
live `fix-now` findings — and `Resolved: yes deadbeefdeadbeef` did the same. Two existing
|
|
245
|
+
tests that encoded the old contract were updated to name a real commit on the story branch;
|
|
246
|
+
they still prove that closing every finding lets the same approve settle `done`.
|
|
247
|
+
|
|
248
|
+
- **The Build handoff cited a story branch that does not exist (#134).** Any run whose stories
|
|
249
|
+
did not all settle in ONE `tldrx next` — the ordinary shape once a stage is re-entered — got
|
|
250
|
+
a Findings row reading ``done — repo `app`, `story/S1`, merged into `epic/e1` ``, and
|
|
251
|
+
`git show story/S1` fails, because the branch the executor cut and merged is
|
|
252
|
+
`story/<run-id>/S1`. The run id in that name is a deliberate invariant (§"Build branch and
|
|
253
|
+
worktree names carry the run id", after the 2026-08-29 audit and #40); the handoff's
|
|
254
|
+
reconstruction path for a story settled by an earlier invocation had its own copy of the
|
|
255
|
+
formula and the copy predated the invariant.
|
|
256
|
+
- Fixed as **one derivation, not two**: `storyBranchOf(runId, storyId)` in
|
|
257
|
+
`src/core/plan/branchModel.ts` is now the only place the name is written, and all three
|
|
258
|
+
callers — the cut in `openStory`, the reconstruction in `fromDisk`, and the
|
|
259
|
+
`--discard-pending` evidence check on an implicit plan — go through it. A second formula
|
|
260
|
+
that matched would only postpone this; the name is a pure function of two ids that are
|
|
261
|
+
both in scope, exactly like `integrationBranchFor` and `epicWorktreeName`, so there is
|
|
262
|
+
nothing to persist and re-read.
|
|
263
|
+
- **Reporting only, and checked rather than assumed.** `StoryOutcome.branch` has three
|
|
264
|
+
consumers and all three render text: the handoff's `finding()`, the review log's
|
|
265
|
+
`- Branch:` line, and the retro's merge-conflict line. Every git operation and every
|
|
266
|
+
command a host is handed to run — `addWorktree`, `commitsBetween`, the reviewer bundle's
|
|
267
|
+
`diff`, the fix list's `diff` — reads `StoryContext.branch`, which `openStory` cut. No
|
|
268
|
+
ref was resolved from the wrong name, so nothing merged, moved or deleted wrongly; it was
|
|
269
|
+
a wrong name in an audit document.
|
|
270
|
+
- Red first, in `test/build-executor.test.ts`: on `30737a5` a second invocation's handoff
|
|
271
|
+
contained ``story/S1`` where `story/260829-build/S1` was expected, and
|
|
272
|
+
`git rev-parse --verify story/S1` exited 128 (`fatal: Needed a single revision`). The test
|
|
273
|
+
now asserts every `story/…` ref the handoff cites resolves in the repo, and a second one
|
|
274
|
+
asserts the template is written nowhere in the executor.
|
|
275
|
+
|
|
276
|
+
### Added
|
|
277
|
+
|
|
278
|
+
- **The Plan prompt now says how to make a `touches` list COMPLETE (#132).** Measured on one
|
|
279
|
+
live run: 3 of 5 stories needed their write surface extended after the fact. S2 could not
|
|
280
|
+
write the failing test its own test plan promised, because the test file was outside
|
|
281
|
+
`touches`. S4 added two enum members and left out the switch sites, so the branch did not
|
|
282
|
+
compile. S8's security criterion read a file the story never declared, so the criterion
|
|
283
|
+
would have passed on nothing. The developers caught all three — at the cost of a round each
|
|
284
|
+
time. Every other rule in `## Output schemas` is generated from a validator, because a
|
|
285
|
+
validator refuses what breaks it and says why; under-declaration breaks no rule at all.
|
|
286
|
+
`["src/thing.ts"]` is a well-formed list, `validateStory` passes it, and the bill arrives
|
|
287
|
+
one stage later at the developer prompt's "change only what `touches` names" and at
|
|
288
|
+
auto-gate condition 7 `boundary`.
|
|
289
|
+
- A `### Completing \`touches\`` sub-section in `renderPlanSchemaContract()`, ~1.3 KB in the
|
|
290
|
+
cached, most-stable part of a prompt whose stage budget is $4: three sweeps, one per real
|
|
291
|
+
failure — its tests · every switch, registration, factory, DI container or barrel a new
|
|
292
|
+
name has to reach · every file a gated criterion reads — plus what omitting one costs, so
|
|
293
|
+
the reason is in the prompt rather than in an issue. The `touches` row of the generated
|
|
294
|
+
table now points at it instead of restating it.
|
|
295
|
+
- Prose on purpose, and the only prose in that file: no compiler runs at Plan time, so
|
|
296
|
+
nothing can compute this list. **Compile simulation stays the escalation path** if the
|
|
297
|
+
checklist measures poorly on the next runs.
|
|
298
|
+
- Red-first in two places: `test/plan-schema-contract.test.ts` pins the three rules and the
|
|
299
|
+
cost statement against the rendered contract, and `test/plan-contract.test.ts` pins them
|
|
300
|
+
against the prompt the real `tldrx next --prepare` bundle writes — a contract that renders
|
|
301
|
+
correctly and is spliced into no prompt is the `templates/story.md` failure again (#48).
|
|
302
|
+
6 red before, green after. `docs/spec.md` §2.13 and the `delivery` expert each carry one
|
|
303
|
+
line pointing at the checklist.
|
|
304
|
+
|
|
305
|
+
- **The landing page now sells the unattended flow it never mentioned (#128).** Measured at
|
|
306
|
+
`95a39db`: `grep -c 'tldrx drive' docs-site/index.md` returned `0`, and so did its Spanish
|
|
307
|
+
twin — a bare `grep drive` exited `1` on both. `tldrx drive` is the star command for handing
|
|
308
|
+
a run over for the night; it has a guide in each locale and a chapter behind that, and the
|
|
309
|
+
front door had still never named it. The landing walked a reader from `run new` through
|
|
310
|
+
`next` / `approve` / `reject` to `run auto` and stopped, which is where the reader stopped
|
|
311
|
+
too — the one flow that most needs explaining was reachable only by someone who already
|
|
312
|
+
knew to go looking for it.
|
|
313
|
+
- One compact section per locale — "When nobody is watching" / "Cuando nadie está viendo" —
|
|
314
|
+
led by the outcome rather than the flags: hand a run over inside boundaries you wrote
|
|
315
|
+
down, and get the run itself back instead of a summary you have to believe. It shows the
|
|
316
|
+
three-command path (`tldrx init` → `tldrx run new … --attended-by host --gates …` →
|
|
317
|
+
`tldrx drive --unattended`) and links to the guide for the rest.
|
|
318
|
+
- The hero is untouched in both locales, and both pages are pure insertions — no existing
|
|
319
|
+
line was edited.
|
|
320
|
+
- Pinned in `test/public-surface-consistency.test.ts`: each landing must name `tldrx drive`
|
|
321
|
+
and link to its guide. Red on both locales before the change, green after. The pin is on
|
|
322
|
+
the entry point, not the prose — heading, wording and placement stay free to change.
|
|
323
|
+
|
|
324
|
+
## 0.6.0 — 2026-09-02
|
|
325
|
+
|
|
326
|
+
### Added
|
|
327
|
+
|
|
328
|
+
- **A gate record now says which entity evaluated it and under whose authority (#122).**
|
|
329
|
+
Measured 2026-09-02 on run `260902-discovery-pipeline-map`:
|
|
330
|
+
`{"type":"gate.approved","actor":"alanmartinez","payload":{"by":"alanmartinez","note":"agent-gate: evidence=sign by alanmartinez, …"}}`
|
|
331
|
+
— a gate an AGENT evaluated and signed, under authority the owner delegated once at
|
|
332
|
+
`run new --gates what:agent`. Nobody named `alanmartinez` looked at that stage. `by:` is a
|
|
333
|
+
name and never a kind, the agent signs under the operator account it is running as, and six
|
|
334
|
+
months later that record reads as "Alan personally reviewed this". The only trace of the
|
|
335
|
+
delegation was the prose inside `note:` — which nothing parses and any hand-typed
|
|
336
|
+
`--note "agent-gate: …"` can forge.
|
|
337
|
+
- Two additive blocks on `run.yml`'s gate mapping and on the `gate.approved` payload:
|
|
338
|
+
`executed_by: {type: human|agent|auto, id?}` and
|
|
339
|
+
`authority: {type: direct|delegated, policy, authorized_by, source}`. Between them they
|
|
340
|
+
answer the four questions an audit asks — who authorized the decision authority, which
|
|
341
|
+
entity evaluated THIS gate, whether it was a person / an agent / the facilitator, and
|
|
342
|
+
under which policy.
|
|
343
|
+
- **The executor's kind is read off how the gate is being closed, never off the policy.**
|
|
344
|
+
A person may always `approve` an `agent`-gated stage with no flag; that is a human acting
|
|
345
|
+
directly, whatever the stage was set up to allow, and it is recorded as one.
|
|
346
|
+
- **Derived, never guessed.** The policy is the run's own frozen `gates_policy`. The
|
|
347
|
+
authorizer is the actor of the `gate.policy_changed` that last moved that stage's policy,
|
|
348
|
+
or — when nothing moved it — the actor of `run.created`, who froze it at `run new`, and
|
|
349
|
+
`source` names which. When the log says neither, `authorized_by` is `null` and `source` is
|
|
350
|
+
`unrecorded`: the absence is said out loud rather than filled in with a plausible name,
|
|
351
|
+
and the validator refuses a record where those two do not travel together.
|
|
352
|
+
- **`by:` is untouched**, and so is `gate.evidence`. `by` is what the note said, and the
|
|
353
|
+
note is the agent's own claim about itself; rewriting it would be inventing a second one.
|
|
354
|
+
`gate.evidence` says what was checked — not who checked it under whose authority — and
|
|
355
|
+
its `role:` is a job an agent gave itself, not an identity the framework measured.
|
|
356
|
+
- **Old records read exactly as they did.** A gate written before these keys has neither,
|
|
357
|
+
validates, loads, and emits byte-for-byte; every reader falls back to `by`, and so does a
|
|
358
|
+
person who signed as themselves — for them the name is the whole truth. Pinned by test in
|
|
359
|
+
`test/gate-authority.test.ts`.
|
|
360
|
+
- `tldrx run status`, `tldrx replay` and the dashboard render a delegated signature as
|
|
361
|
+
`agent alanmartinez (delegated by alanmartinez, policy: agent)` — one shared renderer,
|
|
362
|
+
`describeGateSignature`, so the three cannot disagree. The dashboard carries its own
|
|
363
|
+
closure-free copy (`dashSignature`) because everything serialised to the browser may close
|
|
364
|
+
over nothing, and the two are asserted to agree case for case, exactly as `dashEscape` is
|
|
365
|
+
against `escapeHtml`. `DASHBOARD_MODEL_VERSION` stays at **3**: additions never bump it.
|
|
366
|
+
- A revoked gate DROPS both. `by: null` says nobody signed it, and an executor left beside
|
|
367
|
+
that would be two contradicting claims about one fact.
|
|
368
|
+
|
|
369
|
+
- **`tldrx run status` prints a stage's duration and says when a gate was signed with
|
|
370
|
+
words (#120).** `run.yml` has recorded `started_at`, `ended_at` and `gate.note` on every
|
|
371
|
+
stage since `run new` wrote the first one. #118 put all three on the dashboard's
|
|
372
|
+
`StageRowModel` and drew them; `runStatus.ts` built its own record from the same file and
|
|
373
|
+
was not touched, so the page and the CLI disagreed about what is knowable from one file —
|
|
374
|
+
a reader watching a stage that had been running for hours saw no sign of it in the
|
|
375
|
+
terminal. Each gate row (one per stage) now ends with a compact duration and a `✎` on a
|
|
376
|
+
signed note, and `--verbose` adds the two instants behind a duration, the sentence behind
|
|
377
|
+
an absent one, and the note itself.
|
|
378
|
+
- **One subtraction, not two.** `dashDuration` / `dashDurationAbsence` moved out of
|
|
379
|
+
`dashboard/render.ts` into `core/run/duration.ts`, a leaf that imports nothing and that
|
|
380
|
+
both surfaces read — rather than a second implementation of the same arithmetic. They
|
|
381
|
+
keep their `dash` names because `clientRenderer()` serialises the DEFINITION name into
|
|
382
|
+
the page, so a rename would be a `ReferenceError` on the live dashboard rather than a
|
|
383
|
+
build error; both are still closure-free and still serialised.
|
|
384
|
+
- **Still no stored duration, and still no synthesised zero.** A stage with a `started_at`
|
|
385
|
+
and no `ended_at` reads `not ended`, one with the reverse reads `no start`, and two
|
|
386
|
+
timestamps that do not yield a gap read `bad timestamps`. A stage with NEITHER end says
|
|
387
|
+
nothing on the line — it has no clock to account for and its status column already says
|
|
388
|
+
`pending`; printing "not timed" on every row of a fresh run is noise, not honesty, and
|
|
389
|
+
`--verbose` still names that case in full. `note: ""` is not a signature: it reads as
|
|
390
|
+
`null`, is never marked and is never quoted.
|
|
391
|
+
- **`--json` is additive.** `started_at`, `ended_at` and `note` are appended to each gate
|
|
392
|
+
row; every top-level key keeps its position, so a consumer reading `run`, `waiting.kind`
|
|
393
|
+
or `gates[i].by` is untouched. A new test pins the row's key order the way
|
|
394
|
+
`SINGLE_RUN_KEYS` pins the top level's.
|
|
395
|
+
|
|
396
|
+
- **The docs site's demo dashboard now shows the story grid and the Waves view with
|
|
397
|
+
something in them (#119).** `gen-demo.ts` composed its workspace from two fixtures,
|
|
398
|
+
neither of which had ever reached the Plan phase, so `loadPlan` returned null for all
|
|
399
|
+
eight runs and every plan-shaped rendering on the public page drew its EMPTY state: the
|
|
400
|
+
Waves view said "No waves in this workspace", the story grid drew nothing at all, and
|
|
401
|
+
Plan & build said the Plan phase had written no stories. A demo of a dashboard that
|
|
402
|
+
cannot show two of its own views is a demo of the wrong thing.
|
|
403
|
+
- A **third** synthetic fixture — `test/fixtures/plan/workspace`, one run at Build with
|
|
404
|
+
six stories over two waves, two epics on two repos, a fix round on S2 and two review
|
|
405
|
+
retries on S5 — added to `DEMO_SOURCES` beside the other two. Third rather than an
|
|
406
|
+
edit, because a `03-plan/` dropped into a chain run would move that run's
|
|
407
|
+
`stagesTotal`, `stagesDone` and `percent` (numbers `dashboard-deps.test.ts` reads),
|
|
408
|
+
and `dashboard.test.ts` asserts — correctly — that the views fixture's one run carries
|
|
409
|
+
`plan: null`. Both existing fixtures stay byte-identical.
|
|
410
|
+
- Synthetic like everything else on that page: it lives under `test/fixtures/`, so
|
|
411
|
+
`assertSynthetic` is what permits it to be read at all, and the demo stays
|
|
412
|
+
deterministic (fixed clock, invented root, no machine path).
|
|
413
|
+
- Measured over the composed workspace: 9 runs, 2 experts, 0 unreadable files, 0 skipped
|
|
414
|
+
events, 6 stories across 2 waves, and `run.build` non-null for the first time.
|
|
415
|
+
|
|
416
|
+
- **A stage now carries when it started, when it ended and what its gate said (#118).**
|
|
417
|
+
`run.yml` has recorded `started_at`, `ended_at` and `gate.note` on every stage since
|
|
418
|
+
`run new` wrote the first one, and `StageRowModel` carried none of the three — so the
|
|
419
|
+
phase timeline (#107) printed neither a duration nor a signature and had to carry a
|
|
420
|
+
paragraph explaining why. Three additive fields, read off the same `run.yml` object the
|
|
421
|
+
row was already built from. `DASHBOARD_MODEL_VERSION` stays at **3**: additions never
|
|
422
|
+
bump it, and no existing field reads differently than it did at v3.
|
|
423
|
+
- **No duration is stored.** A duration is a subtraction, it exists only when both ends
|
|
424
|
+
do, and a model field would have to pick a number for the case where one end is
|
|
425
|
+
missing — `0` is a measurement of zero, and inventing one is exactly the class of
|
|
426
|
+
confident-wrong figure this redesign exists to stop. `dashDuration` does the
|
|
427
|
+
subtraction where it is drawn, and a stage that recorded neither end gets a sentence
|
|
428
|
+
naming *which* end is missing rather than a blank cell reading as "it took no time".
|
|
429
|
+
- **An empty note is an absent one.** `note: ""` is what `run new` writes on a gate
|
|
430
|
+
nobody has signed. It reaches the model as `null` and is not quoted at a reader as if
|
|
431
|
+
it were a signature.
|
|
432
|
+
- The timeline draws the duration beside the cost on each stage's summary row and quotes
|
|
433
|
+
the gate's own words inside its drawer — the two of #107's four asks that could not be
|
|
434
|
+
met before.
|
|
435
|
+
- **`tldrx dashboard --serve` pushes only when the page would actually change, and ages
|
|
436
|
+
itself when it would not (#108).** The live server already watched the tree and pushed a
|
|
437
|
+
`reload` per debounced burst; three things were missing, and each one is a different way
|
|
438
|
+
for a live page to lie.
|
|
439
|
+
- **A push per burst is not a push per change.** Every write under `.tldrx/` or
|
|
440
|
+
`tldrx-work/` pushed, including writes to files the model does not read — `--static`
|
|
441
|
+
writes into `.tldrx/cache/`, inside the watched tree, so exporting a page while serving
|
|
442
|
+
one made the served page redraw because it had been photographed. The trigger is still
|
|
443
|
+
the file event; the DECISION is now a rebuild of the model compared against the one the
|
|
444
|
+
page is showing, on a fixed clock so ages do not make every rebuild differ. A digest of
|
|
445
|
+
"the files that matter" was rejected on purpose: that list drifts the moment the model
|
|
446
|
+
reads one more thing, and its failure mode — a dashboard that quietly stops updating —
|
|
447
|
+
is exactly what the comparison cannot do.
|
|
448
|
+
- **Silence is a state the files cannot announce.** Ages, and the `quiet` mark #107 puts
|
|
449
|
+
on half an hour of nothing, are computed against a `now`. With only file-triggered
|
|
450
|
+
pushes a stalled run keeps saying "2m ago" until somebody writes a file: the one state
|
|
451
|
+
worth seeing is the one state the page could not reach. A new `age` event carries a
|
|
452
|
+
timestamp every 25 s, whatever the disk is doing, and doubles as the stream's
|
|
453
|
+
keep-alive — a tick that says something, at the cost of the comment that said nothing.
|
|
454
|
+
- **The ledger is read forward.** `events.jsonl` is append-only, so
|
|
455
|
+
`src/core/dashboard/tail.ts` keeps a byte offset per run and reads only what arrived —
|
|
456
|
+
holding back a line whose newline has not landed, starting over if a ledger shrinks
|
|
457
|
+
under it, and dropping a run's offset when the run goes. That is what lets a `reload`
|
|
458
|
+
carry `{at, appended, runs, added, removed}` and say *three events landed on this run*
|
|
459
|
+
rather than only *something changed*.
|
|
460
|
+
- **`--serve` is a flag now**, and `--serve --static` is a refusal (exit 2) rather than a
|
|
461
|
+
silent pick of one. Serving is still the default.
|
|
462
|
+
- **`listRuns` no longer throws while the disk is being written to.** An entry removed
|
|
463
|
+
between the `readdir` and its `stat`, and a `tldrx-work` that is not a directory, both
|
|
464
|
+
escaped `buildModel` — measured, both reproduced. A live reader asks that question
|
|
465
|
+
while a wave is writing.
|
|
466
|
+
- The live page keeps the reader's FOCUS across a repaint (scroll and open panels were
|
|
467
|
+
already kept), because only a live page redraws when nobody asked, and #107's j/k
|
|
468
|
+
navigation loses its place otherwise. That code is in `liveScript()` alone: the static
|
|
469
|
+
export is byte-identical to what it was before this landed, and a test pins the hash.
|
|
470
|
+
|
|
471
|
+
- **An answer that overtakes an earlier phase's document now says so on that document
|
|
472
|
+
(#104).** A phase document is a point-in-time snapshot, and an owner answer recorded
|
|
473
|
+
three phases later can flip a design it still asserts. Measured twice on
|
|
474
|
+
`260830-ordering-inventory`: `03-plan/stories/S4.md` promised a `[Theory]` proving an
|
|
475
|
+
inert stock-effect default after F021 established Restock for 5 of 64 pairs and the
|
|
476
|
+
shipped test was a different one; `02-how/design.md` and its handoff said "no
|
|
477
|
+
order_number column is created" after F022 ordered one. Both flips were recorded — in
|
|
478
|
+
`questions.md`, in `facts.yml`, in `retro.md` — and neither was in the file a reader
|
|
479
|
+
opens. The audit's conclusion, "read `04-build/` rather than `03-plan/` for what
|
|
480
|
+
actually shipped", was tribal knowledge nothing on the page taught.
|
|
481
|
+
- **The question already names the document.** §2.7 requires `Why asked:` to end with a
|
|
482
|
+
`[src: …]` token proving the gap is real, and a question raised in Build about a plan
|
|
483
|
+
claim cites that plan claim — that is what the citation is for. So the affected set is
|
|
484
|
+
derived from data the grammar already makes mandatory: every `file` ref pointing at a
|
|
485
|
+
`.md` in an EARLIER phase of the run. No new schema, no new habit. A citation into the
|
|
486
|
+
question's own phase is not a supersession — that is an author reading their own
|
|
487
|
+
half-written page — and an optional `affects:` metadata key names documents explicitly
|
|
488
|
+
for when the honest citation is a source file rather than a document.
|
|
489
|
+
- **A marker, not a reconciliation.** One HTML comment carrying the fact id and one
|
|
490
|
+
blockquote line pointing at `questions.md` and `facts.yml`. Rewriting the stale
|
|
491
|
+
sentence would mean knowing which sentence, and guessing that is how a framework
|
|
492
|
+
starts inventing content. The issue asked for an honest marker; full reconciliation is
|
|
493
|
+
explicitly not required.
|
|
494
|
+
- **Three properties it had to have, each pinned by a test.** Append-only
|
|
495
|
+
(`appendFileSync`; the RED test asserts the phase's own bytes are still the file's
|
|
496
|
+
prefix). Idempotent per fact — the comment carries the id, so the `answer-capture`
|
|
497
|
+
hook firing twice on one edit stamps once, while a *different* fact stacks under the
|
|
498
|
+
first. And invisible to the checks that guard the document: the stamp is deliberately
|
|
499
|
+
not a list item and carries no `[src: …]`, because a bullet appended to a `handoff.md`
|
|
500
|
+
would land inside a §2.8 section where every item must be sourced, and `claim-sources`
|
|
501
|
+
would refuse the very document this makes honest. A stamped handoff validates with the
|
|
502
|
+
same `bulletCount` and the same verdict as before.
|
|
503
|
+
- Both recording paths stamp: a first answer and `tldrx answer … --supersede`. Each
|
|
504
|
+
stamped document appends a `doc.superseded` event (new, closed-set §2.9 type) carrying
|
|
505
|
+
the run-relative path, the fact, the question and whether the document was `cited` or
|
|
506
|
+
named in `affects`; `tldrx replay` renders it. A stamp that cannot be written never
|
|
507
|
+
fails the answer — the decision is recorded and the failure goes to the log as `error`.
|
|
508
|
+
- **The dashboard opens on a "Now" strip, and it says what it does not know (#107).** The
|
|
509
|
+
render half of the owner-approved redesign, on top of the model #103 shipped. The runs
|
|
510
|
+
view now leads with one card per **live** run — every run that is neither `done` nor
|
|
511
|
+
`cancelled`, so a run nobody has started and a run nothing can move are both on it, with
|
|
512
|
+
the ones that raise an ask sorted to the front. Each card carries the phase dots, the
|
|
513
|
+
status, the ask in the page's own words with the command that closes it, the spend, and
|
|
514
|
+
how long it has been quiet. It **replaces** the alert stack rather than sitting beside it:
|
|
515
|
+
it is a superset — the same `dashPending` ask, the same `alert__kind` badge, the same
|
|
516
|
+
sentence — over more runs.
|
|
517
|
+
- **A lower bound never gets a bar.** A progress bar is a claim about a denominator, and
|
|
518
|
+
it is honest only when the number over it is the whole of what was spent. On the run
|
|
519
|
+
#103 audited, 30 of 34 turns put nothing in the meter and not one declared a token, so
|
|
520
|
+
`$14.60 of $62.00` under a quarter-full bar was a confident wrong number drawn in the
|
|
521
|
+
one shape a reader cannot argue with. The bar is now reserved for `spend.basis ===
|
|
522
|
+
"measured"`; every other basis gets the metered figure, a **lower bound** mark carrying
|
|
523
|
+
`spend.reason` in its tooltip, and the count of costless turns out of the total — so the
|
|
524
|
+
size of the gap is a number and not an adjective. Host tokens are printed beside the
|
|
525
|
+
dollars and never added to them.
|
|
526
|
+
- **"Quiet" is 30 minutes, and it is the RENDER's threshold.** The model reports
|
|
527
|
+
`ageSeconds` unclamped and bakes in none, which is right — a workspace where a stage
|
|
528
|
+
takes forty minutes and one where it takes forty seconds cannot share a constant. So the
|
|
529
|
+
page picks, in one comparison, in one place, and says so on the reference page. An
|
|
530
|
+
`mtime` reading prints **touched** rather than "last event" (the file was written, which
|
|
531
|
+
is not the run moving) and a ledger dated after the read is named as two clocks
|
|
532
|
+
disagreeing rather than laundered into "0m ago".
|
|
533
|
+
- **The dots are the phases the run's own file declares.** `run new` writes the whole
|
|
534
|
+
workflow up front (`buildPhases`), so a `feature` run really does draw five, what →
|
|
535
|
+
watch, from the moment it exists — and a `docs` run draws what a `docs` run has. Nothing
|
|
536
|
+
is padded to five for the shape of it.
|
|
537
|
+
- **Three drill-ins on the run detail, and each one names what it cannot show (#107).** A
|
|
538
|
+
**phase timeline** — a lane per phase with its cost, each stage a `<details>` opening onto
|
|
539
|
+
the gate, its policy, its signer and an agent signature's evidence, with the execution-path
|
|
540
|
+
table moved inside a closed panel rather than drawn twice. A **story grid** — one status
|
|
541
|
+
cell per story, forty of them legible without scrolling, each opening onto the plan file's
|
|
542
|
+
fields, the attempt the ledger last recorded, the free review retries and every reopen. An
|
|
543
|
+
**event stream** — the three timestamped kinds the model carries (operator notes,
|
|
544
|
+
`budget.blocked`, `story.reopened`) in one time order, filterable by kind with the same
|
|
545
|
+
button vocabulary the runs list uses. Reading a note next to the refusal two minutes before
|
|
546
|
+
it is how a person works out why somebody rebased a branch by hand.
|
|
547
|
+
- The absences are printed where the numbers would be. A stage's `started_at`, its
|
|
548
|
+
`ended_at` and a gate's free-text `note` are in `run.yml` and on no field of the
|
|
549
|
+
dashboard model, so the timeline reports no duration and quotes no signature. A story's
|
|
550
|
+
build log and its fix list are files the Build writes and the page does not read. The
|
|
551
|
+
stream says out loud that it is not the ledger — `tldrx replay <run>` is. A blank cell
|
|
552
|
+
reads as "nothing happened", and that is the failure mode this whole redesign is about.
|
|
553
|
+
- **A Waves view (#107).** The plan as bars: a row per wave, a bar per story in it, fix
|
|
554
|
+
rounds and free review retries marked on the bar. Gantt-lite and deliberately not a Gantt —
|
|
555
|
+
the axis is the WAVE, because that is the only ordering the files actually assert;
|
|
556
|
+
`StoryModel` carries no start and no end, and an invented x-axis would read as measured. A
|
|
557
|
+
story the plan schedules into no wave gets its own row rather than being dropped.
|
|
558
|
+
- **Craft: keyboard reach, tabular money, still one file.** <kbd>j</kbd>/<kbd>k</kbd> move
|
|
559
|
+
between cards and rows, <kbd>enter</kbd> opens the focused one, <kbd>/</kbd> jumps to the
|
|
560
|
+
filters — printed under the filter row, because an undiscoverable shortcut is not a
|
|
561
|
+
feature, and bound only to keys no browser or screen reader already owns. Money and token
|
|
562
|
+
counts are tabular, so a column of cards reads as a column of numbers. The two marks that
|
|
563
|
+
say "this figure is not what it looks like" — `lower bound` and `quiet` — are words, not
|
|
564
|
+
colours, so they survive a greyscale print and a reader who cannot tell amber from citron.
|
|
565
|
+
Both themes still come from `prefers-color-scheme`, the export is still one self-contained
|
|
566
|
+
file with no network reference of any kind, and there is still no framework: vanilla, CSS
|
|
567
|
+
grid, and `<details>`.
|
|
568
|
+
- `DASHBOARD_MODEL_VERSION` does not move. Nothing here reads a file, adds a field or
|
|
569
|
+
changes what one means — it is the drawing half, and the model it draws is #103's
|
|
570
|
+
unchanged.
|
|
571
|
+
|
|
572
|
+
- **The docs site now SHOWS the dashboard instead of describing it (#106).** A new
|
|
573
|
+
[Live demo](https://ederwii.github.io/tldr-experts/demo) page in both languages frames a
|
|
574
|
+
real `tldrx dashboard --static` export — eight runs, two experts, gates, questions,
|
|
575
|
+
dollars — rendered at build time by `docs-site/scripts/gen-demo.ts` and served from
|
|
576
|
+
`/dashboard-demo/index.html`. It is the shipped `buildModel` and the shipped
|
|
577
|
+
`renderDashboard`, not a screenshot: change the renderer and the page on the website
|
|
578
|
+
changes on the next deploy, which the workflow's `paths` filter now guarantees by
|
|
579
|
+
redeploying on any change under `src/core/dashboard/` or the two fixtures behind it.
|
|
580
|
+
- **The data is synthetic, and that is enforced rather than promised.** The generator
|
|
581
|
+
composes its workspace from `test/fixtures/views/` (one detailed run — handoff, open
|
|
582
|
+
questions, budget ledger, events, two trained experts) and `test/fixtures/chain/` (seven
|
|
583
|
+
more, carrying every status a reader should learn to recognise and the dependency edges
|
|
584
|
+
between them). `assertSynthetic()` resolves every source path and refuses anything
|
|
585
|
+
outside `test/fixtures/` — including the framework's own checkout, which IS a tldrx
|
|
586
|
+
workspace and is where a careless default would land. The page is public and permanent;
|
|
587
|
+
a real run read here would publish a client's domain, and it would look like it worked.
|
|
588
|
+
- **Two things the first build got wrong, both caught by the tests that were written
|
|
589
|
+
first.** `model.root` is DRAWN on the page, so the export carried the build machine's
|
|
590
|
+
temp directory (`/var/folders/…/tldrx-demo-AUfHya/`) into a public document and changed
|
|
591
|
+
on every build; the demo now renders with the root it is about, and a test asserts no
|
|
592
|
+
path from the building machine appears anywhere on the page. And `public/demo/` collided
|
|
593
|
+
with `docs-site/demo.md` — `cleanUrls` builds that as `demo.html`, so a site holding both
|
|
594
|
+
asks GitHub Pages to guess what `/demo` means. The export moved to `/dashboard-demo/`.
|
|
595
|
+
- **One banner, and it is additive by construction.** A reader arriving from a search
|
|
596
|
+
result has none of the page's framing, so the export carries one line saying the numbers
|
|
597
|
+
are invented. Strip it and the bytes are identical to what the CLI writes — asserted, so
|
|
598
|
+
the demo cannot drift into being a mock-up of the command instead of a run of it. It
|
|
599
|
+
links nowhere: the export fetches nothing, and that is the property that lets it sit on a
|
|
600
|
+
static site at all.
|
|
601
|
+
- The docs workflow now installs the ROOT dependencies too. Module resolution for the
|
|
602
|
+
dashboard's `yaml` import walks up from `src/`, where `docs-site/node_modules` is not on
|
|
603
|
+
the path — without that step bun quietly auto-installs the package at build time, which
|
|
604
|
+
is an undeclared network fetch. Measured 2026-09-02: `import "yaml"` from `src/` fails
|
|
605
|
+
outright once the root `node_modules` is gone.
|
|
606
|
+
|
|
607
|
+
- **The run headline shows BOTH economies, or says why it cannot (#103).** A cold
|
|
608
|
+
adversarial audit of a real host-attended run (`260830-ordering-inventory`,
|
|
609
|
+
aparece-v2, 2026-09-02) found every ledger surface reconciling to a perfect 0.00 delta
|
|
610
|
+
at **$14.60** — `run.yml` `spent_usd`, the `events.jsonl` sum, the stage sums, the task
|
|
611
|
+
sums and the `budget.yml` phase sums — over 34 turns of which **4** carried money. The
|
|
612
|
+
run's own watch gate note puts the real figure at "about 81 dollars". The framework was
|
|
613
|
+
not mis-metering; the front page was lying by omission, and the auditor's verdict line
|
|
614
|
+
was "they should re-derive the cost". This is that re-derivation, as a REPORTING change
|
|
615
|
+
and not a metering one.
|
|
616
|
+
- `Run.spend` carries the metered dollars, the turns the meter could not see, and what
|
|
617
|
+
was declared about them. **Both spellings of "this turn cost nothing" are counted**:
|
|
618
|
+
the `cost_usd: null` + `metered: false` one the model already knew (14 of that run's
|
|
619
|
+
turns), and the flat `cost_usd: 0.00` written by an executor turn a host session drove
|
|
620
|
+
(16 more), which reads as a measurement of zero and is not one. `unmeteredTasks` keeps
|
|
621
|
+
its exact old meaning and `zeroCostTasks` is a second count beside it — a file that
|
|
622
|
+
says `0.00` is not re-labelled, it is counted.
|
|
623
|
+
- **What is not in the files is named `absent`, never guessed.** `spend.basis` is one of
|
|
624
|
+
`measured` / `declared` / `partial` / `absent`, and `spend.reason` says which in a
|
|
625
|
+
sentence carrying the CLI's own words ("the metered total is a LOWER BOUND, not a
|
|
626
|
+
total"), so the page and `tldrx budget show` cannot word the same fact two ways. No
|
|
627
|
+
price table is consulted, no token is converted to a dollar, and no estimate is
|
|
628
|
+
synthesised from stage prices or turn counts. The audited run is the `absent` case:
|
|
629
|
+
all 920,641 of its declared tokens sit on turns that ALSO carried dollars, so they
|
|
630
|
+
describe none of the 30 turns that carried none.
|
|
631
|
+
- The run detail's `spent` row now names the whole gap. It used to read "+ 14 unmetered
|
|
632
|
+
turns (in-session)" beside $14.60 — 14 of the 30 turns that put nothing in the meter.
|
|
633
|
+
- **A run says when it last moved (`lastEventAt`, `lastEventFrom`, `ageSeconds`).** The
|
|
634
|
+
`ts` of the last line of `events.jsonl`, falling back to the file's mtime when nothing
|
|
635
|
+
in it parses — and `lastEventFrom` NAMES which of the two, because an mtime is the
|
|
636
|
+
weaker fact: the file was touched, which is not the same as the run moving. `ageSeconds`
|
|
637
|
+
is a measurement with no threshold in it; nothing in the model decides what "stale"
|
|
638
|
+
means. It is not clamped either, so a ledger written after `now` reports a negative age
|
|
639
|
+
rather than a comfortable zero. The mtime is carried on `LoadedRun` by the reader that
|
|
640
|
+
already opens the path, so the ledger is still read exactly once per run.
|
|
641
|
+
- **`Run.nextAction` — who is waited on right now, where, and what closes it.** `waiting`
|
|
642
|
+
answers that as prose, and a card that wanted the command in a button had to regex the
|
|
643
|
+
sentence. This is the same answer pre-split, and **nothing in it is a second
|
|
644
|
+
derivation**: `kind` and `message` are `waiting`'s verbatim, `command` and
|
|
645
|
+
`alternatives` are the backticked spans read OUT OF that message, and `waitingOn`
|
|
646
|
+
applies `isMovable` — the framework's own definition of "a human could move it right
|
|
647
|
+
now" — in the same precedence `dashPending` already uses. `unknown` is the honest fifth
|
|
648
|
+
value: a `blocked` run with no sibling named has a `run.yml` recording no cursor, and
|
|
649
|
+
the model will not guess who fixes that.
|
|
650
|
+
|
|
651
|
+
`DASHBOARD_MODEL_VERSION` stays at **3**. Five additions, nothing removed, and no existing
|
|
652
|
+
field reads differently: `spentUsd` is still the same `run.yml` key holding the same
|
|
653
|
+
number, and has meant "METERED dollars, a lower bound when `unmeteredTasks > 0`" since v3.
|
|
654
|
+
A consumer that read it as a total was wrong before this wave and is wrong by exactly the
|
|
655
|
+
same amount after it; what changed is that the page now says how big the bound is.
|
|
656
|
+
|
|
657
|
+
- **`scripts/merge-wave.sh` now gates the documentation site too (#114).** The wave ran
|
|
658
|
+
typecheck, `bun test`, `build` and the runtime-seam grep, pushed, and left the site to
|
|
659
|
+
`.github/workflows/docs.yml` — *after* the merge. That is a build with two failure modes
|
|
660
|
+
the test suite cannot see: `ignoreDeadLinks: false` in `docs-site/.vitepress/config.mts`
|
|
661
|
+
is deliberate, so a page somebody moved breaks it, and `docs-site/package.json` runs
|
|
662
|
+
`gen-changelog.ts` and `gen-demo.ts` before VitePress, either of which can throw on input
|
|
663
|
+
no test feeds it. Both used to land on `main` GREEN and go red as a failed **deploy** —
|
|
664
|
+
`main` broken and the published site stale, the worst of both. `bun run docs:build` is now
|
|
665
|
+
the fifth gate, on the same tree as the other four, and a red one is `docs=1` in the FAIL
|
|
666
|
+
line and nothing pushed.
|
|
667
|
+
- **It blocks, and the cost is why that was affordable.** Measured 2026-09-02 on this
|
|
668
|
+
repo, warm: 4 s wall — VitePress 2.91 s plus a `bun install --frozen-lockfile` in
|
|
669
|
+
`docs-site/` that installs 126 cached packages in 165 ms — against a wave whose
|
|
670
|
+
`bun test` alone is ~440 s. `git status --porcelain` is empty before AND after it, so
|
|
671
|
+
the new gate cannot leave dirt that fails the NEXT wave's dirty-tree guard: everything
|
|
672
|
+
it writes (`.vitepress/dist/`, `.vitepress/cache/`, `reference/changelog.md`,
|
|
673
|
+
`public/`) was already ignored.
|
|
674
|
+
|
|
675
|
+
- **A surprised merge-wave assertion now prints the logs the script kept (#115, instrumentation
|
|
676
|
+
only — the flake is NOT fixed).** CI run 33653699970 (`b64950d`) caught one failure in 3161:
|
|
677
|
+
run A of the #44 concurrency test exiting 2 — *merge conflict* — in a sandbox whose only
|
|
678
|
+
merge adds a file nothing else touches. It could not be diagnosed, because merge-wave keeps
|
|
679
|
+
a red run's logs on purpose and `afterEach` deletes the sandbox they live in first. The
|
|
680
|
+
concurrency tests now assert through `expectExit`, which on any unexpected code raises the
|
|
681
|
+
exit, stdout, stderr **and every file in the `mw-<pid>` directory the script kept**,
|
|
682
|
+
`merge.log` first — the artifact #115 asked for. The next occurrence arrives explained.
|
|
683
|
+
- **What the search ruled out, so nobody repeats it.** Measured 2026-09-02 on macOS,
|
|
684
|
+
git 2.50.1: a truncated or empty `reference-transaction` hook does NOT fail a ref update
|
|
685
|
+
(both exit 0), and 800 real commits against 4000 concurrent `merge-guard.sh --install`
|
|
686
|
+
rewrites produced 0 failures — so the non-atomic hook install is not the cause. No test
|
|
687
|
+
in the suite sets `MW_LOCK_*`, so cross-file env contamination is not either. The one
|
|
688
|
+
mechanism that reproduces exit 2 exactly is the guard REFUSING (`fatal: ref updates
|
|
689
|
+
aborted by hook` → merge-wave's conflict branch), which needs the lock to carry a live
|
|
690
|
+
owner and a token that is not the merging run's. 40 runs of `test/merge-wave.test.ts`
|
|
691
|
+
four-way concurrent under 20 CPU spinners did not produce it.
|
|
692
|
+
|
|
693
|
+
- **A drift guard over the public surfaces (#121).** `test/public-surface-consistency.test.ts`
|
|
694
|
+
fails when the README, the docs site (both locales), `env.yml` and `package.json` stop
|
|
695
|
+
agreeing with each other. It reads files off disk, spawns nothing and touches no network,
|
|
696
|
+
and every failure names the file and line to go fix. It checks that no page states the
|
|
697
|
+
current version as a literal (it must be interpolated), that every release-shaped literal
|
|
698
|
+
left in the prose is a version we deliberately cite as history, that the README release
|
|
699
|
+
table leads with `package.json`'s version, that the docs config derives the version rather
|
|
700
|
+
than hardcoding it, that `env.yml` does not require Bun, that no quickstart tells a reader
|
|
701
|
+
they need Bun to run tldrx, and that no surface carries a claim we have retired.
|
|
702
|
+
- Red-first, against the tree as it stood: 6 of 10 assertions failed, one per real defect.
|
|
703
|
+
- `scripts/release-check.sh` already compared `package.json`, `plugin.json`, `CHANGELOG.md`
|
|
704
|
+
and the README table — but only at release time, when the drift has already shipped. This
|
|
705
|
+
runs on every PR through `ci.yml`.
|
|
706
|
+
|
|
707
|
+
### Changed
|
|
708
|
+
|
|
709
|
+
- The staleness field is spelled `lastEventFrom`, not `lastEventSource`. The model is
|
|
710
|
+
embedded in the static page verbatim, and one of this repo's oldest guards is that the
|
|
711
|
+
exported page contains no `EventSource` — a static export must make no network call. A
|
|
712
|
+
field named `lastEventSource` puts that substring in every page and makes a JSON key
|
|
713
|
+
indistinguishable from the live-reload script the guard exists to catch. The guard is
|
|
714
|
+
right; the name was wrong.
|
|
715
|
+
|
|
716
|
+
### Fixed
|
|
717
|
+
|
|
718
|
+
- **`validateEnv` now enforces the two `env.yml` rules §2.10 designed, and §2.10 stops
|
|
719
|
+
designing the third (#126).** The spec stated four validation rules as fact; the schema
|
|
720
|
+
enforced none. #125 removed one with its field and reworded the rest into "designed and not
|
|
721
|
+
yet enforced" — honest, and it settled nothing.
|
|
722
|
+
- **Ids are unique across `tools`**, and the message says what a duplicate costs rather
|
|
723
|
+
than that it is untidy: `runDoctor` iterates `tools` and probes each entry, so a repeated
|
|
724
|
+
id ran the same `check` twice and printed two rows for one tool. Reported at
|
|
725
|
+
`tools[i].id`, naming where the id was first declared. A non-string id gets its type
|
|
726
|
+
error and no uniqueness complaint on top of it.
|
|
727
|
+
- **At most 64 tools**, `MAX_ENV_TOOLS`, interpolated into the message rather than typed
|
|
728
|
+
into it (gh #38). Nothing bounded the file before; `env.yml` declares 7.
|
|
729
|
+
- **The metacharacter rule is DELETED from the spec, not implemented.** "`check` free of
|
|
730
|
+
`; && | > \`" was not a missing check but a disagreement about what `check` is:
|
|
731
|
+
`ToolChecker.check` runs `runtime.spawn("sh", ["-c", tool.check])`, so every `check` in
|
|
732
|
+
every manifest that ever shipped has been executed BY a shell, and §2.10's own
|
|
733
|
+
`[assumption]` depends on it — `check: "test -n \"$VAR\""` is nothing without a shell to
|
|
734
|
+
expand `$VAR`. Enforcing it would have been a behaviour change that broke the manifest's
|
|
735
|
+
idiom in order to defend an owner against a file they wrote, committed and reviewed like
|
|
736
|
+
code, running on their own machine as themselves. §2.10 now says what `check` is instead,
|
|
737
|
+
and `test/env-validation.test.ts` pins that a metacharacter-bearing `check` validates AND
|
|
738
|
+
that the runtime really shells out — measured through `ToolChecker`, not read off the
|
|
739
|
+
source. The `result:` / `checked_at` prose is untouched: still designed, still not built,
|
|
740
|
+
still says so.
|
|
741
|
+
|
|
742
|
+
- **The status line's gate counter is `machine:N`, and counts every gate a machine closed
|
|
743
|
+
(#127).** `runSnapshot` computed it with `gate.by === "auto"` — the selector #124 had just
|
|
744
|
+
removed from `tldrx status` — so an agent-signed gate was invisible on the one line an
|
|
745
|
+
operator actually watches. Measured on the #122 fixture after `runNext` closed an `agent`
|
|
746
|
+
gate: `gate.by=alanmartinez executed_by={"type":"agent","id":"alanmartinez"}` and
|
|
747
|
+
`autoGates=0`. A run whose only closed gate was signed by an agent showed no segment at all.
|
|
748
|
+
- **The label moved with the selector.** Widening the count and keeping `auto:` would have
|
|
749
|
+
replaced one untrue number with an untrue name: `auto` is a specific actor in this system
|
|
750
|
+
(`AUTO_GATE_ACTOR`), not a synonym for "the machine", and reusing it for the superset is
|
|
751
|
+
the exact ambiguity that hid the agent case the first time. `machine` is the word
|
|
752
|
+
`tldrx status` already uses one screen away — "N gate(s) closed by a machine, not by a
|
|
753
|
+
person" — so the glance and the report are now in one vocabulary.
|
|
754
|
+
- **One selector, in one place.** `closedByMachine` moved out of `status/runItems.ts` into
|
|
755
|
+
`core/run/gateAuthority.ts`, beside `describeGateSignature`, and both surfaces read it.
|
|
756
|
+
The report and the line answer the same question about the same field, and had already
|
|
757
|
+
drifted once: #124 fixed one copy, and #127 was the other copy, still saying `auto`.
|
|
758
|
+
- **The tolerant reader still claims nothing it cannot see.** It does not parse gates, its
|
|
759
|
+
`0` means "cannot see", and it renders as no segment — never as "no machine signed
|
|
760
|
+
anything". Pinned.
|
|
761
|
+
- Three rendered assertions changed, each because the rendering did:
|
|
762
|
+
`test/attended.test.ts` (`0/2 att auto:2 stale:1` → `machine:2`) and two in
|
|
763
|
+
`test/revoke.test.ts`. `test/statusline.test.ts` never asserted the segment.
|
|
764
|
+
|
|
765
|
+
- **`docs/spec.md` §2.10 documented an `env.yml` field that does not exist, and two values
|
|
766
|
+
that were stale (#125).** The example carried `version_re: "([0-9]+\\.[0-9]+\\.[0-9]+)"` on
|
|
767
|
+
both of its tools. There is no such field: `src/core/schemas/env.ts` requires
|
|
768
|
+
`["id", "required", "check", "install"]` and knows `min_version`, and extraction is
|
|
769
|
+
`extractVersion` in `src/core/doctor/version.ts` — "the first dotted numeric run in
|
|
770
|
+
stdout+stderr", one extractor for every tool, exactly as `env.yml`'s own header comment
|
|
771
|
+
has always said. It was designed and then dropped, and the spec kept teaching it. Nothing
|
|
772
|
+
reads `docs/spec.md` at runtime, so the cost was paid by a contributor who copied the
|
|
773
|
+
example; the two stale values were `min_version: "1.1.0"` for Bun (the manifest has said
|
|
774
|
+
`1.3.0` since native `Bun.YAML`) and `required: true` for Bun, which `ab90a71` (#121) had
|
|
775
|
+
just disproved — the published package runs on Node alone.
|
|
776
|
+
- Two more defects the sweep turned up in the same twenty lines, both measured: the
|
|
777
|
+
example's top-level `checked_at:` and its per-tool `result: {found, version, ok, checked_at}`
|
|
778
|
+
are written by nothing. `runDoctor` builds a `DoctorReport` and returns it, and
|
|
779
|
+
`ENV_MANIFEST_PATH` has exactly two readers and no writer. That design is kept and now
|
|
780
|
+
says so in prose ("designed, not built") instead of being shown as a file `doctor`
|
|
781
|
+
produces. `Required in v0: git, bun, claude` became the measured set — `node`, `git`,
|
|
782
|
+
`claude` required; `bun`, `python3`, `graphify`, `gh` optional.
|
|
783
|
+
- **The example is now asserted rather than proofread**, which is the part that stops it
|
|
784
|
+
happening a fourth time on the same line. Three assertions in
|
|
785
|
+
`test/public-surface-consistency.test.ts` (#121's drift guard, extended): it must satisfy
|
|
786
|
+
the same `validateEnv` `loadEnvManifest` runs on the real file; it may use no key the real
|
|
787
|
+
`env.yml` does not use; and it must carry the real manifest's own `required`, `check` and
|
|
788
|
+
`min_version` for every tool it names. `purpose` is deliberately not compared — an
|
|
789
|
+
illustration is allowed to abbreviate it, and to be an excerpt of two tools out of seven.
|
|
790
|
+
RED first, against the unedited spec: the field rule named all five invented keys and the
|
|
791
|
+
value rule named both stale values.
|
|
792
|
+
- The §2.10 Validation paragraph claimed four rules `validateEnv` does not implement. Three
|
|
793
|
+
are now marked as designed and not yet enforced rather than stated as fact; the fourth
|
|
794
|
+
(`check` free of shell metacharacters) is filed separately, because `ToolChecker` runs
|
|
795
|
+
`sh -c <check>` and honouring the rule would be a change of behaviour, not a check to add.
|
|
796
|
+
|
|
797
|
+
- **A revoked gate no longer records what its withdrawn signature rested on (#123).**
|
|
798
|
+
`revoke` reset an approved gate to `pending` and nulled `by` and `at`, but spread
|
|
799
|
+
`gate.evidence` straight through, so the same mapping said *nobody has signed this gate*
|
|
800
|
+
(`status: pending`, `by: null`) and *here is what the signature rested on*
|
|
801
|
+
(`evidence: {path: …, verdict: sign, sampled: 7, of: 34}`). Both cannot be true, and
|
|
802
|
+
`tldrx replay` — which renders the evidence block off `run.yml`, not off the events — drew
|
|
803
|
+
the counts of a withdrawn signature under a gate whose own closing section said
|
|
804
|
+
`Pending gate: alpha is waiting for tldrx approve`. #122 had already cleared `executed_by`
|
|
805
|
+
and `authority` on a revoke for exactly this reason and deliberately left this one call
|
|
806
|
+
open.
|
|
807
|
+
- **Everything that described the signature now leaves the mapping together**: `by`, `at`,
|
|
808
|
+
`executed_by`, `authority` and `evidence`. A revoked gate is back to the five keys a
|
|
809
|
+
pending gate has always had, byte for byte.
|
|
810
|
+
- **Moved, not destroyed.** `run.yml` is STATE — the resume point, read as a description of
|
|
811
|
+
how things are now. `events.jsonl` is HISTORY — append-only, read as a description of what
|
|
812
|
+
happened. The contradiction was a state contradiction, so the pointer leaves the gate; and
|
|
813
|
+
because an audit framework does not delete history, the withdrawn `evidence` block is
|
|
814
|
+
written onto the `gate.revoked` event, beside the `signed_by`/`signed_at` it already
|
|
815
|
+
carried and the envelope's own actor and timestamp — who took it back, and when. The
|
|
816
|
+
committed note never moves from `<phase>/gate-evidence/<stage>.md`, and `tldrx reject`
|
|
817
|
+
now says so on the way out rather than leaving an operator to guess that a cleared
|
|
818
|
+
pointer was not a deleted file.
|
|
819
|
+
- A `revoked:` trail kept ON the gate was the alternative. It was refused because it grows:
|
|
820
|
+
a gate may be approved, revoked, re-approved and revoked again, so the state file would
|
|
821
|
+
accumulate a list of withdrawn signatures — which is what the append-only log is for —
|
|
822
|
+
and every reader would have to learn a "withdrawn" mode for a block whose only truthful
|
|
823
|
+
reading in `run.yml` is "current".
|
|
824
|
+
- **`gate.revoked` is narrated by `tldrx replay` for the first time.** It was in the event
|
|
825
|
+
set and in no narrative: `bullet()` had no case for it, so it fell to `default: return
|
|
826
|
+
null` and a replay of a run whose approval had been taken back showed the approval and
|
|
827
|
+
nothing after it. Evidence moved somewhere no reader looks is evidence deleted, so that
|
|
828
|
+
line is part of this fix — it names both parties, the stale count, what the signature had
|
|
829
|
+
rested on and that the note is still on disk.
|
|
830
|
+
- `evidence` is written on the event **only when there was one**, so every `gate.revoked`
|
|
831
|
+
for a human or auto gate is shape-identical to the ones written before this. Old records
|
|
832
|
+
are untouched in both directions: an approved gate carrying `evidence` and none of #122's
|
|
833
|
+
blocks validates, loads and emits byte-for-byte, and revoking one clears its evidence by
|
|
834
|
+
the same single rule. Pinned in `test/revoke-evidence.test.ts`.
|
|
835
|
+
|
|
836
|
+
- **`tldrx status` now reports an AGENT-signed gate as machine-signed (#124).**
|
|
837
|
+
`machineSignedDetails` is the report whose whole job is naming the gates a machine closed,
|
|
838
|
+
so a person can take one back, and its selector was `gate.by === "auto"`. That catches every
|
|
839
|
+
facilitator-closed gate and no agent-closed one: an `agent` gate records the evidence note's
|
|
840
|
+
`by:`, which is the OPERATOR account the agent was running as — a person's name. Measured on
|
|
841
|
+
run `260902-discovery-pipeline-map` (the record in #122) that gate reads `by: alanmartinez`,
|
|
842
|
+
so the report counted it as human-signed and never offered the revoke. That is the inverse of
|
|
843
|
+
what the report is for, on the one closure kind where the recorded name is not the entity
|
|
844
|
+
that did the checking.
|
|
845
|
+
- The selector is now `gate.executed_by.type !== "human"`, with `by === "auto"` kept as the
|
|
846
|
+
**union member** rather than replaced: a gate signed before #122 has no `executed_by`, and
|
|
847
|
+
there the old heuristic is the only signal there is. It asks `!== "human"` rather than
|
|
848
|
+
naming `agent` and `auto`, because listing the machines by name is exactly how the `agent`
|
|
849
|
+
case went missing the first time.
|
|
850
|
+
- **The wording changed with it.** "N gate(s) signed `by: auto`, not by a person" was true of
|
|
851
|
+
the facilitator and false of an agent gate, where the record *does* carry a person's name.
|
|
852
|
+
The line now reads `N gate(s) closed by a machine, not by a person — <phase>/<stage> signed
|
|
853
|
+
by <signature>, …`, rendering each signature through `describeGateSignature` — the one
|
|
854
|
+
renderer `run status`, `replay` and the dashboard already share, so a fourth reading of one
|
|
855
|
+
fact cannot drift from the other three, and a record with no `executed_by` still prints the
|
|
856
|
+
bare `by` it always printed. A second line, present only when an agent gate is among them,
|
|
857
|
+
says that an `agent` gate is signed under the operator account the agent ran as, so the
|
|
858
|
+
name on it is not the entity that did the checking.
|
|
859
|
+
- **The executor's kind, never the policy.** A person approving an `agent`-gated stage with
|
|
860
|
+
no flag is a human acting directly and is still not reported. Pinned, with the legacy
|
|
861
|
+
fallback and the negative cases, in `test/machine-signed-gates.test.ts`.
|
|
862
|
+
- The statusline's counter was the same selector on a second surface, and was left alone
|
|
863
|
+
here because it is a different label. Filed as #127, and fixed below: it is `machine:N`
|
|
864
|
+
now, off this same `closedByMachine`.
|
|
865
|
+
|
|
866
|
+
- **The merge-wave suite no longer plants its #95 fixture at a machine-global path (#113).**
|
|
867
|
+
`b8d1fcb` gave each invocation a private `$TMPDIR`, so a run's own log root stopped being
|
|
868
|
+
a shared namespace — and the fixture that plants a *foreign* wave's kept log kept writing
|
|
869
|
+
to the constant `mw-999999` in the machine's tmpdir. Every concurrent copy of
|
|
870
|
+
`test/merge-wave.test.ts` planted that one directory and, in its `finally`, deleted it, so
|
|
871
|
+
the first process to finish removed the directory its siblings were still about to assert
|
|
872
|
+
on. Measured 2026-09-02 at `965eb54`, four concurrent runs of that file: **9 failures in
|
|
873
|
+
12** targeted runs and **5 in 8** whole-file runs, every one of them
|
|
874
|
+
`expect(existsSync(foreign)).toBe(true)` receiving false. After the fix, on the same box
|
|
875
|
+
and the same load: **0 in 12** and **0 in 8**.
|
|
876
|
+
- The plant is still `mw-<pid>`, still directly in the shared tmpdir — both load-bearing,
|
|
877
|
+
because a regression to the pre-#95 machine-global scan has to keep finding it — but the
|
|
878
|
+
pid is now derived from the planting process's own, in `test/fixtures/foreignWaveLog.ts`.
|
|
879
|
+
It is a fixture module rather than an inline helper so the guard can be a two-PROCESS
|
|
880
|
+
measurement: a child imports the same function and prints the path it would choose, and
|
|
881
|
+
the test asserts the two differ. A test that asks one process whether its own name is
|
|
882
|
+
unique can only ever say yes.
|
|
883
|
+
|
|
884
|
+
- **`merge-wave.sh` no longer executes a file anything can rewrite underneath it (#117).**
|
|
885
|
+
Bash reads a script incrementally and seeks back into it for each next command; it does
|
|
886
|
+
not snapshot. `git merge --no-ff` sits about a third of the way through `merge-wave.sh`,
|
|
887
|
+
and everything that makes a wave honest lives after that byte — the four gates, the
|
|
888
|
+
`HEAD moved` assertion, the fast-forward check, the push, and the summary line. Measured
|
|
889
|
+
on `GNU bash 3.2.57(1)-release (arm64-apple-darwin25)`: a ~24 KB script rewritten IN PLACE
|
|
890
|
+
while it slept printed its head, lost its tail, and **exited 0**. Silently stopping early
|
|
891
|
+
and reporting success is exactly the failure this script exists to prevent, aimed at
|
|
892
|
+
itself. Reproduced as a wave: with the typecheck gate truncating `scripts/merge-wave.sh`,
|
|
893
|
+
the run merged, ran ONE of three gates, pushed nothing, printed nothing, and exited 0.
|
|
894
|
+
- **The script now re-execs itself from a private snapshot before it does anything.**
|
|
895
|
+
`exec` keeps the pid, so a caller's timeout, `kill`, `$$`, the lock's owner line and
|
|
896
|
+
`$LOGS` all still name the same process; the copy lives in this run's own `$TMPDIR`
|
|
897
|
+
under an `mw-*` name, so #95's leak assertions double as its cleanup check. The
|
|
898
|
+
re-entry flag is an ARGV sentinel rather than an environment variable on purpose: the
|
|
899
|
+
`bun test` gate runs this repo's own merge-wave suite, which spawns the script again,
|
|
900
|
+
and an exported flag would have let an outer wave switch off the inner ones' protection.
|
|
901
|
+
- **Why the live merges of #113/#114 survived, and why that is not a defence.** `git
|
|
902
|
+
merge` unlinks and recreates a changed file rather than truncating it — measured on git
|
|
903
|
+
2.50.1, a merge that shrank a tracked file moved it from inode `192824934` to
|
|
904
|
+
`192824958` — so the running shell kept reading the ORIGINAL bytes off the now-unlinked
|
|
905
|
+
inode. That is how merge `481540d` printed `typecheck/build/seam clean` while
|
|
906
|
+
`git show 481540d:scripts/merge-wave.sh` already ended in `typecheck/build/docs/seam
|
|
907
|
+
clean`: two versions, both intact, in the same second. The merge path was safe only for
|
|
908
|
+
as long as git keeps replacing rather than truncating — an implementation detail of
|
|
909
|
+
somebody else's program, load-bearing for this script's honesty, and nothing here would
|
|
910
|
+
have noticed it changing.
|
|
911
|
+
|
|
912
|
+
- **An interrupted merge wave no longer leaves an ungated commit on `main` for the next
|
|
913
|
+
sibling to push under its own gate result (#116).** Measured live 2026-09-02 while merging
|
|
914
|
+
#109/#110 in the shared checkout: the caller's own 10-minute timeout sent `SIGTERM` at
|
|
915
|
+
minute 10 of an ~8-minute gate run. The `TERM` trap handed back the lock and the marker
|
|
916
|
+
and touched nothing else, so `main` sat for ~15 minutes on a merge commit that no gate
|
|
917
|
+
ever finished and no lock, marker or log advertised — `git status` clean, `.git/merge-wave.lock`
|
|
918
|
+
gone, no `merge-wave.sh` running. All three existing defences are silent on it by
|
|
919
|
+
construction: the lock had been released, the tree was clean, and
|
|
920
|
+
`git merge-base --is-ancestor origin/main HEAD` PASSES for the next wave, because
|
|
921
|
+
origin/main genuinely is an ancestor of the orphan. So the sibling merged on top, gated
|
|
922
|
+
the pair, and published both under one summary line naming only itself. Nobody lied; the
|
|
923
|
+
gate record was simply attached to the wrong tree — the mirror image of the #44 race, and
|
|
924
|
+
about the run that is *dead* rather than the one still alive.
|
|
925
|
+
- **The wave now owns its merge commit until the push.** `merge-wave.sh` records `main`'s
|
|
926
|
+
sha before `git merge --no-ff`, and `unwind` puts it back on every path that does not
|
|
927
|
+
publish: the `INT`/`TERM` traps, a red gate, a failed push. Deliberately narrow on three
|
|
928
|
+
counts — it undoes only the sha *this* run created, only while that sha is still `HEAD`
|
|
929
|
+
(so a third party's commit landing on top, the exit-5 case, is left for a human instead
|
|
930
|
+
of being discarded), and only before `PUSHED=1`. It also runs *before* the lock is
|
|
931
|
+
released, while this invocation still matches the lock token, so the #89 ref guard waves
|
|
932
|
+
it through rather than vetoing the repair.
|
|
933
|
+
- **A red gate rewinds too.** It used to report "main left at merge commit `<sha>`", which
|
|
934
|
+
is the same landmine by another route: the branch still holds every byte of the work and
|
|
935
|
+
the merge is one command away, while the kept log directory is the real inspection
|
|
936
|
+
surface. The summary line now names the sha it rewound off `main`.
|
|
937
|
+
- **And a wave refuses to start on a `main` that is ahead of origin/main — exit 8, naming
|
|
938
|
+
the commits.** `SIGKILL` and a power cut run no trap at all, so the unwind alone cannot
|
|
939
|
+
close the hole. `HEAD`, not `refs/heads/main`, because `HEAD` is what gets merged into
|
|
940
|
+
and what `HEAD:main` pushes; behind or diverged is still the fast-forward check's job at
|
|
941
|
+
the push. Six tests in `test/merge-wave.test.ts` pin it, including the end-to-end repro:
|
|
942
|
+
a wave killed mid-gate, then a sibling wave whose `origin/main` must not contain the
|
|
943
|
+
dead run's commit.
|
|
944
|
+
|
|
945
|
+
- **A docs-scope story bundle is no longer handed to a developer with nothing to write, no
|
|
946
|
+
requirements it can open, and its core acceptance criterion deleted (#111).**
|
|
947
|
+
All three were repaired BY HAND by the unattended driver of `260902-discovery-pipeline-map`
|
|
948
|
+
on 2026-09-02, and all three reproduce from one fixture: a real `run new --scope docs
|
|
949
|
+
--seed <root document>` where the seed is the run's source of truth, lives at the workspace
|
|
950
|
+
root, and the story's repo is a sibling directory.
|
|
951
|
+
- **`touches` was empty.** It was built from what `01-what/handoff.md` CITES with a repo
|
|
952
|
+
prefix, and that could only ever find files already on disk — so a run whose whole output
|
|
953
|
+
is a document nobody has written yet got `touches: []`, while the developer prompt tells
|
|
954
|
+
the sub-agent that a change outside `touches` is a plan deviation. The story was forbidden
|
|
955
|
+
to do the one thing it existed for. `touches` now also carries the documents the run's own
|
|
956
|
+
brief NAMES — `## Decisions` and `01-what/success-metrics.md` — including a path with no
|
|
957
|
+
file behind it yet, which `prompts.ts` has always rendered as "(does not exist yet — this
|
|
958
|
+
story creates it)". Two ways in and no third: the repo has the file, or the path has a
|
|
959
|
+
directory and the repo has that directory; a bare dotted word in prose (`Node.js`) is
|
|
960
|
+
refused. Every addition is written into `notes:`, and a `touches` that is still empty now
|
|
961
|
+
says so in `notes:` instead of leaving it to a comment on an empty list.
|
|
962
|
+
- **The seed documents were unreachable from the story worktree.** They live at the
|
|
963
|
+
workspace root, are never copied, and need not be inside the story's repo — so the
|
|
964
|
+
handoff's `[src: seeds/….md:5]` resolved to nothing from the worktree and the driver
|
|
965
|
+
rewrote every read to an absolute path. The bundle now CARRIES them: `inputs:` lists the
|
|
966
|
+
run's `--seed` documents (read from `run.yml`'s first stage, where `run new` declares
|
|
967
|
+
them) and the developer prompt inlines their content, resolved through the same
|
|
968
|
+
`resolveDeclared` two-base rule the What stage's own inputs use. Bounded at 64 KB per
|
|
969
|
+
bundle — the same budget the story's touched files spend — and anything that does not fit
|
|
970
|
+
is named in `notes:` rather than dropped in silence.
|
|
971
|
+
- **The acceptance filter deleted the story's core criterion for naming a question id.**
|
|
972
|
+
"All four seed questions (Q1–Q4) have a dedicated section in
|
|
973
|
+
`docs/discovery-pipeline-map.md`" was dropped on the `a question id` signal, leaving the
|
|
974
|
+
story with the "(no `## Decisions` bullet …)" placeholder as its whole Done-when list.
|
|
975
|
+
Naming a question is how a document ABOUT the questions is specified. A bullet that names
|
|
976
|
+
a PRODUCT document now survives every signal — which is what the code comment above that
|
|
977
|
+
list has claimed since it was written, and was not true. Two narrower repairs came with
|
|
978
|
+
it: `questions.md` is matched at a path boundary, so `seeds/pipeline-questions.md` — a
|
|
979
|
+
document the TEAM wrote — is no longer read as the What stage's own output (it had
|
|
980
|
+
emptied `goal:` on the same run); and every signal is now tested against the bullet's
|
|
981
|
+
PROSE, not its `[src: …]` token, because a citation says where a claim was checked and not
|
|
982
|
+
what it is about. New `withoutSrcToken` lives in `text/srcToken.ts`, the one file that may
|
|
983
|
+
hold the grammar (#80).
|
|
984
|
+
|
|
985
|
+
- **A closed run no longer sets up the operator's next `git pull` to be refused (#102).**
|
|
986
|
+
Measured on aparece-v2, run `260830-ordering-inventory`, 2026-09-02: the run closed at
|
|
987
|
+
`14:14:00Z`; ninety-two seconds later a commit on `epic/ordering-inventory` carried a
|
|
988
|
+
snapshot of the whole live `tldrx-work/<run>/` tree plus `.tldrx/memory/facts.yml`; PR #10
|
|
989
|
+
merged it; the operator's `git pull` was refused over 5 modified and ~40 untracked paths,
|
|
990
|
+
and the recovery was a rebase.
|
|
991
|
+
- **The cause was a gap, not a bad line of code.** tldrx has never had a code path that
|
|
992
|
+
commits `tldrx-work/` to an epic — story commits have excluded the state dirs since
|
|
993
|
+
`stateDirPrefixes` landed, and the offending commit was authored by the operator's agent
|
|
994
|
+
after the close. What the framework did have was a hole: in a `root_is_repo` workspace it
|
|
995
|
+
writes `run.yml`, `events.jsonl`, `budget.yml` and every phase document straight into the
|
|
996
|
+
operator's WORKING TREE and left them uncommitted for the length of the run, said nothing
|
|
997
|
+
about them at close, and offered no verb for "the run is over, commit its docs". So an
|
|
998
|
+
agent invented one, and half the time invented it onto the epic.
|
|
999
|
+
- **The close now commits that state itself.** `closeRun` — one home for the policy, the
|
|
1000
|
+
same three callers `closeRunWorktrees` had (`tldrx next`, `tldrx approve`, `tldrx run
|
|
1001
|
+
cancel`) — commits `tldrx-work/<run>/` and `.tldrx/memory/` in the workspace checkout, on
|
|
1002
|
+
the branch that checkout is on, and prints one line saying where they went. New
|
|
1003
|
+
`commitPathsOnly` is the inverse of `commitAll`: `git commit -- <pathspec>`, so a
|
|
1004
|
+
`README.md` the operator had STAGED is still staged and still uncommitted afterwards.
|
|
1005
|
+
Two paths refuse rather than guess — a checkout sitting on the run's own epic branch
|
|
1006
|
+
(`on-epic`) and a detached HEAD — and both report instead of failing the close. It never
|
|
1007
|
+
pushes; spec §5 is unchanged.
|
|
1008
|
+
- **`tldrx ship` refuses an epic that carries the framework's own state**, names the paths,
|
|
1009
|
+
and prints the two-command repair (`git checkout <base> -- tldrx-work .tldrx` then a
|
|
1010
|
+
commit — a forward commit, never a rebase). This is the last point tldrx holds the wheel
|
|
1011
|
+
before a PR, and the diff is three-dot so a trunk that gained run state after the epic was
|
|
1012
|
+
cut does not read as the epic carrying it. A `git diff` that fails answers "nothing here":
|
|
1013
|
+
a probe that could not tell must never turn into a refusal.
|
|
1014
|
+
- **Every `*.bak` tldrx writes is gitignored, at any depth.** `tldrx-work/*/*.bak` reached
|
|
1015
|
+
one level and missed `tldrx-work/<run>/04-build/preflight.yml.bak`, which
|
|
1016
|
+
`git check-ignore` matched against the block's own `!tldrx-work/**` re-include — measured,
|
|
1017
|
+
and it was swept into the aparece-v2 rescue commit. Now `tldrx-work/**/*.bak` and
|
|
1018
|
+
`.tldrx/**/*.bak`. Existing workspaces pick the block up on the next `tldrx init` (it is a
|
|
1019
|
+
marked block, so re-running it is idempotent); until then the close excludes `*.bak` from
|
|
1020
|
+
its own pathspec, so the one write path that could have committed them does not.
|
|
1021
|
+
- **A 1M-context model is no longer sized at 200k because of how its name was spelled
|
|
1022
|
+
(#112).** Measured at `d1d9c3f`: `priceFor("claude-sonnet-4-5-20250929[1m]")` returned the
|
|
1023
|
+
`sonnet` row and `contextTokensFor` therefore answered **200,000, not 1,000,000**. The
|
|
1024
|
+
cause was that `[1m]` was carried inside a row's id, so `name.includes("sonnet[1m]")`
|
|
1025
|
+
could only ever match the bare alias — and the suite could not see it, because the one pin
|
|
1026
|
+
that existed (`priceFor("opus[1m]")`) used exactly that convenient spelling while the
|
|
1027
|
+
repo's own fixtures use the dated form (`claude-fable-5[1m]`). It fed `run estimate` and
|
|
1028
|
+
the context ledger, which decides when a stage is near its window.
|
|
1029
|
+
- **`[1m]` is now matched as a MARKER on a name, separately from the family**, so
|
|
1030
|
+
`sonnet[1m]` and `claude-sonnet-4-5-20250929[1m]` land on the same row. A `[1m]` row is
|
|
1031
|
+
offered only to a name that carries the marker, so a dated spelling WITHOUT it is not
|
|
1032
|
+
promoted: `claude-opus-4-5-20251101` still resolves to `opus`/200k.
|
|
1033
|
+
- **A family the table prices answers for its own windows.** haiku has no `[1m]` row
|
|
1034
|
+
because there is no 1M haiku, and its 200k is the one MEASURED number in the file — a
|
|
1035
|
+
marker does not overrule it, so `claude-haiku-4-5-20251001[1m]` stays 200k.
|
|
1036
|
+
- **A family the table does NOT price gets the window its name declares.**
|
|
1037
|
+
`contextTokensFor("claude-fable-5[1m]")` is 1,000,000 rather than a silent 200k default.
|
|
1038
|
+
No fable price row was invented: `priceFor` still returns null for it, so nothing quotes
|
|
1039
|
+
a price for a model this table cannot price — only the sizing changed.
|
|
1040
|
+
- No USD figure moved. The `[1m]` rows already priced identically to their 200k siblings;
|
|
1041
|
+
this was never a billing bug, and `tldrx cost` reads `total_cost_usd` off the CLI.
|
|
1042
|
+
- **A stage with nothing to ask can close its own auto gate again (#109).** `questionsCondition`
|
|
1043
|
+
read zero parsed question blocks as one thing when it is really three. *Unreadable* — the
|
|
1044
|
+
2026-08-29 failure, where a stage wrote `### Q1 — …` from the old template and four questions
|
|
1045
|
+
were swallowed — is still refused, by id. *Missing* — a `questions.md` the stage declared as an
|
|
1046
|
+
output and never wrote — is still refused, and now says so in its own words
|
|
1047
|
+
(`questions.md is a declared output of this stage and was never written — an absent file is not
|
|
1048
|
+
an answer`) instead of sending the reader to a template for a file that does not exist.
|
|
1049
|
+
*Present, readable, and raising nothing* — the GOOD case — now SATISFIES the condition. Measured
|
|
1050
|
+
live 2026-09-02 on run `260902-discovery-pipeline-map`: every clean stage was paying for the
|
|
1051
|
+
2026-08-29 bug, because the state an auto gate exists to close over was indistinguishable from
|
|
1052
|
+
the state it exists to refuse.
|
|
1053
|
+
- **`absent:` has ONE semantic, and both checkers share it (#110, absorbing #105).** Two live
|
|
1054
|
+
failures in the same week, in opposite directions, out of the same resolution: `claim-sources`
|
|
1055
|
+
PASSED `- none [src: absent:04-build/log]` over a directory holding seven files, while the auto
|
|
1056
|
+
gate REFUSED `absent:.tldrx/memory/facts.yml` — "I searched, there is no recorded fact", which is
|
|
1057
|
+
the spec's own spelling of an empty section. Both were `unverified`, which one checker read as
|
|
1058
|
+
"fine" and the other as "stop".
|
|
1059
|
+
- **`absent:<path>` now resolves against the same bases a `file` src does** (workspace root, run
|
|
1060
|
+
directory, named repo, epic worktrees), and accepts `repo:path`. That is the mechanism behind
|
|
1061
|
+
#105: it used to try the workspace root and nothing else, so a run-relative
|
|
1062
|
+
`absent:04-build/log` never even saw the directory it named and reported an absence it had
|
|
1063
|
+
never looked for.
|
|
1064
|
+
- **A fourth outcome, `noted`.** No such path, an empty file, an empty directory ⇒ `ok`, the
|
|
1065
|
+
absence is literal. The path exists with content and no needle ⇒ `noted`: legal, never fatal,
|
|
1066
|
+
never blocking — and never silent. It is counted and named BY PATH in the `claim-sources`
|
|
1067
|
+
detail (`unchecked absence: 2 (04-build/log, .tldrx/memory/facts.yml)`) and therefore in the
|
|
1068
|
+
auto-gate note, so the two can no longer disagree about the same file.
|
|
1069
|
+
- **`absent:<path>#<needle>` makes an absence checkable.** Say what you searched for and the
|
|
1070
|
+
checker searches for it: not found is a *verified* `ok`, found is a REFUSAL that names the line
|
|
1071
|
+
(`` `30 days` IS at docs/retention.md:3 — that is a presence, not an absence ``). It is the
|
|
1072
|
+
upgrade path out of `noted`, and the first form in which an absence over a file with content is
|
|
1073
|
+
ever actually checked.
|
|
1074
|
+
|
|
1075
|
+
- **The docs site advertised a version we had already shipped past (#121).** The hero said
|
|
1076
|
+
0.4.0 in four places — `index.md`, `guides/faq.md` and both Spanish mirrors — while npm
|
|
1077
|
+
served 0.5.0. Hand-bumping it would have bought one release; the number is now read at
|
|
1078
|
+
build time instead, by `docs-site/version.ts`, from `package.json` (the field npm
|
|
1079
|
+
publishes) and the README release table (the row `release-check.sh` already refuses to
|
|
1080
|
+
release without). `config.mts` hands both to the pages through `themeConfig`, on the root
|
|
1081
|
+
and on each locale, and the pages interpolate. `version.ts` throws rather than guesses: a
|
|
1082
|
+
docs build that cannot tell which version it is describing fails, instead of quietly
|
|
1083
|
+
publishing `undefined`. The "0.4.0 was the first beta" sentences stay — that is history,
|
|
1084
|
+
and it is still true — but they now read as history.
|
|
1085
|
+
|
|
1086
|
+
- **Three surfaces disagreed about whether Bun is needed to run tldrx (#121).** `env.yml`
|
|
1087
|
+
declared `bun` `required: true`, and `DoctorReport.healthy` is "no required tool is
|
|
1088
|
+
missing", so `tldrx doctor` exited 1 on machines where tldrx was installed and working.
|
|
1089
|
+
The docs site's quickstart said you need "Node 20 **and** Bun 1.3"; the README and
|
|
1090
|
+
`docs/guide/01-quick-start.md` said an installed tldrx needs only Node. The README was
|
|
1091
|
+
right, and the code says so: `node dist/tldrx.js --version` prints the version and exits 0
|
|
1092
|
+
with no Bun involved, because `src/core/runtime/index.ts` picks its implementation off
|
|
1093
|
+
`typeof Bun` at import time and `nodeRuntime.ts` is complete — `bun build --target=node`
|
|
1094
|
+
inlines the `yaml` package, so a published install resolves zero runtime dependencies.
|
|
1095
|
+
- `bun` is now `required: false` in `env.yml`, still declared and still carrying its 1.3.0
|
|
1096
|
+
floor, with the measurement recorded beside the flag. It remains genuinely required to
|
|
1097
|
+
build the bundle, to run the test suite, and to run the hooks straight from a clone
|
|
1098
|
+
(`plugin/hooks/hooks.json` spawns `bun`); an `install --claude` wiring goes through
|
|
1099
|
+
`tldrx hook <name>`, which is the Node bundle.
|
|
1100
|
+
- Every surface now separates *running the published package* from *building and
|
|
1101
|
+
contributing*, in the same words: both quickstarts, the README, `01-quick-start.md` and
|
|
1102
|
+
`09-troubleshooting.md`.
|
|
1103
|
+
|
|
1104
|
+
- **The landing page claimed an absolute the implementation cannot guarantee (#121).** It
|
|
1105
|
+
told readers that because the files are the state, nothing could get out of step with
|
|
1106
|
+
anything — in the same week that #116 and #117 shipped precisely because state can. It now
|
|
1107
|
+
says what is defensible and still worth saying: the canonical state is on disk,
|
|
1108
|
+
inspectable, diffable, committable, recoverable. Mirrored in Spanish.
|
|
1109
|
+
|
|
1110
|
+
- **Provider wording tightened to what we can show (#121).** "Tool-agnostic" was an
|
|
1111
|
+
unqualified claim about software with one working runner. The README and `docs/concept.md`
|
|
1112
|
+
now say the workflow and the persisted state format are provider-independent, and that the
|
|
1113
|
+
automated runner currently supports Claude Code. The docs site made no stronger claim; it
|
|
1114
|
+
was swept and needed no change.
|
|
1115
|
+
|
|
3
1116
|
## 0.5.0 — 2026-09-02
|
|
4
1117
|
|
|
5
1118
|
### Added
|