tldr-experts 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1433 -0
- package/README.md +61 -13
- package/dist/hooks/answer-capture.js +6 -7
- package/dist/hooks/budget-gate.js +14 -11
- package/dist/hooks/{chunk-4cp363kv.js → chunk-14zn51kh.js} +60 -16
- package/dist/hooks/{chunk-s1c5h7yx.js → chunk-3w55tp71.js} +26 -7
- package/dist/hooks/{chunk-7y2dq0pj.js → chunk-5556vjt5.js} +1 -1
- package/dist/hooks/{chunk-t1ywrfr4.js → chunk-889tybxc.js} +10 -8
- package/dist/hooks/{chunk-9gb21660.js → chunk-a5dq2dcp.js} +54 -14
- package/dist/hooks/{chunk-sq44k6g2.js → chunk-a6rpj2cp.js} +684 -3
- package/dist/hooks/{chunk-tzzwddct.js → chunk-bvm6vjrt.js} +1 -1
- package/dist/hooks/{chunk-phmdk72a.js → chunk-hcrbr430.js} +1 -1
- package/dist/hooks/{chunk-rpcxsqh3.js → chunk-nadqsr3w.js} +1 -1
- package/dist/hooks/{chunk-9zsqxr6y.js → chunk-q8d3sff9.js} +23 -16
- package/dist/hooks/{chunk-c6t5nx0r.js → chunk-qw73rdbr.js} +1 -1
- package/dist/hooks/{chunk-rz541e2b.js → chunk-sae7sqty.js} +2 -0
- package/dist/hooks/{chunk-b8kxzna2.js → chunk-v1c1hpb8.js} +1 -1
- package/dist/hooks/claim-sources.js +19 -16
- package/dist/hooks/dod-gate.js +7 -6
- package/dist/hooks/no-reask.js +9 -9
- package/dist/hooks/session-start.js +46 -21
- package/dist/hooks/statusline.js +8 -9
- package/dist/tldrx.js +6364 -2465
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/skills/tldrx/SKILL.md +1 -1
- package/stages/watch/stage.md +4 -0
- package/templates/expert.md +13 -1
- package/templates/watcher.md +8 -1
- package/workflows/bugfix.yml +8 -4
- package/workflows/docs.yml +8 -4
- package/workflows/feature.yml +8 -4
- package/workflows/hotfix.yml +8 -4
- package/workflows/integration.yml +8 -4
- package/workflows/migration.yml +8 -4
- package/workflows/performance.yml +8 -4
- package/workflows/prototype.yml +8 -4
- package/workflows/refactor.yml +8 -4
- package/workflows/security-patch.yml +8 -4
- package/workflows/spike.yml +8 -4
- package/workflows/upgrade.yml +8 -4
- package/dist/hooks/chunk-m3mewgnw.js +0 -522
- package/templates/epic.md +0 -40
- package/templates/story.md +0 -55
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1438 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-09-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **The dashboard answers the five questions #85 left open (#93).** Each needed a decision
|
|
8
|
+
rather than a patch, and each was taken the CONSERVATIVE way: render what the files
|
|
9
|
+
already say, invent no interaction the page does not already have, and let nothing on a
|
|
10
|
+
read-only page run anything. Every choice below is a **maintainer call, subject to owner
|
|
11
|
+
review** — they are listed on the issue.
|
|
12
|
+
- **The Watchers tab draws the cards.** `05-watch/watchers/*.md` is read into the model as
|
|
13
|
+
the seven fields `Watcher` really declares — `id`, `epic`, `title`, `stories`, `repos`,
|
|
14
|
+
`status`, plus the optional `owner` (#70) — and the tab prints them instead of printing
|
|
15
|
+
the shape it wished it had. A `draft` card also carries **why**: the `absent:` sources it
|
|
16
|
+
cites under `## Signal`, which is the card's own rule for not being `verified`. The
|
|
17
|
+
reading is deliberately the SMALL one — the model resolves no `[src: …]`, calls no
|
|
18
|
+
`parseWatcherCard` and computes no `CardChecklist`, because that re-checks every citation
|
|
19
|
+
against today's working tree and would make a read-only dashboard the only screen in the
|
|
20
|
+
product that runs something. `tldrx watch check` stays the thing that checks, and the
|
|
21
|
+
page says so. A `verified` stamp over an `absent:` Signal is shown as what it is rather
|
|
22
|
+
than silently corrected: `watch check` re-stamps cards, and a viewer that disagreed with
|
|
23
|
+
the file would be a third opinion.
|
|
24
|
+
- **A `draft` card raises no attention card.** The page's rule — an alert means a run is
|
|
25
|
+
waiting on a PERSON right now, derived once in `waiting.ts` — is unchanged. An
|
|
26
|
+
uninstrumented signal is a fact about coverage that stays true until somebody
|
|
27
|
+
instruments it, and it belongs in a panel the way `budget.blocked` does.
|
|
28
|
+
- **Preflight refusals leave their rows on the page.** `04-build/preflight.yml` is read the
|
|
29
|
+
same way `budget.yml` is: read-only, additive, and through the reader that never throws.
|
|
30
|
+
A new **Base gates** section names each of the workspace's own gate commands, the repo
|
|
31
|
+
and base it ran on, its exit code and its `ok`/`failed`/`unmeasured` status — so a Build
|
|
32
|
+
that refused to start is no longer a stage that went backwards for no visible reason.
|
|
33
|
+
The alternative on the issue was to emit an EVENT instead; that is the bigger, better
|
|
34
|
+
change and it is not this one, because the owner's #85 decision covered the two files
|
|
35
|
+
that already existed and an event is a new write on a refusal path. Reading the file it
|
|
36
|
+
already writes is the same doctrine one step further. Red rows are drawn as rows, not as
|
|
37
|
+
alerts, for the same reason `budget.blocked` is.
|
|
38
|
+
- **`keep_worktrees` is one line on the run detail**, and only when it is set (#16). The
|
|
39
|
+
key is written only when true, so drawing `false` would put a row on every run in the
|
|
40
|
+
workspace saying what all of them do. `RunDocument` projects it tolerantly: anything that
|
|
41
|
+
is not the boolean `true` is not a promise to keep the worktrees.
|
|
42
|
+
- **A cancelled run says who closed it, when, and why.** All three facts already travelled
|
|
43
|
+
on `waiting.message` (#86); the run detail printed the status chip and dropped them. It
|
|
44
|
+
is one `kv` row carrying that sentence — not a second derivation, so the page cannot word
|
|
45
|
+
it differently from `tldrx run status`.
|
|
46
|
+
- **An annotated run wears a marker in the runs list** — one ✎ glyph with the note count in
|
|
47
|
+
its `title`, nothing more. #85 §1 asked for it and the wave declined to invent it. This
|
|
48
|
+
is the smallest thing that is true: no column, no badge count, no new sort key, and the
|
|
49
|
+
notes themselves stay on the run detail where they were. Explicitly **provisional** — a
|
|
50
|
+
count in a row is a design decision, and the first person with a real opinion about that
|
|
51
|
+
list should replace it.
|
|
52
|
+
- **`DASHBOARD_MODEL_VERSION` stays at 3.** `watch`, `preflight` and `keepWorktrees` are
|
|
53
|
+
three additions and nothing was removed; no existing field reads differently than it did
|
|
54
|
+
at v3. The argument for bumping is that `docs/dashboard-model.md` promised, under *What
|
|
55
|
+
is NOT in it*, that two named files were unread, and that promise is now void — but a
|
|
56
|
+
documented absence is not a field, and the number is for fields.
|
|
57
|
+
|
|
58
|
+
- **The dashboard reads `budget.yml` and `events.jsonl` (#85).** The audit that filed the issue
|
|
59
|
+
found five gaps with one root: `buildModel()` read `run.yml`, the phase artefacts, the Plan
|
|
60
|
+
artefacts and the expert files, and nothing else — so five facts a reader went looking for were
|
|
61
|
+
nowhere on the page, each because it lives only in one of those two files. The owner's decision
|
|
62
|
+
(2026-09-02) was that the model may read both, read-only and additive. All five are now drawn.
|
|
63
|
+
- **Operator notes** (`tldrx note`, #46) get their own section on the run detail, with the actor,
|
|
64
|
+
the stage or phase, and the time. **All of them**, not the last three: `tldrx run status` caps
|
|
65
|
+
at three because a terminal has a bottom, and a run detail page does not.
|
|
66
|
+
- **Free review retries** (#78/#79) and the **attempt** each story is on. Both are event-only —
|
|
67
|
+
the story file carries `status` and `evidence` and no counters — so a story that burned both
|
|
68
|
+
attempts and was granted two free re-prompts read on disk exactly like one nobody had touched.
|
|
69
|
+
The plan table gains an `attempts` column (`1 of 2`, against the model's new `maxAttempts`), and
|
|
70
|
+
a **Reopens & retries** section shows the `story.reopened` arcs with their `fix`/`attempts`
|
|
71
|
+
reason and the operator's note. A reopen written before the `reason` key existed reads as
|
|
72
|
+
`attempts`, which is the only kind that existed, rather than as a blank.
|
|
73
|
+
- **`budget.blocked`** occurrences are listed with the phase, both economies' numbers, and — for a
|
|
74
|
+
dollar refusal — the exact `tldrx budget raise` command, short-by rounding included, pinned by a
|
|
75
|
+
test against `raiseCommand` because the renderer is serialised into the page and cannot import
|
|
76
|
+
it. Deliberately **not** an attention card: the page's rule is that an alert means a run is
|
|
77
|
+
waiting on a person NOW, and a refusal in the log is not evidence of that.
|
|
78
|
+
- **The `$0.00 of $25.00` progress bar on a host run is fixed properly.** A run whose `budget.yml`
|
|
79
|
+
says `economy: host-tokens` is not priced in dollars at all, so `ceiling_usd` governs nothing
|
|
80
|
+
and the bar stated a fraction of a denominator that does not apply. Such a run now reads in
|
|
81
|
+
TOKENS on both screens — the runs list and the run detail share one `dashSpendText`, because
|
|
82
|
+
suppressing the bar alone would leave the words `$0.00 of $25.00` making the claim the bar
|
|
83
|
+
was — metered against `ceiling_host_tokens`, the ceiling those tokens really are judged
|
|
84
|
+
against, which exists in no other file. A **Budget** panel carries the per-phase ceilings, `on_exceed`,
|
|
85
|
+
`warn_at_pct`, `on_host_tokens_exceed` and the per-phase economy. An unset phase economy is
|
|
86
|
+
reported as *inherits*, never as a choice somebody made.
|
|
87
|
+
- **`DASHBOARD_MODEL_VERSION` stays at 3**, and the issue asked. Eight fields were added and none
|
|
88
|
+
removed. `spentUsd` was the one with a case to answer — a consumer reading it alone is
|
|
89
|
+
demonstrably wrong about a host-attended run now that the ceiling can sit beside it — but it is
|
|
90
|
+
computed from the same `run.yml` key, holds the same number, and has meant "METERED dollars, a
|
|
91
|
+
lower bound when `unmeteredTasks > 0`" since v3 put `unmeteredTasks` and `hostTokens` next to it.
|
|
92
|
+
A field that gained neighbours did not change meaning.
|
|
93
|
+
- **Neither file is opened by the model**, and the ledger is walked **once per run**. `loadRunResult`
|
|
94
|
+
already parsed both for every run and this file had been discarding them, so the page costs the
|
|
95
|
+
reads it always did; the per-story facts come out of one pass rather than from
|
|
96
|
+
`readReviewLedger`, which re-reads the whole ledger per story and would have made a forty-story
|
|
97
|
+
plan forty passes over a file already in memory.
|
|
98
|
+
- **Absence and damage stay graceful.** No `budget.yml` and no `events.jsonl` renders exactly as
|
|
99
|
+
before, with none of the new sections. An unparseable `budget.yml` costs the panel and nothing
|
|
100
|
+
else. A torn ledger line costs that line and **says so** on the page, because "no operator notes"
|
|
101
|
+
over a damaged ledger is the same lie by omission an unlisted corrupt `run.yml` was.
|
|
102
|
+
- Fixed on the way: an event with **no `payload` key at all** parses fine through the tolerant
|
|
103
|
+
`EventLog.readAll`, and reading `payload.story` off it threw a `TypeError` out of `buildModel`
|
|
104
|
+
and killed the live server for the whole workspace. `TldrxEvent.payload` is typed non-optional;
|
|
105
|
+
the type is a claim about `validateEvent`'s output, not about what is in the file.
|
|
106
|
+
|
|
107
|
+
- **The `tldrx drive` mandate carries its own preflight (#84).** Launching a cold unattended
|
|
108
|
+
session took SIX hand-run commands before the mandate could be pasted at all — `tldrx run
|
|
109
|
+
attend host`, then `tldrx run gates set` five times. Every one of them is a precondition of
|
|
110
|
+
the discipline the mandate exists to transfer, so an owner typing them by hand was doing the
|
|
111
|
+
driver's job for it, and a mandate that assumes its own preconditions only works where
|
|
112
|
+
somebody has already been careful. Both modes now open with a **`## Before anything: the
|
|
113
|
+
preflight`** section, ahead of the roles, that establishes attendedness (`tldrx run status
|
|
114
|
+
<run>`), the gate policy (`--json`, stage by stage) and `budget.yml` — and makes the driver
|
|
115
|
+
**state the ceiling it will honour** before the first turn. Any one it cannot establish is a
|
|
116
|
+
**refusal to start** that names the command that failed, because a driver that starts anyway
|
|
117
|
+
has spent money on a run whose gates it may not close.
|
|
118
|
+
- The two modes differ here exactly as they differ at the gate. `--unattended` may SET what
|
|
119
|
+
it finds wrong: `tldrx run attend host <run>`, and `tldrx run gates set <stage>:agent
|
|
120
|
+
--note "…"` for a stage the owner delegated, over a note **quoting the owner's own
|
|
121
|
+
delegation from the launch message** — so the policy change is signed by the owner's words
|
|
122
|
+
rather than the driver's judgement. `--attended` gets the mirror and may not: gates stay
|
|
123
|
+
human there, so it checks attendedness and the budget, reports a stage that is not `human`
|
|
124
|
+
where it was expected, and moves nothing. A test asserts the attended text contains no
|
|
125
|
+
`gates set <stage>:agent` at all.
|
|
126
|
+
- The `<run>` substitution (#75) covers the new lines like every other, `<stage>` untouched:
|
|
127
|
+
the pinned occurrence count moves from 5/7 to **7 attended / 10 unattended**.
|
|
128
|
+
- Both mandates stay inside `MANDATE_MAX_LINES` (119 unattended, 116 attended), paid for out
|
|
129
|
+
of the header, the driving block and the unattended gate. The sections that make the
|
|
130
|
+
document worth pasting — the three-role spine, evidence discipline, parking, review
|
|
131
|
+
calibration by stakes and budget honesty — are untouched.
|
|
132
|
+
|
|
133
|
+
- **The retro aggregate has consumers: `--json`, the reviewer prompt, and a workspace taxonomy
|
|
134
|
+
(#74).** `tldrx retro --all` (#64) produced exactly the dataset that issue asked for — finding
|
|
135
|
+
class × count × runs × one cited example — and nothing read it. The reader existed; the loop it
|
|
136
|
+
was meant to close was open at the far end. Three seams close it.
|
|
137
|
+
- **`tldrx retro --all --json`** — a stable machine shape: `{version, root, runs, contributed,
|
|
138
|
+
deduped, classes, trends[{cls, count, runs, example{run, kind, text, src}}], findings[]}`. A
|
|
139
|
+
deliberate projection rather than a dump of the internal type, with its key sets asserted
|
|
140
|
+
literally by a test, so adding a field is a visible act and renaming one bumps `version`.
|
|
141
|
+
`--json` belongs to `--all` alone: closing one run WRITES a file and has nothing to parse, so
|
|
142
|
+
`retro --json` on its own is a refusal rather than a stringified sentence.
|
|
143
|
+
- **The adversarial reviewer is fed the workspace's top three classes** before it reads the
|
|
144
|
+
diff, so a review starts from what this team keeps getting wrong instead of rediscovering
|
|
145
|
+
`test-cannot-fail` on its own, run after run. Computed by the same `mineAll` over the same
|
|
146
|
+
workspace, so what the reviewer is told is exactly the top of what `tldrx retro --all` prints.
|
|
147
|
+
Additive and absent-safe in every direction — no runs, no findings, nothing but `other`, or a
|
|
148
|
+
broken taxonomy file, and there is no section at all, not a heading and not a blank line.
|
|
149
|
+
`other` is never offered: it names no defect to look for. Mined once per `tldrx next`
|
|
150
|
+
invocation, so a wave of six stories pays for it once and every reviewer in that invocation
|
|
151
|
+
gets the same prior. The section says out loud that it is a PRIOR and not a checklist — a
|
|
152
|
+
reviewer handed three defect classes and no framing finds three defect classes.
|
|
153
|
+
- **`.tldrx/memory/finding-classes.yml`** adds classes for defects the seven built-in ones do
|
|
154
|
+
not name (`version: 1`, `classes:` of `{name, rules}`, 1–16 each, rules compiled
|
|
155
|
+
case-insensitively). Extensions are tried AFTER every built-in rule and before `other`, so a
|
|
156
|
+
workspace class can only ever claim a finding the built-ins left unclassified — which is what
|
|
157
|
+
keeps an unbounded taxonomy testable, and leaves every shipped fixture immune to whatever a
|
|
158
|
+
workspace writes. A file that will not load is a REFUSAL naming the file, the class and the
|
|
159
|
+
rule — including a rule such as `.*` that matches every text and would swallow the taxonomy —
|
|
160
|
+
never a silent fallback, because a rule its author believes is running and is not would make
|
|
161
|
+
every count a lie. The Build reviewer never fails on it: a refusal costs the prior and prints
|
|
162
|
+
one line, so no story loses an attempt to a YAML typo.
|
|
163
|
+
|
|
164
|
+
- **`CONTRIBUTING.md`, and a section an outside contributor can build a model provider from
|
|
165
|
+
(#27 companion).** #27 (a generic model-provider layer) was closed as parked, not rejected:
|
|
166
|
+
`TLDRX_CLAUDE_BIN` covers today's needs and a provider layer with no second provider is
|
|
167
|
+
speculation. So the seam is written down instead. The new file covers the contribution loop,
|
|
168
|
+
the four gates and what CI actually runs, the red-first rules (including "a test that cannot
|
|
169
|
+
fail is worse than no test", with the shipped example of one that could), and a
|
|
170
|
+
**Contributing a model-provider config** section: what `TLDRX_CLAUDE_BIN` does and does not
|
|
171
|
+
buy, `buildClaudeArgs`' exact command surface, the `stream-json` transcript contract
|
|
172
|
+
`AgentStream` parses and the two behaviours an adapter most easily breaks (the read cap and
|
|
173
|
+
the cost ledger), the result-envelope requirement with its fail-closed rule, and the five
|
|
174
|
+
test files a PR would have to touch. Linked from the README.
|
|
175
|
+
|
|
176
|
+
- **`tldrx questions cards [<run>]` — a parked question, as something to decide (#59).** Measured
|
|
177
|
+
on run `260830-ordering-inventory`, 2026-09-01: the host parked four product questions with
|
|
178
|
+
notes and reported them in its tl;dr. The owner's live words were *"cuales preguntas? no las
|
|
179
|
+
veo? por que no me guió por las preguntas Claude?"* — they were on disk, they were in
|
|
180
|
+
`tldrx questions <run>`, and the count was in the summary. None of that PRESENTED them. Counted
|
|
181
|
+
is not asked, and the gap was never the data; it was the arc from parked to answerable.
|
|
182
|
+
- **Three slots, each a refusal.** Two lines of context (which run and file the question is
|
|
183
|
+
parked in, who asked it, when, in what area) so a card pasted into chat stands up away from
|
|
184
|
+
the terminal. The question's OWN `Why asked:` note verbatim, `[src: …]` included — the slot
|
|
185
|
+
for what the binding docs already decide, quoted rather than summarised; a note that cites
|
|
186
|
+
nothing is FLAGGED as somebody's recollection, and a question parked with no note says so.
|
|
187
|
+
The file's lettered options verbatim, or a loud `NEEDS OPTIONS` marker when it carries none,
|
|
188
|
+
because manufacturing A/B/C would be answering the question in the act of asking it.
|
|
189
|
+
- **A reader, and only a reader** (owner decision 2026-09-01: no interactive loop in v1). It
|
|
190
|
+
opens no run, spawns nothing, records no fact, and a test asserts `questions.md` is
|
|
191
|
+
byte-identical across a render. Answers still flow through `tldrx answer`, and every card
|
|
192
|
+
prints the exact line to type. It does not extend the §2.7 grammar: every field on a card is
|
|
193
|
+
one the existing parser already produces.
|
|
194
|
+
- **No open question is a sentence and an exit 0** — and two silences are told apart, because
|
|
195
|
+
"this run never parked anything" and "everything here is answered" send a reader to different
|
|
196
|
+
places.
|
|
197
|
+
|
|
198
|
+
- **`tldrx watch arm --run <id>` — the merge detector that fires the post-merge checklist (#69).**
|
|
199
|
+
`watch check` (#65) answers *"what do I check now?"*. Nothing answered *"the PR just merged, go
|
|
200
|
+
and check it"* — the half that happens without a human remembering, which matters because the
|
|
201
|
+
failure #65 was filed about IS a memory failure: the owner's own CD gap, a merged branch read as
|
|
202
|
+
a deployed one, cost 19 destroyed records. v1 would not have caught it either, because nobody
|
|
203
|
+
ran it.
|
|
204
|
+
- **A bounded FOREGROUND poller, not a daemon** (owner decision 2026-09-01: no GitHub Actions in
|
|
205
|
+
v1). It reads the branch Build cut from `run.yml` — through the same `pickBranch`/`findRepos`
|
|
206
|
+
`tldrx ship` uses, now exported, so the two verbs cannot disagree about which branch a run
|
|
207
|
+
shipped — asks `gh pr view <branch> --json state,mergedAt` per repo, and prints the same
|
|
208
|
+
checklist the moment every PR for that branch has merged. **Three independent bounds**: a hard
|
|
209
|
+
deadline (`--timeout`, default 3600s, max 86400), a floor on the interval (`--interval`,
|
|
210
|
+
default 60s, under 10 REFUSED rather than quietly raised), and a poll cap that holds even if
|
|
211
|
+
the clock does not move.
|
|
212
|
+
- **Every refusal is a sentence, and they are different sentences.** No epic branch ⇒ `ship`
|
|
213
|
+
cannot have run. No PR for the branch ⇒ either `ship` has not run or the branch was never
|
|
214
|
+
pushed, with both commands printed. A PR `CLOSED` without merging ⇒ stop now, because waiting
|
|
215
|
+
for it could only ever time out. A window that expires ⇒ exit `4` and the command that
|
|
216
|
+
re-arms it.
|
|
217
|
+
- **It never pushes, opens or merges anything**, and `--execute` is deliberately not offered: an
|
|
218
|
+
hour-old poller must not start running the workspace's build commands the instant a merge
|
|
219
|
+
lands. Re-running a recorded command stays an explicit, typed decision.
|
|
220
|
+
- **No test in this suite runs the real `gh` or touches a network.** The unit cases drive a
|
|
221
|
+
recording fake transport; the one end-to-end case puts a stub `gh` first on `PATH`. The clock
|
|
222
|
+
and the sleep are injected, so a test covering a one-hour timeout finishes in milliseconds.
|
|
223
|
+
|
|
224
|
+
- **`tldrx plan schema` — the story/epic/waves contract, printed for a human (#71).** #48 deleted
|
|
225
|
+
`templates/story.md` and `templates/epic.md`, rightly: nothing read them, so nothing kept them
|
|
226
|
+
honest. But they were answering a real question — what shape does a story file take? — and after
|
|
227
|
+
#48 the only reader of the generated contract was `checkContracts.ts`, which splices it into the
|
|
228
|
+
Plan stage's prompt. The shape existed for the agent and not for the person writing a story by
|
|
229
|
+
hand, reviewing one an agent wrote, or debugging a `plan` check refusal.
|
|
230
|
+
- **The same bytes the agent gets.** `plan schema` renders `renderPlanSchemaContract()` verbatim,
|
|
231
|
+
generated from the validators the check runs, so it cannot become the second source of truth
|
|
232
|
+
the templates were. `--story`, `--epic` and `--waves` print one example on its own; at most one
|
|
233
|
+
of them, because two would make the answer ambiguous. A test copies the printed story example
|
|
234
|
+
straight into `validateStoryFile` and asserts it comes back clean.
|
|
235
|
+
- **The one verb in `plan` that resolves nothing.** No workspace, no run, no disk, no spend — the
|
|
236
|
+
question comes before any of that exists, and is often asked from outside a workspace entirely.
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
- **`tldrx drive [--attended|--unattended]` — the host/driver mandate, shipped (#63).** Every elite
|
|
240
|
+
run of 2026-08-31/09-01 was driven by a session carrying a hand-written playbook, and that playbook
|
|
241
|
+
was the framework's real quality floor. It lived in the owner's chat pastes, so a third party
|
|
242
|
+
inherited the CLI and rediscovered the discipline — or did not. It is now plain text the package
|
|
243
|
+
prints, versioned with the package, for a human to paste into the driving session or read before
|
|
244
|
+
they start.
|
|
245
|
+
- **What it carries**: the three-role protocol (developer sub-agent → a FRESH adversarial reviewer,
|
|
246
|
+
never the author → the host verifying BOTH in the code, not in their reports); evidence
|
|
247
|
+
discipline (measured / inferred / assumed labelled in the same sentence as the claim, exit codes
|
|
248
|
+
never read through a pipe, verification from the source, remote shas via `git ls-remote`);
|
|
249
|
+
parking product questions with what the docs already decide, because an open question is never a
|
|
250
|
+
licence to ship an unguarded write; review calibration by stakes, so a security-bearing story
|
|
251
|
+
gets the strongest reviewer; and budget honesty — declare a turn once, state a floor when the
|
|
252
|
+
records are incomplete.
|
|
253
|
+
- **Two modes, one spine.** The disciplines are identical — they are about how a claim is made,
|
|
254
|
+
not about who is watching. What differs is the GATE (`--unattended` signs over a validated
|
|
255
|
+
evidence note with `tldrx approve --as-agent`; `--attended` never signs and hands the decision
|
|
256
|
+
over) and DRIVING (who spawns). Each mode is bounded at 120 lines, asserted by a test: a mandate
|
|
257
|
+
long enough to skim is one nobody follows.
|
|
258
|
+
- **A mode is required and never guessed** (exit `1`), the same refusal `tldrx run attend` makes.
|
|
259
|
+
Handing an attended session the unattended text tells it to sign gates that were never its to
|
|
260
|
+
sign. It needs no workspace, opens no run, spawns nothing and writes nothing.
|
|
261
|
+
|
|
262
|
+
- **`tldrx retro --all` — cross-run mining of what keeps catching you (#64).** Each run left gold
|
|
263
|
+
nobody aggregated: a reviewer's verdict and findings in `04-build/log/<story>.md`, a fix list with
|
|
264
|
+
a disposition per finding, `retro.md`'s `## Build feedback`, and the reason a person typed when
|
|
265
|
+
they reopened a story. Across the first six real runs the same finding CLASSES kept coming back,
|
|
266
|
+
and nothing read more than one run at a time.
|
|
267
|
+
- **A trends table**: finding class × count × how many runs it appeared in × one example with the
|
|
268
|
+
`[src: …]` that lets a reader go and check it.
|
|
269
|
+
- **Seven classes, in a documented precedence**: `test-cannot-fail`, `missing-negative-control`,
|
|
270
|
+
`unreachable-structure`, `stale-comment`, `authorization-not-widened`, `schema-drift`, `other`.
|
|
271
|
+
Classification is ordered keyword rules over the finding text — no model, no scoring, no
|
|
272
|
+
threshold — so the same tree always produces the same table and a rule that misfires can be
|
|
273
|
+
pointed at. The rules are tested against fixtures in the shape the real artefacts carry.
|
|
274
|
+
- **Zero new state.** It writes nothing anywhere: no `retro.md`, no `practices.md`, no cache.
|
|
275
|
+
A test asserts the workspace is byte-identical across the call.
|
|
276
|
+
- **Absence is never an error.** A run with no Build phase, no retro, no events log or an
|
|
277
|
+
unreadable one contributes what it has and is still counted; an empty workspace is an empty
|
|
278
|
+
answer at exit `0`. A repeat of one finding WITHIN a run is collapsed (`retro.md` quotes the fix
|
|
279
|
+
list verbatim); the same finding in two runs is two occurrences, which is the point. A `refuted`
|
|
280
|
+
fix-list finding is read and dropped — ranking a class by disproven findings would make the
|
|
281
|
+
table a report on the reviewer.
|
|
282
|
+
- `--all` is refused (exit `1`) alongside a `<run-id>` or `--apply`, before a file is opened: each
|
|
283
|
+
asks for the opposite of what `--all` does.
|
|
284
|
+
- **`tldrx watch check` is the post-merge checklist (#65, owner decision 2026-09-01: manual command
|
|
285
|
+
first, `gh` detector later).** `tldrx ship` opens the PR carrying the handoff and the watcher card
|
|
286
|
+
lists the signals that would prove the feature works — and nothing joined the two, so
|
|
287
|
+
"merged ≠ deployed ≠ verified" stayed a thing a person had to remember. Remembered wrongly once, it
|
|
288
|
+
destroyed 19 records. `watch check` now reads the run's cards under `05-watch/watchers/` and prints
|
|
289
|
+
each card's `## Signal` items as a numbered checklist with `## Where`, the baseline, what broken
|
|
290
|
+
looks like, and the Query block, so there is one screen to work through after a merge.
|
|
291
|
+
- **A feature id is now OPTIONAL.** `tldrx watch check` checks every card in the run — the shape a
|
|
292
|
+
CI job wants; `tldrx watch check <feature>` scopes it to one and still prints the citation
|
|
293
|
+
verdict `check` always printed. The citation re-check is unchanged and still exits 1 on a dead
|
|
294
|
+
`[src: …]`.
|
|
295
|
+
- **Owners are DERIVED, never invented.** The watcher schema has no `owner:` key and adding one
|
|
296
|
+
would have been a key nothing writes (the #48 defect again). The owner of a signal here is the
|
|
297
|
+
repo its own citation names — `[src: api:src/Leaderboard.cs:64]` says `api` — and an item that
|
|
298
|
+
cites no repo says so rather than borrowing the card's.
|
|
299
|
+
- **Runnable means the card AND the workspace say so.** Only a `$ <cmd> → exit <n>` source whose
|
|
300
|
+
command `.tldrx/workspace.yml` declares, in a repo the item or the card names unambiguously, is
|
|
301
|
+
offered. `--execute` (off by default) re-runs exactly those, through `runDeclaredCommand` — the
|
|
302
|
+
same allowlist, argv-never-a-shell and timeout the stage `cmd` check uses, extracted rather than
|
|
303
|
+
copied — and reports the exit each gets NOW against the exit the card recorded. That is the one
|
|
304
|
+
thing in the framework that catches a card whose `$ … → exit 0` has quietly become an exit 1:
|
|
305
|
+
`resolveSrc` checks a `cmd` source's MEMBERSHIP and takes the exit code on the agent's word.
|
|
306
|
+
- **A `## Query` block is never runnable.** It is KQL or SQL for the console named under
|
|
307
|
+
`## Where`, which tldrx has neither credentials nor a client for, so it is reproduced with its
|
|
308
|
+
fence language and marked print-only.
|
|
309
|
+
- **Three refusals, told apart because they need different actions.** No `05-watch/` at all ("its
|
|
310
|
+
Watch stage never ran"), a Watch stage that wrote no card ("no story reached `done`"), and a
|
|
311
|
+
`draft` card — which is not a failure but an answer: the card's own `absent:` sources are quoted
|
|
312
|
+
back as what to instrument. The two empty cases exit 3, never 0; a green meaning "I read no
|
|
313
|
+
cards" is the failure this command exists to stop.
|
|
314
|
+
|
|
315
|
+
- **Evals v1 — five golden-transcript evals, one per stage (#26, owner decision 2026-09-01: small
|
|
316
|
+
v1 now).** The suite proved the harness; nothing proved a STAGE. `test/evals/` now runs each of
|
|
317
|
+
What, How, Plan, Build and Watch through the real facilitator against the scripted stand-in agent
|
|
318
|
+
and asserts that stage's output CONTRACT. About 5 seconds on top of `bun test`.
|
|
319
|
+
- **One stage at a time, not a chain.** Stages are sequential — Plan cannot run until How's gate
|
|
320
|
+
is signed — so playing four stages to reach the fifth would make every eval depend on the ones
|
|
321
|
+
before it, and a regression in What would turn all five red without any of them saying why.
|
|
322
|
+
Each eval instead opens its own run on a workflow preset holding exactly one stage, which needs
|
|
323
|
+
no new code: `workflowPath` already prefers `.tldrx/workflows/<scope>.yml` over the shipped
|
|
324
|
+
presets, and `normalisePhase` already takes the phase folder from the stage's own `phase:`
|
|
325
|
+
rather than from its position. Whatever the stage genuinely reads — a plan for Build, a done
|
|
326
|
+
story for Watch — is seeded onto the disk by the scenario.
|
|
327
|
+
- **Built on `learn`'s machinery, not beside it.** The stand-in `claude`
|
|
328
|
+
(`src/core/learn/agentScript.ts`, `learnAgent.ts`) and the toy-repo sandbox are reused as they
|
|
329
|
+
are; the real CLI is unreachable by the same two doors `learn` closes. No production code
|
|
330
|
+
changed for this.
|
|
331
|
+
- **Contract, not snapshot.** Nothing compares bytes. The evals assert what the FRAMEWORK
|
|
332
|
+
computes: the declared outputs read back off `gate.requested`, the `checks:` outcomes and their
|
|
333
|
+
computed detail (`checkPlan`'s branch-model line), the artifacts re-parsed with the framework's
|
|
334
|
+
OWN validators (`validateQuestions`, `parseWatcherCard`), and the side effects a stage exists
|
|
335
|
+
for — a branch cut, the story's `dod` block re-run and its exit code recorded, a `--no-ff` merge
|
|
336
|
+
into the epic branch with `main` untouched, and a watcher card's status COMPUTED as `verified`
|
|
337
|
+
when the scenario's front matter claimed `draft`.
|
|
338
|
+
- **A check that is `skipped` for any reason other than being write-time-only now fails an eval.**
|
|
339
|
+
`runCheck` falls through to `unknown check id '<x>'`, which is what a renamed or deleted check
|
|
340
|
+
looks like from outside — and it would otherwise read as a pass.
|
|
341
|
+
- **`EVALS` is load-bearing, not bookkeeping.** A coverage test asserts it names every stage
|
|
342
|
+
under `stages/`, so a sixth stage shipping without an eval turns the suite red and says which
|
|
343
|
+
one is missing.
|
|
344
|
+
- **Every eval was watched failing before it was trusted.** One sabotage each, listed with its
|
|
345
|
+
symptom in `test/evals/README.md`; the Plan one is the shape to aim for — nothing crashed, every
|
|
346
|
+
count still matched, and dropping a cross-epic `depends_on` was still caught because the derived
|
|
347
|
+
branch model changed. The same file says how to add a sixth eval, and what v1 deliberately does
|
|
348
|
+
not cover: failure paths, the agent gate, attended runs, and prompt QUALITY, which needs a real
|
|
349
|
+
model and a judge.
|
|
350
|
+
|
|
351
|
+
- **`tldrx story reopen <id> --for-fix --note "<defect>"` — a sanctioned fix round on a `done` story
|
|
352
|
+
(#58, owner decision 2026-09-01).** Measured on `260829-scoring-leaderboard`: S11's adversarial
|
|
353
|
+
review found a real defect (linkEmail succeeds, setDisplayName fails, the account is permanently
|
|
354
|
+
linked and the score never claimable). It was accepted, small and well understood — and the story
|
|
355
|
+
was already `done`, so the only choices were rejecting the whole Build stage, which destroys
|
|
356
|
+
fourteen good stories' closure, or fixing it outside the story machinery, which leaves an
|
|
357
|
+
epic-level commit with no story provenance. `done` → fix round was the missing arc.
|
|
358
|
+
- **No attempt is consumed.** The mechanism is the one that was already there: `story.reopened` is a
|
|
359
|
+
reset boundary the review ledger reads, so the approve that closed the story stops counting and
|
|
360
|
+
the fix runs as attempt 1 of 2 — with both turns available to it.
|
|
361
|
+
- **The same DoD and the same reviewer.** The story goes back to `todo` and the Build pipeline picks
|
|
362
|
+
it up unchanged; a fix a reviewer refuses twice blocks, exactly as the original would have.
|
|
363
|
+
Nothing is waved through because a human asked for it.
|
|
364
|
+
- **It cannot relitigate scope.** The `--note` names a defect, and `status:` is the ONLY line the
|
|
365
|
+
verb moves on the story file — the acceptance criteria the reviewer will judge against are the
|
|
366
|
+
ones that were already there.
|
|
367
|
+
- **One open fix round per story.** It opens on a `story.reopened` carrying the new `reason: fix`
|
|
368
|
+
and closes when the story is `done` again; a second `--for-fix` while one is open is refused,
|
|
369
|
+
naming who opened it and with which defect. The bound is read from `events.jsonl`
|
|
370
|
+
(`ReviewLedger.fixRound`), so it holds across processes.
|
|
371
|
+
- Refused when the story is **not** `done` (an unfinished story is the plain verb's job), when
|
|
372
|
+
`--note` is missing, and on that second round. `reason` is written on every `story.reopened` now —
|
|
373
|
+
`fix` or `attempts` — and an event without it predates the key and is an `attempts` reopen, the
|
|
374
|
+
only kind that existed.
|
|
375
|
+
- The plain verb's `done` refusal now points at `--for-fix` beside `reject --stage`.
|
|
376
|
+
|
|
377
|
+
### Changed
|
|
378
|
+
|
|
379
|
+
- **tldrx is no longer sold as "lightweight" (owner decision, 2026-09-02).** The word survived from
|
|
380
|
+
the concept doc, where it was true of a proposal. What it now describes is five stages, a
|
|
381
|
+
machine-enforced `[src: …]` grammar that refuses a write it cannot resolve, an adversarial
|
|
382
|
+
reviewer that is never the author, a dual-economy budget ledger, and a written trail from `run
|
|
383
|
+
new` to `retro`, held up by a suite that measured 3093 tests across 108 files on 2026-09-02. One line replaces it everywhere, so
|
|
384
|
+
the npm page, the binary's own `--help`, the plugin manifest, the README and both landing pages
|
|
385
|
+
say the same thing: **an evidence-first, file-based AI development framework: five stages, a gate
|
|
386
|
+
on every one, and every claim cited or refused.**
|
|
387
|
+
- Changed in `package.json`, `src/cli/commands/help.ts` (the first line of `tldrx --help`),
|
|
388
|
+
`plugin/.claude-plugin/plugin.json`, `plugin/skills/tldrx/SKILL.md`, `docs/concept.md`,
|
|
389
|
+
`README.md`, and the docs site's `description`, hero and Spanish mirror.
|
|
390
|
+
- **The status tag moved with it, because it was already stale.** 0.4.0 shipped as `beta`
|
|
391
|
+
(`CHANGELOG.md`, `README.md` release table) while the badge, the README preamble, the binary,
|
|
392
|
+
the plugin manifest, the site's "Where this is" section, both site footers and the generated
|
|
393
|
+
release-notes preamble all still said `alpha`. They say `beta` now. The site's beta bar is
|
|
394
|
+
stated as CLEARED rather than as being worked through, which is what the release table has
|
|
395
|
+
claimed since 0.4.0.
|
|
396
|
+
- `docs/RELEASING.md` gains the thing that made this drift invisible: the `status-…` badge is a
|
|
397
|
+
hardcoded shields.io URL and does **not** update on its own, unlike the npm and CI badges
|
|
398
|
+
beside it. The README's own release paragraph now shows `--tag beta`, because `release.sh`
|
|
399
|
+
writes `alpha` when `--tag` is omitted.
|
|
400
|
+
|
|
401
|
+
- **A watcher card may name a HUMAN owner — optional, per item, never invented (#70).**
|
|
402
|
+
`watch check` derived an owner from each item's own citation: `[src: api:src/Leaderboard.cs:64]`
|
|
403
|
+
→ `api`. That was right for v1 and stays the fallback, but it answers a different question from
|
|
404
|
+
the one #70 asked — which repo EMITS a signal is not who gets paged when it stops.
|
|
405
|
+
- **Additive, in both halves.** An optional front-matter `owner:` (validated only when present,
|
|
406
|
+
so every card already on disk still validates) and an optional `(owner: <name>)` annotation on
|
|
407
|
+
an individual item, placed BEFORE its `[src: …]` token because §2.8 makes that token the last
|
|
408
|
+
thing on the line. Resolution is item → card → repo-derived, and the printed line says WHICH
|
|
409
|
+
it is showing: `owner: alice (declared on the item)` versus the pre-#70 `owner: api`, left
|
|
410
|
+
byte-identical for every card that declares nothing.
|
|
411
|
+
- **Filled from the ledger that already names owners, or not at all.** `tldrx init` parks
|
|
412
|
+
"Who owns `<repo>`?" as an `ownership` question and the answer lands in
|
|
413
|
+
`.tldrx/memory/facts.yml`. The Watch prompt was inlining `observability` and `deploy` facts
|
|
414
|
+
only, so a sub-agent asked for an owner had no honest source and would have invented one;
|
|
415
|
+
`ownership` is now inlined, and the brief says the name may come from nowhere else.
|
|
416
|
+
- **A lost name is an error, not an absence.** `(owner: )` is a card that TRIED to name somebody
|
|
417
|
+
and lost it, so it is a shape issue on the card rather than a silent fall-through to the repo
|
|
418
|
+
— which is the exact substitution the issue is about.
|
|
419
|
+
|
|
420
|
+
- **`docs/guide/08-cli-reference.md` documents `note` and `ship` (#72).** Every command in `COMMANDS`
|
|
421
|
+
had a `## tldrx <cmd>` heading except three; #55 wrote `plan`'s, and these are the other two —
|
|
422
|
+
`ship` being the command that opens the PR at the end of a run, and `note` how an operator records
|
|
423
|
+
something against a stage. `DOCUMENTED_SUBCOMMANDS` in `test/cli.test.ts` grew to cover them, which
|
|
424
|
+
is what keeps each of these gaps a red test rather than a note. Only `hook` is left out, and
|
|
425
|
+
deliberately: its seven scripts are documented as the one `<script>` slot `USAGE_SPELLINGS` already
|
|
426
|
+
records as a spelling.
|
|
427
|
+
|
|
428
|
+
- **`tldrx watch`'s one-line summary says checklist.** It read "List and re-check the watcher cards a
|
|
429
|
+
run produced", which described half of what `watch check` is for and disagreed with the `--help`
|
|
430
|
+
text #65 updated.
|
|
431
|
+
|
|
432
|
+
- **`tldrx ship` opens one PR PER REPO when the branch is in more than one (#66, owner decision
|
|
433
|
+
2026-09-01).** Since #57 a chained multi-repo run cuts ONE integration branch, `epic/<run-id>`,
|
|
434
|
+
with the same name in every repo — so `ship` found it in several, every time, by construction, and
|
|
435
|
+
refused with `pass one: --repo <name>`. The last step of every such run was typing the same command
|
|
436
|
+
once per repo and remembering which ones had already gone through.
|
|
437
|
+
- Same handoff as the body of every PR, the repo name in the title, and every URL listed at the
|
|
438
|
+
end. Each PR opens against **that repo's own** `default_branch`.
|
|
439
|
+
- **One repo is byte-identical**, down to the four lines it prints and the number of processes it
|
|
440
|
+
spawns: the `gh pr list` probe below exists for the multi-repo case and never runs when there is
|
|
441
|
+
only one. The single-repo lines are asserted as exact strings, not substrings — "we did not
|
|
442
|
+
change the common case" is not a claim a `toContain` can make.
|
|
443
|
+
- **A partial failure names both sides.** PR 2 of 3 failing still opens PR 3, and the report lists
|
|
444
|
+
the repos that succeeded with their URLs and the repos that failed with the reason (exit `2`).
|
|
445
|
+
Aborting on the first failure would leave a half-shipped run and no statement of which half.
|
|
446
|
+
- **Re-running is safe.** Before creating, each repo is asked whether an open PR for the branch
|
|
447
|
+
already exists (`gh pr list --head`); one that has is skipped and listed. So the fix for a
|
|
448
|
+
partial failure is `tldrx ship` again and nothing else. A `gh` that fails or answers with
|
|
449
|
+
non-JSON is treated as "there is none", so a transient error can never silently turn a real ship
|
|
450
|
+
into a skip.
|
|
451
|
+
- `--repo` still narrows to exactly one, and `ship` still never pushes: an unpushed branch in one
|
|
452
|
+
repo is that repo's failure line, naming its `git push`, while the other repos' PRs still open.
|
|
453
|
+
|
|
454
|
+
### Fixed
|
|
455
|
+
|
|
456
|
+
- **A training pass with nothing to read is no longer paid for (#101).** `runTraining` had
|
|
457
|
+
exactly one "nothing to work on, refuse before the money" check and it guarded one half of one
|
|
458
|
+
mode: `nothingToMineRefusal` fires only for a ROLE expert with zero minable runs. The CODE pass
|
|
459
|
+
was pushed unconditionally and nothing ever looked at `selection.inlined.length`, so an expert
|
|
460
|
+
whose `## Domain` matched no file on disk spawned a sub-agent, was shown no code at all, and
|
|
461
|
+
wrote a knowledge file about nothing at full price. Measured on the training fixture before the
|
|
462
|
+
fix, with a domain of `src/does-not-exist/`: **`{ code: 0, costUsd: 0.37, wroteKnowledge: true }`**
|
|
463
|
+
— a successful run, priced, from zero input. Live cost, from #94's thread: two near-empty
|
|
464
|
+
trainings at $0.82 each whose "code sweeps found nothing in-domain".
|
|
465
|
+
- **The choice, named: SKIP a dead pass, REFUSE only when none survives.** That is
|
|
466
|
+
`nothingToMineRefusal`'s own idiom — refuse when there is nothing to train from — without
|
|
467
|
+
denying a `--mode full` run that still has one real pass. The skipped pass's reason goes to
|
|
468
|
+
stderr before the money, exactly like the #96 pre-start line, and rides back in
|
|
469
|
+
`TrainOutcome.warnings`; `--prepare` prints it too, and its `N sub-agent(s)` line was already
|
|
470
|
+
derived from the surviving passes so it stays honest by construction.
|
|
471
|
+
- **`roleTraining.ts:79`'s uncovered arm is closed.** Its guard reads
|
|
472
|
+
`if (!isRole || minedFiles > 0) return null`, so a NON-role `--mode full` run against zero
|
|
473
|
+
minable runs was never refused: it spawned a second sub-agent to write
|
|
474
|
+
`- none [src: absent:tldrx-work]` — no evidence, no level, full price. That case is now a
|
|
475
|
+
skip. The role case keeps its own, better-worded refusal untouched, because its runs pass is
|
|
476
|
+
the only pass it has.
|
|
477
|
+
- **It is NOT the #96/#98 preflight and does not touch it.** Underfunded and empty are different
|
|
478
|
+
refusals with different remedies, so they are different checks that never consult each other.
|
|
479
|
+
In particular a skipped pass does not re-divide the ceiling: the share `trainPreflight` priced
|
|
480
|
+
and printed is what the surviving sub-agent gets, and the skipped one is simply not spent.
|
|
481
|
+
- **Exit `1`, deliberately.** `2` is this codebase's MONEY refusal (the `MIN_TRAIN_USD` floor and
|
|
482
|
+
the #96 preflight). Every "you asked for something with nothing behind it" refusal in
|
|
483
|
+
`expert train` is `1` — `missingAreaRefusal`, `lightModeRefusal`, and `nothingToMineRefusal`,
|
|
484
|
+
this check's literal sibling, which `test/training.test.ts` pins as "refused (exit 1)".
|
|
485
|
+
- **One doc correction found while checking that.** `docs/guide/08-cli-reference.md` claimed
|
|
486
|
+
`--mode light` on a role expert exits `2`. It exits `1`: `lightModeRefusal` returns
|
|
487
|
+
`EXIT_USAGE`, and `src/cli/exitCodes.ts` defines `EXIT_USAGE = 1`.
|
|
488
|
+
|
|
489
|
+
- **`expert create` now yields an expert that can actually be trained, and states the `## Domain`
|
|
490
|
+
grammar it will be read under (#94).** Live 2026-09-02: `tldrx expert create discoverer` printed
|
|
491
|
+
*"no areas — every level starts at 0"*, and `tldrx expert train discoverer --area discoverer`
|
|
492
|
+
answered *"has no area (areas: none)"*. Neither sentence said HOW to add one; no flag or
|
|
493
|
+
subcommand existed; the sanctioned path — a block in `competencies.yml` — was discoverable only
|
|
494
|
+
by reading the source. Then the same gap cost money twice more: the hand-written `## Domain`
|
|
495
|
+
bullets came out WORKSPACE-relative (`Scavtopia.Workflows/src/…`) because nothing documents the
|
|
496
|
+
grammar, so **a $2.10 full training earned zero evidence — all 13 of its code citations read
|
|
497
|
+
`outside domain`** — plus two near-empty $0.82 runs whose code sweeps found nothing in domain.
|
|
498
|
+
- **`--area <id>` seeds the first competency area, and `--title <text>` names it.** The title is
|
|
499
|
+
not decoration: light mode greps the words of the area title to choose which files the expert
|
|
500
|
+
is shown (`training/selectFiles.ts`), so `create` prints the default title it chose and names
|
|
501
|
+
`--title`. An area id obeys the same slug rule the expert name does — it is a filename
|
|
502
|
+
(`knowledge/<area>.md`) and half of a copy-pasteable `--area` argument.
|
|
503
|
+
- **Both refusals name the FILE and print the block.** `expert train` on a missing area now
|
|
504
|
+
answers with `.tldrx/experts/<name>/competencies.yml` and the five keys to paste (`id`,
|
|
505
|
+
`title`, `level: 0`, `train_prompt`, `evidence: []`), from one helper both the CLI and
|
|
506
|
+
`runTraining` use, so the two paths cannot drift. `create` with no area says the same thing.
|
|
507
|
+
- **`create` writes the front-matter `repos:`**, read off `.tldrx/workspace.yml` (`repos: []`
|
|
508
|
+
when there is none, rather than a guess). `expert.md` had no such key at all, so a
|
|
509
|
+
hand-created expert declared nothing about which repos its `## Domain` bullets are relative
|
|
510
|
+
to — and that is exactly the thing the bullets are relative to.
|
|
511
|
+
- **The created `expert.md` states the grammar in its own `## Domain` section**: bullets are
|
|
512
|
+
repo-RELATIVE with no repo prefix, a whole-repo claim is `` - repo `api` ``, citations arrive
|
|
513
|
+
as `repo:path:line` so `api:src/Checkout/Cart.cs:12` is matched by `` `src/Checkout/` `` and
|
|
514
|
+
**not** by `` `api/src/Checkout/` ``. It is written as prose, deliberately: a worked example
|
|
515
|
+
written as a BULLET would itself be parsed as a declared domain path, and an expert that
|
|
516
|
+
silently claims `api/src/Checkout` puts every real citation outside its own domain — the bug,
|
|
517
|
+
not the fix. Pinned by a test that the created file still parses to zero domain paths.
|
|
518
|
+
- **The `outside domain` warning names the grammar FIRST** when the cited path would match with
|
|
519
|
+
one leading segment dropped from either side — the bullet carrying a repo prefix it should
|
|
520
|
+
not, or the citation being workspace-relative where a `repo:path:line` was wanted. It says the
|
|
521
|
+
measured fact (`X` without its first segment DOES contain this path), not a diagnosis.
|
|
522
|
+
- **And naming another expert no longer reads as exclusivity.** *"train that expert on it
|
|
523
|
+
instead"* was measured false: overlap between domains is legal, and the hint fires whenever
|
|
524
|
+
this expert's own paths miss. It now reads *"also declares a domain that contains it, so it
|
|
525
|
+
may be the better home for this one (overlap is legal: this fired because none of `<expert>`'s
|
|
526
|
+
own paths match)"*.
|
|
527
|
+
- **`init/loadWorkspaceFile.ts` reads `.tldrx/workspace.yml`'s name from `core/paths.ts`**
|
|
528
|
+
(`PROJECT_WORKSPACE_FILE`, the constant #92 added) instead of from `init/runInit.ts`;
|
|
529
|
+
`runInit` keeps exporting `WORKSPACE_FILE` under that name, aliased to the same string, so no
|
|
530
|
+
import site changed. `loadWorkspaceFile` is the small leaf several commands read the repo list
|
|
531
|
+
through, and taking that one constant off `runInit` dragged the whole of `init` — detection,
|
|
532
|
+
map providers, the MCP probe — into every module that read it: `dist/hooks/session-start.js`
|
|
533
|
+
went 37,937 → 54,467 bytes and blew the 50 KB entry-point cap `test/build.test.ts` enforces.
|
|
534
|
+
It is back to 37,937.
|
|
535
|
+
- Documented in `docs/spec.md` §2.3 and §2.8, `docs/guide/04-experts.md`,
|
|
536
|
+
`docs/guide/08-cli-reference.md`, `docs/guide/09-troubleshooting.md` and both docs-site expert
|
|
537
|
+
guides — the grammar had never been written down anywhere outside `expertDomain.ts`.
|
|
538
|
+
|
|
539
|
+
- **`release.sh` ran the gate AFTER pushing the release commit to `main` (#100).** The order was
|
|
540
|
+
commit → push `main` → `release-check.sh` → tag → push tag, so every red item the gate has —
|
|
541
|
+
tests, typecheck, build, the `Bun.*` seam grep, "tag already exists", "already on npm", "tree
|
|
542
|
+
not clean" — landed a `release: X.Y.Z` commit on `origin/main` carrying a **dated** CHANGELOG
|
|
543
|
+
heading and a **dated** README row, with no tag behind it. That is precisely the half-released
|
|
544
|
+
state checklist item 4 exists to prevent, and it costs a revert commit on `main` or a
|
|
545
|
+
hand-repaired CHANGELOG to undo. The order is now commit → **gate** → push `main` → tag → push
|
|
546
|
+
tag: a red gate leaves `origin/main`, the tags and npm untouched, the whole of the damage is one
|
|
547
|
+
local commit, and the script says so and prints the one command that drops it.
|
|
548
|
+
- **The gate did not have to be weakened to move it.** Of its items only "in sync with
|
|
549
|
+
`origin/main`" assumes the push already happened; run against an unpushed release commit it
|
|
550
|
+
would be permanently red, which is the trap in the naive reorder. `release-check.sh` takes a
|
|
551
|
+
new `--pre-push` that restates that one item as **"`origin/main` is HEAD's parent"** — the same
|
|
552
|
+
assertion (nobody moved `main` under you, nothing but the release commit is unpushed) for a
|
|
553
|
+
tree that has not pushed yet, and one notch stronger, since it also refuses a second unpushed
|
|
554
|
+
commit. Items 1–3 already needed the edits, which exist by then; "working tree clean", "on
|
|
555
|
+
main", "tag does not exist", "not on npm", typecheck, tests, build and the seam grep are
|
|
556
|
+
untouched and all still run. **No flag, no default, no CI path changed**: bare
|
|
557
|
+
`release-check.sh` (the `release-gate-hook.sh` PreToolUse deny) and `--ci` (publish.yml)
|
|
558
|
+
behave exactly as before, and there is a test for each.
|
|
559
|
+
- **`sed -i ''` was macOS-only**, which is why this path had never had a test: measured against
|
|
560
|
+
GNU sed 4.9 in `debian:stable-slim`, `sed -i '' -E …` exits **2** with `sed: can't read
|
|
561
|
+
s/^## …/: No such file or directory`, so `release.sh` could not run on the ubuntu CI runner at
|
|
562
|
+
all. Both substitutions now go through a temp file outside the tree and are `cat` back (inode,
|
|
563
|
+
mode and a clean working tree preserved) — byte-identical output on BSD sed, and it runs on
|
|
564
|
+
GNU sed, which is what lets `test/release-gate-order.test.ts` hold the ordering in CI. The new
|
|
565
|
+
file runs the real two scripts against a real bare "origin" with only `bun` and `npm` stubbed,
|
|
566
|
+
and reads the ordering off the shas the gate actually ran against: pre-fix, HEAD and
|
|
567
|
+
`origin/main` were EQUAL at gate time; now they differ by the release commit.
|
|
568
|
+
|
|
569
|
+
- **The prose was validated against the binary, top to bottom, and a lot of it was false.**
|
|
570
|
+
The last docs QA (3ee3723) was a release ago; `watch check`/`watch arm`, `update` and
|
|
571
|
+
its notice, the `drive` preflight, `plan schema`, `questions cards`, `story reopen --for-fix`,
|
|
572
|
+
the host-token economy and the dashboard's new sources have all landed since. Every claim in
|
|
573
|
+
`README.md`, all thirteen English pages, all thirteen Spanish pages and `CONTRIBUTING.md` was
|
|
574
|
+
cold-read for lies first, then checked against `tldrx <cmd> --help` and the source. What was
|
|
575
|
+
found was not drift at the edges — most of it was load-bearing.
|
|
576
|
+
- **A flag that does not exist.** The README's cost section explained what `--max-budget-usd`
|
|
577
|
+
does. That is the flag tldrx passes DOWN to `claude`, not one a user may type: `tldrx next
|
|
578
|
+
--max-budget-usd 5` answers `unknown flag`. The user-facing flag is `--max-usd`.
|
|
579
|
+
- **"Every run-targeting command exits `2`" was wrong twice**, in the README, the CLI reference
|
|
580
|
+
and the FAQ. `tldrx cost` refuses at exit **1** (`cost.ts` returns `EXIT_USAGE`, and its own
|
|
581
|
+
`--help` never lists a 2), and `tldrx run status` does not refuse at all — it lists every open
|
|
582
|
+
run and exits **0**, which is the whole point of it, since it is the screen you read to find
|
|
583
|
+
the id the others are asking for. All three pages also named five commands as taking a
|
|
584
|
+
positional `<run>`; the real split is that most take either form, `replay` and `retro` take
|
|
585
|
+
the positional only (`--run` there is an unknown flag, measured), and seven take `--run` only.
|
|
586
|
+
- **A documented command that silently creates a directory where you asked for a file.**
|
|
587
|
+
`tldrx dashboard --static --out ./somewhere/page.html` was in the reference. `--out` is a
|
|
588
|
+
directory and the filename is not negotiable (`writeStatic.ts` joins `index.html` and
|
|
589
|
+
`mkdirSync`s the path), so that line makes a directory literally named `page.html`.
|
|
590
|
+
- **A required tool the Quickstart did not name.** `env.yml` declares four `required: true`
|
|
591
|
+
tools; the install page named Node, `git` and `claude` and never mentioned **Bun ≥ 1.3**, so a
|
|
592
|
+
reader who followed it exactly got `tldrx doctor` exit 1 and no explanation.
|
|
593
|
+
- **The `tldrx learn` chapter list was stale AND swapped** on both language Quickstarts: 6 and 7
|
|
594
|
+
are `the agent gate` and `attended`, and the pages had two other titles in the other order —
|
|
595
|
+
so `--chapter 7` sent a reader looking for the agent gate to the wrong chapter.
|
|
596
|
+
- **Counts that had grown.** The `.gitignore` block `init` writes excludes **eight** paths, not
|
|
597
|
+
the five the README listed. The `[src: …]` grammar has **eight** kinds, not the seven the
|
|
598
|
+
evidence table showed — `aidlc:` was missing while the page's own next paragraph said
|
|
599
|
+
"eight". The What, How and Plan stages each declare `questions.md` as an output and Plan also
|
|
600
|
+
declares `budget.yml`; the stages table showed none of them, which matters because the auto
|
|
601
|
+
gate is measured on exactly that file. `tldrx init` seeds **three** kinds of expert — role,
|
|
602
|
+
stack and **domain** — and both expert sections said two.
|
|
603
|
+
- **Twelve `workflows/*.yml` said the auto gate has "the five spec §5 conditions"** and listed
|
|
604
|
+
five. `evaluateAutoGate` has had **seven** since the stories and boundary conditions landed.
|
|
605
|
+
- **The `tldrx drive` mandate's preflight (#84) was absent from every page that enumerates what
|
|
606
|
+
the mandate carries**, in both languages, and the English driving guide still taught the
|
|
607
|
+
three-command recipe the preflight replaced.
|
|
608
|
+
- **`CONTRIBUTING.md` overstated the ref guard and understated the lock.** The guard refuses a
|
|
609
|
+
ref move while ANOTHER invocation holds the lock — the holding run's own git children carry
|
|
610
|
+
`MW_LOCK_TOKEN` and pass, which is what lets the wave commit and push through its own guard;
|
|
611
|
+
as written it would have refused the merge it exists to protect. `merge-wave.sh`'s seven exit
|
|
612
|
+
codes were documented nowhere, in a file whose own rule is to read exit codes. The lock's wait
|
|
613
|
+
is bounded (`MW_LOCK_WAIT_S`, default 3600 s, then exit 6) and `CLAUDE.md` implied it was not.
|
|
614
|
+
- **The Bun/Node seam rule was stated more broadly than it is enforced**, in `CONTRIBUTING.md`,
|
|
615
|
+
`CLAUDE.md` and `docs/RELEASING.md`: the grep scans `src` only, and `scripts/build.ts` calls
|
|
616
|
+
`Bun.build` for a living. A contributor reading it literally files a bug against the build.
|
|
617
|
+
- **`publish.yml` does not "run the same check".** `release-check.sh --ci` skips items 4 and 5
|
|
618
|
+
entirely; the seam check runs in no CI workflow at all. Said plainly now, in both the README
|
|
619
|
+
and `docs/RELEASING.md`.
|
|
620
|
+
- **Four translated strings that had to stay English** in the Spanish mirror: two `--note`
|
|
621
|
+
samples, the `## Qn · Title` heading the §2.7 parser matches on, and `$0.00 of $25.00`, which
|
|
622
|
+
is what the dashboard actually renders.
|
|
623
|
+
- Fixed on the way: the `0.5.0` CHANGELOG section had grown **two `### Fixed` groups** and had
|
|
624
|
+
two additions (Evals v1, `story reopen --for-fix`) filed under `### Removed` — an artefact of
|
|
625
|
+
unioning sibling branches. One group per kind now, in the order `0.3.0` used, with every entry
|
|
626
|
+
byte-identical and only the duplicate heading gone.
|
|
627
|
+
- Two defects found while checking and **filed rather than fixed**, both outside this pass:
|
|
628
|
+
[#99](https://github.com/ederwii/tldr-experts/issues/99) `test/merge-wave.test.ts` asserts over
|
|
629
|
+
a shared `TMPDIR`, so a sibling process fails it (measured: red under load, green alone, and
|
|
630
|
+
green for the full suite with no sibling), and
|
|
631
|
+
[#100](https://github.com/ederwii/tldr-experts/issues/100) `release.sh` pushes the release
|
|
632
|
+
commit to `main` before it runs `release-check.sh`.
|
|
633
|
+
|
|
634
|
+
- **`expert train` says what it is about to spend, on which model, BEFORE it spends it (#96).**
|
|
635
|
+
Live 2026-09-02: `tldrx expert train discoverer --area discoverer --mode full` inherited the
|
|
636
|
+
claude CLI's last-used model — `fable-5`, a premium tier — and ran against the default ceiling.
|
|
637
|
+
Full mode splits that ceiling between its two sub-agents, so the code pass was handed $1.00. It
|
|
638
|
+
died with `Reached maximum budget ($1)` at 54 s, **$1.31 spent and recorded, nothing written to
|
|
639
|
+
`competencies.yml`.** Three defaults compounded and not one of them was said out loud.
|
|
640
|
+
- **A pre-start line names the model, its tier, and where the name came from** — `--model`,
|
|
641
|
+
`$ANTHROPIC_MODEL`, or a `model:` key in `.claude/settings.local.json` /
|
|
642
|
+
`.claude/settings.json` / `~/.claude/settings.json`: `model claude-fable-5[1m] (premium,
|
|
643
|
+
inherited from your claude CLI via ~/.claude/settings.json) — pass --model to override ·
|
|
644
|
+
--mode full · $3.00 across 2 sub-agent(s), $1.50 each`. When nothing on the box says which
|
|
645
|
+
model the CLI will pick, the line says THAT rather than inventing a tier.
|
|
646
|
+
- **A refusal — exit `2`, nothing spawned, nothing spent — when the share cannot fit and the
|
|
647
|
+
ceiling is the DEFAULT one.** The test is arithmetic, not a category: does the money one
|
|
648
|
+
sub-agent gets reach what one pass on that tier costs? Measured full trainings run
|
|
649
|
+
**$1.21–$1.60 end to end on a mid model** (two `training.jsonl` lines on `aparece-platform`,
|
|
650
|
+
`docs/audits/2026-08-29/experts-knowledge.md` §E, plus the top of the band in #96), so one
|
|
651
|
+
pass is ~$0.70 mid and ~$1.76 premium (opus lists at 2.5x sonnet). Full mode's $1.50 a pass
|
|
652
|
+
does not reach it. The refusal names both remedies — `--model sonnet`, or `--max-usd <n>`.
|
|
653
|
+
- **An explicit `--max-usd` is never refused.** The operator looked at the number; that is the
|
|
654
|
+
whole decision this check exists to ask for. It warns in one line and proceeds.
|
|
655
|
+
- **The full-mode default `--max-usd` is now $3.00**, up from the $2.00 light mode still uses,
|
|
656
|
+
because full mode pays for two sub-agent passes and the one repair round a rejected knowledge
|
|
657
|
+
file earns comes out of the same share. $1.50 a pass is ~2x the measured per-pass midpoint.
|
|
658
|
+
Deliberately not scaled higher for a premium model: `--max-budget-usd` is a stop after the
|
|
659
|
+
turn, not a cap (spec §2.6.1 — a $1.50 ceiling has realised $5.15), so a bigger default cannot
|
|
660
|
+
make a premium turn affordable, only more expensive to lose. `budget-gate` prices `--mode
|
|
661
|
+
full` at $3.00 to match.
|
|
662
|
+
- Salvaging the partial evidence a budget death throws away is a design call and was NOT built;
|
|
663
|
+
the question is asked on #96.
|
|
664
|
+
|
|
665
|
+
- **`expert train --prepare` carries that check into the bundle (#98).** The check above was wired
|
|
666
|
+
to the spawning path only, and `--prepare` spawns nothing — but it writes the ceiling into
|
|
667
|
+
`pending.json` (`max_budget_usd: 1.50`) and into the prompt text (`Ceiling for this sub-agent:
|
|
668
|
+
$1.50`), and a host session then spends against it. Measured on `b5d59c5`: a full `--prepare` with
|
|
669
|
+
a premium model inherited from `~/.claude/settings.json` printed `$1.50 ceiling each`, returned
|
|
670
|
+
`preflight: null`, and wrote two bundles saying `model: null` — the same trap, one command later,
|
|
671
|
+
on the host's money.
|
|
672
|
+
- The model line and any warning now go to **stdout with the prepared block** and into an optional
|
|
673
|
+
**`preflight`** key on each bundle's `pending.json`. Absent when there is no warning, so an
|
|
674
|
+
unremarkable bundle stays byte-identical to the one this command has always written.
|
|
675
|
+
- **What it may CLAIM differs by half.** Headless, tldrx spawns `claude` with no `--model`, so the
|
|
676
|
+
CLI's default is a prediction about a process this code starts: the refusal stands. On
|
|
677
|
+
`--prepare` an **explicit `--model`** is an instruction written into the bundle and is refused
|
|
678
|
+
the same way (exit `2`, no bundle written); an **inherited** model only warns, naming both
|
|
679
|
+
remedies — refusing a bundle over a settings-file key, for a session tldrx does not control,
|
|
680
|
+
would be asserting more than is known. `--commit` says nothing: that money is already spent.
|
|
681
|
+
|
|
682
|
+
- **`test/merge-wave.test.ts` no longer fails a wave over another wave's log directory (#95, #97).**
|
|
683
|
+
`merge-wave.sh` writes its logs to `${TMPDIR:-/tmp}/mw-$$`, and the test that asserts a green run
|
|
684
|
+
cleans up after itself diffed a listing of the machine's SHARED tmpdir. Every wave on the box
|
|
685
|
+
writes there — this file's other tests, every sibling agent's wave, and the real merge wave whose
|
|
686
|
+
`bun test` is running this very file — so the assertion was about ONE run's cleanup and measured
|
|
687
|
+
the whole machine. It went red twice on trees whose diff touches neither the script nor its tests:
|
|
688
|
+
**#95**, on a sibling's DELIBERATELY KEPT red-wave log (`mw-35458`, carrying another run's
|
|
689
|
+
`poison.txt` merge — merge-wave keeps a failed run's logs on purpose, "every FAIL above names the
|
|
690
|
+
directory it kept"); and **#97**, on a CONCURRENT invocation's LIVE `mw-15412`, twelve minutes into
|
|
691
|
+
#90's wave, with `main` left at an unpushed merge commit. Both cost a full re-gate, and the same
|
|
692
|
+
trees passed standalone.
|
|
693
|
+
- **Every invocation now gets a private `$TMPDIR` inside its own sandbox**, so "did THIS run clean
|
|
694
|
+
up after itself" has an answer that does not depend on what else the box is doing. Per
|
|
695
|
+
invocation, not per sandbox: this file deliberately runs two waves at once. The scan is read only
|
|
696
|
+
after the run has exited, so anything left in that root is a genuine leftover and no liveness
|
|
697
|
+
rule has to be guessed at — a concurrent wave's live `mw-<pid>` is, correctly, invisible from
|
|
698
|
+
there.
|
|
699
|
+
- **What the test asserts about merge-wave is unchanged.** A green run that stops removing its log
|
|
700
|
+
directory still fails it, and a red run that stops keeping the one it names still fails it — both
|
|
701
|
+
re-measured by mutating `scripts/merge-wave.sh` and watching the assertions fire.
|
|
702
|
+
- **A red run's kept logs now leave with the sandbox.** They were being written to the machine's
|
|
703
|
+
tmpdir and never removed — 1300 `mw-*` directories had accumulated there by the time #95 was
|
|
704
|
+
filed, including the one it tripped on.
|
|
705
|
+
|
|
706
|
+
- **A story's first attempt is dispatched at what the plan priced it, not at that figure halved
|
|
707
|
+
again (#91).** `03-plan/budget.yml` has been read since 2026-08-30, but the price it carries was
|
|
708
|
+
divided by the worst case ONE story can be asked for — `MAX_ATTEMPTS × (1 + REVIEWER_SHARE)` =
|
|
709
|
+
2.5 — before a single attempt had run. Measured on run `260901-leaderboard-v2` (finding F-4):
|
|
710
|
+
Delivery priced S2 at $2.10 of a $3.85 Build stage and the developer was dispatched under
|
|
711
|
+
**$0.84**. A deliberately-atomic large story starved on the one attempt that mattered while
|
|
712
|
+
trivial ones carried slack, and the plan's own measured pricing — the whole point of writing the
|
|
713
|
+
file — was thrown away in the arithmetic that read it.
|
|
714
|
+
- **`developerPriceDivisor(attempt)`** now decides it. Attempt 1 is the pass Delivery priced and
|
|
715
|
+
gets `price / (1 + REVIEWER_SHARE)` — the whole price less the reviewer's derived quarter, so
|
|
716
|
+
S2's ceiling goes $0.84 → **$1.68**. Attempt 2 is a contingency nobody priced and keeps the
|
|
717
|
+
pre-#91 figure, `price / (MAX_ATTEMPTS × (1 + REVIEWER_SHARE))`. No attempt is ever handed less
|
|
718
|
+
than it was handed before.
|
|
719
|
+
- **The even split is untouched.** A plan with no `budget.yml`, one that does not validate, one
|
|
720
|
+
priced in `host-tokens`, and any story the plan did not name all still get
|
|
721
|
+
`stage / (stories × attempts × 1.25)`, pinned by tests that were green before this change and
|
|
722
|
+
are green after it.
|
|
723
|
+
- **What this costs, stated.** The worst case one PRICED story can be asked for goes from
|
|
724
|
+
`0.8 × price` to `1.2 × price`. The phase ceiling is metered once, at stage entry —
|
|
725
|
+
`runNext.runExecutor` skips the brake while a stage is `running` — so nothing re-checks the
|
|
726
|
+
envelope between two spawns of the same headless `runAll`; that is the window
|
|
727
|
+
`REVIEWER_FLOOR_USD` already opens by design. `priceScale` still holds the sum of the declared
|
|
728
|
+
prices inside the stage, and `remainingWork` still clamps the brake's estimate to the stage's
|
|
729
|
+
own price, so the brake can never refuse more often than it used to. The measured REMAINDER of
|
|
730
|
+
a story's price would be tighter on the second attempt, and is recoverable (`agent.result`
|
|
731
|
+
carries `key` = the story id and a row-level `cost_usd`) — it is not read, because the
|
|
732
|
+
budget-gate hook's `remainingWork` would have to read the same ledger on its hot path to stay
|
|
733
|
+
in step, and the worst case is `0.8 + 0.4` either way.
|
|
734
|
+
- **`remainingWork` mirrors the schedule, turn by turn.** `RemainingStory` gains
|
|
735
|
+
`developerCapsUsd` — one cap per turn still to dispatch, in the order they run — because
|
|
736
|
+
`cap × turns` stopped being the truth. The turns still to run are the LAST of the story's run
|
|
737
|
+
of attempts, so a story with one attempt behind it is priced as the attempt 2 it is about to
|
|
738
|
+
become. `renderRemainingWork` prints `dev $1.20 ×2` while the turns cost the same and
|
|
739
|
+
`dev $3.60 + $1.80` once they do not, so the total on the line still adds up. A test asserts
|
|
740
|
+
the mirrored divisor equals the executor's own, as it already did for the three constants.
|
|
741
|
+
|
|
742
|
+
- **The merge lock now has something to say about raw git in the shared checkout (#89).** The
|
|
743
|
+
lock serialises merge-wave INVOCATIONS; it never serialised git. Measured 2026-09-02: while
|
|
744
|
+
agent A's gates were running, agent B typed `git reset --hard origin/main` into the same
|
|
745
|
+
shared checkout — reflog `reset: moving to origin/main` — and A's merge commit stopped being
|
|
746
|
+
reachable from `main` mid-gate. #44's gated-HEAD assertion fired for the first time in anger
|
|
747
|
+
and refused to push. The aftermath was caught; the damage was not prevented.
|
|
748
|
+
- **`scripts/merge-guard.sh`, installed as a `reference-transaction` hook on every
|
|
749
|
+
merge-wave run.** That is the only hook git will let abort work in progress: it fires for
|
|
750
|
+
every reference update and, in its `prepared` state alone, aborts the whole transaction on
|
|
751
|
+
a non-zero exit. Measured on git 2.50.1 with the guard refusing — `git reset --hard`,
|
|
752
|
+
`git checkout -B main`, `git merge --no-ff`, `git commit` (including `--no-verify`, which
|
|
753
|
+
does **not** bypass it) and `git update-ref` all exit 128 with `fatal: ref updates aborted
|
|
754
|
+
by hook`, and **the ref does not move**. There is no pre-reset, pre-checkout or
|
|
755
|
+
pre-merge-anything hook; this is the strongest mechanism git offers without a daemon.
|
|
756
|
+
- **Where the line honestly lies, because it is not where it looks.** `git reset --hard`
|
|
757
|
+
writes the WORKING TREE before it opens any ref transaction — measured: the file content
|
|
758
|
+
had already changed to the target commit's while HEAD still pointed at the old one. So the
|
|
759
|
+
guard saves the **commit**, which is what #89 lost, and cannot save the checked-out files;
|
|
760
|
+
a wave whose worktree is clobbered mid-gate still gates a tree nobody meant it to, and its
|
|
761
|
+
gated-HEAD assertion will not fire, because HEAD is exactly where it left it. `git merge`
|
|
762
|
+
is the better case — refused before the worktree is touched at all. And a hook in a
|
|
763
|
+
checkout is bypassable by anyone willing to: `git -c core.hooksPath=…`, deleting the hook,
|
|
764
|
+
or exporting `MW_LOCK_TOKEN`. It is built against accidents, not intent.
|
|
765
|
+
- **The lock is the sentinel; the marker is what that lock looks like to a human.**
|
|
766
|
+
`merge-wave.sh` writes `.MERGE-WAVE-IN-PROGRESS` at the root of the shared checkout naming
|
|
767
|
+
the branch, the owning pid and host, and the way back in. Gitignored, so a wave cannot trip
|
|
768
|
+
its own dirty-tree guard; removed by `release()` on every path out, INT and TERM traps
|
|
769
|
+
included. The guard keys on the LOCK rather than on the marker, because only the lock
|
|
770
|
+
carries an owner and only an owner can be tested for death — a SIGKILLed wave must not
|
|
771
|
+
leave a file behind that wedges the checkout.
|
|
772
|
+
- **Scope, so an agent's own work is never in the way.** In the shared checkout every ref
|
|
773
|
+
update is refused while a foreign wave holds the lock. From a linked worktree only
|
|
774
|
+
`refs/heads/main` is — refs live in the common dir, so `git update-ref refs/heads/main`
|
|
775
|
+
typed in a worktree destroys the wave just as thoroughly (measured: it succeeds, unguarded).
|
|
776
|
+
The holder's own git is recognised by an exported token written inside the lock, so
|
|
777
|
+
merge-wave cannot deadlock itself, and a lock whose owner is dead is ignored exactly as the
|
|
778
|
+
waiting loop already ignores it — `scripts/merge-lock.sh` is that one vocabulary, shared by
|
|
779
|
+
writer and guard so the two cannot drift apart.
|
|
780
|
+
- **The state name git passes is not portable, and assuming it was shipped a red CI.** macOS
|
|
781
|
+
git 2.50.1 calls the abortable state `prepared`; the Linux runner's git calls it something
|
|
782
|
+
else (`fatal: in 'preparing' phase, update aborted by the reference-transaction hook`, run
|
|
783
|
+
33589554234 on `e0e76a2`). The guard's `*)` arm printed a usage error and exited 2, so on
|
|
784
|
+
that machine every ref update in every sandbox was refused and the whole merge-wave suite
|
|
785
|
+
went red at once while macOS stayed green — #49's failure shape exactly. It now recognises
|
|
786
|
+
`committed` and `aborted` and treats **everything else** as the prepare state, which cannot
|
|
787
|
+
repeat the blanket refusal: git honours the exit code in the prepare state only, so an
|
|
788
|
+
unrecognised name is either handled correctly or ignored. The test asks git which states it
|
|
789
|
+
passes rather than hardcoding them, and additionally probes the names this machine does NOT
|
|
790
|
+
use — the only way the portability property is testable anywhere.
|
|
791
|
+
|
|
792
|
+
- **And the convention it only approximates is now written down**, verbatim and identically,
|
|
793
|
+
in `CONTRIBUTING.md` and `CLAUDE.md`: agents touch the shared checkout ONLY through
|
|
794
|
+
`scripts/merge-wave.sh`, every other piece of work happens in their own worktree, and never
|
|
795
|
+
`git reset --hard` or `git checkout -B main` in the shared checkout. A test asserts both
|
|
796
|
+
files still say it.
|
|
797
|
+
|
|
798
|
+
- **A cancelled run is no longer told to retry itself (#86).** `tldrx run cancel` is the way to
|
|
799
|
+
close a run whose stage FAILED — that is the case `run.cancelled` exists for, and it is a
|
|
800
|
+
run-level field precisely so the stages keep their failure, "which is history, not state".
|
|
801
|
+
`deriveRunStatus` read that decision first; `waitingFor` could not read it at all. It is typed
|
|
802
|
+
against `WaitingRun`, the smallest shape both `RunFile` and the dashboard's tolerantly-read
|
|
803
|
+
`RunDocument` satisfy, and that shape had `cursor` and `phases` and nothing else — so the answer
|
|
804
|
+
came from the status of the stage at the cursor, which a cancellation deliberately leaves
|
|
805
|
+
`failed`. A run somebody had closed reported `waiting.kind: failed` and
|
|
806
|
+
`"… FAILED — retry: \`tldrx next\`"`, counted as movable, and could wear `← next` on the
|
|
807
|
+
dashboard. Both screens, because both have read the same derivation since #60.
|
|
808
|
+
- `WaitingRun` grows an optional `cancelled`, `RunDocument` projects it, and `waitingFor` reads
|
|
809
|
+
it FIRST — before the cursor checks, in the same order and for the same reason
|
|
810
|
+
`deriveRunStatus` does. A ninth waiting kind, `cancelled`, joins `WAITING_KINDS`; it is not in
|
|
811
|
+
`MOVABLE_KINDS`, so `tldrx status` stops offering the run and the dashboard raises no card.
|
|
812
|
+
- The message names **who closed it, when, and their note** — three facts that were dropped
|
|
813
|
+
everywhere, on the one screen that exists to say what a run needs. It offers no command:
|
|
814
|
+
`tldrx next` on a cancelled run already advances nothing, and printing it is what made an
|
|
815
|
+
operator think there was something left to do.
|
|
816
|
+
|
|
817
|
+
- **An out-of-order `--commit` no longer leaves a `stage.started` behind (#87).** `runExecutor`
|
|
818
|
+
stamped the stage `running` and appended `stage.started` on its way in, before the executor had
|
|
819
|
+
had a chance to say anything. #82 restores the stage's STATUS when that executor turns out to be
|
|
820
|
+
refusing a sequencing mistake, so `run.yml` comes back byte for byte — but `events.jsonl` is
|
|
821
|
+
append-only truth and the line could not be unwritten. The live run `260901-leaderboard-v2` left
|
|
822
|
+
one at 2026-09-02T00:36:37Z: a `stage.started` with no matching `stage.done` or `stage.failed`,
|
|
823
|
+
which `renderReplay` and anything counting starts read as a start. A `--commit` never STARTS a
|
|
824
|
+
stage — it settles a cycle a `--prepare` started, and on the ordinary commit the stage is
|
|
825
|
+
already `running` and no event was emitted anyway — so it no longer emits one at all. The
|
|
826
|
+
`markRunning` STAMP stays, keeping the original `started_at`, so a `--commit` that does settle a
|
|
827
|
+
cycle on a stage something demoted still finishes through. The refusal now precedes the event by
|
|
828
|
+
construction: there is no event. Pinned by measuring the WHOLE log, not a `stage.started` count.
|
|
829
|
+
|
|
830
|
+
- **An unreadable `result.json` is refused like an absent one, loudly (#88).** Owner decision,
|
|
831
|
+
2026-09-02. #82 split `PendingError` into `absent` (the host has not written the file) and
|
|
832
|
+
`unreadable` (the host wrote it and got it wrong), and left the second failing the stage. But
|
|
833
|
+
nothing was attempted there either — no sub-agent ran, no cent moved, no branch changed — and
|
|
834
|
+
the fix is the same single command: rewrite the file, run `tldrx next --commit` again. Failing
|
|
835
|
+
it actively OBSTRUCTED that fix, because it demoted the stage out of `running`, and the
|
|
836
|
+
phase-budget gate is skipped exactly when a stage is `running` — which is how #82's live run
|
|
837
|
+
took a `budget.blocked` it had not earned. A host that fat-fingers its JSON paid the same tax.
|
|
838
|
+
It is #79's model — FORM never costs an attempt, CONTENT/WORK always does — applied to run
|
|
839
|
+
state.
|
|
840
|
+
- Corruption does not pass silently. Both call sites in `build.ts` now go through one door that
|
|
841
|
+
appends a typed **`result.unreadable`** event naming the run-dir-relative path, the parser's
|
|
842
|
+
own message, the role (`developer` or `reviewer`) and the story, and the refusal line says
|
|
843
|
+
which file to rewrite and which command to run. It is the only thing a sequencing refusal ever
|
|
844
|
+
writes; `run.yml` still comes back byte for byte, and `tldrx replay` renders the line.
|
|
845
|
+
- `parseReview`'s fail-closed rule is untouched and is a different, harsher contract: it governs
|
|
846
|
+
an envelope that PARSES and is not a valid verdict (unreadable ⇒ `changes`, never `approve`).
|
|
847
|
+
A file that does not parse at all never reaches it.
|
|
848
|
+
|
|
849
|
+
- **Watch READS the branch it diffs off `run.yml`; it used to derive one (#90).** The watch stage
|
|
850
|
+
diffed `feature.epic?.branch` — the `branch:` an epic file DECLARES, written at Plan time before a
|
|
851
|
+
line of code exists. Under the integration branch model (#57) the Build executor deliberately
|
|
852
|
+
ignores that value: every epic's stories merge into one `epic/<run-slug>` branch and the epic
|
|
853
|
+
stays in the plan as a label. So on `260901-leaderboard-v2` the prompt told BOTH watchers,
|
|
854
|
+
verbatim, that `epic/leaderboard-v2-api` *"does not resolve in scavtopia-workflows. Treat this
|
|
855
|
+
feature's code as UNSEEN — cite `absent:` rather than guessing at what it emits"* — about a branch
|
|
856
|
+
nothing had ever cut, while `build.epic_branch` recorded the real one three lines away in the same
|
|
857
|
+
run. An obedient watcher would have written an all-`absent:` card, and **that card PASSES
|
|
858
|
+
`claim-sources`**, because an `absent:` citation resolves by construction. Confident, validated,
|
|
859
|
+
useless coverage; caught only because the host's brief carried the real branch independently.
|
|
860
|
+
- **The branch now comes from the run's record and nowhere else** (`core/watch/recordedBranch.ts`).
|
|
861
|
+
`branch_model: integration` → the run's one recorded branch, for every feature. `per-epic`, and
|
|
862
|
+
every run written before `branch_model` existed → the recorded LIST, with the epic's declaration
|
|
863
|
+
used *only as a key into it*. A declaration is never returned as a branch: either it is in the
|
|
864
|
+
record and the record's entry is what is returned, or the answer is "unrecorded".
|
|
865
|
+
- **A branch the record CLAIMS and the repo cannot find is now a refusal**, not an instruction.
|
|
866
|
+
It is the run contradicting itself, so the stage refuses before anything spawns (`refused`, so
|
|
867
|
+
the stage goes back to `ready`), naming the recorded value, the feature and the repo, and saying
|
|
868
|
+
what fixes it. The treat-as-UNSEEN instruction survives for the one honest case — the record
|
|
869
|
+
names no branch at all — where the prompt now cites `build.epic_branch` rather than a name it
|
|
870
|
+
made up. The two absences used to render as the same sentence.
|
|
871
|
+
- Thirteen tests, red first (6 red / 6 green before, 13 green after): the derivation reproduced
|
|
872
|
+
under both branch models (the prompt's branch lines asserted byte for byte against
|
|
873
|
+
`build.epic_branch`), the loud path in both `headless` and `--prepare`, `--commit` deliberately
|
|
874
|
+
NOT refusing a turn that is already paid for, and the honest absence.
|
|
875
|
+
|
|
876
|
+
- **A recorded `default_branch` that does not resolve is incoherent state too (#92).** #90 taught
|
|
877
|
+
Watch to tell a run that recorded NO branch (honest) from a branch its own `build.epic_branch`
|
|
878
|
+
claims and the repo cannot find (incoherent, refused). It left the third case alone: the BASE of
|
|
879
|
+
the same diff. `.tldrx/workspace.yml` declares `default_branch: main` for a repo where `main` does
|
|
880
|
+
not resolve, and the prompt rendered *"`main`, the `default_branch` of api, does not resolve
|
|
881
|
+
there. Treat this feature's code as UNSEEN — cite `absent:` rather than guessing at what it
|
|
882
|
+
emits"* — inviting the same all-`absent:` card that passes `claim-sources` and covers nothing.
|
|
883
|
+
Narrower blast radius than #90 (a misdetected or renamed default branch, not an every-run
|
|
884
|
+
derivation), same shape: **a value the workspace RECORDS that the repo cannot find.**
|
|
885
|
+
- **Watch refuses at `--prepare` and headless**, naming the repo, the recorded value and
|
|
886
|
+
`.tldrx/workspace.yml`, and saying that the value was DETECTED from that repo — so a repo with
|
|
887
|
+
no such branch means the record has gone stale (a rename, a fresh clone with only remote
|
|
888
|
+
branches, a misdetection). `epicDiff` carries it as `baseMissing`, the sibling of #90's
|
|
889
|
+
`branchMissing`, and checks the base FIRST: a base that is not there voids every diff in that
|
|
890
|
+
repo rather than emptying one, so there is nothing to learn by asking about the branch. On
|
|
891
|
+
`--commit` it stays tolerant, the same call #90 made — those cards are already paid for.
|
|
892
|
+
- **`tldrx doctor` reports it**, which is where the issue asked whether it belonged, and the
|
|
893
|
+
answer is yes: this is a workspace RECORDING something false, and `doctor` is the one command
|
|
894
|
+
whose job is what this machine and this workspace actually have. One
|
|
895
|
+
`git rev-parse --verify --quiet` per declared repo, nothing fetched or written. A **warning**
|
|
896
|
+
that never moves the exit code — `healthy` is about the TOOLS this machine has, and a repo can
|
|
897
|
+
legitimately be mid-clone or mid-rename on a developer's box; a `doctor` that exits 1 for that
|
|
898
|
+
is a `doctor` people stop running. A repo that is not on disk, has no `default_branch` recorded,
|
|
899
|
+
or is not a git repo is reported as SKIPPED with its reason, never folded into "all resolve".
|
|
900
|
+
- **`boundary` has the gap and deliberately keeps its verdict — measured, not assumed.** It does
|
|
901
|
+
diff `<default_branch>...<epic_branch>` (`boundary.ts`, `base:` read from `workspace.yml`;
|
|
902
|
+
changing the record from `main` to `trunk` changes the target's `base` byte for byte), and an
|
|
903
|
+
unresolvable base came back `{ok: true, detail: "n/a (nothing could be diffed: \`trunk\` does not
|
|
904
|
+
resolve in app)"}` — the gate passes GREEN and stops measuring for as long as the record is
|
|
905
|
+
wrong. It is **not** made to refuse: it spawns nothing and writes nothing, so the fault costs it
|
|
906
|
+
a measurement rather than producing a false one, and `boundary.ts`'s own contract is that it
|
|
907
|
+
"must not refuse a gate for a reason that has nothing to do with the boundary" — one stale
|
|
908
|
+
record would otherwise brick every Build gate in the workspace. What changed is the WORDING: its
|
|
909
|
+
two absences used to be the same sentence (`` `<ref>` does not resolve in <repo> ``), so a
|
|
910
|
+
merged-and-deleted epic branch (nothing to fix) read exactly like a stale `default_branch`
|
|
911
|
+
(`n/a` until someone fixes it). The base's reason now names the record and points at
|
|
912
|
+
`tldrx doctor`; the branch's is untouched.
|
|
913
|
+
- `.tldrx/workspace.yml` is spelled once, in `paths.ts`, now that three operator-facing messages
|
|
914
|
+
tell someone to go and edit it.
|
|
915
|
+
- Fifteen tests, red first (8 red / 7 green before, 15 green after), including the verbatim
|
|
916
|
+
pre-fix string captured off the real prompt, the `--commit` tolerance, doctor's warning level
|
|
917
|
+
asserted through `runDoctor` end to end, and the boundary determination pinned on all four
|
|
918
|
+
counts — that it reads `default_branch`, that it stays `ok`, that its reason now names the
|
|
919
|
+
record, and that a missing epic branch keeps its own wording.
|
|
920
|
+
|
|
921
|
+
- **The dashboard says the framework's CURRENT vocabulary, not 0.2.0's.** The live page
|
|
922
|
+
shipped in 0.2.0 and has had one change since; the framework has had a great many. An audit of
|
|
923
|
+
`src/core/dashboard/` against today's `run.yml` and `waiting.ts` found seven words the files use
|
|
924
|
+
and the page dropped. A page that drops a word does not look wrong — it looks finished, which is
|
|
925
|
+
worse, and every one of these was measured on a rendered fixture before it was fixed.
|
|
926
|
+
- **`prepared` and `running` rendered as "nothing".** `waitingFor` has had eight kinds since the
|
|
927
|
+
2026-08-29 audit; the WAITING ON column named five and fell through to `nothing — <status>` for
|
|
928
|
+
the rest. So a host-attended run with a `--prepare` bundle on disk — the entire attended loop —
|
|
929
|
+
said it was waiting on nothing while `isMovable` had it wearing `← next` on the same row. Every
|
|
930
|
+
kind now prints `waiting.message`, which is the sentence the CLI already prints, and `prepared`
|
|
931
|
+
joins gate/answer/failed as a card: it is a run waiting on a person.
|
|
932
|
+
- **An `agent` gate was counted as a human one.** The execution-path eyebrow read
|
|
933
|
+
`N human, M auto`, so a run opened `--gates what:agent,plan:agent,build:agent` reported as
|
|
934
|
+
all-human — the exact opposite of what it was set up to do, in the one number that eyebrow
|
|
935
|
+
exists to give. It now counts all three, the same arithmetic `renderGates` does.
|
|
936
|
+
- **Nothing showed what an agent gate was signed over.** `run.yml` records the verdict, the
|
|
937
|
+
sample, what resolved and what was refuted, and the run-relative path of the COMMITTED evidence
|
|
938
|
+
note; the model dropped the whole block, so `agent by reviewer` and `human by alan` read as the
|
|
939
|
+
same kind of fact. A human signature is a name and a person accountable for it. An agent's is a
|
|
940
|
+
name and nothing, unless what it checked is beside it. Now `path[].gateEvidence`, printed under
|
|
941
|
+
the signer — the path as text, never a link.
|
|
942
|
+
- **`$0.00 spent` on a run whose turns a host session paid for.** `spentUsd` is metered dollars
|
|
943
|
+
and the meter drew it as the whole story: the exact failure `unmeteredNote` exists to stop the
|
|
944
|
+
CLI making, made in a progress bar. `attendedBy`, `unmeteredTasks` and `hostTokens` now ride the
|
|
945
|
+
model, the card prints `attended: host` in the CLI's own words, and the two currencies are shown
|
|
946
|
+
side by side and never added — there is no exchange rate between a metered dollar and a host token.
|
|
947
|
+
- **`build.branch_model` (#57) and the epic branches were invisible** in a section headed
|
|
948
|
+
*Plan & build*. A chained plan on one integration branch and independent per-epic branches drew
|
|
949
|
+
the identical table. A null model is reported as unrecorded, never guessed at as `per-epic`.
|
|
950
|
+
- **A `stale` stage looked finished.** A stage left behind by `tldrx reject --stage` wears
|
|
951
|
+
`done` with its outputs still on disk, derived from a decision that has been withdrawn. It now
|
|
952
|
+
wears a `stale` chip too.
|
|
953
|
+
- **The page claimed it read `events.jsonl`.** It never has (measured: nothing in `model.ts` or
|
|
954
|
+
`loadPhaseArtefacts` opens the ledger). That sentence is why a reader could not tell an empty
|
|
955
|
+
ledger from an unread one, so the *How to use it* tab now says which files it reads, that the
|
|
956
|
+
ledger is not among them, and which commands do read it. The Watchers tab, which existed
|
|
957
|
+
precisely to be honest about a gap, printed an invented card shape whose field names matched
|
|
958
|
+
none of the seven `Watcher` actually carries; it now prints the real one and names
|
|
959
|
+
`tldrx watch list`.
|
|
960
|
+
- **`review` was grey.** A `PLAN_STATUSES` value the page's tone function had never heard of, so
|
|
961
|
+
a story in review landed in the same colour as one nobody had started.
|
|
962
|
+
- **The test that should have caught all of this asserted the opposite.** `WAITING_KINDS` is now a
|
|
963
|
+
value rather than a bare type union, and the chain fixture's coverage test names the three kinds
|
|
964
|
+
it cannot hold instead of claiming it covers every kind — the claim that let `prepared` go
|
|
965
|
+
unrendered for months. `modelVersion` stays at `3`: every model change here is an ADDITION, and
|
|
966
|
+
the rule in `model.ts` is that only a removal or a change of meaning bumps it.
|
|
967
|
+
- **What the same audit found and did NOT fix is #85**, because each of it is a decision rather
|
|
968
|
+
than a patch: reading `events.jsonl` (operator notes, story attempts and reopens), reading
|
|
969
|
+
`budget.yml` (per-phase ceilings, `on_exceed`, whether `next` is affordable), reading the
|
|
970
|
+
watcher cards, and surfacing a preflight refusal. #86 is a separate bug the audit turned up in
|
|
971
|
+
`run/waiting.ts` — a cancelled run is offered a retry — which both screens share.
|
|
972
|
+
|
|
973
|
+
- **The #80 guard's kind sweep could not tell a regex literal in CODE from one quoted in a
|
|
974
|
+
COMMENT (#83).** The marker half of that guard has a discriminator and says why: a `[` inside a
|
|
975
|
+
regex opens a character class, so code that matches the `[src:` marker must spell it `\[src`
|
|
976
|
+
while prose writes `[src: …]` bare. The kind half had no equivalent, because prose quoting a
|
|
977
|
+
regex LITERAL copies it character for character — there is no signature inside `/^F\d{3,6}$/`
|
|
978
|
+
that a real production has and a quoted one lacks. #81 paid the tax: the doc comment explaining
|
|
979
|
+
which two literals had just been DELETED tripped the sweep in the very file that had stopped
|
|
980
|
+
classifying, and had to be reworded to name the shapes without writing them.
|
|
981
|
+
- **The discriminator is the line's ROLE, not its characters.** A line that is comment prose —
|
|
982
|
+
a whole-line `//`, a block opener, or one of its `*` continuation lines — is not a
|
|
983
|
+
classification, and the sweep now reads the file with those lines removed.
|
|
984
|
+
- **Line-local, holding no state, and that is the design rather than a shortcut.** #80's own
|
|
985
|
+
first attempt reached for a lexer and was bitten twice: eleven offenders that were all doc
|
|
986
|
+
comments, then five more that were template literals nested inside interpolations once
|
|
987
|
+
comments and strings were stripped. This predicate never looks inside a string, so an
|
|
988
|
+
unclosed `/**` in a prompt cannot unbalance it and blind the sweep to the code below —
|
|
989
|
+
asserted, not assumed. What it cannot lex it keeps: a shape quoted in a trailing comment
|
|
990
|
+
after code is still read as code, so this half still fails CLOSED.
|
|
991
|
+
- **Measured in both directions on the real tree, not only on fixtures.** Re-injecting #81's
|
|
992
|
+
exact doc comment into `validateFactsFile.ts` leaves the sweep green; injecting two real
|
|
993
|
+
productions as CODE into the same file turns it red with `validateFactsFile.ts classifies
|
|
994
|
+
answer, fact` — the identical message #81 saw for prose. The sweep is also no longer
|
|
995
|
+
vacuous by assumption: a test pins the kinds the canonical grammar itself trips
|
|
996
|
+
(`answer`, `cmd`, `fact`) and the six productions the deleted `core/map/srcToken.ts`
|
|
997
|
+
carried, so a production that stops matching anything becomes visible rather than silent.
|
|
998
|
+
- **What it removed, measured over the tree.** 28 files matched at least one production
|
|
999
|
+
before, 20 after; the 8 that stopped matching were doc comments mentioning `→ exit` and
|
|
1000
|
+
nothing else — each one production away from #81's surprise red. No file lost a
|
|
1001
|
+
classification: the most kinds any file decides is still 3, and it is still `srcToken.ts`.
|
|
1002
|
+
|
|
1003
|
+
- **A handshake called in the wrong ORDER no longer fails the stage and the run (#82).** On the
|
|
1004
|
+
live run `260901-leaderboard-v2` (2026-09-02T00:36Z) the driver ran `tldrx next --commit
|
|
1005
|
+
--review` while no reviewer bundle was out. The framework said exactly the right thing — "no
|
|
1006
|
+
reviewer bundle is out — run `tldrx next --prepare --review` first" — and then emitted
|
|
1007
|
+
`stage.failed` twice and flipped the run to `status: failed`, so a mistyped command needed an
|
|
1008
|
+
explicit recovery. Nothing had been attempted: no spawn, no cent, no branch, no story moved.
|
|
1009
|
+
Same family as #78/#79 (form vs work), applied to state transitions instead of attempts.
|
|
1010
|
+
- **Five refusals are reclassified**, all of them the handshake asked for by the wrong end:
|
|
1011
|
+
`--commit --review` with no reviewer bundle out; `--commit` with no story `in_progress`;
|
|
1012
|
+
`--prepare --review` over a story whose developer half has not run; and either `--commit`
|
|
1013
|
+
before its `result.json` has been written. Each now exits `1`, names the command that fixes
|
|
1014
|
+
it, and leaves `run.yml` byte for byte as it was — no `stage.failed`, no status moved, no
|
|
1015
|
+
task recorded, no cent metered.
|
|
1016
|
+
- **This is the behaviour the framework already had one layer over.** A single-agent stage sent
|
|
1017
|
+
`--commit` before its `--prepare` has always returned exit `1` and touched nothing
|
|
1018
|
+
(`commitStage`). Build was the outlier because Build owns its own middle, so its refusals came
|
|
1019
|
+
back as `ok: false` and `runNext` could only read that as a failed stage. The fix gives Build
|
|
1020
|
+
the same door rather than inventing a second contract.
|
|
1021
|
+
- **`sequencing` is a second flag, not a widening of `refused`.** `refused` sends the stage back
|
|
1022
|
+
to `ready`, which is right for a precondition an operator must go and fix — a dirty repo, a
|
|
1023
|
+
red base tree — because the cycle cannot continue. A sequencing refusal is a cycle that is
|
|
1024
|
+
perfectly fine, held by the wrong end: the bundle is still out and sending the stage back to
|
|
1025
|
+
`ready` would throw away the state the next command needs. Honoured only when the outcome
|
|
1026
|
+
carries no tasks, no cost and no epic claim, so a refusal that spent something still records
|
|
1027
|
+
it; defaulting to "record it" is the direction a mistake here is recoverable in.
|
|
1028
|
+
- **The bug had a price, and it was the budget brake.** `runExecutor` skips the phase-budget
|
|
1029
|
+
gate exactly when a stage is already `running`, because a Build stage hands out one story per
|
|
1030
|
+
`--prepare`/`--commit` cycle. Failing the stage demoted it out of `running`, so the next
|
|
1031
|
+
`--prepare` was priced as a fresh stage start: on the live run that was a `budget.blocked`
|
|
1032
|
+
ten seconds after the refusal, $2.66 left against a re-charged $4.32 estimate, for money
|
|
1033
|
+
already partly spent. Reproduced in the fixture and pinned.
|
|
1034
|
+
- **`PendingError` carries a typed `kind`** — `absent` (the file was never written; a step of
|
|
1035
|
+
the handshake) vs `unreadable` (somebody wrote it and got it wrong; still a failure). Typed
|
|
1036
|
+
rather than matched on the message, for the reason #79 gave: a caller reading the words breaks
|
|
1037
|
+
the moment the message improves.
|
|
1038
|
+
- Genuine failures are untouched and pinned: a red Definition of Done still blocks its story, a
|
|
1039
|
+
reviewer that dies is still recorded as a failed check, and a plan that cannot be loaded is
|
|
1040
|
+
still `stage.failed` with a `failed` run. Two existing pins moved deliberately, both of them
|
|
1041
|
+
asserting the old exit `5` of a pure sequencing refusal.
|
|
1042
|
+
|
|
1043
|
+
- **`facts.yml` ids and citable ids were two spellings of one shape; now they are one constant
|
|
1044
|
+
(#81).** `src/core/facts/validateFactsFile.ts` defined its own `F` and `Q` id patterns, agreeing
|
|
1045
|
+
with `SRC_PATTERNS.fact` / `.answer` character for character, with nothing asserting that they
|
|
1046
|
+
must. Found while fixing #80 and filed rather than fixed there, because it is not #80's defect:
|
|
1047
|
+
the `[src: …]` token grammar has exactly one reader now, and what was duplicated here is the id
|
|
1048
|
+
SHAPE that grammar happens to share with the facts file, reached by a different reader answering
|
|
1049
|
+
a different question — which is why #80's kind sweep allowlisted the file rather than folding it in.
|
|
1050
|
+
- **They must agree, and the DIRECTION is what makes it load-bearing.** One string makes one
|
|
1051
|
+
trip: `formatFactId` mints `F102`, `validateFactsFile` admits it into `.tldrx/memory/facts.yml`,
|
|
1052
|
+
and `classifySrc` reads that same `F102` back out of a `[src: F102]` token for `knowledgeFile`
|
|
1053
|
+
to resolve against the store. So every id the facts file accepts must be citable, or a fact
|
|
1054
|
+
exists that cannot be cited and is invisible to every reader downstream of it. Nothing in that
|
|
1055
|
+
pipeline ever runs both readers on the same string, so the drift would have been silent. (The
|
|
1056
|
+
other direction is merely untidy: a citable id no facts file holds is caught later, as a
|
|
1057
|
+
citation that resolves to nothing.)
|
|
1058
|
+
- **Derived, not merely asserted.** `validateFactsFile.ts` imports `SRC_PATTERNS.fact` and
|
|
1059
|
+
`.answer` — the same publication surface `srcGrammarContract.ts` already generates the
|
|
1060
|
+
documented grammar from — so there is one spelling rather than two that match. The #80
|
|
1061
|
+
allowlist entry is deleted along with the need for it, and that sweep now covers the file like
|
|
1062
|
+
any other; an exemption that has stopped being needed is one that will some day cover something
|
|
1063
|
+
it was never granted for.
|
|
1064
|
+
- **A behavioural guard too, because sharing a constant only holds while nobody re-types a
|
|
1065
|
+
literal.** The kind sweep needs TWO productions in one file before it fires, so a single
|
|
1066
|
+
respelled `F` shape slips straight past it — measured by sabotage, not assumed.
|
|
1067
|
+
`test/map-citations.test.ts` now runs both readers over a table of ids spanning both digit
|
|
1068
|
+
boundaries and compares their verdicts; loosening either shape by one digit turns it red with
|
|
1069
|
+
`F12: facts.yml holds it but the grammar refuses it`.
|
|
1070
|
+
- **One drift had already happened, in prose.** The `source.q` refusal read `expected ^Q\d+$ or
|
|
1071
|
+
null` while the reader ran `^Q\d{1,6}$` — the shape's third spelling, telling an author a
|
|
1072
|
+
seven-digit question id was acceptable and then refusing it. Both refusal messages are now
|
|
1073
|
+
generated from the pattern they enforce, so neither can drift from its reader again.
|
|
1074
|
+
- **The docs site said 0.3.1, named a flag that does not exist, and told readers to hand-write
|
|
1075
|
+
a mandate the CLI now prints.** A cold read of every page against `tldrx <cmd> --help`, EN and
|
|
1076
|
+
ES. Three claims were false rather than merely stale: the version, printed on six pages, was two
|
|
1077
|
+
releases behind what `npm i -g tldr-experts` installs; `--max-budget-usd` was named as a `tldrx
|
|
1078
|
+
next` flag in both budget pages, but it is what tldrx passes down to `claude` and `tldrx next
|
|
1079
|
+
--max-budget-usd 3` exits `1` on an unknown flag; and `guides/driving` said "there is no keyword
|
|
1080
|
+
for the third part — the mandate is prose you write" three weeks after `tldrx drive
|
|
1081
|
+
--attended|--unattended` shipped to print exactly that, versioned with the package. Also
|
|
1082
|
+
corrected: the hero claimed a gate you own at the end of *each* stage, where `feature` ships two
|
|
1083
|
+
`auto` ones; the quickstart said "five role experts" one line under output reading `experts 6
|
|
1084
|
+
seeded`, and reported `$0.31 of $4.00` on a run opened at `$5.00` without saying the second
|
|
1085
|
+
figure is the stage's ceiling.
|
|
1086
|
+
- **The site covered none of the last release's commands, and the Spanish mirror had fallen a
|
|
1087
|
+
release behind the English one.** Added across both languages: `story reopen --for-fix`,
|
|
1088
|
+
`questions lint`, `retro --all --json`, `watch check --execute`, `update_check: off`, `seed
|
|
1089
|
+
answer`, exit `130`, `budget.yml`'s `ceiling_host_tokens` / `on_host_tokens_exceed` (the host
|
|
1090
|
+
economy was documented as unboundable), the integration-branch model for chained epics (#57),
|
|
1091
|
+
a FAQ entry for `tldrx update` and the version notice, and a link to `CONTRIBUTING.md`. The ES
|
|
1092
|
+
CLI reference was additionally missing `update`, `drive`, `retro --all` and `plan schema`
|
|
1093
|
+
outright. `reference/cli` no longer pins a version number that goes stale every release. The
|
|
1094
|
+
Spanish pages' code comments were half-translated; terminal output stays English, the docs
|
|
1095
|
+
author's own `#` comments do not.
|
|
1096
|
+
|
|
1097
|
+
- **`tldrx map --check` ran a SECOND, divergent `[src: …]` grammar; there is one grammar now
|
|
1098
|
+
(#80).** `src/core/map/srcToken.ts` was not a thin wrapper over the claim-sources reader — it was
|
|
1099
|
+
a parallel set of regexes, and they disagreed on five axes: its `file` pattern
|
|
1100
|
+
(`[^\s:]+`) refused a path containing a colon, its answer pattern (`^Q\d+$`) had no digit cap,
|
|
1101
|
+
its token pattern was **global** so a citation written mid-sentence counted as one, it stripped no
|
|
1102
|
+
trailing backtick or full stop before matching, and it had never heard of `aidlc:`. So a citation
|
|
1103
|
+
`claim-sources` accepted could be reported by `map --check` as a problem, and one it refused could
|
|
1104
|
+
be reported as fine — the "two readers of one question drift, and the looser one wins the argument
|
|
1105
|
+
at the wrong moment" hazard `core/text/handoff.ts` is written against. It also carried #77's
|
|
1106
|
+
defect on its own path, printing the symptoms `bullet has no [src: …] token` and
|
|
1107
|
+
`unparseable src token` and nothing else.
|
|
1108
|
+
- **The parser is deleted, not wrapped.** `checkCitations` calls `parseSrcToken`,
|
|
1109
|
+
`diagnoseSrcToken` and `describeSrcFailure` from `src/core/text/srcToken.ts`, so every `map
|
|
1110
|
+
--check` grammar failure gets the #77 treatment by construction: the rule id, the rule in its
|
|
1111
|
+
own words, the line as written, and a line that would have passed. `CitationProblem` gained a
|
|
1112
|
+
`rule` field, so a caller can tell "you wrote it wrong" from "it has drifted" without parsing
|
|
1113
|
+
prose. `srcToken(srcs)` — the BUILDER, which had no equivalent on the canonical side — moved
|
|
1114
|
+
across rather than went, and now joins on the same `SRC_SEPARATOR` the reader splits on.
|
|
1115
|
+
`isBullet` stayed behind in `checkCitations.ts`: it is about DOCUMENTS, and mixing the two is
|
|
1116
|
+
what the deleted file got wrong.
|
|
1117
|
+
- **The migration hazard was measured before the switch, not asserted after it.** Both grammars
|
|
1118
|
+
were run over every `.tldrx/map/**` document and init handoff in the two real workspaces
|
|
1119
|
+
available — **39 documents, 692 lines, 435 of them carrying a citation** — and they disagreed on
|
|
1120
|
+
**zero** lines: no multi-token line, no non-bullet citation, and not one token that failed to end
|
|
1121
|
+
its line. The axes where they *would* differ are pinned as a table in
|
|
1122
|
+
`test/map-citations.test.ts`. Unification is **stricter** on six shapes (a mid-line token, a
|
|
1123
|
+
`..` in a path, a 7-digit `Q`, a 4-digit exit code, a backwards line range, a line number of
|
|
1124
|
+
zero) and **looser** on five that the old reader wrongly refused (a path containing a colon, an
|
|
1125
|
+
`aidlc:…#Q<n>` src, a `cmd` whose command holds a backtick, an `absent:` path with a space, a
|
|
1126
|
+
token wrapped in backticks). An `aidlc:` citation used to be misread as repo `aidlc` and
|
|
1127
|
+
reported as an unknown repo; it is a kind now.
|
|
1128
|
+
- **A guard makes re-adding a copy go red** (#48's lesson: deleting one file does not stop a
|
|
1129
|
+
second). Two halves, both on the SHAPE rather than the file name — no file outside the canonical
|
|
1130
|
+
grammar and two documented non-parsers may hold a regex that matches the `[src:` marker, and no
|
|
1131
|
+
file outside it may classify two or more src KINDS. The guard is proven against the deleted
|
|
1132
|
+
file's own two regexes rather than assumed. It also turned up a real second copy of the fact and
|
|
1133
|
+
question id shapes in `validateFactsFile.ts`, filed as #81 rather than fixed here.
|
|
1134
|
+
- **`map --check`'s summary counts the two failures separately.** A citation that does not parse
|
|
1135
|
+
never reached the filesystem, so folding it into "N of M citations do not resolve" reported a
|
|
1136
|
+
denominator it was never in — and, when every problem was a grammar one, printed "3 of 0".
|
|
1137
|
+
|
|
1138
|
+
- **A `[src: …]` rejection now states the RULE it enforced, and the grammar is published where the
|
|
1139
|
+
writers read it (#77).** Run `260830-ordering-inventory` lost **three story attempts** to one
|
|
1140
|
+
message. Three review envelopes were refused with "no `[src: …]`" — the SYMPTOM — while all three
|
|
1141
|
+
carried a citation. The host guessed the grammar twice, got it wrong twice, and finally opened
|
|
1142
|
+
`dist/tldrx.js` to extract three rules that were written down nowhere a writer could read them:
|
|
1143
|
+
the token must END its line (`TRAILING_TOKEN_RE` is anchored), a `]` **inside** the token
|
|
1144
|
+
truncates the match (`[^\]]*`), and a `cmd` source needs the real `→`, never ASCII `->`.
|
|
1145
|
+
- **Every rejection on the path names its rule, quotes the line, and shows a corrected one.**
|
|
1146
|
+
`SRC_RULES` (`src/core/text/srcToken.ts`) is fifteen rules, each with the pattern that enforces
|
|
1147
|
+
it and a worked `bad`/`good` pair; `diagnoseSrcToken` maps a failure onto exactly one. The hook
|
|
1148
|
+
denies, the gate's `claim-sources` detail, `parseFixFindings`' `refuted` refusal and every
|
|
1149
|
+
`classifySrc` error now carry the id. The deny messages take the document text and quote the
|
|
1150
|
+
offending line back — naming `L14` and stopping there is what sent the host looking.
|
|
1151
|
+
- **The grammar is GENERATED and spliced into the prompt** (`renderSrcGrammarContract`), following
|
|
1152
|
+
the #35 precedent: kinds from `SRC_KINDS`, patterns from `SRC_PATTERNS` printed `.source` and
|
|
1153
|
+
all, rules and examples from `SRC_RULES`, the four-section rules from the same constants
|
|
1154
|
+
`handoff.ts` enforces. It reaches the writers through the check-contracts registry (every stage
|
|
1155
|
+
declaring `claim-sources` over a `.md` output), the Watch executor — which read `stage.md`
|
|
1156
|
+
raw and so was the one writer never given any contract — and the reviewer prompt, whose
|
|
1157
|
+
`refuted` verdict is held to this grammar and was never told what it is.
|
|
1158
|
+
- **The trap is behavioural, not textual.** `test/src-grammar.test.ts` pushes every documented
|
|
1159
|
+
`bad` back through `diagnoseSrcToken` and every `good` through `parseSrcToken`. Loosen a regex
|
|
1160
|
+
without updating its rule and the suite goes red; a doc that has stopped being true cannot ship
|
|
1161
|
+
quietly. It caught one bug on its first run: `RegExp.source` re-escapes non-ASCII, so
|
|
1162
|
+
`CMD_RE.source` spells the arrow as a `\u`-escape — the contract would have documented "use the
|
|
1163
|
+
real `→`" with the arrow itself written as an escape sequence. `readableSource` decodes it.
|
|
1164
|
+
- Not changed: what the parser ACCEPTS. Tolerating `->` is a product decision and stays open
|
|
1165
|
+
as #77's item 3.
|
|
1166
|
+
- **A grammar-rejected review envelope no longer consumes a story ATTEMPT (#78).** Measured on run
|
|
1167
|
+
`260830-ordering-inventory` (2026-09-01): stories S2, S3 and S5 each recorded
|
|
1168
|
+
`check: review · verdict: changes · attempt: 1` over a summary beginning *"I would sign this:
|
|
1169
|
+
every named acceptance criterion is met"*. Those were `fixlist` envelopes refused by the
|
|
1170
|
+
**claim-sources** check — a `refuted` finding whose `[src: …]` sat mid-line, where §2.8's
|
|
1171
|
+
end-anchored parser cannot see it — and each was charged to the story as a failure of its WORK.
|
|
1172
|
+
Three of the run's attempts went on formatting. A malformed envelope is a fault in the reviewer's
|
|
1173
|
+
*report*; conflating the instrument with the result is what this fixes.
|
|
1174
|
+
- **The framework asks again, for free.** A refusal the claim-sources check raised re-prompts the
|
|
1175
|
+
same reviewer for a corrected envelope, carrying what was refused verbatim under
|
|
1176
|
+
`## Your previous envelope was REFUSED`. **Bounded at two** per envelope round (owner decision,
|
|
1177
|
+
2026-09-01): the third refusal is recorded as the ordinary `changes` and costs the attempt, so a
|
|
1178
|
+
reviewer that cannot write the grammar at all still settles instead of looping free. The bound
|
|
1179
|
+
resets when a verdict is finally counted — it is per envelope round, not per story.
|
|
1180
|
+
- **Both doors, one rule.** A spawned reviewer re-prompts itself in-process;
|
|
1181
|
+
`tldrx next --commit --review` leaves the bundle out with the refusal spliced into its
|
|
1182
|
+
`prompt.md`, bins the refused `result.json` and settles nothing. Attempt accounting must not
|
|
1183
|
+
depend on which door a verdict came through, so both go through the same predicate.
|
|
1184
|
+
- **Auditable, because it is bookkeeping.** Each free round appends one `story.review_retried`
|
|
1185
|
+
(§2.9) carrying the story, the attempt it did **not** spend, which retry it was, the bound, and
|
|
1186
|
+
the refusal. `readReviewLedger` counts them, which is how the bound survives a fresh
|
|
1187
|
+
`tldrx next` and the one-envelope-per-process host handshake alike. Each re-prompt is a real
|
|
1188
|
+
metered turn and gets its own task row: it costs the story no attempt, never no money.
|
|
1189
|
+
- **Scope guard, pinned by test.** Only the claim-sources grammar — *widened to every
|
|
1190
|
+
envelope-FORMAT refusal by #79 below, in the same release.* A verdict's CONTENT and a red DoD
|
|
1191
|
+
keep exactly the cost they had.
|
|
1192
|
+
- **The message is #77's, inherited rather than copied.** The re-prompt carries the refusals
|
|
1193
|
+
verbatim and points at the `Citation grammar` section #77 splices into the
|
|
1194
|
+
same prompt, so the reviewer is told which rule it broke, on which line, with a corrected
|
|
1195
|
+
example — and there is no second copy of the grammar to keep in step. The classifier
|
|
1196
|
+
is a typed INDEX over `problems`, not a second list, precisely so #78 could not end up
|
|
1197
|
+
string-matching the text #77 was rewriting.
|
|
1198
|
+
|
|
1199
|
+
- **Every envelope-FORMAT refusal gets that same free round, not just the citation (#79).** #78 drew
|
|
1200
|
+
its scope at the claim-sources check because that is what the evidence named, and filed the rest.
|
|
1201
|
+
But `parseFixFindings` refuses an envelope for five other shapes — a `fixlist` that is missing,
|
|
1202
|
+
not an array or empty; a row that is not an object; a row with no `finding` text; a row with no
|
|
1203
|
+
valid `disposition` — and `parseReview` refuses a sixth, a verdict WORD outside the enum (#36).
|
|
1204
|
+
Every one of them is a fault in how the reviewer wrote its *report*, exactly the argument #78 made,
|
|
1205
|
+
and every one of them still cost the story an attempt. Owner decision (2026-09-01, on the issue):
|
|
1206
|
+
all of it, **one mental model — FORM never costs an attempt, CONTENT/WORK always does.**
|
|
1207
|
+
- **Nothing about the mechanism changed.** Same bound (two per envelope round, the third is the
|
|
1208
|
+
ordinary `changes`), same counter, same `story.review_retried` event, same two doors, same
|
|
1209
|
+
per-turn metering. #79 widened *what earns a correction* and touched nothing else.
|
|
1210
|
+
- **The verdict WORD (#36) is now free too, and #36's message is unchanged.** A reviewer that
|
|
1211
|
+
writes `sign` — the gate vocabulary — said nothing wrong about the diff; it reached for a word
|
|
1212
|
+
the story enum does not have. It is told so, by name, and asked again. `Review.formatProblems`
|
|
1213
|
+
is the union of the fix-list index and that verdict fault, because it is also what the corrected
|
|
1214
|
+
envelope's prompt is rendered from: a refusal missing from it is one the reviewer is never told
|
|
1215
|
+
about.
|
|
1216
|
+
- **The guard is the INDEX, which is what survives the next widening.** `ParsedFixlist.format`
|
|
1217
|
+
stays a typed subset of `problems`, built one push site at a time through a single
|
|
1218
|
+
`refuseFormat` helper — never a second list of strings, and never "everything `parseFixFindings`
|
|
1219
|
+
said". The free round is granted only when the index claims **every** reason the envelope was
|
|
1220
|
+
refused, so a refusal about the WORK added later costs the attempt until somebody deliberately
|
|
1221
|
+
indexes it as form. Defaulting to *costs* is the direction a mistake is recoverable in.
|
|
1222
|
+
- **A non-empty `findings[]` is deliberately NOT the content signal**, though it is the obvious
|
|
1223
|
+
candidate. Measured across the nine `aparece-v2` runs (2026-09-01): all 25 recorded review logs
|
|
1224
|
+
carry a non-empty `findings[]` and all 25 are `approve` — the one verdict whose own prompt line
|
|
1225
|
+
says *"Empty on `approve`"*. Reviewers use it as a narrative evidence log whatever the verdict,
|
|
1226
|
+
so gating on it would have made the free round almost never fire and quietly narrowed #78 as
|
|
1227
|
+
well. A judgement about the work is caught where it is actually stated: a declared `changes`
|
|
1228
|
+
raises no format refusal at all, so it costs its attempt.
|
|
1229
|
+
- **Nine tests red first**, each newly-free class proven to re-prompt without spending an attempt
|
|
1230
|
+
and to record the event, plus the bound re-proven on a shape refusal. #78's CONTENT and DoD pins
|
|
1231
|
+
are unchanged and still green.
|
|
1232
|
+
|
|
1233
|
+
- **`merge-wave.sh` no longer leaves a conflicted tree behind, wedging every queued sibling (#76).**
|
|
1234
|
+
On a merge conflict the script exited `2` **without** `git merge --abort`, and the `EXIT` trap
|
|
1235
|
+
then released the lock. The conflicted index survived that handover, so the next queued
|
|
1236
|
+
invocation acquired the lock, failed its dirty-tree guard and exited `1` `FAIL dirty tree` having
|
|
1237
|
+
merged nothing — and so did every one after it, until a human ran the abort by hand. Observed
|
|
1238
|
+
live 2026-09-01 by two agents: a sibling's abandoned merge left `UU CHANGELOG.md` plus 14 staged
|
|
1239
|
+
paths in the shared checkout, and cluster L's first merge-wave returned `1` with nothing merged.
|
|
1240
|
+
Under the concurrent multi-cluster pattern, one conflict wedged every other cluster.
|
|
1241
|
+
- **Collect, then abort** — the order `mergeNoFf` already uses one directory over
|
|
1242
|
+
(`src/core/build/git.ts:314-326`). The agent still learns exactly which files conflicted and
|
|
1243
|
+
still has to rebase and retry; the checkout it hands back is the one it was given. This is the
|
|
1244
|
+
same class of hazard the lock was written for: state from one invocation leaking into the next.
|
|
1245
|
+
- **The refusal now names what is dirty.** `FAIL dirty tree` alone accused the caller of leaving
|
|
1246
|
+
junk in their own checkout when, inside the lock, the likeliest cause is another run's residue
|
|
1247
|
+
in the shared one. It now says so and lists the paths.
|
|
1248
|
+
- Proved by a repro that runs the real script against a real conflicting merge and asserts
|
|
1249
|
+
`git status --porcelain` is empty afterwards, `MERGE_HEAD` is gone, `HEAD` has not moved, the
|
|
1250
|
+
lock is released, and a second invocation merges instead of being refused.
|
|
1251
|
+
|
|
1252
|
+
- **The built-CLI dashboard test no longer gates a stale `dist/` (#73).** `beforeAll` read
|
|
1253
|
+
`if (existsSync(DIST)) return`, so the tests ran against whatever `dist/tldrx.js` happened to be
|
|
1254
|
+
lying around — locally, a build from before the working-tree changes the run was checking.
|
|
1255
|
+
Measured 2026-09-01 while fixing #60: the guard served a binary built at 14:43, and the model
|
|
1256
|
+
version assertion read `Expected: 3, Received: 2`. That is the lucky direction; the same staleness
|
|
1257
|
+
hiding a regression is silent, and `bun run build` is a separate later step in both
|
|
1258
|
+
`scripts/merge-wave.sh` and CI. CI was safe only by accident — a fresh checkout has no `dist/` —
|
|
1259
|
+
which means the guard only ever applied where it did harm.
|
|
1260
|
+
- **Always rebuild, and the number is the reason.** `bun scripts/build.ts` costs 199 ms cold and
|
|
1261
|
+
55–59 ms warm on the reference machine (3 runs, 2026-09-01) against a ~420 s suite: under 0.05%.
|
|
1262
|
+
A stamp of `src/` would cost more to keep honest than it saves, and `test/build.test.ts:78`
|
|
1263
|
+
already built unconditionally — this file was the exception.
|
|
1264
|
+
- A new assertion pins the property rather than the mechanism: `dist/tldrx.js` may not predate the
|
|
1265
|
+
newest file under `src/`, `scripts/build.ts` or `package.json`.
|
|
1266
|
+
|
|
1267
|
+
- **`tldrx drive` fills the mandate's `<run>` in (#75).** The mandate's every command read
|
|
1268
|
+
`tldrx next --prepare <run>`, and the header told the reader to find-replace — 7 occurrences
|
|
1269
|
+
unattended, 5 attended (measured; the issue estimated ~8), by hand, at the exact moment somebody
|
|
1270
|
+
is trying to start a run. One occurrence missed sends a session at the wrong run.
|
|
1271
|
+
- **An id, or the one open run.** `tldrx drive --unattended <run>` or `--run <id>` (the positional
|
|
1272
|
+
wins, `ship`'s order) substitutes **textually and never validates** — an id naming no run is the
|
|
1273
|
+
operator's typo to notice, and the command stays the one thing in the CLI that runs anywhere.
|
|
1274
|
+
With no id, the ONE open run of the current workspace is used.
|
|
1275
|
+
- **It refuses to guess between two.** Where `RunStore.resolve` would call it ambiguous, drive
|
|
1276
|
+
declines to substitute, leaves `<run>` standing and names the ids on stderr — a mandate silently
|
|
1277
|
+
aimed at the wrong run is the bug being fixed, not a smaller version of it. No workspace, no
|
|
1278
|
+
runs, an unreadable `tldrx-work/`: all keep the placeholder and still exit `0`.
|
|
1279
|
+
|
|
1280
|
+
- **`tldrx status` no longer calls a RUNNING run "cannot start yet" (#60).** Verbatim from
|
|
1281
|
+
aparece-v2, 2026-09-01: `run 260830-ordering-inventory (…) cannot start yet — it was proposed to
|
|
1282
|
+
follow money-and-payments` / `at 04-build / build · run status running · waiting: prepared` /
|
|
1283
|
+
`blocked by money-and-payments — it is pending`. The run was building, with S1 verified minutes
|
|
1284
|
+
earlier. `triage.depends_on` is an order a split PROPOSED before either run existed, and it was
|
|
1285
|
+
out-ranking what the run was observably doing.
|
|
1286
|
+
- **Observed state outranks proposed order.** One rule, in the resolver both screens read
|
|
1287
|
+
(`src/core/run/dependencies.ts`): a run that has left `pending` has started, and a proposal
|
|
1288
|
+
cannot un-start it. `hasStarted` is the one definition — `pending` is the only status a run that
|
|
1289
|
+
has never run a stage can wear, because runs are created with every stage `pending` and every
|
|
1290
|
+
path to any other value goes through a stage that was `running`.
|
|
1291
|
+
- **The proposal becomes a footnote, not the headline.** A started run renders its own cursor and
|
|
1292
|
+
waiting kind, gains back the command it was denied, and carries
|
|
1293
|
+
`proposed to follow <run> — started anyway` as a secondary line. `blocked by` is now reserved
|
|
1294
|
+
for a run that really cannot move — one that has not started. A run that has NOT started is
|
|
1295
|
+
unchanged: same words, same withheld command.
|
|
1296
|
+
- **It gets the `← next` slot back.** The ordering hint had demoted the only run with work in
|
|
1297
|
+
flight and pointed the owner at the sibling that had not begun. `runnable` is now
|
|
1298
|
+
`movable && (started || nothing outstanding)`.
|
|
1299
|
+
- **`prepared` and `running` get their own summary lines.** Both used to fall through to
|
|
1300
|
+
"is blocked at <phase>/<stage>" — the same wrong word, one layer down.
|
|
1301
|
+
- **The dashboard was making the identical claim** about the same shape and is fixed with it:
|
|
1302
|
+
fixture run `charlie` is `awaiting_gate` behind a `pending` sibling, and the page said
|
|
1303
|
+
"blocked by bravo" while suppressing the gate alert for a signature a person could give right
|
|
1304
|
+
then. `DASHBOARD_MODEL_VERSION` → **3**: `blockedBy` is unchanged and still records the
|
|
1305
|
+
proposal, the new `runs[].started` says whether it still holds anything back, and `runnable`
|
|
1306
|
+
reads `true` for that one shape where it read `false`.
|
|
1307
|
+
|
|
1308
|
+
- **A cost/token declaration on a story commit now attaches to the build task (#68).** Measured on
|
|
1309
|
+
two live runs: the leaderboard host declared $2.25 on a story commit and `tldrx cost` kept
|
|
1310
|
+
04-build at $0.00 — "the declaration didn't attach to the build task the way it did for
|
|
1311
|
+
what/how/plan" — and ordering 260830 showed the same shape. The run's recorded build spend was a
|
|
1312
|
+
floor, budget arithmetic ran blind on the most expensive phase, and ~8M host tokens were invisible.
|
|
1313
|
+
- **The seam, not the accounting.** `ExecutorContext` never carried `--cost-usd`/`--tokens`, so
|
|
1314
|
+
Build's `commit()` could only read the envelope's own `cost_usd` and wrote
|
|
1315
|
+
`round2(result.cost_usd ?? 0)` — a METERED `$0.00`. It now resolves
|
|
1316
|
+
`options.costUsd ?? result.cost_usd`, which is exactly what `commitStage` does for what/how/plan,
|
|
1317
|
+
and the declared tokens ride onto the task row and the `agent.result` payload the cost report
|
|
1318
|
+
reads. The reviewer half (`--commit --review`) takes the same precedence.
|
|
1319
|
+
- **Nothing declared is UNMETERED, not $0.00.** `cost_usd: null` + `metered: false`, the spelling
|
|
1320
|
+
every other host turn already gets, because `$0.00` is a measurement and a false one.
|
|
1321
|
+
- **Backward-safe.** A declaration is never allowed to overwrite a measurement — an envelope that
|
|
1322
|
+
reports its own cost still wins over nothing declared — and no recorded zero is rewritten. Only
|
|
1323
|
+
new declarations attach.
|
|
1324
|
+
|
|
1325
|
+
- **`onStderrLine` on the runtime seam, so progress prints before the verdict it produced (#67).**
|
|
1326
|
+
`SpawnOptions` had `onStdoutLine` and nothing for the other stream, so stderr — where every tldrx
|
|
1327
|
+
UI writes its progress — could only be handed back as one string at exit. `tldrx learn` printed
|
|
1328
|
+
`01-what/what done — $0.31 of $4.00` and only THEN the `[00:00] writing …` lines that produced it.
|
|
1329
|
+
- Implemented in **both** `bunRuntime.ts` and `nodeRuntime.ts`, with the same contract
|
|
1330
|
+
`onStdoutLine` already has: the full text still accumulates in `SpawnResult.stderr`, a trailing
|
|
1331
|
+
partial line is delivered at close, and omitting the callback leaves the buffered path
|
|
1332
|
+
byte-for-byte as it was. Node gets its own `LineSplitter` per stream — one shared buffer would
|
|
1333
|
+
splice a half-written stdout line onto the front of a stderr line.
|
|
1334
|
+
- **The learn workaround is deleted**, per the instructions it carried since phase 1:
|
|
1335
|
+
`realStepRunner` passes the callback and `playChapter` no longer writes `result.stderr` after the
|
|
1336
|
+
step. Measured by playing chapter 2 by hand on both checkouts: the summary was at line 107 with
|
|
1337
|
+
its progress lines at 109-124, and is now at 123 with them at 105-121.
|
|
1338
|
+
- The tests turn on TIMING, not content: a buffered implementation could hand the same lines to the
|
|
1339
|
+
same callback by splitting at exit, so the line is required to arrive while the spawn's promise
|
|
1340
|
+
is still pending.
|
|
1341
|
+
|
|
1342
|
+
- **`tldrx plan` has a section in the CLI reference (#55).** `docs/guide/08-cli-reference.md` had no
|
|
1343
|
+
`## tldrx plan` heading at all, so `sync-dod` — the one mechanical repair for stories whose dod
|
|
1344
|
+
block a `workspace.yml` edit orphaned — was undocumented. The section states the four per-line
|
|
1345
|
+
outcomes, the git-history ancestry, the `.md.bak`, and that it runs no agent and moves no cursor.
|
|
1346
|
+
The #54 docs-coverage test is widened from `run` to `run` and `plan`, and now also asserts the
|
|
1347
|
+
heading rather than a passing mention. It is still a scoped list rather than a generalisation over
|
|
1348
|
+
every command: `hook`'s seven scripts are a deliberate spelling, and `note` and `ship` have the
|
|
1349
|
+
same gap #55 was about (filed separately).
|
|
1350
|
+
|
|
1351
|
+
- **`tldrx update`, and a one-line notice when a newer version exists (#62, owner decision
|
|
1352
|
+
2026-09-01: on by default, with an opt-out).** The owner installed 0.4.0 on a second machine and
|
|
1353
|
+
found there was no way to ask the tool to update itself, and no way to be told a newer one existed.
|
|
1354
|
+
- **`tldrx update` is `npm i -g tldr-experts@latest`, run for you**, plus the part a wrapper does
|
|
1355
|
+
not give you: the CHANGELOG between the version you had and the version you now have.
|
|
1356
|
+
- **The new version is READ BACK from what npm installed** —
|
|
1357
|
+
`$(npm root -g)/tldr-experts/package.json`, with the delta taken from the `CHANGELOG.md` beside
|
|
1358
|
+
it. The process printing that line is the OLD build and cannot know what the new one is; when the
|
|
1359
|
+
read-back fails it says so and prints no changelog rather than inventing one.
|
|
1360
|
+
- **The notice never touches the network on the hot path.** A command reads
|
|
1361
|
+
`~/.tldrx/version-check.json` and nothing else. The registry call happens in a DETACHED child
|
|
1362
|
+
(`stdio: "ignore"`, `unref()`) spawned after the output is written, and its answer is for the
|
|
1363
|
+
NEXT invocation. Cached for 24 h; silent on any network failure, on a body that is not the JSON
|
|
1364
|
+
it asked for, and on a home directory it cannot write.
|
|
1365
|
+
- **Never in `--json`, never during a hook, never off a terminal.** `tldrx hook` and
|
|
1366
|
+
`tldrx statusline` are suppressed by name (spec §0: a hook is deterministic), `--json` anywhere
|
|
1367
|
+
in argv is suppressed, a non-TTY stdout is suppressed, and so is CI. The line itself goes to
|
|
1368
|
+
stderr, so no command's stdout changes shape.
|
|
1369
|
+
- One line, in the issue's own wording: `tldr-experts 0.5.0 available (you have 0.4.0) — tldrx
|
|
1370
|
+
update`. Asserted as an exact string, because "roughly this sentence" is how one line becomes
|
|
1371
|
+
three.
|
|
1372
|
+
- **Opt out** with `TLDRX_UPDATE_CHECK=off` for a shell (spelled like `TLDRX_UI` and
|
|
1373
|
+
`TLDRX_CLAUDE_BIN`; `0`, `false`, `no` and `never` also work), or `update_check: off` in
|
|
1374
|
+
`~/.tldrx/config.yml` for the machine. A config file that does not parse is not an opt-out and
|
|
1375
|
+
not a crash.
|
|
1376
|
+
|
|
1377
|
+
- **`budget.yml` no longer adds host tokens to dollars (#61, owner decision 2026-09-01).**
|
|
1378
|
+
`validateRunBudget` summed EVERY phase ceiling and compared the total to `ceiling_usd` — but since
|
|
1379
|
+
the `economy:` label landed, a phase priced in `host-tokens` carries a host-session token allowance
|
|
1380
|
+
and not money. A realistic allowance therefore made a valid file invalid
|
|
1381
|
+
(`phase ceilings sum to 200018 > ceiling_usd 25`), and it did so in the one check that fails
|
|
1382
|
+
**closed**: `RunStore.open` threw, and the budget-gate hook then denied every spawn on the run.
|
|
1383
|
+
- **Separate ceilings, per economy.** `ceiling_usd` stays dollars-only and a new optional
|
|
1384
|
+
`ceiling_host_tokens` — at the run level and per phase — is the host-token allowance. The two are
|
|
1385
|
+
never added and never converted: there is no exchange rate here, and inventing one would be a
|
|
1386
|
+
guess about a price, which is the whole reason the label exists.
|
|
1387
|
+
- **The sum runs once per economy.** Σ the `metered-usd` phases against `ceiling_usd`; Σ the
|
|
1388
|
+
`host-tokens` phases against `ceiling_host_tokens`. The dollar half is byte-identical arithmetic
|
|
1389
|
+
with the same refusal in the same words.
|
|
1390
|
+
- **Additive, and absence changes nothing.** With no `ceiling_host_tokens` declared, the token sum
|
|
1391
|
+
has nothing to compare against and is not checked — deliberately the lax side, since the only
|
|
1392
|
+
other number on the run is dollars. The compat bar is the live `260830-ordering-inventory`
|
|
1393
|
+
budget.yml, mid-Build in another workspace while this was written and asserted verbatim in
|
|
1394
|
+
`test/economy.test.ts`: it validates, its phase sum of exactly 62.00 against a 62.00 ceiling still
|
|
1395
|
+
passes, one cent more is still refused in the same words, and every phase still reads
|
|
1396
|
+
`metered-usd` with no token ceiling.
|
|
1397
|
+
- `hostTokenCeiling` (f353d8d, #22b) prefers the new field and **still falls back to `ceiling_usd`**,
|
|
1398
|
+
because the files written before it existed put the allowance in that one unlabelled scalar and
|
|
1399
|
+
they are still on disk. The emitter round-trips both, so `budget raise` cannot erase a token
|
|
1400
|
+
ceiling it rewrites past.
|
|
1401
|
+
- The workaround the issue documented is gone: `test/hooks.test.ts` and `test/economy.test.ts` no
|
|
1402
|
+
longer raise the run ceiling to `60000`/`100000` — dollar figures that meant nothing — to let a
|
|
1403
|
+
token phase validate.
|
|
1404
|
+
- **`RunStore.ceilingsToWrite` now carries a phase's `economy` and `ceiling_host_tokens` from disk**
|
|
1405
|
+
beside its `ceiling_usd`. That seam re-reads CEILINGS before every save so a raise landing
|
|
1406
|
+
mid-stage is not clobbered by the copy the process opened with — and a phase's ceiling is three
|
|
1407
|
+
fields, not one. Preserving the number without the label would have been the worse half of both:
|
|
1408
|
+
a token allowance kept on a phase this reader had been told is priced in dollars, where
|
|
1409
|
+
`hostTokenCeiling` can no longer see it. For a file with no labels and no token ceilings — every
|
|
1410
|
+
budget.yml on disk today, the live one included — all three are identical on both sides and the
|
|
1411
|
+
save writes exactly what it always wrote.
|
|
1412
|
+
|
|
1413
|
+
### Removed
|
|
1414
|
+
|
|
1415
|
+
- **`templates/story.md` and `templates/epic.md` are deleted (#48, owner decision 2026-09-01,
|
|
1416
|
+
option (a)).** They stated the Plan front-matter schema, shipped in the npm package, and
|
|
1417
|
+
`grep -rn 'story\.md' src/` found nothing that read either one. Since 3ae0ce9 the live copy is
|
|
1418
|
+
generated: `src/core/plan/schemaContract.ts` builds the story, the epic and `waves.yml` from
|
|
1419
|
+
`STORY_KEYS` / `EPIC_KEYS` / `PLAN_STATUSES` / the `MAX_*` constants and splices them into the Plan
|
|
1420
|
+
prompt. The drift guard 7ac298c added held the two files to that contract; deleting them removes
|
|
1421
|
+
the second copy instead of maintaining it.
|
|
1422
|
+
- **The consumers were tests, and they now generate.** `test/plan.test.ts` and
|
|
1423
|
+
`test/plan-schema-contract.test.ts` were the only readers; both take the story and the epic from
|
|
1424
|
+
`planContractExamples()`. Nothing in `src/`, `stages/`, `workflows/`, `plugin/` or `docs/` read
|
|
1425
|
+
either file, and `run new` never copied them.
|
|
1426
|
+
- **`templates` stays in `package.json` → `files`.** The directory still ships eleven templates
|
|
1427
|
+
that ARE read at runtime — `templates/expert.md` and `templates/experts/<role>.md` are read by
|
|
1428
|
+
`createExpert.ts` and `roleExperts.ts` in an installed package — so removing the entry would
|
|
1429
|
+
break `tldrx expert create` to delete two files that no longer exist.
|
|
1430
|
+
- **The drift guard changed meaning and kept its teeth.** It now asserts the GENERATED story and
|
|
1431
|
+
epic validate through `validateStoryFile` / `validateEpicFile` with keys equal to `STORY_KEYS` /
|
|
1432
|
+
`EPIC_KEYS` in order, that neither file is back on disk, and that no OTHER shipped template has
|
|
1433
|
+
grown the same front matter under a new name. Proven, not assumed: setting the example's
|
|
1434
|
+
`status:` to `wip` turns 4 tests red, and renaming `test_plan` in the generator turns 6 red.
|
|
1435
|
+
|
|
3
1436
|
## 0.4.0 — 2026-09-01
|
|
4
1437
|
|
|
5
1438
|
### Changed
|