tldr-experts 0.3.1 → 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 +963 -0
- package/README.md +73 -5
- package/dist/hooks/answer-capture.js +8 -8
- package/dist/hooks/budget-gate.js +78 -14
- package/dist/hooks/{chunk-zdxgragg.js → chunk-4cp363kv.js} +118 -7
- package/dist/hooks/{chunk-afamdvyn.js → chunk-7y2dq0pj.js} +1 -1
- package/dist/hooks/{chunk-458wgg9j.js → chunk-9zsqxr6y.js} +70 -2
- package/dist/hooks/{chunk-s5qsb4k6.js → chunk-b8kxzna2.js} +1 -1
- package/dist/hooks/{chunk-9kkm6q0t.js → chunk-c6t5nx0r.js} +35 -3
- package/dist/hooks/{chunk-sznsenee.js → chunk-m3mewgnw.js} +36 -17
- package/dist/hooks/{chunk-0z27twdk.js → chunk-phmdk72a.js} +10 -5
- package/dist/hooks/{chunk-3t91gvpp.js → chunk-rpcxsqh3.js} +8 -2
- package/dist/hooks/{chunk-ybacnpxd.js → chunk-rz541e2b.js} +3 -0
- package/dist/hooks/{chunk-xpxe94qe.js → chunk-s1c5h7yx.js} +54 -25
- package/dist/hooks/{chunk-ztczwtj0.js → chunk-sq44k6g2.js} +152 -14
- package/dist/hooks/{chunk-ytvmc5ns.js → chunk-t1ywrfr4.js} +19 -23
- package/dist/hooks/{chunk-3g61yg59.js → chunk-tzzwddct.js} +1 -1
- package/dist/hooks/claim-sources.js +28 -20
- package/dist/hooks/dod-gate.js +5 -5
- package/dist/hooks/no-reask.js +8 -8
- package/dist/hooks/session-start.js +12 -12
- package/dist/hooks/statusline.js +9 -9
- package/dist/tldrx.js +10962 -6172
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/stages/build/stage.yml +2 -1
- package/stages/plan/stage.md +11 -0
- package/templates/epic.md +5 -3
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,6 +40,65 @@ 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`.
|
|
@@ -79,6 +139,9 @@ signs something it should not have, `tldrx reject --stage <phase>/<stage> --note
|
|
|
79
139
|
the cursor back and marks the later stages `stale`. When it is one BUILD STORY you disagree with — a
|
|
80
140
|
story two reviewers refused, which is terminal for the rest of the run —
|
|
81
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.
|
|
82
145
|
What an auto gate cannot do: [`docs/guide/03-runs-and-gates.md`](docs/guide/03-runs-and-gates.md).
|
|
83
146
|
|
|
84
147
|
## What you see while it runs
|
|
@@ -152,7 +215,11 @@ else, because those five are machine-local or regenerated: `.tldrx/graphify-out/
|
|
|
152
215
|
|
|
153
216
|
## Documentation
|
|
154
217
|
|
|
155
|
-
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) ·
|
|
156
223
|
[2 The loop](docs/guide/02-the-loop.md) (the four steps, what a stage file controls, the two execution modes) ·
|
|
157
224
|
[3 Runs and gates](docs/guide/03-runs-and-gates.md) (`run new`→`retro`, gate policy, `run auto`, unlock/cancel, dashboard, tickets) ·
|
|
158
225
|
[4 Experts](docs/guide/04-experts.md) (loading rules, role experts, training, levels) ·
|
|
@@ -175,6 +242,7 @@ back on the registry is 0.3.0.
|
|
|
175
242
|
|
|
176
243
|
| Version | Date | Status | Contains |
|
|
177
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 |
|
|
178
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 |
|
|
179
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 |
|
|
180
248
|
| 0.2.0 | 2026-08-29 | `alpha` | Build executor (worktree + branch per story, epic branches, DoD gate, reviewer), Watch cards, live dashboard |
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
FactsStore
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-t1ywrfr4.js";
|
|
5
5
|
import {
|
|
6
6
|
parseHookInput,
|
|
7
7
|
readStdin
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-b8kxzna2.js";
|
|
9
9
|
import {
|
|
10
10
|
EventLog
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import"./chunk-
|
|
11
|
+
} from "./chunk-rz541e2b.js";
|
|
12
|
+
import"./chunk-c6t5nx0r.js";
|
|
13
13
|
import {
|
|
14
14
|
MAX_FACT_CHARS,
|
|
15
15
|
detectAnswered,
|
|
@@ -17,13 +17,13 @@ import {
|
|
|
17
17
|
recordAnswer,
|
|
18
18
|
replaceBlock,
|
|
19
19
|
serializeQuestions
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import"./chunk-
|
|
20
|
+
} from "./chunk-rpcxsqh3.js";
|
|
21
|
+
import"./chunk-m3mewgnw.js";
|
|
22
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;
|
|
@@ -8,32 +8,37 @@ 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
17
|
economyFor,
|
|
18
|
+
hostTokensIn,
|
|
19
|
+
isAttendedByHostView,
|
|
18
20
|
isHostTokens,
|
|
19
21
|
loadRunView,
|
|
20
22
|
newestActiveRun,
|
|
21
23
|
nowRfc3339,
|
|
22
24
|
raiseCommand,
|
|
23
25
|
remainingWork,
|
|
26
|
+
renderRunEconomies,
|
|
27
|
+
runSpend,
|
|
24
28
|
shortBy,
|
|
25
29
|
validateRunBudget,
|
|
26
|
-
wouldExceed
|
|
27
|
-
|
|
30
|
+
wouldExceed,
|
|
31
|
+
wouldExceedHostTokens
|
|
32
|
+
} from "./chunk-4cp363kv.js";
|
|
28
33
|
import {
|
|
29
34
|
EventLog
|
|
30
|
-
} from "./chunk-
|
|
31
|
-
import"./chunk-
|
|
32
|
-
import"./chunk-
|
|
35
|
+
} from "./chunk-rz541e2b.js";
|
|
36
|
+
import"./chunk-9zsqxr6y.js";
|
|
37
|
+
import"./chunk-phmdk72a.js";
|
|
33
38
|
import {
|
|
34
39
|
noteDeprecations
|
|
35
|
-
} from "./chunk-
|
|
36
|
-
import"./chunk-
|
|
40
|
+
} from "./chunk-rpcxsqh3.js";
|
|
41
|
+
import"./chunk-m3mewgnw.js";
|
|
37
42
|
import"./chunk-39zh2e44.js";
|
|
38
43
|
import {
|
|
39
44
|
PROJECT_WORK_DIR,
|
|
@@ -41,7 +46,7 @@ import {
|
|
|
41
46
|
locateWork,
|
|
42
47
|
parseYaml,
|
|
43
48
|
stageYamlPath
|
|
44
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-sq44k6g2.js";
|
|
45
50
|
|
|
46
51
|
// src/hooks/budget-gate.ts
|
|
47
52
|
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
@@ -107,6 +112,7 @@ await runHook("budget-gate", async () => {
|
|
|
107
112
|
}
|
|
108
113
|
if (budget === null)
|
|
109
114
|
failClosed(command, `${view.dir}/budget.yml is missing or unreadable`);
|
|
115
|
+
const attended = isAttendedByHostView(view);
|
|
110
116
|
const stage = cursorStage(view);
|
|
111
117
|
const declared = stage?.budget_usd ?? stageBudgetFromLibrary(root, view.cursor.stage);
|
|
112
118
|
const work = declared === null ? null : remainingWork({
|
|
@@ -116,18 +122,59 @@ await runHook("budget-gate", async () => {
|
|
|
116
122
|
stageSpentUsd: stage?.cost_usd ?? 0,
|
|
117
123
|
perAgentMaxUsd: budget.per_agent_max_usd,
|
|
118
124
|
maxUsd: null,
|
|
119
|
-
economy: economyFor(budget, view.cursor.phase)
|
|
125
|
+
economy: economyFor(budget, view.cursor.phase),
|
|
126
|
+
attended
|
|
120
127
|
});
|
|
121
128
|
const estimate = estimateFor(command, work === null ? null : work.usd);
|
|
122
129
|
if (estimate <= 0)
|
|
123
130
|
return;
|
|
131
|
+
const economies = renderRunEconomies(view);
|
|
132
|
+
const spend = runSpend(view);
|
|
124
133
|
if (isHostTokens(budget, view.cursor.phase)) {
|
|
125
|
-
|
|
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
|
+
`);
|
|
126
156
|
return;
|
|
127
157
|
}
|
|
128
158
|
const decision = wouldExceed(budget, view.cursor.phase, estimate);
|
|
129
159
|
if (!decision.blocked)
|
|
130
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
|
+
}
|
|
131
178
|
new EventLog(join2(view.dir, "events.jsonl")).tryAppend({
|
|
132
179
|
ts: nowRfc3339(),
|
|
133
180
|
run: view.run,
|
|
@@ -141,11 +188,28 @@ await runHook("budget-gate", async () => {
|
|
|
141
188
|
remaining_usd: decision.remaining,
|
|
142
189
|
ceiling_usd: decision.ceiling,
|
|
143
190
|
estimate_usd: decision.estimate,
|
|
144
|
-
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
|
|
145
197
|
}
|
|
146
198
|
});
|
|
147
|
-
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}`));
|
|
148
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
|
+
}
|
|
149
213
|
function estimateFor(command, stageBudget) {
|
|
150
214
|
const flagged = Number(MAX_USD_RE.exec(command)?.[1] ?? MAX_BUDGET_RE.exec(command)?.[1] ?? NaN);
|
|
151
215
|
if (/^tldrx run auto\b/.test(command)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseHandoff
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-9zsqxr6y.js";
|
|
4
4
|
import {
|
|
5
5
|
MAX_PLAN_STORIES,
|
|
6
6
|
MAX_STORIES_PER_WAVE,
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
requirePattern,
|
|
12
12
|
requireStringList,
|
|
13
13
|
requireVersion1
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-phmdk72a.js";
|
|
15
15
|
import {
|
|
16
16
|
classifySrc
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-m3mewgnw.js";
|
|
18
18
|
import {
|
|
19
19
|
asDocument,
|
|
20
20
|
isRecord,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
PROJECT_FRAMEWORK_DIR,
|
|
32
32
|
listRunDirs,
|
|
33
33
|
parseYaml
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-sq44k6g2.js";
|
|
35
35
|
|
|
36
36
|
// src/hooks/lib/runFile.ts
|
|
37
37
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -67,7 +67,8 @@ function loadRunView(runDir) {
|
|
|
67
67
|
status: str(s?.status),
|
|
68
68
|
expert: typeof s?.expert === "string" ? s.expert : null,
|
|
69
69
|
budget_usd: typeof s?.budget_usd === "number" ? s.budget_usd : null,
|
|
70
|
-
cost_usd: typeof s?.cost_usd === "number" ? s.cost_usd : null
|
|
70
|
+
cost_usd: typeof s?.cost_usd === "number" ? s.cost_usd : null,
|
|
71
|
+
tasks: tasksOf(s?.tasks)
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
}
|
|
@@ -81,10 +82,69 @@ function loadRunView(runDir) {
|
|
|
81
82
|
scope: str(doc.scope),
|
|
82
83
|
status: str(doc.status),
|
|
83
84
|
updated_at: str(doc.updated_at),
|
|
85
|
+
attended_by: typeof doc.attended_by === "string" && doc.attended_by !== "" ? doc.attended_by : null,
|
|
84
86
|
cursor,
|
|
85
87
|
phases
|
|
86
88
|
};
|
|
87
89
|
}
|
|
90
|
+
function tasksOf(value) {
|
|
91
|
+
if (!Array.isArray(value))
|
|
92
|
+
return [];
|
|
93
|
+
return value.map((task) => ({
|
|
94
|
+
cost_usd: typeof task?.cost_usd === "number" ? task.cost_usd : null,
|
|
95
|
+
metered: task?.metered !== false,
|
|
96
|
+
tokens: typeof task?.tokens === "number" ? task.tokens : null
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
function isAttendedByHostView(view) {
|
|
100
|
+
return view.attended_by === "host";
|
|
101
|
+
}
|
|
102
|
+
function runSpend(view) {
|
|
103
|
+
let meteredUsd = 0;
|
|
104
|
+
let hostTokens = 0;
|
|
105
|
+
let unmeteredTasks = 0;
|
|
106
|
+
for (const phase of view.phases) {
|
|
107
|
+
for (const stage of phase.stages) {
|
|
108
|
+
for (const task of stage.tasks) {
|
|
109
|
+
if (task.cost_usd !== null)
|
|
110
|
+
meteredUsd += task.cost_usd;
|
|
111
|
+
if (!task.metered || task.cost_usd === null)
|
|
112
|
+
unmeteredTasks += 1;
|
|
113
|
+
if (task.tokens !== null)
|
|
114
|
+
hostTokens += task.tokens;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { meteredUsd: Math.round(meteredUsd * 100) / 100, hostTokens, unmeteredTasks };
|
|
119
|
+
}
|
|
120
|
+
function hostTokensIn(view, phaseId) {
|
|
121
|
+
let tokens = 0;
|
|
122
|
+
for (const phase of view.phases) {
|
|
123
|
+
if (phaseId !== null && phase.id !== phaseId)
|
|
124
|
+
continue;
|
|
125
|
+
for (const stage of phase.stages) {
|
|
126
|
+
for (const task of stage.tasks) {
|
|
127
|
+
if (task.tokens !== null)
|
|
128
|
+
tokens += task.tokens;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return tokens;
|
|
133
|
+
}
|
|
134
|
+
function renderRunEconomies(view) {
|
|
135
|
+
const spend = runSpend(view);
|
|
136
|
+
const attended = isAttendedByHostView(view);
|
|
137
|
+
if (spend.hostTokens === 0 && spend.unmeteredTasks === 0 && !attended)
|
|
138
|
+
return null;
|
|
139
|
+
const parts = [`$${spend.meteredUsd.toFixed(2)} metered`];
|
|
140
|
+
if (spend.hostTokens > 0)
|
|
141
|
+
parts.push(`${String(spend.hostTokens)} host tokens`);
|
|
142
|
+
if (spend.unmeteredTasks > 0) {
|
|
143
|
+
parts.push(`${String(spend.unmeteredTasks)} unmetered turn${spend.unmeteredTasks === 1 ? "" : "s"}`);
|
|
144
|
+
}
|
|
145
|
+
const who = attended ? " (attended_by: host — the framework does not spawn on this run)" : "";
|
|
146
|
+
return `spend so far: ${parts.join(" + ")}${who}. ` + "The dollar figure is METERED spend only; host tokens are a different currency and are never converted.";
|
|
147
|
+
}
|
|
88
148
|
function isTerminal(status) {
|
|
89
149
|
return TERMINAL_STATUSES.includes(status);
|
|
90
150
|
}
|
|
@@ -126,6 +186,8 @@ function nowRfc3339() {
|
|
|
126
186
|
|
|
127
187
|
// src/core/budget/RunBudget.ts
|
|
128
188
|
var ON_EXCEED = ["block", "warn"];
|
|
189
|
+
var ON_HOST_TOKENS_EXCEED = ["warn", "block"];
|
|
190
|
+
var DEFAULT_ON_HOST_TOKENS_EXCEED = "warn";
|
|
129
191
|
var ECONOMIES = ["metered-usd", "host-tokens"];
|
|
130
192
|
var DEFAULT_ECONOMY = "metered-usd";
|
|
131
193
|
var DEFAULT_WARN_AT_PCT = 80;
|
|
@@ -158,6 +220,9 @@ function validateRunBudget(input) {
|
|
|
158
220
|
if (doc.economy !== undefined && doc.economy !== null) {
|
|
159
221
|
requireEnum(doc.economy, ECONOMIES, "economy", issues);
|
|
160
222
|
}
|
|
223
|
+
if (doc.on_host_tokens_exceed !== undefined && doc.on_host_tokens_exceed !== null) {
|
|
224
|
+
requireEnum(doc.on_host_tokens_exceed, ON_HOST_TOKENS_EXCEED, "on_host_tokens_exceed", issues);
|
|
225
|
+
}
|
|
161
226
|
if (doc.warn_at_pct !== undefined) {
|
|
162
227
|
requireNumber(doc.warn_at_pct, "warn_at_pct", issues);
|
|
163
228
|
const pct = doc.warn_at_pct;
|
|
@@ -203,6 +268,7 @@ function asRunBudget(input) {
|
|
|
203
268
|
warn_at_pct: doc.warn_at_pct ?? DEFAULT_WARN_AT_PCT,
|
|
204
269
|
on_exceed: doc.on_exceed ?? "block",
|
|
205
270
|
economy: doc.economy ?? DEFAULT_ECONOMY,
|
|
271
|
+
on_host_tokens_exceed: doc.on_host_tokens_exceed ?? DEFAULT_ON_HOST_TOKENS_EXCEED,
|
|
206
272
|
phases: (doc.phases ?? []).map((phase) => ({
|
|
207
273
|
id: phase.id,
|
|
208
274
|
ceiling_usd: phase.ceiling_usd,
|
|
@@ -211,6 +277,16 @@ function asRunBudget(input) {
|
|
|
211
277
|
}))
|
|
212
278
|
};
|
|
213
279
|
}
|
|
280
|
+
function hostTokenCeiling(budget, phaseId) {
|
|
281
|
+
if (budget === null || !isHostTokens(budget, phaseId))
|
|
282
|
+
return null;
|
|
283
|
+
if (phaseId !== undefined && phaseId !== null) {
|
|
284
|
+
const phase = budget.phases.find((entry) => entry.id === phaseId);
|
|
285
|
+
if (phase !== undefined)
|
|
286
|
+
return phase.ceiling_usd;
|
|
287
|
+
}
|
|
288
|
+
return budget.ceiling_usd;
|
|
289
|
+
}
|
|
214
290
|
|
|
215
291
|
// src/core/budget/remainingWork.ts
|
|
216
292
|
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
|
|
@@ -326,6 +402,12 @@ var SKIPPED_DIRS = new Set([
|
|
|
326
402
|
"test-results"
|
|
327
403
|
]);
|
|
328
404
|
|
|
405
|
+
// src/core/plan/branchModel.ts
|
|
406
|
+
var BRANCH_MODELS = ["per-epic", "integration"];
|
|
407
|
+
function isBranchModelKind(value) {
|
|
408
|
+
return typeof value === "string" && BRANCH_MODELS.includes(value);
|
|
409
|
+
}
|
|
410
|
+
|
|
329
411
|
// src/core/experts/expertDocument.ts
|
|
330
412
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
331
413
|
import { join as join3 } from "node:path";
|
|
@@ -1082,6 +1164,8 @@ function round(n) {
|
|
|
1082
1164
|
}
|
|
1083
1165
|
|
|
1084
1166
|
// src/core/build/review.ts
|
|
1167
|
+
var VERDICT_WORDS = ["approve", "fixlist", "changes"];
|
|
1168
|
+
var VERDICT_ENUM = VERDICT_WORDS.join("|");
|
|
1085
1169
|
var REVIEWER_FAILED = "the reviewer sub-agent failed";
|
|
1086
1170
|
function looksLikeReviewerError(detail) {
|
|
1087
1171
|
const text = detail.trim();
|
|
@@ -1111,6 +1195,7 @@ function staticOnly(input) {
|
|
|
1111
1195
|
staticUsd: input.stageBudgetUsd,
|
|
1112
1196
|
clamped: false,
|
|
1113
1197
|
economy: input.economy ?? DEFAULT_ECONOMY,
|
|
1198
|
+
attended: input.attended === true,
|
|
1114
1199
|
stories: [],
|
|
1115
1200
|
done: 0,
|
|
1116
1201
|
total: 0,
|
|
@@ -1119,6 +1204,7 @@ function staticOnly(input) {
|
|
|
1119
1204
|
}
|
|
1120
1205
|
function measure(input) {
|
|
1121
1206
|
const economy = input.economy ?? DEFAULT_ECONOMY;
|
|
1207
|
+
const hostPaysDeveloper = economy === "host-tokens" || input.attended === true;
|
|
1122
1208
|
const fallback = {
|
|
1123
1209
|
basis: "static",
|
|
1124
1210
|
usd: input.stageBudgetUsd,
|
|
@@ -1126,6 +1212,7 @@ function measure(input) {
|
|
|
1126
1212
|
staticUsd: input.stageBudgetUsd,
|
|
1127
1213
|
clamped: false,
|
|
1128
1214
|
economy,
|
|
1215
|
+
attended: input.attended === true,
|
|
1129
1216
|
stories: [],
|
|
1130
1217
|
done: 0,
|
|
1131
1218
|
total: 0,
|
|
@@ -1157,7 +1244,7 @@ function measure(input) {
|
|
|
1157
1244
|
if (attemptsLeft === 0)
|
|
1158
1245
|
continue;
|
|
1159
1246
|
const developerTurns = Math.max(attemptsLeft - (story.status === "review" ? 1 : 0), 0);
|
|
1160
|
-
const developerCapUsd =
|
|
1247
|
+
const developerCapUsd = hostPaysDeveloper ? 0 : caps.developer(story.id);
|
|
1161
1248
|
const reviewerCapUsd = caps.reviewer(story.id);
|
|
1162
1249
|
const usd = round2(developerCapUsd * developerTurns + reviewerCapUsd * attemptsLeft);
|
|
1163
1250
|
if (usd <= 0 && developerTurns === 0 && attemptsLeft === 0)
|
|
@@ -1182,6 +1269,7 @@ function measure(input) {
|
|
|
1182
1269
|
staticUsd: input.stageBudgetUsd,
|
|
1183
1270
|
clamped,
|
|
1184
1271
|
economy,
|
|
1272
|
+
attended: input.attended === true,
|
|
1185
1273
|
stories,
|
|
1186
1274
|
done: progress.done,
|
|
1187
1275
|
total: progress.total,
|
|
@@ -1314,6 +1402,20 @@ function wouldExceed(budget, phaseId, estimate) {
|
|
|
1314
1402
|
function round3(n) {
|
|
1315
1403
|
return Math.round(n * 1e6) / 1e6;
|
|
1316
1404
|
}
|
|
1405
|
+
function wouldExceedHostTokens(budget, phaseId, spentTokens) {
|
|
1406
|
+
const ceiling = hostTokenCeiling(budget, phaseId);
|
|
1407
|
+
if (ceiling === null)
|
|
1408
|
+
return null;
|
|
1409
|
+
const phase = phaseId === null ? undefined : budget.phases.find((p) => p.id === phaseId);
|
|
1410
|
+
const over = ceiling > 0 && spentTokens > ceiling;
|
|
1411
|
+
return {
|
|
1412
|
+
over,
|
|
1413
|
+
blocked: over && budget.on_host_tokens_exceed === "block",
|
|
1414
|
+
scope: phase === undefined ? "run" : "phase",
|
|
1415
|
+
spent: spentTokens,
|
|
1416
|
+
ceiling
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1317
1419
|
|
|
1318
1420
|
// src/core/run/gatePolicy.ts
|
|
1319
1421
|
var GATE_POLICIES = ["human", "auto", "agent"];
|
|
@@ -1450,6 +1552,12 @@ function validateRunFile(input) {
|
|
|
1450
1552
|
requireString(branch, `build.epic_branch[${i}]`, issues);
|
|
1451
1553
|
});
|
|
1452
1554
|
}
|
|
1555
|
+
if (doc.build.branch_model !== undefined && !isBranchModelKind(doc.build.branch_model)) {
|
|
1556
|
+
issues.push({
|
|
1557
|
+
path: "build.branch_model",
|
|
1558
|
+
message: `expected one of ${BRANCH_MODELS.join(" | ")}`
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1453
1561
|
} else {
|
|
1454
1562
|
issues.push({ path: "build", message: "expected a mapping" });
|
|
1455
1563
|
}
|
|
@@ -1479,6 +1587,9 @@ function validateRunFile(input) {
|
|
|
1479
1587
|
}
|
|
1480
1588
|
if (doc.attended_by !== undefined)
|
|
1481
1589
|
requireEnum(doc.attended_by, ATTENDED_BY, "attended_by", issues);
|
|
1590
|
+
if (doc.keep_worktrees !== undefined && typeof doc.keep_worktrees !== "boolean") {
|
|
1591
|
+
issues.push({ path: "keep_worktrees", message: `expected a boolean, got ${typeof doc.keep_worktrees}` });
|
|
1592
|
+
}
|
|
1482
1593
|
if (!requireArray(doc.phases, "phases", issues))
|
|
1483
1594
|
return result(issues, deprecations);
|
|
1484
1595
|
const phases = doc.phases;
|
|
@@ -1652,4 +1763,4 @@ function shortBy(estimate, remaining) {
|
|
|
1652
1763
|
return Math.max(0.01, Math.ceil((estimate - remaining) * 100) / 100);
|
|
1653
1764
|
}
|
|
1654
1765
|
|
|
1655
|
-
export { loadRunView, cursorStage, openRunViews, newestActiveRun, currentActor, nowRfc3339, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, parseEvidence, evidencePath, gateEvidencePath, stackExpertNames, remainingWork, wouldExceed, isTerminal2 as isTerminal, isFinished, isAttendedByHost, flatten, stageAt, deriveRunStatus, derivePhaseStatus, validateRunFile, asRunFile, raiseCommand, shortBy };
|
|
1766
|
+
export { loadRunView, isAttendedByHostView, runSpend, hostTokensIn, renderRunEconomies, cursorStage, openRunViews, newestActiveRun, currentActor, nowRfc3339, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, parseEvidence, evidencePath, gateEvidencePath, stackExpertNames, remainingWork, wouldExceed, wouldExceedHostTokens, isTerminal2 as isTerminal, isFinished, isAttendedByHost, flatten, stageAt, deriveRunStatus, derivePhaseStatus, validateRunFile, asRunFile, raiseCommand, shortBy };
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
hasSrcMarker,
|
|
3
3
|
parseSrcToken,
|
|
4
4
|
resolveSrc
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-m3mewgnw.js";
|
|
6
6
|
|
|
7
7
|
// src/core/text/handoff.ts
|
|
8
8
|
var HANDOFF_SECTIONS = ["Findings", "Decisions", "Unknowns", "Evidence ledger"];
|
|
@@ -61,6 +61,42 @@ function parseHandoff(text) {
|
|
|
61
61
|
flush();
|
|
62
62
|
return { sections, headings };
|
|
63
63
|
}
|
|
64
|
+
function parseItems(text) {
|
|
65
|
+
const items = [];
|
|
66
|
+
let section = "";
|
|
67
|
+
let pending = null;
|
|
68
|
+
const flush = () => {
|
|
69
|
+
if (pending === null)
|
|
70
|
+
return;
|
|
71
|
+
const joined = pending.parts.join(" ");
|
|
72
|
+
items.push({ line: pending.line, text: joined, section: pending.section, token: parseSrcToken(joined) });
|
|
73
|
+
pending = null;
|
|
74
|
+
};
|
|
75
|
+
const lines = text.split(`
|
|
76
|
+
`);
|
|
77
|
+
for (let i = 0;i < lines.length; i++) {
|
|
78
|
+
const line = lines[i] ?? "";
|
|
79
|
+
const heading = H2_RE.exec(line);
|
|
80
|
+
if (heading !== null && heading[1] !== undefined) {
|
|
81
|
+
flush();
|
|
82
|
+
section = heading[1];
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const bullet = BULLET_RE.exec(line);
|
|
86
|
+
if (bullet !== null && bullet[1] !== undefined) {
|
|
87
|
+
flush();
|
|
88
|
+
pending = { line: i + 1, parts: [bullet[1]], section };
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (pending !== null && CONTINUATION_RE.test(line)) {
|
|
92
|
+
pending.parts.push(line.trim());
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
flush();
|
|
96
|
+
}
|
|
97
|
+
flush();
|
|
98
|
+
return items;
|
|
99
|
+
}
|
|
64
100
|
function isHandoff(text) {
|
|
65
101
|
return missingSections(parseHandoff(text)).length === 0;
|
|
66
102
|
}
|
|
@@ -141,5 +177,37 @@ function validateHandoff(text, ctx) {
|
|
|
141
177
|
bulletCount: report.bulletCount
|
|
142
178
|
};
|
|
143
179
|
}
|
|
180
|
+
function validateCitations(text, ctx) {
|
|
181
|
+
const malformed = [];
|
|
182
|
+
const unresolved = [];
|
|
183
|
+
const unverified = [];
|
|
184
|
+
let cited = 0;
|
|
185
|
+
for (const item of parseItems(text)) {
|
|
186
|
+
if (item.token === null) {
|
|
187
|
+
if (!hasSrcMarker(item.text))
|
|
188
|
+
continue;
|
|
189
|
+
cited++;
|
|
190
|
+
malformed.push({
|
|
191
|
+
line: item.line,
|
|
192
|
+
message: "malformed citation — the `[src: …]` token must be the last thing on the line " + "(closing quotes, brackets and a final `.` are allowed after it, words are not)"
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
cited++;
|
|
197
|
+
for (const error of item.token.errors) {
|
|
198
|
+
unresolved.push({ line: item.line, message: `[src: ${error.raw}] — ${error.message}` });
|
|
199
|
+
}
|
|
200
|
+
const claim = item.text.replace(item.token.raw, " ").trim();
|
|
201
|
+
for (const ref of item.token.refs) {
|
|
202
|
+
const resolution = resolveSrc(ref, ctx, item.section, claim);
|
|
203
|
+
const issue = { line: item.line, message: `[src: ${ref.raw}] — ${resolution.message ?? "unresolvable"}` };
|
|
204
|
+
if (resolution.outcome === "refused")
|
|
205
|
+
unresolved.push(issue);
|
|
206
|
+
else if (resolution.outcome === "unverified")
|
|
207
|
+
unverified.push(issue);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { malformed, unresolved, unverified, cited };
|
|
211
|
+
}
|
|
144
212
|
|
|
145
|
-
export { parseHandoff, isHandoff, validateHandoff };
|
|
213
|
+
export { parseHandoff, isHandoff, validateHandoff, validateCitations };
|