tldr-experts 0.4.0 → 0.5.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 +1433 -0
- package/README.md +61 -13
- package/dist/hooks/answer-capture.js +6 -7
- package/dist/hooks/budget-gate.js +14 -11
- package/dist/hooks/{chunk-4cp363kv.js → chunk-14zn51kh.js} +60 -16
- package/dist/hooks/{chunk-s1c5h7yx.js → chunk-3w55tp71.js} +26 -7
- package/dist/hooks/{chunk-7y2dq0pj.js → chunk-5556vjt5.js} +1 -1
- package/dist/hooks/{chunk-t1ywrfr4.js → chunk-889tybxc.js} +10 -8
- package/dist/hooks/{chunk-9gb21660.js → chunk-a5dq2dcp.js} +54 -14
- package/dist/hooks/{chunk-sq44k6g2.js → chunk-a6rpj2cp.js} +684 -3
- package/dist/hooks/{chunk-tzzwddct.js → chunk-bvm6vjrt.js} +1 -1
- package/dist/hooks/{chunk-phmdk72a.js → chunk-hcrbr430.js} +1 -1
- package/dist/hooks/{chunk-rpcxsqh3.js → chunk-nadqsr3w.js} +1 -1
- package/dist/hooks/{chunk-9zsqxr6y.js → chunk-q8d3sff9.js} +23 -16
- package/dist/hooks/{chunk-c6t5nx0r.js → chunk-qw73rdbr.js} +1 -1
- package/dist/hooks/{chunk-rz541e2b.js → chunk-sae7sqty.js} +2 -0
- package/dist/hooks/{chunk-b8kxzna2.js → chunk-v1c1hpb8.js} +1 -1
- package/dist/hooks/claim-sources.js +19 -16
- package/dist/hooks/dod-gate.js +7 -6
- package/dist/hooks/no-reask.js +9 -9
- package/dist/hooks/session-start.js +46 -21
- package/dist/hooks/statusline.js +8 -9
- package/dist/tldrx.js +6364 -2465
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/skills/tldrx/SKILL.md +1 -1
- package/stages/watch/stage.md +4 -0
- package/templates/expert.md +13 -1
- package/templates/watcher.md +8 -1
- package/workflows/bugfix.yml +8 -4
- package/workflows/docs.yml +8 -4
- package/workflows/feature.yml +8 -4
- package/workflows/hotfix.yml +8 -4
- package/workflows/integration.yml +8 -4
- package/workflows/migration.yml +8 -4
- package/workflows/performance.yml +8 -4
- package/workflows/prototype.yml +8 -4
- package/workflows/refactor.yml +8 -4
- package/workflows/security-patch.yml +8 -4
- package/workflows/spike.yml +8 -4
- package/workflows/upgrade.yml +8 -4
- package/dist/hooks/chunk-m3mewgnw.js +0 -522
- package/templates/epic.md +0 -40
- package/templates/story.md +0 -55
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# tldr-experts
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/tldr-experts) [](https://github.com/ederwii/tldr-experts/actions/workflows/ci.yml) ](https://www.npmjs.com/package/tldr-experts) [](https://github.com/ederwii/tldr-experts/actions/workflows/ci.yml) 
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**An evidence-first, file-based AI development framework: five stages, a gate on every one, and every claim cited or refused.** Open source, tool-agnostic in design, piloted on Claude Code. **Beta:** every command is implemented and verified by running it, the `version: 1` file formats only grow from here, and `tldrx --help` is the authoritative command surface.
|
|
6
6
|
|
|
7
7
|
One loop — *Investigate → Handoff → Interview → Gate* — five phases, **what · how · plan · build ·
|
|
8
8
|
watch**, one stage per command, each stopping at a gate you own; the files ARE the state, the
|
|
@@ -21,6 +21,11 @@ tldrx init # detect repos, map the code, write .tldrx/, ask only
|
|
|
21
21
|
tldrx install --claude # write the skill, hooks and status line into ./.claude/
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Later: **`tldrx update`** pulls the newest published version and prints the CHANGELOG between the
|
|
25
|
+
one you had and the one you now have. Any command will tell you, in one line, when there is a newer
|
|
26
|
+
one — off the hot path, cached for a day, silent when it cannot reach the registry, and never in
|
|
27
|
+
`--json` output or during a hook. Turn it off with `TLDRX_UPDATE_CHECK=off`.
|
|
28
|
+
|
|
24
29
|
**Never used it before?** `tldrx learn` teaches the loop by running it: eight chapters, ~15 minutes,
|
|
25
30
|
in a throwaway sandbox with a toy repo and a stand-in agent. Every command in it is the real one —
|
|
26
31
|
`init`, `run new`, `next`, `approve`, a Build that cuts a branch and runs a real DoD — so nothing it
|
|
@@ -63,7 +68,21 @@ and names the `--prepare` command instead.
|
|
|
63
68
|
|
|
64
69
|
### Overnight, with the checking kept
|
|
65
70
|
|
|
66
|
-
Two commands and a prompt
|
|
71
|
+
Two commands and a prompt — and the prompt now ships with the package:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
tldrx drive --unattended # print the mandate; paste it into the session that drives the run
|
|
75
|
+
tldrx drive --attended # the same disciplines, but every gate stays yours to sign
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`tldrx drive` needs no workspace, opens no run and writes nothing: it prints the discipline the
|
|
79
|
+
first real runs were driven by — the three-role protocol (developer → a **fresh** adversarial
|
|
80
|
+
reviewer, never the author → the host verifying both in the code, not in their reports), evidence
|
|
81
|
+
labelled `measured` / `inferred` / `assumed`, product questions parked rather than decided, the
|
|
82
|
+
reviewer calibrated to the story's stakes, and the cost declared once. The two modes differ in
|
|
83
|
+
exactly two places: who drives the turns, and who may close a gate.
|
|
84
|
+
|
|
85
|
+
The rest of this section is what that mandate says, in the shape you would type it by hand.
|
|
67
86
|
|
|
68
87
|
```bash
|
|
69
88
|
tldrx run new payments --scope feature --budget 25 \
|
|
@@ -138,7 +157,11 @@ Those are the shipped defaults, and every scope keeps at least one human gate. O
|
|
|
138
157
|
signs something it should not have, `tldrx reject --stage <phase>/<stage> --note "…"` revokes it, moves
|
|
139
158
|
the cursor back and marks the later stages `stale`. When it is one BUILD STORY you disagree with — a
|
|
140
159
|
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
|
|
160
|
+
`tldrx story reopen <id> --note "…"` gives that one story another run of attempts and nothing else;
|
|
161
|
+
for one named defect in a story already `done`, `--for-fix` opens a fix round instead — no attempt
|
|
162
|
+
consumed, the same DoD and the same reviewer, one open round at a time.
|
|
163
|
+
To move who may close a gate after `run new` froze it, `tldrx run gates set <stage>:<policy> --note "…"`
|
|
164
|
+
is the only sanctioned way, and it records the old→new value with your reason.
|
|
142
165
|
When you fix `.tldrx/workspace.yml` mid-run and the approved stories still cite the old command strings,
|
|
143
166
|
`tldrx plan sync-dod` rewrites just their dod lines — renames followed, removed commands dropped, and
|
|
144
167
|
anything with no ancestor in the file's history flagged rather than guessed at.
|
|
@@ -190,7 +213,7 @@ context 83.7 KB of 160.0 KB (~23.8k tok, 12% of sonnet's ~200.0k window)
|
|
|
190
213
|
```
|
|
191
214
|
|
|
192
215
|
Over `prompt_max_bytes` the stage is **refused** (exit 2) before anything spawns; `max_reads` stops
|
|
193
|
-
the sub-agent at a read ceiling; `--effort` changes what a turn costs. What `--max-
|
|
216
|
+
the sub-agent at a read ceiling; `--effort` changes what a turn costs. What `--max-usd` does is
|
|
194
217
|
end a run *after* the turn it is already in — measured, one turn spent **$5.15** against a **$1.50**
|
|
195
218
|
ceiling — so size the prompt for the money you are willing to lose. Afterwards `tldrx cost [--all]` adds up what was actually charged, per attempt, per stage, per
|
|
196
219
|
run, read off `agent.result` events and nothing else. Retries are never merged — a retry is
|
|
@@ -200,18 +223,32 @@ Details: [`docs/guide/06-budgets-and-cost.md`](docs/guide/06-budgets-and-cost.md
|
|
|
200
223
|
|
|
201
224
|
## Several runs
|
|
202
225
|
|
|
203
|
-
With several runs open and no id,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
226
|
+
With several runs open and no id, a run-targeting command **refuses rather than guessing** and
|
|
227
|
+
lists them — `tldrx next: 3 runs are open — pass one:`. That means "you left off the id", not "it
|
|
228
|
+
broke". Two of them refuse differently and it is worth knowing which: `tldrx run status` is not a
|
|
229
|
+
refusal at all — it lists every open run and exits `0`, and it is the screen you read to find the
|
|
230
|
+
id the others want — and `tldrx cost` refuses at exit `1`, not `2`.
|
|
231
|
+
|
|
232
|
+
Most run-targeting commands take the id either way, a positional `<run>` or `--run <id>`: `next`,
|
|
233
|
+
`cost`, `note`, `gate template`, `questions`, `budget show`, `ship`, `tickets`, and `run attend` ·
|
|
234
|
+
`status` · `estimate` · `auto` · `unlock` · `cancel`. `replay` and `retro` take the positional only
|
|
235
|
+
— `--run` there is an unknown flag. `approve`, `reject`, `answer`, `interview`, `plan`,
|
|
236
|
+
`story reopen`, `watch` and `run gates set` take `--run <id>` only.
|
|
237
|
+
|
|
238
|
+
`tldrx retro --all` goes the other way: it reads **every** run in the workspace and prints one
|
|
239
|
+
table of what keeps catching you — finding class × count × how many runs × one example with its
|
|
240
|
+
citation — mined from the review logs, the fix lists, `retro.md` and the `story.reopened` reasons.
|
|
241
|
+
Strictly read-only: it writes nothing, anywhere.
|
|
207
242
|
|
|
208
243
|
## What to commit
|
|
209
244
|
|
|
210
245
|
**Both `.tldrx/` and `tldrx-work/`.** The files are the state — the map, the facts, the questions and
|
|
211
246
|
their answers, `run.yml`, `budget.yml`, `events.jsonl`, the handoffs, the plan — so a teammate who clones
|
|
212
|
-
the repo gets the run. The block `tldrx init` appends to `.gitignore` excludes
|
|
213
|
-
else, because those
|
|
214
|
-
`.tldrx/worktrees/`, `tldrx-work/*/.lock`,
|
|
247
|
+
the repo gets the run. The block `tldrx init` appends to `.gitignore` excludes eight paths and nothing
|
|
248
|
+
else, because those eight are machine-local, regenerated, or a backup git already holds the history
|
|
249
|
+
of: `.tldrx/graphify-out/`, `.tldrx/cache/`, `.tldrx/worktrees/`, `tldrx-work/*/.lock`,
|
|
250
|
+
`tldrx-work/*/.agent/`, `tldrx-work/*/*.bak`, `.tldrx/memory/*.bak` and
|
|
251
|
+
`.claude/settings.json.bak-tldrx-*`.
|
|
215
252
|
|
|
216
253
|
## Documentation
|
|
217
254
|
|
|
@@ -233,6 +270,12 @@ Design docs: [`docs/concept.md`](docs/concept.md) (why) · [`docs/spec.md`](docs
|
|
|
233
270
|
open decisions) · [`docs/ROADMAP.md`](docs/ROADMAP.md) (next) · [`CHANGELOG.md`](CHANGELOG.md) (shipped) ·
|
|
234
271
|
[`docs/dashboard-model.md`](docs/dashboard-model.md).
|
|
235
272
|
|
|
273
|
+
Contributing: **[`CONTRIBUTING.md`](CONTRIBUTING.md)** — the loop a change goes through, the four gates
|
|
274
|
+
and what CI actually runs, the red-first test rules, and
|
|
275
|
+
[how to contribute a model-provider config](CONTRIBUTING.md#contributing-a-model-provider-config)
|
|
276
|
+
(the `TLDRX_CLAUDE_BIN` seam, the `stream-json` transcript contract, and what a generic provider
|
|
277
|
+
would have to supply).
|
|
278
|
+
|
|
236
279
|
## Releases and status tags
|
|
237
280
|
|
|
238
281
|
Install name is **`tldr-experts`**; it installs two commands, **`tldrx`** (short) and `tldr-experts` (same binary).
|
|
@@ -242,6 +285,7 @@ back on the registry is 0.3.0.
|
|
|
242
285
|
|
|
243
286
|
| Version | Date | Status | Contains |
|
|
244
287
|
|---|---|---|---|
|
|
288
|
+
| 0.5.0 | 2026-09-02 | `beta` | `tldrx drive` and its own preflight, `watch check` / `watch arm`, `questions cards`, `plan schema`, `retro --all` (with its findings fed back into every reviewer prompt) and `story reopen --for-fix`; `tldrx update` plus a cached newer-version notice; the dashboard reads `budget.yml` and `events.jsonl` — operator notes, reopens and retries, the per-phase budget panel, and a host-attended run metered in tokens against `ceiling_host_tokens`; a rejected review envelope no longer burns a story attempt; `ship` opens one PR per repo; merge-wave lock + ref guard; five golden-transcript evals, one per stage; `CONTRIBUTING.md` and a model-provider contract |
|
|
245
289
|
| 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
290
|
| 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 |
|
|
247
291
|
| 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 |
|
|
@@ -256,6 +300,10 @@ path documented; `stable` = 1.0, semver from here on. The badge above shows the
|
|
|
256
300
|
|
|
257
301
|
## Releasing
|
|
258
302
|
|
|
259
|
-
**One command: `scripts/release.sh X.Y.Z --tag
|
|
303
|
+
**One command: `scripts/release.sh X.Y.Z --tag beta`.** The tag is not optional in practice: omit
|
|
304
|
+
`--tag` and the script writes `alpha`, which is no longer this project's status. It is the only
|
|
305
|
+
sanctioned path — a Claude Code hook denies hand-made `git tag` / `npm publish`, and `publish.yml`
|
|
306
|
+
runs `release-check.sh --ci` (the file checks only) plus its own typecheck, tests and build.
|
|
307
|
+
Checklist and judgement calls: `docs/RELEASING.md`.
|
|
260
308
|
|
|
261
309
|
MIT, © 2026 Alan Martinez — a placeholder made while scaffolding; change it freely before anything ships.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
FactsStore
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-889tybxc.js";
|
|
5
5
|
import {
|
|
6
6
|
parseHookInput,
|
|
7
7
|
readStdin
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-v1c1hpb8.js";
|
|
9
9
|
import {
|
|
10
10
|
EventLog
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import"./chunk-
|
|
11
|
+
} from "./chunk-sae7sqty.js";
|
|
12
|
+
import"./chunk-qw73rdbr.js";
|
|
13
13
|
import {
|
|
14
14
|
MAX_FACT_CHARS,
|
|
15
15
|
detectAnswered,
|
|
@@ -17,13 +17,12 @@ import {
|
|
|
17
17
|
recordAnswer,
|
|
18
18
|
replaceBlock,
|
|
19
19
|
serializeQuestions
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import"./chunk-m3mewgnw.js";
|
|
20
|
+
} from "./chunk-nadqsr3w.js";
|
|
22
21
|
import"./chunk-39zh2e44.js";
|
|
23
22
|
import {
|
|
24
23
|
PROJECT_WORK_DIR,
|
|
25
24
|
factsPath
|
|
26
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-a6rpj2cp.js";
|
|
27
26
|
|
|
28
27
|
// src/hooks/answer-capture.ts
|
|
29
28
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
budgetGateDeny
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-a5dq2dcp.js";
|
|
5
5
|
import {
|
|
6
6
|
allow,
|
|
7
7
|
deny,
|
|
8
8
|
readPayload,
|
|
9
9
|
runHook,
|
|
10
10
|
toolInput
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import"./chunk-
|
|
11
|
+
} from "./chunk-bvm6vjrt.js";
|
|
12
|
+
import"./chunk-v1c1hpb8.js";
|
|
13
13
|
import {
|
|
14
14
|
asRunBudget,
|
|
15
15
|
currentActor,
|
|
@@ -29,24 +29,23 @@ import {
|
|
|
29
29
|
validateRunBudget,
|
|
30
30
|
wouldExceed,
|
|
31
31
|
wouldExceedHostTokens
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-14zn51kh.js";
|
|
33
33
|
import {
|
|
34
34
|
EventLog
|
|
35
|
-
} from "./chunk-
|
|
36
|
-
import"./chunk-
|
|
37
|
-
import"./chunk-phmdk72a.js";
|
|
35
|
+
} from "./chunk-sae7sqty.js";
|
|
36
|
+
import"./chunk-hcrbr430.js";
|
|
38
37
|
import {
|
|
39
38
|
noteDeprecations
|
|
40
|
-
} from "./chunk-
|
|
41
|
-
import"./chunk-m3mewgnw.js";
|
|
39
|
+
} from "./chunk-nadqsr3w.js";
|
|
42
40
|
import"./chunk-39zh2e44.js";
|
|
41
|
+
import"./chunk-q8d3sff9.js";
|
|
43
42
|
import {
|
|
44
43
|
PROJECT_WORK_DIR,
|
|
45
44
|
findWorkspaceRoot,
|
|
46
45
|
locateWork,
|
|
47
46
|
parseYaml,
|
|
48
47
|
stageYamlPath
|
|
49
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-a6rpj2cp.js";
|
|
50
49
|
|
|
51
50
|
// src/hooks/budget-gate.ts
|
|
52
51
|
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
@@ -74,7 +73,9 @@ function loadRunBudget(runDir) {
|
|
|
74
73
|
var SPAWN_RE = /^(claude -p|tldrx next|tldrx run auto|tldrx expert train|tldrx seed triage)\b/;
|
|
75
74
|
var RUN_ARG_RE = /--run[= ]([\w.-]+)/;
|
|
76
75
|
var DEFAULT_TRAIN_USD = 2;
|
|
76
|
+
var DEFAULT_FULL_TRAIN_USD = 3;
|
|
77
77
|
var DEFAULT_TRIAGE_USD = 1;
|
|
78
|
+
var FULL_MODE_RE = /--mode[= ]full\b/;
|
|
78
79
|
var MAX_USD_RE = /--max-usd[= ]([0-9]+(?:\.[0-9]+)?)/;
|
|
79
80
|
var MAX_BUDGET_RE = /--max-budget-usd[= ]([0-9]+(?:\.[0-9]+)?)/;
|
|
80
81
|
await runHook("budget-gate", async () => {
|
|
@@ -216,7 +217,9 @@ function estimateFor(command, stageBudget) {
|
|
|
216
217
|
return Number.isFinite(flagged) ? flagged : stageBudget ?? 0;
|
|
217
218
|
}
|
|
218
219
|
if (/^tldrx expert train\b/.test(command)) {
|
|
219
|
-
|
|
220
|
+
if (Number.isFinite(flagged))
|
|
221
|
+
return flagged;
|
|
222
|
+
return FULL_MODE_RE.test(command) ? DEFAULT_FULL_TRAIN_USD : DEFAULT_TRAIN_USD;
|
|
220
223
|
}
|
|
221
224
|
if (/^tldrx seed triage\b/.test(command)) {
|
|
222
225
|
return Number.isFinite(flagged) ? flagged : DEFAULT_TRIAGE_USD;
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
parseHandoff
|
|
3
|
-
} from "./chunk-9zsqxr6y.js";
|
|
4
1
|
import {
|
|
5
2
|
MAX_PLAN_STORIES,
|
|
6
3
|
MAX_STORIES_PER_WAVE,
|
|
@@ -11,10 +8,7 @@ import {
|
|
|
11
8
|
requirePattern,
|
|
12
9
|
requireStringList,
|
|
13
10
|
requireVersion1
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import {
|
|
16
|
-
classifySrc
|
|
17
|
-
} from "./chunk-m3mewgnw.js";
|
|
11
|
+
} from "./chunk-hcrbr430.js";
|
|
18
12
|
import {
|
|
19
13
|
asDocument,
|
|
20
14
|
isRecord,
|
|
@@ -27,11 +21,17 @@ import {
|
|
|
27
21
|
requireVersion,
|
|
28
22
|
result
|
|
29
23
|
} from "./chunk-39zh2e44.js";
|
|
24
|
+
import {
|
|
25
|
+
parseHandoff
|
|
26
|
+
} from "./chunk-q8d3sff9.js";
|
|
30
27
|
import {
|
|
31
28
|
PROJECT_FRAMEWORK_DIR,
|
|
29
|
+
SRC_PATTERNS,
|
|
30
|
+
classifySrc,
|
|
32
31
|
listRunDirs,
|
|
33
|
-
parseYaml
|
|
34
|
-
|
|
32
|
+
parseYaml,
|
|
33
|
+
readableSource
|
|
34
|
+
} from "./chunk-a6rpj2cp.js";
|
|
35
35
|
|
|
36
36
|
// src/hooks/lib/runFile.ts
|
|
37
37
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -223,6 +223,9 @@ function validateRunBudget(input) {
|
|
|
223
223
|
if (doc.on_host_tokens_exceed !== undefined && doc.on_host_tokens_exceed !== null) {
|
|
224
224
|
requireEnum(doc.on_host_tokens_exceed, ON_HOST_TOKENS_EXCEED, "on_host_tokens_exceed", issues);
|
|
225
225
|
}
|
|
226
|
+
if (doc.ceiling_host_tokens !== undefined && doc.ceiling_host_tokens !== null) {
|
|
227
|
+
requireNumber(doc.ceiling_host_tokens, "ceiling_host_tokens", issues);
|
|
228
|
+
}
|
|
226
229
|
if (doc.warn_at_pct !== undefined) {
|
|
227
230
|
requireNumber(doc.warn_at_pct, "warn_at_pct", issues);
|
|
228
231
|
const pct = doc.warn_at_pct;
|
|
@@ -236,7 +239,9 @@ function validateRunBudget(input) {
|
|
|
236
239
|
if (phases.length > MAX_PHASES) {
|
|
237
240
|
issues.push({ path: "phases", message: `${phases.length} phases exceeds the ${MAX_PHASES} cap` });
|
|
238
241
|
}
|
|
242
|
+
const runEconomy = ECONOMIES.includes(doc.economy) ? doc.economy : DEFAULT_ECONOMY;
|
|
239
243
|
let sum = 0;
|
|
244
|
+
let tokenSum = 0;
|
|
240
245
|
phases.forEach((phase, i) => {
|
|
241
246
|
const path = `phases[${i}]`;
|
|
242
247
|
if (!isRecord(phase)) {
|
|
@@ -250,12 +255,27 @@ function validateRunBudget(input) {
|
|
|
250
255
|
if (phase.economy !== undefined && phase.economy !== null) {
|
|
251
256
|
requireEnum(phase.economy, ECONOMIES, `${path}.economy`, issues);
|
|
252
257
|
}
|
|
258
|
+
if (phase.ceiling_host_tokens !== undefined && phase.ceiling_host_tokens !== null) {
|
|
259
|
+
requireNumber(phase.ceiling_host_tokens, `${path}.ceiling_host_tokens`, issues);
|
|
260
|
+
}
|
|
261
|
+
const economy = ECONOMIES.includes(phase.economy) ? phase.economy : runEconomy;
|
|
262
|
+
if (economy === "host-tokens") {
|
|
263
|
+
const tokens = typeof phase.ceiling_host_tokens === "number" ? phase.ceiling_host_tokens : typeof phase.ceiling_usd === "number" ? phase.ceiling_usd : 0;
|
|
264
|
+
tokenSum += tokens;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
253
267
|
if (typeof phase.ceiling_usd === "number")
|
|
254
268
|
sum += phase.ceiling_usd;
|
|
255
269
|
});
|
|
256
270
|
if (typeof doc.ceiling_usd === "number" && sum > doc.ceiling_usd + 0.000000001) {
|
|
257
271
|
issues.push({ path: "phases", message: `phase ceilings sum to ${sum} > ceiling_usd ${doc.ceiling_usd}` });
|
|
258
272
|
}
|
|
273
|
+
if (typeof doc.ceiling_host_tokens === "number" && tokenSum > doc.ceiling_host_tokens + 0.000000001) {
|
|
274
|
+
issues.push({
|
|
275
|
+
path: "phases",
|
|
276
|
+
message: `phase host-token ceilings sum to ${tokenSum} > ceiling_host_tokens ${doc.ceiling_host_tokens}`
|
|
277
|
+
});
|
|
278
|
+
}
|
|
259
279
|
return result(issues, deprecations);
|
|
260
280
|
}
|
|
261
281
|
function asRunBudget(input) {
|
|
@@ -269,11 +289,13 @@ function asRunBudget(input) {
|
|
|
269
289
|
on_exceed: doc.on_exceed ?? "block",
|
|
270
290
|
economy: doc.economy ?? DEFAULT_ECONOMY,
|
|
271
291
|
on_host_tokens_exceed: doc.on_host_tokens_exceed ?? DEFAULT_ON_HOST_TOKENS_EXCEED,
|
|
292
|
+
ceiling_host_tokens: doc.ceiling_host_tokens ?? null,
|
|
272
293
|
phases: (doc.phases ?? []).map((phase) => ({
|
|
273
294
|
id: phase.id,
|
|
274
295
|
ceiling_usd: phase.ceiling_usd,
|
|
275
296
|
spent_usd: phase.spent_usd,
|
|
276
|
-
economy: phase.economy ?? null
|
|
297
|
+
economy: phase.economy ?? null,
|
|
298
|
+
ceiling_host_tokens: phase.ceiling_host_tokens ?? null
|
|
277
299
|
}))
|
|
278
300
|
};
|
|
279
301
|
}
|
|
@@ -283,9 +305,9 @@ function hostTokenCeiling(budget, phaseId) {
|
|
|
283
305
|
if (phaseId !== undefined && phaseId !== null) {
|
|
284
306
|
const phase = budget.phases.find((entry) => entry.id === phaseId);
|
|
285
307
|
if (phase !== undefined)
|
|
286
|
-
return phase.ceiling_usd;
|
|
308
|
+
return phase.ceiling_host_tokens ?? phase.ceiling_usd;
|
|
287
309
|
}
|
|
288
|
-
return budget.ceiling_usd;
|
|
310
|
+
return budget.ceiling_host_tokens ?? budget.ceiling_usd;
|
|
289
311
|
}
|
|
290
312
|
|
|
291
313
|
// src/core/budget/remainingWork.ts
|
|
@@ -971,6 +993,21 @@ function stackExpertNames(root, repos) {
|
|
|
971
993
|
return names;
|
|
972
994
|
}
|
|
973
995
|
|
|
996
|
+
// src/core/text/srcGrammarContract.ts
|
|
997
|
+
var KINDS = {
|
|
998
|
+
file: { shape: "`[repo:]path:line[-line]`", example: "api:src/Selector.ts:241" },
|
|
999
|
+
doc: { shape: "`https://` + a non-space URL", example: "https://example.com/spec" },
|
|
1000
|
+
answer: { shape: `\`Q<n>\` (${readableSource(SRC_PATTERNS.answer)})`, example: "Q3" },
|
|
1001
|
+
fact: { shape: `\`F<nnn>\` (${readableSource(SRC_PATTERNS.fact)})`, example: "F102" },
|
|
1002
|
+
cmd: { shape: `\`$ <command> → exit <n>\` (${readableSource(SRC_PATTERNS.cmd)})`, example: "$ bun test → exit 0" },
|
|
1003
|
+
graph: { shape: "`graph:<node id>`", example: "graph:hunt-engine" },
|
|
1004
|
+
absent: { shape: "`absent:<the path you looked at>`", example: "absent:docs/retention.md" },
|
|
1005
|
+
aidlc: {
|
|
1006
|
+
shape: `\`aidlc:<file>:<line>\` or \`aidlc:<file>#Q<n>\` (${readableSource(SRC_PATTERNS.aidlcLine)})`,
|
|
1007
|
+
example: "aidlc:intents/260821/design.md:14"
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
|
|
974
1011
|
// src/core/build/prompts.ts
|
|
975
1012
|
var MAX_TOUCHED_BYTES = 64 * 1024;
|
|
976
1013
|
|
|
@@ -1180,6 +1217,9 @@ function looksLikeSpawnError(detail) {
|
|
|
1180
1217
|
var MAX_ATTEMPTS = 2;
|
|
1181
1218
|
var REVIEWER_SHARE = 0.25;
|
|
1182
1219
|
var REVIEWER_FLOOR_USD = 1;
|
|
1220
|
+
function developerPriceDivisor(attempt) {
|
|
1221
|
+
return attempt <= 1 ? 1 + REVIEWER_SHARE : MAX_ATTEMPTS * (1 + REVIEWER_SHARE);
|
|
1222
|
+
}
|
|
1183
1223
|
function remainingWork(input) {
|
|
1184
1224
|
try {
|
|
1185
1225
|
return measure(input);
|
|
@@ -1244,9 +1284,12 @@ function measure(input) {
|
|
|
1244
1284
|
if (attemptsLeft === 0)
|
|
1245
1285
|
continue;
|
|
1246
1286
|
const developerTurns = Math.max(attemptsLeft - (story.status === "review" ? 1 : 0), 0);
|
|
1247
|
-
const
|
|
1287
|
+
const firstTurn = MAX_ATTEMPTS - developerTurns + 1;
|
|
1288
|
+
const developerCapsUsd = Array.from({ length: developerTurns }, (_unused, i) => hostPaysDeveloper ? 0 : caps.developer(story.id, firstTurn + i));
|
|
1289
|
+
const developerCapUsd = developerCapsUsd[0] ?? 0;
|
|
1248
1290
|
const reviewerCapUsd = caps.reviewer(story.id);
|
|
1249
|
-
const
|
|
1291
|
+
const developerUsd = developerCapsUsd.reduce((sum, cap) => sum + cap, 0);
|
|
1292
|
+
const usd = round2(developerUsd + reviewerCapUsd * attemptsLeft);
|
|
1250
1293
|
if (usd <= 0 && developerTurns === 0 && attemptsLeft === 0)
|
|
1251
1294
|
continue;
|
|
1252
1295
|
stories.push({
|
|
@@ -1255,6 +1298,7 @@ function measure(input) {
|
|
|
1255
1298
|
developerTurns,
|
|
1256
1299
|
reviews: attemptsLeft,
|
|
1257
1300
|
developerCapUsd,
|
|
1301
|
+
developerCapsUsd,
|
|
1258
1302
|
reviewerCapUsd,
|
|
1259
1303
|
usd
|
|
1260
1304
|
});
|
|
@@ -1294,11 +1338,11 @@ class CapMath {
|
|
|
1294
1338
|
this.scale = input.stageBudgetUsd <= 0 || sum <= input.stageBudgetUsd ? 1 : input.stageBudgetUsd / sum;
|
|
1295
1339
|
this.maxBudgetUsd = this.agentCap(1);
|
|
1296
1340
|
}
|
|
1297
|
-
developer(storyId) {
|
|
1341
|
+
developer(storyId, attempt) {
|
|
1298
1342
|
const price = this.priceOf(storyId);
|
|
1299
1343
|
if (price === null)
|
|
1300
1344
|
return this.agentCap(1 / this.worstCaseShares());
|
|
1301
|
-
return this.agentCap(this.shareOf(price / (
|
|
1345
|
+
return this.agentCap(this.shareOf(price / developerPriceDivisor(attempt)));
|
|
1302
1346
|
}
|
|
1303
1347
|
reviewer(storyId) {
|
|
1304
1348
|
const price = this.priceOf(storyId);
|
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
stageAt,
|
|
17
17
|
validateRunBudget,
|
|
18
18
|
validateRunFile
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-14zn51kh.js";
|
|
20
20
|
import {
|
|
21
21
|
EventLog
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-sae7sqty.js";
|
|
23
23
|
import {
|
|
24
24
|
backupPathFor,
|
|
25
25
|
isAlive,
|
|
@@ -28,17 +28,17 @@ import {
|
|
|
28
28
|
workspaceRootOfRunDir,
|
|
29
29
|
writeAtomic,
|
|
30
30
|
yamlScalar
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-qw73rdbr.js";
|
|
32
32
|
import {
|
|
33
33
|
noteDeprecations,
|
|
34
34
|
openBlocks,
|
|
35
35
|
parseQuestions
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-nadqsr3w.js";
|
|
37
37
|
import {
|
|
38
38
|
listRunDirs,
|
|
39
39
|
parseYaml,
|
|
40
40
|
parseYamlRepairing
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-a6rpj2cp.js";
|
|
42
42
|
|
|
43
43
|
// src/core/statusline/runSnapshot.ts
|
|
44
44
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -169,16 +169,21 @@ function emitBudgetYaml(budget) {
|
|
|
169
169
|
`on_exceed: ${yamlScalar(budget.on_exceed)}`,
|
|
170
170
|
...budget.economy === DEFAULT_ECONOMY ? [] : [`economy: ${yamlScalar(budget.economy)}`],
|
|
171
171
|
...budget.on_host_tokens_exceed === DEFAULT_ON_HOST_TOKENS_EXCEED ? [] : [`on_host_tokens_exceed: ${yamlScalar(budget.on_host_tokens_exceed)}`],
|
|
172
|
+
...budget.ceiling_host_tokens === null ? [] : [`ceiling_host_tokens: ${tokens(budget.ceiling_host_tokens)}`],
|
|
172
173
|
"phases:"
|
|
173
174
|
];
|
|
174
175
|
for (const phase of budget.phases) {
|
|
175
176
|
const economy = phase.economy === null ? "" : `, economy: ${yamlScalar(phase.economy)}`;
|
|
176
|
-
|
|
177
|
+
const hostTokens = phase.ceiling_host_tokens === null ? "" : `, ceiling_host_tokens: ${tokens(phase.ceiling_host_tokens)}`;
|
|
178
|
+
lines.push(` - {id: ${yamlScalar(phase.id)}, ceiling_usd: ${money(phase.ceiling_usd)}, ` + `spent_usd: ${money(phase.spent_usd)}${economy}${hostTokens}}`);
|
|
177
179
|
}
|
|
178
180
|
return `${lines.join(`
|
|
179
181
|
`)}
|
|
180
182
|
`;
|
|
181
183
|
}
|
|
184
|
+
function tokens(value) {
|
|
185
|
+
return String(Math.trunc(value));
|
|
186
|
+
}
|
|
182
187
|
|
|
183
188
|
// src/core/run/RunStore.ts
|
|
184
189
|
class RunStoreError extends Error {
|
|
@@ -335,7 +340,12 @@ class RunStore {
|
|
|
335
340
|
...onDisk,
|
|
336
341
|
phases: this.currentBudget.phases.map((mine) => {
|
|
337
342
|
const theirs = onDisk.phases.find((p) => p.id === mine.id);
|
|
338
|
-
return theirs === undefined ? mine : {
|
|
343
|
+
return theirs === undefined ? mine : {
|
|
344
|
+
...mine,
|
|
345
|
+
ceiling_usd: theirs.ceiling_usd,
|
|
346
|
+
ceiling_host_tokens: theirs.ceiling_host_tokens,
|
|
347
|
+
economy: theirs.economy
|
|
348
|
+
};
|
|
339
349
|
})
|
|
340
350
|
};
|
|
341
351
|
} catch {
|
|
@@ -571,6 +581,9 @@ function stageAtCursor(run, cursor) {
|
|
|
571
581
|
return null;
|
|
572
582
|
}
|
|
573
583
|
function waitingFor(run, runDir) {
|
|
584
|
+
if (run.cancelled !== undefined && run.cancelled !== null) {
|
|
585
|
+
return { kind: "cancelled", message: cancelledMessage(run.cancelled), questions: [] };
|
|
586
|
+
}
|
|
574
587
|
const cursor = run.cursor;
|
|
575
588
|
if (cursor === null) {
|
|
576
589
|
return { kind: "blocked", message: "run.yml records no cursor, so nothing can say where it is", questions: [] };
|
|
@@ -641,6 +654,12 @@ function waitingFor(run, runDir) {
|
|
|
641
654
|
};
|
|
642
655
|
}
|
|
643
656
|
}
|
|
657
|
+
function cancelledMessage(cancelled) {
|
|
658
|
+
const by = cancelled.by.trim() === "" ? "someone" : cancelled.by.trim();
|
|
659
|
+
const when = cancelled.at.trim() === "" ? "" : ` at ${cancelled.at.trim()}`;
|
|
660
|
+
const note = cancelled.note.trim();
|
|
661
|
+
return `run cancelled by ${by}${when}${note === "" ? "" : ` — ${note}`}`;
|
|
662
|
+
}
|
|
644
663
|
function openQuestionIds2(path) {
|
|
645
664
|
if (!existsSync5(path))
|
|
646
665
|
return [];
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
withWorkspaceLock,
|
|
4
4
|
workspaceRootOfFactsPath,
|
|
5
5
|
writeAtomic
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-qw73rdbr.js";
|
|
7
7
|
import {
|
|
8
8
|
FACT_CONFIDENCES,
|
|
9
9
|
FACT_KINDS,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
isLive,
|
|
15
15
|
isRetired,
|
|
16
16
|
noteDeprecations
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-nadqsr3w.js";
|
|
18
18
|
import {
|
|
19
19
|
asDocument,
|
|
20
20
|
isRecord,
|
|
@@ -26,15 +26,17 @@ import {
|
|
|
26
26
|
result
|
|
27
27
|
} from "./chunk-39zh2e44.js";
|
|
28
28
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
SRC_PATTERNS,
|
|
30
|
+
parseYaml,
|
|
31
|
+
readableSource
|
|
32
|
+
} from "./chunk-a6rpj2cp.js";
|
|
31
33
|
|
|
32
34
|
// src/core/facts/FactsStore.ts
|
|
33
35
|
import { existsSync, readFileSync } from "node:fs";
|
|
34
36
|
|
|
35
37
|
// src/core/facts/validateFactsFile.ts
|
|
36
|
-
var ID_RE =
|
|
37
|
-
var Q_RE =
|
|
38
|
+
var ID_RE = SRC_PATTERNS.fact;
|
|
39
|
+
var Q_RE = SRC_PATTERNS.answer;
|
|
38
40
|
function validateFactsFile(input) {
|
|
39
41
|
const issues = [];
|
|
40
42
|
const deprecations = [];
|
|
@@ -60,7 +62,7 @@ function validateFactsFile(input) {
|
|
|
60
62
|
requireKeys(row, ["id", "fact", "area", "repos", "kind", "confidence", "source", "supersedes", "superseded_by", "retired"], path, issues);
|
|
61
63
|
const id = typeof row.id === "string" ? row.id : "";
|
|
62
64
|
if (!ID_RE.test(id))
|
|
63
|
-
issues.push({ path: `${path}.id`, message:
|
|
65
|
+
issues.push({ path: `${path}.id`, message: `id must match ${readableSource(ID_RE)}` });
|
|
64
66
|
else if (byId.has(id))
|
|
65
67
|
issues.push({ path: `${path}.id`, message: `duplicate fact id ${id}` });
|
|
66
68
|
else {
|
|
@@ -82,7 +84,7 @@ function validateFactsFile(input) {
|
|
|
82
84
|
requireKeys(row.source, ["who", "when", "run", "q"], `${path}.source`, issues);
|
|
83
85
|
const q = row.source.q;
|
|
84
86
|
if (typeof q === "string" && !Q_RE.test(q)) {
|
|
85
|
-
issues.push({ path: `${path}.source.q`, message:
|
|
87
|
+
issues.push({ path: `${path}.source.q`, message: `expected ${readableSource(Q_RE)} or null` });
|
|
86
88
|
}
|
|
87
89
|
} else if (row.source !== undefined) {
|
|
88
90
|
issues.push({ path: `${path}.source`, message: "expected a mapping" });
|