tldr-experts 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +323 -0
- package/README.md +1 -0
- package/dist/hooks/answer-capture.js +13 -8
- package/dist/hooks/budget-gate.js +4 -4
- package/dist/hooks/{chunk-jp6jscsd.js → chunk-00h4xah7.js} +47 -3
- package/dist/hooks/{chunk-54vzevgt.js → chunk-26xkmfdr.js} +23 -5
- package/dist/hooks/{chunk-tj66vg1n.js → chunk-7hhjx1wk.js} +3 -3
- package/dist/hooks/{chunk-nqwg5ddj.js → chunk-ab6cv5v5.js} +6 -0
- package/dist/hooks/{chunk-d0rp8c68.js → chunk-bm9aqtg3.js} +1 -1
- package/dist/hooks/{chunk-fm141jak.js → chunk-p0ns6fex.js} +2 -1
- package/dist/hooks/{chunk-g8kkq85r.js → chunk-rr7n3j59.js} +26 -7
- package/dist/hooks/{chunk-3kmx3dmz.js → chunk-rw9zk47w.js} +33 -4
- package/dist/hooks/{chunk-k4nqzdw5.js → chunk-xxcy0tsa.js} +27 -22
- package/dist/hooks/dod-gate.js +2 -2
- package/dist/hooks/no-reask.js +3 -3
- package/dist/hooks/session-start.js +9 -9
- package/dist/hooks/statusline.js +7 -7
- package/dist/tldrx.js +8338 -6915
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,328 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0 — 2026-09-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **A run can now end in a pull request toward main, on purpose (#253).** Measured on two
|
|
8
|
+
workspaces on 0.16.1: $89.82 over 28 h and $26.38 over 5.6 h, and neither run could end in a
|
|
9
|
+
PR whatever it spent — every preset ends at `watch`, and `tldrx ship` refused to push the
|
|
10
|
+
branch it opens the PR for, citing a spec sentence that is not in the spec (what
|
|
11
|
+
`docs/spec.md` actually says is that the BUILD phase has no push wrapper). The last mile —
|
|
12
|
+
the only one the owner's sentence names, *"desde que le doy el run, hasta que me entrega un
|
|
13
|
+
PR hacia main"* — was a person typing `git push -u origin epic/<slug>`, then `tldrx ship`,
|
|
14
|
+
then watching the checks. It is now a decision taken once, at `tldrx run new --ship
|
|
15
|
+
<push|pr|merge>`, and frozen into `run.yml` as an additive `ship: {push, pr, auto_merge}`
|
|
16
|
+
block the way `gates_policy` is: not a sixth phase, because shipping has none of a phase's
|
|
17
|
+
properties — it spends nothing, produces a URL, and is gated by what every earlier gate
|
|
18
|
+
already signed. When `run auto` sees the run close it runs the same `tldrx ship` a person
|
|
19
|
+
would type: the epic is pushed through the ONE push wrapper in the codebase (in
|
|
20
|
+
`core/build/git.ts`, beside Build's git seam, with `core/run/ship.ts` pinned as its single
|
|
21
|
+
caller — the Build phase itself still has none), the PR opens with the body it always had,
|
|
22
|
+
and under `merge` the PR is armed with `gh pr merge --auto --merge` so the remote's own
|
|
23
|
+
checks decide. A PR that reports NO check is left open and the record says `merge: absent —
|
|
24
|
+
no checks to wait on` (§7), because GitHub's auto-merge over nothing to wait on is a merge
|
|
25
|
+
now, not a merge when green — the mutation that drops that guard reddens exactly one test.
|
|
26
|
+
`run.yml` gets one record (`pr_urls`, `merge`, per-repo `merges`, `shipped_at`) beside the
|
|
27
|
+
policy, so a loop re-run on a closed run ships nothing twice — while `tldrx ship` typed again
|
|
28
|
+
is the recovery after a partial failure: it re-arms the repo whose merge failed, leaves a
|
|
29
|
+
queued one alone, and never erases a recorded failure (pre-merge review caught the first
|
|
30
|
+
version overwriting it with an empty string and exit 0) — and `run.finished` carries `pr_url`
|
|
31
|
+
and `merge`.
|
|
32
|
+
A ship that is refused is the loop's exit 2 — it was asked for a PR and did not deliver one.
|
|
33
|
+
Absent the flag every run means what it meant: nothing pushed, nothing opened, the refusal
|
|
34
|
+
that names the `git push` command unchanged byte for byte. Every gate is still signed by
|
|
35
|
+
whoever `--gates` says, so an unattended ship is a deliberate `--gates none --ship merge`
|
|
36
|
+
and `run.yml` records both. Found on the way: `run.yml` is emitted key by key, so the
|
|
37
|
+
block was dropped by the first save until the emitter learned it — pinned by a round-trip
|
|
38
|
+
test.
|
|
39
|
+
- **`questions_policy: recommended` — the loop answers a question that carries its own
|
|
40
|
+
`Recommended:` line, and escalates only the ones that do not (#251).** Measured 2026-09-12 on
|
|
41
|
+
two headless runs at 0.16.1, every gate `auto`, `--wait-answers 8h --wait-gates 8h`: $89.82
|
|
42
|
+
and 28 h wall on one, $26.38 and 5.6 h on the other, the agents busy 4.9 h and 56 min of it,
|
|
43
|
+
and **10 owner questions, 9 carrying a `Recommended:` line the asking agent had written**. The
|
|
44
|
+
loop parked on every one, sent `question.raised`, and waited for a person to type
|
|
45
|
+
`tldrx answer` — over a pick the framework already held in a parsed field (`RECOMMENDED_RE`),
|
|
46
|
+
on the decision card and in the notification payload. Nothing acted on it, by design:
|
|
47
|
+
`waitForAnswers` "reads and does nothing else", and `Fact.decided_by` had no honest value for
|
|
48
|
+
a machine's decision. So: `run new --questions <stage,stage|stage:recommended|all|none>` —
|
|
49
|
+
`--gates`' grammar, extracted to ONE parser both flags now call rather than a second copy of
|
|
50
|
+
"split on the first colon" — freezes `questions_policy` into `run.yml` beside `gates_policy`,
|
|
51
|
+
additive and absent by default, so a run opened without the flag is byte-identical to one
|
|
52
|
+
opened before it existed and reads `human` everywhere. Under `recommended` for the cursor
|
|
53
|
+
stage, the moment a stage parks the loop takes each blocking question whose block names one
|
|
54
|
+
of its own options on a `Recommended:` line, through the SAME `writeAnswerSlot` +
|
|
55
|
+
`captureAnswers` that `tldrx answer` is — the footer, the `question.answered` and the
|
|
56
|
+
`fact.added` are a person's bytes — and the fact says the rest: `decided_by: agent-default`
|
|
57
|
+
(a THIRD value beside `owner`/`driver`, because `owner` there would be an audit record lying
|
|
58
|
+
in the dangerous direction), `alternatives` (the options not taken) and `recommended_why`
|
|
59
|
+
(the line's own reason). One `question.auto_answered` per answer reaches the notify hook with
|
|
60
|
+
the pick, the alternatives and the `--supersede` line that reverses it; no `question.raised`
|
|
61
|
+
goes out for a question the loop answered; and the deferred-gate release #247 built is now
|
|
62
|
+
one closure both the human-answer and the loop-answer paths call, so the second cannot form
|
|
63
|
+
a second opinion about when a gate is worth asking about. It never invents a pick: no
|
|
64
|
+
`Recommended:` line, a letter naming no option, or a block tagged with the new additive §2.7
|
|
65
|
+
keys `irreversible: true` / `money: true` is escalated exactly as before — the fixture that
|
|
66
|
+
proves it goes red the moment the `money:` guard is dropped. `tldrx run questions set
|
|
67
|
+
<stage>:<policy> --note "…"` mirrors `run gates set` on a shared engine, one
|
|
68
|
+
`questions.policy_changed` event, every refusal the gates verb has. The close's decided-tally
|
|
69
|
+
counts `agent-default` apart from "not stated" — it IS stated — and names it only when it is
|
|
70
|
+
non-zero, so every close that recorded none reads as it did.
|
|
71
|
+
- **`tldrx run auto --until-done [<n>]` relaunches the loop in-process after an exit it can do
|
|
72
|
+
nothing else with — a stage failure past `--retry-failed`, a thrown error that used to reach
|
|
73
|
+
`fail()` as a bare 1, a refusal whose remedy is mechanical — at most `n` times (default 5),
|
|
74
|
+
writing `run.relaunched` with the exit it recovered from; never over exit 4 (a person's),
|
|
75
|
+
never over a `budget.blocked` (nothing in-process moves the ceiling, #232/#244), and never
|
|
76
|
+
twice over the same last line. Measured 2026-09-12: five hand relaunches and 18 h before a
|
|
77
|
+
story ran (#252).** The loop body is now `runAutoOnce` and `runAuto` a bounded supervisor over
|
|
78
|
+
it — in the same process, because the loop holds no state and re-reads run.yml every
|
|
79
|
+
iteration, which is exactly what makes "run it again" honest. The run id the first attempt
|
|
80
|
+
resolves is pinned for every relaunch, and so is its spend baseline: "spent by this loop" and
|
|
81
|
+
`--max-usd` describe the one command a person typed, and a relaunch that reset the loop's own
|
|
82
|
+
ceiling would have been a ceiling in name only (a test pins it: `[fail, ok, ok]` under
|
|
83
|
+
`--max-usd 0.80` stops at exit 2 after two spawns, not three). One derivation,
|
|
84
|
+
`relaunchVerdict`, decides both the relaunch and whether the run-end notification is held, so
|
|
85
|
+
an attempt that is relaunched sends no `run.failed` and `run.finished` / `run.failed` go out
|
|
86
|
+
once, from the last attempt. A throw is caught only under the flag — without it the loop is
|
|
87
|
+
byte-identical, pinned against `--until-done 0`, and `fail()` still gets its exit 1. The
|
|
88
|
+
epic-claim-at-cut half of the issue is #262 and lives in the Build executor, not here.
|
|
89
|
+
|
|
90
|
+
- **Build entry proves the Definition of Done can run IN A WORKTREE before a developer is
|
|
91
|
+
paid (#254).** Measured on a live workspace, 2026-09-12 at 0.16.1: **18 h from `run auto` to
|
|
92
|
+
the first story that could run**, five relaunches, two of them environment and both invisible
|
|
93
|
+
to Build entry. The repo's `install:` named `./install.sh`, which existed in the human's
|
|
94
|
+
checkout and was never committed — a `git worktree` carries TRACKED FILES ONLY — so every
|
|
95
|
+
story's install failed identically in its own tree, each time after the story was opened and
|
|
96
|
+
immediately before the paid turn, and each time a person fixed it and relaunched. The base
|
|
97
|
+
pre-flight (#41) was green throughout and was right to be: it measures in the checkout, which
|
|
98
|
+
is where the file is. So Build entry now asks the other question once, before `agent.spawned`:
|
|
99
|
+
one throwaway detached worktree at the base sha, the declared `install:` run inside it, then a
|
|
100
|
+
resolution of each declared DoD command's first token in that tree. **Never the suite** — the
|
|
101
|
+
base pre-flight already measures the suite in the checkout and paying twice for it at every
|
|
102
|
+
entry is a cost nobody asked for. A refusal is exit 2, once, naming the exact path or binary.
|
|
103
|
+
Two decisions are the whole design. The `install:` command's own path token is refused by
|
|
104
|
+
`git ls-files` with NOTHING executed, and that is sound only because of WHEN the install runs:
|
|
105
|
+
nothing has installed anything yet, so a relative path the install NAMES cannot be one it
|
|
106
|
+
PRODUCED. A DoD command's path token gets the opposite treatment — tracked-ness decides
|
|
107
|
+
nothing there, because `node_modules/.bin/vitest` is untracked in every repo on earth and is
|
|
108
|
+
present by the time the DoD runs; it is probed AFTER the install, in that tree, and an
|
|
109
|
+
untracked twin in the checkout is rendered as advice, never as the verdict. The same rule has
|
|
110
|
+
a second half: a `dod` list runs in order, so a path named by any command AFTER the first may
|
|
111
|
+
be an artefact an earlier `dod` command builds (`dod: ["npm run build", "dist/check.sh"]`) —
|
|
112
|
+
the probe runs no suite, cannot tell that from "nobody committed it", and therefore writes an
|
|
113
|
+
advisory instead of a refusal. A bare binary name is never downgraded, at any index: nothing
|
|
114
|
+
a `dod` command does can put one on this process's PATH. Refusing what would have worked is
|
|
115
|
+
the expensive direction for an entry gate, so wherever the two readings could not be told
|
|
116
|
+
apart, this refuses less — each half pinned by a control that reddens when it is removed.
|
|
117
|
+
One existing behaviour moved with it: an install that fails deterministically in a fresh tree
|
|
118
|
+
is now refused at entry instead of blocking story 1 (`story-worktree-deps.test.ts` (d) says
|
|
119
|
+
so and says why). The per-story install is untouched and still answers for an install that
|
|
120
|
+
passes on the base and fails on a story's branch. The result is cached beside the base result in
|
|
121
|
+
`04-build/preflight.yml` under an additive `worktree:` key, so a resumed run does not re-pay
|
|
122
|
+
the install, and the cache is narrowed by three things and not two: the base sha, the
|
|
123
|
+
declaration hash (the install, every command probed, and the whole allowlist — the operator's
|
|
124
|
+
fix is an edit to `workspace.yml` and that edit must not be invisible), and AGE. A green gets
|
|
125
|
+
six hours rather than the base green's forever, because this row is a claim about an
|
|
126
|
+
ENVIRONMENT — the host's PATH, the registry the install reached — and none of that is in the
|
|
127
|
+
sha; a red gets the base red's 30 minutes, because #162 is the filed failure where a row
|
|
128
|
+
measured over a broken environment kept refusing after somebody fixed it. The gate costs
|
|
129
|
+
exactly zero where it could learn exactly nothing: a repo with no `install:` whose DoD names
|
|
130
|
+
only bare binaries opens no worktree at all, since `PATH` is the same in both trees and the
|
|
131
|
+
base pre-flight has already RUN the command there.
|
|
132
|
+
|
|
133
|
+
### Fixed
|
|
134
|
+
|
|
135
|
+
- **One blocked story no longer stops every later wave — the boundary asks per STORY, from
|
|
136
|
+
`depends_on` (#260).** Measured 2026-09-12 on one workspace at 0.16.1: a loop told to build 8
|
|
137
|
+
stories built 5, never started 3, and reported the stage `done`. `03-plan/waves.yml` scheduled
|
|
138
|
+
W1=[S1] W2=[S2,S5,S6,S7,S8] W3=[S3] W4=[S4]; S7 blocked in W2 and the build skipped W3 and W4
|
|
139
|
+
outright, leaving S3 and S4 at `todo` while S2 — the only thing S3 depends on — was `done`. It
|
|
140
|
+
happened twice on the same run, the second time after a `story reopen S7` and a relaunch. The
|
|
141
|
+
mechanism was one line: after each wave the parallel path asked `waveFailed`, which is
|
|
142
|
+
`stories.some(status === "blocked")`, and broke out of the loop — `depends_on` appeared **zero
|
|
143
|
+
times** in `executors/build.ts`, so the one fact that could tell "a story that needs the
|
|
144
|
+
blocked code" from "a story that never did" was never read, though `validatePlan` already
|
|
145
|
+
guarantees every dependency sits in an earlier wave and is therefore settled at that moment.
|
|
146
|
+
The rule the `break` defended is kept, and it is the reason this is a frontier and not a
|
|
147
|
+
deletion: **a story runs when every one of its `depends_on` is `done`**, and a story whose
|
|
148
|
+
dependency did not land is NOT attempted — nothing fans out over code that was not landed,
|
|
149
|
+
which is how one red story becomes N of them. What changes is that the rule stops applying to
|
|
150
|
+
stories that never needed that code. A story held back is now `blocked` WITH the reason,
|
|
151
|
+
`dependency S7 blocked` — absent-with-reason, never a silent `todo` — and that sentence is
|
|
152
|
+
what `blocked_reason` on `gate.requested` and the continue note read, so the gate stops being
|
|
153
|
+
"held by stories" with nothing a person or a turn can act on (the #239 shape, one layer down).
|
|
154
|
+
It is transitive for free: S4 behind a blocked S3 reads `dependency S3 blocked`. The documents
|
|
155
|
+
stop hiding it too — the stage line counts every story `waves.yml` scheduled instead of the
|
|
156
|
+
rows the executor happened to hold (`5 of 8`, never `5 of 5`), the skipped stories are named
|
|
157
|
+
on stdout with the dependency each waits on instead of one line that said the next wave "may"
|
|
158
|
+
have depended on something, and the handoff's `none — every scheduled story reached done` is
|
|
159
|
+
now decided against `waves.yml`: a scheduled story with no outcome at all is named in
|
|
160
|
+
`## Unknowns` rather than dropped, which is the record that was lying in the dangerous
|
|
161
|
+
direction (§7). **`--parallel 1` is untouched**: the sequential path never stopped a later
|
|
162
|
+
story, a story whose dependency blocked is still attempted there, and this frontier replaces
|
|
163
|
+
the parallel path's stop only — turning today's finished stories into blocked ones would be a
|
|
164
|
+
second change, in the opposite direction. Both directions are pinned by tests that are the
|
|
165
|
+
same fixture with one field moved, and the existing test that froze the defect as correct (an
|
|
166
|
+
independent S4 stopped by a blocked S2) was rewritten to give S4 the `depends_on` that makes
|
|
167
|
+
its assertion true for the reason the code's own comment gives — argued in the test, never
|
|
168
|
+
flipped in silence.
|
|
169
|
+
|
|
170
|
+
- **A fix-list finding now declares what it IS, and a documentation defect stops holding a story
|
|
171
|
+
(#255).** Measured 2026-09-12 across two workspaces at 0.16.1: three stories with a green
|
|
172
|
+
definition of done, a merged commit and an APPROVING reviewer settled `blocked` — every one of
|
|
173
|
+
them on a `fix-now` finding whose whole content was a stale docstring or an `[src: …]` line
|
|
174
|
+
that did not resolve. The reviewer was not wrong: the prompt says `fix-now` is "this story's
|
|
175
|
+
own correctness", and a stale docstring in the story's own files honestly reads that way. What
|
|
176
|
+
the record could not say was that the defect was TEXT — a disposition answers "where does this
|
|
177
|
+
go" and nothing answered "what is it" — so a blocked story held the Build gate's `stories`
|
|
178
|
+
condition and three unattended nights ended at a person. `severity` was no help and is not the
|
|
179
|
+
vehicle: it was a free string defaulted to `unrated` that NO consumer read (grep over `src`:
|
|
180
|
+
one render site), so the effect was binary, `fix-now` or nothing. Every finding now carries a
|
|
181
|
+
required `kind` — `correctness` and `security` are behaviour and hold the story exactly as
|
|
182
|
+
before, `docs` and `style` are the way the repo reads and do not — and a `docs`/`style` finding
|
|
183
|
+
submitted `fix-now` is routed to `defer-with-log`, staying in the artefact, in the PR body and
|
|
184
|
+
on `retro.md` with a `Normalised-from: fix-now` line saying what it was submitted as. Nothing
|
|
185
|
+
is lost; it just stops costing a night.
|
|
186
|
+
This makes a gate WEAKER on purpose, so both directions are priced and both are tested.
|
|
187
|
+
Unblocking is not free: declaring a `fix-now` finding `docs` costs the SAME `[src: …]` citation
|
|
188
|
+
`refuted` already costs, through the same §2.8 parser (owner decision, 2026-09-13) — one exit,
|
|
189
|
+
one rule. The sentence that forced it is "the docstring says cents, the code returns dollars":
|
|
190
|
+
its own words do not say which side is wrong, and a one-word field deciding that a money bug is
|
|
191
|
+
a typo is the one failure with no log line to find it by, so the reviewer cites the behaviour
|
|
192
|
+
that makes it harmless or leaves it `correctness`. When in doubt it blocks: a `kind` that is
|
|
193
|
+
absent, unreadable or outside the enum is refused — for FREE, indexed as a fault in the report
|
|
194
|
+
so it costs the story no attempt, because a reviewer that forgot a field did not do bad work.
|
|
195
|
+
Reading is tolerant the other way (§7): a fix list already on disk with no `Kind:` line, or one
|
|
196
|
+
whose `Kind:` does not narrow to the enum, reads as *not stated*, and not stated blocks.
|
|
197
|
+
The four reviewer-prompt goldens MOVED, and that is the change, not an accident: the schema
|
|
198
|
+
alone would have left a reviewer nobody told about the new field, so the prompt's own prose
|
|
199
|
+
explains it, and `test/build-golden.test.ts` froze that prose. The diff is exactly eleven
|
|
200
|
+
inserted lines at one insertion point in each of the four reviewer prompts and nothing else — no
|
|
201
|
+
event, no `run.yml` row, no exit code. The prose teaches only the JUDGEMENT (which of the four
|
|
202
|
+
words is true of the defect) and never spells the field name: gh #133's guard derives that list
|
|
203
|
+
from `REVIEW_SCHEMA` itself, and it caught the first version of this wording.
|
|
204
|
+
|
|
205
|
+
- **The spawned developer can delete a file, and a permission refusal is now a recorded reason
|
|
206
|
+
instead of two burned attempts (#261).** Measured 2026-09-12 on one workspace at 0.16.1: story
|
|
207
|
+
S7, "delete an unused file", was undoable. The developer's allowance carried no verb that
|
|
208
|
+
removes a path — `Write`/`Edit` can empty a file, nothing could unlink it or take it out of
|
|
209
|
+
the index — so every `git rm` and `git mv` it tried came back "This command requires
|
|
210
|
+
approval", which in a headless `-p` run is a prompt nobody is there to answer. Worse was what
|
|
211
|
+
the framework did with that: a refused turn exits 0 and returns an envelope, so the DoD ran
|
|
212
|
+
green on an untouched tree, the empty commit went to review, the reviewer faulted a diff that
|
|
213
|
+
was never written, and the SECOND attempt bought the same wall — the developer's own sentence
|
|
214
|
+
living only in `result.raw.json`, in no event, no story file and no handoff. Two halves, and
|
|
215
|
+
the second is the one that generalises. The allowance grows the file-lifecycle git verbs —
|
|
216
|
+
`Bash(git rm *)`, `Bash(git mv *)`, `Bash(git restore *)`, the space form, never a bare `rm`
|
|
217
|
+
— because they are index operations on the story's OWN tree, undone by exactly the
|
|
218
|
+
`git checkout` that undoes an `Edit`, on a branch that never leaves the machine (`git push`
|
|
219
|
+
stays asserted absent); the 2026-08-29 audit's line — no permission-free shell, only git verbs
|
|
220
|
+
— is the reason the three are git verbs and is unchanged. And a tool call refused for approval
|
|
221
|
+
now BLOCKS the story at once with `` permission — `<command>` `` and why, one string reaching
|
|
222
|
+
the story file, the handoff's `## Unknowns` and `gate.requested`'s `blocked_reason`, so the
|
|
223
|
+
next verb that is missing costs one attempt and says so instead of two and nothing. What the detector
|
|
224
|
+
reads is a structural field first — `tool_result_meta[].non_execution_kind: "user-rejected"`, measured
|
|
225
|
+
on `claude` 2.1.270, present on both refusals measured and absent on every command that ran, including
|
|
226
|
+
one the layer allowed and git itself failed — and the `requires approval` sentence only as a fallback,
|
|
227
|
+
and only on a `Bash` call whose result errored. That fence exists because pre-merge review measured the
|
|
228
|
+
unfenced version reading a plain `Read` of a file CONTAINING the phrase as a refusal: this CHANGELOG is
|
|
229
|
+
one of the files that contains it, and the block happens before the DoD and the commit, so a false
|
|
230
|
+
positive would discard real work and spend the attempt. A refusal carrying neither signal is a miss
|
|
231
|
+
taken on purpose, and the sentence half depends on prose the host writes and can change without notice. What the
|
|
232
|
+
new grant covers, measured against `claude` 2.1.270 with `Bash(git rm *)` as the only rule:
|
|
233
|
+
`git rm -r`, `git rm -rf` and `git rm -r -- .` all pass (a trailing `*` matches the whole
|
|
234
|
+
argument tail, flags included); a path outside the repo is refused by GIT, not by the rule;
|
|
235
|
+
`git -C <elsewhere> rm` is refused by the rule, since the command does not begin `git rm`.
|
|
236
|
+
The open question about the same grammar, **#215** — does an `allow` rule of the
|
|
237
|
+
`Bash(<cmd> *)` form reach a command substitution in its arguments? — was measured alongside
|
|
238
|
+
this, `claude` 2.1.270, with `git rm -n` as a non-destructive instrument: under
|
|
239
|
+
`Bash(git rm *)`, `git rm -n -- "$(echo MARKER.txt)"`, `git rm -n "$(echo MARKER.txt)"` and
|
|
240
|
+
`git rm -n -r "$(echo .)"` were **all denied** ("Contains shell syntax that cannot be
|
|
241
|
+
statically analyzed"), while a bare `git rm MARKER.txt` ran. **The nuance matters more than
|
|
242
|
+
the result: the layer refuses the SYNTAX of a substitution, not the action** — in the same
|
|
243
|
+
measurement the agent rewrote the command with the value already expanded, and then it ran.
|
|
244
|
+
For `git rm <path>` that expanded form is exactly what this grant hands out, so the direction
|
|
245
|
+
is the safe one; nobody should build a guarantee on "substitutions are blocked", because what
|
|
246
|
+
is blocked is a spelling. And all of it is HOST behaviour, in the agent CLI's permission
|
|
247
|
+
layer, which can change without a line of tldrx moving — which is why the argument that holds
|
|
248
|
+
this grant up is the tree-scope one and not the permission layer's manners.
|
|
249
|
+
- **`run.yml` can finally explain its own `cost_usd`, and a Build turn stopped losing its
|
|
250
|
+
accounting entirely (#222).** A field audit across three workspaces measured a row reading
|
|
251
|
+
`input_tokens: 84, output_tokens: 37150, cost_usd: 1.98` for a 124 KB prompt — while the
|
|
252
|
+
provider's own result for that same turn reported `cache_read_input_tokens: 4911750`. 84 plus
|
|
253
|
+
37,150 tokens do not cost $1.98; the 4.9 M cache reads do, and nothing on the row could say
|
|
254
|
+
so. Across 101 surviving transcripts in one workspace, cache reads were 97.3% of the input
|
|
255
|
+
side and the ledger's column saw 0.002% of it. The counters were parsed on every turn
|
|
256
|
+
(`spawnAgent.ts`) and thrown away one function later. They are now RECORDED: additively on the
|
|
257
|
+
task row, and — the half the issue did not know about — on the `agent.result` of the executor
|
|
258
|
+
path, which emitted **no `usage` at all**, so every Build and Watch turn lost all four
|
|
259
|
+
counters rather than only the two cache ones (measured: `grep -c usage` over the four frozen
|
|
260
|
+
golden `*-events.txt` returned 0, 0, 0, 0 with ten `agent.result` lines among them, and
|
|
261
|
+
`costView.ts`'s `toAttempt` therefore priced a whole Build off `{0,0,0,0}`). Nothing is
|
|
262
|
+
displayed that was not displayed before: this is a fix to the RECORD, and the dashboard and
|
|
263
|
+
`tldrx cost` are byte-identical. Two decisions worth naming. The cache counters are gated one
|
|
264
|
+
at a time rather than under `tokenSplit`'s both-or-nothing rule, because `input`+`output` are
|
|
265
|
+
two halves of one total that `turnTokens` adds and the cache counters are separate quantities
|
|
266
|
+
at separate prices (1.25x and 0.1x an input token) that nothing adds — and the spec's own
|
|
267
|
+
measured pair of calls is the proof that a paired rule would be wrong: the second reported
|
|
268
|
+
`cache_creation: 0` beside 37,059 cache reads, the single most useful number on the row. And
|
|
269
|
+
`turnTokens` names them in its input type while deliberately reading neither, so the exclusion
|
|
270
|
+
is a decision on the record rather than a field nobody has noticed yet: it returns one
|
|
271
|
+
unlabelled `number`, and folding 4.9 M cache reads into an `input_tokens: 84` would make that
|
|
272
|
+
number a total in no currency at all. `ExecutorTask` now carries the provider's `usage` whole
|
|
273
|
+
instead of a hand-copied two-field subset, which is what let the cache counters be dropped at
|
|
274
|
+
that seam in the first place, and the event block has one spelling for all three emitters
|
|
275
|
+
(`envelope.ts`'s `usagePayload`). A row from before this exists reads absent, as it should:
|
|
276
|
+
nothing reconstructs a counter nobody wrote down.
|
|
277
|
+
|
|
278
|
+
- **A gate about to sign itself is no longer sent as a Yes/No the owner cannot answer (#247).**
|
|
279
|
+
#203 holds back the `gate.requested` notification while open questions are the only thing
|
|
280
|
+
holding an `auto` gate, and released it when the answers landed if the gate was still
|
|
281
|
+
`pending`. Under `--wait-gates` that test is true by construction: the only thing that
|
|
282
|
+
self-closes an auto gate mid-wait runs one iteration later, inside the gate wait, after the
|
|
283
|
+
loop has already decided. Measured 2026-09-12 on two live workspaces, 3 of 3 questioned
|
|
284
|
+
stages: the Yes/No went out 600 ms after the last answer and the gate signed itself on the
|
|
285
|
+
very next poll — so the one tap the message invited ran `approve` on an already-approved gate,
|
|
286
|
+
and because the in-wait `gate.approved`/`stage.done` fall between two notify windows nothing
|
|
287
|
+
ever arrived to settle it: the bridge kept the prompt open and re-mentioned its owner every
|
|
288
|
+
escalate tick for two hours. *"esa pregunta está de más, es confusa."* The release now asks
|
|
289
|
+
the gate's CONDITIONS rather than its status — the same `reevaluateAutoGate` the poll uses,
|
|
290
|
+
extracted so the poll and the release read one measurement and neither can form a second
|
|
291
|
+
opinion (§7) — together with the policy that will act on them. Every condition holding and a
|
|
292
|
+
`--wait-gates` to sign it means there is no decision to take: nothing is sent, and the run
|
|
293
|
+
says which stage and why on stdout rather than going quiet. A gate something else still holds
|
|
294
|
+
is notified exactly as #203 promised, and now worded from that one reading: the re-measured
|
|
295
|
+
verdict supplies the summary's `held by:` sentence and the open questions are read in the same
|
|
296
|
+
breath, so a payload can no longer name four open questions beside `holding: "none"` — which
|
|
297
|
+
is what the adapter was handed, measured, and why it drew the default two buttons. #239 fixed
|
|
298
|
+
WHICH command a questions-held gate offers; this is WHEN, and whether, it is offered at all.
|
|
299
|
+
- **A turn that wrote a lot of files could not be RECORDED, and the invocation died owing the
|
|
300
|
+
money it had just spent (#248).** Measured on a live headless run: `tldrx run auto … --retry-failed 2`,
|
|
301
|
+
eight stories, dead at *"refusing to append an invalid event: payload 4135 bytes exceeds the
|
|
302
|
+
4096 byte cap"*. The field was `outputs` — every run-relative path the turn wrote, 3924 bytes of
|
|
303
|
+
it under a 212-byte envelope — on the `agent.result` that `recordExecutorTasks` appends. What
|
|
304
|
+
that one refused append cost is the reason this is three fixes and not one. The append was
|
|
305
|
+
RAW, so it never reached the capped seam; the seam would not have saved it anyway, because
|
|
306
|
+
`capPayload` knew how to name exactly one absence and it was `detail`, which an `agent.result`
|
|
307
|
+
does not have. The throw landed one line AFTER the try/catch #160 built for precisely this, so
|
|
308
|
+
it escaped `runNext` entirely and came out of the CLI as exit 1 — a code `--retry-failed` does
|
|
309
|
+
not retry, since its branch keys on exit 5. A loop told to survive two failures survived zero.
|
|
310
|
+
And `claimEpicBranches` had put the run's epic branch in memory one line before the throw, with
|
|
311
|
+
the `store.save()` after it: the branch was cut, on disk, and `run.yml` did not say so, so the
|
|
312
|
+
relaunch read its OWN epic as a stranger's and refused to stack onto it — with the only way
|
|
313
|
+
back in being `tldrx next --reuse-epic`, which `run auto` does not expose. So: `capPayload`
|
|
314
|
+
learns `outputs`, and names it by COUNT (`outputs_omitted: 80`) with the full list written
|
|
315
|
+
beside the event at `<phase>/log/overflow/…-agent.result-outputs.txt` — never a truncated list,
|
|
316
|
+
which reads downstream as the whole one. The capped seam moves out of `runExecutor` to module
|
|
317
|
+
scope and all four raw `agent.result` appends route through it, not just the one that was hit:
|
|
318
|
+
all four carry `outputs` (measured), and the other three had simply not been reached yet. A
|
|
319
|
+
throw while recording rows now fails the STAGE with exit 5, in the same family as the executor's
|
|
320
|
+
own throw, and says how many of how many rows reached `run.yml` rather than a boolean that
|
|
321
|
+
would be false for a partial truth. It repaints no row: those turns finished, and only their
|
|
322
|
+
event failed to land. And a claim is saved the moment it is earned. `capPayload` still knows
|
|
323
|
+
its two fields BY NAME and has no general rule for shrinking whatever is biggest — a third
|
|
324
|
+
growable payload field has to be taught to it, deliberately, or it is refused whole.
|
|
325
|
+
|
|
3
326
|
## 0.17.0 — 2026-09-12
|
|
4
327
|
|
|
5
328
|
### Added
|
package/README.md
CHANGED
|
@@ -316,6 +316,7 @@ back on the registry is 0.3.0.
|
|
|
316
316
|
|
|
317
317
|
| Version | Date | Status | Contains |
|
|
318
318
|
|---|---|---|---|
|
|
319
|
+
| 0.18.0 | 2026-09-13 | `beta` | the release that lets an unattended run survive its own accidents and end somewhere, every item measured on live runs at 0.16.1 rather than reasoned about: a turn that wrote a lot of files could not be RECORDED — an `agent.result` whose `outputs` field held 3924 bytes of run-relative paths blew the 4096-byte event cap through a RAW append that never reached the capped seam, the throw landed one line after the try/catch built for exactly this and came out as exit 1, which `--retry-failed` does not retry since its branch keys on exit 5, so a loop told to survive two failures survived zero, and the epic branch had been cut on disk one line before the throw with the save after it, leaving the relaunch to read its own epic as a stranger's; `outputs` is now named by COUNT with the full list written beside the event rather than truncated, all four raw appends route through the capped seam at module scope, a throw while recording fails the STAGE with exit 5 saying how many of how many rows landed, the claim is saved the moment it is earned, and `capPayload` still knows its two fields BY NAME — a third growable field has to be taught to it deliberately or it is refused whole; Build entry now proves the Definition of Done can run IN A WORKTREE before a developer is paid, after 18 h from `run auto` to the first story that could run on one workspace, five relaunches, because the repo's `install:` named a file that existed in the human's checkout and was never committed and `git worktree` carries TRACKED FILES ONLY, so every story failed identically after the story was opened and immediately before the paid turn while the base pre-flight was green and right to be, measuring in the checkout where the file is — one throwaway worktree at the base sha, the install run inside it, each DoD command's first token resolved there, NEVER the suite, refusal exit 2 naming the exact path or binary, cached under an additive `worktree:` key narrowed by base sha, declaration hash and AGE (six hours for a green, because the row is a claim about an ENVIRONMENT that no sha contains; the base red's 30 minutes for a red), and costing zero where it could learn nothing; where the two readings could not be told apart it deliberately refuses LESS — a path named by a `dod` command after the first may be an artefact an earlier one builds, so that is an advisory and not a verdict; one blocked story no longer stops every later wave, after a loop told to build 8 stories built 5, never started 3, and reported the stage `done`, twice on the same run, because the parallel path asked only "did any story block" and `depends_on` appeared ZERO times in the Build executor — a story now runs when every one of its `depends_on` is `done`, one held back is `blocked` WITH the reason `dependency S7 blocked` (transitive for free) which is what the gate's `blocked_reason` and the continue note read, the stage line counts every story `waves.yml` scheduled rather than the rows the executor happened to hold, and a scheduled story with no outcome at all is named in `## Unknowns` instead of dropped, while `--parallel 1` is untouched on purpose — a story whose dependency blocked is still attempted there, and changing that would be a second change in the opposite direction; the spawned developer can now delete and rename paths in its own tree, after a story that was simply "delete an unused file" proved undoable — nothing in the allowance could unlink a path or take it out of the index, so every `git rm` came back needing approval nobody was there to give, the refused turn exited 0 with an envelope, the DoD ran green on an untouched tree and the reviewer faulted a diff that was never written, twice — the file-lifecycle git verbs are granted as index operations on the story's OWN tree (never a bare `rm`, `git push` still asserted absent), and a tool call refused for approval now BLOCKS the story at once with the command and why, reaching the story file, the handoff and the gate, so the next missing verb costs one attempt instead of two and nothing; that detector reads a structural field first and the English refusal sentence only as a fenced fallback, because pre-merge review measured the unfenced version reading a plain file-read of a document CONTAINING the phrase as a refusal — this very CHANGELOG is one of them — and a refusal carrying neither signal is a miss taken on purpose, with the sentence half depending on prose the host can change without notice; a fix-list finding now declares what it IS, after three stories with a green definition of done, a merged commit and an APPROVING reviewer settled `blocked`, each on a `fix-now` finding whose whole content was a stale docstring or a citation that did not resolve — the reviewer was not wrong and `severity` was never the vehicle (a free string NO consumer read, one render site under `src`), so a required `kind` splits behaviour from text: `correctness` and `security` hold the story exactly as before, a `docs`/`style` finding submitted `fix-now` is routed to `defer-with-log` with a `Normalised-from:` line, and because this makes a gate WEAKER on purpose the unblocking is priced — declaring a finding `docs` costs the SAME `[src: …]` citation that refuting one already costs, a `kind` that is absent or outside the enum is refused for free and indexed as a fault so it costs the story no attempt, a fix list already on disk with no `Kind:` line reads as not stated and not stated blocks, and the four reviewer-prompt goldens MOVED because the schema alone would have left a reviewer nobody told about the field; `run.yml` can finally explain its own `cost_usd` — a row read `input_tokens: 84, output_tokens: 37150, cost_usd: 1.98` for a 124 KB prompt while the provider reported 4,911,750 cache reads for that same turn, and across 101 transcripts in one workspace cache reads were 97.3% of the input side against a column that saw 0.002% of it — the counters were parsed on every turn and thrown away one function later, and the half the issue did not know about is that the executor path's `agent.result` emitted NO `usage` at all, so every Build and Watch turn lost all four counters and the whole Build was priced off zeroes (measured: zero `usage` lines across the four frozen goldens with ten `agent.result` among them); they are recorded additively and gated one at a time rather than both-or-nothing, `turnTokens` names them while deliberately reading neither so the exclusion is a decision on the record, nothing displayed changed — the dashboard and `tldrx cost` are byte-identical — and a row from before this reads absent, since nothing reconstructs a counter nobody wrote down; a gate about to sign itself is no longer sent as a Yes/No the owner cannot answer, after 3 of 3 questioned stages on two live workspaces sent the prompt 600 ms after the last answer and self-signed on the very next poll, so the one tap it invited ran `approve` on an already-approved gate and, because the in-wait approval fell between two notify windows, the bridge kept the prompt open and re-mentioned its owner every escalate tick for two hours — the release now asks the gate's CONDITIONS through the same re-evaluation the poll uses, extracted so neither can form a second opinion, sends nothing when there is no decision to take and says which stage and why on stdout instead of going quiet, and words a genuinely held gate from that one reading so a payload can no longer name four open questions beside `holding: "none"`; and three things a run could not do before: it can end in a pull request toward main on purpose, frozen at `run new --ship` with `push`, `pr` or `merge` into an additive `run.yml` block and not a sixth phase, pushed through the ONE push wrapper in the codebase with a PR that reports NO check left open and recorded as `merge: absent — no checks to wait on` because auto-merge over nothing to wait on is a merge now rather than a merge when green — pre-merge review caught the first version erasing a recorded merge failure with an empty string and exit 0, and the emitter dropped the whole block until it was taught the key; `questions_policy: recommended` answers a blocking question that carries its own `Recommended:` line and escalates the ones that do not, after 10 owner questions across two headless runs ($89.82 over 28 h and $26.38 over 5.6 h, the agents busy 4.9 h and 56 min of it) parked the loop on 9 that already held the pick in a parsed field, writing `decided_by: agent-default` as a THIRD value beside owner and driver because `owner` there would be an audit record lying in the dangerous direction, and never inventing a pick — no line, a letter naming no option, or a block marked irreversible or money escalates exactly as before; and `run auto --until-done [<n>]` relaunches the loop in-process after an exit it can do nothing else with, at most `n` times (default 5), after five hand relaunches and 18 h before a story ran — never over a person's exit 4, never over a budget block since nothing in-process moves a ceiling, never twice over the same last line, with the run id and the spend baseline pinned across relaunches so `--max-usd` still describes the one command a person typed; the epic-claim-at-cut half of that issue lives in the Build executor and is NOT fixed here |
|
|
319
320
|
| 0.17.0 | 2026-09-12 | `beta` | three things the framework knew and recorded wrongly, each found by reading its own records rather than by using it: a gate notification now carries the condition HOLDING the gate as data instead of leaving it to be guessed from the text of the command it suggests — `holding` says questions, stories or nothing-mechanical, and when a story is blocked with a recorded reason the payload also hands over a ready-made rejection that lets the loop carry on, with the note DERIVED from the blocked story rather than canned, because a rejection's note is fed to the next turn's prompt and a generic one would hand that turn an empty instruction, so when no reason can be derived the offer is absent rather than invented; the emitted command keeps a literal placeholder and never interpolates the reason, which keeps the quoting hazard out of the record and puts it where a substituting client can see it; `run status` stopped printing more money left than it had ceiling — the run ceiling now has ONE live copy, read from `budget.yml` by every live screen, and `run.yml`'s mirror is documented as the creation value and no longer written by a raise, since the break was never that a raise failed to write the mirror but that an ordinary concurrent save carried a stale copy over it, and the fix was to delete the half-sync rather than to build a better one — pre-merge review caught the first version pairing a LIVE spend with that now-frozen ceiling inside `tldrx replay`, which would have reproduced the same impossible line in a different command on every run whose budget had been raised, measured on the real CLI as `$12.00 spent of $10.00 ceiling` with no concurrency required; and a task row now records the ROLE its turn actually ran under, where every Build turn had been filed as the developer including the reviewer's — the role was known at spawn and written to the event stream, and was dropped on the way to the ledger, so the audit record named the wrong actor for work it had itself measured; the new key is additive and written on EVERY row including the developer's, because a role present only on reviewers would make its absence mean developer-or-not-recorded and send a reader back to inferring the role from an absence, which is the guess the change exists to remove |
|
|
320
321
|
| 0.16.1 | 2026-09-12 | `beta` | five things the framework knew and did not say, or said wrong — four of them found by using it rather than by reading it: a red base pre-flight now KEEPS its output, so a refusal that blocks every story in a Build names the failing test and cites the file, where it used to record only the last line of stdout — measured 2026-09-10, a stage refused with `tail: "Test run completed with non-success exit code: 2"` while the cause, `DockerUnavailableException`, sat on line 12 of 163,702 lines the run had already captured and thrown away, so diagnosing a refusal the framework had itself measured meant re-running the workspace's test command by hand; it now routes through the same seam #211 built for a story's DoD, which had been naming its failing test correctly all along on the same command, the same day, in the same repo — the path with the SMALLER blast radius was the legible one; a gate notification now offers the command that CLEARS it rather than always `tldrx approve` — questions open give `tldrx answer <id>`, unfinished stories give `tldrx run status`, and `approve` is offered only when nothing mechanical is outstanding, after an owner approved a Build gate by mistake twice in one evening over unbuilt stories, each time from a phone, each time needing a revoke, while a ten-minute heartbeat repeated `Run: tldrx approve` seven times under a sentence that correctly named the five open questions holding it; `tldrx reject --and-continue` lets a rejection mean "redo it this way and carry on" instead of ending the run — the loop resumed after an approve and stopped after a reject, so the button meaning "there is still work to do" was the one that stopped the work and only a terminal could revive it; five real rejections that night all meant continue, five cost a manual relaunch, and a bare `tldrx reject` still writes a byte-identical `run.yml` and stops exactly as before; the expert-recompute fixture anchors its evidence dates to a `now` it can move, so `bun test` stops going red by the calendar — pristine `main` was red at the exact sha of the published 0.16.0 with no commit in between, and a clock moved one year forward reddened FIVE cases, not the one that had already fired; and `test/merge-wave.test.ts`'s concurrency failure, documented as a known flake since #115 and carrying a written licence to re-run it, was never one: `merge-guard.sh` rewrote `.git/hooks/reference-transaction` IN PLACE while a sibling wave's `git merge` was exec'ing it — ETXTBSY on Linux at 31% under contention, benign on macOS, which is why it was green locally and red in CI, and why a same-sha re-run failed 2 for 2 rather than passing; the hook is now written to a temp file and RENAMED into place, the refusal that used to borrow `2`/`merge conflict` for a hook abort now says what it was and exits 11, and AGENTS.md §4 withdraws the re-run licence for those two cases while naming the interrupted-merge case (#237) as still open and undiagnosed — because "all real" for a whole file costs the same as "all flake", in the other direction |
|
|
321
322
|
| 0.16.0 | 2026-09-11 | `beta` | an unattended run can now clear the one kind of failure it was stopping on, and a gate that refuses says why it refused: measured 2026-09-10 on a real unattended `run auto`, the loop drove itself through what → how → plan and signed all three `auto` gates by itself, and still needed a person four times — three of those were content or money decisions a loop must not make, and the fourth was a plan that failed its own check by five characters over a cap, where a person relaunched the same command and the next attempt fixed the two files and passed, so the loop stopped on the one failure it could have cleared; `tldrx run auto --retry-failed <n>` now runs a failed stage again at most `n` times in a row, bounding exit `5` and nothing else — a usage error (`1`), a money refusal (`2`) and an awaiting-human park (`4`) are each attempted ONCE however large `n` is, because a phase ceiling means a human decides about money and a retry would turn that sentence into a delay — only CONSECUTIVE failures count since what is bounded is "this run is stuck" and not "this run has ever failed", a retry SPENDS as a fresh metered stage under the same phase ceiling and the same `--max-usd`, `0` is the default and a default invocation's lines are byte-identical to what they were, and when the bound is spent the loop stops on the failure's own exit `5` and says the count LAST, so the sentence that reaches a phone is what the loop tried and not a bare number; and an `auto` gate that REFUSES now writes down the verdict its note was always designed to carry — a gate sat pending ~40 minutes while `run status` and `--verbose` named no condition at all, and the reason surfaced only when a person guessed at the `tldrx approve` the status line suggested, which is the one route nobody unattended is going to take — recording all seven conditions WITH THEIR VALUES on the still-`pending` gate, since a note that dropped the passing ones would answer "was it the money" with the same silence, and naming the holding ids on the gate row and on the `waiting` line; it writes only over a `pending` gate, so a gate a person has since signed keeps THEIR words, and only when the verdict would change, so a four-hour `--wait-gates` poll writes once per distinct verdict rather than thousands of times — and that test and that write are a compare-and-set under the workspace lock, because pre-merge review reproduced, with two real processes, a check-then-act over an earlier snapshot erasing a concurrent `approve` outright, and the poll runs every two seconds precisely while a person is deciding |
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
conflictOf
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-7hhjx1wk.js";
|
|
5
5
|
import {
|
|
6
6
|
FactsStore,
|
|
7
7
|
formatJaccard
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-26xkmfdr.js";
|
|
9
9
|
import {
|
|
10
10
|
parseHookInput,
|
|
11
11
|
readStdin
|
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
EventLog,
|
|
15
15
|
PHASE_ID_RE
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-00h4xah7.js";
|
|
17
17
|
import {
|
|
18
18
|
PHASE_IDS
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
import"./chunk-
|
|
21
|
-
import"./chunk-
|
|
19
|
+
} from "./chunk-bm9aqtg3.js";
|
|
20
|
+
import"./chunk-xxcy0tsa.js";
|
|
21
|
+
import"./chunk-ab6cv5v5.js";
|
|
22
22
|
import {
|
|
23
23
|
ADVISORY_KEY,
|
|
24
24
|
MAX_FACT_CHARS,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
renderQuestionBlock,
|
|
29
29
|
replaceBlock,
|
|
30
30
|
serializeQuestions
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-p0ns6fex.js";
|
|
32
32
|
import {
|
|
33
33
|
ITERATION_ONLY_SLOT,
|
|
34
34
|
PROJECT_FRAMEWORK_DIR,
|
|
@@ -407,7 +407,11 @@ function answerProvenance(block, overrides, repoNames, fallbackRepos) {
|
|
|
407
407
|
return {
|
|
408
408
|
repos,
|
|
409
409
|
unresolved: named.unresolved,
|
|
410
|
-
source: override?.decidedBy === undefined ? {} : { decided_by: override.decidedBy }
|
|
410
|
+
source: override?.decidedBy === undefined ? {} : { decided_by: override.decidedBy },
|
|
411
|
+
row: {
|
|
412
|
+
...override?.alternatives === undefined || override.alternatives.length === 0 ? {} : { alternatives: override.alternatives },
|
|
413
|
+
...override?.recommendedWhy === undefined ? {} : { recommended_why: override.recommendedWhy }
|
|
414
|
+
}
|
|
411
415
|
};
|
|
412
416
|
}
|
|
413
417
|
function unresolvedEntries(captured) {
|
|
@@ -445,6 +449,7 @@ function captureAnswers(questionsPath, ctx) {
|
|
|
445
449
|
fact: text,
|
|
446
450
|
...truncated ? { truncated: true } : {},
|
|
447
451
|
...clash === null ? {} : { conflicts_with: [clash.fact.id] },
|
|
452
|
+
...prov.row,
|
|
448
453
|
area,
|
|
449
454
|
repos: prov.repos,
|
|
450
455
|
kind: "answer",
|
|
@@ -22,10 +22,10 @@ import {
|
|
|
22
22
|
validateRunBudget,
|
|
23
23
|
wouldExceed,
|
|
24
24
|
wouldExceedHostTokens
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-rw9zk47w.js";
|
|
26
26
|
import {
|
|
27
27
|
EventLog
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-00h4xah7.js";
|
|
29
29
|
import {
|
|
30
30
|
cursorStage,
|
|
31
31
|
hostTokensIn,
|
|
@@ -35,10 +35,10 @@ import {
|
|
|
35
35
|
renderRunEconomies,
|
|
36
36
|
runSpend
|
|
37
37
|
} from "./chunk-4mjxyfp9.js";
|
|
38
|
-
import"./chunk-
|
|
38
|
+
import"./chunk-xxcy0tsa.js";
|
|
39
39
|
import {
|
|
40
40
|
noteDeprecations
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-p0ns6fex.js";
|
|
42
42
|
import {
|
|
43
43
|
PROJECT_WORK_DIR,
|
|
44
44
|
findWorkspaceRoot,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GATE_POLICIES,
|
|
3
|
-
validateGatesPolicy
|
|
4
|
-
|
|
3
|
+
validateGatesPolicy,
|
|
4
|
+
validateStagePolicy
|
|
5
|
+
} from "./chunk-xxcy0tsa.js";
|
|
5
6
|
import {
|
|
6
7
|
asDocument,
|
|
7
8
|
isRecord,
|
|
@@ -27,6 +28,7 @@ var EVENT_TYPES = [
|
|
|
27
28
|
"run.unlocked",
|
|
28
29
|
"run.cancelled",
|
|
29
30
|
"run.attended",
|
|
31
|
+
"run.relaunched",
|
|
30
32
|
"phase.started",
|
|
31
33
|
"phase.done",
|
|
32
34
|
"stage.started",
|
|
@@ -44,6 +46,7 @@ var EVENT_TYPES = [
|
|
|
44
46
|
"gate.rejected",
|
|
45
47
|
"gate.revoked",
|
|
46
48
|
"gate.policy_changed",
|
|
49
|
+
"questions.policy_changed",
|
|
47
50
|
"story.reopened",
|
|
48
51
|
"story.base_fastforwarded",
|
|
49
52
|
"story.review_retried",
|
|
@@ -222,6 +225,12 @@ function skippedNote(skipped) {
|
|
|
222
225
|
}
|
|
223
226
|
var WARNED = new Set;
|
|
224
227
|
|
|
228
|
+
// src/core/run/questionsPolicy.ts
|
|
229
|
+
var QUESTION_POLICIES = ["human", "recommended"];
|
|
230
|
+
function validateQuestionsPolicy(value, stageIds, issues) {
|
|
231
|
+
validateStagePolicy(value, stageIds, issues, { field: "questions_policy", policies: QUESTION_POLICIES });
|
|
232
|
+
}
|
|
233
|
+
|
|
225
234
|
// src/core/plan/branchModel.ts
|
|
226
235
|
var BRANCH_MODELS = ["per-epic", "integration"];
|
|
227
236
|
function isBranchModelKind(value) {
|
|
@@ -429,6 +438,9 @@ function describe(value) {
|
|
|
429
438
|
// src/core/run/duration.ts
|
|
430
439
|
var DURATION_BASES = ["spawned", "prepare-to-commit"];
|
|
431
440
|
|
|
441
|
+
// src/core/run/shipPolicy.ts
|
|
442
|
+
var AUTO_MERGE_POLICIES = ["never", "checks"];
|
|
443
|
+
|
|
432
444
|
// src/core/run/RunFile.ts
|
|
433
445
|
var STAGE_STATUSES = [
|
|
434
446
|
"pending",
|
|
@@ -599,6 +611,32 @@ function validateRunFile(input) {
|
|
|
599
611
|
if (doc[key] !== undefined)
|
|
600
612
|
requireString(doc[key], key, issues);
|
|
601
613
|
}
|
|
614
|
+
if (doc.ship !== undefined) {
|
|
615
|
+
if (isRecord(doc.ship)) {
|
|
616
|
+
requireKeys(doc.ship, ["push", "pr", "auto_merge"], "ship", issues);
|
|
617
|
+
for (const key of ["push", "pr"]) {
|
|
618
|
+
if (doc.ship[key] !== undefined && typeof doc.ship[key] !== "boolean") {
|
|
619
|
+
issues.push({ path: `ship.${key}`, message: `expected a boolean, got ${typeof doc.ship[key]}` });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
requireEnum(doc.ship.auto_merge, AUTO_MERGE_POLICIES, "ship.auto_merge", issues);
|
|
623
|
+
if (requireArray(doc.ship.pr_urls, "ship.pr_urls", issues)) {
|
|
624
|
+
doc.ship.pr_urls.forEach((url, i) => requireString(url, `ship.pr_urls[${i}]`, issues));
|
|
625
|
+
}
|
|
626
|
+
requireString(doc.ship.merge, "ship.merge", issues);
|
|
627
|
+
if (doc.ship.merges !== undefined) {
|
|
628
|
+
if (isRecord(doc.ship.merges)) {
|
|
629
|
+
for (const [name, state] of Object.entries(doc.ship.merges))
|
|
630
|
+
requireString(state, `ship.merges.${name}`, issues);
|
|
631
|
+
} else {
|
|
632
|
+
issues.push({ path: "ship.merges", message: "expected a mapping of repo name to merge state" });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
requireString(doc.ship.shipped_at, "ship.shipped_at", issues);
|
|
636
|
+
} else {
|
|
637
|
+
issues.push({ path: "ship", message: "expected a mapping" });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
602
640
|
if (doc.keep_worktrees !== undefined && typeof doc.keep_worktrees !== "boolean") {
|
|
603
641
|
issues.push({ path: "keep_worktrees", message: `expected a boolean, got ${typeof doc.keep_worktrees}` });
|
|
604
642
|
}
|
|
@@ -618,6 +656,7 @@ function validateRunFile(input) {
|
|
|
618
656
|
}
|
|
619
657
|
}
|
|
620
658
|
validateGatesPolicy(doc.gates_policy, declaredStageIds, issues);
|
|
659
|
+
validateQuestionsPolicy(doc.questions_policy, declaredStageIds, issues);
|
|
621
660
|
let stageCount = 0;
|
|
622
661
|
let taskCount = 0;
|
|
623
662
|
let running = 0;
|
|
@@ -715,7 +754,12 @@ function validateRunFile(input) {
|
|
|
715
754
|
if (task.dedupe !== undefined && typeof task.dedupe !== "string") {
|
|
716
755
|
issues.push({ path: `${tp}.dedupe`, message: "expected a string" });
|
|
717
756
|
}
|
|
718
|
-
for (const key of [
|
|
757
|
+
for (const key of [
|
|
758
|
+
"input_tokens",
|
|
759
|
+
"output_tokens",
|
|
760
|
+
"cache_creation_input_tokens",
|
|
761
|
+
"cache_read_input_tokens"
|
|
762
|
+
]) {
|
|
719
763
|
const value = task[key];
|
|
720
764
|
if (value === undefined)
|
|
721
765
|
continue;
|
|
@@ -3,9 +3,10 @@ import {
|
|
|
3
3
|
withWorkspaceLock,
|
|
4
4
|
workspaceRootOfFactsPath,
|
|
5
5
|
writeAtomic
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ab6cv5v5.js";
|
|
7
7
|
import {
|
|
8
8
|
FACT_CONFIDENCES,
|
|
9
|
+
FACT_DECIDERS,
|
|
9
10
|
FACT_KINDS,
|
|
10
11
|
MAX_FACTS,
|
|
11
12
|
MAX_FACT_CHARS,
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
isLive,
|
|
15
16
|
isRetired,
|
|
16
17
|
noteDeprecations
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-p0ns6fex.js";
|
|
18
19
|
import {
|
|
19
20
|
SRC_PATTERNS,
|
|
20
21
|
asDocument,
|
|
@@ -85,10 +86,10 @@ function validateFactsFile(input) {
|
|
|
85
86
|
issues.push({ path: `${path}.source.q`, message: `expected ${readableSource(Q_RE)} or null` });
|
|
86
87
|
}
|
|
87
88
|
const decidedBy = row.source.decided_by;
|
|
88
|
-
if (decidedBy !== undefined && decidedBy
|
|
89
|
+
if (decidedBy !== undefined && !FACT_DECIDERS.includes(decidedBy)) {
|
|
89
90
|
issues.push({
|
|
90
91
|
path: `${path}.source.decided_by`,
|
|
91
|
-
message:
|
|
92
|
+
message: `expected ${FACT_DECIDERS.join(", ")} or absent`
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
95
|
} else if (row.source !== undefined) {
|
|
@@ -109,6 +110,21 @@ function validateFactsFile(input) {
|
|
|
109
110
|
links.forEach((id2, i2) => requireString(id2, `${path}.conflicts_with[${i2}]`, issues));
|
|
110
111
|
}
|
|
111
112
|
}
|
|
113
|
+
if (row.alternatives !== undefined) {
|
|
114
|
+
if (requireArray(row.alternatives, `${path}.alternatives`, issues)) {
|
|
115
|
+
const options = row.alternatives;
|
|
116
|
+
if (options.length === 0) {
|
|
117
|
+
issues.push({
|
|
118
|
+
path: `${path}.alternatives`,
|
|
119
|
+
message: "expected at least one option not taken, or the key absent"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
options.forEach((option, i2) => requireString(option, `${path}.alternatives[${i2}]`, issues));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (row.recommended_why !== undefined) {
|
|
126
|
+
requireString(row.recommended_why, `${path}.recommended_why`, issues);
|
|
127
|
+
}
|
|
112
128
|
const superseded = typeof row.superseded_by === "string";
|
|
113
129
|
const retired = isRecord(row.retired) && row.retired.at !== null && row.retired.at !== undefined;
|
|
114
130
|
if (superseded && retired) {
|
|
@@ -245,7 +261,9 @@ class FactsStore {
|
|
|
245
261
|
superseded_by: null,
|
|
246
262
|
retired: input.retired ?? null,
|
|
247
263
|
...cut || input.truncated === true ? { truncated: true } : {},
|
|
248
|
-
...input.conflicts_with !== undefined && input.conflicts_with.length > 0 ? { conflicts_with: [...input.conflicts_with] } : {}
|
|
264
|
+
...input.conflicts_with !== undefined && input.conflicts_with.length > 0 ? { conflicts_with: [...input.conflicts_with] } : {},
|
|
265
|
+
...input.alternatives !== undefined && input.alternatives.length > 0 ? { alternatives: [...input.alternatives] } : {},
|
|
266
|
+
...input.recommended_why === undefined ? {} : { recommended_why: input.recommended_why }
|
|
249
267
|
};
|
|
250
268
|
this.rows.push(fact);
|
|
251
269
|
return fact;
|