tldr-experts 0.33.0 → 0.34.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 +188 -0
- package/README.md +1 -0
- package/dist/hooks/answer-capture.js +3 -3
- package/dist/hooks/budget-gate.js +12 -8
- package/dist/hooks/{chunk-5wh26b5f.js → chunk-4d22xm3c.js} +22 -2
- package/dist/hooks/{chunk-4w2e56zw.js → chunk-5q89ffsn.js} +57 -3
- package/dist/hooks/{chunk-px7swzs1.js → chunk-9kzevq39.js} +1 -1
- package/dist/hooks/{chunk-s4kz5nn2.js → chunk-dw8gggv9.js} +5 -14
- package/dist/hooks/{chunk-vhy24kv6.js → chunk-kcrphpwc.js} +3 -2
- package/dist/hooks/dod-gate.js +1 -1
- package/dist/hooks/session-start.js +5 -5
- package/dist/hooks/statusline.js +4 -4
- package/dist/tldrx.js +1362 -1076
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,193 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.34.0 — 2026-09-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`budget show`, `run estimate` and the `budget-gate` hook now price a Build stage's
|
|
8
|
+
`reviewer_share`, `story_cap_multiplier` and `story_cap_floor_usd` the same way `tldrx next`'s
|
|
9
|
+
brake does, instead of the shipped defaults (see #333).** #214 fixed exactly one of the four
|
|
10
|
+
`stage.yml` calibrations `remainingWork` takes — `attempts` — leaving these three still
|
|
11
|
+
defaulted by the three readers while the brake resolved all four through `spec.tuning`.
|
|
12
|
+
Measured against the Build fixture (three stories priced 2.00/2.50/4.50, two done, stage
|
|
13
|
+
ceiling $60): a stage that writes `story_cap_multiplier: 1` ("take the plan's price
|
|
14
|
+
literally") was quoted $24.25 by `budget show`/`run estimate`/the hook against the $10.75 the
|
|
15
|
+
brake would actually price and dispatch — 2.3x — with the phase ceiling able to sit between
|
|
16
|
+
the two, so `budget show` said BLOCKED and the hook denied a `tldrx next` the brake itself
|
|
17
|
+
allows. `buildStageDefaults` (`core/run/workflowPreset.ts`) now resolves and returns the
|
|
18
|
+
stage's full `StageTuning`, not just `attempts` and `timeout_s`, by re-reading the same
|
|
19
|
+
`stage.yml` `facilitator/stageSpec.ts`'s `overlay` already reads — tolerant exactly as before,
|
|
20
|
+
so an unreadable stage file still falls back to the shipped defaults. `run estimate` already
|
|
21
|
+
held the full tuning via `loadStageSpec`; it was just not forwarding three of the four fields
|
|
22
|
+
to `remainingWork`.
|
|
23
|
+
- **`round2` and the spec §3 exit-code table are each defined in exactly one place, with a shape
|
|
24
|
+
test refusing a second copy (see #174).** `round2` (`Math.round(n * 100) / 100`) was
|
|
25
|
+
independently restated in nine files; the spec §3 exit codes (`cli/exitCodes.ts`) were
|
|
26
|
+
independently restated, under a mix of matching and diverging names (`EXIT_REFUSED` for
|
|
27
|
+
`EXIT_GATE_REFUSED`, `EXIT_AWAITING` for `EXIT_AWAITING_HUMAN`), in fourteen more — wider than
|
|
28
|
+
the issue's own grep found, which only named `runNext.ts`/`runAuto.ts`; the broader set was
|
|
29
|
+
measured here (`grep -rlE '^const EXIT_[A-Z_]+\s*=\s*[0-9]+;' src/`) and folded into the same
|
|
30
|
+
fix rather than filed separately, since it is the identical defect the issue is about. Every
|
|
31
|
+
local copy now imports the canonical one (`core/build/caps.ts`'s exported `round2`,
|
|
32
|
+
`cli/exitCodes.ts`'s table, aliased where a file's own name differed) instead of restating it.
|
|
33
|
+
Mechanism this was hiding: a golden-guard mutation of `cli/exitCodes.ts` left
|
|
34
|
+
`test/build-golden.test.ts` byte-identical, because the Build path never read that file's
|
|
35
|
+
constants at all — it read `runNext.ts`'s and `runAuto.ts`'s own private copies. The new
|
|
36
|
+
`test/round2-exitcodes-one-derivation.test.ts` asserts, on the SHAPE (a local, unexported
|
|
37
|
+
`function round2(`/`const EXIT_*` declaration) rather than on the measured names, that no file
|
|
38
|
+
under `src/` besides the two canonical ones defines either — on the precedent of
|
|
39
|
+
`test/phase-ids-one-derivation.test.ts` (#187): deleting today's copies does not stop a third
|
|
40
|
+
one from coming back under a name this file never mentions.
|
|
41
|
+
- **`merge-wave.sh --status` now says why it could not look instead of answering `idle` (see
|
|
42
|
+
#332).** AGENTS.md §2 calls this command the authoritative instrument for "is a wave alive" —
|
|
43
|
+
the reference-transaction guard and the merge lock exist precisely so nobody has to `ps` plus
|
|
44
|
+
a marker's mtime. Measured on the first autonomous run against this repo (2026-09-17): run
|
|
45
|
+
from outside a git repository, or with its `merge-lock.sh` sibling missing, it answered `idle`
|
|
46
|
+
and exit 0 — a false all-clear that could start a wave on top of one already live. It now
|
|
47
|
+
prints `unknown: <reason>` and exits **15**, its own code (`14` stays release.sh's — see
|
|
48
|
+
AGENTS.md §2 and docs/RELEASING.md), so a caller polling stdout can no longer read "could not
|
|
49
|
+
check" as "nothing is happening".
|
|
50
|
+
- **`merge-wave.sh` now diffs a released, dated CHANGELOG section's content, not just its
|
|
51
|
+
heading, so a rebase-then-merge across a release can no longer land a bullet inside it
|
|
52
|
+
silently.** The exit-12 gate only ever counted unreleased headings; a branch written before
|
|
53
|
+
a release and rebased onto main after that release merged CLEANLY — no conflict, no heading
|
|
54
|
+
touched — and its bullet landed inside the now-dated, supposedly immutable section, caught
|
|
55
|
+
only by a human reading the diff or much later by `release-check.sh`'s own #200 check. The
|
|
56
|
+
wave now compares every dated section against what main had immediately before the merge and
|
|
57
|
+
refuses with its own exit **16**, naming the section and the first difference (see #336).
|
|
58
|
+
- **A spawning test run whose `$TMPDIR` sits inside a git work tree can no longer commit its
|
|
59
|
+
scratch paths onto the enclosing branch (#335).** Fixtures resolve their sandbox through
|
|
60
|
+
`os.tmpdir()`/`$TMPDIR` and run real git inside it; when that path is inside a worktree,
|
|
61
|
+
git's own upward repo search walks out of the fixture and finds the WORKTREE's repo, so
|
|
62
|
+
`git add`/`git commit` there land on whatever branch is checked out — #335 measured 960
|
|
63
|
+
tracked scratch paths and dozens of foreign commits on a real branch this way, and the same
|
|
64
|
+
underlying mechanism reproduced here on `490f637` from a fresh throwaway worktree as the same
|
|
65
|
+
two test failures the issue reported (`test/gitignore-shadow.test.ts`,
|
|
66
|
+
`test/install.test.ts`). The `bun test` preload `test/preload/tmpdirGuard.ts` now refuses,
|
|
67
|
+
named, before any test runs, when the resolved `$TMPDIR` is inside a git work tree — opt out
|
|
68
|
+
only with `TLDRX_ALLOW_TMPDIR_IN_WORKTREE=1`, once the isolation is verified some other way.
|
|
69
|
+
- **A turn killed mid-flight by SIGINT/SIGTERM now banks its cost as absent, not a measured
|
|
70
|
+
`$0.00` (see #355).** `interrupt.ts`'s `recordPartialResult` wrote `cost_usd: 0` with no
|
|
71
|
+
`metered` key on both the task row and the `agent.result` payload — legal by `RunFile.ts`'s
|
|
72
|
+
validator, which only ever checked the converse (a `null` needs `metered: false`), so it
|
|
73
|
+
passed silently while `tallyOf` summed the row into the metered bucket as a real, measured
|
|
74
|
+
zero and `spendBasis` called the run's total complete. It now writes `cost_usd: null,
|
|
75
|
+
metered: false` on the row and the payload, the same idiom every other unknown-cost turn in
|
|
76
|
+
this codebase already uses, so a Ctrl-C mid-turn reads as a stated lower bound.
|
|
77
|
+
- **A killed parent's abandoned Build-story turn now leaves a row in `run.yml`, not nothing (see
|
|
78
|
+
#337).** The headless half of this shipped earlier as the orphan case #337 was filed for; the
|
|
79
|
+
fix deliberately skipped every Build-executor phase, reasoning the per-story ceiling ledger
|
|
80
|
+
already covered it — true for `tldrx cost`'s per-story view, but that ledger has no `run.yml`
|
|
81
|
+
task row to show for it, so `tallyOf`/`spendBasis` (and so `budget.spent_usd`) never saw the
|
|
82
|
+
abandoned attempt at all, not even as unmetered. `demoteStaleRunning` now reads which of a
|
|
83
|
+
stuck Build stage's `agent.spawned` turns no `agent.result` ever answered
|
|
84
|
+
(`build/orphanedTurns.ts`) and records one `cost_usd: null, metered: false` row per abandoned
|
|
85
|
+
story, same as the headless case.
|
|
86
|
+
- **The boundary gate now diffs the branch Build actually cut under `branch_model: integration`,
|
|
87
|
+
instead of an epic file's unused per-epic `branch:` label (see #225).** `epicTargets` read
|
|
88
|
+
`03-plan/epics/*.md`'s own `branch:` as the ref to diff — right under `per-epic`, where Build
|
|
89
|
+
cuts exactly that name, and silently wrong under `integration`, where every epic's stories
|
|
90
|
+
merge into ONE recorded branch and the epic's `branch:` is never cut at all. A run with eight
|
|
91
|
+
such epics measured `boundary=n/a (nothing could be diffed: … does not resolve …)` and
|
|
92
|
+
auto-signed a +31,465/−237-line PR across 117 files with the condition having diffed nothing.
|
|
93
|
+
The branch now comes from `run.yml`'s `build.epic_branch`, through the same `recordedEpicBranch`
|
|
94
|
+
derivation `watch` and `ship` already use (#90) — the epic file supplies repos and stories,
|
|
95
|
+
never the ref. A run that recorded an epic branch and still cannot diff it now HOLDS the gate
|
|
96
|
+
instead of reading `n/a`; `n/a` stays reserved for a run that named no branch to diff at all.
|
|
97
|
+
- **The boundary gate reads a run's stories through the same validated, all-phases walk `ship`
|
|
98
|
+
and carried-findings ownership already share, instead of its own `03-plan/`-only, schema-less
|
|
99
|
+
reader (see #189).** `boundary.ts`'s `storyTouches` was a third `stories/*.md` reader: it
|
|
100
|
+
looked only under `03-plan/` (missing a story filed under `04-build/`, the phase a carried fix
|
|
101
|
+
list's own story is written under), parsed front matter with no schema (taking a story a real
|
|
102
|
+
reader would report as unreadable), and dropped an unparseable file silently. It now calls the
|
|
103
|
+
shared `scanStories`/`phaseDirsOf` leaf (extracted from `build/carriedRows.ts` into
|
|
104
|
+
`build/storyScan.ts` so `boundary.ts` — which `carriedRows.ts` already imports from — does not
|
|
105
|
+
import it back), closing a divergence `carriedRows.ts` had documented as "somebody else's
|
|
106
|
+
issue" since #171.
|
|
107
|
+
- **An agent gate can no longer sign over evidence dated before the stage it is closing even
|
|
108
|
+
started (see #144).** A field evaluation of a delegated `watch` gate found its evidence note
|
|
109
|
+
timestamped 7h before its stage began, asserting facts already false by the time it signed,
|
|
110
|
+
with `refuted: 0` — the resolution half of that bug was #140; this is the remaining half
|
|
111
|
+
AGENTS.md §7 names: "a gate's evidence must not predate what it signs". `validateEvidence`
|
|
112
|
+
refuses a note whose `at:` is before the CURRENT attempt's own start — the new, additive
|
|
113
|
+
`RunStage.attempt_started_at`, which `markRunning` overwrites on every run, never the frozen
|
|
114
|
+
`started_at` it sits beside (§7: existing fields only grow, never change meaning; `started_at`
|
|
115
|
+
is set once, at the stage's first run, and stays that forever) — on both the engine's own
|
|
116
|
+
signer and `approve --as-agent`, one derivation, both doors. Two more holes pre-merge review
|
|
117
|
+
found in the same check: `at` was only confirmed non-empty, so a raw string compare read a
|
|
118
|
+
date-only `at` as earlier than a same-day, full-precision stage start; `at` is now confirmed a
|
|
119
|
+
real RFC3339 instant first, and the two timestamps are compared as instants, not strings. And
|
|
120
|
+
the full test gate caught the new check's own collateral: `tldrx learn`'s chapter 6 planted its
|
|
121
|
+
stand-in evidence note with a fixed past date, which the real wall clock had by now walked
|
|
122
|
+
past — its `at:` is stamped off `Date.now()` (with a 5-minute forward buffer, since the note is
|
|
123
|
+
written before the step that starts the stage it signs) rather than a date fixed at authoring
|
|
124
|
+
time.
|
|
125
|
+
- **`tldrx reject` now archives a stage's scratch evidence note instead of leaving it for the
|
|
126
|
+
next attempt to sign over (see #199).** A reject sent a stage back to `ready` but never
|
|
127
|
+
touched `.agent/<stage>/evidence.md`, so a note that had already said `verdict: sign` over one
|
|
128
|
+
attempt's outputs was still the first (and only) thing either signer found after the stage
|
|
129
|
+
re-ran with different outputs — and could sign again over an attempt it never read. The note
|
|
130
|
+
is renamed to `evidence.rejected-<at>.md` in the same scratch directory (moved, not deleted:
|
|
131
|
+
it is still the record of a real check somebody made), so the re-run meets the ordinary "no
|
|
132
|
+
evidence note at …" state instead. `reject` also advances the stage's `attempt_started_at`
|
|
133
|
+
anchor to its own moment (see #144's entry above) — closing a gap pre-merge review found:
|
|
134
|
+
without it, `approve --evidence <path>` pointed straight at the just-archived note still
|
|
135
|
+
signed, since nothing yet had moved the anchor past it. Archiving is collision-safe too
|
|
136
|
+
(`-2`, `-3`, … suffixes): two rejections landing in the same clock second used to let the
|
|
137
|
+
second silently overwrite the first's archive.
|
|
138
|
+
- **`reject --stage` (i.e. a revoke) no longer strands a later stage `running` with no verb that
|
|
139
|
+
owns it, and `run unlock`'s stranded-run note no longer promises a demotion `next` does not
|
|
140
|
+
make (see #228).** A revoke marked every later stage `stale: true` but left `status: running`
|
|
141
|
+
untouched; `next` only demotes a `running` stage behind a DEAD-PID lock, and a revoke leaves no
|
|
142
|
+
lock behind at all, so the cursor could walk back into that stranded stage mid-flight once the
|
|
143
|
+
revoked stage was re-approved. A later `running` stage with nothing holding it (no `--prepare`
|
|
144
|
+
bundle waiting) now goes back to `ready` alongside going stale, named on the outcome and on the
|
|
145
|
+
additive `gate.revoked` event; one holding a bundle is a sub-agent turn already paid for, so
|
|
146
|
+
the revoke refuses instead and names it. `run unlock` had the same false promise for a
|
|
147
|
+
lockless `running` stage ("`tldrx next` demotes it") — it now performs that demotion itself
|
|
148
|
+
when nothing is holding the run, rather than describing a move nothing makes. A revoke also
|
|
149
|
+
advances `attempt_started_at` (see #144, #199) on the stage it revokes and on every stage it
|
|
150
|
+
demotes, for the same reason `reject` does.
|
|
151
|
+
- **A mutating generator command in a story's dod block can no longer be run by the base-tree
|
|
152
|
+
pre-flight as a routine measurement (see #362).** Measured live, tldrx 0.31.1: a `.NET`
|
|
153
|
+
story's dod block declared `dotnet ef migrations add …` beside ordinary build/test commands,
|
|
154
|
+
and the base-tree probe ran it against the UNTOUCHED base tree — a DELTA-gate measurement is
|
|
155
|
+
not supposed to mutate the tree it is measuring, and it would have written a migration triple
|
|
156
|
+
into pristine `main` as a side effect of asking a question. `isGeneratorCommand`
|
|
157
|
+
(`schemas/commandAllowlist.ts`) detects a generator BY NAME — the command text, not a
|
|
158
|
+
workspace-declared slot key, since a team names its migration-add slot whatever it likes — and
|
|
159
|
+
two doors read that one derivation: `validateStoryDod` now refuses a generator command at Plan
|
|
160
|
+
time, so a plan is never written this way, and `dodRunner.ts`'s `measureBaseCommand` refuses
|
|
161
|
+
to run one against either base tree it measures, as defense in depth for a story file that
|
|
162
|
+
reached Build some other way. Neither door touches a story's own fresh worktree, where a
|
|
163
|
+
generator legitimately runs once, by the developer.
|
|
164
|
+
- **`tldrx next` now self-closes a parked `auto` gate the moment every condition it was held on
|
|
165
|
+
clears, `--wait-gates` or not (see #342).** Before this, the only door that ever re-measured an
|
|
166
|
+
already-`awaiting_gate` stage was `run auto --wait-gates`'s own poll (`waitForGate` →
|
|
167
|
+
`selfCloseAutoGate`); without that flag, `run auto` answering its own blocking questions (or a
|
|
168
|
+
person answering them by hand) left the gate `awaiting_gate` forever, and the next `next` call
|
|
169
|
+
reported it as terminal-for-this-call with a generic `gate pending: tldrx approve` — never
|
|
170
|
+
noticing the questions it was parked on had since been answered. `next` now re-runs the same
|
|
171
|
+
seven conditions (`reevaluateAutoGate`) on every `awaiting_gate` stage whose policy is `auto`
|
|
172
|
+
before giving up, and signs it through the same `approve` door a freshly-closed gate uses —
|
|
173
|
+
`human` and `agent` gates are untouched, and a verdict that still fails falls through to the
|
|
174
|
+
ordinary human-gate report exactly as before.
|
|
175
|
+
- **The default Build budget gate now parks at a story boundary instead of stopping the stage
|
|
176
|
+
incomplete with no boundary the operator chose (see #354).** `runNext.ts`'s `budgetRefusal`
|
|
177
|
+
meters the phase ceiling exactly once, at stage entry — the Build executor itself never
|
|
178
|
+
re-checked the envelope between two dispatches of one headless run, so a phase that could
|
|
179
|
+
afford its entry estimate but ran short partway through kept dispatching until the money was
|
|
180
|
+
gone, and the only way the gate fired again was an all-or-nothing refusal on a RETRY of a
|
|
181
|
+
failed stage. Modeled directly on #298's rate-limit park: before starting a new story's
|
|
182
|
+
developer, the executor now checks whether the stage's remainder still covers that story's
|
|
183
|
+
floor and, if not, lets whatever is already running finish and settle, then stops — recording
|
|
184
|
+
one `budget.parked` event (mirroring `agent.rate_limited`'s shape) and naming the shortfall as
|
|
185
|
+
the reason in both the operator line and the handoff's `## Unknowns`. `run auto --wait-gates`
|
|
186
|
+
resumes the parked stage itself once an operator's `tldrx budget raise --stage` (or
|
|
187
|
+
`--rebalance-finished`) actually raises the STAGE's own `budget_usd` — checked fresh on every
|
|
188
|
+
poll against the phase's real remainder, never a clock — and `run status` names the park
|
|
189
|
+
instead of reading as a decision waiting on a person.
|
|
190
|
+
|
|
3
191
|
## 0.33.0 — 2026-09-17
|
|
4
192
|
|
|
5
193
|
### Added
|
package/README.md
CHANGED
|
@@ -335,6 +335,7 @@ back on the registry is 0.3.0.
|
|
|
335
335
|
|
|
336
336
|
| Version | Date | Status | Contains |
|
|
337
337
|
|---|---|---|---|
|
|
338
|
+
| 0.34.0 | 2026-09-17 | `beta` | Four waves of the audited backlog, every branch reviewed before main: run auto no longer stops itself (parked auto gates self-close, Build parks at a story boundary on budget), gates cannot sign stale or backdated evidence, interrupted turns bank their cost as absent instead of a measured zero, the boundary condition diffs the branch the run recorded, mutating generators are refused from dod blocks, merge-wave refuses edits to released CHANGELOG sections and answers `unknown` instead of `idle`, and the test harness refuses a TMPDIR inside a work tree — 14 issues, the first of them shipped end-to-end by a tldrx run against this repo |
|
|
338
339
|
| 0.33.0 | 2026-09-17 | `beta` | Six follow-ups from the 0.32.0 cycle, each measured on the second unattended run (run #42) or on the review that shipped it. The plan sequencing rule counts a bare camelCase field only when it is unambiguous or the plan spells it elsewhere as snake_case or in backticks, so unrelated stories sharing a word like `toString` no longer refuse each other (#370); `task.done` clamps an oversized `as_is_note` the way it clamps `permission_refused` (#368); the asked-no-diff and red-DoD requeues settle through one helper that takes data (#369); a workspace can opt its base pre-flight probe into a story-shaped worktree with `probe_in_worktree: true`, and the Build-entry probe runs `tool_restore:` first (#371, the remainder of #363); AGENTS §11 names the negated closing verb that still auto-closes and widens the pre-wave grep (#372); and CI keeps merge-wave's sandboxes on failure so the next #237 red carries its `merge.log` (#373). Minor release: a new workspace field and a new plan refusal shape. |
|
|
339
340
|
| 0.32.0 | 2026-09-16 | `beta` | Seven framework defects measured on the second unattended run (run #42), where every one of nine operator stalls was the framework's, not the code's. A refused compound shell line is a rework note for the developer instead of a human stop (#360); a dependency hold is re-evaluated the moment the dependency lands (#361) and a rescued story is written back to `todo` the instant the release is decided (#366); an event payload over the size cap is clamped to a bounded head instead of failing the whole build stage (#359); a headless developer is told it is unattended, that `## Inputs` is where to start and not an allowlist, and a turn that asks instead of deciding is a named `asked_no_diff` failure (#364); the rate-limit park waits for 90% of the window, not the provider's first warning at half of it (#367); the plan refuses a story that enforces an invariant on a field before the story that populates it, matching the field across spellings (#365); and the preflight restores workspace tools before its first probe and names the tree it ran in (#363, in part). Minor release: a new plan refusal, a changed park threshold and a new failure kind. |
|
|
340
341
|
| 0.31.1 | 2026-09-16 | `beta` | One fix, measured on the first unattended run under 0.31.0: a seed placed at the documented `.tldrx/seeds/` location was invisible to the seed-solution detector, because the filter that drops the `what` stage's own framework inputs dropped every `.tldrx/` path, so `how` ran at full price for exactly the seeds #346 was written to skip (#358). |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
conflictOf
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-9kzevq39.js";
|
|
5
5
|
import {
|
|
6
6
|
FactsStore,
|
|
7
7
|
formatJaccard
|
|
@@ -14,10 +14,10 @@ import"./chunk-ae6bkfs5.js";
|
|
|
14
14
|
import {
|
|
15
15
|
EventLog,
|
|
16
16
|
PHASE_ID_RE
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-4d22xm3c.js";
|
|
18
18
|
import {
|
|
19
19
|
PHASE_IDS
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-kcrphpwc.js";
|
|
21
21
|
import"./chunk-7ypyp8xk.js";
|
|
22
22
|
import {
|
|
23
23
|
ADVISORY_KEY,
|
|
@@ -14,11 +14,9 @@ import {
|
|
|
14
14
|
REBALANCE_SOURCE,
|
|
15
15
|
applyRebalance,
|
|
16
16
|
asRunBudget,
|
|
17
|
-
currentActor,
|
|
18
17
|
describeRebalance,
|
|
19
18
|
economyFor,
|
|
20
19
|
isHostTokens,
|
|
21
|
-
nowRfc3339,
|
|
22
20
|
planRebalance,
|
|
23
21
|
raiseCommand,
|
|
24
22
|
raiseGrantVerdict,
|
|
@@ -27,10 +25,12 @@ import {
|
|
|
27
25
|
validateRunBudget,
|
|
28
26
|
wouldExceed,
|
|
29
27
|
wouldExceedHostTokens
|
|
30
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-dw8gggv9.js";
|
|
31
29
|
import {
|
|
32
|
-
EventLog
|
|
33
|
-
|
|
30
|
+
EventLog,
|
|
31
|
+
currentActor,
|
|
32
|
+
nowRfc3339
|
|
33
|
+
} from "./chunk-4d22xm3c.js";
|
|
34
34
|
import {
|
|
35
35
|
cursorStage,
|
|
36
36
|
hostTokensIn,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from "./chunk-57s4mdhy.js";
|
|
43
43
|
import {
|
|
44
44
|
buildStageDefaults
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-kcrphpwc.js";
|
|
46
46
|
import {
|
|
47
47
|
noteDeprecations
|
|
48
48
|
} from "./chunk-sjykymar.js";
|
|
@@ -125,7 +125,8 @@ await runHook("budget-gate", async () => {
|
|
|
125
125
|
const attended = isAttendedByHostView(view);
|
|
126
126
|
const stage = cursorStage(view);
|
|
127
127
|
const declared = stage?.budget_usd ?? stageBudgetFromLibrary(root, view.cursor.stage);
|
|
128
|
-
const
|
|
128
|
+
const tuning = buildStageDefaults(root, view.scope, view.cursor.stage);
|
|
129
|
+
const attempts = tuning.attempts;
|
|
129
130
|
const work = declared === null ? null : remainingWork({
|
|
130
131
|
runDir: view.dir,
|
|
131
132
|
phaseId: view.cursor.phase,
|
|
@@ -135,7 +136,10 @@ await runHook("budget-gate", async () => {
|
|
|
135
136
|
maxUsd: null,
|
|
136
137
|
economy: economyFor(budget, view.cursor.phase),
|
|
137
138
|
attended,
|
|
138
|
-
attempts
|
|
139
|
+
attempts,
|
|
140
|
+
reviewerShare: tuning.reviewerShare,
|
|
141
|
+
storyCapMultiplier: tuning.storyCapMultiplier,
|
|
142
|
+
storyCapFloorUsd: tuning.storyCapFloorUsd
|
|
139
143
|
});
|
|
140
144
|
const estimate = estimateFor(command, work === null ? null : work.usd);
|
|
141
145
|
if (estimate <= 0)
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
GATE_POLICIES,
|
|
3
3
|
validateGatesPolicy,
|
|
4
4
|
validateStagePolicy
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-kcrphpwc.js";
|
|
6
6
|
import {
|
|
7
7
|
asDocument,
|
|
8
8
|
isRecord,
|
|
@@ -69,6 +69,7 @@ var EVENT_TYPES = [
|
|
|
69
69
|
"budget.blocked",
|
|
70
70
|
"budget.raised",
|
|
71
71
|
"budget.granted",
|
|
72
|
+
"budget.parked",
|
|
72
73
|
"fact.added",
|
|
73
74
|
"fact.retired",
|
|
74
75
|
"fact.superseded",
|
|
@@ -231,6 +232,19 @@ function skippedNote(skipped) {
|
|
|
231
232
|
}
|
|
232
233
|
var WARNED = new Set;
|
|
233
234
|
|
|
235
|
+
// src/hooks/lib/actor.ts
|
|
236
|
+
function currentActor() {
|
|
237
|
+
const user = process.env.USER ?? process.env.USERNAME ?? "";
|
|
238
|
+
return user.trim() === "" ? "unknown" : user.trim();
|
|
239
|
+
}
|
|
240
|
+
function nowRfc3339() {
|
|
241
|
+
return `${new Date().toISOString().slice(0, 19)}Z`;
|
|
242
|
+
}
|
|
243
|
+
var RFC3339_RE = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
244
|
+
function isRfc3339(value) {
|
|
245
|
+
return RFC3339_RE.test(value) && !Number.isNaN(Date.parse(value));
|
|
246
|
+
}
|
|
247
|
+
|
|
234
248
|
// src/core/run/questionsPolicy.ts
|
|
235
249
|
var QUESTION_POLICIES = ["human", "recommended"];
|
|
236
250
|
function validateQuestionsPolicy(value, stageIds, issues) {
|
|
@@ -300,6 +314,9 @@ function parseEvidence(text) {
|
|
|
300
314
|
const role = requireEnum2(doc.role, EVIDENCE_ROLES, "role", issues);
|
|
301
315
|
const by = requireText(doc.by, "by", issues);
|
|
302
316
|
const at = requireText(doc.at, "at", issues);
|
|
317
|
+
if (at !== "" && !isRfc3339(at)) {
|
|
318
|
+
issues.push(front(0, `\`at\` must be a valid RFC3339 timestamp, got \`${at}\``));
|
|
319
|
+
}
|
|
303
320
|
const verdict = requireEnum2(doc.verdict, EVIDENCE_VERDICTS, "verdict", issues);
|
|
304
321
|
const read = requireStrings(doc.read, "read", issues);
|
|
305
322
|
const citations = requireCitations(doc.citations, issues);
|
|
@@ -728,6 +745,9 @@ function validateRunFile(input) {
|
|
|
728
745
|
if (stage.stale !== undefined && typeof stage.stale !== "boolean") {
|
|
729
746
|
issues.push({ path: `${path}.stale`, message: "expected true or false" });
|
|
730
747
|
}
|
|
748
|
+
if (stage.attempt_started_at !== undefined && stage.attempt_started_at !== null && typeof stage.attempt_started_at !== "string") {
|
|
749
|
+
issues.push({ path: `${path}.attempt_started_at`, message: "expected a string or null" });
|
|
750
|
+
}
|
|
731
751
|
checkOrder(stage.started_at, stage.ended_at, path, issues);
|
|
732
752
|
if (cursor !== null && cursor.phase === phaseId && cursor.stage === stageId)
|
|
733
753
|
cursorResolves = true;
|
|
@@ -918,4 +938,4 @@ function gateEvidencePath(runDir, phaseId, stageId) {
|
|
|
918
938
|
return join2(runDir, ...gateEvidenceRelPath(phaseId, stageId).split("/"));
|
|
919
939
|
}
|
|
920
940
|
|
|
921
|
-
export { EventLog, parseEvidence, PHASE_ID_RE, OUTCOME_NOT_RECORDED, isTerminal, isFinished, isAttendedByHost, flatten, stageAt, deriveRunStatus, derivePhaseStatus, validateRunFile, asRunFile, evidencePath, gateEvidencePath };
|
|
941
|
+
export { EventLog, currentActor, nowRfc3339, parseEvidence, PHASE_ID_RE, OUTCOME_NOT_RECORDED, isTerminal, isFinished, isAttendedByHost, flatten, stageAt, deriveRunStatus, derivePhaseStatus, validateRunFile, asRunFile, evidencePath, gateEvidencePath };
|
|
@@ -6,12 +6,11 @@ import {
|
|
|
6
6
|
WAVES_FILE,
|
|
7
7
|
asRunBudget,
|
|
8
8
|
buildProgress,
|
|
9
|
-
nowRfc3339,
|
|
10
9
|
spentBasis,
|
|
11
10
|
storyDependsOn,
|
|
12
11
|
tallyOf,
|
|
13
12
|
validateRunBudget
|
|
14
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-dw8gggv9.js";
|
|
15
14
|
import {
|
|
16
15
|
EventLog,
|
|
17
16
|
OUTCOME_NOT_RECORDED,
|
|
@@ -22,9 +21,10 @@ import {
|
|
|
22
21
|
isAttendedByHost,
|
|
23
22
|
isFinished,
|
|
24
23
|
isTerminal,
|
|
24
|
+
nowRfc3339,
|
|
25
25
|
stageAt,
|
|
26
26
|
validateRunFile
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-4d22xm3c.js";
|
|
28
28
|
import {
|
|
29
29
|
cursorStage,
|
|
30
30
|
isAttendedByHostView,
|
|
@@ -163,6 +163,9 @@ function stage(s) {
|
|
|
163
163
|
];
|
|
164
164
|
if (s.stale === true)
|
|
165
165
|
lines.push(" stale: true");
|
|
166
|
+
if (s.attempt_started_at !== undefined && s.attempt_started_at !== null) {
|
|
167
|
+
lines.push(` attempt_started_at: ${yamlScalar(s.attempt_started_at)}`);
|
|
168
|
+
}
|
|
166
169
|
if (s.tasks.length === 0) {
|
|
167
170
|
lines.push(" tasks: []");
|
|
168
171
|
} else {
|
|
@@ -795,6 +798,7 @@ function statusWithOutcome(status, outcome) {
|
|
|
795
798
|
// src/core/run/autoGate.ts
|
|
796
799
|
var AUTO_GATE_ACTOR = "auto";
|
|
797
800
|
var RATE_LIMIT_RESUME_NOTE_PREFIX = "run auto resumed this stage: its auto gate was held by an unfinished " + "story a rate-limit warning parked, and the provider's own clock has since cleared it.";
|
|
801
|
+
var BUDGET_RESUME_NOTE_PREFIX = "run auto resumed this stage: its auto gate was held by an unfinished " + "story a budget shortfall parked, and the phase can now afford it.";
|
|
798
802
|
var RETRYABLE_CONDITIONS = new Set(["checks", "claim-sources"]);
|
|
799
803
|
var AUTO_GATE_REFUSED_PREFIX = "auto-gate refused — held by: ";
|
|
800
804
|
var HELD_SEPARATOR = " · ";
|
|
@@ -1350,6 +1354,48 @@ function currentRateLimitPark(runDir, stageId) {
|
|
|
1350
1354
|
return park;
|
|
1351
1355
|
}
|
|
1352
1356
|
|
|
1357
|
+
// src/core/run/budgetPark.ts
|
|
1358
|
+
function currentBudgetPark(runDir, stageId) {
|
|
1359
|
+
let events;
|
|
1360
|
+
try {
|
|
1361
|
+
events = EventLog.forRun(runDir).readAll().events;
|
|
1362
|
+
} catch {
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
let stageStartIndex = -1;
|
|
1366
|
+
for (let index = events.length - 1;index >= 0; index -= 1) {
|
|
1367
|
+
const event = events[index];
|
|
1368
|
+
if (event !== undefined && event.stage === stageId && event.type === "stage.started") {
|
|
1369
|
+
stageStartIndex = index;
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (stageStartIndex === -1)
|
|
1374
|
+
return null;
|
|
1375
|
+
let park = null;
|
|
1376
|
+
for (let index = events.length - 1;index >= stageStartIndex; index -= 1) {
|
|
1377
|
+
const event = events[index];
|
|
1378
|
+
if (event === undefined || event.type !== "budget.parked" || event.stage !== stageId)
|
|
1379
|
+
continue;
|
|
1380
|
+
const payload = event.payload;
|
|
1381
|
+
const parked = payload.parked;
|
|
1382
|
+
if (typeof parked !== "string" || parked === "")
|
|
1383
|
+
continue;
|
|
1384
|
+
if (typeof payload.remainder_usd !== "number" || typeof payload.floor_usd !== "number")
|
|
1385
|
+
continue;
|
|
1386
|
+
park = { remainderUsd: payload.remainder_usd, floorUsd: payload.floor_usd, parked };
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
if (park === null)
|
|
1390
|
+
return null;
|
|
1391
|
+
const view = storiesView(runDir);
|
|
1392
|
+
if (view === null || view.unfinished.length === 0)
|
|
1393
|
+
return null;
|
|
1394
|
+
if (view.unfinished.some((story) => story.status !== "todo"))
|
|
1395
|
+
return null;
|
|
1396
|
+
return park;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1353
1399
|
// src/core/run/waiting.ts
|
|
1354
1400
|
var MOVABLE_KINDS = [
|
|
1355
1401
|
"gate",
|
|
@@ -1408,6 +1454,14 @@ function waitingFor(run, runDir) {
|
|
|
1408
1454
|
questions: open
|
|
1409
1455
|
};
|
|
1410
1456
|
}
|
|
1457
|
+
const budgetPark = currentBudgetPark(runDir, entry.stage.id);
|
|
1458
|
+
if (budgetPark !== null) {
|
|
1459
|
+
return {
|
|
1460
|
+
kind: "gate",
|
|
1461
|
+
message: `gate on ${entry.phase.id}/${entry.stage.id} — parked by a budget shortfall ` + `($${budgetPark.remainderUsd.toFixed(2)} left, $${budgetPark.floorUsd.toFixed(2)} needed) ` + "— resumes automatically once the phase can afford it (`tldrx budget raise`, " + "or `run auto --rebalance-finished`)",
|
|
1462
|
+
questions: open
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1411
1465
|
}
|
|
1412
1466
|
return {
|
|
1413
1467
|
kind: "gate",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
STAGE_TUNING_DEFAULTS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-kcrphpwc.js";
|
|
4
4
|
import {
|
|
5
5
|
MAX_PLAN_STORIES,
|
|
6
6
|
MAX_STORIES_PER_WAVE,
|
|
@@ -28,15 +28,6 @@ import {
|
|
|
28
28
|
result
|
|
29
29
|
} from "./chunk-0a3b8w1c.js";
|
|
30
30
|
|
|
31
|
-
// src/hooks/lib/actor.ts
|
|
32
|
-
function currentActor() {
|
|
33
|
-
const user = process.env.USER ?? process.env.USERNAME ?? "";
|
|
34
|
-
return user.trim() === "" ? "unknown" : user.trim();
|
|
35
|
-
}
|
|
36
|
-
function nowRfc3339() {
|
|
37
|
-
return `${new Date().toISOString().slice(0, 19)}Z`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
31
|
// src/core/budget/spentFigure.ts
|
|
41
32
|
var SPENT_BASES = ["lower-bound", "complete"];
|
|
42
33
|
function spentBasis(unmetered) {
|
|
@@ -229,6 +220,9 @@ var REVIEWER_SHARE = STAGE_TUNING_DEFAULTS.reviewerShare;
|
|
|
229
220
|
var REVIEWER_FLOOR_USD = 2;
|
|
230
221
|
var STORY_CAP_MULTIPLIER = STAGE_TUNING_DEFAULTS.storyCapMultiplier;
|
|
231
222
|
var STORY_CAP_FLOOR_USD = STAGE_TUNING_DEFAULTS.storyCapFloorUsd;
|
|
223
|
+
function round2(n) {
|
|
224
|
+
return Math.round(n * 100) / 100;
|
|
225
|
+
}
|
|
232
226
|
function shortBy(estimate, remaining) {
|
|
233
227
|
return Math.max(0.01, Math.ceil((estimate - remaining) * 100) / 100);
|
|
234
228
|
}
|
|
@@ -1214,9 +1208,6 @@ function reviewEventErrored(payload) {
|
|
|
1214
1208
|
return false;
|
|
1215
1209
|
return typeof payload.detail === "string" && looksLikeReviewerError(payload.detail);
|
|
1216
1210
|
}
|
|
1217
|
-
function round2(n) {
|
|
1218
|
-
return Math.round(n * 100) / 100;
|
|
1219
|
-
}
|
|
1220
1211
|
|
|
1221
1212
|
// src/core/budget/wouldExceed.ts
|
|
1222
1213
|
function totalSpent(budget) {
|
|
@@ -1451,4 +1442,4 @@ function round6(n) {
|
|
|
1451
1442
|
return Math.round(n * 100) / 100;
|
|
1452
1443
|
}
|
|
1453
1444
|
|
|
1454
|
-
export {
|
|
1445
|
+
export { spentBasis, tallyOf, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ON_GRANT_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, stackExpertNames, WAVES_FILE, BUILD_PHASE2 as BUILD_PHASE, buildProgress, storyDependsOn, shortBy, remainingWork, wouldExceed, wouldExceedHostTokens, raiseGrantVerdict, REBALANCE_SOURCE, planRebalance, applyRebalance, describeRebalance, raiseCommand };
|
|
@@ -122,13 +122,14 @@ var PHASE_IDS = ["01-what", "02-how", "03-plan", "04-build", "05-watch"];
|
|
|
122
122
|
var PHASE_SEGMENT_RE = /^0[1-5]-[a-z]+$/;
|
|
123
123
|
var DEFAULT_TIMEOUT_S = 7200;
|
|
124
124
|
function buildStageDefaults(root, scope, stageId) {
|
|
125
|
-
const fallback = {
|
|
125
|
+
const fallback = { ...STAGE_TUNING_DEFAULTS, timeoutS: DEFAULT_TIMEOUT_S };
|
|
126
126
|
try {
|
|
127
127
|
const preset = loadWorkflowPreset(root, scope);
|
|
128
128
|
const stage = (stageId === undefined ? undefined : preset.stages.find((s) => s.id === stageId)) ?? preset.stages.find((s) => s.phase === PHASE_IDS[3]);
|
|
129
129
|
if (stage === undefined)
|
|
130
130
|
return fallback;
|
|
131
|
-
|
|
131
|
+
const doc = existsSync(stage.source) ? parseYaml(readFileSync(stage.source, "utf8")) : null;
|
|
132
|
+
return { ...readStageTuning(doc), timeoutS: stage.timeout_s };
|
|
132
133
|
} catch {
|
|
133
134
|
return fallback;
|
|
134
135
|
}
|
package/dist/hooks/dod-gate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
questionsCard
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-9kzevq39.js";
|
|
5
5
|
import"./chunk-bfvcebn6.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-5q89ffsn.js";
|
|
24
24
|
import"./chunk-ae6bkfs5.js";
|
|
25
25
|
import {
|
|
26
26
|
expertsDir,
|
|
@@ -28,14 +28,14 @@ import {
|
|
|
28
28
|
pathsIntersect,
|
|
29
29
|
readExpertDomain,
|
|
30
30
|
stackExpertNames
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-dw8gggv9.js";
|
|
32
32
|
import {
|
|
33
33
|
isFinished
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-4d22xm3c.js";
|
|
35
35
|
import {
|
|
36
36
|
openRunViews
|
|
37
37
|
} from "./chunk-57s4mdhy.js";
|
|
38
|
-
import"./chunk-
|
|
38
|
+
import"./chunk-kcrphpwc.js";
|
|
39
39
|
import"./chunk-7ypyp8xk.js";
|
|
40
40
|
import {
|
|
41
41
|
openBlocks,
|
package/dist/hooks/statusline.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
bar,
|
|
4
4
|
runSnapshot
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-5q89ffsn.js";
|
|
6
6
|
import"./chunk-ae6bkfs5.js";
|
|
7
|
-
import"./chunk-
|
|
8
|
-
import"./chunk-
|
|
7
|
+
import"./chunk-dw8gggv9.js";
|
|
8
|
+
import"./chunk-4d22xm3c.js";
|
|
9
9
|
import"./chunk-57s4mdhy.js";
|
|
10
|
-
import"./chunk-
|
|
10
|
+
import"./chunk-kcrphpwc.js";
|
|
11
11
|
import"./chunk-7ypyp8xk.js";
|
|
12
12
|
import"./chunk-sjykymar.js";
|
|
13
13
|
import {
|