tldr-experts 0.14.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,203 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## 0.14.2 — 2026-09-09
5
+
6
+ ### Fixed
7
+
8
+ - **A stage's `claim-sources` check reads the disk it is judging, not a snapshot taken stages
9
+ ago (#206).** `src/core/text/srcToken.ts` memoises the question and fact indexes at MODULE
10
+ scope, keyed by run dir and workspace root, and nothing outside `test/` ever dropped them —
11
+ `clearSrcCaches()` had 20 call sites, all in tests. `tldrx run auto` is one Node process for a
12
+ whole run (`runAuto.ts`, no re-exec per stage), so the FIRST citation resolved anywhere in the
13
+ run froze both indexes at whatever was on disk at that instant, and every later stage's check
14
+ judged its own outputs against that frozen view. Measured live on three real workspaces at
15
+ 0.14.1: a `how` stage refused with `no such question Q2 … declared: Q1` over Q2–Q4 it had
16
+ itself written into `02-how/questions.md` minutes earlier, and another refused with
17
+ `it has 145 live fact(s)` over a `facts.yml` that held 148 — the three extra facts written by
18
+ the owner's own answers two seconds BEFORE the stage started. Roughly $11 of paid turns thrown
19
+ away for ids that were real the whole time. The indexes are now refreshed inside
20
+ `toSrcContext` — the one place a citation context is built, and a place every caller reaches
21
+ exactly once per check, gate or hook invocation — so the memoisation that pays for itself (one
22
+ document's forty citations read `facts.yml` once) is kept and the memory that outlives the
23
+ check is not. The git blob memo is deliberately left per-process: its cap bounds forks, not
24
+ staleness. Resolution rules are unchanged; what changed is when the checker last looked.
25
+
26
+
27
+ ## 0.14.1 — 2026-09-09
28
+
29
+ ### Fixed
30
+
31
+ - **A training test no longer runs on the fixed 5000 ms default: the spawner heuristic now
32
+ names the training shape (#194).** `test/machine-load.test.ts` decides which files must take
33
+ the load-aware budget by READING each test file for one of a few literal markers, and a
34
+ training turn matched none of them — it spawns the agent through `spawnAgent.ts`, two imports
35
+ away from any `Bun.spawn` the test file itself writes. Measured on `main` at cd8721a with a
36
+ probe that wraps `Bun.spawn` and `node:child_process` (validated first against a file the
37
+ heuristic already claims, which recorded 87 spawns, so the instrument can see the thing):
38
+ `test/knowledge-value.test.ts` spawned **3** real `claude` children while
39
+ `grep -c setDefaultTimeout` on it returned **0** — bun's fixed 5000 ms, which is the false RED
40
+ #43 was filed about, on the box's clock rather than the assertion's. The list now also names
41
+ `runTraining` and `makeTrainingWorkspace`, which claims two more files (81 → 83) and adds two
42
+ guard rows; `knowledge-value.test.ts` takes `spawnTestTimeout()`. The non-vacuity test is
43
+ anchored on that file as well as on `cli.test.ts`, because deleting a marker from a proxy list
44
+ otherwise shrinks the row set into a SHORTER green instead of a red — a marker list that can
45
+ quietly stop covering something is how this hid in the first place. The list is still a proxy:
46
+ the same probe found 14 more spawning files it does not claim, filed with the per-file counts
47
+ as #201.
48
+ - **The five phase ids are written out in one file now, not three, and a shape test refuses
49
+ the fourth (#187).** Measured on `main` at cd8721a: `PHASE_IDS`
50
+ (`src/core/run/workflowPreset.ts:28`) and `QUESTION_PHASES`
51
+ (`src/core/run/questionCards.ts:52`) were two `as const` array literals with the same five
52
+ ids in the same order, and `grep -rln "QUESTION_PHASES\|PHASE_IDS" test/` exited **1** —
53
+ nothing anywhere pinned them equal. Reading for the shape rather than the two names found a
54
+ THIRD: `questionFiles` in `src/cli/commands/questions.ts:175`, the list `questions lint`
55
+ walks, inline and unnamed — while the doc comment over `QUESTION_PHASES` claimed the list was
56
+ "kept in one place so the two verbs of `tldrx questions` cannot disagree", which was false of
57
+ the lint verb the sentence names. `workflowPreset.ts` owns the list; the other two import it,
58
+ so every existing consumer's import line is untouched and the `as const` tuple type survives
59
+ the indirection (proved with a type probe: the negative case, `99-nope` in slot five, fails
60
+ `tsc` with TS2322). `test/phase-ids-one-derivation.test.ts` pins it from both ends, because
61
+ neither end alone is enough: an identical second copy reddens the shape guard, and a copy
62
+ that has already drifted slips past that pattern and reddens the equality guard instead.
63
+ - **The changelog no longer credits a release with work it did not ship, and a gate now says so
64
+ (#200).** Measured on `main` at 0.14.0: `awk '/^## /{h=$0} /wait-gates/{print h}' CHANGELOG.md`
65
+ put #197's `--wait-gates` and `gate.timeout` bullets under `## 0.13.0 — 2026-09-08`, while
66
+ `git show v0.13.0:CHANGELOG.md | grep -c wait-gates` returned **0** — the tag that section
67
+ names never carried the flag. They were merged in 069a73e, after v0.13.1 was cut, and shipped
68
+ in v0.14.0; the implementer appended them to the FIRST `### Added` in the file instead of the
69
+ unreleased heading, which is the whole mechanism. Two older sections had the same slip:
70
+ 0.3.1 carried a528985's four bullets (73 lines, committed 07:06 on 2026-08-31, twenty-six
71
+ minutes after v0.3.1 was tagged, first shipped in v0.4.0) and 0.6.1 had lost a blank line. All three are
72
+ now byte-identical to their tags, and the bullets sit under the release that actually shipped
73
+ them — a released section is **restored**, never edited, and the two corrections that had to
74
+ land inside a tagged section (0.14.0 and 0.4.0) are recorded in `CHANGELOG.amendments` rather
75
+ than smuggled in. The v0.14.0 GitHub release notes were re-published from the corrected
76
+ section and now carry both moved bullets; v0.13.0's were generated at tag time from a changelog
77
+ that did not carry them and are already right, so they are untouched.
78
+ - **`scripts/release-check.sh` refuses a rewritten released section.** For every dated
79
+ `## X.Y.Z — YYYY-MM-DD` heading whose tag is present, the section's text must equal that
80
+ section at `git show vX.Y.Z:CHANGELOG.md` — the one copy nobody can edit afterwards — and a
81
+ mismatch fails by version, quoting the first differing line and the remedy. Nothing checked
82
+ this before, which is why three sections drifted without anyone noticing. It runs in both
83
+ `--pre-push` and `--ci`, and never fails for a reason it cannot judge: a checkout that fetched
84
+ no tags (`publish.yml`'s does not) and a tag whose own section still said `unreleased` (v0.0.2,
85
+ which predates the dating convention) are skipped and COUNTED in one line, because a silent
86
+ skip is how a gate becomes a decoration. A deliberate correction is recorded in
87
+ `CHANGELOG.amendments` (`<version> <source-sha> <why>`) — a second file on purpose, since the
88
+ failure being caught is an append nobody meant and an append edits one file. That listing is
89
+ **not a licence**, which a reviewer proved by injecting an invented bullet into an amended
90
+ section and watching the first version of this check pass it: an amended section must still
91
+ contain the tag's section as an ORDERED SUBSEQUENCE (nothing deleted, nothing reworded), and
92
+ every line it adds must exist verbatim in `<source-sha>:CHANGELOG.md`. Both halves say the same
93
+ thing — an amendment moves text the changelog already carried, and cannot write a new claim
94
+ into a shipped release. A source sha that is not a commit here is refused, not trusted.
95
+ - **An auto gate now says WHY it did not close, and the reason reaches the phone (#203).**
96
+ Measured on a real workspace, 0.14.0, the first `run auto --wait-answers 4h --wait-gates 4h`
97
+ with the notify hook: the `what` stage finished for $1.98, every declared check passed, the
98
+ policy was `auto`, and the owner got *"waiting at a auto gate that did not close by itself —
99
+ a person signs it"*. No reason. The cause was four open questions, and `evaluateAutoGate`
100
+ had computed exactly that sentence — into a stdout line nobody was watching, because
101
+ `gate.requested` was appended a hundred lines BEFORE the verdict existed. The verdict is now
102
+ taken one statement earlier, the event carries `why` and `held_by`, and the notification
103
+ renders them. Both keys are present only for an `auto` policy: on a `human` or `agent` gate
104
+ nothing measured the seven conditions, and `held_by: []` there would read as "checked, and
105
+ nothing held it".
106
+ - **The questions are notified before the gate that is downstream of them (#203).** When the
107
+ ONLY failing condition is `questions`, `question.raised` goes out first and `gate.requested`'s
108
+ notification is held back — sent only if the gate is still pending once the questions settle,
109
+ or when `--wait-gates` lapses, and never at all when the loop closes the gate itself. The
110
+ owner used to be told to sign something before being told what the thing was, which sent him
111
+ to the wrong tap. The EVENT is appended either way: this defers a notification, never an
112
+ audit record.
113
+ - **An `auto` gate closes itself when the thing holding it clears (#203).** `--wait-gates` used
114
+ to poll `gate.status` and nothing else, so an auto gate whose only blocker was an open
115
+ question permanently degraded into a `human` gate for that stage: the owner answered all four
116
+ and nothing signed. Each poll now re-runs the seven conditions off disk for an `auto` policy
117
+ and, when every one holds, signs through the **same `approve` door** `next` uses — checks
118
+ re-run off disk, actor `auto`, the seven-condition note, the ordinary `gate.approved`. Never
119
+ for `human` and never for `agent`; a person's `approve` or `reject` still lands first and
120
+ overrides at any moment, and a refusal from `approve` leaves the gate waiting rather than
121
+ reporting a close.
122
+ - **"a auto gate" is now "an auto gate".** One article, on the one sentence that reaches a lock
123
+ screen, chosen by a helper rather than a literal because it depends on the policy word and
124
+ `auto` is the only vowel among the four.
125
+
126
+ ### Added
127
+
128
+ - **A question can carry its own recommendation: `Recommended: <letter> — <why> [src: …]`
129
+ (#203).** Until now the only recommendation slot in the framework was the `agent` gate's
130
+ evidence note, which an `auto` gate never writes — so on the run above all four questions
131
+ rendered `recommendation: null`, while the stage that raised them was the one thing in the
132
+ run that knew the trade-off. The §2.7 block grammar gains one optional line, after the
133
+ options and before the `[Answer]:` slot, and the what/how/plan prompts and
134
+ `templates/questions.md` ask for one on every question with a real `[src:]`. The parse is
135
+ strict about the two machine-readable parts (one letter `A`–`E`, a dash before any prose) so
136
+ that it can be tolerant about everything else: **a line that does not match is ignored, never
137
+ refused** — it is guidance, so a typo costs the guidance and not the gate — and
138
+ `questions lint` says nothing about it. Precedence, stated once: an evidence note's
139
+ `recommend:` entry wins over the block's line for the same question id, and a question with
140
+ neither still renders no recommendation at all. Nothing manufactures one.
141
+
142
+ - **`tldrx facts add --repo <name>` is now checked against `workspace.yml`, through the one
143
+ implementation `tldrx answer --repo` already refused on (#186).** Measured on a scratch
144
+ workspace at `cd8721a`: `facts add "ghost repo test" --area test --decided-by driver --repo
145
+ ghost` exited **0** and wrote `repos: [ghost]`, while `answer Q1 "…" --repo ghost` on the same
146
+ workspace exited **1** with `--repo ghost is not a repo in this workspace — it has api`. The
147
+ mechanism was the whole story — `grep -n loadWorkspace src/cli/commands/facts.ts` exited 1:
148
+ that path never looked at what the workspace declares, it copied the flag onto the record. A
149
+ mis-scoped fact is the silent kind of wrong: nothing refuses it later, it is simply invisible
150
+ to every `{{facts}}` filter keyed on the real repo name, forever, and nothing anywhere says
151
+ why. The check is now a leaf (`src/cli/repoScope.ts`) both commands call rather than a
152
+ sentence copied into two files — two copies of a refusal drift, and the drift is silent
153
+ because each command's own test keeps passing — so `facts add` also inherits the
154
+ de-duplication the same flag already had on `answer` (`--repo api --repo api` scopes once).
155
+ Refused before the store is opened, exit **1**, `answer`'s family and `answer`'s wording,
156
+ including the "this workspace declares no repos" variant that keeps the sentence from
157
+ dangling. A test counts the sentence over `src/` and fails on the second copy.
158
+
4
159
  ## 0.14.0 — 2026-09-09
5
160
 
6
161
  ### Added
7
162
 
163
+ - **`tldrx run auto --wait-gates <duration>` — the loop waits for a signature the way it
164
+ already waits for an answer (#197).** Measured on a real workspace the day the notify hook
165
+ first drove a run: the question loop closed itself — the hook delivered Q1, the owner
166
+ answered from his chat, and `run auto` printed `waited 132s … resuming` — and then the very
167
+ next thing the stage did was reach its human gate, where the same owner approved from the
168
+ same chat and the loop had already exited 4. Three human gates a feature run is three manual
169
+ re-launches the hook was supposed to make unnecessary. The cause was scoped, not accidental:
170
+ `--wait-answers` gates its whole wait on an open-question card, which is `null` when the park
171
+ is a gate, so the wait was never reached. `--wait-gates` is a sibling flag rather than a wider
172
+ `--wait-answers`, because the two parks are closed by different verbs and calling a signature
173
+ an "answer" would be the flag name lying about what a person did. Approved → the loop carries
174
+ on; rejected → it stops and prints the note, which is now its LAST line so the `run.failed`
175
+ payload carries it to the phone of whoever has to act on it; lapsed → exit 4 with the same
176
+ lines it always had, after one `gate.timeout` (new kind, `question.timeout`'s twin, carrying
177
+ the approve and reject lines, the gate's policy, `waited_ms`, and `cost_usd` only when this
178
+ loop is the one that measured it). Nothing is spent while it polls — it reads files — and
179
+ whether a gate is pending is read through `waitingFor`, the one derivation `tldrx run status`
180
+ and the dashboard already share, never a second copy. **It waits FOR a signature and never
181
+ produces one**: there is no engine-side signing in this loop, so a stage on
182
+ `gates_policy: agent` stops it exactly as a `human` one does and is waited on identically —
183
+ an owner who switched three gates to `agent` expecting the loop to carry on was measuring
184
+ who MAY sign, not that anything had. Both wait flags may be given together, and without the
185
+ flag a gate exits 4 on the spot exactly as before.
186
+ - **The heartbeat stops telling a waiting owner that nothing is waiting on him at a GATE.**
187
+ `--notify-every`'s `status` payload learned in 0.11.1 not to say "Nothing is waiting on you"
188
+ over a run parked on a question — the fix that exists because a heartbeat is believed — but
189
+ its parked-ness came from the blocking-question predicate alone, so a run parked on a
190
+ signature got `waiting_on: []` and the exact sentence the fix was written to prevent. A
191
+ pending gate is now named in `waiting_on_gate` (`<phase>/<stage>`) beside `gate_policy`, the
192
+ summary says the run is waiting for a person to sign that stage, and `command` is the literal
193
+ `tldrx approve` line — the same spelling `gate.requested` and every decision card use, now
194
+ one exported helper instead of four literals. `waiting_on_gate` is a SIBLING key rather than
195
+ a member of `waiting_on`: an adapter maps every id in `waiting_on` to `tldrx answer <id>`,
196
+ and a stage id there would make it build a command nobody can type. Both keys are absent when
197
+ no gate is pending, so a heartbeat over a moving run is byte-identical to the one it sent
198
+ before. `gate.requested` now names the policy too, so an owner reading it on a phone knows
199
+ whether he is signing a `human` gate or overriding an `agent` one.
200
+
8
201
  - **`run auto` now closes an `agent` gate itself, over a note it wrote and had validated (#198).**
9
202
  `gates_policy: agent` said who MAY sign a gate; nothing in the engine produced the evidence note
10
203
  it is signed over. The only writer was the `tldrx gate template` skeleton a host session fills
@@ -112,44 +305,6 @@
112
305
 
113
306
  ### Added
114
307
 
115
- - **`tldrx run auto --wait-gates <duration>` — the loop waits for a signature the way it
116
- already waits for an answer (#197).** Measured on a real workspace the day the notify hook
117
- first drove a run: the question loop closed itself — the hook delivered Q1, the owner
118
- answered from his chat, and `run auto` printed `waited 132s … resuming` — and then the very
119
- next thing the stage did was reach its human gate, where the same owner approved from the
120
- same chat and the loop had already exited 4. Three human gates a feature run is three manual
121
- re-launches the hook was supposed to make unnecessary. The cause was scoped, not accidental:
122
- `--wait-answers` gates its whole wait on an open-question card, which is `null` when the park
123
- is a gate, so the wait was never reached. `--wait-gates` is a sibling flag rather than a wider
124
- `--wait-answers`, because the two parks are closed by different verbs and calling a signature
125
- an "answer" would be the flag name lying about what a person did. Approved → the loop carries
126
- on; rejected → it stops and prints the note, which is now its LAST line so the `run.failed`
127
- payload carries it to the phone of whoever has to act on it; lapsed → exit 4 with the same
128
- lines it always had, after one `gate.timeout` (new kind, `question.timeout`'s twin, carrying
129
- the approve and reject lines, the gate's policy, `waited_ms`, and `cost_usd` only when this
130
- loop is the one that measured it). Nothing is spent while it polls — it reads files — and
131
- whether a gate is pending is read through `waitingFor`, the one derivation `tldrx run status`
132
- and the dashboard already share, never a second copy. **It waits FOR a signature and never
133
- produces one**: there is no engine-side signing in this loop, so a stage on
134
- `gates_policy: agent` stops it exactly as a `human` one does and is waited on identically —
135
- an owner who switched three gates to `agent` expecting the loop to carry on was measuring
136
- who MAY sign, not that anything had. Both wait flags may be given together, and without the
137
- flag a gate exits 4 on the spot exactly as before.
138
- - **The heartbeat stops telling a waiting owner that nothing is waiting on him at a GATE.**
139
- `--notify-every`'s `status` payload learned in 0.11.1 not to say "Nothing is waiting on you"
140
- over a run parked on a question — the fix that exists because a heartbeat is believed — but
141
- its parked-ness came from the blocking-question predicate alone, so a run parked on a
142
- signature got `waiting_on: []` and the exact sentence the fix was written to prevent. A
143
- pending gate is now named in `waiting_on_gate` (`<phase>/<stage>`) beside `gate_policy`, the
144
- summary says the run is waiting for a person to sign that stage, and `command` is the literal
145
- `tldrx approve` line — the same spelling `gate.requested` and every decision card use, now
146
- one exported helper instead of four literals. `waiting_on_gate` is a SIBLING key rather than
147
- a member of `waiting_on`: an adapter maps every id in `waiting_on` to `tldrx answer <id>`,
148
- and a stage id there would make it build a command nobody can type. Both keys are absent when
149
- no gate is pending, so a heartbeat over a moving run is byte-identical to the one it sent
150
- before. `gate.requested` now names the policy too, so an owner reading it on a phone knows
151
- whether he is signing a `human` gate or overriding an `agent` one.
152
-
153
308
  - **`tldrx expert rescore [<name>] [--area <a>]` — score the knowledge you already paid for.**
154
309
  Its sibling `recompute` is arithmetic over the evidence rows already in `competencies.yml`;
155
310
  `rescore` RE-READS `knowledge/*.md` and derives their evidence again under today's rules. It
@@ -1487,6 +1642,7 @@
1487
1642
  correctly and is spliced into no prompt is the `templates/story.md` failure again (#48).
1488
1643
  6 red before, green after. `docs/spec.md` §2.13 and the `delivery` expert each carry one
1489
1644
  line pointing at the checklist.
1645
+
1490
1646
  - **The landing page now sells the unattended flow it never mentioned (#128).** Measured at
1491
1647
  `95a39db`: `grep -c 'tldrx drive' docs-site/index.md` returned `0`, and so did its Spanish
1492
1648
  twin — a bare `grep drive` exited `1` on both. `tldrx drive` is the star command for handing
@@ -4073,6 +4229,79 @@ same amount after it; what changed is that the page now says how big the bound i
4073
4229
 
4074
4230
  ### Fixed
4075
4231
 
4232
+ - **A trainer that `cd`s no longer writes its knowledge file into a different git repo.**
4233
+ Measured 2026-08-31 on `~/scavtopia` (five repos, ten `expert train --mode light` runs): the
4234
+ `mcp` run was rejected with `mcp.md.partial was never written`, and the file had been written —
4235
+ 46 lines, 9,567 bytes, complete and usable — to
4236
+ `whiteboard/.tldrx/experts/mcp/knowledge/mcp.md.partial`. The sub-agent ran
4237
+ `cd <workspace>/whiteboard` to execute that repo's declared gate command, then wrote the
4238
+ RELATIVE path the prompt had given it, and the path resolved against the repo it had `cd`'d
4239
+ into. Three costs from the one bug: **$1.23 charged for work that was finished and then
4240
+ orphaned**, a parasitic `.tldrx/` tree left inside an unrelated git repo (`git -C whiteboard
4241
+ status` → `?? .tldrx/`), and **no repair round possible** — the missing-file branch returns
4242
+ before the repair check, so this failure mode was unrecoverable by construction even with
4243
+ budget left. Fixed at both ends.
4244
+ - **Prevention: the prompt now states an ABSOLUTE output path**, workspace-root-resolved, and
4245
+ says why — "If you `cd` into a repo to run its gate command, a relative path then resolves
4246
+ against THAT repo … and throws the whole paid run away. That is measured, not hypothetical."
4247
+ Both training prompts carry it, and so does the repair round's target, for the same reason.
4248
+ - **Recovery: when the file is missing, the declared repo roots are probed** for the stray
4249
+ relative write before "never written" is said. A file found there is moved back and validated
4250
+ exactly as if it had landed correctly — recovery is not a pass, the same `parseKnowledgeFile`
4251
+ still judges it, and a recovered file that fails can still be repaired because the probe runs
4252
+ ABOVE the repair round.
4253
+ - **The note is honest and names the mess.** `recovered: the trainer wrote to
4254
+ whiteboard/.tldrx/… , inside the `whiteboard` repo — a relative `.tldrx/…` path resolves
4255
+ against whatever directory it had `cd`'d into.` The empty parasitic directories are removed
4256
+ on the way out; a directory holding anything else is **left in place and named**, with the
4257
+ `git -C <repo> status` to run, because a tool that deletes inside a repo it was never asked
4258
+ to touch is a worse bug than the one it is fixing. A repo carrying its own
4259
+ `.tldrx/workspace.yml` is skipped entirely — that file may belong to a nested workspace, and
4260
+ taking it would be theft rather than recovery.
4261
+ - When no stray is found the verdict is unchanged and now says where it looked.
4262
+
4263
+ - **A rejected training run records WHICH problems, not just how many.**
4264
+ Measured 2026-08-31: `components` failed with 12 problems for $1.02, and `training.jsonl` — the
4265
+ durable record — held only the string `"…does not validate — 12 problem(s)"`. The twelve went
4266
+ to stdout, where five of them were printed and the rest elided as `(+7 more)`. Anyone who had
4267
+ not captured stdout, which is anyone running this normally, could not tell why a $1.02 run
4268
+ failed. The list is now persisted twice.
4269
+ - **On the ledger**: `check.failed.payload` carries `problems` (the rendered per-problem
4270
+ lines), `problems_total`, `errors`, and `task`. The list is fitted to the record's 4 KB
4271
+ payload cap and reports `problems_omitted` when it does not fit — an append that THROWS on
4272
+ an oversize payload would take the cost line down with the reasons, which is the opposite of
4273
+ the point. The repair round's own `check.failed` carries what it sent back, so "what did the
4274
+ repair actually fix" is answerable later.
4275
+ - **In the file**: `<area>.rejected.md` now opens with a `# REJECTED` header — expert/area,
4276
+ mode, timestamp, dollars spent, error and warning counts, and every problem, uncapped —
4277
+ above the trainer's bytes exactly as written, separated by a rule. A quarantine with no
4278
+ verdict (a sub-agent that died, a rollback) gets no header: there were no reasons to state
4279
+ and inventing them would be inventing the reason.
4280
+
4281
+ - **`## Sources` is now taught as prose with the refused shape shown.** Same batch: four of the
4282
+ five problems the `components` report printed are one mistake four times — `L34 Sources: no
4283
+ [src: …] token`, `L35`, `L36`, `L37`. The trainer had written the recap as a bulleted list.
4284
+ The prompt already said "**Sources** — prose", and a writer who reads that as a style note
4285
+ writes bullets, because bullets are what the other four sections take. The rule it collides
4286
+ with is genuinely file-wide — `parseKnowledgeFile` requires a `[src: …]` token on EVERY list
4287
+ item in every declared section, recap included, and an unsourced one is an error that rejects
4288
+ the file whole. Both prompts now show the accepted prose next to the refused bullets, the same
4289
+ move the execution-claim rule makes. Whether an unsourced recap bullet should be a warning
4290
+ rather than an error is a real question and is deliberately NOT settled here.
4291
+
4292
+
4293
+ - **`tldrx expert train` already exits nonzero when a training fails.** The 2026-08-31 batch
4294
+ report measured shell `EXIT=0` on all ten invocations, including the three that failed their
4295
+ check — but nine of those ten ran on a build that predates this one (`dist/tldrx.js` was
4296
+ rewritten mid-batch at 05:40Z). On the current source the code path is intact:
4297
+ `runTraining` returns `EXIT_AGENT_FAILED` (5), `expert train` returns `outcome.code`,
4298
+ `dispatch` returns it, and `bin/tldrx.ts` does `process.exit(await dispatch(...))`. Now pinned
4299
+ by three tests that drive the REAL CLI as a subprocess with a fake `claude` on PATH and assert
4300
+ the PROCESS exit code — one for a file that does not validate, one for a file that was never
4301
+ written, one for the passing case — because "`runTraining` returns 5" and "the process exits
4302
+ 5" are two different claims. Falsified before being trusted: making `expert train` return
4303
+ `EXIT_OK` breaks two of the three.
4304
+
4076
4305
  - **`tldrx learn` — the cold-player QA round (#30).** A first-time player played all eight chapters
4077
4306
  and returned SHIP-with-fixlist. Everything they found is fixed or recorded:
4078
4307
  - **Chapter 8 no longer lies about the brake.** It said "the phase has already spent its Watch
@@ -5274,66 +5503,6 @@ none of these behaves byte-identically to the release before them.
5274
5503
 
5275
5504
  ### Fixed
5276
5505
 
5277
- - **A trainer that `cd`s no longer writes its knowledge file into a different git repo.**
5278
- Measured 2026-08-31 on `~/scavtopia` (five repos, ten `expert train --mode light` runs): the
5279
- `mcp` run was rejected with `mcp.md.partial was never written`, and the file had been written —
5280
- 46 lines, 9,567 bytes, complete and usable — to
5281
- `whiteboard/.tldrx/experts/mcp/knowledge/mcp.md.partial`. The sub-agent ran
5282
- `cd <workspace>/whiteboard` to execute that repo's declared gate command, then wrote the
5283
- RELATIVE path the prompt had given it, and the path resolved against the repo it had `cd`'d
5284
- into. Three costs from the one bug: **$1.23 charged for work that was finished and then
5285
- orphaned**, a parasitic `.tldrx/` tree left inside an unrelated git repo (`git -C whiteboard
5286
- status` → `?? .tldrx/`), and **no repair round possible** — the missing-file branch returns
5287
- before the repair check, so this failure mode was unrecoverable by construction even with
5288
- budget left. Fixed at both ends.
5289
- - **Prevention: the prompt now states an ABSOLUTE output path**, workspace-root-resolved, and
5290
- says why — "If you `cd` into a repo to run its gate command, a relative path then resolves
5291
- against THAT repo … and throws the whole paid run away. That is measured, not hypothetical."
5292
- Both training prompts carry it, and so does the repair round's target, for the same reason.
5293
- - **Recovery: when the file is missing, the declared repo roots are probed** for the stray
5294
- relative write before "never written" is said. A file found there is moved back and validated
5295
- exactly as if it had landed correctly — recovery is not a pass, the same `parseKnowledgeFile`
5296
- still judges it, and a recovered file that fails can still be repaired because the probe runs
5297
- ABOVE the repair round.
5298
- - **The note is honest and names the mess.** `recovered: the trainer wrote to
5299
- whiteboard/.tldrx/… , inside the `whiteboard` repo — a relative `.tldrx/…` path resolves
5300
- against whatever directory it had `cd`'d into.` The empty parasitic directories are removed
5301
- on the way out; a directory holding anything else is **left in place and named**, with the
5302
- `git -C <repo> status` to run, because a tool that deletes inside a repo it was never asked
5303
- to touch is a worse bug than the one it is fixing. A repo carrying its own
5304
- `.tldrx/workspace.yml` is skipped entirely — that file may belong to a nested workspace, and
5305
- taking it would be theft rather than recovery.
5306
- - When no stray is found the verdict is unchanged and now says where it looked.
5307
-
5308
- - **A rejected training run records WHICH problems, not just how many.**
5309
- Measured 2026-08-31: `components` failed with 12 problems for $1.02, and `training.jsonl` — the
5310
- durable record — held only the string `"…does not validate — 12 problem(s)"`. The twelve went
5311
- to stdout, where five of them were printed and the rest elided as `(+7 more)`. Anyone who had
5312
- not captured stdout, which is anyone running this normally, could not tell why a $1.02 run
5313
- failed. The list is now persisted twice.
5314
- - **On the ledger**: `check.failed.payload` carries `problems` (the rendered per-problem
5315
- lines), `problems_total`, `errors`, and `task`. The list is fitted to the record's 4 KB
5316
- payload cap and reports `problems_omitted` when it does not fit — an append that THROWS on
5317
- an oversize payload would take the cost line down with the reasons, which is the opposite of
5318
- the point. The repair round's own `check.failed` carries what it sent back, so "what did the
5319
- repair actually fix" is answerable later.
5320
- - **In the file**: `<area>.rejected.md` now opens with a `# REJECTED` header — expert/area,
5321
- mode, timestamp, dollars spent, error and warning counts, and every problem, uncapped —
5322
- above the trainer's bytes exactly as written, separated by a rule. A quarantine with no
5323
- verdict (a sub-agent that died, a rollback) gets no header: there were no reasons to state
5324
- and inventing them would be inventing the reason.
5325
-
5326
- - **`## Sources` is now taught as prose with the refused shape shown.** Same batch: four of the
5327
- five problems the `components` report printed are one mistake four times — `L34 Sources: no
5328
- [src: …] token`, `L35`, `L36`, `L37`. The trainer had written the recap as a bulleted list.
5329
- The prompt already said "**Sources** — prose", and a writer who reads that as a style note
5330
- writes bullets, because bullets are what the other four sections take. The rule it collides
5331
- with is genuinely file-wide — `parseKnowledgeFile` requires a `[src: …]` token on EVERY list
5332
- item in every declared section, recap included, and an unsourced one is an error that rejects
5333
- the file whole. Both prompts now show the accepted prose next to the refused bullets, the same
5334
- move the execution-claim rule makes. Whether an unsourced recap bullet should be a warning
5335
- rather than an error is a real question and is deliberately NOT settled here.
5336
-
5337
5506
  - **A rejected knowledge file gets ONE repair round before the money is thrown away.**
5338
5507
  Measured 2026-08-30 on `~/scavtopia`: `tldrx expert train dotnet-stack --area dotnet --mode
5339
5508
  light` spent **$1.69**, the trainer wrote `knowledge/dotnet.md.partial`, and the validator
@@ -5666,19 +5835,6 @@ none of these behaves byte-identically to the release before them.
5666
5835
 
5667
5836
  ### Verified, not changed
5668
5837
 
5669
- - **`tldrx expert train` already exits nonzero when a training fails.** The 2026-08-31 batch
5670
- report measured shell `EXIT=0` on all ten invocations, including the three that failed their
5671
- check — but nine of those ten ran on a build that predates this one (`dist/tldrx.js` was
5672
- rewritten mid-batch at 05:40Z). On the current source the code path is intact:
5673
- `runTraining` returns `EXIT_AGENT_FAILED` (5), `expert train` returns `outcome.code`,
5674
- `dispatch` returns it, and `bin/tldrx.ts` does `process.exit(await dispatch(...))`. Now pinned
5675
- by three tests that drive the REAL CLI as a subprocess with a fake `claude` on PATH and assert
5676
- the PROCESS exit code — one for a file that does not validate, one for a file that was never
5677
- written, one for the passing case — because "`runTraining` returns 5" and "the process exits
5678
- 5" are two different claims. Falsified before being trusted: making `expert train` return
5679
- `EXIT_OK` breaks two of the three.
5680
-
5681
-
5682
5838
  - **The walk already skips vendored and generated trees**, and always did: `SKIPPED_DIRS`
5683
5839
  in `detect/walk.ts` covers `node_modules`, `dist`, `build`, `out`, `bin`, `obj`,
5684
5840
  `target`, `.venv`, `Pods`, `.next`, `.expo`, `coverage` and more, plus every
package/README.md CHANGED
@@ -316,6 +316,8 @@ back on the registry is 0.3.0.
316
316
 
317
317
  | Version | Date | Status | Contains |
318
318
  |---|---|---|---|
319
+ | 0.14.2 | 2026-09-09 | `beta` | every citation check starts from a fresh view of `questions.md` and `facts.yml`: `srcToken.ts` memoised both indexes at module scope and nothing outside `test/` ever dropped them, so in `tldrx run auto` — one Node process for a whole run — the FIRST citation resolved anywhere froze the view every later stage was then judged against; measured on three real unattended workspaces at 0.14.1, a `how` stage refused with `no such question Q2 … declared: Q1` over questions it had itself written minutes earlier, and another refused over `145 live fact(s)` when `facts.yml` held 148, the three extra written by the owner's answers two seconds before the stage started — roughly $11 of paid turns thrown away for ids that were real the whole time; the indexes are now refreshed inside `toSrcContext`, the one place a citation context is built and a place every caller reaches exactly once per check, gate or hook, so a document's forty citations still read `facts.yml` once while nothing survives the check that read it |
320
+ | 0.14.1 | 2026-09-09 | `beta` | an auto gate that says what holds it and closes itself, and three derivations cut to one: measured on a real workspace at 0.14.0, the first `run auto --wait-answers 4h --wait-gates 4h` with a notify hook finished its What stage for $1.98 with every declared check green and told the owner *"waiting at a auto gate that did not close by itself — a person signs it"* — no reason, because `evaluateAutoGate` had computed exactly that sentence into a stdout line nobody was watching while `gate.requested` was appended a hundred lines BEFORE the verdict existed; the verdict is now taken one statement earlier and `why`/`held_by` ride the event to the phone (present only for an `auto` policy, since `held_by: []` on a `human` gate would read as "checked, and nothing held it"), the questions that hold a gate are notified BEFORE the gate downstream of them so the owner stops being told to sign a thing before being told what it is, and an `auto` gate whose only blocker was an open question stops permanently degrading into a `human` one — each poll re-runs the seven conditions off disk and signs through the same `approve` door `next` uses, never for `human` and never for `agent`, a person's `approve` or `reject` still landing first; a question can carry its own `Recommended: <letter> — <why> [src: …]`, parsed tolerantly so an older note reads as not recorded rather than refusing; `tldrx facts add --repo <name>` is now checked against `workspace.yml` through the ONE leaf `answer --repo` already refused on, because the unvalidated flag wrote `repos: [ghost]` at exit 0 and every prompt or filter keyed on a real repo name was then silently blind to that fact; the five phase ids are written out in one file instead of three — `PHASE_IDS`, `QUESTION_PHASES`, and an inline literal walked by `questions lint` that `QUESTION_PHASES`' own doc comment claimed could not exist — pinned from both ends, because an identical second copy and a copy that has already drifted redden different guards and neither alone is enough; a training test stops running on bun's fixed 5000 ms after a spawn probe, validated first against a file the heuristic already claimed (87 spawns recorded, so the instrument can see the thing), found it spawning three real children unclaimed by any marker, with the 14 further unclaimed files filed rather than fixed; and `release-check.sh` refuses a rewritten released section — every dated heading whose tag is present must equal `git show vX.Y.Z:CHANGELOG.md` byte for byte, three sections having drifted with nothing checking, and an amendment recorded in `CHANGELOG.amendments` must still contain the tag's section as an ordered subsequence and may only add lines that exist verbatim in its source sha, after a reviewer proved the first version of that check happily passed an invented bullet |
319
321
  | 0.14.0 | 2026-09-09 | `beta` | an `agent` gate the engine can actually close, and three computations of the same answer cut to one: `gates_policy: agent` named who MAY sign a gate but nothing in the engine produced the evidence note it is signed over — measured on 0.13.1, an owner ran `tldrx run gates set what:agent`, was told "an agent may now close it", and the loop stopped at the next gate anyway with exit 4 — so `run auto` now spawns one bounded **gate signer** when a stage's checks pass under an `agent` policy: the stage's own model and effort, a quarter of its per-agent ceiling, a tool allowance that reads anything and writes exactly one file, and a prompt carrying the stage's declared outputs, the seven `auto` conditions as measured and the §2.8 skeleton `gate template` itself renders — the note going through the UNCHANGED `approve --as-agent` path, so a refusal, a note that does not validate, a signer that wrote nothing and a signer that died are one outcome, pending for a person with the reason named on stdout and now in the `gate.requested` summary, and there is no flag to turn it on because an `agent` policy is already the owner's recorded decision; the turn is recorded like any other (`role: gate-signer`, a `run.yml` task row, a row in `tldrx cost`) and taken BEFORE the stage moves to `awaiting_gate`, because the other order had a person sign the gate the engine was mid-signing, three runs out of three; and the suite stops being run three times per change — the pre-merge reviewer now runs only the test files that cover its diff plus `typecheck`, never the full `bun test`, since the wave re-runs every gate on the MERGED tree anyway, `publish.yml` refuses unless `ci` has a `success` run for the same sha instead of recomputing typecheck/tests/build (~87 min/week of runner time, with a `cancelled` ci run failing by name and the remedy), `ci` cancels a run a newer push has already superseded (25 of 122 push runs began under 10 minutes apart), the docs deploy finally fires on `src/cli/helpText.ts` so a help-registry change stops deploying nothing while the published CLI reference goes stale, and `AGENTS.md` §2 now says out loud that a slash in a branch name is a directory, which is what the review-record gate builds |
320
322
  | 0.13.1 | 2026-09-08 | `beta` | a stage prompt that opens by saying what to do: every stage prompt now leads with a generated brief — who the reader is, which stage of which run, that the template below is to be FILLED, the exact path of every declared output, and that a question goes in the questions file rather than back to an operator who is not there — because on a real workspace at 0.13.0 a What sub-agent read its 66,452-byte prompt, found no request in it, wrote none of its six declared outputs and asked what to do, $0.29 spent; the brief is generated from the same `outputs:` list `pending.json` records, so it cannot name a path the commit will not look for, and the failure was never a regression — the spliced citation grammar grew the stage section 5,007 B → 13,180 B and the missing instruction was finally outnumbered; and the `N runs are open` nudge, the one imperative-shaped sentence in that agent's window and the one it duly answered, stops reaching sub-agents at all — `spawnAgent` marks every child it spawns and `session-start` emits nothing when it sees the marker, an absent marker still being a human's session and behaving exactly as before |
321
323
  | 0.13.0 | 2026-09-08 | `beta` | evidence a role expert can actually earn, and a review that leaves a record: `--mode full`'s runs pass mines `tldrx-work/**` while the domain gate judged every citation it produced against folders of code — measured at four role experts, **$9.47 and one evidence row**, and unfixable from the workspace because the single spelling the matcher would reach is the one `domainPaths()` drops — so the gate now treats the run record as in-domain for the file mined FROM it, scoped to the pass and never to the expert's `kind:` (a light file citing a handoff is still out of domain and still says so), two shipped role templates stop declaring `.tldrx/map/**` and `.tldrx/map/{repo}/gotchas.md` paths that matched nothing at all, and a pass that validated, spent money and earned zero rows now prints `the level did not move — $X.XX bought 0 evidence row(s)` with its reasons carried into `check.passed` instead of a silent ledger; `tldrx expert rescore` recovers what was already bought for $0 by re-reading `knowledge/*.md` under today's rules, dating rows by the knowledge file's own `trained_at` and never by the clock, with `rescored_at` additive beside `at` — its ABSENCE keeping the meaning every existing row had — and one `evidence.rescored` line per file it actually moved, so a free re-derivation can never be read as a paid turn; `scripts/merge-wave.sh` refuses a branch carrying no `.review/<branch>.md` with **exit 10**, its own code because `2` in that script is already "merge conflict", a stale record refusing rather than warning and staleness measured as "the code moved" rather than "the sha differs", since committing the record moves the head past exactly the sha it names; and the mutation check moves from the reviewer, whose allowance is `Read`/`Grep`/`Glob`/`Bash(git diff *)` and holds no pen, to the developer's contract that can run it, leaving the reviewer the read it can actually perform |
@@ -1,20 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  conflictOf
4
- } from "./chunk-3ns54mnq.js";
4
+ } from "./chunk-w8qm1jbn.js";
5
5
  import {
6
6
  FactsStore,
7
7
  formatJaccard
8
- } from "./chunk-nwzevx0j.js";
8
+ } from "./chunk-mzwqb08p.js";
9
9
  import {
10
10
  parseHookInput,
11
11
  readStdin
12
- } from "./chunk-mvz7jwt8.js";
12
+ } from "./chunk-tpz0zb0t.js";
13
+ import {
14
+ PHASE_IDS
15
+ } from "./chunk-dzkg8c68.js";
13
16
  import {
14
17
  EventLog,
15
18
  PHASE_ID_RE
16
- } from "./chunk-f1w9sy07.js";
17
- import"./chunk-yvgkw0gm.js";
19
+ } from "./chunk-dnat9z30.js";
20
+ import"./chunk-db7wf3hy.js";
18
21
  import {
19
22
  ADVISORY_KEY,
20
23
  MAX_FACT_CHARS,
@@ -24,7 +27,7 @@ import {
24
27
  renderQuestionBlock,
25
28
  replaceBlock,
26
29
  serializeQuestions
27
- } from "./chunk-1knfmj6j.js";
30
+ } from "./chunk-bqmstq21.js";
28
31
  import {
29
32
  ITERATION_ONLY_SLOT,
30
33
  PROJECT_FRAMEWORK_DIR,
@@ -32,7 +35,7 @@ import {
32
35
  commandProbeIssues,
33
36
  factsPath,
34
37
  parseYaml
35
- } from "./chunk-6fywd8xd.js";
38
+ } from "./chunk-wwwmkey4.js";
36
39
 
37
40
  // src/hooks/answer-capture.ts
38
41
  import { existsSync as existsSync5 } from "fs";
@@ -334,7 +337,7 @@ import { appendFileSync as appendFileSync2, existsSync as existsSync3, readFileS
334
337
  import { join as join3 } from "node:path";
335
338
 
336
339
  // src/core/run/questionCards.ts
337
- var QUESTION_PHASES = ["01-what", "02-how", "03-plan", "04-build", "05-watch"];
340
+ var QUESTION_PHASES = PHASE_IDS;
338
341
 
339
342
  // src/core/answers/raiseConflict.ts
340
343
  var RAISED_BY = "tldrx";
@@ -379,6 +382,7 @@ function raiseConflictQuestion(args) {
379
382
  { letter: "B", text: `${args.oldFactId} is right — supersede ${args.newFactId}` },
380
383
  { letter: "C", text: "Both are partly right — write the correction below" }
381
384
  ],
385
+ recommended: null,
382
386
  answer: "",
383
387
  answerIndex: -1,
384
388
  footer: null,
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-qefqmgjn.js";
4
+ } from "./chunk-dxk7eaqn.js";
5
5
  import {
6
6
  allow,
7
7
  deny,
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-v2xzr4qd.js";
12
- import"./chunk-mvz7jwt8.js";
11
+ } from "./chunk-gvz0rzb9.js";
12
+ import"./chunk-tpz0zb0t.js";
13
13
  import {
14
14
  asRunBudget,
15
15
  currentActor,
@@ -29,20 +29,20 @@ import {
29
29
  validateRunBudget,
30
30
  wouldExceed,
31
31
  wouldExceedHostTokens
32
- } from "./chunk-fsgsn8vb.js";
32
+ } from "./chunk-fefnr71h.js";
33
33
  import {
34
34
  EventLog
35
- } from "./chunk-f1w9sy07.js";
35
+ } from "./chunk-dnat9z30.js";
36
36
  import {
37
37
  noteDeprecations
38
- } from "./chunk-1knfmj6j.js";
38
+ } from "./chunk-bqmstq21.js";
39
39
  import {
40
40
  PROJECT_WORK_DIR,
41
41
  findWorkspaceRoot,
42
42
  locateWork,
43
43
  parseYaml,
44
44
  stageYamlPath
45
- } from "./chunk-6fywd8xd.js";
45
+ } from "./chunk-wwwmkey4.js";
46
46
 
47
47
  // src/hooks/budget-gate.ts
48
48
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -10,7 +10,7 @@ import {
10
10
  spentBasis,
11
11
  tallyOf,
12
12
  validateRunBudget
13
- } from "./chunk-fsgsn8vb.js";
13
+ } from "./chunk-fefnr71h.js";
14
14
  import {
15
15
  EventLog,
16
16
  asRunFile,
@@ -22,7 +22,7 @@ import {
22
22
  isTerminal,
23
23
  stageAt,
24
24
  validateRunFile
25
- } from "./chunk-f1w9sy07.js";
25
+ } from "./chunk-dnat9z30.js";
26
26
  import {
27
27
  backupPathFor,
28
28
  isAlive,
@@ -31,19 +31,19 @@ import {
31
31
  workspaceRootOfRunDir,
32
32
  writeAtomic,
33
33
  yamlScalar
34
- } from "./chunk-yvgkw0gm.js";
34
+ } from "./chunk-db7wf3hy.js";
35
35
  import {
36
36
  isAdvisory,
37
37
  noteDeprecations,
38
38
  openBlocks,
39
39
  parseQuestions
40
- } from "./chunk-1knfmj6j.js";
40
+ } from "./chunk-bqmstq21.js";
41
41
  import {
42
42
  FRAMEWORK_ROOT,
43
43
  listRunDirs,
44
44
  parseYaml,
45
45
  parseYamlRepairing
46
- } from "./chunk-6fywd8xd.js";
46
+ } from "./chunk-wwwmkey4.js";
47
47
 
48
48
  // src/core/statusline/runSnapshot.ts
49
49
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";