tldr-experts 0.16.0 → 0.17.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 +200 -0
- package/README.md +2 -0
- package/dist/hooks/answer-capture.js +2 -2
- package/dist/hooks/budget-gate.js +1 -1
- package/dist/hooks/{chunk-vvr4rk82.js → chunk-g8kkq85r.js} +6 -4
- package/dist/hooks/{chunk-rz0qr006.js → chunk-jp6jscsd.js} +3 -0
- package/dist/hooks/{chunk-z0hnthw4.js → chunk-tj66vg1n.js} +1 -1
- package/dist/hooks/session-start.js +3 -3
- package/dist/hooks/statusline.js +2 -2
- package/dist/tldrx.js +137 -29
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,205 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0 — 2026-09-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **A `gate.requested` payload says what is HOLDING the gate, and hands over the rejection that
|
|
8
|
+
carries on (#243).** The owner's phone renders a gate as two buttons, Yes and No, and the No
|
|
9
|
+
branch rejects nothing — it logs *"gate stays open"*. That is not the adapter being lazy: it is
|
|
10
|
+
the adapter working with what it was handed. Of the three things #239 taught the framework to
|
|
11
|
+
distinguish, exactly one travelled as data — `detail.stories` says there is unbuilt work —
|
|
12
|
+
while "there are open questions" travelled in nothing but the PREFIX of `command`
|
|
13
|
+
(`tldrx answer …`). Routing on that is a second, untested, out-of-repo copy of the mapping,
|
|
14
|
+
and it goes quietly wrong the day a command is reworded. So the branch `clearingCommand`
|
|
15
|
+
already takes is now NAMED: `detail.holding` is `questions`, `stories` or `none`, and
|
|
16
|
+
`clearingCommand` switches on it, so the field and the command are two renderings of one
|
|
17
|
+
derivation and cannot disagree (§7). And the payload now spells the other half of what a third
|
|
18
|
+
button needs: `detail.continue_command` is `tldrx reject --run <id> --and-continue --note "…"`
|
|
19
|
+
— #242's verb, with the substitutable `…` `answer_command` established, rather than
|
|
20
|
+
`reject_command`'s `<why>`, which is prose for a human and would have been sent literally as
|
|
21
|
+
the next turn's prompt. Its note is DERIVED, never canned: `continue_note` is the blocked
|
|
22
|
+
story and the handoff's own reason for it, the same two facts already on the payload as
|
|
23
|
+
`blocked_story` and `blocked_reason`. Both keys are absent together wherever the gate cannot
|
|
24
|
+
name what has to change — held by open questions (the gate is downstream of them, and a
|
|
25
|
+
one-tap refusal there is the mirror of the mistake #239 was filed over), held by nothing
|
|
26
|
+
mechanical (the reason to refuse a judgement is in a person's head), and held by stories none
|
|
27
|
+
of which is blocked or whose blocked one recorded no reason. `--note` is mechanically required
|
|
28
|
+
and becomes the next turn's prompt, so a canned *"rejected from Slack"* would satisfy the flag
|
|
29
|
+
and empty the rule it exists for: a button that hands the re-run an empty instruction is worse
|
|
30
|
+
than no button. Every key is additive — a consumer that reads none of them gets exactly the
|
|
31
|
+
payload it got before. Which buttons an adapter draws, and how, stays the adapter's.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **`run status` could print more money left than it had ceiling, and the fix was to delete a
|
|
36
|
+
copy rather than synchronise one (#236).** Measured live on a hosted run after four raises:
|
|
37
|
+
`budget $0.00 spent of $190.00 ceiling ($200.00 left)` — a sentence that cannot be true. The
|
|
38
|
+
two figures came from two files. The remainder has always been `budget.yml`'s; the ceiling was
|
|
39
|
+
`run.yml`'s `budget.ceiling_usd`, and the issue's own diagnosis — that `budget raise` never
|
|
40
|
+
wrote that key — was WRONG: it wrote it, on the line after it wrote `budget.yml`, and had since
|
|
41
|
+
the command's first commit. What defeated it was concurrency. `RunStore.save()` re-reads
|
|
42
|
+
budget.yml's ceilings from disk before every write, precisely because a store that loaded the
|
|
43
|
+
file an hour ago must not clobber a raise (`ceilingsToWrite`); run.yml's mirror had no such
|
|
44
|
+
re-read, so `rollUp` carried the pre-raise value straight back out. A long-lived `run auto`
|
|
45
|
+
saving after an operator's raise reverted the mirror, and the ceiling on screen fell below the
|
|
46
|
+
remainder beside it. Nothing decided against the stale figure — every refusal, gate and brake
|
|
47
|
+
already reads budget.yml (measured: all 47 `ceiling_usd` references read; the mirror's four
|
|
48
|
+
readers are all display), so this was a lie on a screen and not money spent wrongly, which is
|
|
49
|
+
the part the issue left open. The fix follows §7 rather than the obvious repair: two copies
|
|
50
|
+
that must both be fresh is the shape the house rule forbids, and writing both harder is exactly
|
|
51
|
+
what concurrency beat. So the live readers — `run status`, the open-runs table, `tldrx
|
|
52
|
+
statusline`'s validated path and the dashboard's headline — now read budget.yml, the same field
|
|
53
|
+
`budget show` reads, and `budget raise` no longer writes the mirror at all. run.yml's
|
|
54
|
+
`budget.ceiling_usd` and `per_agent_max_usd` are documented for what they always were: the
|
|
55
|
+
figures the run was CREATED with. That is not a `version: 1` meaning change — the keys are
|
|
56
|
+
required and still written, they held the creation ceiling before this change too, and what
|
|
57
|
+
moved is only that nothing reads them as the current one. The corollary is the part worth
|
|
58
|
+
writing down, because the first cut of this change got it wrong and a pre-merge review caught
|
|
59
|
+
it: run.yml's budget block is **half live**. `spent_usd` is re-derived by `rollUp` on every
|
|
60
|
+
save; `ceiling_usd` beside it is frozen. Printing the two as one sentence therefore reproduces
|
|
61
|
+
the same defect somewhere else, which is precisely what `tldrx replay` did — measured through
|
|
62
|
+
the CLI on a run raised $10 → $30 with $12 spent: `Status: **pending** · $12.00 spent of $10.00
|
|
63
|
+
ceiling`, no corruption and no concurrency needed, just a raise. "A replay narrates the
|
|
64
|
+
document" does not rescue it when the document itself is mixed. So the ceiling is resolved once
|
|
65
|
+
in `replay/loadRun.ts`, the single place both files are in hand, and `tldrx replay` and the
|
|
66
|
+
dashboard share that derivation. Swept for the same class: the only other frozen keys in
|
|
67
|
+
run.yml are `budget.per_agent_max_usd`, which no reader anywhere displays, and `created_with`,
|
|
68
|
+
which is frozen on purpose and paired with `last_written_by` to show exactly that difference.
|
|
69
|
+
One reader stays on the mirror by design: the statusline's tolerant fallback, which runs only
|
|
70
|
+
when run.yml fails validation, where a second tolerant parser for budget.yml would be a worse
|
|
71
|
+
trade than a figure on an already-degraded screen.
|
|
72
|
+
|
|
73
|
+
- **A task row now says which ROLE took the turn, instead of filing every Build turn under the
|
|
74
|
+
developer (#234).** `run.yml`'s `tasks[].expert` is the STAGE's expert — one value copied onto
|
|
75
|
+
every row from `stage.experts[0]` — and a Build stage declares `experts: [developer]` while
|
|
76
|
+
running two roles under it. So the reviewer's turn, with its own money, its own token split
|
|
77
|
+
and its own measured span, was recorded as the developer's, and the golden fixtures had that
|
|
78
|
+
frozen: three of the four Build scenarios shipped a `fake-reviewer-*` session labelled
|
|
79
|
+
`developer`. The role was never unknown — the executor spawns under it and emits it on
|
|
80
|
+
`agent.spawned` — it was thrown away one function later, because `ExecutorTask` had nowhere to
|
|
81
|
+
put it. Nothing downstream computed a wrong NUMBER from this (no reader reads the field; the
|
|
82
|
+
per-role cost report it looked like it fed does not exist), which is exactly why it was worth
|
|
83
|
+
fixing now rather than after something started reading it: what was broken is the audit
|
|
84
|
+
record, and §7's rule is that those never lie in the dangerous direction. The fix is a new
|
|
85
|
+
additive `tasks[].role`, NOT a new meaning for `expert` — a `version: 1` field never changes
|
|
86
|
+
what it says — carried from the four places the Build executor records a turn. A turn whose
|
|
87
|
+
role nothing recorded carries no key at all: absent is "not recorded", never a `developer` of
|
|
88
|
+
convenience, which is the same guess the bug was made of. Rows written before the key exist
|
|
89
|
+
unchanged and still validate. `docs/spec.md` documents the new field and, while it was open,
|
|
90
|
+
the `tasks[].expert` that has been written on every row since 0.1 and was never documented.
|
|
91
|
+
|
|
92
|
+
## 0.16.1 — 2026-09-12
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
|
|
96
|
+
- **`tldrx reject --and-continue` — a rejection that means "redo it this way and carry on"
|
|
97
|
+
(#242).** `run auto --wait-gates` resumed after an approve and STOPPED after a reject, so on a
|
|
98
|
+
phone the button meaning *there is still work to do* was the one that ended the run: the only
|
|
99
|
+
way to act on a rejection was to walk to a terminal and relaunch. The stop was deliberate and
|
|
100
|
+
its reasoning is real — resuming re-spends the stage on a decision the person who rejected it
|
|
101
|
+
has not been shown the result of — but it is the reasoning for ONE kind of rejection, "stop, I
|
|
102
|
+
will look", and the issue measured five consecutive live rejections that all meant the other
|
|
103
|
+
one: *"faltan 4 stories sin arrancar; continuar el build"*, *"Rehacer S2 y las waves 3 y 4"*,
|
|
104
|
+
*"Una ronda mas"*. Five rejections, five manual relaunches, the assumption holding zero times
|
|
105
|
+
out of five. So the rejection now SAYS which act it is instead of the loop guessing from the
|
|
106
|
+
note's words or from what was holding the gate: `--and-continue` records `and_continue: true`
|
|
107
|
+
on the gate record `tldrx reject` writes — the same object `--wait-gates` already reads the
|
|
108
|
+
gate's `status` off, in the same read, so there is no second derivation and no second process
|
|
109
|
+
to ask — and the loop re-runs the stage with the note, exactly as the manual relaunch did. A
|
|
110
|
+
bare `tldrx reject` is byte-identical to what it wrote before and stops the loop exactly as
|
|
111
|
+
before; `and_continue` is additive and only ever `true`, so a gate written before this key
|
|
112
|
+
existed reads as "stop". It is cleared when the stage parks on its gate again, so it never
|
|
113
|
+
outlives the rejection it describes. The rationale in `runAuto.ts` now names both kinds and
|
|
114
|
+
says which one is the default. Which BUTTON a notification offers for which effect is
|
|
115
|
+
deliberately not part of this change.
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
|
|
119
|
+
- **The `merge-wave` "known flake" was a real race, and it was in the guard's own INSTALL
|
|
120
|
+
(#115).** For months `test/merge-wave.test.ts`'s concurrency cases reddened CI, went green on
|
|
121
|
+
a same-sha re-run, and were waved through under §4's re-run licence — three separate cases in
|
|
122
|
+
one night alone. Nobody had read the failure detail. `gh run view 34671878974 --log-failed`
|
|
123
|
+
(sha `8cd4df2`) says it in two lines: `fatal: cannot exec '.git/hooks/reference-transaction':
|
|
124
|
+
Text file busy` → `update aborted by the reference-transaction hook`. `merge-wave.sh` installs
|
|
125
|
+
the ref guard BEFORE it queues for the lock — deliberately, so an unguarded window never
|
|
126
|
+
exists — so two invocations overlap on exactly one file, and `install_hook` wrote it with
|
|
127
|
+
`cat > "$hook"`: truncate-and-rewrite THE SAME INODE, while the invocation holding the lock
|
|
128
|
+
has a `git merge` exec'ing it. On Linux exec of a file open for write is ETXTBSY; on macOS the
|
|
129
|
+
identical race is benign, which is the whole of why it was green locally and red on CI, and
|
|
130
|
+
why a same-sha re-run failed 2-for-2 rather than passing. The hook is now written to
|
|
131
|
+
`reference-transaction.tmp.$$`, made executable there, and `mv -f`'d into place — the exec'd
|
|
132
|
+
inode is never the written inode, and no reader can catch a zero-length window. It is the
|
|
133
|
+
answer `$MARKER` in `merge-wave.sh` already used, and the trap AGENTS.md §12 already named.
|
|
134
|
+
The race is untestable on macOS by design, so the WRITE is what is pinned: a reinstall must
|
|
135
|
+
land on a new inode and must leave the inode a hard link is holding byte-identical — red on
|
|
136
|
+
both counts before the fix. And the refusal that hid all this now tells the truth: a merge the
|
|
137
|
+
ref-transaction hook aborted, with no conflicting path anywhere, says so and exits **11**, its
|
|
138
|
+
own code, instead of borrowing `2` and telling the agent to go rebase against a conflict that
|
|
139
|
+
never existed. A mislabelled refusal is how a deterministic defect becomes folklore; §4's
|
|
140
|
+
re-run licence for this test is withdrawn in the same change.
|
|
141
|
+
- **The views fixture no longer decays: `test/experts.test.ts` was a wall-clock time bomb
|
|
142
|
+
(#240).** `main` went red at `e1d284d` — the exact sha of published 0.16.0, with no commit in
|
|
143
|
+
between — because `competencyLevel` weighs every evidence row by its AGE and the fixture dated
|
|
144
|
+
its rows ABSOLUTELY (`at: 2026-08-20`, …). An in-process test hands the reader `VIEWS_NOW` and
|
|
145
|
+
is hermetic; a test that spawns the CLI cannot, because the CLI reads `new Date()`. So
|
|
146
|
+
`dotnet-stack/ef-core` sat 3% above the level-3 threshold on the day the assertion was written
|
|
147
|
+
and fell through it eleven days later, by the calendar alone. Bumping the expected number would
|
|
148
|
+
only have re-armed the bomb for a later date, so the FIXTURE moved instead:
|
|
149
|
+
`makeViewsWorkspace({ now })` re-dates the copied evidence so every row keeps the age the
|
|
150
|
+
fixture meant, relative to the clock the assertion is evaluated against, and `NOW` in
|
|
151
|
+
`experts.test.ts` is the real clock rather than a pinned calendar day. Measured with the clock
|
|
152
|
+
moved a year forward: reverted, five tests red (the one that reds today plus four with longer
|
|
153
|
+
fuses); fixed, the whole suite is green at +1 year and the fixture's own consumers are green at
|
|
154
|
+
+5. §8's hermeticity rule now covers the clock as well as `$TMPDIR`.
|
|
155
|
+
- **A parked gate hands over the command that CLEARS it, not `tldrx approve` whatever is
|
|
156
|
+
holding it (#239).** Measured on an owner's phone, 2026-09-10: a gate held BY five
|
|
157
|
+
unanswered questions was announced as `Run: tldrx approve --run <id>`, and the
|
|
158
|
+
`--notify-every` heartbeat repeated that same line seven times in an hour. The sentence
|
|
159
|
+
named the holding condition correctly — #203 was working — and the ACTION contradicted it,
|
|
160
|
+
which is the worse half: a notification exists to be obeyed off a lock screen. Two Build
|
|
161
|
+
gates were approved by mistake that evening, both over unbuilt stories, both revoked with
|
|
162
|
+
`reject --stage`; the owner said so himself — *"no sé por qué me avisa que ya puedo cerrarlo
|
|
163
|
+
si aún hay preguntas"*. `command` now follows the holding condition, in ONE mapping shared by
|
|
164
|
+
`gate.requested` and the parked heartbeat — and over ONE reading of the pending gate,
|
|
165
|
+
`gateStories`, so the alert and its reminder can never offer two different taps: open blocking questions → the `tldrx answer` line (the gate is downstream of
|
|
166
|
+
them); unfinished stories → `tldrx run status` (not `reject` — nobody has decided to abandon
|
|
167
|
+
that work, and a one-tap refusal is the mirror of the mistake being fixed — and not `null`,
|
|
168
|
+
because what is missing there is knowledge, not a signature); nothing mechanical outstanding
|
|
169
|
+
→ `tldrx approve`, which is what the field always meant. `approve_command` and
|
|
170
|
+
`reject_command` stay in the `detail` of every payload, so an adapter that renders buttons
|
|
171
|
+
keeps both. The open question ids are read off disk by the loop through `blockingQuestionIds`
|
|
172
|
+
— the one predicate `--wait-answers` polls — at the moment the notification is SENT, so a
|
|
173
|
+
gate whose questions cleared while the send was deferred does not point at an answered
|
|
174
|
+
question; the heartbeat reads the gate's stories the same way, every tick, instead of the
|
|
175
|
+
hard-wired "nothing unfinished" that kept it saying `approve` at a Build gate held by unbuilt
|
|
176
|
+
work — the gate the two mistaken approvals were on.
|
|
177
|
+
- **The Build gate's summary has a verb again (#239).** `deliveredPhrase` is a noun phrase and
|
|
178
|
+
three callers embed it after a label, so `It 5 of 6 stories delivered, S6 blocked (…)` was
|
|
179
|
+
reaching lock screens as a typo. The article is fixed at the one call site that needed a
|
|
180
|
+
sentence — `It has 5 of 6 stories delivered` — and the phrase's contract is unchanged for
|
|
181
|
+
`run next`, `ship` and the decision card.
|
|
182
|
+
|
|
183
|
+
- **A red base pre-flight now keeps what the command SAID, so a stage-wide refusal names a
|
|
184
|
+
cause (#229).** #211 taught a red story DoD to keep its output — the file on disk, an
|
|
185
|
+
excerpt, the failing line, a failure-shaped `tail` — and the base row, produced a hundred
|
|
186
|
+
lines away in the same file, was left on `outcome.tail`: the last line of stdout+stderr.
|
|
187
|
+
The blast radius and the evidence were the wrong way round. A red story DoD blocks ONE
|
|
188
|
+
story; a red base refuses the WHOLE stage before anything is dispatched or charged.
|
|
189
|
+
Measured in the field: a `dotnet test` whose 163,702 captured lines named a dead container
|
|
190
|
+
daemon on line 12 refused all six stories of a stage with `Test run completed with
|
|
191
|
+
non-success exit code: 2` — the sentence every failing run of that runner prints, whatever
|
|
192
|
+
broke. Nothing in the run directory contained the word `Docker`, so diagnosing the refusal
|
|
193
|
+
meant re-running by hand the command the pre-flight exists to have already run. The base
|
|
194
|
+
row now goes through the SAME seam, not a second reading of it: `tail` is the
|
|
195
|
+
failure-looking line, `excerpt` the few lines around it, and the whole bounded tail is
|
|
196
|
+
written to `04-build/log/dod-output/base-<hash>-1.txt`, which the refusal cites by file and
|
|
197
|
+
line. A GREEN base still writes nothing — #211's argument holds harder here, since a green
|
|
198
|
+
base is re-used from cache far more often than a story's — and an `unmeasured` row is
|
|
199
|
+
untouched: nothing ran, it refuses nothing, and its `tail` is already a reason sentence.
|
|
200
|
+
`04-build/preflight.yml` stays `version: 1`: four optional fields, and every older file
|
|
201
|
+
still reads.
|
|
202
|
+
|
|
3
203
|
## 0.16.0 — 2026-09-11
|
|
4
204
|
|
|
5
205
|
### Added
|
package/README.md
CHANGED
|
@@ -316,6 +316,8 @@ back on the registry is 0.3.0.
|
|
|
316
316
|
|
|
317
317
|
| Version | Date | Status | Contains |
|
|
318
318
|
|---|---|---|---|
|
|
319
|
+
| 0.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
|
+
| 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 |
|
|
319
321
|
| 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 |
|
|
320
322
|
| 0.15.0 | 2026-09-10 | `beta` | defaults for the models actually running today, and records that name what happened: measured 2026-09-07/09 across three real workspaces, the first engine-driven run of each was ended by a calibration rather than by the work — a `how` turn and two Build developer turns killed at a 900 s per-turn clock while Opus turns on real repositories run 15-50 minutes, a 202 KB prompt refused by a ceiling whose own message called it "29% of a 200k window", and a 169 KB `facts.yml` sliced to 96 KB on its way into a design turn that then died. So a turn gets two hours (`timeout_s` 900 → 7200), a prompt 400 KB and inputs 256 KB, a phase ceiling holds every attempt its stages may take so the first retry of a stage that spent anything is no longer refused by arithmetic — `warn_at_pct` still measured against one attempt's share, so the warning still arrives before the money — and the four numbers that were calibrations rather than invariants (`attempts`, `fixlist_rounds`, `reviewer_share`, `gate_signer_share`) became optional `stage.yml` keys, refused by name out of range instead of clamped, absent meaning today's constant byte for byte, with `tldrx run auto --prompt-max-bytes` and `--max-reads` for the unattended run that would otherwise need a file edit to get past one refusal; a story's Definition of Done now runs with its dependencies installed — the `install:` slot has sat unread in `templates/workspace.yml` since the beginning and now runs in every fresh story worktree through the same allowlist-and-argv runner, recorded with its own exit code and duration, blocking the story rather than paying a turn to discover it — an exit 127 is reported as a named absent binary and not as a red test, a declared command may be run WITH ARGUMENTS (the exact `Bash(npm run test)` grant matched nothing the developer actually typed, so its own 127 was first seen by the gate, after the turn was paid for), and every DoD check says which tree it ran in; the Build gate now names story outcomes on every policy and not only `auto` — two runs approved from a phone printed `run is done` over zero stories delivered — `run.yml` records an additive `outcome:` written once by all three commands that close a run and rendered by six surfaces, and `tldrx ship` refuses with exit 1 instead of opening a PR over nothing; a red DoD keeps its real failure — the last 200 lines on disk (gitignored, since a tail can carry a secret), up to five failure-looking lines as the detail rather than the last `DeprecationWarning` on stderr, the failing line cited at the line it starts on, and the next attempt told it was the check and not a reviewer; a watcher card may honestly say `Query: none — <reason> [src: …]`, earned only over a card whose own `## Signal` cites `absent:` and refused like any unsourced item otherwise, after a stage spent real money writing the honest answer and was refused for it; a truncated input is told to the OWNER at spawn and not only to the sub-agent, a turn killed on timeout keeps the usage it had already streamed and never a price; and the maintain skill says which sha a review record must cite — the code head — a rule that cost a wave and was written down nowhere an agent reads |
|
|
321
323
|
| 0.14.3 | 2026-09-10 | `beta` | foreign uncommitted work no longer stops a Build, and the dashboard flake that blocked four merges in two days has a root cause: the dirty-tree guard used to count every `git status --porcelain` entry and refuse, offering only "commit it" or "stash it" — neither of which an agent may take with another person's files — and measured across three real workspaces on 0.14.2, every first engine-driven run reaching Build stopped at `04-build`, over seed docs, a data export and one untracked note; the dirt is now classified, `own` and `overlapping` refusing or passing exactly as before while everything `foreign` is set aside with a pathspec-limited `git stash push` as the LAST step before the epic branch is cut, recorded as `worktree.foreign_work_aside` and given back with `--index` on every exit path, success or failure, nothing ever deleted and nothing force-popped, a repo mid-merge, rebase, cherry-pick or bisect refused outright because that state has no clean undo, and a pop git refuses said as the stage's last line and carried into the handoff and the notification; the refusal's printed remedy is now the SAME string the engine runs, limited to the paths it listed and relaunching by mode, after an owner ran the pathspec-less line exactly as printed and it swept the run's own records under `tldrx-work/<run>/` into the stash until `tldrx next` answered `no run`; every path handed to git for a write is `:(literal)` and `git status` is read with `-z`, since a glob pathspec moved the neighbouring `x.txt` for a file called `[x].txt`; and the dashboard's live tests stop racing a typed millisecond — five consecutive runs of the two files went red 3 times, at 5084.27 / 5108.01 / 5256.49 ms against a hard-coded 5000 under load averages 65–107 on 14 cores — every deadline now deriving from one `eventWaitMs()` helper that scales like every other budget, with `test/machine-load.test.ts` refusing a hard-coded deadline in either file so it cannot come back at somebody's merge, while that measurement surfaced the product half: `watchWorkspace` armed its mtime sweep only in `poll` mode, so a dropped FSEvents notification left a live dashboard silently stale for the life of the process — measured with `fseventsd` at 98–115% CPU, directory events that never arrived AT ALL at 82,556 ms and 113,942 ms — and the sweep now runs in watch mode too, at 2 s, so a dropped notification is bounded rather than fatal |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
conflictOf
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-tj66vg1n.js";
|
|
5
5
|
import {
|
|
6
6
|
FactsStore,
|
|
7
7
|
formatJaccard
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
EventLog,
|
|
15
15
|
PHASE_ID_RE
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-jp6jscsd.js";
|
|
17
17
|
import {
|
|
18
18
|
PHASE_IDS
|
|
19
19
|
} from "./chunk-d0rp8c68.js";
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
isTerminal,
|
|
22
22
|
stageAt,
|
|
23
23
|
validateRunFile
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-jp6jscsd.js";
|
|
25
25
|
import {
|
|
26
26
|
cursorStage,
|
|
27
27
|
isAttendedByHostView,
|
|
@@ -99,7 +99,8 @@ function gate(g) {
|
|
|
99
99
|
const evidence = g.evidence === undefined ? "" : `, evidence: ${gateEvidence(g.evidence)}`;
|
|
100
100
|
const executor = g.executed_by === undefined ? "" : `, executed_by: ${gateExecutor(g.executed_by)}`;
|
|
101
101
|
const authority = g.authority === undefined ? "" : `, authority: ${gateAuthority(g.authority)}`;
|
|
102
|
-
|
|
102
|
+
const andContinue = g.and_continue === undefined ? "" : ", and_continue: true";
|
|
103
|
+
return `{type: ${yamlScalar(g.type)}, status: ${yamlScalar(g.status)}, by: ${yamlScalar(g.by)}, ` + `at: ${yamlScalar(g.at)}, note: ${yamlScalar(g.note)}${evidence}${executor}${authority}${andContinue}}`;
|
|
103
104
|
}
|
|
104
105
|
function task(t, indent) {
|
|
105
106
|
const inner = `${indent} `;
|
|
@@ -108,8 +109,9 @@ function task(t, indent) {
|
|
|
108
109
|
const tokens = t.tokens === undefined ? "" : `, tokens: ${String(t.tokens)}`;
|
|
109
110
|
const inTokens = t.input_tokens === undefined ? "" : `, input_tokens: ${String(t.input_tokens)}`;
|
|
110
111
|
const outTokens = t.output_tokens === undefined ? "" : `, output_tokens: ${String(t.output_tokens)}`;
|
|
112
|
+
const role = t.role === undefined ? "" : `, role: ${yamlScalar(t.role)}`;
|
|
111
113
|
return [
|
|
112
|
-
`${indent} - {id: ${yamlScalar(t.id)}, status: ${yamlScalar(t.status)}, expert: ${yamlScalar(t.expert)}, ` + `model: ${yamlScalar(t.model)}, cost_usd: ${cost}${metered}${tokens}${inTokens}${outTokens},`,
|
|
114
|
+
`${indent} - {id: ${yamlScalar(t.id)}, status: ${yamlScalar(t.status)}, expert: ${yamlScalar(t.expert)}${role}, ` + `model: ${yamlScalar(t.model)}, cost_usd: ${cost}${metered}${tokens}${inTokens}${outTokens},`,
|
|
113
115
|
`${inner}error: ${yamlScalar(t.error)}, session_id: ${yamlScalar(t.session_id)},`,
|
|
114
116
|
`${inner}started_at: ${yamlScalar(t.started_at)}, ended_at: ${yamlScalar(t.ended_at)},`,
|
|
115
117
|
...t.stopped_by === undefined || t.stopped_by === null ? [] : [`${inner}stopped_by: ${yamlScalar(t.stopped_by)},`],
|
|
@@ -605,7 +607,7 @@ function fromStore(root) {
|
|
|
605
607
|
expert: entry?.stage.expert ?? null,
|
|
606
608
|
done: stages.filter((stage2) => isTerminal(stage2.status)).length,
|
|
607
609
|
total: stages.length,
|
|
608
|
-
ceilingUsd:
|
|
610
|
+
ceilingUsd: store.budget.ceiling_usd,
|
|
609
611
|
spentUsd: run.budget.spent_usd,
|
|
610
612
|
openCount: open.length,
|
|
611
613
|
machineGates: stages.filter((s) => s.gate.status === "approved" && closedByMachine(s.gate)).length,
|
|
@@ -700,6 +700,9 @@ function validateRunFile(input) {
|
|
|
700
700
|
requireEnum(task.status, STAGE_STATUSES, `${tp}.status`, issues);
|
|
701
701
|
if (task.cost_usd !== null)
|
|
702
702
|
requireNumber(task.cost_usd, `${tp}.cost_usd`, issues);
|
|
703
|
+
if (task.role !== undefined && typeof task.role !== "string") {
|
|
704
|
+
issues.push({ path: `${tp}.role`, message: "expected a string" });
|
|
705
|
+
}
|
|
703
706
|
if (task.metered !== undefined && typeof task.metered !== "boolean") {
|
|
704
707
|
issues.push({ path: `${tp}.metered`, message: "expected true or false" });
|
|
705
708
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
questionsCard
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-tj66vg1n.js";
|
|
5
5
|
import"./chunk-54vzevgt.js";
|
|
6
6
|
import {
|
|
7
7
|
allow,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
runSnapshot,
|
|
21
21
|
statusWithOutcome,
|
|
22
22
|
whatIsWaiting
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-g8kkq85r.js";
|
|
24
24
|
import {
|
|
25
25
|
expertsDir,
|
|
26
26
|
loadExperts,
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "./chunk-3kmx3dmz.js";
|
|
31
31
|
import {
|
|
32
32
|
isFinished
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-jp6jscsd.js";
|
|
34
34
|
import"./chunk-d0rp8c68.js";
|
|
35
35
|
import {
|
|
36
36
|
openRunViews
|
package/dist/hooks/statusline.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
bar,
|
|
4
4
|
runSnapshot
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-g8kkq85r.js";
|
|
6
6
|
import"./chunk-3kmx3dmz.js";
|
|
7
|
-
import"./chunk-
|
|
7
|
+
import"./chunk-jp6jscsd.js";
|
|
8
8
|
import"./chunk-d0rp8c68.js";
|
|
9
9
|
import"./chunk-4mjxyfp9.js";
|
|
10
10
|
import"./chunk-k4nqzdw5.js";
|
package/dist/tldrx.js
CHANGED
|
@@ -11902,7 +11902,8 @@ function gate(g) {
|
|
|
11902
11902
|
const evidence = g.evidence === undefined ? "" : `, evidence: ${gateEvidence(g.evidence)}`;
|
|
11903
11903
|
const executor = g.executed_by === undefined ? "" : `, executed_by: ${gateExecutor(g.executed_by)}`;
|
|
11904
11904
|
const authority = g.authority === undefined ? "" : `, authority: ${gateAuthority(g.authority)}`;
|
|
11905
|
-
|
|
11905
|
+
const andContinue = g.and_continue === undefined ? "" : ", and_continue: true";
|
|
11906
|
+
return `{type: ${yamlScalar(g.type)}, status: ${yamlScalar(g.status)}, by: ${yamlScalar(g.by)}, ` + `at: ${yamlScalar(g.at)}, note: ${yamlScalar(g.note)}${evidence}${executor}${authority}${andContinue}}`;
|
|
11906
11907
|
}
|
|
11907
11908
|
function task(t, indent) {
|
|
11908
11909
|
const inner = `${indent} `;
|
|
@@ -11911,8 +11912,9 @@ function task(t, indent) {
|
|
|
11911
11912
|
const tokens = t.tokens === undefined ? "" : `, tokens: ${String(t.tokens)}`;
|
|
11912
11913
|
const inTokens = t.input_tokens === undefined ? "" : `, input_tokens: ${String(t.input_tokens)}`;
|
|
11913
11914
|
const outTokens = t.output_tokens === undefined ? "" : `, output_tokens: ${String(t.output_tokens)}`;
|
|
11915
|
+
const role = t.role === undefined ? "" : `, role: ${yamlScalar(t.role)}`;
|
|
11914
11916
|
return [
|
|
11915
|
-
`${indent} - {id: ${yamlScalar(t.id)}, status: ${yamlScalar(t.status)}, expert: ${yamlScalar(t.expert)}, ` + `model: ${yamlScalar(t.model)}, cost_usd: ${cost}${metered}${tokens}${inTokens}${outTokens},`,
|
|
11917
|
+
`${indent} - {id: ${yamlScalar(t.id)}, status: ${yamlScalar(t.status)}, expert: ${yamlScalar(t.expert)}${role}, ` + `model: ${yamlScalar(t.model)}, cost_usd: ${cost}${metered}${tokens}${inTokens}${outTokens},`,
|
|
11916
11918
|
`${inner}error: ${yamlScalar(t.error)}, session_id: ${yamlScalar(t.session_id)},`,
|
|
11917
11919
|
`${inner}started_at: ${yamlScalar(t.started_at)}, ended_at: ${yamlScalar(t.ended_at)},`,
|
|
11918
11920
|
...t.stopped_by === undefined || t.stopped_by === null ? [] : [`${inner}stopped_by: ${yamlScalar(t.stopped_by)},`],
|
|
@@ -13291,6 +13293,9 @@ function validateRunFile(input) {
|
|
|
13291
13293
|
requireEnum(task2.status, STAGE_STATUSES, `${tp}.status`, issues);
|
|
13292
13294
|
if (task2.cost_usd !== null)
|
|
13293
13295
|
requireNumber(task2.cost_usd, `${tp}.cost_usd`, issues);
|
|
13296
|
+
if (task2.role !== undefined && typeof task2.role !== "string") {
|
|
13297
|
+
issues.push({ path: `${tp}.role`, message: "expected a string" });
|
|
13298
|
+
}
|
|
13294
13299
|
if (task2.metered !== undefined && typeof task2.metered !== "boolean") {
|
|
13295
13300
|
issues.push({ path: `${tp}.metered`, message: "expected true or false" });
|
|
13296
13301
|
}
|
|
@@ -15652,7 +15657,7 @@ var ENTRIES = [
|
|
|
15652
15657
|
{
|
|
15653
15658
|
name: "wait-gates",
|
|
15654
15659
|
arg: "<duration>",
|
|
15655
|
-
meaning: "Instead of exiting 4 the moment a stage parks on a pending GATE, poll the run for this long and resume if somebody signs it. `--wait-answers`' sibling for the other half of exit 4: a gate is closed by `tldrx approve` / `tldrx reject`, not by an answer. Approved → the loop carries on; rejected → it stops and prints the note; lapsed → exit 4 with the same lines it always had, after one `gate.timeout` notification. It WAITS FOR a signature and never produces one. A stage on `gates_policy: agent` has already had the engine's own gate signer run on it before this flag ever sees the gate (see the `gates_policy: agent` note below), so what is left to wait for here is a PERSON — the same wait a `human` gate gets. Nothing is spent while it waits. Both wait flags may be given together.",
|
|
15660
|
+
meaning: "Instead of exiting 4 the moment a stage parks on a pending GATE, poll the run for this long and resume if somebody signs it. `--wait-answers`' sibling for the other half of exit 4: a gate is closed by `tldrx approve` / `tldrx reject`, not by an answer. Approved → the loop carries on; rejected → it stops and prints the note, unless the rejection was `tldrx reject --and-continue`, which re-runs the stage with the note instead (#242); lapsed → exit 4 with the same lines it always had, after one `gate.timeout` notification. It WAITS FOR a signature and never produces one. A stage on `gates_policy: agent` has already had the engine's own gate signer run on it before this flag ever sees the gate (see the `gates_policy: agent` note below), so what is left to wait for here is a PERSON — the same wait a `human` gate gets. Nothing is spent while it waits. Both wait flags may be given together.",
|
|
15656
15661
|
sub: "auto"
|
|
15657
15662
|
},
|
|
15658
15663
|
{
|
|
@@ -15988,6 +15993,11 @@ var ENTRIES = [
|
|
|
15988
15993
|
args: [],
|
|
15989
15994
|
flags: [
|
|
15990
15995
|
{ name: "note", arg: "<text>", meaning: "What has to change. Required — a rejection with no reason is not actionable." },
|
|
15996
|
+
{
|
|
15997
|
+
name: "and-continue",
|
|
15998
|
+
arg: null,
|
|
15999
|
+
meaning: "This rejection means “redo it this way and carry on”, not “stop, I will look”. The stage goes back to `ready` with the note exactly as a bare rejection leaves it — what changes is that an unattended `tldrx run auto --wait-gates` re-runs the stage instead of exiting 4, so a rejection sent from a phone does not need a walk to a terminal to take effect. Recorded on the gate, so the waiting loop reads it rather than guessing from the note’s words. Without it a rejection stops the loop, which is the default and always was. Refused with exit 1 beside `--stage`: a revoke leaves that gate pending for a decision nobody has made yet, so there is no rejection for it to describe."
|
|
16000
|
+
},
|
|
15991
16001
|
{
|
|
15992
16002
|
name: "stage",
|
|
15993
16003
|
arg: "<phase>/<stage>",
|
|
@@ -15998,6 +16008,7 @@ var ENTRIES = [
|
|
|
15998
16008
|
],
|
|
15999
16009
|
examples: [
|
|
16000
16010
|
'tldrx reject --note "contracts.md does not name the events"',
|
|
16011
|
+
'tldrx reject --and-continue --note "S2 fell over on a missing binary — redo S2 and waves 3 and 4"',
|
|
16001
16012
|
'tldrx reject --stage 02-how/design --note "the auto gate signed over four open questions"'
|
|
16002
16013
|
],
|
|
16003
16014
|
exits: [EXIT_OK, EXIT_USAGE, EXIT_GATE_REFUSED, EXIT_NOT_FOUND]
|
|
@@ -28963,6 +28974,9 @@ function blockedReasons(runDir) {
|
|
|
28963
28974
|
}
|
|
28964
28975
|
return out;
|
|
28965
28976
|
}
|
|
28977
|
+
function gateStories(runDir, phaseId) {
|
|
28978
|
+
return phaseId === BUILD_PHASE2 ? storiesView(runDir) : null;
|
|
28979
|
+
}
|
|
28966
28980
|
function gateStoriesPayload(view) {
|
|
28967
28981
|
const blocked = view.firstBlocked;
|
|
28968
28982
|
return {
|
|
@@ -28970,6 +28984,14 @@ function gateStoriesPayload(view) {
|
|
|
28970
28984
|
...blocked === null ? {} : { blocked_story: blocked.id, blocked_reason: blocked.reason }
|
|
28971
28985
|
};
|
|
28972
28986
|
}
|
|
28987
|
+
function continueNote(view) {
|
|
28988
|
+
const blocked = view.firstBlocked;
|
|
28989
|
+
if (blocked === null)
|
|
28990
|
+
return null;
|
|
28991
|
+
if (blocked.reason === REASON_NOT_RECORDED)
|
|
28992
|
+
return null;
|
|
28993
|
+
return `${blocked.id} is blocked: ${blocked.reason}. Redo ${blocked.id}.`;
|
|
28994
|
+
}
|
|
28973
28995
|
var REASON_IN_SUMMARY = 80;
|
|
28974
28996
|
var NAMED_IN_SUMMARY = 3;
|
|
28975
28997
|
function deliveredPhrase(view) {
|
|
@@ -29949,7 +29971,7 @@ function buildStatus(run, budget, runDir) {
|
|
|
29949
29971
|
phases,
|
|
29950
29972
|
budget: {
|
|
29951
29973
|
spent_usd: run.budget.spent_usd,
|
|
29952
|
-
ceiling_usd:
|
|
29974
|
+
ceiling_usd: budget.ceiling_usd,
|
|
29953
29975
|
remaining_usd: remaining(budget)
|
|
29954
29976
|
},
|
|
29955
29977
|
attempts: stageAttempts(runDir, run.cursor.phase, run.cursor.stage),
|
|
@@ -30156,7 +30178,7 @@ function openRunRow(store) {
|
|
|
30156
30178
|
cursor: `${run.cursor.phase}/${run.cursor.stage}`,
|
|
30157
30179
|
waiting: whatIsWaiting(run, store.runDir).kind,
|
|
30158
30180
|
spentUsd: run.budget.spent_usd,
|
|
30159
|
-
ceilingUsd:
|
|
30181
|
+
ceilingUsd: store.budget.ceiling_usd
|
|
30160
30182
|
};
|
|
30161
30183
|
}
|
|
30162
30184
|
function openRunRows(stores) {
|
|
@@ -30291,6 +30313,9 @@ function approveCommand(runId) {
|
|
|
30291
30313
|
function rejectCommand(runId) {
|
|
30292
30314
|
return `tldrx reject --run ${runId} --note "<why>"`;
|
|
30293
30315
|
}
|
|
30316
|
+
function continueCommand(runId) {
|
|
30317
|
+
return `tldrx reject --run ${runId} --and-continue --note "…"`;
|
|
30318
|
+
}
|
|
30294
30319
|
function toQuestion(block2, runId, recommendations) {
|
|
30295
30320
|
const recommended = recommendations.get(block2.id);
|
|
30296
30321
|
return {
|
|
@@ -30664,20 +30689,41 @@ function gatePhrase(policy) {
|
|
|
30664
30689
|
return "gate";
|
|
30665
30690
|
}
|
|
30666
30691
|
}
|
|
30667
|
-
function
|
|
30692
|
+
function clearingCommand(runId, openQuestions2, unfinishedStories) {
|
|
30693
|
+
switch (gateHolding(openQuestions2, unfinishedStories)) {
|
|
30694
|
+
case "questions":
|
|
30695
|
+
return answerCommand(openQuestions2[0] ?? "", runId);
|
|
30696
|
+
case "stories":
|
|
30697
|
+
return `tldrx run status ${runId}`;
|
|
30698
|
+
default:
|
|
30699
|
+
return approveCommand(runId);
|
|
30700
|
+
}
|
|
30701
|
+
}
|
|
30702
|
+
function gateHolding(openQuestions2, unfinishedStories) {
|
|
30703
|
+
if (openQuestions2[0] !== undefined)
|
|
30704
|
+
return "questions";
|
|
30705
|
+
if (unfinishedStories > 0)
|
|
30706
|
+
return "stories";
|
|
30707
|
+
return "none";
|
|
30708
|
+
}
|
|
30709
|
+
function gateNotification(ctx, costUsd, policy = null, held2 = [], stories = null, openQuestions2 = []) {
|
|
30668
30710
|
const approve = approveCommand(ctx.runId);
|
|
30669
30711
|
const why = held2.length === 0 ? "" : policy === "auto" ? ` It is held by: ${held2.join("; ")}.` : ` The engine's signer held it: ${held2.join("; ")}.`;
|
|
30670
|
-
const delivered = stories === null ? "" : ` It ${deliveredPhrase(stories)}.`;
|
|
30712
|
+
const delivered = stories === null ? "" : ` It has ${deliveredPhrase(stories)}.`;
|
|
30713
|
+
const holding = gateHolding(openQuestions2, stories?.unfinished.length ?? 0);
|
|
30714
|
+
const note = holding === "stories" && stories !== null ? continueNote(stories) : null;
|
|
30671
30715
|
return {
|
|
30672
30716
|
...base(ctx, "gate.requested"),
|
|
30673
30717
|
summary: `${ctx.runId} finished ${ctx.stage ?? "a stage"} for $${costUsd.toFixed(2)} and is waiting ` + `at ${gateArticle(policy)} ${gatePhrase(policy)}.${delivered}${why} Nothing runs after it until the gate is ` + "approved or rejected.",
|
|
30674
|
-
command:
|
|
30718
|
+
command: clearingCommand(ctx.runId, openQuestions2, stories?.unfinished.length ?? 0),
|
|
30675
30719
|
detail: {
|
|
30676
30720
|
cost_usd: costUsd,
|
|
30677
30721
|
approve_command: approve,
|
|
30678
30722
|
reject_command: rejectCommand(ctx.runId),
|
|
30679
30723
|
...policy === null ? {} : { gate_policy: policy },
|
|
30680
30724
|
...stories === null ? {} : gateStoriesPayload(stories),
|
|
30725
|
+
holding,
|
|
30726
|
+
...note === null ? {} : { continue_command: continueCommand(ctx.runId), continue_note: note },
|
|
30681
30727
|
...held2.length === 0 ? {} : policy === "auto" ? { held_by: held2 } : { signer_held: held2 }
|
|
30682
30728
|
}
|
|
30683
30729
|
};
|
|
@@ -30737,7 +30783,7 @@ function runEndNotification(ctx, exitCode, spentUsd, lastLine2, tally = { usd: s
|
|
|
30737
30783
|
}
|
|
30738
30784
|
};
|
|
30739
30785
|
}
|
|
30740
|
-
function statusNotification(ctx, statusText, waitingOn = [], waitingOnGate = null, truncation = null) {
|
|
30786
|
+
function statusNotification(ctx, statusText, waitingOn = [], waitingOnGate = null, truncation = null, stories = null) {
|
|
30741
30787
|
const ids = [...waitingOn];
|
|
30742
30788
|
const parked = ids.length > 0;
|
|
30743
30789
|
const gateSummary = waitingOnGate === null ? "" : `${ctx.runId} is parked at ${waitingOnGate.stage} waiting for a person to SIGN it: ` + `${gatePhrase(waitingOnGate.policy)}. Nothing runs after it and nothing is being spent ` + "while it waits." + (parked ? ` It also has ${String(ids.length)} open question(s): ${ids.join(", ")}.` : "");
|
|
@@ -30745,7 +30791,7 @@ function statusNotification(ctx, statusText, waitingOn = [], waitingOnGate = nul
|
|
|
30745
30791
|
return {
|
|
30746
30792
|
...base(ctx, "status"),
|
|
30747
30793
|
summary: (waitingOnGate !== null ? gateSummary : parked ? `${ctx.runId} is parked at ${ctx.stage ?? "an unnamed stage"} waiting on YOU: ` + `${String(ids.length)} open question(s), ${ids.join(", ")}. Nothing is being spent ` + "while it waits, and it resumes the moment one is answered." : `${ctx.runId} is still running at ${ctx.stage ?? "an unnamed stage"}. ` + "Nothing is waiting on you — this is the periodic heartbeat `--notify-every` asked for.") + tail2,
|
|
30748
|
-
command: waitingOnGate !== null ?
|
|
30794
|
+
command: waitingOnGate !== null || parked ? clearingCommand(ctx.runId, ids, stories?.unfinished.length ?? 0) : `tldrx run status ${ctx.runId}`,
|
|
30749
30795
|
detail: {
|
|
30750
30796
|
status_text: statusText,
|
|
30751
30797
|
waiting_on: ids,
|
|
@@ -31055,15 +31101,23 @@ function reject(store, ctx) {
|
|
|
31055
31101
|
...stage2,
|
|
31056
31102
|
status: "ready",
|
|
31057
31103
|
ended_at: null,
|
|
31058
|
-
gate: {
|
|
31104
|
+
gate: {
|
|
31105
|
+
...stage2.gate,
|
|
31106
|
+
status: "rejected",
|
|
31107
|
+
by: ctx.actor,
|
|
31108
|
+
at: ctx.at,
|
|
31109
|
+
note: ctx.note,
|
|
31110
|
+
and_continue: ctx.andContinue === true ? true : undefined
|
|
31111
|
+
}
|
|
31059
31112
|
})));
|
|
31060
31113
|
store.append(event2(ctx.at, store.runId, entry.stage.id, "gate.rejected", ctx.actor, {
|
|
31061
31114
|
phase: entry.phase.id,
|
|
31062
31115
|
note: ctx.note,
|
|
31063
|
-
from
|
|
31116
|
+
from,
|
|
31117
|
+
...ctx.andContinue === true ? { and_continue: true } : {}
|
|
31064
31118
|
}));
|
|
31065
31119
|
store.save();
|
|
31066
|
-
return { stage: entry.stage.id, phase: entry.phase.id, note: ctx.note, from };
|
|
31120
|
+
return { stage: entry.stage.id, phase: entry.phase.id, note: ctx.note, from, andContinue: ctx.andContinue === true };
|
|
31067
31121
|
}
|
|
31068
31122
|
function revoke(store, ctx, target) {
|
|
31069
31123
|
if (ctx.note.trim() === "") {
|
|
@@ -33636,6 +33690,14 @@ function emitPreflightYaml(preflight) {
|
|
|
33636
33690
|
lines.push(` - repo: ${yamlScalar(row2.repo)}`, ` command: ${yamlScalar(row2.command)}`, ` base_ref: ${yamlScalar(row2.baseRef)}`, ` base_sha: ${yamlScalar(row2.baseSha)}`, ...typeof row2.exitCode === "number" ? [` exit_code: ${String(row2.exitCode)}`] : [], ` timed_out: ${row2.timedOut ? "true" : "false"}`, ` status: ${yamlScalar(row2.status)}`, ` tail: ${yamlScalar(row2.tail)}`);
|
|
33637
33691
|
if (row2.refusedBecause !== undefined)
|
|
33638
33692
|
lines.push(` refused_because: ${yamlScalar(row2.refusedBecause)}`);
|
|
33693
|
+
if (row2.excerpt !== undefined)
|
|
33694
|
+
lines.push(` excerpt: ${yamlScalar(row2.excerpt)}`);
|
|
33695
|
+
if (row2.outputPath !== undefined)
|
|
33696
|
+
lines.push(` output_path: ${yamlScalar(row2.outputPath)}`);
|
|
33697
|
+
if (row2.outputBytes !== undefined)
|
|
33698
|
+
lines.push(` output_bytes: ${String(row2.outputBytes)}`);
|
|
33699
|
+
if (row2.outputLine !== undefined)
|
|
33700
|
+
lines.push(` output_line: ${String(row2.outputLine)}`);
|
|
33639
33701
|
if (row2.commandHash !== undefined)
|
|
33640
33702
|
lines.push(` command_hash: ${yamlScalar(row2.commandHash)}`);
|
|
33641
33703
|
if (row2.checkedAt !== undefined)
|
|
@@ -33678,6 +33740,8 @@ function parsePreflight(text3) {
|
|
|
33678
33740
|
return null;
|
|
33679
33741
|
}
|
|
33680
33742
|
const hash = asText(row2.command_hash);
|
|
33743
|
+
const excerpt = asText(row2.excerpt);
|
|
33744
|
+
const outputPath = asText(row2.output_path);
|
|
33681
33745
|
const rowCheckedAt = asText(row2.checked_at);
|
|
33682
33746
|
results.push({
|
|
33683
33747
|
repo,
|
|
@@ -33690,6 +33754,10 @@ function parsePreflight(text3) {
|
|
|
33690
33754
|
...refusedBecause === "" ? {} : { refusedBecause },
|
|
33691
33755
|
status: row2.status === "ok" || row2.status === "failed" ? row2.status : "unmeasured",
|
|
33692
33756
|
...hash === "" ? {} : { commandHash: hash },
|
|
33757
|
+
...excerpt === "" ? {} : { excerpt },
|
|
33758
|
+
...outputPath === "" ? {} : { outputPath },
|
|
33759
|
+
...Number.isInteger(row2.output_bytes) ? { outputBytes: row2.output_bytes } : {},
|
|
33760
|
+
...Number.isInteger(row2.output_line) ? { outputLine: row2.output_line } : {},
|
|
33693
33761
|
...rowCheckedAt === "" ? {} : { checkedAt: rowCheckedAt }
|
|
33694
33762
|
});
|
|
33695
33763
|
}
|
|
@@ -33757,7 +33825,8 @@ function baseFailureLine(result2) {
|
|
|
33757
33825
|
const at = result2.baseSha === "" ? "" : ` (${result2.baseSha})`;
|
|
33758
33826
|
const why = result2.tail === "" ? "" : ` — ${result2.tail}`;
|
|
33759
33827
|
const ran = result2.exitCode === undefined ? "was refused and never ran" : `exited ${String(result2.exitCode)}`;
|
|
33760
|
-
|
|
33828
|
+
const cite2 = result2.outputPath === undefined ? "" : ` [src: ${result2.outputPath}:${String(result2.outputLine ?? 1)}]`;
|
|
33829
|
+
return ` · \`${result2.command}\` ${ran}` + `${result2.timedOut ? " (timed out)" : ""} in repo ${result2.repo}` + ` on \`${result2.baseRef}\`${at}${why}${cite2}`;
|
|
33761
33830
|
}
|
|
33762
33831
|
function baseRefusalLines(failures, workspace) {
|
|
33763
33832
|
const failed3 = [];
|
|
@@ -33888,6 +33957,9 @@ var FAILURE_RE = /FAIL|Failed|failed|\bfail\b|Error|error:|assert|✗|✖|not ok
|
|
|
33888
33957
|
function dodOutputRel(storyId, index) {
|
|
33889
33958
|
return `${BUILD_PHASE}/${LOG_DIR}/${DOD_OUTPUT_DIR}/${storyId}-${String(index + 1)}.txt`;
|
|
33890
33959
|
}
|
|
33960
|
+
function baseOutputId(repo, command2) {
|
|
33961
|
+
return `base-${hashText(JSON.stringify([repo, command2]))}`;
|
|
33962
|
+
}
|
|
33891
33963
|
function meaningfulLines(output) {
|
|
33892
33964
|
return output.split(`
|
|
33893
33965
|
`).map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
|
|
@@ -33994,6 +34066,8 @@ async function baseResultOf(parts, repo, command2) {
|
|
|
33994
34066
|
try {
|
|
33995
34067
|
const outcome = await runDodCommand(command2, repoDir, timeoutMs, parts.workspace.commands);
|
|
33996
34068
|
const exitCode = outcome.timedOut ? 124 : outcome.exitCode;
|
|
34069
|
+
const output = outcome.output ?? "";
|
|
34070
|
+
const kept = exitCode === 0 && !outcome.timedOut ? null : writeDodOutput(parts.runDir, baseOutputId(repo, command2), 0, output);
|
|
33997
34071
|
measured = {
|
|
33998
34072
|
repo,
|
|
33999
34073
|
command: command2,
|
|
@@ -34001,7 +34075,13 @@ async function baseResultOf(parts, repo, command2) {
|
|
|
34001
34075
|
baseSha,
|
|
34002
34076
|
exitCode,
|
|
34003
34077
|
timedOut: outcome.timedOut,
|
|
34004
|
-
tail: outcome.tail,
|
|
34078
|
+
tail: kept === null ? outcome.tail : failureSummaryLine(output),
|
|
34079
|
+
...kept === null ? {} : {
|
|
34080
|
+
excerpt: failureExcerpt(output),
|
|
34081
|
+
outputPath: kept.rel,
|
|
34082
|
+
outputBytes: kept.bytes,
|
|
34083
|
+
outputLine: kept.line
|
|
34084
|
+
},
|
|
34005
34085
|
status: exitCode === 0 && !outcome.timedOut ? "ok" : "failed",
|
|
34006
34086
|
commandHash: hash
|
|
34007
34087
|
};
|
|
@@ -35047,6 +35127,7 @@ function loadRunResult(root2, id) {
|
|
|
35047
35127
|
budget = null;
|
|
35048
35128
|
}
|
|
35049
35129
|
}
|
|
35130
|
+
const withLiveCeiling = budget?.ceiling_usd === null || budget?.ceiling_usd === undefined ? run : { ...run, ceiling_usd: budget.ceiling_usd };
|
|
35050
35131
|
const { events, error, skipped, mtime } = readEvents2(dir);
|
|
35051
35132
|
return {
|
|
35052
35133
|
kind: "ok",
|
|
@@ -35054,7 +35135,7 @@ function loadRunResult(root2, id) {
|
|
|
35054
35135
|
root: root2,
|
|
35055
35136
|
dir,
|
|
35056
35137
|
id,
|
|
35057
|
-
run,
|
|
35138
|
+
run: withLiveCeiling,
|
|
35058
35139
|
budget,
|
|
35059
35140
|
events,
|
|
35060
35141
|
eventsError: error,
|
|
@@ -37129,6 +37210,7 @@ class BuildSession {
|
|
|
37129
37210
|
costUsd: cost ?? 0,
|
|
37130
37211
|
sessionId: result2.session_id,
|
|
37131
37212
|
error: null,
|
|
37213
|
+
role: "developer",
|
|
37132
37214
|
outputs: result2.outputs,
|
|
37133
37215
|
...cost === null ? { metered: false } : {},
|
|
37134
37216
|
...this.ctx.tokens === null ? {} : { tokens: this.ctx.tokens }
|
|
@@ -37687,6 +37769,7 @@ class BuildSession {
|
|
|
37687
37769
|
costUsd: round25(agent.costUsd),
|
|
37688
37770
|
sessionId: agent.sessionId,
|
|
37689
37771
|
error: agent.error,
|
|
37772
|
+
role: "developer",
|
|
37690
37773
|
outputs: agent.envelope?.outputs ?? [],
|
|
37691
37774
|
metered: agent.metered,
|
|
37692
37775
|
inputTokens: agent.usage.input_tokens,
|
|
@@ -37814,6 +37897,7 @@ class BuildSession {
|
|
|
37814
37897
|
costUsd: task2.costUsd,
|
|
37815
37898
|
sessionId: task2.sessionId,
|
|
37816
37899
|
error: null,
|
|
37900
|
+
role: "reviewer",
|
|
37817
37901
|
outputs: [],
|
|
37818
37902
|
...task2.metered ? {} : { metered: false },
|
|
37819
37903
|
...task2.tokens === undefined ? {} : { tokens: task2.tokens },
|
|
@@ -37887,6 +37971,7 @@ class BuildSession {
|
|
|
37887
37971
|
costUsd: task2.costUsd,
|
|
37888
37972
|
sessionId: task2.sessionId,
|
|
37889
37973
|
error: task2.error ?? null,
|
|
37974
|
+
role: "reviewer",
|
|
37890
37975
|
outputs: [],
|
|
37891
37976
|
...task2.metered ? {} : { metered: false },
|
|
37892
37977
|
...task2.tokens === undefined ? {} : { tokens: task2.tokens },
|
|
@@ -38193,6 +38278,7 @@ class BuildSession {
|
|
|
38193
38278
|
at: this.ctx.at,
|
|
38194
38279
|
preparing: this.ctx.mode === "prepare",
|
|
38195
38280
|
timeoutMs: this.ctx.spec.planned.timeout_s * 1000,
|
|
38281
|
+
runDir: this.ctx.runDir,
|
|
38196
38282
|
write: (work) => this.writes.run(work),
|
|
38197
38283
|
advisories: this.advisories
|
|
38198
38284
|
};
|
|
@@ -39438,6 +39524,7 @@ function recordExecutorTasks(store, options, phaseId, stageId, spec, outcome) {
|
|
|
39438
39524
|
id,
|
|
39439
39525
|
status: task2.error === null ? "done" : "failed",
|
|
39440
39526
|
expert: spec.planned.experts[0] ?? null,
|
|
39527
|
+
...task2.role === undefined ? {} : { role: task2.role },
|
|
39441
39528
|
model: task2.model,
|
|
39442
39529
|
cost_usd: metered ? round26(task2.costUsd) : null,
|
|
39443
39530
|
...metered ? {} : { metered: false },
|
|
@@ -39615,7 +39702,7 @@ async function finishStage(store, options, phaseId, stageId, spec, notes, gateOv
|
|
|
39615
39702
|
...s,
|
|
39616
39703
|
status: "awaiting_gate",
|
|
39617
39704
|
ended_at: nowish(options),
|
|
39618
|
-
gate: { ...s.gate, type: "approve", status: "pending" }
|
|
39705
|
+
gate: { ...s.gate, type: "approve", status: "pending", and_continue: undefined }
|
|
39619
39706
|
}));
|
|
39620
39707
|
const autoVerdict = policy === "auto" ? await evaluateAutoGate({
|
|
39621
39708
|
root: options.root,
|
|
@@ -40326,7 +40413,8 @@ async function runAuto(options) {
|
|
|
40326
40413
|
} catch {
|
|
40327
40414
|
return;
|
|
40328
40415
|
}
|
|
40329
|
-
|
|
40416
|
+
const gate2 = pendingGate(runDir2);
|
|
40417
|
+
await notifier.send(statusNotification(notifyCtx(), text3, stillBlocking(runDir2), gate2, cutInputs(stageIdOf()), gate2 === null ? null : gateStories(runDir2, gate2.phase)), stageIdOf());
|
|
40330
40418
|
})();
|
|
40331
40419
|
}, options.notifyEveryMs);
|
|
40332
40420
|
const finish = async (code, spentUsd) => {
|
|
@@ -40384,11 +40472,11 @@ async function runAuto(options) {
|
|
|
40384
40472
|
if (requested !== null && !autoApproved) {
|
|
40385
40473
|
const cost = requested;
|
|
40386
40474
|
const policy = gatePolicyNow(runDir2);
|
|
40387
|
-
const stories =
|
|
40475
|
+
const stories = gateStories(runDir2, requestedPhase);
|
|
40388
40476
|
const send = async () => {
|
|
40389
40477
|
if (notifier === null)
|
|
40390
40478
|
return;
|
|
40391
|
-
await notifier.send(gateNotification(notifyCtx(), cost, policy, gateHeld(fresh), stories), stageIdOf());
|
|
40479
|
+
await notifier.send(gateNotification(notifyCtx(), cost, policy, gateHeld(fresh), stories, stillBlocking(runDir2)), stageIdOf());
|
|
40392
40480
|
};
|
|
40393
40481
|
if (onlyHeldByQuestions(fresh))
|
|
40394
40482
|
return { costUsd: cost, deferredGate: send };
|
|
@@ -40500,6 +40588,10 @@ async function runAuto(options) {
|
|
|
40500
40588
|
continue;
|
|
40501
40589
|
}
|
|
40502
40590
|
if (waited.resolution === "rejected") {
|
|
40591
|
+
if (waited.andContinue) {
|
|
40592
|
+
say(`waited ${String(Math.round(waited.ms / 1000))}s at ${cursorBefore} — ` + `the gate on ${gate2.stage} was REJECTED with --and-continue` + (waited.note === null ? "" : `: ${waited.note}`) + " — re-running the stage with the note, resuming");
|
|
40593
|
+
continue;
|
|
40594
|
+
}
|
|
40503
40595
|
rejection = `waited ${String(Math.round(waited.ms / 1000))}s at ${cursorBefore} — ` + `the gate on ${gate2.stage} was REJECTED` + (waited.note === null ? "" : `: ${waited.note}`);
|
|
40504
40596
|
} else {
|
|
40505
40597
|
await flushGate();
|
|
@@ -40561,6 +40653,7 @@ function pendingGate(runDir2) {
|
|
|
40561
40653
|
return {
|
|
40562
40654
|
stage: `${cursor.phase}/${cursor.stage}`,
|
|
40563
40655
|
stageId: cursor.stage,
|
|
40656
|
+
phase: cursor.phase,
|
|
40564
40657
|
policy: gatePolicyFor(store.run.gates_policy, cursor.stage)
|
|
40565
40658
|
};
|
|
40566
40659
|
} catch {
|
|
@@ -40580,16 +40673,22 @@ async function waitForGate(runDir2, stageId, limitMs, gate2) {
|
|
|
40580
40673
|
for (;; ) {
|
|
40581
40674
|
const elapsed = Date.now() - started;
|
|
40582
40675
|
const found = gateOf(runDir2, stageId);
|
|
40583
|
-
if (found !== null && found.status === "approved")
|
|
40584
|
-
return { resolution: "approved", ms: elapsed, note: null };
|
|
40676
|
+
if (found !== null && found.status === "approved") {
|
|
40677
|
+
return { resolution: "approved", ms: elapsed, note: null, andContinue: false };
|
|
40678
|
+
}
|
|
40585
40679
|
if (found !== null && found.status === "rejected") {
|
|
40586
|
-
return {
|
|
40680
|
+
return {
|
|
40681
|
+
resolution: "rejected",
|
|
40682
|
+
ms: elapsed,
|
|
40683
|
+
note: found.note.trim() === "" ? null : found.note.trim(),
|
|
40684
|
+
andContinue: found.andContinue
|
|
40685
|
+
};
|
|
40587
40686
|
}
|
|
40588
40687
|
if (found !== null && await selfCloseAutoGate(runDir2, stageId, gate2)) {
|
|
40589
|
-
return { resolution: "approved", ms: Date.now() - started, note: null };
|
|
40688
|
+
return { resolution: "approved", ms: Date.now() - started, note: null, andContinue: false };
|
|
40590
40689
|
}
|
|
40591
40690
|
if (elapsed >= limitMs)
|
|
40592
|
-
return { resolution: "lapsed", ms: elapsed, note: null };
|
|
40691
|
+
return { resolution: "lapsed", ms: elapsed, note: null, andContinue: false };
|
|
40593
40692
|
const pollMs = pollInterval(limitMs);
|
|
40594
40693
|
await new Promise((resolve11) => setTimeout(resolve11, Math.min(pollMs, limitMs - elapsed)));
|
|
40595
40694
|
}
|
|
@@ -40622,7 +40721,11 @@ async function selfCloseAutoGate(runDir2, stageId, gate2) {
|
|
|
40622
40721
|
function gateOf(runDir2, stageId) {
|
|
40623
40722
|
try {
|
|
40624
40723
|
const found = flatten(RunStore.open(runDir2).run).find((entry) => entry.stage.id === stageId);
|
|
40625
|
-
return found === undefined ? null : {
|
|
40724
|
+
return found === undefined ? null : {
|
|
40725
|
+
status: found.stage.gate.status,
|
|
40726
|
+
note: found.stage.gate.note,
|
|
40727
|
+
andContinue: found.stage.gate.and_continue === true
|
|
40728
|
+
};
|
|
40626
40729
|
} catch {
|
|
40627
40730
|
return null;
|
|
40628
40731
|
}
|
|
@@ -44938,7 +45041,6 @@ function budgetRaise(argv) {
|
|
|
44938
45041
|
return EXIT_GATE_REFUSED;
|
|
44939
45042
|
}
|
|
44940
45043
|
store.mutateBudget(() => outcome.budget);
|
|
44941
|
-
store.mutate((run) => ({ ...run, budget: { ...run.budget, ceiling_usd: outcome.runCeilingAfter } }));
|
|
44942
45044
|
store.append({
|
|
44943
45045
|
ts: nowRfc3339(),
|
|
44944
45046
|
run: store.runId,
|
|
@@ -45134,11 +45236,12 @@ function costReport(argv) {
|
|
|
45134
45236
|
var rejectCommand2 = {
|
|
45135
45237
|
name: "reject",
|
|
45136
45238
|
summary: "Request changes at the current gate, or revoke an approval already given",
|
|
45137
|
-
usage: "tldrx reject --note <text> [--stage <phase>/<stage>] [--run <id>] [--root <path>]",
|
|
45239
|
+
usage: "tldrx reject --note <text> [--and-continue] [--stage <phase>/<stage>] [--run <id>] [--root <path>]",
|
|
45138
45240
|
implemented: true,
|
|
45139
45241
|
async run(argv) {
|
|
45140
45242
|
try {
|
|
45141
45243
|
const args = parseArgs(argv, ["run", "note", "root", "stage"]);
|
|
45244
|
+
const andContinue = boolFlag(args, "and-continue");
|
|
45142
45245
|
const note = stringFlag(args, "note") ?? args.positionals.join(" ");
|
|
45143
45246
|
if (note.trim() === "") {
|
|
45144
45247
|
throw new UsageError('reject needs --note: `tldrx reject --note "what to change"`');
|
|
@@ -45146,11 +45249,14 @@ var rejectCommand2 = {
|
|
|
45146
45249
|
const root2 = workspaceRootFrom(args);
|
|
45147
45250
|
const wanted = stringFlag(args, "run");
|
|
45148
45251
|
const target = stringFlag(args, "stage");
|
|
45252
|
+
if (andContinue && target !== undefined && target !== "") {
|
|
45253
|
+
throw new UsageError("--and-continue is about a rejection at the CURRENT gate; --stage revokes an approval " + "already given, which leaves that gate pending for a decision nobody has made yet");
|
|
45254
|
+
}
|
|
45149
45255
|
const resolved = target === undefined || target === "" ? resolveRunOrExplain("tldrx reject", root2, wanted) : resolveIncludingFinished(root2, wanted);
|
|
45150
45256
|
if (!isResolved(resolved))
|
|
45151
45257
|
return resolved.exit;
|
|
45152
45258
|
const store = resolved.store;
|
|
45153
|
-
const ctx = { root: root2, actor: currentActor(), at: nowRfc3339(), note };
|
|
45259
|
+
const ctx = { root: root2, actor: currentActor(), at: nowRfc3339(), note, andContinue };
|
|
45154
45260
|
if (target !== undefined && target !== "") {
|
|
45155
45261
|
const outcome2 = revoke(store, ctx, target);
|
|
45156
45262
|
const signed = outcome2.signedBy === "auto" ? "it had been auto-approved by the facilitator" : `it had been approved by ${outcome2.signedBy}`;
|
|
@@ -45171,9 +45277,11 @@ var rejectCommand2 = {
|
|
|
45171
45277
|
}
|
|
45172
45278
|
const outcome = reject(store, ctx);
|
|
45173
45279
|
const came = outcome.from === "failed" ? " (it had failed)" : "";
|
|
45280
|
+
const loop = outcome.andContinue ? "an unattended `tldrx run auto --wait-gates` re-runs the stage instead of stopping (--and-continue)" : "an unattended `tldrx run auto --wait-gates` STOPS here — pass --and-continue to have it carry on instead";
|
|
45174
45281
|
process.stdout.write(`rejected ${outcome.phase}/${outcome.stage}${came} — back to \`ready\`
|
|
45175
45282
|
` + `note: ${outcome.note}
|
|
45176
45283
|
the note goes into the next prompt — \`tldrx next\` to re-run the stage
|
|
45284
|
+
` + `${loop}
|
|
45177
45285
|
`);
|
|
45178
45286
|
return EXIT_OK;
|
|
45179
45287
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tldr-experts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "tldr-experts: an evidence-first, file-based AI development framework - five stages, a gate on every one, and every claim cited or refused. Installs the `tldrx` (and `tldr-experts`) command. Beta.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alan Martinez",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$doc": "Shape verified from https://code.claude.com/docs/en/plugins.md (Quickstart > Create the plugin manifest). Fields used here: name, description, version, author.name. Only plugin.json goes inside .claude-plugin/; skills/, agents/ and hooks/ live at the plugin root.",
|
|
3
3
|
"name": "tldrx",
|
|
4
4
|
"description": "tldr-experts: an evidence-first, file-based AI development framework. Five stages, a gate on every one, every claim cited or refused. Beta.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.17.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alan Martinez"
|
|
8
8
|
}
|