tldr-experts 0.3.0 → 0.4.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 +1957 -0
- package/README.md +99 -12
- package/dist/hooks/answer-capture.js +21 -12
- package/dist/hooks/budget-gate.js +95 -11
- package/dist/hooks/{chunk-0bt6yb2q.js → chunk-39zh2e44.js} +10 -1
- package/dist/hooks/chunk-4cp363kv.js +1766 -0
- package/dist/hooks/{chunk-t8tdv11p.js → chunk-7y2dq0pj.js} +1 -1
- package/dist/hooks/chunk-9zsqxr6y.js +213 -0
- package/dist/hooks/{chunk-a8p2rc94.js → chunk-b8kxzna2.js} +1 -1
- package/dist/hooks/{chunk-j234zf0t.js → chunk-c6t5nx0r.js} +37 -166
- package/dist/hooks/{chunk-g395gk7e.js → chunk-m3mewgnw.js} +36 -17
- package/dist/hooks/chunk-phmdk72a.js +164 -0
- package/dist/hooks/chunk-rpcxsqh3.js +199 -0
- package/dist/hooks/{chunk-azctppjh.js → chunk-rz541e2b.js} +7 -1
- package/dist/hooks/{chunk-y0jdr3et.js → chunk-s1c5h7yx.js} +71 -29
- package/dist/hooks/{chunk-p274ckxv.js → chunk-sq44k6g2.js} +155 -12
- package/dist/hooks/{chunk-x98qs959.js → chunk-t1ywrfr4.js} +35 -42
- package/dist/hooks/{chunk-1zwcxd3f.js → chunk-tzzwddct.js} +1 -1
- package/dist/hooks/claim-sources.js +31 -23
- package/dist/hooks/dod-gate.js +12 -37
- package/dist/hooks/no-reask.js +9 -9
- package/dist/hooks/session-start.js +176 -422
- package/dist/hooks/statusline.js +14 -11
- package/dist/tldrx.js +22798 -12309
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/tldrx/SKILL.md +16 -3
- package/stages/build/stage.md +5 -0
- package/stages/build/stage.yml +2 -1
- package/stages/plan/stage.md +11 -0
- package/templates/budget.yml +5 -0
- package/templates/epic.md +5 -3
- package/dist/hooks/chunk-ae6bkfs5.js +0 -0
- package/dist/hooks/chunk-kw4tffzf.js +0 -139
- package/dist/hooks/chunk-sdjnnmzz.js +0 -497
- package/dist/hooks/chunk-t56k6146.js +0 -14
package/README.md
CHANGED
|
@@ -13,10 +13,6 @@ do: a command that cannot do the thing exits non-zero and says which thing.
|
|
|
13
13
|
|
|
14
14
|
## Quick start
|
|
15
15
|
|
|
16
|
-
> **Not on npm yet.** Every published version was unpublished on 2026-08-29 (`npm view tldr-experts
|
|
17
|
-
> version` → `E404 Unpublished`) and there is no `v0.3.0` tag, so the `npm i -g` line 404s until
|
|
18
|
-
> `scripts/release.sh 0.3.0` is run. Until then: clone and `bun link`, or `bun <repo>/bin/tldrx.ts <cmd>`.
|
|
19
|
-
|
|
20
16
|
```bash
|
|
21
17
|
npm i -g tldr-experts # installs `tldrx` (short) and `tldr-experts` (same binary)
|
|
22
18
|
cd your-project
|
|
@@ -25,6 +21,11 @@ tldrx init # detect repos, map the code, write .tldrx/, ask only
|
|
|
25
21
|
tldrx install --claude # write the skill, hooks and status line into ./.claude/
|
|
26
22
|
```
|
|
27
23
|
|
|
24
|
+
**Never used it before?** `tldrx learn` teaches the loop by running it: eight chapters, ~15 minutes,
|
|
25
|
+
in a throwaway sandbox with a toy repo and a stand-in agent. Every command in it is the real one —
|
|
26
|
+
`init`, `run new`, `next`, `approve`, a Build that cuts a branch and runs a real DoD — so nothing it
|
|
27
|
+
shows you can drift from what the binary does, and it costs $0.00 and touches nothing you own.
|
|
28
|
+
|
|
28
29
|
Then open Claude Code there and type **`/tldrx`**. It runs `tldrx status`, finds what is already
|
|
29
30
|
waiting on you — unanswered setup questions, a proposed split nobody decided, a run waiting on a gate,
|
|
30
31
|
an expert no stage can lean on yet — and walks you through it one item at a time, asking every decision
|
|
@@ -39,13 +40,79 @@ tldrx run auto # `next`, over and over, until something actually need
|
|
|
39
40
|
dependencies, so an installed `tldrx` needs only Node; Bun builds it. Full walkthrough:
|
|
40
41
|
[`docs/guide/01-quick-start.md`](docs/guide/01-quick-start.md).
|
|
41
42
|
|
|
43
|
+
## Trying it: three ways to run
|
|
44
|
+
|
|
45
|
+
`tldrx run auto` and `tldrx run attend host` read like two speeds of the same thing. They are
|
|
46
|
+
opposites and they do not compose. **`auto` is an engine, not a lock**: a headless loop in which
|
|
47
|
+
the *framework* spawns a metered sub-agent, stage after stage. **`attend host` is a lock, not an
|
|
48
|
+
engine**: it sets one field, spends nothing and runs no stage, and from then on the framework never
|
|
49
|
+
spawns on that run — every turn is a `--prepare` / `--commit` handshake with a session you drive.
|
|
50
|
+
`run auto` on an attended run is refused outright (exit `1`); a bare `tldrx next` there exits `4`
|
|
51
|
+
and names the `--prepare` command instead.
|
|
52
|
+
|
|
53
|
+
| | who executes each turn | what a turn costs | where it stops |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| `tldrx run auto` | the framework — `claude -p`, spawned stage after stage | metered per spawn, rolled up by `tldrx cost` | the first human gate or open question (`4`), stage failure (`5`), ceiling (`2`) |
|
|
56
|
+
| `tldrx run attend host`, driven from a session | your session's own sub-agents | host-billed; the framework records `cost_usd: null, metered: false` | every turn — `--prepare` writes the bundle, `--commit` settles it |
|
|
57
|
+
| the same, under a **mandate** | your session's own sub-agents | host-billed | a new product decision, a ceiling raise, a boundary exit — nothing else |
|
|
58
|
+
|
|
59
|
+
- **A small run you were going to watch anyway** → `run auto`. One command, and it stops the moment it needs you.
|
|
60
|
+
- **A Claude Code session already open, and you care about cost or quality** → `run attend host`, driven from it: the context is warm, the turns are host-billed, and the framework writes the Build reviewer's bundle rather than spawning a second reader beside one you are already paying for.
|
|
61
|
+
- **Overnight, hands off, and you still want the adversarial check** → `run attend host` plus a mandate, below.
|
|
62
|
+
- **CI or cron** → `run auto`. It is the only one of the three with no session behind it.
|
|
63
|
+
|
|
64
|
+
### Overnight, with the checking kept
|
|
65
|
+
|
|
66
|
+
Two commands and a prompt. There is no keyword for this: the mandate is prose you write.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
tldrx run new payments --scope feature --budget 25 \
|
|
70
|
+
--attended-by host --gates what:agent,plan:agent,build:agent,watch:agent
|
|
71
|
+
tldrx run attend host 260101-payments # or flip a run that is already open
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`--gates` **replaces the workflow's gates wholesale**, and a stage you leave out of the list becomes
|
|
75
|
+
`auto` — so name every gate you want signed. Then, in the session, the mandate:
|
|
76
|
+
|
|
77
|
+
> Act as my unattended verification gate on run `260101-payments`, until it reaches its last gate.
|
|
78
|
+
>
|
|
79
|
+
> Drive every stage yourself — `tldrx next --prepare 260101-payments`, then
|
|
80
|
+
> `tldrx next --commit 260101-payments` — dispatching your own sub-agents for the turns. The
|
|
81
|
+
> framework must never spawn.
|
|
82
|
+
>
|
|
83
|
+
> For every build story, run an INDEPENDENT adversarial review through the `--review` handshake:
|
|
84
|
+
> `tldrx next --prepare --review`, one read-only sub-agent over the diff, then
|
|
85
|
+
> `tldrx next --commit --review`. Its job is to find what the developer got wrong, not to agree
|
|
86
|
+
> with it.
|
|
87
|
+
>
|
|
88
|
+
> Approve a gate only after you have checked it yourself — that the citations resolve, that every
|
|
89
|
+
> touched path is one this run declared, and that the diff matches the stories it claims to
|
|
90
|
+
> implement — and write that check down as evidence: `tldrx gate template`, fill it in, then
|
|
91
|
+
> `tldrx approve --as-agent`.
|
|
92
|
+
>
|
|
93
|
+
> Interrupt me ONLY for a new product decision, a budget-ceiling raise, or work that has to go
|
|
94
|
+
> outside the declared boundary. Everything else you decide, and log.
|
|
95
|
+
>
|
|
96
|
+
> Never push. The final merge is mine.
|
|
97
|
+
|
|
98
|
+
The whole chapter — the three switches, what "never spawns" is enforced by, the review handshake,
|
|
99
|
+
the fix list, the evidence note and the four fallthroughs:
|
|
100
|
+
[10 Unattended mode](docs/guide/10-unattended-mode.md).
|
|
101
|
+
|
|
42
102
|
## How much human is in the loop
|
|
43
103
|
|
|
44
104
|
Every stage ends at a gate; what you choose is **who closes it**. `human` waits for `tldrx approve`.
|
|
45
|
-
`auto` lets the harness close it, and only when all
|
|
46
|
-
phase has no open question, the spend is inside both the stage and phase ceilings, the stage did not
|
|
47
|
-
fail,
|
|
48
|
-
and
|
|
105
|
+
`auto` lets the harness close it, and only when all **seven** conditions hold: the stage's checks pass,
|
|
106
|
+
its phase has no open question, the spend is inside both the stage and phase ceilings, the stage did not
|
|
107
|
+
fail, the claim-sources validator reports nothing — and, on a Build stage, every story in the plan
|
|
108
|
+
reached `done` **and** the epic branch changed nothing the run never declared it would touch. Any one
|
|
109
|
+
failing falls back to the human gate and says which one and what it measured.
|
|
110
|
+
|
|
111
|
+
`agent` is the third policy, and the strongest: those same seven, plus no budget decision taken while
|
|
112
|
+
the stage ran, plus a validated **evidence note** the agent signed — a checklist whose own bullets each
|
|
113
|
+
carry a `[src: …]` that resolves. It arrives by choice (`--gates plan:agent`), never by default, and it
|
|
114
|
+
falls through to a person on an open question, a moved ceiling, work outside the declared boundary, or
|
|
115
|
+
its own refusal. See [10 Unattended mode](docs/guide/10-unattended-mode.md).
|
|
49
116
|
|
|
50
117
|
| Scope | what | how | plan | build | watch |
|
|
51
118
|
|---|---|---|---|---|---|
|
|
@@ -59,11 +126,23 @@ and says which one and what it measured.
|
|
|
59
126
|
| `security-patch` | auto | auto | — | human | human |
|
|
60
127
|
| `migration` | auto | auto | auto | human | human |
|
|
61
128
|
|
|
129
|
+
`--parallel <n>` on `next` / `run auto` builds that many of a wave's stories at once
|
|
130
|
+
(merges still land in the wave's listed order; default 1 is unchanged).
|
|
131
|
+
|
|
132
|
+
A scope with `—` under `plan` does not run the Plan phase, and Build writes the one story that
|
|
133
|
+
decision implies (`04-build/implicit-plan.yml`) from your What handoff rather than refusing;
|
|
134
|
+
`run status` says `plan: implicit (scope skips Plan)`. A `03-plan/` you write yourself always wins.
|
|
135
|
+
|
|
62
136
|
Those are the shipped defaults, and every scope keeps at least one human gate. Override per run with
|
|
63
137
|
`--gates <stage,stage>` — **the list is the human gates** — or `--gates all|none`. When the machine
|
|
64
138
|
signs something it should not have, `tldrx reject --stage <phase>/<stage> --note "…"` revokes it, moves
|
|
65
|
-
the cursor back and marks the later stages `stale`.
|
|
66
|
-
|
|
139
|
+
the cursor back and marks the later stages `stale`. When it is one BUILD STORY you disagree with — a
|
|
140
|
+
story two reviewers refused, which is terminal for the rest of the run —
|
|
141
|
+
`tldrx story reopen <id> --note "…"` gives that one story another run of attempts and nothing else.
|
|
142
|
+
When you fix `.tldrx/workspace.yml` mid-run and the approved stories still cite the old command strings,
|
|
143
|
+
`tldrx plan sync-dod` rewrites just their dod lines — renames followed, removed commands dropped, and
|
|
144
|
+
anything with no ancestor in the file's history flagged rather than guessed at.
|
|
145
|
+
What an auto gate cannot do: [`docs/guide/03-runs-and-gates.md`](docs/guide/03-runs-and-gates.md).
|
|
67
146
|
|
|
68
147
|
## What you see while it runs
|
|
69
148
|
|
|
@@ -136,14 +215,20 @@ else, because those five are machine-local or regenerated: `.tldrx/graphify-out/
|
|
|
136
215
|
|
|
137
216
|
## Documentation
|
|
138
217
|
|
|
139
|
-
The
|
|
218
|
+
**[The documentation site](https://ederwii.github.io/tldr-experts/)** is the place to start if you have
|
|
219
|
+
never used this: a landing page, a Quickstart and one short page per concept, written for a reader
|
|
220
|
+
rather than for an agent. Source in [`docs-site/`](docs-site/).
|
|
221
|
+
|
|
222
|
+
The reference guide, in `docs/guide/`: [1 Quick start](docs/guide/01-quick-start.md) ·
|
|
140
223
|
[2 The loop](docs/guide/02-the-loop.md) (the four steps, what a stage file controls, the two execution modes) ·
|
|
141
224
|
[3 Runs and gates](docs/guide/03-runs-and-gates.md) (`run new`→`retro`, gate policy, `run auto`, unlock/cancel, dashboard, tickets) ·
|
|
142
225
|
[4 Experts](docs/guide/04-experts.md) (loading rules, role experts, training, levels) ·
|
|
143
226
|
[5 Seeds and triage](docs/guide/05-seeds-and-triage.md) (`--seed`, `--from`, splitting a big seed) ·
|
|
144
227
|
[6 Budgets and cost](docs/guide/06-budgets-and-cost.md) · [7 Claude Code](docs/guide/07-claude-code.md) (plugin, hooks, `/tldrx`) ·
|
|
145
228
|
[8 CLI reference](docs/guide/08-cli-reference.md) (every command, flag and exit code) ·
|
|
146
|
-
[9 Troubleshooting](docs/guide/09-troubleshooting.md) (every refusal, and the move that clears it)
|
|
229
|
+
[9 Troubleshooting](docs/guide/09-troubleshooting.md) (every refusal, and the move that clears it) ·
|
|
230
|
+
[10 Unattended mode](docs/guide/10-unattended-mode.md) (`attended_by: host`, `gates_policy: agent`, the
|
|
231
|
+
review handshake, the fix list, decision cards).
|
|
147
232
|
Design docs: [`docs/concept.md`](docs/concept.md) (why) · [`docs/spec.md`](docs/spec.md) (the schemas, and §7's
|
|
148
233
|
open decisions) · [`docs/ROADMAP.md`](docs/ROADMAP.md) (next) · [`CHANGELOG.md`](CHANGELOG.md) (shipped) ·
|
|
149
234
|
[`docs/dashboard-model.md`](docs/dashboard-model.md).
|
|
@@ -157,6 +242,8 @@ back on the registry is 0.3.0.
|
|
|
157
242
|
|
|
158
243
|
| Version | Date | Status | Contains |
|
|
159
244
|
|---|---|---|---|
|
|
245
|
+
| 0.4.0 | 2026-09-01 | `beta` | FIRST BETA — 40-issue hardening burn (DoD pre-flight + `plan sync-dod`, merge-wave lock + gated-HEAD, load-aware tests, claim-sources across all outputs), `tldrx learn` 8-chapter sandbox tutorial (cold-player QA), `tldrx ship` / `tldrx note` / `run gates set`, budget policies + dual-economy wiring, single integration branch for chained epics, epic worktrees live to run close, bilingual docs site |
|
|
246
|
+
| 0.3.1 | 2026-08-31 | `alpha` | Unattended mode (gates_policy agent, review handshake, fixlist, decision cards, dual economy), 6 contact fixes from the first feature-scope runs, colored init, training repair round |
|
|
160
247
|
| 0.3.0 | 2026-08-30 | `alpha` | expert training with provenance, auto gates with an undo, `tldrx status`, seed triage, the token economy (context ledger, `max_reads`, `cost`, `estimate`), `install --claude`, `interview`, the ticket mirror, `--help` with flags and exit codes |
|
|
161
248
|
| 0.2.0 | 2026-08-29 | `alpha` | Build executor (worktree + branch per story, epic branches, DoD gate, reviewer), Watch cards, live dashboard |
|
|
162
249
|
| 0.1.0 | 2026-08-29 | `alpha` | greenfield `init --stack` + `run new --seed`, story/epic/waves schemas, `tldrx budget show\|raise`, sections must hold list items |
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
FactsStore
|
|
4
|
-
|
|
5
|
-
} from "./chunk-x98qs959.js";
|
|
3
|
+
FactsStore
|
|
4
|
+
} from "./chunk-t1ywrfr4.js";
|
|
6
5
|
import {
|
|
7
6
|
parseHookInput,
|
|
8
7
|
readStdin
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-b8kxzna2.js";
|
|
10
9
|
import {
|
|
11
10
|
EventLog
|
|
12
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-rz541e2b.js";
|
|
12
|
+
import"./chunk-c6t5nx0r.js";
|
|
13
13
|
import {
|
|
14
|
+
MAX_FACT_CHARS,
|
|
14
15
|
detectAnswered,
|
|
15
16
|
parseQuestions,
|
|
16
17
|
recordAnswer,
|
|
17
18
|
replaceBlock,
|
|
18
19
|
serializeQuestions
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
import"./chunk-
|
|
21
|
-
import"./chunk-
|
|
22
|
-
import"./chunk-0bt6yb2q.js";
|
|
20
|
+
} from "./chunk-rpcxsqh3.js";
|
|
21
|
+
import"./chunk-m3mewgnw.js";
|
|
22
|
+
import"./chunk-39zh2e44.js";
|
|
23
23
|
import {
|
|
24
24
|
PROJECT_WORK_DIR,
|
|
25
25
|
factsPath
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-sq44k6g2.js";
|
|
27
27
|
|
|
28
28
|
// src/hooks/answer-capture.ts
|
|
29
29
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -68,7 +68,7 @@ function filePathOf(payload) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// src/hooks/lib/workspace.ts
|
|
71
|
-
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
71
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
72
72
|
function locateWork(filePath) {
|
|
73
73
|
if (filePath === "")
|
|
74
74
|
return null;
|
|
@@ -99,8 +99,15 @@ function nowRfc3339() {
|
|
|
99
99
|
|
|
100
100
|
// src/core/answers/captureAnswers.ts
|
|
101
101
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
102
|
+
var TRUNCATION_MARK = " …";
|
|
102
103
|
function factTextFor(title, answer) {
|
|
103
|
-
|
|
104
|
+
const whole = `${title} — ${answer}`;
|
|
105
|
+
if (whole.length <= MAX_FACT_CHARS)
|
|
106
|
+
return whole;
|
|
107
|
+
return `${whole.slice(0, MAX_FACT_CHARS - TRUNCATION_MARK.length)}${TRUNCATION_MARK}`;
|
|
108
|
+
}
|
|
109
|
+
function factWasTruncated(title, answer) {
|
|
110
|
+
return `${title} — ${answer}`.length > MAX_FACT_CHARS;
|
|
104
111
|
}
|
|
105
112
|
function captureAnswers(questionsPath, ctx) {
|
|
106
113
|
if (!existsSync(questionsPath))
|
|
@@ -114,8 +121,10 @@ function captureAnswers(questionsPath, ctx) {
|
|
|
114
121
|
FactsStore.update(factsPath(ctx.root), (store) => {
|
|
115
122
|
for (const block of answered) {
|
|
116
123
|
const area = block.metadata?.area ?? "unscoped";
|
|
124
|
+
const truncated = factWasTruncated(block.title, block.answer);
|
|
117
125
|
const fact = store.append({
|
|
118
126
|
fact: factTextFor(block.title, block.answer),
|
|
127
|
+
...truncated ? { truncated: true } : {},
|
|
119
128
|
area,
|
|
120
129
|
repos: [],
|
|
121
130
|
kind: "answer",
|
|
@@ -8,34 +8,45 @@ import {
|
|
|
8
8
|
readPayload,
|
|
9
9
|
runHook,
|
|
10
10
|
toolInput
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import"./chunk-
|
|
11
|
+
} from "./chunk-tzzwddct.js";
|
|
12
|
+
import"./chunk-b8kxzna2.js";
|
|
13
13
|
import {
|
|
14
14
|
asRunBudget,
|
|
15
15
|
currentActor,
|
|
16
16
|
cursorStage,
|
|
17
|
+
economyFor,
|
|
18
|
+
hostTokensIn,
|
|
19
|
+
isAttendedByHostView,
|
|
20
|
+
isHostTokens,
|
|
17
21
|
loadRunView,
|
|
18
22
|
newestActiveRun,
|
|
19
23
|
nowRfc3339,
|
|
20
24
|
raiseCommand,
|
|
25
|
+
remainingWork,
|
|
26
|
+
renderRunEconomies,
|
|
27
|
+
runSpend,
|
|
21
28
|
shortBy,
|
|
22
29
|
validateRunBudget,
|
|
23
|
-
wouldExceed
|
|
24
|
-
|
|
30
|
+
wouldExceed,
|
|
31
|
+
wouldExceedHostTokens
|
|
32
|
+
} from "./chunk-4cp363kv.js";
|
|
25
33
|
import {
|
|
26
34
|
EventLog
|
|
27
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-rz541e2b.js";
|
|
36
|
+
import"./chunk-9zsqxr6y.js";
|
|
37
|
+
import"./chunk-phmdk72a.js";
|
|
28
38
|
import {
|
|
29
39
|
noteDeprecations
|
|
30
|
-
} from "./chunk-
|
|
31
|
-
import"./chunk-
|
|
40
|
+
} from "./chunk-rpcxsqh3.js";
|
|
41
|
+
import"./chunk-m3mewgnw.js";
|
|
42
|
+
import"./chunk-39zh2e44.js";
|
|
32
43
|
import {
|
|
33
44
|
PROJECT_WORK_DIR,
|
|
34
45
|
findWorkspaceRoot,
|
|
35
46
|
locateWork,
|
|
36
47
|
parseYaml,
|
|
37
48
|
stageYamlPath
|
|
38
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-sq44k6g2.js";
|
|
39
50
|
|
|
40
51
|
// src/hooks/budget-gate.ts
|
|
41
52
|
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
@@ -101,13 +112,69 @@ await runHook("budget-gate", async () => {
|
|
|
101
112
|
}
|
|
102
113
|
if (budget === null)
|
|
103
114
|
failClosed(command, `${view.dir}/budget.yml is missing or unreadable`);
|
|
115
|
+
const attended = isAttendedByHostView(view);
|
|
104
116
|
const stage = cursorStage(view);
|
|
105
|
-
const
|
|
117
|
+
const declared = stage?.budget_usd ?? stageBudgetFromLibrary(root, view.cursor.stage);
|
|
118
|
+
const work = declared === null ? null : remainingWork({
|
|
119
|
+
runDir: view.dir,
|
|
120
|
+
phaseId: view.cursor.phase,
|
|
121
|
+
stageBudgetUsd: declared,
|
|
122
|
+
stageSpentUsd: stage?.cost_usd ?? 0,
|
|
123
|
+
perAgentMaxUsd: budget.per_agent_max_usd,
|
|
124
|
+
maxUsd: null,
|
|
125
|
+
economy: economyFor(budget, view.cursor.phase),
|
|
126
|
+
attended
|
|
127
|
+
});
|
|
128
|
+
const estimate = estimateFor(command, work === null ? null : work.usd);
|
|
106
129
|
if (estimate <= 0)
|
|
107
130
|
return;
|
|
131
|
+
const economies = renderRunEconomies(view);
|
|
132
|
+
const spend = runSpend(view);
|
|
133
|
+
if (isHostTokens(budget, view.cursor.phase)) {
|
|
134
|
+
const tokens = wouldExceedHostTokens(budget, view.cursor.phase, hostTokensIn(view, view.cursor.phase));
|
|
135
|
+
const over = tokens !== null && tokens.over ? ` ${view.cursor.phase} is OVER its host-token ceiling: ` + `${String(tokens.spent)} declared of ${String(tokens.ceiling)} allowed.` : "";
|
|
136
|
+
const stops = tokens !== null && tokens.blocked && !attended;
|
|
137
|
+
if (over !== "") {
|
|
138
|
+
recordBudgetEvent(view, view.cursor.stage, stops ? "budget.blocked" : "budget.warned", {
|
|
139
|
+
phase: view.cursor.phase,
|
|
140
|
+
scope: tokens?.scope ?? "phase",
|
|
141
|
+
economy: "host-tokens",
|
|
142
|
+
attended_by: view.attended_by,
|
|
143
|
+
host_tokens: tokens?.spent ?? 0,
|
|
144
|
+
ceiling_tokens: tokens?.ceiling ?? 0,
|
|
145
|
+
estimate_usd: estimate,
|
|
146
|
+
metered_usd: spend.meteredUsd,
|
|
147
|
+
unmetered_tasks: spend.unmeteredTasks
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (stops && tokens !== null) {
|
|
151
|
+
deny(`[tldrx] budget-gate: refusing to start stage "${view.cursor.stage}" — phase ${view.cursor.phase} is ` + `priced in \`host-tokens\` and has declared ${String(tokens.spent)} of ${String(tokens.ceiling)} ` + "allowed. Raise that phase's ceiling in budget.yml (under this economy the number is a TOKEN " + "allowance), or set `on_host_tokens_exceed: warn` to go back to a note." + `${economies === null ? "" : `
|
|
152
|
+
${economies}`}`);
|
|
153
|
+
}
|
|
154
|
+
process.stderr.write(`tldrx hook budget-gate: ${view.cursor.phase} is priced in \`host-tokens\` — ` + "no dollar ceiling to enforce here; `tldrx next` refuses a headless spawn on it." + over + `${economies === null ? "" : ` ${economies}`}
|
|
155
|
+
`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
108
158
|
const decision = wouldExceed(budget, view.cursor.phase, estimate);
|
|
109
159
|
if (!decision.blocked)
|
|
110
160
|
return;
|
|
161
|
+
if (attended) {
|
|
162
|
+
recordBudgetEvent(view, view.cursor.stage, "budget.warned", {
|
|
163
|
+
phase: view.cursor.phase,
|
|
164
|
+
scope: decision.scope,
|
|
165
|
+
remaining_usd: decision.remaining,
|
|
166
|
+
ceiling_usd: decision.ceiling,
|
|
167
|
+
estimate_usd: decision.estimate,
|
|
168
|
+
economy: economyFor(budget, view.cursor.phase),
|
|
169
|
+
attended_by: view.attended_by,
|
|
170
|
+
metered_usd: spend.meteredUsd,
|
|
171
|
+
host_tokens: spend.hostTokens,
|
|
172
|
+
unmetered_tasks: spend.unmeteredTasks
|
|
173
|
+
});
|
|
174
|
+
process.stderr.write(`tldrx hook budget-gate: ${view.cursor.phase} has $${decision.remaining.toFixed(2)} left of ` + `$${decision.ceiling.toFixed(2)} and the stage estimate is $${estimate.toFixed(2)} — NOT refusing, ` + "because this run is attended_by: host and the framework spawns nothing on it." + `${economies === null ? "" : ` ${economies}`}
|
|
175
|
+
`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
111
178
|
new EventLog(join2(view.dir, "events.jsonl")).tryAppend({
|
|
112
179
|
ts: nowRfc3339(),
|
|
113
180
|
run: view.run,
|
|
@@ -121,11 +188,28 @@ await runHook("budget-gate", async () => {
|
|
|
121
188
|
remaining_usd: decision.remaining,
|
|
122
189
|
ceiling_usd: decision.ceiling,
|
|
123
190
|
estimate_usd: decision.estimate,
|
|
124
|
-
blocked_by: currentActor()
|
|
191
|
+
blocked_by: currentActor(),
|
|
192
|
+
economy: economyFor(budget, view.cursor.phase),
|
|
193
|
+
attended_by: view.attended_by,
|
|
194
|
+
metered_usd: spend.meteredUsd,
|
|
195
|
+
host_tokens: spend.hostTokens,
|
|
196
|
+
unmetered_tasks: spend.unmeteredTasks
|
|
125
197
|
}
|
|
126
198
|
});
|
|
127
|
-
deny(budgetGateDeny(view.cursor.stage, view.cursor.phase, decision.remaining, decision.ceiling, estimate, raiseCommand(view.run, view.cursor.phase, shortBy(estimate, decision.remaining)))
|
|
199
|
+
deny(budgetGateDeny(view.cursor.stage, view.cursor.phase, decision.remaining, decision.ceiling, estimate, raiseCommand(view.run, view.cursor.phase, shortBy(estimate, decision.remaining))) + (economies === null ? "" : `
|
|
200
|
+
${economies}`));
|
|
128
201
|
});
|
|
202
|
+
function recordBudgetEvent(view, stage, type, payload) {
|
|
203
|
+
new EventLog(join2(view.dir, "events.jsonl")).tryAppend({
|
|
204
|
+
ts: nowRfc3339(),
|
|
205
|
+
run: view.run,
|
|
206
|
+
stage,
|
|
207
|
+
type,
|
|
208
|
+
actor: "hook:budget-gate",
|
|
209
|
+
cost_usd: 0,
|
|
210
|
+
payload
|
|
211
|
+
});
|
|
212
|
+
}
|
|
129
213
|
function estimateFor(command, stageBudget) {
|
|
130
214
|
const flagged = Number(MAX_USD_RE.exec(command)?.[1] ?? MAX_BUDGET_RE.exec(command)?.[1] ?? NaN);
|
|
131
215
|
if (/^tldrx run auto\b/.test(command)) {
|
|
@@ -67,6 +67,15 @@ function requireString(value, path, issues) {
|
|
|
67
67
|
issues.push({ path, message: `expected a string, got ${describe(value)}` });
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
function requireRecord(value, path, issues) {
|
|
71
|
+
if (value === undefined)
|
|
72
|
+
return false;
|
|
73
|
+
if (!isRecord(value)) {
|
|
74
|
+
issues.push({ path, message: `expected a mapping, got ${describe(value)}` });
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
70
79
|
function asDocument(input, issues) {
|
|
71
80
|
if (!isRecord(input)) {
|
|
72
81
|
issues.push({ path: "", message: `expected a mapping at the document root, got ${describe(input)}` });
|
|
@@ -85,4 +94,4 @@ function describeValue(value) {
|
|
|
85
94
|
return typeof value === "number" || typeof value === "boolean" ? String(value) : describe(value);
|
|
86
95
|
}
|
|
87
96
|
|
|
88
|
-
export { result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, asDocument };
|
|
97
|
+
export { result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, requireRecord, asDocument };
|