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.
Files changed (44) hide show
  1. package/CHANGELOG.md +1433 -0
  2. package/README.md +61 -13
  3. package/dist/hooks/answer-capture.js +6 -7
  4. package/dist/hooks/budget-gate.js +14 -11
  5. package/dist/hooks/{chunk-4cp363kv.js → chunk-14zn51kh.js} +60 -16
  6. package/dist/hooks/{chunk-s1c5h7yx.js → chunk-3w55tp71.js} +26 -7
  7. package/dist/hooks/{chunk-7y2dq0pj.js → chunk-5556vjt5.js} +1 -1
  8. package/dist/hooks/{chunk-t1ywrfr4.js → chunk-889tybxc.js} +10 -8
  9. package/dist/hooks/{chunk-9gb21660.js → chunk-a5dq2dcp.js} +54 -14
  10. package/dist/hooks/{chunk-sq44k6g2.js → chunk-a6rpj2cp.js} +684 -3
  11. package/dist/hooks/{chunk-tzzwddct.js → chunk-bvm6vjrt.js} +1 -1
  12. package/dist/hooks/{chunk-phmdk72a.js → chunk-hcrbr430.js} +1 -1
  13. package/dist/hooks/{chunk-rpcxsqh3.js → chunk-nadqsr3w.js} +1 -1
  14. package/dist/hooks/{chunk-9zsqxr6y.js → chunk-q8d3sff9.js} +23 -16
  15. package/dist/hooks/{chunk-c6t5nx0r.js → chunk-qw73rdbr.js} +1 -1
  16. package/dist/hooks/{chunk-rz541e2b.js → chunk-sae7sqty.js} +2 -0
  17. package/dist/hooks/{chunk-b8kxzna2.js → chunk-v1c1hpb8.js} +1 -1
  18. package/dist/hooks/claim-sources.js +19 -16
  19. package/dist/hooks/dod-gate.js +7 -6
  20. package/dist/hooks/no-reask.js +9 -9
  21. package/dist/hooks/session-start.js +46 -21
  22. package/dist/hooks/statusline.js +8 -9
  23. package/dist/tldrx.js +6364 -2465
  24. package/package.json +2 -2
  25. package/plugin/.claude-plugin/plugin.json +2 -2
  26. package/plugin/skills/tldrx/SKILL.md +1 -1
  27. package/stages/watch/stage.md +4 -0
  28. package/templates/expert.md +13 -1
  29. package/templates/watcher.md +8 -1
  30. package/workflows/bugfix.yml +8 -4
  31. package/workflows/docs.yml +8 -4
  32. package/workflows/feature.yml +8 -4
  33. package/workflows/hotfix.yml +8 -4
  34. package/workflows/integration.yml +8 -4
  35. package/workflows/migration.yml +8 -4
  36. package/workflows/performance.yml +8 -4
  37. package/workflows/prototype.yml +8 -4
  38. package/workflows/refactor.yml +8 -4
  39. package/workflows/security-patch.yml +8 -4
  40. package/workflows/spike.yml +8 -4
  41. package/workflows/upgrade.yml +8 -4
  42. package/dist/hooks/chunk-m3mewgnw.js +0 -522
  43. package/templates/epic.md +0 -40
  44. package/templates/story.md +0 -55
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # tldr-experts
2
2
 
3
- [![npm](https://img.shields.io/npm/v/tldr-experts?label=npm%20tldr-experts)](https://www.npmjs.com/package/tldr-experts) [![ci](https://github.com/ederwii/tldr-experts/actions/workflows/ci.yml/badge.svg)](https://github.com/ederwii/tldr-experts/actions/workflows/ci.yml) ![status](https://img.shields.io/badge/status-alpha-orange)
3
+ [![npm](https://img.shields.io/npm/v/tldr-experts?label=npm%20tldr-experts)](https://www.npmjs.com/package/tldr-experts) [![ci](https://github.com/ederwii/tldr-experts/actions/workflows/ci.yml/badge.svg)](https://github.com/ederwii/tldr-experts/actions/workflows/ci.yml) ![status](https://img.shields.io/badge/status-beta-blue)
4
4
 
5
- **A lightweight, file-based AI development workflow.** Open source, tool-agnostic in design, piloted on Claude Code. **Alpha:** every command is implemented and verified by running it; interfaces may change without notice, and `tldrx --help` is the authoritative surface.
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. There is no keyword for this: the mandate is prose you write.
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-budget-usd` does is
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, every run-targeting command **refuses rather than guessing**,
204
- exits `2`, and lists them — `tldrx next: 3 runs are open — pass one:`. That means "you left off
205
- the id", not "it broke". Pass a positional `<run>` on `next`, `run status`, `cost`, `replay` and
206
- `retro`; `--run <id>` on the rest. `tldrx run status` with several open lists them all, exit `0`.
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 five paths and nothing
213
- else, because those five are machine-local or regenerated: `.tldrx/graphify-out/`, `.tldrx/cache/`,
214
- `.tldrx/worktrees/`, `tldrx-work/*/.lock`, `tldrx-work/*/.agent/`.
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 alpha`.** It is the only sanctioned path — a Claude Code hook denies hand-made `git tag` / `npm publish`, and `publish.yml` re-runs the same checks. Checklist and judgement calls: `docs/RELEASING.md`.
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-t1ywrfr4.js";
4
+ } from "./chunk-889tybxc.js";
5
5
  import {
6
6
  parseHookInput,
7
7
  readStdin
8
- } from "./chunk-b8kxzna2.js";
8
+ } from "./chunk-v1c1hpb8.js";
9
9
  import {
10
10
  EventLog
11
- } from "./chunk-rz541e2b.js";
12
- import"./chunk-c6t5nx0r.js";
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-rpcxsqh3.js";
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-sq44k6g2.js";
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-9gb21660.js";
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-tzzwddct.js";
12
- import"./chunk-b8kxzna2.js";
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-4cp363kv.js";
32
+ } from "./chunk-14zn51kh.js";
33
33
  import {
34
34
  EventLog
35
- } from "./chunk-rz541e2b.js";
36
- import"./chunk-9zsqxr6y.js";
37
- import"./chunk-phmdk72a.js";
35
+ } from "./chunk-sae7sqty.js";
36
+ import"./chunk-hcrbr430.js";
38
37
  import {
39
38
  noteDeprecations
40
- } from "./chunk-rpcxsqh3.js";
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-sq44k6g2.js";
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
- return Number.isFinite(flagged) ? flagged : DEFAULT_TRAIN_USD;
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-phmdk72a.js";
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
- } from "./chunk-sq44k6g2.js";
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 developerCapUsd = hostPaysDeveloper ? 0 : caps.developer(story.id);
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 usd = round2(developerCapUsd * developerTurns + reviewerCapUsd * attemptsLeft);
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 / (MAX_ATTEMPTS * (1 + REVIEWER_SHARE))));
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-4cp363kv.js";
19
+ } from "./chunk-14zn51kh.js";
20
20
  import {
21
21
  EventLog
22
- } from "./chunk-rz541e2b.js";
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-c6t5nx0r.js";
31
+ } from "./chunk-qw73rdbr.js";
32
32
  import {
33
33
  noteDeprecations,
34
34
  openBlocks,
35
35
  parseQuestions
36
- } from "./chunk-rpcxsqh3.js";
36
+ } from "./chunk-nadqsr3w.js";
37
37
  import {
38
38
  listRunDirs,
39
39
  parseYaml,
40
40
  parseYamlRepairing
41
- } from "./chunk-sq44k6g2.js";
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
- lines.push(` - {id: ${yamlScalar(phase.id)}, ceiling_usd: ${money(phase.ceiling_usd)}, ` + `spent_usd: ${money(phase.spent_usd)}${economy}}`);
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 : { ...mine, ceiling_usd: theirs.ceiling_usd };
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 [];
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  toolInput
3
- } from "./chunk-tzzwddct.js";
3
+ } from "./chunk-bvm6vjrt.js";
4
4
 
5
5
  // src/hooks/lib/wouldBe.ts
6
6
  import { existsSync, readFileSync } from "node:fs";
@@ -3,7 +3,7 @@ import {
3
3
  withWorkspaceLock,
4
4
  workspaceRootOfFactsPath,
5
5
  writeAtomic
6
- } from "./chunk-c6t5nx0r.js";
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-rpcxsqh3.js";
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
- parseYaml
30
- } from "./chunk-sq44k6g2.js";
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 = /^F\d{3,6}$/;
37
- var Q_RE = /^Q\d{1,6}$/;
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: "id must match ^F\\d{3,6}$" });
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: "expected ^Q\\d+$ or null" });
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" });