tldr-experts 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +162 -0
- package/README.md +2 -0
- package/dist/hooks/{chunk-zccma7kt.js → chunk-gv3sj8gv.js} +16 -3
- package/dist/hooks/session-start.js +1 -1
- package/dist/hooks/statusline.js +1 -1
- package/dist/tldrx.js +196 -38
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,167 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.1 — 2026-09-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`tldrx reject --and-continue` — a rejection that means "redo it this way and carry on"
|
|
8
|
+
(#242).** `run auto --wait-gates` resumed after an approve and STOPPED after a reject, so on a
|
|
9
|
+
phone the button meaning *there is still work to do* was the one that ended the run: the only
|
|
10
|
+
way to act on a rejection was to walk to a terminal and relaunch. The stop was deliberate and
|
|
11
|
+
its reasoning is real — resuming re-spends the stage on a decision the person who rejected it
|
|
12
|
+
has not been shown the result of — but it is the reasoning for ONE kind of rejection, "stop, I
|
|
13
|
+
will look", and the issue measured five consecutive live rejections that all meant the other
|
|
14
|
+
one: *"faltan 4 stories sin arrancar; continuar el build"*, *"Rehacer S2 y las waves 3 y 4"*,
|
|
15
|
+
*"Una ronda mas"*. Five rejections, five manual relaunches, the assumption holding zero times
|
|
16
|
+
out of five. So the rejection now SAYS which act it is instead of the loop guessing from the
|
|
17
|
+
note's words or from what was holding the gate: `--and-continue` records `and_continue: true`
|
|
18
|
+
on the gate record `tldrx reject` writes — the same object `--wait-gates` already reads the
|
|
19
|
+
gate's `status` off, in the same read, so there is no second derivation and no second process
|
|
20
|
+
to ask — and the loop re-runs the stage with the note, exactly as the manual relaunch did. A
|
|
21
|
+
bare `tldrx reject` is byte-identical to what it wrote before and stops the loop exactly as
|
|
22
|
+
before; `and_continue` is additive and only ever `true`, so a gate written before this key
|
|
23
|
+
existed reads as "stop". It is cleared when the stage parks on its gate again, so it never
|
|
24
|
+
outlives the rejection it describes. The rationale in `runAuto.ts` now names both kinds and
|
|
25
|
+
says which one is the default. Which BUTTON a notification offers for which effect is
|
|
26
|
+
deliberately not part of this change.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **The `merge-wave` "known flake" was a real race, and it was in the guard's own INSTALL
|
|
31
|
+
(#115).** For months `test/merge-wave.test.ts`'s concurrency cases reddened CI, went green on
|
|
32
|
+
a same-sha re-run, and were waved through under §4's re-run licence — three separate cases in
|
|
33
|
+
one night alone. Nobody had read the failure detail. `gh run view 34671878974 --log-failed`
|
|
34
|
+
(sha `8cd4df2`) says it in two lines: `fatal: cannot exec '.git/hooks/reference-transaction':
|
|
35
|
+
Text file busy` → `update aborted by the reference-transaction hook`. `merge-wave.sh` installs
|
|
36
|
+
the ref guard BEFORE it queues for the lock — deliberately, so an unguarded window never
|
|
37
|
+
exists — so two invocations overlap on exactly one file, and `install_hook` wrote it with
|
|
38
|
+
`cat > "$hook"`: truncate-and-rewrite THE SAME INODE, while the invocation holding the lock
|
|
39
|
+
has a `git merge` exec'ing it. On Linux exec of a file open for write is ETXTBSY; on macOS the
|
|
40
|
+
identical race is benign, which is the whole of why it was green locally and red on CI, and
|
|
41
|
+
why a same-sha re-run failed 2-for-2 rather than passing. The hook is now written to
|
|
42
|
+
`reference-transaction.tmp.$$`, made executable there, and `mv -f`'d into place — the exec'd
|
|
43
|
+
inode is never the written inode, and no reader can catch a zero-length window. It is the
|
|
44
|
+
answer `$MARKER` in `merge-wave.sh` already used, and the trap AGENTS.md §12 already named.
|
|
45
|
+
The race is untestable on macOS by design, so the WRITE is what is pinned: a reinstall must
|
|
46
|
+
land on a new inode and must leave the inode a hard link is holding byte-identical — red on
|
|
47
|
+
both counts before the fix. And the refusal that hid all this now tells the truth: a merge the
|
|
48
|
+
ref-transaction hook aborted, with no conflicting path anywhere, says so and exits **11**, its
|
|
49
|
+
own code, instead of borrowing `2` and telling the agent to go rebase against a conflict that
|
|
50
|
+
never existed. A mislabelled refusal is how a deterministic defect becomes folklore; §4's
|
|
51
|
+
re-run licence for this test is withdrawn in the same change.
|
|
52
|
+
- **The views fixture no longer decays: `test/experts.test.ts` was a wall-clock time bomb
|
|
53
|
+
(#240).** `main` went red at `e1d284d` — the exact sha of published 0.16.0, with no commit in
|
|
54
|
+
between — because `competencyLevel` weighs every evidence row by its AGE and the fixture dated
|
|
55
|
+
its rows ABSOLUTELY (`at: 2026-08-20`, …). An in-process test hands the reader `VIEWS_NOW` and
|
|
56
|
+
is hermetic; a test that spawns the CLI cannot, because the CLI reads `new Date()`. So
|
|
57
|
+
`dotnet-stack/ef-core` sat 3% above the level-3 threshold on the day the assertion was written
|
|
58
|
+
and fell through it eleven days later, by the calendar alone. Bumping the expected number would
|
|
59
|
+
only have re-armed the bomb for a later date, so the FIXTURE moved instead:
|
|
60
|
+
`makeViewsWorkspace({ now })` re-dates the copied evidence so every row keeps the age the
|
|
61
|
+
fixture meant, relative to the clock the assertion is evaluated against, and `NOW` in
|
|
62
|
+
`experts.test.ts` is the real clock rather than a pinned calendar day. Measured with the clock
|
|
63
|
+
moved a year forward: reverted, five tests red (the one that reds today plus four with longer
|
|
64
|
+
fuses); fixed, the whole suite is green at +1 year and the fixture's own consumers are green at
|
|
65
|
+
+5. §8's hermeticity rule now covers the clock as well as `$TMPDIR`.
|
|
66
|
+
- **A parked gate hands over the command that CLEARS it, not `tldrx approve` whatever is
|
|
67
|
+
holding it (#239).** Measured on an owner's phone, 2026-09-10: a gate held BY five
|
|
68
|
+
unanswered questions was announced as `Run: tldrx approve --run <id>`, and the
|
|
69
|
+
`--notify-every` heartbeat repeated that same line seven times in an hour. The sentence
|
|
70
|
+
named the holding condition correctly — #203 was working — and the ACTION contradicted it,
|
|
71
|
+
which is the worse half: a notification exists to be obeyed off a lock screen. Two Build
|
|
72
|
+
gates were approved by mistake that evening, both over unbuilt stories, both revoked with
|
|
73
|
+
`reject --stage`; the owner said so himself — *"no sé por qué me avisa que ya puedo cerrarlo
|
|
74
|
+
si aún hay preguntas"*. `command` now follows the holding condition, in ONE mapping shared by
|
|
75
|
+
`gate.requested` and the parked heartbeat — and over ONE reading of the pending gate,
|
|
76
|
+
`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
|
|
77
|
+
them); unfinished stories → `tldrx run status` (not `reject` — nobody has decided to abandon
|
|
78
|
+
that work, and a one-tap refusal is the mirror of the mistake being fixed — and not `null`,
|
|
79
|
+
because what is missing there is knowledge, not a signature); nothing mechanical outstanding
|
|
80
|
+
→ `tldrx approve`, which is what the field always meant. `approve_command` and
|
|
81
|
+
`reject_command` stay in the `detail` of every payload, so an adapter that renders buttons
|
|
82
|
+
keeps both. The open question ids are read off disk by the loop through `blockingQuestionIds`
|
|
83
|
+
— the one predicate `--wait-answers` polls — at the moment the notification is SENT, so a
|
|
84
|
+
gate whose questions cleared while the send was deferred does not point at an answered
|
|
85
|
+
question; the heartbeat reads the gate's stories the same way, every tick, instead of the
|
|
86
|
+
hard-wired "nothing unfinished" that kept it saying `approve` at a Build gate held by unbuilt
|
|
87
|
+
work — the gate the two mistaken approvals were on.
|
|
88
|
+
- **The Build gate's summary has a verb again (#239).** `deliveredPhrase` is a noun phrase and
|
|
89
|
+
three callers embed it after a label, so `It 5 of 6 stories delivered, S6 blocked (…)` was
|
|
90
|
+
reaching lock screens as a typo. The article is fixed at the one call site that needed a
|
|
91
|
+
sentence — `It has 5 of 6 stories delivered` — and the phrase's contract is unchanged for
|
|
92
|
+
`run next`, `ship` and the decision card.
|
|
93
|
+
|
|
94
|
+
- **A red base pre-flight now keeps what the command SAID, so a stage-wide refusal names a
|
|
95
|
+
cause (#229).** #211 taught a red story DoD to keep its output — the file on disk, an
|
|
96
|
+
excerpt, the failing line, a failure-shaped `tail` — and the base row, produced a hundred
|
|
97
|
+
lines away in the same file, was left on `outcome.tail`: the last line of stdout+stderr.
|
|
98
|
+
The blast radius and the evidence were the wrong way round. A red story DoD blocks ONE
|
|
99
|
+
story; a red base refuses the WHOLE stage before anything is dispatched or charged.
|
|
100
|
+
Measured in the field: a `dotnet test` whose 163,702 captured lines named a dead container
|
|
101
|
+
daemon on line 12 refused all six stories of a stage with `Test run completed with
|
|
102
|
+
non-success exit code: 2` — the sentence every failing run of that runner prints, whatever
|
|
103
|
+
broke. Nothing in the run directory contained the word `Docker`, so diagnosing the refusal
|
|
104
|
+
meant re-running by hand the command the pre-flight exists to have already run. The base
|
|
105
|
+
row now goes through the SAME seam, not a second reading of it: `tail` is the
|
|
106
|
+
failure-looking line, `excerpt` the few lines around it, and the whole bounded tail is
|
|
107
|
+
written to `04-build/log/dod-output/base-<hash>-1.txt`, which the refusal cites by file and
|
|
108
|
+
line. A GREEN base still writes nothing — #211's argument holds harder here, since a green
|
|
109
|
+
base is re-used from cache far more often than a story's — and an `unmeasured` row is
|
|
110
|
+
untouched: nothing ran, it refuses nothing, and its `tail` is already a reason sentence.
|
|
111
|
+
`04-build/preflight.yml` stays `version: 1`: four optional fields, and every older file
|
|
112
|
+
still reads.
|
|
113
|
+
|
|
114
|
+
## 0.16.0 — 2026-09-11
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
|
|
118
|
+
- **`tldrx run auto --retry-failed <n>` — a bounded retry on a failed stage (#233).** Measured
|
|
119
|
+
on a real unattended run at 0.15.0: the loop drove itself through what → how → plan and
|
|
120
|
+
signed all three `auto` gates by itself, and still needed a person four times. Three of the
|
|
121
|
+
four were content decisions or money, which a loop must not make. The fourth was a plan that
|
|
122
|
+
failed its `plan` check by five characters over a cap — a person relaunched `run auto`, the
|
|
123
|
+
next attempt fixed the two files and passed, and nothing else happened. So the loop stopped
|
|
124
|
+
on the one failure it could have cleared, and it stopped because `run auto` returned on every
|
|
125
|
+
non-zero exit except `4`. Now `--retry-failed <n>` lets it run that stage again, at most `n`
|
|
126
|
+
times in a row. It bounds exit `5` and nothing else: a usage error (`1`), a money refusal
|
|
127
|
+
(`2`) and an awaiting-human park (`4`) are each attempted ONCE however large `n` is, because
|
|
128
|
+
a phase ceiling means *a human decides about money* and a retry would turn that sentence into
|
|
129
|
+
a delay. Only CONSECUTIVE failures count — any other outcome puts the count back to zero,
|
|
130
|
+
since what is bounded is "this run is stuck", not "this run has ever failed". A retry SPENDS:
|
|
131
|
+
it is a fresh metered stage under the same phase ceiling and the same `--max-usd`, which is
|
|
132
|
+
what stops it running up a bill. `0` is the default and a default invocation's lines are
|
|
133
|
+
byte-identical to what they were; anything outside `0..3` is refused by name with exit `1`,
|
|
134
|
+
from the one constant the loop and the flag parser share. When the bound is spent the loop
|
|
135
|
+
stops on the failure's own exit `5` and SAYS the count last — `3 consecutive stage failures
|
|
136
|
+
at 03-plan/plan …` — so the sentence that reaches a phone is what the loop tried, not a bare
|
|
137
|
+
`5`.
|
|
138
|
+
|
|
139
|
+
### Fixed
|
|
140
|
+
|
|
141
|
+
- **An `auto` gate that REFUSES now writes the verdict down, so `run status` says which of the
|
|
142
|
+
seven conditions is holding it (#230).** The note has always been the designed answer to
|
|
143
|
+
"which of the seven stopped it" — and it was written only by a gate that CLOSED, so the one
|
|
144
|
+
record built for that question was `note: ""` in exactly the case it exists for. Measured
|
|
145
|
+
2026-09-10 on an unattended `run auto`: a gate sat pending ~40 minutes, `run status` and
|
|
146
|
+
`run status --verbose` named no condition, and the reason (`claim-sources`, one unresolvable
|
|
147
|
+
source) surfaced only when a person guessed at the `tldrx approve` the status line suggested
|
|
148
|
+
— the one route nobody unattended is going to take. Each re-measure that refuses now records
|
|
149
|
+
`auto-gate refused — held by: <ids> · <all seven with their values>` on the gate that is
|
|
150
|
+
still `pending`, and `run status` names the ids on the gate row and on the `waiting` line
|
|
151
|
+
(`--verbose` still quotes the whole note). All seven values, not just the failures: a note
|
|
152
|
+
that dropped `budget=$0.30 of $6.00` would answer "was it the money" with the same silence.
|
|
153
|
+
It writes only a `pending` gate — a gate a person has since signed keeps THEIR words — and
|
|
154
|
+
only when the note would change, so a four-hour `--wait-gates` poll writes `run.yml` once per
|
|
155
|
+
distinct verdict rather than thousands of times. That `pending` test and the write are a
|
|
156
|
+
**compare-and-set under the workspace lock**, over a `run.yml` read inside it: a check-then-act
|
|
157
|
+
over a snapshot taken a moment earlier erased a concurrent `approve` outright — `save()` writes
|
|
158
|
+
the whole snapshot and re-reads only `budget.yml`'s ceilings, so the gate fell back to
|
|
159
|
+
`pending`, `by`/`at` to null and the person's words were gone. Caught in pre-merge review and
|
|
160
|
+
reproduced with two real processes, which is now the test: the poll runs every two seconds
|
|
161
|
+
precisely while a person is deciding, so an audit record that destroys the evidence of a human
|
|
162
|
+
decision is not a theoretical interleaving. A gate carrying a refusal note is no longer
|
|
163
|
+
counted among the "signed gates carry a note" rows, because nobody closed it.
|
|
164
|
+
|
|
3
165
|
## 0.15.0 — 2026-09-10
|
|
4
166
|
|
|
5
167
|
### 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.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 |
|
|
320
|
+
| 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 |
|
|
319
321
|
| 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 |
|
|
320
322
|
| 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 |
|
|
321
323
|
| 0.14.2 | 2026-09-09 | `beta` | every citation check starts from a fresh view of `questions.md` and `facts.yml`: `srcToken.ts` memoised both indexes at module scope and nothing outside `test/` ever dropped them, so in `tldrx run auto` — one Node process for a whole run — the FIRST citation resolved anywhere froze the view every later stage was then judged against; measured on three real unattended workspaces at 0.14.1, a `how` stage refused with `no such question Q2 … declared: Q1` over questions it had itself written minutes earlier, and another refused over `145 live fact(s)` when `facts.yml` held 148, the three extra written by the owner's answers two seconds before the stage started — roughly $11 of paid turns thrown away for ids that were real the whole time; the indexes are now refreshed inside `toSrcContext`, the one place a citation context is built and a place every caller reaches exactly once per check, gate or hook, so a document's forty citations still read `facts.yml` once while nothing survives the check that read it |
|
|
@@ -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} `;
|
|
@@ -507,6 +508,16 @@ function statusWithOutcome(status, outcome) {
|
|
|
507
508
|
|
|
508
509
|
// src/core/run/autoGate.ts
|
|
509
510
|
var AUTO_GATE_ACTOR = "auto";
|
|
511
|
+
var AUTO_GATE_REFUSED_PREFIX = "auto-gate refused — held by: ";
|
|
512
|
+
var HELD_SEPARATOR = " · ";
|
|
513
|
+
function heldByNote(note) {
|
|
514
|
+
if (!note.startsWith(AUTO_GATE_REFUSED_PREFIX))
|
|
515
|
+
return [];
|
|
516
|
+
const rest = note.slice(AUTO_GATE_REFUSED_PREFIX.length);
|
|
517
|
+
const end = rest.indexOf(HELD_SEPARATOR);
|
|
518
|
+
const ids = (end === -1 ? rest : rest.slice(0, end)).split(", ").map((id) => id.trim());
|
|
519
|
+
return ids.filter((id) => id !== "");
|
|
520
|
+
}
|
|
510
521
|
|
|
511
522
|
// src/core/run/gateAuthority.ts
|
|
512
523
|
var UNRECORDED_AUTHORIZER = "unrecorded";
|
|
@@ -729,12 +740,14 @@ function waitingFor(run, runDir) {
|
|
|
729
740
|
}
|
|
730
741
|
const open = blockingQuestionIds(join6(runDir, cursor.phase, "questions.md"));
|
|
731
742
|
switch (entry.stage.status) {
|
|
732
|
-
case "awaiting_gate":
|
|
743
|
+
case "awaiting_gate": {
|
|
744
|
+
const held = heldByNote(entry.stage.gate?.note ?? "");
|
|
733
745
|
return {
|
|
734
746
|
kind: "gate",
|
|
735
|
-
message: `gate on ${entry.phase.id}/${entry.stage.id} — \`tldrx approve\` or \`tldrx reject --note "…"\``,
|
|
747
|
+
message: `gate on ${entry.phase.id}/${entry.stage.id}` + (held.length === 0 ? "" : ` — held by ${held.join(", ")}`) + ` — \`tldrx approve\` or \`tldrx reject --note "…"\``,
|
|
736
748
|
questions: open
|
|
737
749
|
};
|
|
750
|
+
}
|
|
738
751
|
case "awaiting_answer":
|
|
739
752
|
return {
|
|
740
753
|
kind: "answer",
|
package/dist/hooks/statusline.js
CHANGED
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} `;
|
|
@@ -15643,10 +15644,16 @@ var ENTRIES = [
|
|
|
15643
15644
|
meaning: "Send the workspace's declared notify hook a `status` payload this often while the loop runs — `30s`, `10m`, `2h`, or a bare number of seconds. Off by default, and it does nothing at all unless `.tldrx/workspace.yml` declares a `notify:` command (§2.18). A `status` payload carries what `tldrx run status` prints. It asks for nothing while the run is moving — and when the run is PARKED on an open question it says so and repeats the literal answer command, because a heartbeat that keeps saying nothing is waiting on you while a run waits on you is worse than silence.",
|
|
15644
15645
|
sub: "auto"
|
|
15645
15646
|
},
|
|
15647
|
+
{
|
|
15648
|
+
name: "retry-failed",
|
|
15649
|
+
arg: "<n>",
|
|
15650
|
+
meaning: 'How many times in a row the loop may run a FAILED stage again before it stops. 0 — the default, and what every invocation before this got — means one attempt and then exit 5. A retry is the same `tldrx next` a person would have typed: the stage is on disk as `failed` with its reason recorded, and the next attempt is told what the last one did. It bounds EXIT 5 AND NOTHING ELSE — a usage error (1), a money refusal (2) and an awaiting-human park (4) are attempted once however large the bound, because each is a decision a person owns; a phase ceiling especially, which means "a human decides about money" and would otherwise become a delay. Only CONSECUTIVE failures count: a stage that succeeds puts the count back to zero. A retry SPENDS — it is a fresh metered stage under the same phase ceiling and the same --max-usd — and when the bound is spent the loop stops on the failure\'s own exit 5, with the count in the last line.',
|
|
15651
|
+
sub: "auto"
|
|
15652
|
+
},
|
|
15646
15653
|
{
|
|
15647
15654
|
name: "wait-gates",
|
|
15648
15655
|
arg: "<duration>",
|
|
15649
|
-
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.",
|
|
15656
|
+
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.",
|
|
15650
15657
|
sub: "auto"
|
|
15651
15658
|
},
|
|
15652
15659
|
{
|
|
@@ -15695,6 +15702,7 @@ var ENTRIES = [
|
|
|
15695
15702
|
"tldrx run auto --parallel 3",
|
|
15696
15703
|
"tldrx run auto --prompt-max-bytes 500000 --max-reads 300",
|
|
15697
15704
|
"tldrx run auto --notify-every 10m",
|
|
15705
|
+
"tldrx run auto --retry-failed 2",
|
|
15698
15706
|
"tldrx run auto --wait-answers 30m",
|
|
15699
15707
|
"tldrx run auto --wait-answers 4h --wait-gates 4h",
|
|
15700
15708
|
"tldrx run unlock 260101-checkout --force",
|
|
@@ -15703,7 +15711,7 @@ var ENTRIES = [
|
|
|
15703
15711
|
exits: [EXIT_OK, EXIT_USAGE, EXIT_GATE_REFUSED, EXIT_NOT_FOUND, EXIT_AWAITING_HUMAN, EXIT_AGENT_FAILED],
|
|
15704
15712
|
notes: [
|
|
15705
15713
|
"`run attend host` is a LOCK, not an engine. It sets one field, spends nothing, runs no stage and touches no branch — and from then on THE FRAMEWORK WILL NOT SPAWN on that run: every turn is a `tldrx next --prepare` / `tldrx next --commit` handshake with the session driving it, the Build reviewer included. `run attend --none` hands it back.",
|
|
15706
|
-
"`run auto` is an ENGINE, not a lock. It calls `next` HEADLESS over and over, so THE FRAMEWORK spawns a metered sub-agent stage after stage, and it stops at the first thing it may not decide: a human gate or an open question (4), a stage failure (5), a phase ceiling or this loop's own --max-usd (2). It is REFUSED ON AN ATTENDED RUN (exit 1, before the event log is opened) — a lock and an engine are alternatives, never layers.",
|
|
15714
|
+
"`run auto` is an ENGINE, not a lock. It calls `next` HEADLESS over and over, so THE FRAMEWORK spawns a metered sub-agent stage after stage, and it stops at the first thing it may not decide: a human gate or an open question (4), a stage failure (5) — unless `--retry-failed <n>` lets it run that stage again, bounded, up to n times in a row — a phase ceiling or this loop's own --max-usd (2). It is REFUSED ON AN ATTENDED RUN (exit 1, before the event log is opened) — a lock and an engine are alternatives, never layers.",
|
|
15707
15715
|
"`run auto` can also TELL SOMEBODY. When `.tldrx/workspace.yml` declares a `notify:` command (§2.18), the loop hands that command one `version: 1` JSON object on stdin at every moment a person is needed — an open question with its options and the literal `tldrx answer` line, a gate with the literal approve line, a finished or failed run with its exit code and family — plus a periodic `status` under `--notify-every`. The framework names no chat tool: the command is the owner's own, run as argv with no shell, and its exit code is recorded as `notify.sent` / `notify.failed` and NEVER changes the run's outcome.",
|
|
15708
15716
|
"Under `run auto`, a stage whose `gates_policy` is `agent` gets one bounded GATE-SIGNER turn of its own. When the stage's checks have passed, the engine spawns a single sub-agent at the stage's model and effort, on a quarter of the stage's per-agent ceiling, allowed to read and to write exactly one file: `.agent/<stage>/evidence.md`. The note then goes through the UNCHANGED `approve --as-agent` path — the same validator a person's note goes through — so `verdict: sign` plus every condition holding closes the gate under the note's own `by:`, and anything else leaves it pending for a person with the reasons named. The turn is recorded like any other (`agent.spawned` / `agent.result`, `role: gate-signer`) and shows up in `tldrx cost`. There is no flag for it: `gates_policy: agent` is already the owner's recorded decision that an agent may close this gate, and `human` gates are never touched.",
|
|
15709
15717
|
"`run status` with several runs open LISTS them and exits 0 — it is the screen you read to find the id every other command wants.",
|
|
@@ -15981,6 +15989,11 @@ var ENTRIES = [
|
|
|
15981
15989
|
args: [],
|
|
15982
15990
|
flags: [
|
|
15983
15991
|
{ name: "note", arg: "<text>", meaning: "What has to change. Required — a rejection with no reason is not actionable." },
|
|
15992
|
+
{
|
|
15993
|
+
name: "and-continue",
|
|
15994
|
+
arg: null,
|
|
15995
|
+
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."
|
|
15996
|
+
},
|
|
15984
15997
|
{
|
|
15985
15998
|
name: "stage",
|
|
15986
15999
|
arg: "<phase>/<stage>",
|
|
@@ -15991,6 +16004,7 @@ var ENTRIES = [
|
|
|
15991
16004
|
],
|
|
15992
16005
|
examples: [
|
|
15993
16006
|
'tldrx reject --note "contracts.md does not name the events"',
|
|
16007
|
+
'tldrx reject --and-continue --note "S2 fell over on a missing binary — redo S2 and waves 3 and 4"',
|
|
15994
16008
|
'tldrx reject --stage 02-how/design --note "the auto gate signed over four open questions"'
|
|
15995
16009
|
],
|
|
15996
16010
|
exits: [EXIT_OK, EXIT_USAGE, EXIT_GATE_REFUSED, EXIT_NOT_FOUND]
|
|
@@ -28956,6 +28970,9 @@ function blockedReasons(runDir) {
|
|
|
28956
28970
|
}
|
|
28957
28971
|
return out;
|
|
28958
28972
|
}
|
|
28973
|
+
function gateStories(runDir, phaseId) {
|
|
28974
|
+
return phaseId === BUILD_PHASE2 ? storiesView(runDir) : null;
|
|
28975
|
+
}
|
|
28959
28976
|
function gateStoriesPayload(view) {
|
|
28960
28977
|
const blocked = view.firstBlocked;
|
|
28961
28978
|
return {
|
|
@@ -29357,6 +29374,22 @@ async function evaluateAutoGate(input) {
|
|
|
29357
29374
|
function heldBy(verdict) {
|
|
29358
29375
|
return verdict.conditions.filter((condition) => !condition.ok).map((condition) => condition.id);
|
|
29359
29376
|
}
|
|
29377
|
+
var AUTO_GATE_REFUSED_PREFIX = "auto-gate refused — held by: ";
|
|
29378
|
+
var HELD_SEPARATOR = " · ";
|
|
29379
|
+
function refusalNote(verdict) {
|
|
29380
|
+
const held2 = heldBy(verdict);
|
|
29381
|
+
if (held2.length === 0)
|
|
29382
|
+
return "";
|
|
29383
|
+
return `${AUTO_GATE_REFUSED_PREFIX}${held2.join(", ")}${HELD_SEPARATOR}` + verdict.conditions.map(render2).join("; ");
|
|
29384
|
+
}
|
|
29385
|
+
function heldByNote(note) {
|
|
29386
|
+
if (!note.startsWith(AUTO_GATE_REFUSED_PREFIX))
|
|
29387
|
+
return [];
|
|
29388
|
+
const rest = note.slice(AUTO_GATE_REFUSED_PREFIX.length);
|
|
29389
|
+
const end = rest.indexOf(HELD_SEPARATOR);
|
|
29390
|
+
const ids = (end === -1 ? rest : rest.slice(0, end)).split(", ").map((id) => id.trim());
|
|
29391
|
+
return ids.filter((id) => id !== "");
|
|
29392
|
+
}
|
|
29360
29393
|
async function reevaluateAutoGate(input) {
|
|
29361
29394
|
const phase = input.run.phases.find((entry) => entry.stages.some((stage3) => stage3.id === input.stageId));
|
|
29362
29395
|
const stage2 = phase?.stages.find((entry) => entry.id === input.stageId);
|
|
@@ -29377,7 +29410,7 @@ async function reevaluateAutoGate(input) {
|
|
|
29377
29410
|
runDir: input.runDir,
|
|
29378
29411
|
stage: planned
|
|
29379
29412
|
});
|
|
29380
|
-
|
|
29413
|
+
const verdict = await evaluateAutoGate({
|
|
29381
29414
|
root: input.root,
|
|
29382
29415
|
runDir: input.runDir,
|
|
29383
29416
|
phaseId: phase.id,
|
|
@@ -29386,6 +29419,30 @@ async function reevaluateAutoGate(input) {
|
|
|
29386
29419
|
budget: input.budget,
|
|
29387
29420
|
checks
|
|
29388
29421
|
});
|
|
29422
|
+
if (!verdict.ok)
|
|
29423
|
+
recordRefusal(input.runDir, input.stageId, verdict);
|
|
29424
|
+
return verdict;
|
|
29425
|
+
}
|
|
29426
|
+
function recordRefusal(runDir, stageId, verdict) {
|
|
29427
|
+
const note = refusalNote(verdict);
|
|
29428
|
+
if (note === "")
|
|
29429
|
+
return;
|
|
29430
|
+
try {
|
|
29431
|
+
withWorkspaceLock(workspaceRootOfRunDir(runDir), () => {
|
|
29432
|
+
const store = RunStore.open(runDir);
|
|
29433
|
+
const current = store.run.phases.flatMap((phase) => phase.stages).find((stage2) => stage2.id === stageId);
|
|
29434
|
+
if (current === undefined || current.gate.status !== "pending" || current.gate.note === note)
|
|
29435
|
+
return;
|
|
29436
|
+
store.mutate((run) => ({
|
|
29437
|
+
...run,
|
|
29438
|
+
phases: run.phases.map((phase) => ({
|
|
29439
|
+
...phase,
|
|
29440
|
+
stages: phase.stages.map((stage2) => stage2.id === stageId && stage2.gate.status === "pending" ? { ...stage2, gate: { ...stage2.gate, note } } : stage2)
|
|
29441
|
+
}))
|
|
29442
|
+
}));
|
|
29443
|
+
store.save();
|
|
29444
|
+
});
|
|
29445
|
+
} catch {}
|
|
29389
29446
|
}
|
|
29390
29447
|
function render2(condition) {
|
|
29391
29448
|
return `${condition.id}=${condition.detail}`;
|
|
@@ -29708,12 +29765,14 @@ function waitingFor(run, runDir) {
|
|
|
29708
29765
|
}
|
|
29709
29766
|
const open = blockingQuestionIds(join66(runDir, cursor.phase, "questions.md"));
|
|
29710
29767
|
switch (entry.stage.status) {
|
|
29711
|
-
case "awaiting_gate":
|
|
29768
|
+
case "awaiting_gate": {
|
|
29769
|
+
const held2 = heldByNote(entry.stage.gate?.note ?? "");
|
|
29712
29770
|
return {
|
|
29713
29771
|
kind: "gate",
|
|
29714
|
-
message: `gate on ${entry.phase.id}/${entry.stage.id} — \`tldrx approve\` or \`tldrx reject --note "…"\``,
|
|
29772
|
+
message: `gate on ${entry.phase.id}/${entry.stage.id}` + (held2.length === 0 ? "" : ` — held by ${held2.join(", ")}`) + ` — \`tldrx approve\` or \`tldrx reject --note "…"\``,
|
|
29715
29773
|
questions: open
|
|
29716
29774
|
};
|
|
29775
|
+
}
|
|
29717
29776
|
case "awaiting_answer":
|
|
29718
29777
|
return {
|
|
29719
29778
|
kind: "answer",
|
|
@@ -30020,7 +30079,7 @@ function renderGates(rows, verbose = false) {
|
|
|
30020
30079
|
const auto = rows.filter((row2) => row2.policy === "auto").length;
|
|
30021
30080
|
const agent = rows.filter((row2) => row2.policy === "agent").length;
|
|
30022
30081
|
const human = rows.length - auto - agent;
|
|
30023
|
-
const noted2 = rows.filter((row2) => row2.note !== null).length;
|
|
30082
|
+
const noted2 = rows.filter((row2) => isSigned(row2) && row2.note !== null).length;
|
|
30024
30083
|
const lines = [
|
|
30025
30084
|
`gates ${String(human)} human, ${String(auto)} auto` + (agent === 0 ? "" : `, ${String(agent)} agent`)
|
|
30026
30085
|
];
|
|
@@ -30043,10 +30102,13 @@ function renderGates(rows, verbose = false) {
|
|
|
30043
30102
|
var NOTE_MARK = "✎";
|
|
30044
30103
|
function gateTail(row2) {
|
|
30045
30104
|
const parts = [briefDuration(row2.started_at, row2.ended_at)];
|
|
30046
|
-
if (row2.note !== null)
|
|
30105
|
+
if (isSigned(row2) && row2.note !== null)
|
|
30047
30106
|
parts.push(NOTE_MARK);
|
|
30048
30107
|
return parts.filter((part) => part !== "").join(" ");
|
|
30049
30108
|
}
|
|
30109
|
+
function isSigned(row2) {
|
|
30110
|
+
return row2.status === "approved" || row2.status === "rejected";
|
|
30111
|
+
}
|
|
30050
30112
|
function briefDuration(startedAt, endedAt) {
|
|
30051
30113
|
const measured = dashDuration(startedAt, endedAt);
|
|
30052
30114
|
if (measured !== "")
|
|
@@ -30076,7 +30138,8 @@ function describeGate(row2) {
|
|
|
30076
30138
|
return `rejected by ${describeGateSignature(row2)}`;
|
|
30077
30139
|
if (row2.status === "n-a")
|
|
30078
30140
|
return `${row2.type}: n-a`;
|
|
30079
|
-
|
|
30141
|
+
const held2 = heldByNote(row2.note ?? "");
|
|
30142
|
+
return `${row2.type}: ${row2.status}` + (held2.length === 0 ? "" : ` — held by ${held2.join(", ")}`);
|
|
30080
30143
|
}
|
|
30081
30144
|
var NOTES_SHOWN = 3;
|
|
30082
30145
|
function renderOperatorNotes(notes) {
|
|
@@ -30611,14 +30674,22 @@ function gatePhrase(policy) {
|
|
|
30611
30674
|
return "gate";
|
|
30612
30675
|
}
|
|
30613
30676
|
}
|
|
30614
|
-
function
|
|
30677
|
+
function clearingCommand(runId, openQuestions2, unfinishedStories) {
|
|
30678
|
+
const first2 = openQuestions2[0];
|
|
30679
|
+
if (first2 !== undefined)
|
|
30680
|
+
return answerCommand(first2, runId);
|
|
30681
|
+
if (unfinishedStories > 0)
|
|
30682
|
+
return `tldrx run status ${runId}`;
|
|
30683
|
+
return approveCommand(runId);
|
|
30684
|
+
}
|
|
30685
|
+
function gateNotification(ctx, costUsd, policy = null, held2 = [], stories = null, openQuestions2 = []) {
|
|
30615
30686
|
const approve = approveCommand(ctx.runId);
|
|
30616
30687
|
const why = held2.length === 0 ? "" : policy === "auto" ? ` It is held by: ${held2.join("; ")}.` : ` The engine's signer held it: ${held2.join("; ")}.`;
|
|
30617
|
-
const delivered = stories === null ? "" : ` It ${deliveredPhrase(stories)}.`;
|
|
30688
|
+
const delivered = stories === null ? "" : ` It has ${deliveredPhrase(stories)}.`;
|
|
30618
30689
|
return {
|
|
30619
30690
|
...base(ctx, "gate.requested"),
|
|
30620
30691
|
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.",
|
|
30621
|
-
command:
|
|
30692
|
+
command: clearingCommand(ctx.runId, openQuestions2, stories?.unfinished.length ?? 0),
|
|
30622
30693
|
detail: {
|
|
30623
30694
|
cost_usd: costUsd,
|
|
30624
30695
|
approve_command: approve,
|
|
@@ -30684,7 +30755,7 @@ function runEndNotification(ctx, exitCode, spentUsd, lastLine2, tally = { usd: s
|
|
|
30684
30755
|
}
|
|
30685
30756
|
};
|
|
30686
30757
|
}
|
|
30687
|
-
function statusNotification(ctx, statusText, waitingOn = [], waitingOnGate = null, truncation = null) {
|
|
30758
|
+
function statusNotification(ctx, statusText, waitingOn = [], waitingOnGate = null, truncation = null, stories = null) {
|
|
30688
30759
|
const ids = [...waitingOn];
|
|
30689
30760
|
const parked = ids.length > 0;
|
|
30690
30761
|
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(", ")}.` : "");
|
|
@@ -30692,7 +30763,7 @@ function statusNotification(ctx, statusText, waitingOn = [], waitingOnGate = nul
|
|
|
30692
30763
|
return {
|
|
30693
30764
|
...base(ctx, "status"),
|
|
30694
30765
|
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,
|
|
30695
|
-
command: waitingOnGate !== null ?
|
|
30766
|
+
command: waitingOnGate !== null || parked ? clearingCommand(ctx.runId, ids, stories?.unfinished.length ?? 0) : `tldrx run status ${ctx.runId}`,
|
|
30696
30767
|
detail: {
|
|
30697
30768
|
status_text: statusText,
|
|
30698
30769
|
waiting_on: ids,
|
|
@@ -31002,15 +31073,23 @@ function reject(store, ctx) {
|
|
|
31002
31073
|
...stage2,
|
|
31003
31074
|
status: "ready",
|
|
31004
31075
|
ended_at: null,
|
|
31005
|
-
gate: {
|
|
31076
|
+
gate: {
|
|
31077
|
+
...stage2.gate,
|
|
31078
|
+
status: "rejected",
|
|
31079
|
+
by: ctx.actor,
|
|
31080
|
+
at: ctx.at,
|
|
31081
|
+
note: ctx.note,
|
|
31082
|
+
and_continue: ctx.andContinue === true ? true : undefined
|
|
31083
|
+
}
|
|
31006
31084
|
})));
|
|
31007
31085
|
store.append(event2(ctx.at, store.runId, entry.stage.id, "gate.rejected", ctx.actor, {
|
|
31008
31086
|
phase: entry.phase.id,
|
|
31009
31087
|
note: ctx.note,
|
|
31010
|
-
from
|
|
31088
|
+
from,
|
|
31089
|
+
...ctx.andContinue === true ? { and_continue: true } : {}
|
|
31011
31090
|
}));
|
|
31012
31091
|
store.save();
|
|
31013
|
-
return { stage: entry.stage.id, phase: entry.phase.id, note: ctx.note, from };
|
|
31092
|
+
return { stage: entry.stage.id, phase: entry.phase.id, note: ctx.note, from, andContinue: ctx.andContinue === true };
|
|
31014
31093
|
}
|
|
31015
31094
|
function revoke(store, ctx, target) {
|
|
31016
31095
|
if (ctx.note.trim() === "") {
|
|
@@ -33583,6 +33662,14 @@ function emitPreflightYaml(preflight) {
|
|
|
33583
33662
|
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)}`);
|
|
33584
33663
|
if (row2.refusedBecause !== undefined)
|
|
33585
33664
|
lines.push(` refused_because: ${yamlScalar(row2.refusedBecause)}`);
|
|
33665
|
+
if (row2.excerpt !== undefined)
|
|
33666
|
+
lines.push(` excerpt: ${yamlScalar(row2.excerpt)}`);
|
|
33667
|
+
if (row2.outputPath !== undefined)
|
|
33668
|
+
lines.push(` output_path: ${yamlScalar(row2.outputPath)}`);
|
|
33669
|
+
if (row2.outputBytes !== undefined)
|
|
33670
|
+
lines.push(` output_bytes: ${String(row2.outputBytes)}`);
|
|
33671
|
+
if (row2.outputLine !== undefined)
|
|
33672
|
+
lines.push(` output_line: ${String(row2.outputLine)}`);
|
|
33586
33673
|
if (row2.commandHash !== undefined)
|
|
33587
33674
|
lines.push(` command_hash: ${yamlScalar(row2.commandHash)}`);
|
|
33588
33675
|
if (row2.checkedAt !== undefined)
|
|
@@ -33625,6 +33712,8 @@ function parsePreflight(text3) {
|
|
|
33625
33712
|
return null;
|
|
33626
33713
|
}
|
|
33627
33714
|
const hash = asText(row2.command_hash);
|
|
33715
|
+
const excerpt = asText(row2.excerpt);
|
|
33716
|
+
const outputPath = asText(row2.output_path);
|
|
33628
33717
|
const rowCheckedAt = asText(row2.checked_at);
|
|
33629
33718
|
results.push({
|
|
33630
33719
|
repo,
|
|
@@ -33637,6 +33726,10 @@ function parsePreflight(text3) {
|
|
|
33637
33726
|
...refusedBecause === "" ? {} : { refusedBecause },
|
|
33638
33727
|
status: row2.status === "ok" || row2.status === "failed" ? row2.status : "unmeasured",
|
|
33639
33728
|
...hash === "" ? {} : { commandHash: hash },
|
|
33729
|
+
...excerpt === "" ? {} : { excerpt },
|
|
33730
|
+
...outputPath === "" ? {} : { outputPath },
|
|
33731
|
+
...Number.isInteger(row2.output_bytes) ? { outputBytes: row2.output_bytes } : {},
|
|
33732
|
+
...Number.isInteger(row2.output_line) ? { outputLine: row2.output_line } : {},
|
|
33640
33733
|
...rowCheckedAt === "" ? {} : { checkedAt: rowCheckedAt }
|
|
33641
33734
|
});
|
|
33642
33735
|
}
|
|
@@ -33704,7 +33797,8 @@ function baseFailureLine(result2) {
|
|
|
33704
33797
|
const at = result2.baseSha === "" ? "" : ` (${result2.baseSha})`;
|
|
33705
33798
|
const why = result2.tail === "" ? "" : ` — ${result2.tail}`;
|
|
33706
33799
|
const ran = result2.exitCode === undefined ? "was refused and never ran" : `exited ${String(result2.exitCode)}`;
|
|
33707
|
-
|
|
33800
|
+
const cite2 = result2.outputPath === undefined ? "" : ` [src: ${result2.outputPath}:${String(result2.outputLine ?? 1)}]`;
|
|
33801
|
+
return ` · \`${result2.command}\` ${ran}` + `${result2.timedOut ? " (timed out)" : ""} in repo ${result2.repo}` + ` on \`${result2.baseRef}\`${at}${why}${cite2}`;
|
|
33708
33802
|
}
|
|
33709
33803
|
function baseRefusalLines(failures, workspace) {
|
|
33710
33804
|
const failed3 = [];
|
|
@@ -33835,6 +33929,9 @@ var FAILURE_RE = /FAIL|Failed|failed|\bfail\b|Error|error:|assert|✗|✖|not ok
|
|
|
33835
33929
|
function dodOutputRel(storyId, index) {
|
|
33836
33930
|
return `${BUILD_PHASE}/${LOG_DIR}/${DOD_OUTPUT_DIR}/${storyId}-${String(index + 1)}.txt`;
|
|
33837
33931
|
}
|
|
33932
|
+
function baseOutputId(repo, command2) {
|
|
33933
|
+
return `base-${hashText(JSON.stringify([repo, command2]))}`;
|
|
33934
|
+
}
|
|
33838
33935
|
function meaningfulLines(output) {
|
|
33839
33936
|
return output.split(`
|
|
33840
33937
|
`).map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
|
|
@@ -33941,6 +34038,8 @@ async function baseResultOf(parts, repo, command2) {
|
|
|
33941
34038
|
try {
|
|
33942
34039
|
const outcome = await runDodCommand(command2, repoDir, timeoutMs, parts.workspace.commands);
|
|
33943
34040
|
const exitCode = outcome.timedOut ? 124 : outcome.exitCode;
|
|
34041
|
+
const output = outcome.output ?? "";
|
|
34042
|
+
const kept = exitCode === 0 && !outcome.timedOut ? null : writeDodOutput(parts.runDir, baseOutputId(repo, command2), 0, output);
|
|
33944
34043
|
measured = {
|
|
33945
34044
|
repo,
|
|
33946
34045
|
command: command2,
|
|
@@ -33948,7 +34047,13 @@ async function baseResultOf(parts, repo, command2) {
|
|
|
33948
34047
|
baseSha,
|
|
33949
34048
|
exitCode,
|
|
33950
34049
|
timedOut: outcome.timedOut,
|
|
33951
|
-
tail: outcome.tail,
|
|
34050
|
+
tail: kept === null ? outcome.tail : failureSummaryLine(output),
|
|
34051
|
+
...kept === null ? {} : {
|
|
34052
|
+
excerpt: failureExcerpt(output),
|
|
34053
|
+
outputPath: kept.rel,
|
|
34054
|
+
outputBytes: kept.bytes,
|
|
34055
|
+
outputLine: kept.line
|
|
34056
|
+
},
|
|
33952
34057
|
status: exitCode === 0 && !outcome.timedOut ? "ok" : "failed",
|
|
33953
34058
|
commandHash: hash
|
|
33954
34059
|
};
|
|
@@ -38140,6 +38245,7 @@ class BuildSession {
|
|
|
38140
38245
|
at: this.ctx.at,
|
|
38141
38246
|
preparing: this.ctx.mode === "prepare",
|
|
38142
38247
|
timeoutMs: this.ctx.spec.planned.timeout_s * 1000,
|
|
38248
|
+
runDir: this.ctx.runDir,
|
|
38143
38249
|
write: (work) => this.writes.run(work),
|
|
38144
38250
|
advisories: this.advisories
|
|
38145
38251
|
};
|
|
@@ -39562,7 +39668,7 @@ async function finishStage(store, options, phaseId, stageId, spec, notes, gateOv
|
|
|
39562
39668
|
...s,
|
|
39563
39669
|
status: "awaiting_gate",
|
|
39564
39670
|
ended_at: nowish(options),
|
|
39565
|
-
gate: { ...s.gate, type: "approve", status: "pending" }
|
|
39671
|
+
gate: { ...s.gate, type: "approve", status: "pending", and_continue: undefined }
|
|
39566
39672
|
}));
|
|
39567
39673
|
const autoVerdict = policy === "auto" ? await evaluateAutoGate({
|
|
39568
39674
|
root: options.root,
|
|
@@ -40210,6 +40316,8 @@ var EXIT_USAGE4 = 1;
|
|
|
40210
40316
|
var EXIT_REFUSED3 = 2;
|
|
40211
40317
|
var EXIT_NOT_FOUND4 = 3;
|
|
40212
40318
|
var EXIT_AWAITING_HUMAN3 = 4;
|
|
40319
|
+
var EXIT_AGENT_FAILED3 = 5;
|
|
40320
|
+
var MAX_RETRY_FAILED = 3;
|
|
40213
40321
|
var MAX_ITERATIONS = 96;
|
|
40214
40322
|
async function runAuto(options) {
|
|
40215
40323
|
const lines = [];
|
|
@@ -40271,7 +40379,8 @@ async function runAuto(options) {
|
|
|
40271
40379
|
} catch {
|
|
40272
40380
|
return;
|
|
40273
40381
|
}
|
|
40274
|
-
|
|
40382
|
+
const gate2 = pendingGate(runDir2);
|
|
40383
|
+
await notifier.send(statusNotification(notifyCtx(), text3, stillBlocking(runDir2), gate2, cutInputs(stageIdOf()), gate2 === null ? null : gateStories(runDir2, gate2.phase)), stageIdOf());
|
|
40275
40384
|
})();
|
|
40276
40385
|
}, options.notifyEveryMs);
|
|
40277
40386
|
const finish = async (code, spentUsd) => {
|
|
@@ -40329,11 +40438,11 @@ async function runAuto(options) {
|
|
|
40329
40438
|
if (requested !== null && !autoApproved) {
|
|
40330
40439
|
const cost = requested;
|
|
40331
40440
|
const policy = gatePolicyNow(runDir2);
|
|
40332
|
-
const stories =
|
|
40441
|
+
const stories = gateStories(runDir2, requestedPhase);
|
|
40333
40442
|
const send = async () => {
|
|
40334
40443
|
if (notifier === null)
|
|
40335
40444
|
return;
|
|
40336
|
-
await notifier.send(gateNotification(notifyCtx(), cost, policy, gateHeld(fresh), stories), stageIdOf());
|
|
40445
|
+
await notifier.send(gateNotification(notifyCtx(), cost, policy, gateHeld(fresh), stories, stillBlocking(runDir2)), stageIdOf());
|
|
40337
40446
|
};
|
|
40338
40447
|
if (onlyHeldByQuestions(fresh))
|
|
40339
40448
|
return { costUsd: cost, deferredGate: send };
|
|
@@ -40349,6 +40458,7 @@ async function runAuto(options) {
|
|
|
40349
40458
|
}
|
|
40350
40459
|
return { costUsd: null, deferredGate: null };
|
|
40351
40460
|
};
|
|
40461
|
+
let consecutiveFailures = 0;
|
|
40352
40462
|
try {
|
|
40353
40463
|
for (let iteration = 0;iteration < MAX_ITERATIONS; iteration++) {
|
|
40354
40464
|
const store = RunStore.open(runDir2);
|
|
@@ -40400,7 +40510,19 @@ async function runAuto(options) {
|
|
|
40400
40510
|
if (send !== null)
|
|
40401
40511
|
await send();
|
|
40402
40512
|
};
|
|
40513
|
+
if (outcome.code !== EXIT_AGENT_FAILED3)
|
|
40514
|
+
consecutiveFailures = 0;
|
|
40403
40515
|
if (outcome.code !== EXIT_OK5) {
|
|
40516
|
+
const retryBound = options.retryFailedStages ?? 0;
|
|
40517
|
+
if (outcome.code === EXIT_AGENT_FAILED3) {
|
|
40518
|
+
consecutiveFailures += 1;
|
|
40519
|
+
if (consecutiveFailures <= retryBound) {
|
|
40520
|
+
for (const line of outcome.lines)
|
|
40521
|
+
say(` ${line}`);
|
|
40522
|
+
say(`retrying ${cursorBefore} — failure ${String(consecutiveFailures)} of ` + `${String(retryBound)} retries allowed (--retry-failed ${String(retryBound)})`);
|
|
40523
|
+
continue;
|
|
40524
|
+
}
|
|
40525
|
+
}
|
|
40404
40526
|
let rejection = null;
|
|
40405
40527
|
if (outcome.code === EXIT_AWAITING_HUMAN3) {
|
|
40406
40528
|
const card = openQuestions2();
|
|
@@ -40432,6 +40554,10 @@ async function runAuto(options) {
|
|
|
40432
40554
|
continue;
|
|
40433
40555
|
}
|
|
40434
40556
|
if (waited.resolution === "rejected") {
|
|
40557
|
+
if (waited.andContinue) {
|
|
40558
|
+
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");
|
|
40559
|
+
continue;
|
|
40560
|
+
}
|
|
40435
40561
|
rejection = `waited ${String(Math.round(waited.ms / 1000))}s at ${cursorBefore} — ` + `the gate on ${gate2.stage} was REJECTED` + (waited.note === null ? "" : `: ${waited.note}`);
|
|
40436
40562
|
} else {
|
|
40437
40563
|
await flushGate();
|
|
@@ -40447,6 +40573,9 @@ async function runAuto(options) {
|
|
|
40447
40573
|
say(line);
|
|
40448
40574
|
if (rejection !== null)
|
|
40449
40575
|
say(rejection);
|
|
40576
|
+
if (outcome.code === EXIT_AGENT_FAILED3 && retryBound > 0) {
|
|
40577
|
+
say(`stopped: ${String(consecutiveFailures)} consecutive stage failures at ${cursorBefore} ` + `— the --retry-failed ${String(retryBound)} bound is spent, and the exit code is still ` + "5, the failure's own");
|
|
40578
|
+
}
|
|
40450
40579
|
return await finish(outcome.code, spentByLoop);
|
|
40451
40580
|
}
|
|
40452
40581
|
const after = RunStore.open(runDir2);
|
|
@@ -40490,6 +40619,7 @@ function pendingGate(runDir2) {
|
|
|
40490
40619
|
return {
|
|
40491
40620
|
stage: `${cursor.phase}/${cursor.stage}`,
|
|
40492
40621
|
stageId: cursor.stage,
|
|
40622
|
+
phase: cursor.phase,
|
|
40493
40623
|
policy: gatePolicyFor(store.run.gates_policy, cursor.stage)
|
|
40494
40624
|
};
|
|
40495
40625
|
} catch {
|
|
@@ -40509,16 +40639,22 @@ async function waitForGate(runDir2, stageId, limitMs, gate2) {
|
|
|
40509
40639
|
for (;; ) {
|
|
40510
40640
|
const elapsed = Date.now() - started;
|
|
40511
40641
|
const found = gateOf(runDir2, stageId);
|
|
40512
|
-
if (found !== null && found.status === "approved")
|
|
40513
|
-
return { resolution: "approved", ms: elapsed, note: null };
|
|
40642
|
+
if (found !== null && found.status === "approved") {
|
|
40643
|
+
return { resolution: "approved", ms: elapsed, note: null, andContinue: false };
|
|
40644
|
+
}
|
|
40514
40645
|
if (found !== null && found.status === "rejected") {
|
|
40515
|
-
return {
|
|
40646
|
+
return {
|
|
40647
|
+
resolution: "rejected",
|
|
40648
|
+
ms: elapsed,
|
|
40649
|
+
note: found.note.trim() === "" ? null : found.note.trim(),
|
|
40650
|
+
andContinue: found.andContinue
|
|
40651
|
+
};
|
|
40516
40652
|
}
|
|
40517
40653
|
if (found !== null && await selfCloseAutoGate(runDir2, stageId, gate2)) {
|
|
40518
|
-
return { resolution: "approved", ms: Date.now() - started, note: null };
|
|
40654
|
+
return { resolution: "approved", ms: Date.now() - started, note: null, andContinue: false };
|
|
40519
40655
|
}
|
|
40520
40656
|
if (elapsed >= limitMs)
|
|
40521
|
-
return { resolution: "lapsed", ms: elapsed, note: null };
|
|
40657
|
+
return { resolution: "lapsed", ms: elapsed, note: null, andContinue: false };
|
|
40522
40658
|
const pollMs = pollInterval(limitMs);
|
|
40523
40659
|
await new Promise((resolve11) => setTimeout(resolve11, Math.min(pollMs, limitMs - elapsed)));
|
|
40524
40660
|
}
|
|
@@ -40551,7 +40687,11 @@ async function selfCloseAutoGate(runDir2, stageId, gate2) {
|
|
|
40551
40687
|
function gateOf(runDir2, stageId) {
|
|
40552
40688
|
try {
|
|
40553
40689
|
const found = flatten(RunStore.open(runDir2).run).find((entry) => entry.stage.id === stageId);
|
|
40554
|
-
return found === undefined ? null : {
|
|
40690
|
+
return found === undefined ? null : {
|
|
40691
|
+
status: found.stage.gate.status,
|
|
40692
|
+
note: found.stage.gate.note,
|
|
40693
|
+
andContinue: found.stage.gate.and_continue === true
|
|
40694
|
+
};
|
|
40555
40695
|
} catch {
|
|
40556
40696
|
return null;
|
|
40557
40697
|
}
|
|
@@ -42691,7 +42831,8 @@ var VALUE_FLAGS = [
|
|
|
42691
42831
|
"wait-answers",
|
|
42692
42832
|
"wait-gates",
|
|
42693
42833
|
"prompt-max-bytes",
|
|
42694
|
-
"max-reads"
|
|
42834
|
+
"max-reads",
|
|
42835
|
+
"retry-failed"
|
|
42695
42836
|
];
|
|
42696
42837
|
function durationFlag(args, name) {
|
|
42697
42838
|
const raw = stringFlag(args, name);
|
|
@@ -42703,6 +42844,15 @@ function durationFlag(args, name) {
|
|
|
42703
42844
|
}
|
|
42704
42845
|
return ms;
|
|
42705
42846
|
}
|
|
42847
|
+
function retryFailedFlag(args) {
|
|
42848
|
+
const value = numberFlag(args, "retry-failed");
|
|
42849
|
+
if (value === undefined)
|
|
42850
|
+
return;
|
|
42851
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_RETRY_FAILED) {
|
|
42852
|
+
throw new UsageError(`--retry-failed must be a whole number between 0 and ${String(MAX_RETRY_FAILED)} ` + "(0 is the default: a failed stage stops the loop). A stage that has failed that many " + "times in a row is failing for a reason another spawn will not discover, and every " + "attempt is real money.");
|
|
42853
|
+
}
|
|
42854
|
+
return value;
|
|
42855
|
+
}
|
|
42706
42856
|
var runCommand = {
|
|
42707
42857
|
name: "run",
|
|
42708
42858
|
summary: "Create, inspect or auto-run a piece of work",
|
|
@@ -42715,6 +42865,7 @@ var runCommand = {
|
|
|
42715
42865
|
` + ` tldrx run auto [<run>] [--max-usd <n>] [--until <stage>] [--model <m>] [--effort <level>]
|
|
42716
42866
|
` + ` [--notify-every <duration>] [--wait-answers <duration>]
|
|
42717
42867
|
` + ` [--wait-gates <duration>] [--prompt-max-bytes <n>] [--max-reads <n>]
|
|
42868
|
+
` + ` [--retry-failed <n>]
|
|
42718
42869
|
` + ` [--yolo] [--parallel <n>] [--gate-agent] [--ui scene|compact|plain|off]
|
|
42719
42870
|
` + ` [--run <id>] [--root <path>]
|
|
42720
42871
|
` + ` tldrx run gates set <stage>:<human|auto|agent> --note <text> [--run <id>] [--root <path>]
|
|
@@ -42877,6 +43028,7 @@ async function runAutoLoop(argv) {
|
|
|
42877
43028
|
notifyEveryMs: durationFlag(args, "notify-every"),
|
|
42878
43029
|
waitAnswersMs: durationFlag(args, "wait-answers"),
|
|
42879
43030
|
waitGatesMs: durationFlag(args, "wait-gates"),
|
|
43031
|
+
retryFailedStages: retryFailedFlag(args),
|
|
42880
43032
|
actor: currentActor(),
|
|
42881
43033
|
at: nowRfc3339(),
|
|
42882
43034
|
onLine: (line) => ui2.log(() => process.stdout.write(`${line}
|
|
@@ -43294,7 +43446,7 @@ var PROPOSE_STAGE = "propose";
|
|
|
43294
43446
|
var EXIT_OK9 = 0;
|
|
43295
43447
|
var EXIT_USAGE6 = 1;
|
|
43296
43448
|
var EXIT_REFUSED7 = 2;
|
|
43297
|
-
var
|
|
43449
|
+
var EXIT_AGENT_FAILED4 = 5;
|
|
43298
43450
|
var DEFAULT_TRIAGE_EFFORT = "low";
|
|
43299
43451
|
var DEFAULT_TRIAGE_USD = 1;
|
|
43300
43452
|
var MIN_TRIAGE_USD = 0.25;
|
|
@@ -43428,7 +43580,7 @@ async function propose(options, workspaceCommands, inventory, seed, outDir) {
|
|
|
43428
43580
|
metered = outcome.metered;
|
|
43429
43581
|
if (!outcome.ok) {
|
|
43430
43582
|
return {
|
|
43431
|
-
code:
|
|
43583
|
+
code: EXIT_AGENT_FAILED4,
|
|
43432
43584
|
costUsd,
|
|
43433
43585
|
outDir,
|
|
43434
43586
|
inventory,
|
|
@@ -43449,7 +43601,7 @@ async function propose(options, workspaceCommands, inventory, seed, outDir) {
|
|
|
43449
43601
|
if (mode === "commit")
|
|
43450
43602
|
writeRaw(outDir, PROPOSE_STAGE, JSON.stringify(raw ?? null, null, 2));
|
|
43451
43603
|
return {
|
|
43452
|
-
code:
|
|
43604
|
+
code: EXIT_AGENT_FAILED4,
|
|
43453
43605
|
costUsd,
|
|
43454
43606
|
outDir,
|
|
43455
43607
|
inventory,
|
|
@@ -45051,11 +45203,12 @@ function costReport(argv) {
|
|
|
45051
45203
|
var rejectCommand2 = {
|
|
45052
45204
|
name: "reject",
|
|
45053
45205
|
summary: "Request changes at the current gate, or revoke an approval already given",
|
|
45054
|
-
usage: "tldrx reject --note <text> [--stage <phase>/<stage>] [--run <id>] [--root <path>]",
|
|
45206
|
+
usage: "tldrx reject --note <text> [--and-continue] [--stage <phase>/<stage>] [--run <id>] [--root <path>]",
|
|
45055
45207
|
implemented: true,
|
|
45056
45208
|
async run(argv) {
|
|
45057
45209
|
try {
|
|
45058
45210
|
const args = parseArgs(argv, ["run", "note", "root", "stage"]);
|
|
45211
|
+
const andContinue = boolFlag(args, "and-continue");
|
|
45059
45212
|
const note = stringFlag(args, "note") ?? args.positionals.join(" ");
|
|
45060
45213
|
if (note.trim() === "") {
|
|
45061
45214
|
throw new UsageError('reject needs --note: `tldrx reject --note "what to change"`');
|
|
@@ -45063,11 +45216,14 @@ var rejectCommand2 = {
|
|
|
45063
45216
|
const root2 = workspaceRootFrom(args);
|
|
45064
45217
|
const wanted = stringFlag(args, "run");
|
|
45065
45218
|
const target = stringFlag(args, "stage");
|
|
45219
|
+
if (andContinue && target !== undefined && target !== "") {
|
|
45220
|
+
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");
|
|
45221
|
+
}
|
|
45066
45222
|
const resolved = target === undefined || target === "" ? resolveRunOrExplain("tldrx reject", root2, wanted) : resolveIncludingFinished(root2, wanted);
|
|
45067
45223
|
if (!isResolved(resolved))
|
|
45068
45224
|
return resolved.exit;
|
|
45069
45225
|
const store = resolved.store;
|
|
45070
|
-
const ctx = { root: root2, actor: currentActor(), at: nowRfc3339(), note };
|
|
45226
|
+
const ctx = { root: root2, actor: currentActor(), at: nowRfc3339(), note, andContinue };
|
|
45071
45227
|
if (target !== undefined && target !== "") {
|
|
45072
45228
|
const outcome2 = revoke(store, ctx, target);
|
|
45073
45229
|
const signed = outcome2.signedBy === "auto" ? "it had been auto-approved by the facilitator" : `it had been approved by ${outcome2.signedBy}`;
|
|
@@ -45088,9 +45244,11 @@ var rejectCommand2 = {
|
|
|
45088
45244
|
}
|
|
45089
45245
|
const outcome = reject(store, ctx);
|
|
45090
45246
|
const came = outcome.from === "failed" ? " (it had failed)" : "";
|
|
45247
|
+
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";
|
|
45091
45248
|
process.stdout.write(`rejected ${outcome.phase}/${outcome.stage}${came} — back to \`ready\`
|
|
45092
45249
|
` + `note: ${outcome.note}
|
|
45093
45250
|
the note goes into the next prompt — \`tldrx next\` to re-run the stage
|
|
45251
|
+
` + `${loop}
|
|
45094
45252
|
`);
|
|
45095
45253
|
return EXIT_OK;
|
|
45096
45254
|
} catch (error) {
|
|
@@ -48535,7 +48693,7 @@ var EXIT_OK14 = 0;
|
|
|
48535
48693
|
var EXIT_USAGE9 = 1;
|
|
48536
48694
|
var EXIT_GATE_REFUSED2 = 2;
|
|
48537
48695
|
var EXIT_NOT_FOUND12 = 3;
|
|
48538
|
-
var
|
|
48696
|
+
var EXIT_AGENT_FAILED5 = 5;
|
|
48539
48697
|
var TRAIN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
48540
48698
|
async function runTraining(options) {
|
|
48541
48699
|
const said2 = [];
|
|
@@ -48775,7 +48933,7 @@ async function trainWithPreflight(options, said2) {
|
|
|
48775
48933
|
}));
|
|
48776
48934
|
if (!outcome.ok) {
|
|
48777
48935
|
rollback(previous);
|
|
48778
|
-
return fail3(
|
|
48936
|
+
return fail3(EXIT_AGENT_FAILED5, [
|
|
48779
48937
|
`${options.expert}/${area.id}: the ${task2.key} sub-agent failed — ${outcome.error ?? "no result"}`,
|
|
48780
48938
|
outcome.metered ? ` $${round29(outcome.costUsd).toFixed(2)} spent and recorded; nothing was written to competencies.yml` : ` ${providerWarning ?? "the provider turn was unmetered in dollars"}; nothing was written to competencies.yml`
|
|
48781
48939
|
], sum(tasks));
|
|
@@ -49114,7 +49272,7 @@ function reject2(log, options, area, costUsd, lines, detail) {
|
|
|
49114
49272
|
...fitProblems(detail.problems)
|
|
49115
49273
|
}
|
|
49116
49274
|
}));
|
|
49117
|
-
return { code:
|
|
49275
|
+
return { code: EXIT_AGENT_FAILED5, lines, costUsd };
|
|
49118
49276
|
}
|
|
49119
49277
|
function fitProblems(problems) {
|
|
49120
49278
|
const budget = MAX_PAYLOAD_BYTES2 - 1024;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tldr-experts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
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.16.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alan Martinez"
|
|
8
8
|
}
|