tldr-experts 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +994 -0
- package/README.md +26 -7
- package/dist/hooks/answer-capture.js +20 -11
- package/dist/hooks/budget-gate.js +28 -8
- package/dist/hooks/chunk-0z27twdk.js +159 -0
- package/dist/hooks/{chunk-0bt6yb2q.js → chunk-39zh2e44.js} +10 -1
- package/dist/hooks/{chunk-1zwcxd3f.js → chunk-3g61yg59.js} +1 -1
- package/dist/hooks/chunk-3t91gvpp.js +193 -0
- package/dist/hooks/{chunk-kw4tffzf.js → chunk-458wgg9j.js} +30 -24
- package/dist/hooks/{chunk-j234zf0t.js → chunk-9kkm6q0t.js} +4 -165
- package/dist/hooks/{chunk-t8tdv11p.js → chunk-afamdvyn.js} +1 -1
- package/dist/hooks/{chunk-a8p2rc94.js → chunk-s5qsb4k6.js} +1 -1
- package/dist/hooks/{chunk-g395gk7e.js → chunk-sznsenee.js} +1 -1
- package/dist/hooks/{chunk-y0jdr3et.js → chunk-xpxe94qe.js} +23 -10
- package/dist/hooks/{chunk-azctppjh.js → chunk-ybacnpxd.js} +4 -1
- package/dist/hooks/{chunk-x98qs959.js → chunk-ytvmc5ns.js} +19 -22
- package/dist/hooks/chunk-zdxgragg.js +1655 -0
- package/dist/hooks/{chunk-p274ckxv.js → chunk-ztczwtj0.js} +6 -1
- package/dist/hooks/claim-sources.js +9 -9
- package/dist/hooks/dod-gate.js +12 -37
- package/dist/hooks/no-reask.js +9 -9
- package/dist/hooks/session-start.js +176 -422
- package/dist/hooks/statusline.js +14 -11
- package/dist/tldrx.js +12029 -6330
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/tldrx/SKILL.md +16 -3
- package/stages/build/stage.md +5 -0
- package/templates/budget.yml +5 -0
- package/dist/hooks/chunk-ae6bkfs5.js +0 -0
- package/dist/hooks/chunk-sdjnnmzz.js +0 -497
- package/dist/hooks/chunk-t56k6146.js +0 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,999 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 — 2026-08-31
|
|
4
|
+
|
|
5
|
+
**Unattended mode.** Twelve of the entries below are one feature: a run a **host session**
|
|
6
|
+
drives end to end, and a gate an **agent** may close over a check it wrote down. `attended_by: host`
|
|
7
|
+
stops the framework spawning on a run at all; `economy: host-tokens` stops a ceiling that is not
|
|
8
|
+
dollars from buying a metered spawn; the dispatch-notes slot gives the host the one place to add
|
|
9
|
+
what the bundle cannot know; `next --prepare/--commit --review` makes the Build reviewer the
|
|
10
|
+
second delegable role, so one review is done once; the `fixlist` verdict gives a review that
|
|
11
|
+
SIGNS somewhere to put its findings, for one bounded round that costs no attempt;
|
|
12
|
+
`gates_policy: agent` closes a gate on the seven auto conditions **plus** a boundary check, a
|
|
13
|
+
budget-event check and a validated evidence note, and falls through to a person on a question, a
|
|
14
|
+
moved ceiling, work nobody scoped, or its own refusal — rendered as a decision card rather than
|
|
15
|
+
a dashboard. Three smaller pieces stop a turn being wasted before it starts: `preconditions:`
|
|
16
|
+
on a stage, a story branch fast-forwarded onto its epic before dispatch, and a budget brake that
|
|
17
|
+
counts the work that is LEFT rather than the price the stage was written at. Two measurements
|
|
18
|
+
from 2026-08-30 are the whole argument: **$9.95** of spawns that died on caps a Plan agent had
|
|
19
|
+
priced in host tokens, and a framework reviewer that spawned beside a host already reading the
|
|
20
|
+
same diff. New chapter: `docs/guide/10-unattended-mode.md`. Every part is additive — a run with
|
|
21
|
+
none of these behaves byte-identically to the release before them.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **A story branch that has fallen behind its epic is fast-forwarded before a developer is
|
|
26
|
+
dispatched onto it.** Measured 2026-08-30 on `260830-tenancy-identity-customers`: S3 was
|
|
27
|
+
reopened, `story reopen` keeps its branch by design, and that branch still sat at the S1-era
|
|
28
|
+
epic tip while the epic had since gained S2 and S5. S3's handlers needed S2's contract, so a
|
|
29
|
+
dispatch on that base would not have compiled. The host fast-forwarded by hand before
|
|
30
|
+
dispatching. That is the one case this automates.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
· S3: fast-forwarded `story/260830-tenancy/S3` to `epic/tenancy` — 2 commit(s), b5a2474 → ae9c8dd
|
|
34
|
+
```
|
|
35
|
+
- **Where.** Inside `openStory`, which is the one place a story worktree is opened, and only
|
|
36
|
+
on the two openings that are about to put a DEVELOPER on the branch: the headless pipeline
|
|
37
|
+
and `tldrx next --prepare`. The review openings (`--prepare --review`, `--commit --review`,
|
|
38
|
+
an errored review re-run) and `--commit` measure nothing and move nothing — a fast-forward
|
|
39
|
+
there would drag other stories' commits onto a branch whose whole meaning is "what this
|
|
40
|
+
story built", for a base nobody is about to compile against.
|
|
41
|
+
- **The requeue case, which fires far more often than the reopen one.** A `changes` verdict
|
|
42
|
+
merges the story into its epic and then hands it a second attempt; before this, attempt 2
|
|
43
|
+
was dispatched onto attempt 1's base. It now starts on the current epic tip.
|
|
44
|
+
- **A diverged branch is warned about, never resolved.** Commits on both sides is the second
|
|
45
|
+
live case — a dead spawn's partial commit on a stale base, where no fast-forward existed
|
|
46
|
+
and the host preserved the partial on a backup branch and re-pointed the story branch by
|
|
47
|
+
hand. Which of two histories survives is a decision, so the framework does not make it: it
|
|
48
|
+
names both counts, both shas, and the two options, changes nothing, and lets the dispatch
|
|
49
|
+
proceed on the old base — saying, in as many words, which base that is.
|
|
50
|
+
- **A dirty worktree is left alone**, whatever the topology says. It is the operator's.
|
|
51
|
+
- **Never a rebase.** Rewriting a branch a developer has already committed to is the class of
|
|
52
|
+
move the run-id-in-branch-name fix (2026-08-29 audit §B) exists to prevent. The only write
|
|
53
|
+
is `git merge --ff-only`, which refuses rather than inventing a merge commit. Measured
|
|
54
|
+
2026-08-31 against a real repository, which is what the design asked to verify before
|
|
55
|
+
building: blocked by a file in the way it exits non-zero and leaves HEAD and the file
|
|
56
|
+
exactly as they were — atomic-or-nothing, so a failed fast-forward needs no repair, only a
|
|
57
|
+
line saying it did not happen.
|
|
58
|
+
- **`story.base_fastforwarded`** joins the closed §2.9 event set — the only event in it that
|
|
59
|
+
records tldrx moving a ref. It carries `story`, `repo`, `branch`, `base`, `from`, `to` and
|
|
60
|
+
`commits`, `tldrx replay` narrates it, and it is appended ONLY when the ref actually moved:
|
|
61
|
+
a divergent or dirty branch produces a warning and no event, because nothing happened.
|
|
62
|
+
- **An up-to-date branch is silent** and emits nothing, so a run with nothing to say about
|
|
63
|
+
its bases is what it was before.
|
|
64
|
+
- `tldrx story reopen` is unchanged: it still runs no git command, spends nothing and touches
|
|
65
|
+
no branch. The detection belongs where a worktree is being opened anyway and where the
|
|
66
|
+
operator is about to dispatch.
|
|
67
|
+
|
|
68
|
+
- **Decision cards — the shape an interrupt takes when a run stops for a person.** Measured
|
|
69
|
+
2026-08-30: an unattended run stopped on two owner questions, and the host did NOT show the
|
|
70
|
+
owner the dashboard or the `2 open question(s) in 01-what/questions.md` line the framework
|
|
71
|
+
actually prints. It hand-composed, in chat, the question, the options and a recommendation
|
|
72
|
+
with one line of why. The owner answered both in seconds. The card is what made the
|
|
73
|
+
interrupt cheap; hand-composing it is what the framework was making the host pay for.
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
DECISION — 260830-tenancy · 01-what/what
|
|
77
|
+
Q2 · Should an existing customer's tenant be inferred or asked for?
|
|
78
|
+
Why asked: no tenant column on the customer aggregate [src: absent:api:src/.../Customer.cs]
|
|
79
|
+
A) infer from the invoice email domain — no new UI, wrong for resellers
|
|
80
|
+
B) ask once at first login — one screen, correct for everyone
|
|
81
|
+
C) other — write it below
|
|
82
|
+
Recommends B — one screen, correct for everyone [src: 01-what/handoff.md:22]
|
|
83
|
+
tldrx answer Q2 "…" --run 260830-tenancy
|
|
84
|
+
```
|
|
85
|
+
- **Pure rendering of things that already exist.** The question, its `Why asked:` line and
|
|
86
|
+
its lettered options come out of `questions.md` through the **§2.7 parser** — the
|
|
87
|
+
questions grammar is not touched, and a block the parser cannot read does not appear on a
|
|
88
|
+
card any more than it appears anywhere else. The `Recommends` line comes out of the
|
|
89
|
+
evidence note's optional `recommend: [{q, option, why, src}]` array, which the evidence
|
|
90
|
+
grammar already validates.
|
|
91
|
+
- **A question with no recommendation gets no line.** Never a manufactured one and never a
|
|
92
|
+
placeholder: the whole value of that line is that an agent stood behind it with a
|
|
93
|
+
citation.
|
|
94
|
+
- **One renderer, three surfaces.** `tldrx run auto --gate-agent` at the stop;
|
|
95
|
+
`tldrx next`'s agent-gate fallthrough, where the card is **appended** to the fallthrough
|
|
96
|
+
list so nothing that reads those lines today loses a byte; and `tldrx status`, where a run
|
|
97
|
+
waiting on answers now shows the card rather than `open questions: Q1, Q2`.
|
|
98
|
+
- **A card per fallthrough kind.** Budget and boundary get their own card over the same
|
|
99
|
+
frame — the measured fact, then the commands (`widen the scope …` / `approve` / `reject`
|
|
100
|
+
for a boundary; the phase's two numbers plus `budget show` for a budget event) — and every
|
|
101
|
+
other reason an agent gate fell through is carried as one gate card naming its reasons.
|
|
102
|
+
- **`--gate-agent` is rendering only.** It does not upgrade any stage to
|
|
103
|
+
`gates_policy: agent`: a run keeps the policy it was opened with, and a flag that could
|
|
104
|
+
raise one at stop time would make the frozen policy decorative. On an
|
|
105
|
+
`attended_by: host` run it changes nothing — `run auto` is still refused at exit `1`
|
|
106
|
+
before the event log is opened, and nothing spawns.
|
|
107
|
+
- **Nothing else moves.** `tldrx answer`, `questions.md`, the live dashboard and every exit
|
|
108
|
+
code are unchanged, and `run auto` without the flag prints exactly the block it always did.
|
|
109
|
+
- **The budget brake counts the work that is LEFT, not the price the stage was written at.**
|
|
110
|
+
Measured 2026-08-31 on `260830-tenancy-identity-customers`: four of seven stories done, one
|
|
111
|
+
mid-attempt-2, two blocked, and the entire remaining metered cost a developer share and a
|
|
112
|
+
reviewer floor — **$2.50**. The brake compared the phase's remaining dollars against
|
|
113
|
+
`stage.budget_usd`, **$18.00**, a number written before a single story ran and never
|
|
114
|
+
revised. It refused the stage twice and the host ran `budget raise --take-from` twice, for
|
|
115
|
+
money nothing was going to spend.
|
|
116
|
+
|
|
117
|
+
For a Build stage with a plan on disk, `tldrx next`'s refusal, the `budget-gate` hook and
|
|
118
|
+
`tldrx budget show`'s `est.` column now all use one figure computed by one function: `Σ`
|
|
119
|
+
over the unsettled stories of the caps the executor would actually hand out — the
|
|
120
|
+
`03-plan/budget.yml` price through the same scale/share arithmetic, the developer and
|
|
121
|
+
reviewer shares, the `$1.00` reviewer floor, and the attempts each story has left.
|
|
122
|
+
- **The refusal shows its arithmetic**: `remaining work: S4 dev $1.50 + reviewer $1.00 =
|
|
123
|
+
$2.50`, under a line naming how many stories are done and what the stage's static
|
|
124
|
+
estimate was. A number an operator cannot take apart is one they cannot argue with, and
|
|
125
|
+
`$18.00` cited nothing.
|
|
126
|
+
- **`blocked` costs $0.00**, and the blocked ids are named rather than quietly dropped: the
|
|
127
|
+
executor dispatches a blocked story only after `tldrx story reopen`, which is a human
|
|
128
|
+
decision and which legitimately raises the figure again.
|
|
129
|
+
- **A story at `review` has already paid the developer turn under review.** Only a `changes`
|
|
130
|
+
verdict buys another one.
|
|
131
|
+
- **Under `economy: host-tokens` the developer turns are $0.00** — the host session pays for
|
|
132
|
+
them — while the reviewer floors stay, because outside attended mode `reviewAndSettle`
|
|
133
|
+
still spawns a metered reviewer and that floor is real money.
|
|
134
|
+
- **It can only NARROW.** The figure is capped at `stage.budget_usd`, so this brake can
|
|
135
|
+
never refuse more often than it did before; the reviewer floor can otherwise lift a naive
|
|
136
|
+
sum past the ceiling. Asserted in both directions, across a spread of plan shapes, because
|
|
137
|
+
a brake that loosened by accident is the failure to fear here.
|
|
138
|
+
- **`budget.blocked` gains `estimate_basis: plan|static`** and, on the plan basis,
|
|
139
|
+
`static_estimate_usd`, `stories_done` and `stories_total`.
|
|
140
|
+
- **With no plan on disk, and outside Build, every path is byte-identical**, wording
|
|
141
|
+
included: the estimate is `budget_usd` and the message still reads `the stage estimate
|
|
142
|
+
is $X`.
|
|
143
|
+
|
|
144
|
+
- **`preconditions:` on a stage — the check that runs before the money does.** A stage may
|
|
145
|
+
declare operational facts that must hold before it is worth dispatching at all:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
preconditions:
|
|
149
|
+
- {id: docker, repo: api, command: "docker compose ps", expect_exit: 0}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The grounding is measured, 2026-08-30: before dispatching a Build story the host checked
|
|
153
|
+
the Docker daemon and the .NET SDK **by hand**, because a story has two attempts, an agent
|
|
154
|
+
cannot debug its way out of a daemon that is down, and the whole turn would have been spent
|
|
155
|
+
proving it. That check took about a second and protected an attempt worth dollars.
|
|
156
|
+
- **Same allowlist rule as a `cmd` check and a story's `` ```dod `` block — and now literally the
|
|
157
|
+
same function.** Only a command byte-equal to one `.tldrx/workspace.yml` declares runs,
|
|
158
|
+
argv-split, never through a shell. The comparison and both refusal sentences moved to
|
|
159
|
+
`schemas/commandAllowlist.ts`, so the three sites can no longer drift into three readings
|
|
160
|
+
of one rule. It is enforced **at load**: a stage naming an undeclared command never
|
|
161
|
+
becomes a runnable stage, so `tldrx run new` over it refuses too.
|
|
162
|
+
- **Red ⇒ refused, exit `2`, having spent nothing.** The id and the command's own exit code
|
|
163
|
+
are named, the stage is left exactly where it was (`ready`), no bundle is written and
|
|
164
|
+
nothing is spawned. The list stops at the first red one.
|
|
165
|
+
- **`--prepare` runs them no less than headless** — a bundle written for a host whose Docker
|
|
166
|
+
is down is the same wasted attempt as a spawn into one. `--commit` never runs them: it
|
|
167
|
+
settles a turn that already happened.
|
|
168
|
+
- **Every run is on the record**: one `check.passed` / `check.failed` event with
|
|
169
|
+
`kind: precondition`, carrying the repo, the command, the exit code and the duration, and
|
|
170
|
+
one operator line — `· precondition: docker compose ps → exit 0 (1.2s)`.
|
|
171
|
+
- **A stage that declares none is byte-identical**: no event, no line, no shipped stage file
|
|
172
|
+
changed. `[assumption]` — per stage, not per story; a per-story precondition is a real
|
|
173
|
+
want and is deliberately not designed here.
|
|
174
|
+
- **The `fixlist` verdict, its artifact and its router — the review that SIGNS and still has
|
|
175
|
+
findings.** Measured 2026-08-31, driving `260830-tenancy-identity-customers` by hand: the
|
|
176
|
+
reviewer signed story S5 — every acceptance criterion met, zero scope violations — and in the
|
|
177
|
+
same breath named three real correctness/security defects the criteria never covered (a
|
|
178
|
+
concurrent double-confirm minting two sessions, a non-atomic confirm, a false security comment
|
|
179
|
+
beside a non-constant-time compare). S1 and S3 went the same way that night. Binary
|
|
180
|
+
`approve`/`changes` has nowhere to put those: `approve` throws them away, `changes` spends the
|
|
181
|
+
story's one requeue on a diff nobody faulted. So all three loops were run in chat — number the
|
|
182
|
+
findings, decide fix-now vs defer-with-log, route them to the author, re-verify — and none of
|
|
183
|
+
it reached a file. This is that loop, as a verdict and an artifact.
|
|
184
|
+
- **`fixlist` settles the story at `review` and spends NO attempt.** The requeue counter counts
|
|
185
|
+
verdicts that FAULTED the diff, and a signature is not one. `04-build/fixlist/<story>-<n>.md`
|
|
186
|
+
is written beside it by the EXECUTOR, never by the reviewer — which holds no write tool, the
|
|
187
|
+
same reason the review log is written there. Numbered `## <n> · <finding> [<severity>]`
|
|
188
|
+
sections, each with `Where:`, `Disposition:` and `Resolved:`.
|
|
189
|
+
- **A disposition ROUTES a finding; `Resolved:` CLOSES it.** Two questions, two fields, because
|
|
190
|
+
one field cannot answer the first once the second is true. `fix-now` · `defer-with-log` ·
|
|
191
|
+
`refuted` · `out-of-scope`, and **`refuted` must carry an `[src: …]`** in its `where` or
|
|
192
|
+
`detail`, through the §2.8 grammar and the §2.8 parser — a reviewer's verdict is a claim like
|
|
193
|
+
every other, and that night's host disproved one by grepping both sides before acting on it.
|
|
194
|
+
A fix list with an uncited `refuted` is refused whole and the verdict falls to `changes`.
|
|
195
|
+
- **The router: `tldrx next --prepare --fixlist <path>`.** The open findings land under
|
|
196
|
+
`## Fix list` in the DEVELOPER's prompt, numbered, with their `Do NOT` lines verbatim — a
|
|
197
|
+
bound the reviewer put on a fix is worth as much as the fix. `pending.json` gains
|
|
198
|
+
`fixlist: {path, round, findings, open}` and `resume_session`, the prior turn's `session_id`,
|
|
199
|
+
so the host can resume that sub-agent rather than pay to rebuild its context. **The framework
|
|
200
|
+
resumes nothing itself** — `spawnAgent` has no `--resume` — so the bundle carries the fix list
|
|
201
|
+
and the merged commit and hands the id back to the party that can act on it. Omit the flag and
|
|
202
|
+
the latest still-open round is carried by itself, the same courtesy `--prepare` already
|
|
203
|
+
extends to a story waiting on a review.
|
|
204
|
+
- **One round per story, and the second is refused out loud.** A free round that could be taken
|
|
205
|
+
twice is a story that never has to settle. A second `fixlist` is read as `changes` — which
|
|
206
|
+
costs the attempt the first one did not — the refusal names the round already on disk, and the
|
|
207
|
+
SECOND reviewer's prompt withdraws the verdict rather than offering one the executor would
|
|
208
|
+
then refuse. `story reopen` resets the count with every other one in the review ledger.
|
|
209
|
+
- **A story cannot settle `done` over an open `fix-now`.** An `approve` there settles `blocked`
|
|
210
|
+
and the reason names the file, the finding's number and its heading, plus the two ways to
|
|
211
|
+
close it. The check reads the FILE, not the envelope that produced it: the file is the state,
|
|
212
|
+
and a host closes a finding by writing `Resolved: yes` in it or re-routing its `Disposition:`.
|
|
213
|
+
That edit is the host's — §B.2's third role — because the author works in a story worktree of
|
|
214
|
+
another repo and its own prompt forbids writing outside it.
|
|
215
|
+
- **Fail-closed, unchanged and asserted.** A `verdict: "fixlist"` whose `fixlist[]` is missing,
|
|
216
|
+
empty or unreadable is `changes`, never a free round and never `approve`. Both economies reach
|
|
217
|
+
the same code: the host writes the envelope into the review bundle, or a spawned reviewer
|
|
218
|
+
returns it — `REVIEW_SCHEMA` gained the verdict and the optional array, and `parseReview`
|
|
219
|
+
narrows both.
|
|
220
|
+
- `defer-with-log` findings are appended to `retro.md`'s `## Build feedback` as the artifact is
|
|
221
|
+
written — the existing second writer with its existing verbatim dedup — so a deferred defect
|
|
222
|
+
reaches the owner through a channel that already exists rather than a new one.
|
|
223
|
+
|
|
224
|
+
- **`tldrx next --prepare --review` / `--commit --review` — the reviewer is the second
|
|
225
|
+
delegable role.** A Build story has two sub-agents and only the developer was ever
|
|
226
|
+
delegable; the reviewer was the FRAMEWORK's spawn in both modes, which on a host-driven
|
|
227
|
+
run buys a second reading of a diff the host is already reading, and a bill nobody
|
|
228
|
+
budgeted. Now it rides the same handshake, one directory down:
|
|
229
|
+
`.agent/<stage>/<story>/review/{prompt.md,pending.json,result.json}` — nested so a
|
|
230
|
+
reviewer bundle can never be read as a developer one.
|
|
231
|
+
- **`--prepare --review` writes the bundle and spawns nothing.** `prompt.md` is what a
|
|
232
|
+
spawned reviewer would have been sent, from the same renderer. `pending.json` carries
|
|
233
|
+
`role: reviewer`, `result_schema` (the reviewer's `--json-schema` envelope, verbatim,
|
|
234
|
+
so the host needs no source to know the shape), and a `review:` block with the diff
|
|
235
|
+
command, the merged commit, the attempt and the **DoD results recovered from
|
|
236
|
+
`events.jsonl`** — the DoD is not re-run, and the prompt says so.
|
|
237
|
+
- **`--commit --review` settles it through the existing seam.** The envelope goes through
|
|
238
|
+
the same `parseReview` with the same fail-closed rule (unreadable ⇒ `changes`, never
|
|
239
|
+
`approve`) and the same `reviewAndSettle`: `approve` ⇒ `done` with its evidence,
|
|
240
|
+
`changes` ⇒ one requeue then `blocked`, `MAX_ATTEMPTS` and the requeue counter
|
|
241
|
+
untouched. A host that never writes `result.json` has produced no verdict and spends
|
|
242
|
+
no attempt.
|
|
243
|
+
- **The trail says whose review it was.** No `agent.spawned`; a `task.started` with
|
|
244
|
+
`role: reviewer, mode: prepare` instead. The `check: review` event carries
|
|
245
|
+
`source: host` (written only for a host review, so the spawned path's payload is
|
|
246
|
+
unchanged byte for byte), and the task row is `cost_usd: null, metered: false` unless
|
|
247
|
+
the envelope declares `cost_usd` / `tokens`.
|
|
248
|
+
- **On `attended_by: host` the framework never calls `spawnReviewer` at all.** Half B
|
|
249
|
+
merges the story and hands the review over, so a full attended story cycle emits zero
|
|
250
|
+
`agent.spawned`. Outside attended mode `--review` is opt-in and the headless reviewer
|
|
251
|
+
is unchanged.
|
|
252
|
+
|
|
253
|
+
### Changed
|
|
254
|
+
|
|
255
|
+
- **`tldrx next --prepare` on a story awaiting review now writes the reviewer bundle
|
|
256
|
+
instead of spawning a reviewer.** It used to spawn a metered one — which is the single
|
|
257
|
+
thing `--prepare` exists not to do. Measured 2026-08-31 on the live
|
|
258
|
+
`260830-tenancy-identity-customers`: story S3's reviewer died at its $1.00 cap, the
|
|
259
|
+
story parked at `review`, and the `--prepare` that was supposed to rescue it spawned a
|
|
260
|
+
replacement that a two-minute host timeout then killed mid-read. The story is still
|
|
261
|
+
parked at `review` afterwards, its attempt still unspent, and the verdict is the host's
|
|
262
|
+
to write. Headless `tldrx next` still re-runs the review by spawning, unchanged.
|
|
263
|
+
- **`tldrx next`'s `attended_by: host` refusal names `--commit --review`** when the stage
|
|
264
|
+
is holding a reviewer bundle. It named `--commit`, which is the wrong half: that door
|
|
265
|
+
reads the DEVELOPER's `result.json` and re-runs a pipeline that has already merged.
|
|
266
|
+
- **`--discard-pending` bins the reviewer bundle too**, alongside the developer bundles it
|
|
267
|
+
already binned — a stale review `result.json` would otherwise be read by the next
|
|
268
|
+
`--commit --review` as a verdict on work it never saw.
|
|
269
|
+
|
|
270
|
+
- **`tldrx init` says what it is doing while it does it** — a live line per step, in the
|
|
271
|
+
same `--ui scene|compact|plain|off` view family as the agent progress view, on stderr.
|
|
272
|
+
It used to print NOTHING until it was finished. Measured 2026-08-30 on a five-repo
|
|
273
|
+
workspace: **36.0 s of total silence** with the default `--provider auto` against
|
|
274
|
+
**1.3 s** with `--provider static` — so ~97% of the wait is `graphify update` running
|
|
275
|
+
once per repo inside `buildMap`, and the command looked hung for all of it.
|
|
276
|
+
- **Ten steps announce themselves**: detecting repos, building the code map, writing
|
|
277
|
+
`workspace.yml`, planning the interview, seeding experts, reading conventions, writing
|
|
278
|
+
`process.yml`/`facts.yml`, `init-questions.md`, `init-handoff.md`, and the `.gitignore`
|
|
279
|
+
+ `CLAUDE.md` blocks. `--mcp` adds an eleventh, because it health-checks every server.
|
|
280
|
+
- **The slow ones name the repo they are inside.** `detecting repos` reports each repo
|
|
281
|
+
with its stack, confidence and default branch as detection finishes it; the map step
|
|
282
|
+
reports `<repo> — 6 documents via graphify` per repo. The wait is now legible instead
|
|
283
|
+
of merely long.
|
|
284
|
+
- **A terminal gets a spinner, colour and an in-place rewrite; a pipe gets plain lines.**
|
|
285
|
+
A finished step is printed once and never touched again, so it survives in scrollback
|
|
286
|
+
after the command exits — a step list is a HISTORY, not a picture of a moment, which is
|
|
287
|
+
why this is not `ui/driver.ts` with a different renderer. In `plain` a step still open
|
|
288
|
+
after 5 s says `still <label> — 12 s`, because there is no spinner there to prove the
|
|
289
|
+
process is alive.
|
|
290
|
+
- **A schoolhouse** (`core/ui/campus.ts`) is painted above the steps in `scene` mode,
|
|
291
|
+
drawn in the same hand as the classroom the agent view renders — `init` is the survey
|
|
292
|
+
that happens before the school opens.
|
|
293
|
+
- **`--quiet`** turns the live view off and keeps the report. **`--ui <mode>`** works on
|
|
294
|
+
`init` exactly as it does on `next`, `run auto` and `expert train`, and a bad value is
|
|
295
|
+
a usage error raised before any work is done.
|
|
296
|
+
- **`core/ui/color.ts`** — the framework's first ANSI palette, resolved per STREAM rather
|
|
297
|
+
than per process. `tldrx init > report.txt` on a terminal has a piped stdout and a TTY
|
|
298
|
+
stderr: the live lines are still worth colouring and the file must still be plain text.
|
|
299
|
+
`palette(false)` is the identity for every ink, so a renderer never branches on colour
|
|
300
|
+
and the uncoloured path stays byte-for-byte deterministic in a test. `FORCE_COLOR` beats
|
|
301
|
+
`NO_COLOR` beats `CI` beats the stream.
|
|
302
|
+
|
|
303
|
+
- **The dispatch-notes slot** — `.agent/<stage>/dispatch-notes.md`, and for a Build story
|
|
304
|
+
`.agent/<stage>/<story>/dispatch-notes.md`: the one place a HOST can add context to a
|
|
305
|
+
prompt the framework generated. Measured over one full run of
|
|
306
|
+
`260830-tenancy-identity-customers`, 2026-08-30, EVERY stage needed host-added context the
|
|
307
|
+
bundle lacked — a deferred decision at What, non-inlined seed docs and a staleness warning
|
|
308
|
+
at How, the owner's answers at Plan, "Docker is up" at Build — and the host had exactly two
|
|
309
|
+
places to put any of it, neither of which is one: `stage.md` is the framework's file,
|
|
310
|
+
shared by every run of that workflow, and an edit to `prompt.md` is thrown away by the next
|
|
311
|
+
`--prepare`.
|
|
312
|
+
- **Rendered under `## Dispatch notes`, between `## Inputs` and `## Previous attempt`.**
|
|
313
|
+
Behind the expert blocks on purpose: the slot is the most volatile thing in the document
|
|
314
|
+
— a human writes it between one cycle and the next — and a per-cycle file ahead of the
|
|
315
|
+
largest stable section would pay the cache-WRITE price on every stage. The same position
|
|
316
|
+
in the Build developer prompt, directly under `## Inputs`, because that is where the
|
|
317
|
+
brief ends and `## Investigate` step 1 tells the developer the files above ARE the brief.
|
|
318
|
+
- **Absent ⇒ nothing changes, byte for byte.** No section, no `dispatch_notes` key in
|
|
319
|
+
`pending.json`, `0 B` in the context ledger. Asserted by adding the file, re-preparing,
|
|
320
|
+
removing it, re-preparing, and comparing the two prompts byte for byte.
|
|
321
|
+
- **Capped at 8 KB, and never free.** The stage's file and the story's file feed ONE slot,
|
|
322
|
+
spent stage-file-first, so neither can quietly double the budget; the overflow is named
|
|
323
|
+
in the prompt, on stdout, and in `pending.json`
|
|
324
|
+
(`dispatch_notes: {bytes, truncated, max_bytes, sources[]}`). The rendered section's
|
|
325
|
+
bytes are charged to the context ledger and count against `prompt_max_bytes` like
|
|
326
|
+
everything else — asserted with a ceiling the prompt clears without notes and breaks
|
|
327
|
+
with them. The byte cut never splits a character in half.
|
|
328
|
+
- **Context, never configuration.** The framework does not parse it, does not substitute
|
|
329
|
+
`{{placeholders}}` in it, does not require `[src: …]` tokens on it, and it cannot change
|
|
330
|
+
a declared input, an output, a check or a cap. The section says all of that to the
|
|
331
|
+
sub-agent in its own preamble, because a note that reads like an instruction is otherwise
|
|
332
|
+
indistinguishable from the stage's own rules.
|
|
333
|
+
- **Survives `--discard-pending`.** The flag bins `pending.json`, `result.json` and
|
|
334
|
+
`result.raw.json`; the notes are an INPUT to the rendering that is about to be redone,
|
|
335
|
+
not an output of the one being binned.
|
|
336
|
+
- **Per-cycle scratch, deliberately.** `.agent/` is gitignored, and that is the whole
|
|
337
|
+
point: a caveat that must outlive the cycle is a FACT, and `.tldrx/memory/facts.yml` is
|
|
338
|
+
the durable channel that already reaches every prompt with attribution behind it. Two
|
|
339
|
+
durable channels for the same thing would make neither authoritative.
|
|
340
|
+
- **`attended_by: host` — a run a host session drives, that the framework never spawns on.**
|
|
341
|
+
One optional top-level key in `run.yml`, set at creation with `tldrx run new --attended-by host`
|
|
342
|
+
or flipped later with `tldrx run attend host` / `tldrx run attend --none`. The finding it is
|
|
343
|
+
built for is one sentence of field notes from 2026-08-30: a bare `tldrx next` on a Build stage
|
|
344
|
+
runs the WHOLE remaining headless pipeline — every wave, every story, as paid spawns — when the
|
|
345
|
+
host wanted one re-review. Six of six of those spawns then died on `Reached maximum budget` at
|
|
346
|
+
caps a Plan agent had authored assuming host-billed sub-agents. $9.95, nothing delivered. The
|
|
347
|
+
affordance was missing at the RUN level: `--prepare`/`--commit` is a decision per invocation,
|
|
348
|
+
and nothing on the run could say "this one is being driven by a host session".
|
|
349
|
+
- **A bare `tldrx next` exits `4` and names the exact command** the stage is waiting for —
|
|
350
|
+
`--prepare` when it is ready, `--commit` when a bundle is already out. Four, not two: the run
|
|
351
|
+
is not refusing the work, it is waiting on the host to take a turn, which is the same shape as
|
|
352
|
+
waiting at a gate and the code `run auto` already stops cleanly on. The refusal is the first
|
|
353
|
+
thing in `runStage` — ahead of the budget gate, ahead of reading an input, ahead of assembling
|
|
354
|
+
a prompt — so nothing is billed and nothing is written.
|
|
355
|
+
- **`--dry-run` is refused with it, and the message says why.** `--dry-run` is `mode: headless`:
|
|
356
|
+
it spawns a real sub-agent and the turn is billed, and only the non-handoff FILES are reverted
|
|
357
|
+
afterwards. That is measured, not read — one `agent.spawned`, one `agent.result`, the cost on
|
|
358
|
+
the ledger. The CLI reference said "Spawns nothing, writes nothing" and a comment in
|
|
359
|
+
`next.ts` said the same; both were wrong and both are corrected here.
|
|
360
|
+
- **`tldrx run auto` is refused at exit `1`**, before the event log is opened, so nothing is
|
|
361
|
+
written. A loop whose whole job is calling `next` headless has nothing to do on a run where
|
|
362
|
+
`next` headless is a refusal.
|
|
363
|
+
- **Three layers, because "nothing spawns" is a promise about money.** `runNext` refuses; every
|
|
364
|
+
executor (`build`, `watch`) refuses a headless context with `refused: true`, so the stage goes
|
|
365
|
+
back to `ready` rather than being marked failed; and `spawnAgent` itself throws while an
|
|
366
|
+
attended run is in flight. The third is what makes "no run path can reach a spawn" a property
|
|
367
|
+
rather than a claim about three `if`s — a fourth call site is always one merge away.
|
|
368
|
+
- **`tldrx run attend` is deliberately small**: it sets one field, appends one `run.attended`
|
|
369
|
+
event carrying the new value and the old, and touches no stage, no output, no branch and no
|
|
370
|
+
money. `--none` REMOVES the key rather than blanking it, because `attended_by: null` is not a
|
|
371
|
+
legal value. A direction is required and never guessed (exit `1`); setting what is already set
|
|
372
|
+
appends nothing, since a decision nobody made does not belong in the log; a `done` or
|
|
373
|
+
`cancelled` run is refused (exit `2`).
|
|
374
|
+
- **`tldrx run status` prints `attended: host`** and the status line gains an `att` marker
|
|
375
|
+
beside `auto:N` / `stale:N`, leading them because it is the one that changes what `tldrx next`
|
|
376
|
+
will do. `--json` gains `attended_by`, appended after `unmetered_tasks` so every existing key
|
|
377
|
+
keeps its position.
|
|
378
|
+
- **Additive, and asserted as such.** Absent — which is every run.yml written before this — the
|
|
379
|
+
framework may spawn and every path is what it was: the two-stage headless fixture's event
|
|
380
|
+
sequence is asserted against the one captured from `main` at `dae1d07`, event for event, and
|
|
381
|
+
an ordinary run.yml never mentions the key. A value the reader does not understand is a schema
|
|
382
|
+
error rather than a silent downgrade to "spawn anyway"; `requireKeys` ignores unknown top-level
|
|
383
|
+
keys, so an older binary reading a run.yml with `attended_by` still validates it — but it will
|
|
384
|
+
DROP the key on its next save, since `emitRunYaml` only writes what it knows.
|
|
385
|
+
- Out of scope on purpose: `tldrx expert train` and `tldrx seed triage --propose` spawn outside a
|
|
386
|
+
run and are untouched. `attended_by` is a property of a run.
|
|
387
|
+
- **`economy: metered-usd | host-tokens` on `budget.yml`** — a price gets a currency, and a
|
|
388
|
+
headless spawn under a ceiling that is not money is refused before it spends. Measured
|
|
389
|
+
2026-08-30 on `260830-tenancy-identity-customers`: the Plan agent priced the run assuming
|
|
390
|
+
HOST-billed sub-agents — turns the host session pays for, which this process never meters
|
|
391
|
+
and which are ~free to the run — and the executor then enforced those figures as dollar
|
|
392
|
+
ceilings on METERED spawns. Six spawns of six died on `Reached maximum budget`, each
|
|
393
|
+
having spent real money to get there: **$9.95**, for nothing. The money model was a single
|
|
394
|
+
scalar with no unit on it and no way to say *"this number is not dollars."*
|
|
395
|
+
- **One optional key, three places**: the run level of `budget.yml`, any `phases[]` entry
|
|
396
|
+
of it (which overrides the run), and the root of `03-plan/budget.yml`, so a Plan agent
|
|
397
|
+
can say which economy it was pricing in. Resolution is phase-then-run.
|
|
398
|
+
- **`tldrx next` refuses a headless spawn on a `host-tokens` phase — exit 2, above prompt
|
|
399
|
+
assembly, before a byte is written or a cent is spent.** The message names the number,
|
|
400
|
+
the unit, and both ways out (`tldrx next --prepare`, or re-label and re-price the
|
|
401
|
+
phase). `--prepare` / `--commit` are untouched: the in-session handshake is exactly
|
|
402
|
+
where a host-billed turn belongs.
|
|
403
|
+
- **The two are never converted into one another.** There is no exchange rate here and
|
|
404
|
+
inventing one would be a guess about a price. The budget-gate hook does not deny on such
|
|
405
|
+
a phase (there is no dollar ceiling to enforce, and it says so on stderr), the auto
|
|
406
|
+
gate's money condition reads `n/a (host-tokens economy)` rather than comparing a spend
|
|
407
|
+
in dollars to a ceiling in tokens, and `tldrx run estimate` prices the stage in TOKENS
|
|
408
|
+
with no dollar figure at all.
|
|
409
|
+
- **A `03-plan/budget.yml` priced in `host-tokens` contributes no story caps.** Its
|
|
410
|
+
numbers are not dollars, so the Build executor falls back to the uniform share it used
|
|
411
|
+
before plan prices were read at all — and says so on stderr, through the advisory seam
|
|
412
|
+
that already existed for an unusable plan budget.
|
|
413
|
+
- **An unknown value is REFUSED, never defaulted to dollars** — a unit nothing here
|
|
414
|
+
understands is not one it may quietly read as money. An empty `economy:` key, and an
|
|
415
|
+
absent one, both mean `metered-usd`.
|
|
416
|
+
- **`tldrx budget raise` no longer erases what it rewrites past.** The label round-trips
|
|
417
|
+
through the same emitter the raise goes through; a raise that dropped it would turn a
|
|
418
|
+
token budget back into dollars silently, from the one command an operator reaches for
|
|
419
|
+
when a ceiling binds.
|
|
420
|
+
- **Absent label ⇒ byte-identical behaviour**, asserted: an unlabelled `budget.yml` emits
|
|
421
|
+
no `economy:` line, an unlabelled headless stage still spawns, and every existing budget
|
|
422
|
+
test passes untouched.
|
|
423
|
+
|
|
424
|
+
- **`tldrx story reopen <id> --note "<why>"`** — one Build story, given another run of
|
|
425
|
+
developer attempts, by a person. The third verb of the family that landed 2026-08-30 and
|
|
426
|
+
the only one a HUMAN signs: the other two stop the machine reading a transport failure
|
|
427
|
+
as a judgement, and this one is for when the machine read the run correctly and the
|
|
428
|
+
owner disagrees. Found on `260830-tenancy-identity-customers`, where story S3 sat
|
|
429
|
+
`blocked` after two GENUINE `changes` verdicts (its developers ran and committed nothing;
|
|
430
|
+
both reviewers correctly refused an empty diff) — no rescue applied and none should have,
|
|
431
|
+
but S3 gates wave 3 and the owner had decided it ships. The only reopening verb was
|
|
432
|
+
`tldrx reject --stage`, which acts on a STAGE, and hand-editing `run.yml` or a story file
|
|
433
|
+
is forbidden by design.
|
|
434
|
+
- **The note is required** — a reopen with no reason is not actionable — and one
|
|
435
|
+
`story.reopened` is appended carrying the actor, the note, the status the story came
|
|
436
|
+
from, its wave, and how many verdicts the closed run of attempts consumed.
|
|
437
|
+
- **The story goes back to `todo` and its attempt counter restarts at 1 of 2.** Nothing
|
|
438
|
+
is erased to make that true: `story.reopened` is a RESET BOUNDARY that
|
|
439
|
+
`readReviewLedger` reads, so verdicts before it stop counting while every event stays
|
|
440
|
+
in `events.jsonl` for `replay`, `cost` and `retro`. The full reset is the honest
|
|
441
|
+
choice precisely because the history survives it — "you get two more turns" is what
|
|
442
|
+
overruling a block means, and a half-reset would be a number nobody could explain from
|
|
443
|
+
the record.
|
|
444
|
+
- **It runs no agent, spends nothing, deletes nothing and refunds nothing.** The story's
|
|
445
|
+
BRANCH is what carries the last developer's commits forward and it is untouched; the
|
|
446
|
+
worktree is left exactly as the build left it (kept at `review`, already removed for a
|
|
447
|
+
`blocked` story) and is reopened from the branch when the next turn needs it.
|
|
448
|
+
- **It does not make the stage runnable, and does not pretend to.** Sending a stage back
|
|
449
|
+
is `reject`'s own signed decision, so the output names the command that fits where the
|
|
450
|
+
Build stage actually is: `tldrx next` when it is ready, `tldrx reject --note` at a
|
|
451
|
+
pending gate, `tldrx reject --stage` over a signed one.
|
|
452
|
+
- Refuses with exit 2, saying why: an id the plan does not have (naming the ones it
|
|
453
|
+
does); a `done` story, because undoing finished work is a decision about the stage and
|
|
454
|
+
belongs to `reject --stage`; a `todo` story, which is already pending; and a missing
|
|
455
|
+
`--note`. An unknown run id is still exit 3.
|
|
456
|
+
- `tldrx replay` narrates it (`story S3 REOPENED by alan — back to \`todo\` from
|
|
457
|
+
\`blocked\` — "…"`), and the Build stage says so in one line, with the note, when it
|
|
458
|
+
picks the story up. A reopened story is byte-identical on disk to a never-started one,
|
|
459
|
+
and a narrative that showed two `changes` verdicts and then a third developer turn with
|
|
460
|
+
nothing in between would read as the framework losing count.
|
|
461
|
+
- **`--parallel <n>` on `tldrx next` and `tldrx run auto`** — how many stories of ONE
|
|
462
|
+
build wave run at once. `waves.yml` already puts every dependency in an earlier wave, so
|
|
463
|
+
a wave's stories are independent by construction. Also settable per scope as
|
|
464
|
+
`build: {parallel: N}` at the top of a workflow, or per stage as `parallel:` in
|
|
465
|
+
`stage.yml`; the flag beats the workflow, which beats the stage file. **The default is 1
|
|
466
|
+
and at 1 the executor takes exactly the path it always did** — verified byte-identical on
|
|
467
|
+
the event sequence against `main`, not asserted. Above 1 the wave runs in two halves:
|
|
468
|
+
developer + DoD + commit concurrently, then merge + reviewer serially in the wave's
|
|
469
|
+
LISTED order, so the epic branch reads the same whatever order the machine finished in.
|
|
470
|
+
The reviewer half is serial for a reason and not only the merge: a reviewer reads
|
|
471
|
+
`git diff <epic>...<story>`, whose merge base moves every time another story merges into
|
|
472
|
+
that epic. A red story does not cancel its siblings, but the wave ends `failed` and the
|
|
473
|
+
next wave does not start. Ctrl-C/SIGTERM kills every live child, not the first.
|
|
474
|
+
Budgets are untouched: the stage ceiling was already divided by
|
|
475
|
+
`stories x attempts x (developer + reviewer)`, so N at once costs what N in a row cost.
|
|
476
|
+
- The live view gives each running story its own column — `S1 reading … · S2 $ dotnet
|
|
477
|
+
test …` — in the scene, the compact one-liner and `--ui plain`. A lane leaves the line
|
|
478
|
+
when its sub-agent finishes. With nothing parallel the view is what it always was.
|
|
479
|
+
|
|
480
|
+
- **`gates_policy: agent` — a gate an agent may close, over a check it wrote down.** The third
|
|
481
|
+
answer to "who closes a gate", beside `human` (waits) and `auto` (the harness signs when seven
|
|
482
|
+
measured conditions hold). Measured 2026-08-30 on `260830-tenancy-identity-customers`: the host
|
|
483
|
+
ran a defined checklist at every gate and typed it into `approve --note "<free text>"`, where
|
|
484
|
+
nothing validated it, `replay` could not render it, and `run.yml` recorded a person's name for a
|
|
485
|
+
check a sub-agent had made. The evidence note (above) was the artefact; this is the gate.
|
|
486
|
+
- **Strictly stronger than an auto gate, never a cheaper one.** Three things, not one: every
|
|
487
|
+
one of the seven `auto` conditions unchanged and unweakened (including the `boundary`
|
|
488
|
+
condition landed alongside it), PLUS no budget decision in this stage's window, PLUS an
|
|
489
|
+
evidence note that parses, sources every bullet, and whose verdict is `sign`.
|
|
490
|
+
- **The budget requirement is an EVENT, not an arithmetic.** A `budget.raised` or
|
|
491
|
+
`budget.blocked` in `events.jsonl` at or after the stage's `started_at` falls the gate to a
|
|
492
|
+
person even when the spend is comfortably under the ceiling. Condition 3 already compares
|
|
493
|
+
numbers; what it cannot see is that somebody *raised* the ceiling to let this stage through,
|
|
494
|
+
and a decision made to unblock a stage may not then be signed off by the machine that was
|
|
495
|
+
blocked. Asserted in both directions on one fixture: the same gate closes without the event
|
|
496
|
+
and falls through with it, with nothing else changed.
|
|
497
|
+
- **The same door, so the trail reads the same.** A closing agent gate goes through `approve`:
|
|
498
|
+
the checks are re-run off disk, `gate.by` records the note's `by:`, one ordinary
|
|
499
|
+
`gate.approved` is appended, and the cursor advances. `AUTO_GATE_ACTOR` is untouched —
|
|
500
|
+
`by: auto` still means "the facilitator closed it with no note but its own conditions".
|
|
501
|
+
- **The note is COPIED into the run tree**, at `<phase>/gate-evidence/<stage>.md`, and that
|
|
502
|
+
copy is what `gate.evidence.path` points at. `.agent/` is gitignored by spec §1, and a gate
|
|
503
|
+
whose evidence lives only in a gitignored directory is a gate nobody can audit from a clone.
|
|
504
|
+
A copy, not a move: the scratch original stays where the agent left it.
|
|
505
|
+
- **Four fallthroughs are named in their own right**, because a person's next move differs for
|
|
506
|
+
each: `questions` (a decision nobody has made), `budget-event` (a ceiling somebody moved),
|
|
507
|
+
`boundary` (work nobody scoped) and `refusal` (the note's verdict is `refuse` or
|
|
508
|
+
`sign-with-fixlist` — the agent doing its job, not failing at it). Any other failing condition
|
|
509
|
+
reports as `condition`, a missing or broken note as `evidence`. Each is tested in isolation:
|
|
510
|
+
a report that fired three at once would not answer "which of these stopped it", which is the
|
|
511
|
+
first question anybody asks.
|
|
512
|
+
- **`tldrx approve --as-agent [--evidence <path>]`** is the same decision taken by hand, and it
|
|
513
|
+
splits the two refusals apart by exit code. **Exit 2** is "this note is broken" — fix the
|
|
514
|
+
file, nothing was signed. **Exit 4** is "a person decides": the note parsed perfectly and its
|
|
515
|
+
verdict is not `sign`. **Exit 1** is `--as-agent` on a stage whose policy is not `agent`: a
|
|
516
|
+
run keeps the policy it was opened with, and a flag that could upgrade one at approve time
|
|
517
|
+
would make the frozen policy decorative.
|
|
518
|
+
- **A person may always overrule it.** A plain `tldrx approve` on an agent-gated stage works
|
|
519
|
+
exactly as it does anywhere else, is recorded as the person, and writes no `evidence` key. An
|
|
520
|
+
agent gate is one an agent MAY close, never one a person may not.
|
|
521
|
+
- **`tldrx replay` renders the check**, not just the signature: who signed, how many files they
|
|
522
|
+
read, how many citations they spot-checked and what those resolved to, how many touched paths
|
|
523
|
+
they audited and how many were outside the surface, and the path to the note. Rendered from
|
|
524
|
+
the note's FRONT MATTER and from `run.yml` — never from its prose, which would change the
|
|
525
|
+
narrative every time somebody rephrased a sentence. A note that has gone missing is SAID to
|
|
526
|
+
be missing rather than invented, and the counts `run.yml` recorded still stand.
|
|
527
|
+
- **`--gates` gains a qualified form**: `--gates plan:agent,build:agent`. A bare entry still
|
|
528
|
+
means `human`, so every invocation anybody has already typed means exactly what it meant. An
|
|
529
|
+
unknown policy is its own usage error, distinct from an unknown stage.
|
|
530
|
+
- **Additive, and asserted as such.** No shipped scope uses `agent`; it arrives via
|
|
531
|
+
`--gates`, or a fork's own workflow file, and never by default. An absent `gates_policy`
|
|
532
|
+
entry still reads as `human`. A gate with no `evidence` emits no key at all, so every
|
|
533
|
+
`run.yml` written before this round-trips byte-for-byte through a save — asserted by
|
|
534
|
+
comparing the emitter's output against the file on disk. The gate mapping never rejected an
|
|
535
|
+
unknown key (measured against the reader that predates this), so a `run.yml` carrying
|
|
536
|
+
`gate.evidence` still validates on an older binary; a `gates_policy` naming `agent` there
|
|
537
|
+
fails loudly instead, which is the right failure — a policy the reader does not understand is
|
|
538
|
+
not one it may downgrade to "sign it anyway".
|
|
539
|
+
- **The emitter had to be extended, not worked around.** `emitRunYaml`'s `gate()` wrote exactly
|
|
540
|
+
five keys as a flow mapping, so a sixth held in memory would have been dropped, silently, by
|
|
541
|
+
the next save. 47 new tests.
|
|
542
|
+
|
|
543
|
+
- **The gate evidence note** — `.agent/<stage>/evidence.md`, plus `tldrx gate template` to
|
|
544
|
+
write the blank form. This is the artefact half of the `agent` gate (design §A): a third
|
|
545
|
+
answer to "who closes a gate", between `human` (waits) and `auto` (the harness signs when
|
|
546
|
+
seven measured conditions hold). Measured 2026-08-30 on
|
|
547
|
+
`260830-tenancy-identity-customers`: the host ran a defined checklist at every gate and
|
|
548
|
+
typed `approve --note "<evidence>"` by hand, into a free-text field where nothing validated
|
|
549
|
+
it and `replay` could not render it. There was no value meaning *"an agent checked it,
|
|
550
|
+
showed its work, and is accountable for the check"*, so there was nowhere to put the check.
|
|
551
|
+
- **Front matter is the machine half, the body is the human half** — the §2.13 story
|
|
552
|
+
pattern, reused rather than reinvented. Required keys: `version gate role by at verdict
|
|
553
|
+
read citations touches diff_vs_stories`; `caveats` and `recommend` are optional and
|
|
554
|
+
default to `[]`. Four H2 sections in order — `Read` · `Citations checked` ·
|
|
555
|
+
`Touches audited` · `Verdict` — each with at least one list item.
|
|
556
|
+
- **Every bullet goes through the EXISTING §2.8 resolver.** Not a second grammar and not a
|
|
557
|
+
second checker: `srcToken.ts` tokenizes and resolves, and the section rule is
|
|
558
|
+
`handoff.ts`'s, lifted into a shared `validateSections` that `validateHandoff` now calls
|
|
559
|
+
too. Two readers of "is this bullet sourced" drift, and the looser one would win the
|
|
560
|
+
argument at exactly the moment a gate is being signed. A checklist whose own claims are
|
|
561
|
+
unsourced is what `claim-sources` exists to refuse, and an evidence note is a claim about
|
|
562
|
+
a claim.
|
|
563
|
+
- **`unverified` REFUSES here, unlike in a handoff.** A citation nothing could check does
|
|
564
|
+
not fail a stage (spec §2.8) but it is precisely what stops an AUTO gate closing (spec
|
|
565
|
+
§5, condition 5). An agent gate is strictly stronger than an auto gate, never a cheaper
|
|
566
|
+
one, so a `doc:` URL nothing in the workspace names cannot be what a signature rests on.
|
|
567
|
+
- **Seven refusals, each with its own message and its own `kind`**, so a caller routes on
|
|
568
|
+
the reason rather than on a string: unreadable or incomplete front matter · a missing
|
|
569
|
+
section or one holding only prose · a bullet with no `src` token or one that does not
|
|
570
|
+
resolve · `sampled > of` or `resolved + refuted > sampled` · `sampled: 0` with citations
|
|
571
|
+
on record · a verdict that is not `sign` · a `gate:` naming a stage other than the one at
|
|
572
|
+
the cursor. `verdict` is the kind that means "a person decides" rather than "this note is
|
|
573
|
+
broken" — `sign-with-fixlist` and `refuse` fall to a human by design, and the verdict
|
|
574
|
+
space is three because a reviewer can meet every acceptance criterion and still have
|
|
575
|
+
found three real defects nobody wrote a criterion for.
|
|
576
|
+
- **`tldrx gate template` fills what a tool can COUNT and leaves every judgement blank**:
|
|
577
|
+
the gate at the cursor, the time, how many citations the §2.8 resolver found across the
|
|
578
|
+
stage's declared outputs (patterns like `03-plan/stories/<id>.md` included), and how many
|
|
579
|
+
touched paths the plan declares. The blank form deliberately does not validate — a
|
|
580
|
+
template that parsed clean out of the box would be a signature nobody had to earn — and
|
|
581
|
+
it writes no `[src: …]` anywhere, the same rule `questions lint --fix` follows. It spends
|
|
582
|
+
nothing, spawns nothing, approves nothing and moves no cursor; an evidence note already on
|
|
583
|
+
disk is left alone (exit `2`) unless `--force` says otherwise.
|
|
584
|
+
- `validateEvidence(text, srcContext, {gate})` is the function `approve --as-agent` will
|
|
585
|
+
call before it records anything. Nothing in this change signs a gate, reads a
|
|
586
|
+
`gates_policy`, or writes into the run tree: the artefact layer lands first, on purpose.
|
|
587
|
+
|
|
588
|
+
- **Auto-gate condition 7, `boundary` — the stage stayed inside the surface the run declared.**
|
|
589
|
+
The other six ask whether the artefact is sound and whether the work finished. None of them
|
|
590
|
+
asks the question a reviewer asks first: *is this the work we scoped?* Measured 2026-08-30
|
|
591
|
+
on `260830-tenancy-identity-customers` — the host ran this check BY HAND at every gate,
|
|
592
|
+
because the framework ran it nowhere ("touches outside What boundary is NOT checked
|
|
593
|
+
anywhere"), and that run's own S3 review surfaced the shape it was worried about: a
|
|
594
|
+
Platform-layer file edited by a module story.
|
|
595
|
+
- **The surface** is the union of every `file:`-kind `[src: …]` citation in
|
|
596
|
+
`01-what/handoff.md` and `02-how/handoff.md`, and every `touches:` entry of every story
|
|
597
|
+
under `03-plan/stories/` — or of `04-build/implicit-plan.yml` when the scope skipped Plan.
|
|
598
|
+
A directory entry covers everything beneath it, which is how a story declares the files it
|
|
599
|
+
is about to create and the forced companions (a lockfile, a generated client) that come
|
|
600
|
+
with them. The citation half reuses `citedRepoPaths`, the derivation the implicit plan
|
|
601
|
+
already builds `touches:` from — the same §2.8 tokenizer, not a second one.
|
|
602
|
+
- **The measurement** is `git diff --name-only <default_branch>...<epic_branch>`, once per
|
|
603
|
+
repo the plan's epics name, through the Build phase's existing git seam. Nothing is
|
|
604
|
+
checked out, fetched or written, and the epic's own `branch:` is what is diffed — the ref
|
|
605
|
+
`openStory` actually cut, not one re-derived here.
|
|
606
|
+
- **Offending paths are NAMED**, up to eight then `+N more`, prefixed with their repo:
|
|
607
|
+
`boundary=2 changed path(s), 1 outside the surface: app:platform/Auth.cs; work outside the
|
|
608
|
+
declared surface is a boundary change — a human decides whether to widen the scope`.
|
|
609
|
+
"1 path outside the surface" is not something anybody can act on.
|
|
610
|
+
- **A human may still approve over it**, and that is the whole point: widening a boundary is
|
|
611
|
+
a decision, and the framework has no basis for making it. Work nobody scoped is often the
|
|
612
|
+
right work.
|
|
613
|
+
- **It never refuses on an absence.** Outside Build, with no epic branch cut yet, with no
|
|
614
|
+
repo on disk, with no plan, or on a run whose What cited no repo path at all, it is `n/a`
|
|
615
|
+
**with the reason in the note** — a condition that could not measure must not report that
|
|
616
|
+
it measured zero. Same shape as condition 6's `n/a (not a build stage)`.
|
|
617
|
+
- **`tldrx-work/`, `.tldrx/` and `.agent/` are excluded from BOTH sides**, through the same
|
|
618
|
+
`isStatePath` filter the implicit-plan derivation already applies. A handoff cites the
|
|
619
|
+
run's own state as evidence, and in a `root_is_repo: true` workspace the state sits inside
|
|
620
|
+
the product repo — neither is a boundary question.
|
|
621
|
+
- **A bare citation widens the surface rather than shrinking it.** `file := [repo ":"] path
|
|
622
|
+
":" line` makes the repo prefix optional, and `citedRepoPaths` skips the bare form because
|
|
623
|
+
it feeds a developer prompt, where a wrong guess puts another repo's file in front of an
|
|
624
|
+
agent. Here the risk is inverted — an unattributable citation would manufacture a false
|
|
625
|
+
refusal — so a bare path is admitted to every repo's surface. A check that refuses wrongly
|
|
626
|
+
is a check that gets turned off.
|
|
627
|
+
|
|
628
|
+
### Changed
|
|
629
|
+
|
|
630
|
+
- **The init report is coloured and carries a roll-up.** Repo names, confidence
|
|
631
|
+
(green/yellow/red), the counts and the `created`/`kept` verbs are inked, and a new
|
|
632
|
+
`files N written · N created · N kept` line answers "how much of this run was
|
|
633
|
+
regenerated, how much is new, and how much was mine and left alone" without reading the
|
|
634
|
+
per-file list. `stripAnsi(coloured) === uncoloured`, asserted.
|
|
635
|
+
- `detectWorkspace` and `buildMap` take optional progress callbacks. Both default to
|
|
636
|
+
doing nothing, so `tldrx map` and every other existing caller behave exactly as before.
|
|
637
|
+
- `tldrx init` now writes progress bytes to stderr like every other long-running command,
|
|
638
|
+
so the two `build.test.ts` cases that spawn it to exercise the node seam pass `--quiet`.
|
|
639
|
+
Their `stderr === ""` assertion is how a REAL warning gets noticed, and it still is.
|
|
640
|
+
- **`tldrx cost` is organised by ECONOMY, and prints no grand total.** `STAGE · ECONOMY ·
|
|
641
|
+
MEASURED · DECLARED`, one footer per economy, and a third line for attempts that reported
|
|
642
|
+
neither — no row spans both columns and nothing adds a dollar to a token. A footer that
|
|
643
|
+
printed `$1.70` under a run which had also burned 1.5M host tokens is the sentence the
|
|
644
|
+
label exists to stop. `--json` carries `economy` on every stage row and the set of
|
|
645
|
+
economies on the run. Attempt lines are unchanged, all four token counters included, and a
|
|
646
|
+
declared `--tokens` figure past a million now reads `~1.2M` rather than `~1200.0k`.
|
|
647
|
+
|
|
648
|
+
- `tldrx doctor` prints where the framework's own files are: a `framework <path>` line
|
|
649
|
+
naming the installed package that ships `stages/`, `workflows/` and `templates/`, and
|
|
650
|
+
saying that a project's overrides live in `.tldrx/stages/` and `.tldrx/workflows/`. The
|
|
651
|
+
`/tldrx` skill says the same in two lines. Measured 2026-08-30: a real session spent
|
|
652
|
+
1m22s on `find / -name build -type d -path "*stages*"` because nothing printed it.
|
|
653
|
+
|
|
654
|
+
### Fixed
|
|
655
|
+
|
|
656
|
+
- **A rejected knowledge file gets ONE repair round before the money is thrown away.**
|
|
657
|
+
Measured 2026-08-30 on `~/scavtopia`: `tldrx expert train dotnet-stack --area dotnet --mode
|
|
658
|
+
light` spent **$1.69**, the trainer wrote `knowledge/dotnet.md.partial`, and the validator
|
|
659
|
+
refused it for **two** bullets that asserted an execution (`exit 0`) and cited a file line.
|
|
660
|
+
The file went to `dotnet.rejected.md`, nothing reached `competencies.yml`, the status did not
|
|
661
|
+
move — $1.69 for zero evidence, over a mistake the checker could name in one line and the
|
|
662
|
+
writer could have fixed in one edit. `expert train` now hands those exact problems back to the
|
|
663
|
+
same trainer for one more turn before anything is quarantined.
|
|
664
|
+
- **A fresh spawn carrying the ORIGINAL prompt**, not a resumed session: `spawnAgent` has no
|
|
665
|
+
`--resume` and the session id is captured for the ledger only. Appending to the original
|
|
666
|
+
prompt is what keeps the repair possible at all — the citations to be fixed point into files
|
|
667
|
+
that were inlined in that prompt and nowhere else — and the byte-identical prefix reads the
|
|
668
|
+
cache the first turn paid to create. The appended `# REPAIR ROUND` section carries the
|
|
669
|
+
numbered verdict, the rejected file with a line-number gutter whose numbers are the `L<n>`
|
|
670
|
+
numbers in the verdict, and the reminder that deleting an offending bullet is a legal fix.
|
|
671
|
+
- **One round, and the gate does not move.** The repaired file goes through the same
|
|
672
|
+
`parseKnowledgeFile`, same shape, same scope. A second failure rejects exactly as the first
|
|
673
|
+
used to, quarantines the same way, and returns the same exit `5`. An unsourced claim still
|
|
674
|
+
cannot become evidence; it has simply been told once that it is unsourced.
|
|
675
|
+
- **Paid out of `--max-usd`, never on top of it.** The repair turn's ceiling is
|
|
676
|
+
`min(this sub-agent's share, whatever is left of the run's ceiling)`. Below the `$0.25`
|
|
677
|
+
floor it does not spawn and says so — a cold `claude -p` that dies on
|
|
678
|
+
`error_max_budget_usd` before its first reply costs money and produces nothing.
|
|
679
|
+
- **The operator is told while it happens**, so an extra sub-agent never spends silently:
|
|
680
|
+
`repairing: 3 problem(s) sent back to the trainer — one round, $0.31 of the ceiling left`,
|
|
681
|
+
then either `repaired: the second file validates` or `the repaired file does not validate
|
|
682
|
+
either (2 problem(s)) — one round is all there is`. Both turns land in `training.jsonl`; the
|
|
683
|
+
repair as `task: "code:repair"`, `repair: true`, with the number of problems it was sent.
|
|
684
|
+
- Not on `--commit`: there the sub-agent belongs to the host session and this process spawned
|
|
685
|
+
nothing, so repairing is running `--commit` again. Not for a file that was never written —
|
|
686
|
+
there is no verdict to send back.
|
|
687
|
+
|
|
688
|
+
- **The execution-claim rule is now TAUGHT, with an example and a counter-example.** The same
|
|
689
|
+
$1.69 run is the evidence that stating it once in a paragraph does not work. Both training
|
|
690
|
+
prompts — the spawned one and `--print-prompt` — now give the four literal shapes the checker
|
|
691
|
+
looks for (`exit <n>`, `<n>/<n> passed`, `build is green`, and the bare word `measured` in the
|
|
692
|
+
sentence itself), one conforming line, one refused line, and why the refused one is refused: a
|
|
693
|
+
`workspace.yml` line DECLARES a command and is not a record of running it. Two further gaps
|
|
694
|
+
closed: **not making the claim** is named as the other legal answer (the trainer that failed
|
|
695
|
+
had no command in reach of the sentence it was writing, so "run it and cite it" was not
|
|
696
|
+
actionable), and the `(measured)` **annotation** is explicitly exempted — §2.3 asks for it on
|
|
697
|
+
every bullet, `\bmeasured\b` is one of the patterns, and nothing had ever told a writer that
|
|
698
|
+
`claimCheck` strips the annotation before it looks. The runs-mode prompt never stated the rule
|
|
699
|
+
at all, though the validator has always applied it to both files; it does now.
|
|
700
|
+
|
|
701
|
+
- **`duplicate src` is documented as non-fatal, and the rejection report stops implying
|
|
702
|
+
otherwise.** It always was a warning — one call site, one `severity: "warning"`, one validation
|
|
703
|
+
path for both shapes — but the report did not say which lines were fatal and the headline
|
|
704
|
+
counted warnings as problems, so the same $1.69 run read as "3 problems" when only 2 rejected
|
|
705
|
+
it and the third was a duplicate that costs one bullet its evidence row. `describeKnowledgeIssues`
|
|
706
|
+
now lists errors first, prefixes warnings with `warning:` exactly as `knowledgeWarnings` does,
|
|
707
|
+
and the headline counts errors only (new `knowledgeErrors`). The reason it is a warning is
|
|
708
|
+
recorded where the message is emitted: "earns no second row" is a statement about scoring, not
|
|
709
|
+
about honesty, and throwing away every other finding in a file over a repeated citation is not
|
|
710
|
+
rigour.
|
|
711
|
+
|
|
712
|
+
- **A developer that FAILED is no longer recorded as a consumed attempt.** The
|
|
713
|
+
developer-side sibling of the reviewer fix below, found by the same run on 2026-08-30.
|
|
714
|
+
Five developer spawns on `260830-tenancy-identity-customers` died with
|
|
715
|
+
`Reached maximum budget ($0.30 | $0.40 | $0.50 | $0.90 | $1.50)` before delivering
|
|
716
|
+
anything the pipeline could see, and every one was settled as the story `blocked` —
|
|
717
|
+
terminal in-run — so six of seven stories were reported as tried and failed when five of
|
|
718
|
+
them had never been tried. A failed developer spawn now puts the story back at the status
|
|
719
|
+
it held BEFORE the attempt (`todo`, or `review` when a reviewer had asked for changes),
|
|
720
|
+
keeps its worktree, spends no attempt, and stops the in-process loop rather than buying
|
|
721
|
+
the same error twice. Its `check.failed` carries `check: "developer"`,
|
|
722
|
+
`status: "error"` and the error verbatim as `detail`; the review log, the operator line
|
|
723
|
+
and `retro.md` all say the developer **FAILED**, never that anything was reviewed. The
|
|
724
|
+
next `tldrx next` — headless or `--prepare` — offers the story again as a fresh developer
|
|
725
|
+
run at the **same attempt number**. A developer that RAN and produced work its DoD faulted
|
|
726
|
+
is a different thing entirely and still blocks, unchanged, as do two `changes` verdicts.
|
|
727
|
+
- **Runs recorded by the old code pick those stories back up.** A `blocked` story whose last
|
|
728
|
+
attempt recorded no commit, no check of any kind and no reviewer — the only trace the old
|
|
729
|
+
code left, the error itself having gone to `run.yml` alone — is read as the errored spawn
|
|
730
|
+
it was and offered again, with `S2 was blocked by a developer that FAILED (…) — that was
|
|
731
|
+
never an attempt, so it is offered again`. It is paired with the story's own plan, because
|
|
732
|
+
a story with an empty dod block blocks with exactly the same event shape and that block is
|
|
733
|
+
a plan bug. Measured read-only against the live run: `tldrx next --prepare` now offers
|
|
734
|
+
**S2** and, after it, S5, S4, S6 and S7 — while S3, which was blocked by two genuine
|
|
735
|
+
`changes` verdicts, stays blocked.
|
|
736
|
+
- **The auto gate will not sign a Build stage whose stories are not all `done`.** Its five
|
|
737
|
+
conditions were all about the ARTEFACT — citations, questions, money, status — and none of
|
|
738
|
+
them looked at what the stage was for. On the live run all five held while six of seven
|
|
739
|
+
stories sat `blocked` and the epic branch carried one story's work, and the gate signed the
|
|
740
|
+
stage, then signed it again after a human revoked it. A sixth condition now reads the story
|
|
741
|
+
statuses where they live, refuses with `stories=1 of 7 done — S2:blocked, S3:blocked, …`
|
|
742
|
+
and falls through to the human gate. A person may still approve over blocked stories —
|
|
743
|
+
what is worth shipping is their judgement — and outside the Build phase the condition is
|
|
744
|
+
measured as `n/a` and always holds.
|
|
745
|
+
- **A merge that moved nothing is no longer called "merged".** `git merge --no-ff` of a
|
|
746
|
+
branch that is already an ancestor exits 0 and says "Already up to date", and the handoff's
|
|
747
|
+
Gate section rendered that as landed work: on the live run it read
|
|
748
|
+
`(S1, S3, S5, S4, S7 merged)` when the epic tip carried only S1's commits. The executor now
|
|
749
|
+
counts what the merge is about to move BEFORE it moves it — afterwards it cannot, because a
|
|
750
|
+
merged branch is an ancestor either way — and the Gate line, the story Finding and the
|
|
751
|
+
review log all say “added nothing — identical to `epic/x`” for a count of zero.
|
|
752
|
+
- **A reviewer that FAILED is no longer recorded as a reviewer that asked for changes.**
|
|
753
|
+
Found live 2026-08-30 by the first `feature`-scope run to reach Build: the headless
|
|
754
|
+
reviewer of a 39-file, +1879-line story was given $0.26, died mid-read with
|
|
755
|
+
`Reached maximum budget ($0.26)`, and the executor wrote that transport error down as
|
|
756
|
+
`verdict: "changes"`. That single line spent the story's one requeue, sent a fresh
|
|
757
|
+
developer at code nobody had faulted, and would have blocked the story after a second
|
|
758
|
+
reviewer hit the same wall — with **zero review ever performed**. A failed reviewer now
|
|
759
|
+
settles the story at `review` with `verdict: "error"`: the attempt counter is untouched
|
|
760
|
+
(only a real verdict spends it), the `check.failed` event carries the error as its
|
|
761
|
+
`detail` plus `verdict: "error"` so no ledger counts it as changes-requested, and every
|
|
762
|
+
operator-facing line, the review log and `retro.md` all say the reviewer **FAILED**.
|
|
763
|
+
Fail-closed is unchanged — an unfinished review is still never an approval. Inventing the
|
|
764
|
+
verdict is what stopped.
|
|
765
|
+
- **`tldrx next` on a story whose review errored re-runs only the REVIEW.** The diff is
|
|
766
|
+
already committed and merged and its DoD went green, so there is nothing for a developer
|
|
767
|
+
to redo. Both doors do it: the headless path and `tldrx next --prepare`, which used to
|
|
768
|
+
hand the host session a full "attempt 2" developer bundle. The commit and the DoD results
|
|
769
|
+
come back out of `events.jsonl`, so the resumed reviewer sees the same proof the first one
|
|
770
|
+
did — including on the live run, where a `task.started` for the attempt that was never owed
|
|
771
|
+
sits AFTER the DoD it did not run, and must not erase it. Runs recorded by the OLD code resume too — a `verdict: "changes"` whose `detail` is
|
|
772
|
+
one of the framework's own transport errors is read as the failure it was, including a
|
|
773
|
+
story already left at `in_progress` by a wrongly-prepared attempt 2.
|
|
774
|
+
- **The Build executor reads `03-plan/budget.yml`.** The Plan writes a per-story price map,
|
|
775
|
+
the Plan gate validates it, and until now **nothing read it**: the executor split its
|
|
776
|
+
stage into equal shares, so the story priced at $4.75 and the one priced at $0.75 both got
|
|
777
|
+
$1.03. A priced story now gets `price / (attempts x (developer + reviewer))` as its
|
|
778
|
+
developer ceiling and a quarter of that as its reviewer's; an unpriced one keeps the
|
|
779
|
+
uniform share; prices adding up to more than the stage are scaled down proportionally. A
|
|
780
|
+
`budget.yml` that will not parse or validate is an advisory on stderr, never a refused
|
|
781
|
+
build.
|
|
782
|
+
- **A reviewer is never given less than $1.00.** Whatever the arithmetic says, clamped by
|
|
783
|
+
what the stage has left and by `per_agent_max_usd`. A reviewer that cannot finish reading
|
|
784
|
+
the diff approves nothing and blocks nothing — it converts the entire developer turn
|
|
785
|
+
beside it into a story stuck at `review`, which is what $0.26 did on 2026-08-30.
|
|
786
|
+
- **`tldrx cost` no longer prints `0 in · 0 out · 0 cache write · 0 cache read` for a turn
|
|
787
|
+
the host declared tokens for.** `tldrx next --commit --tokens 342527` writes that number
|
|
788
|
+
onto the task row and the `agent.result` payload, and the cost view ignored it. It now
|
|
789
|
+
renders as `~342.5k declared (host session)`, kept apart from the four measured counters
|
|
790
|
+
rather than folded into them: nobody measured those, and four zeroes claim the turn used
|
|
791
|
+
no tokens.
|
|
792
|
+
- **A stage whose declared outputs are a SHAPE no longer fails while the files sit next to
|
|
793
|
+
the error.** Found live 2026-08-30 by the first `feature`-scope run to reach Plan: the
|
|
794
|
+
stage wrote `03-plan/epics/E1.md` and `03-plan/stories/S1.md`..`S7.md`, and
|
|
795
|
+
`tldrx next --commit` refused it with "`03-plan/epics/<epic>.md` was declared as an output
|
|
796
|
+
but does not exist on disk; `03-plan/stories/<id>.md` was declared as an output but does
|
|
797
|
+
not exist on disk". Plan cannot name its outputs — it does not know how many stories there
|
|
798
|
+
will be until it has written them — so `stage.yml` declares the shape, and every
|
|
799
|
+
filesystem call was asking `existsSync` about a path with a literal `<id>` in it.
|
|
800
|
+
A declared path holding an angle-bracket token is now a **pattern**: it matches any file
|
|
801
|
+
in that directory with the pattern's fixed prefix and suffix, resolved against the run dir
|
|
802
|
+
and then the workspace root, in the same "first base wins" order everything else uses.
|
|
803
|
+
The fix is in `paths.ts`, at the seam, not in the validator: `present`/`missing` count a
|
|
804
|
+
pattern by its matches (so a stage taking `stories/<id>.md` as an INPUT gets past the gap
|
|
805
|
+
check), the prompt is handed the concrete files rather than the shape, the previous-attempt
|
|
806
|
+
inline shows every one of them, and `--dry-run` reverts each file it matched and names it.
|
|
807
|
+
`{repo}` expansion is untouched and still runs first, so `{repo}` and a token compose.
|
|
808
|
+
Plain paths behave exactly as before, down to the wording of their failure.
|
|
809
|
+
- A pattern output that matches nothing now fails honestly — "`03-plan/stories/<id>.md` was
|
|
810
|
+
declared as an output but **no file matches it on disk**", rather than claiming a file
|
|
811
|
+
nobody ever named was looked for and not found. Its `sections:` contract binds **every**
|
|
812
|
+
matched file, and the failure names the concrete file that broke it, not the shape.
|
|
813
|
+
- **`tldrx run estimate` prices cache traffic, which is where the money actually goes.**
|
|
814
|
+
Measured 2026-08-30 on a real workspace: a What stage was estimated at **$0.33** and the
|
|
815
|
+
one comparable real attempt cost **$1.70** — **5x**. That attempt's ledger says why: 56
|
|
816
|
+
input · 29.0k output · **166.3k cache write** · **3,747.1k cache read**. The estimate
|
|
817
|
+
multiplied input and output only, so it was adding up the two columns the money was not in.
|
|
818
|
+
Both cache counters had been on every `agent.result` since wave N and `modelPrices.ts` had
|
|
819
|
+
carried the multipliers the whole time — nothing needed new data, only arithmetic that used
|
|
820
|
+
it. The estimate now prices four terms: measured prompt tokens at the input rate, plus the
|
|
821
|
+
**median** cache write (**1.25x** input), cache read (**0.1x** input) and output of past
|
|
822
|
+
attempts at the same stage id, falling back to attempts at any stage and **naming which
|
|
823
|
+
sample it used**. It prints the breakdown —
|
|
824
|
+
`input ~189 · cache write ~166k · cache read ~3,747k · output ~29k → ~$1.46` — and keeps
|
|
825
|
+
saying "ESTIMATE" in words. With no history the old behaviour stands (it refuses to guess
|
|
826
|
+
the output half) and it now says `cache traffic not modelled — first attempt of this kind`
|
|
827
|
+
rather than pricing a silent zero. The input and cache-write terms overlap on a cold first
|
|
828
|
+
turn, so a first attempt leans high; that is stated in the output's own honesty line, not
|
|
829
|
+
corrected away.
|
|
830
|
+
- **`tldrx cost` shows the cache write / cache read columns on every attempt line**, not only
|
|
831
|
+
on stage and run totals, and no longer hides them on a stage that ran once — previously an
|
|
832
|
+
attempt line carried cost, task and model and nothing about where the money went.
|
|
833
|
+
|
|
834
|
+
- **tldrx state survives the project's own `.gitignore` rules, and `doctor` detects a rule
|
|
835
|
+
that shadows it.** Found by a real user 2026-08-30: their repo carried the stock .NET
|
|
836
|
+
`[Ll]og/` ignore, which swallowed `tldrx-work/<run>/04-build/log/S1.md` — the Build phase's
|
|
837
|
+
per-story review log, which spec §1 marks committed and the handoff cites as
|
|
838
|
+
`[src: 04-build/log/<id>.md:1]`. Nothing errored; the file was written, `git status` stayed
|
|
839
|
+
quiet, and a teammate's clone never got it. `init`'s managed block only ever ADDED ignores,
|
|
840
|
+
so any pre-existing project rule (`log/`, `docs/`, a `*.yml` in a subdir) could hide state
|
|
841
|
+
and nothing noticed. The block now opens with `!tldrx-work/`, `!tldrx-work/**`, `!.tldrx/`
|
|
842
|
+
and `!.tldrx/**` — the bare pair and the `**` pair are both needed, because gitignore cannot
|
|
843
|
+
re-include a file whose parent directory is excluded — and the framework's own ignores follow
|
|
844
|
+
AFTER them, since a later pattern wins. Measured with `git check-ignore -v` against a repo
|
|
845
|
+
carrying `[Ll]og/`, not asserted from memory: the story log comes back not-ignored while the
|
|
846
|
+
product's `Logs/build.log`, `tldrx-work/*/.lock` and `.tldrx/cache/` stay ignored. Re-running
|
|
847
|
+
`init` upgrades a block written before this in place, markers and neighbouring rules kept.
|
|
848
|
+
- **`tldrx doctor` now says when a rule outside that block is still hiding state.** It runs
|
|
849
|
+
`git check-ignore --verbose --no-index -z` over four paths that must be tracked — the newest
|
|
850
|
+
run's `run.yml` and `events.jsonl`, a synthetic `04-build/log/` probe, and
|
|
851
|
+
`.tldrx/memory/facts.yml` — and prints each offender with the rule's own `file:line:pattern`,
|
|
852
|
+
so a `.git/info/exclude` or a nested `.gitignore` is named too. A warning: it never moves the
|
|
853
|
+
exit code, which is about the tools this machine has. `--json` gains `gitignoreShadow`, where
|
|
854
|
+
`null` means no workspace was scanned rather than nothing found.
|
|
855
|
+
- **The `## Inputs` preamble no longer claims files the budget dropped.** Measured on a
|
|
856
|
+
real Build prompt, 2026-08-30: 9 of 15 declared inputs were inlined, the other 6 carried
|
|
857
|
+
"It exists on disk; do not guess at its content" — and the preamble above them still read
|
|
858
|
+
"Their full content is inlined below, so there is nothing to open and nothing else to
|
|
859
|
+
find." The two documents the run existed to edit were among the six. The preamble is now
|
|
860
|
+
conditional in every prompt that has one (stage prompts and the developer prompt share one
|
|
861
|
+
renderer): with everything inlined it is the sentence it always was; with anything dropped
|
|
862
|
+
it is `Inlined below: <n> of <m> declared inputs.` followed by "The rest exist on disk —
|
|
863
|
+
READ them at the listed paths before relying on them; do not guess: <list>".
|
|
864
|
+
- **A touched path the story's worktree cannot read is flagged as such.** The developer works
|
|
865
|
+
in a worktree of the story branch, so a path that exists in the repo but is not committed
|
|
866
|
+
at that branch is unreadable there — and `existsSync(worktree/path)` called it a file the
|
|
867
|
+
story creates. Build now asks git (`git cat-file -e <branch>:<path>`) and marks it `NOT in
|
|
868
|
+
this worktree — its content is only what the handoff quotes`, plus one stderr line per
|
|
869
|
+
path: `warning: input <path> is not committed, so the story worktree cannot read it`. A
|
|
870
|
+
path that exists nowhere is still "does not exist yet — this story creates it".
|
|
871
|
+
- **The story's own goal wins the developer prompt's inline budget.** `touches` was spent in
|
|
872
|
+
list order, so on that same run `AGENTS.md` — cited once in passing — was inlined whole
|
|
873
|
+
and the two documents the goal named were in the dropped tail. Touched paths the story's
|
|
874
|
+
`goal`, acceptance criteria, test plan or title NAME now sort first into the 64 KB; a brief
|
|
875
|
+
that names nothing changes no order at all.
|
|
876
|
+
- **The developer is told to run an acceptance criterion's embedded pattern BEFORE it
|
|
877
|
+
edits.** Found on a real run's second Build of 2026-08-30: a derived criterion carried a
|
|
878
|
+
literal grep (`` Pending `DECISIONS-NEEDED.md` # ``, backticks included) and the markers
|
|
879
|
+
it was meant to count had been written three different ways, so it reported 0 against two
|
|
880
|
+
files that still held five real markers — the in-session driver only caught it by
|
|
881
|
+
measuring the inventory by hand. The developer prompt's `## Investigate` list now carries
|
|
882
|
+
the rule verbatim: validate the pattern against the current tree first; a criterion that
|
|
883
|
+
reports zero while the goal says the work exists is broken, so measure the real inventory,
|
|
884
|
+
use THAT as the completion test, and record the discrepancy in the handoff. The criterion
|
|
885
|
+
text itself stays data the story may not edit. `stages/build/stage.md` says so too.
|
|
886
|
+
- **The implicit story no longer `touches` tldrx's own state.** `touches` is derived from
|
|
887
|
+
every repo path the What handoff cites, and a handoff cites state as evidence: measured
|
|
888
|
+
2026-08-30, 13 touched paths of which three were `run.yml`, a `.tldrx/triage/**/split.yml`
|
|
889
|
+
and a `.agent/**/prompt.md`. The developer prompt inlines every touched path and calls a
|
|
890
|
+
change outside `touches` a plan deviation, so those three read as permission to rewrite
|
|
891
|
+
the run's own bookkeeping. Anything with `tldrx-work`, `.tldrx` or `.agent` as a path
|
|
892
|
+
segment is now dropped from `touches` and recorded in `notes:` as `excluded <path> from
|
|
893
|
+
touches: tldrx state is never story-writable`. Product documents are untouched.
|
|
894
|
+
- **A document your answer settles now joins the implicit story's `touches`.** Measured on a
|
|
895
|
+
real run, 2026-08-30: the run existed to settle six ADRs, the owner answered all six, and
|
|
896
|
+
the one thing the story could not edit was `ADR-D013-DELIVERY-ZONE-GEOMETRY.md` — the What
|
|
897
|
+
handoff never cited the file, `touches` is built from what the handoff cites, and the
|
|
898
|
+
developer prompt says a change outside `touches` is a plan deviation. The plan's own
|
|
899
|
+
`notes:` said so: "F010 settle no touched document". Build now reads the mapping rule
|
|
900
|
+
backwards as well — a file whose name carries a decision key (`ADR-D013-*.md`,
|
|
901
|
+
`decision-7.md`) that a fact of this run names is added, searched for beside the
|
|
902
|
+
already-touched files first and then across the repo, under the same ≤24 cap — and writes
|
|
903
|
+
`added <path> to touches: settled by F<n>` into `notes:`. A document no fact names is
|
|
904
|
+
never added.
|
|
905
|
+
- **The developer gets the WHOLE answer.** `.tldrx/memory/facts.yml` capped a fact at 300
|
|
906
|
+
chars and `captureAnswers` writes one as `"<question> — <answer>"`, so on that same run all
|
|
907
|
+
six answers were cut and four lost the clause naming the ADR they settle — including the
|
|
908
|
+
words "Accepts ADR-D009 as written." The cap is now 2000 (spec §2.5; the bound only moved
|
|
909
|
+
outwards, so every facts.yml already on disk stays valid), a cut fact ends in ` …` and
|
|
910
|
+
carries `truncated: true`, `01-what/questions.md` is a declared input of the implicit story
|
|
911
|
+
and is inlined into the prompt, and each apply-bullet quotes the full `[Answer]:` text and
|
|
912
|
+
cites both the fact and the line it came from: `[src: F010; 01-what/questions.md:82]`.
|
|
913
|
+
- **The implicit story's goal is the work, not the What's stale scoping.** With answered facts
|
|
914
|
+
the `goal:` list holds nothing but the apply-bullets, and the What handoff's Decisions move
|
|
915
|
+
to a `context:` list rendered under `## Context (from the What stage)` — after the objective,
|
|
916
|
+
labelled background, explicitly not a task. Before this, a run opened to get six decisions
|
|
917
|
+
answered told its developer, as its stated goal, "Out of scope: selecting an answer on the
|
|
918
|
+
owner's behalf … every relevant ADR is status `proposed`". The plan note now names the facts
|
|
919
|
+
the story is for (`… applies the run's answered decisions (F005–F010) …`). With no answers
|
|
920
|
+
nothing moves: the What's decisions are still the goal.
|
|
921
|
+
- **`tldrx next --prepare --discard-pending` re-derives an implicit plan**, instead of
|
|
922
|
+
re-rendering the same story. The flag was handled only for stages with no executor, so on
|
|
923
|
+
Build it did nothing at all: `04-build/implicit-plan.yml` is written once and read forever
|
|
924
|
+
after, and re-preparing could not pick up a fix. It now bins the bundle's `pending.json`,
|
|
925
|
+
`result.json` and `result.raw.json`, derives the plan again from the handoff and the answers
|
|
926
|
+
as they stand, and prepares a fresh bundle — reusing this run's own epic branch and story
|
|
927
|
+
worktree rather than re-cutting or refusing them. It refuses to rewind a plan something has
|
|
928
|
+
been built off (recorded evidence, a settled story, or a commit on `story/<run>/S1` beyond
|
|
929
|
+
the epic) and prints which of those stopped it.
|
|
930
|
+
- **The dirty-tree check ignores tldrx's own state (`tldrx-work/`, `.tldrx/`).** In a
|
|
931
|
+
`root_is_repo: true` workspace the framework's state lives INSIDE the product repo, so
|
|
932
|
+
Build refused the files it had just written itself. Measured 2026-08-30: `tldrx next
|
|
933
|
+
--prepare 260830-decisions-gate` exited 2 with `repo \`aparece-v2\` has 4 uncommitted
|
|
934
|
+
change(s) on \`main\``, and all four were tldrx's — `run.yml` and `events.jsonl` (rewritten
|
|
935
|
+
on every `next`), `.lock` (the run lock) and `04-build/` (the implicit plan written seconds
|
|
936
|
+
earlier). A user's uncommitted answers under `tldrx-work/` blocked it the same way, though
|
|
937
|
+
those are committed on the user's cadence, not as a precondition of Build. Product dirt
|
|
938
|
+
still refuses exactly as before — same message, same fix — and the message now lists only
|
|
939
|
+
product paths; when the only dirt was state, one line says how many files were excused. A
|
|
940
|
+
story commit excludes the same two paths by pathspec: a story worktree is a checkout of the
|
|
941
|
+
same repo, so `git add -A` could otherwise sweep the run folder into the diff a reviewer
|
|
942
|
+
reads (measured: it did). Multi-repo workspaces, whose state is a sibling of the repos
|
|
943
|
+
rather than inside them, are untouched.
|
|
944
|
+
- **A scope that skips the Plan phase can Build.** `docs`, `hotfix`, `performance`,
|
|
945
|
+
`prototype` and `security-patch` all list `build` in `stages:` and `plan` in `skips:`, and
|
|
946
|
+
every one of them was a dead end: `stages/build/stage.yml` declares `03-plan/waves.yml` as
|
|
947
|
+
an input and the executor's first act was to load `03-plan/`, so a real `docs` run parked
|
|
948
|
+
at `04-build (ready)` could only fail its own Build stage with `03-plan/ does not
|
|
949
|
+
validate — stories/: the Plan wrote no stories`. Build now writes the one story that
|
|
950
|
+
decision implies into `04-build/implicit-plan.yml`, deterministically and with no model
|
|
951
|
+
involved: title from `run.yml`, `goal` from `01-what/handoff.md` § Decisions verbatim
|
|
952
|
+
(`[src: …]` tokens kept), `acceptance` from `01-what/success-metrics.md`, `touches` from
|
|
953
|
+
the repo paths that handoff CITES and that exist (≤24, first-cited order, a citation with
|
|
954
|
+
no repo prefix skipped rather than guessed at), `dod` from the commands `workspace.yml`
|
|
955
|
+
declares for the roles the scope calls for, and `budget_usd` from the Build stage ceiling.
|
|
956
|
+
A real `03-plan/` always wins. `tldrx next` prints one line naming the reason, and
|
|
957
|
+
`tldrx run status` prints `plan: implicit (scope skips Plan)` so a synthesised plan never
|
|
958
|
+
reads like one a person approved. The plan carries the work **forward**: bullets whose
|
|
959
|
+
subject is the What stage's own deliverable are dropped on five literal signals
|
|
960
|
+
(`questions.md`, `### Q`, an `01-what/` path, a question id, the run's-questions
|
|
961
|
+
vocabulary) with every drop and its signal recorded in the story's `notes:`, every
|
|
962
|
+
live fact stamped with this run adds
|
|
963
|
+
`Apply <fact> to the touched files [src: F<n>]` to `goal`, and `acceptance` gains a check
|
|
964
|
+
that each document one of those facts settles — the fact's text mentions that file's ADR
|
|
965
|
+
id or decision number — no longer reads `Status: proposed`. A mapping that cannot be
|
|
966
|
+
derived is reported in the story's `notes:` and falls back to "apply every listed fact;
|
|
967
|
+
leave a one-line note per file saying which fact changed it", never to a guess. A fact
|
|
968
|
+
cut at §2.5's 300-char cap is matched against the full `[Answer]:` behind it in
|
|
969
|
+
`01-what/questions.md`: measured on a real run, `captureAnswers` had sliced the ADR
|
|
970
|
+
clause off four of six facts, so 2 of 6 mapped on the stored text and 6 of 6 map with
|
|
971
|
+
the answer. The grep in that criterion is complete or names `notes:` wholesale — a
|
|
972
|
+
`(+1 more)` inside a command is something a person pastes and reads wrong. The
|
|
973
|
+
developer prompt states plainly that Plan was skipped and this story applies the run's
|
|
974
|
+
answered decisions.
|
|
975
|
+
- **`skips:` in a workflow is read rather than decorative.** The schema declared the key and
|
|
976
|
+
the loader dropped it, so nothing could tell "the Plan phase has not run yet" from "no Plan
|
|
977
|
+
phase was ever going to run" — a distinction that cannot be made from disk, since both look
|
|
978
|
+
like an absent `03-plan/`. `WorkflowPreset.skips` now carries it down to `StageSpec`.
|
|
979
|
+
- **A DoD command is looked up by its `workspace.yml` KEY, not by matching the command text.**
|
|
980
|
+
Measured on a real .NET workspace: `lint: dotnet format --verify-no-changes` has no "lint"
|
|
981
|
+
anywhere in the string, so a text match silently found nothing and would have handed a docs
|
|
982
|
+
run an empty Definition of Done. `WorkspaceContext.commandRoles` keeps the keys.
|
|
983
|
+
- Build's declared `03-plan/…` inputs are treated as satisfied when the scope skips Plan, and
|
|
984
|
+
**only** those: every other missing input is still exit 1.
|
|
985
|
+
|
|
986
|
+
### Verified, not changed
|
|
987
|
+
|
|
988
|
+
- **The walk already skips vendored and generated trees**, and always did: `SKIPPED_DIRS`
|
|
989
|
+
in `detect/walk.ts` covers `node_modules`, `dist`, `build`, `out`, `bin`, `obj`,
|
|
990
|
+
`target`, `.venv`, `Pods`, `.next`, `.expo`, `coverage` and more, plus every
|
|
991
|
+
dot-directory below the root, and it is honoured by `walkFiles` (so by `countCodeFiles`
|
|
992
|
+
and `readSourceTree`) and by `findRepos`. There are now tests that say so: a fixture with
|
|
993
|
+
a `.ts` file planted in each of those trees, and a real git repo inside `node_modules`
|
|
994
|
+
that must not be reported as a workspace member. The slow part of `init` was never the
|
|
995
|
+
walk — it is `graphify update`, once per repo.
|
|
996
|
+
|
|
3
997
|
## 0.3.0 — 2026-08-30
|
|
4
998
|
|
|
5
999
|
Every measurement below was taken on a real workspace on 2026-08-29 unless another date is
|