tldr-experts 0.4.0 → 0.6.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 (48) hide show
  1. package/CHANGELOG.md +2225 -0
  2. package/README.md +78 -16
  3. package/dist/hooks/answer-capture.js +136 -13
  4. package/dist/hooks/budget-gate.js +14 -11
  5. package/dist/hooks/{chunk-t1ywrfr4.js → chunk-17gv74sr.js} +10 -8
  6. package/dist/hooks/{chunk-s1c5h7yx.js → chunk-5k4dggq0.js} +72 -19
  7. package/dist/hooks/{chunk-4cp363kv.js → chunk-7eqhfddw.js} +81 -578
  8. package/dist/hooks/{chunk-rpcxsqh3.js → chunk-97ncnegd.js} +1 -1
  9. package/dist/hooks/{chunk-sq44k6g2.js → chunk-9w64mxdh.js} +1007 -3
  10. package/dist/hooks/{chunk-9gb21660.js → chunk-dhbxzjfs.js} +52 -14
  11. package/dist/hooks/{chunk-phmdk72a.js → chunk-g4db8rbs.js} +2 -30
  12. package/dist/hooks/chunk-h7151g8w.js +803 -0
  13. package/dist/hooks/{chunk-7y2dq0pj.js → chunk-ka6bkb64.js} +1 -1
  14. package/dist/hooks/chunk-rrkdfk7s.js +30 -0
  15. package/dist/hooks/{chunk-c6t5nx0r.js → chunk-v4ay50nq.js} +1 -1
  16. package/dist/hooks/{chunk-b8kxzna2.js → chunk-vrhczzxs.js} +1 -1
  17. package/dist/hooks/{chunk-tzzwddct.js → chunk-x6n8b5fp.js} +1 -1
  18. package/dist/hooks/claim-sources.js +21 -20
  19. package/dist/hooks/dod-gate.js +7 -6
  20. package/dist/hooks/no-reask.js +8 -9
  21. package/dist/hooks/session-start.js +58 -28
  22. package/dist/hooks/statusline.js +10 -11
  23. package/dist/tldrx.js +8491 -3059
  24. package/env.yml +9 -2
  25. package/package.json +2 -2
  26. package/plugin/.claude-plugin/plugin.json +2 -2
  27. package/plugin/skills/tldrx/SKILL.md +9 -2
  28. package/stages/watch/stage.md +4 -0
  29. package/templates/expert.md +13 -1
  30. package/templates/questions.md +8 -0
  31. package/templates/watcher.md +8 -1
  32. package/workflows/bugfix.yml +8 -4
  33. package/workflows/docs.yml +8 -4
  34. package/workflows/feature.yml +8 -4
  35. package/workflows/hotfix.yml +8 -4
  36. package/workflows/integration.yml +8 -4
  37. package/workflows/migration.yml +8 -4
  38. package/workflows/performance.yml +8 -4
  39. package/workflows/prototype.yml +8 -4
  40. package/workflows/refactor.yml +8 -4
  41. package/workflows/security-patch.yml +8 -4
  42. package/workflows/spike.yml +8 -4
  43. package/workflows/upgrade.yml +8 -4
  44. package/dist/hooks/chunk-9zsqxr6y.js +0 -213
  45. package/dist/hooks/chunk-m3mewgnw.js +0 -522
  46. package/dist/hooks/chunk-rz541e2b.js +0 -204
  47. package/templates/epic.md +0 -40
  48. 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. The workflow and the persisted state format are provider-independent; the automated runner currently supports 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
@@ -36,8 +41,10 @@ tldrx run new payments --scope feature --seed docs/payments/ --budget 25
36
41
  tldrx run auto # `next`, over and over, until something actually needs you
37
42
  ```
38
43
 
39
- **Runtime: Node ≥ 20 or Bun** — the published package is a pre-built bundle with zero runtime
40
- dependencies, so an installed `tldrx` needs only Node; Bun builds it. Full walkthrough:
44
+ **To run it: Node ≥ 20, and nothing else** — the published package is a pre-built bundle with
45
+ zero runtime dependencies, and `dist/` is a Node bundle (Bun runs it too, if you have it).
46
+ **To build or contribute: Bun ≥ 1.3**, which compiles that bundle and runs the test suite.
47
+ `tldrx doctor` is the authority on the rest. Full walkthrough:
41
48
  [`docs/guide/01-quick-start.md`](docs/guide/01-quick-start.md).
42
49
 
43
50
  ## Trying it: three ways to run
@@ -63,7 +70,21 @@ and names the `--prepare` command instead.
63
70
 
64
71
  ### Overnight, with the checking kept
65
72
 
66
- Two commands and a prompt. There is no keyword for this: the mandate is prose you write.
73
+ Two commands and a prompt — and the prompt now ships with the package:
74
+
75
+ ```bash
76
+ tldrx drive --unattended # print the mandate; paste it into the session that drives the run
77
+ tldrx drive --attended # the same disciplines, but every gate stays yours to sign
78
+ ```
79
+
80
+ `tldrx drive` needs no workspace, opens no run and writes nothing: it prints the discipline the
81
+ first real runs were driven by — the three-role protocol (developer → a **fresh** adversarial
82
+ reviewer, never the author → the host verifying both in the code, not in their reports), evidence
83
+ labelled `measured` / `inferred` / `assumed`, product questions parked rather than decided, the
84
+ reviewer calibrated to the story's stakes, and the cost declared once. The two modes differ in
85
+ exactly two places: who drives the turns, and who may close a gate.
86
+
87
+ The rest of this section is what that mandate says, in the shape you would type it by hand.
67
88
 
68
89
  ```bash
69
90
  tldrx run new payments --scope feature --budget 25 \
@@ -138,7 +159,11 @@ Those are the shipped defaults, and every scope keeps at least one human gate. O
138
159
  signs something it should not have, `tldrx reject --stage <phase>/<stage> --note "…"` revokes it, moves
139
160
  the cursor back and marks the later stages `stale`. When it is one BUILD STORY you disagree with — a
140
161
  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.
162
+ `tldrx story reopen <id> --note "…"` gives that one story another run of attempts and nothing else;
163
+ for one named defect in a story already `done`, `--for-fix` opens a fix round instead — no attempt
164
+ consumed, the same DoD and the same reviewer, one open round at a time.
165
+ To move who may close a gate after `run new` froze it, `tldrx run gates set <stage>:<policy> --note "…"`
166
+ is the only sanctioned way, and it records the old→new value with your reason.
142
167
  When you fix `.tldrx/workspace.yml` mid-run and the approved stories still cite the old command strings,
143
168
  `tldrx plan sync-dod` rewrites just their dod lines — renames followed, removed commands dropped, and
144
169
  anything with no ancestor in the file's history flagged rather than guessed at.
@@ -190,7 +215,7 @@ context 83.7 KB of 160.0 KB (~23.8k tok, 12% of sonnet's ~200.0k window)
190
215
  ```
191
216
 
192
217
  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
218
+ the sub-agent at a read ceiling; `--effort` changes what a turn costs. What `--max-usd` does is
194
219
  end a run *after* the turn it is already in — measured, one turn spent **$5.15** against a **$1.50**
195
220
  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
221
  run, read off `agent.result` events and nothing else. Retries are never merged — a retry is
@@ -200,24 +225,49 @@ Details: [`docs/guide/06-budgets-and-cost.md`](docs/guide/06-budgets-and-cost.md
200
225
 
201
226
  ## Several runs
202
227
 
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`.
228
+ With several runs open and no id, a run-targeting command **refuses rather than guessing** and
229
+ lists them — `tldrx next: 3 runs are open — pass one:`. That means "you left off the id", not "it
230
+ broke". Two of them refuse differently and it is worth knowing which: `tldrx run status` is not a
231
+ refusal at all — it lists every open run and exits `0`, and it is the screen you read to find the
232
+ id the others want — and `tldrx cost` refuses at exit `1`, not `2`.
233
+
234
+ Most run-targeting commands take the id either way, a positional `<run>` or `--run <id>`: `next`,
235
+ `cost`, `note`, `gate template`, `questions`, `budget show`, `ship`, `tickets`, and `run attend` ·
236
+ `status` · `estimate` · `auto` · `unlock` · `cancel`. `replay` and `retro` take the positional only
237
+ — `--run` there is an unknown flag. `approve`, `reject`, `answer`, `interview`, `plan`,
238
+ `story reopen`, `watch` and `run gates set` take `--run <id>` only.
239
+
240
+ `tldrx retro --all` goes the other way: it reads **every** run in the workspace and prints one
241
+ table of what keeps catching you — finding class × count × how many runs × one example with its
242
+ citation — mined from the review logs, the fix lists, `retro.md` and the `story.reopened` reasons.
243
+ Strictly read-only: it writes nothing, anywhere.
207
244
 
208
245
  ## What to commit
209
246
 
210
247
  **Both `.tldrx/` and `tldrx-work/`.** The files are the state — the map, the facts, the questions and
211
248
  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/`.
249
+ the repo gets the run. The block `tldrx init` appends to `.gitignore` excludes eight paths and nothing
250
+ else, because those eight are machine-local, regenerated, or a backup git already holds the history
251
+ of: `.tldrx/graphify-out/`, `.tldrx/cache/`, `.tldrx/worktrees/`, `tldrx-work/*/.lock`,
252
+ `tldrx-work/*/.agent/`, `tldrx-work/**/*.bak`, `.tldrx/**/*.bak` and
253
+ `.claude/settings.json.bak-tldrx-*`.
254
+
255
+ **You do not have to commit them by hand.** Closing a run — `tldrx approve` on the last gate,
256
+ `tldrx next`, or `tldrx run cancel` — commits `tldrx-work/<run>/` and `.tldrx/memory/` in the
257
+ workspace checkout, on the branch that checkout is on, and prints one line saying where they went.
258
+ Only those paths: anything else you had staged is still staged afterwards. It is deliberately never
259
+ the epic branch — an epic under review carries feature code, and run state on it collides with the
260
+ same live files in your working tree the moment the PR merges, which is why `tldrx ship` refuses an
261
+ epic that carries any (#102).
215
262
 
216
263
  ## Documentation
217
264
 
218
265
  **[The documentation site](https://ederwii.github.io/tldr-experts/)** is the place to start if you have
219
266
  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/).
267
+ rather than for an agent. Source in [`docs-site/`](docs-site/). It carries a
268
+ **[live demo of `tldrx dashboard`](https://ederwii.github.io/tldr-experts/demo)** — a real export,
269
+ rendered from the test suite's synthetic fixtures on every deploy, so you can see what the tool
270
+ draws before installing anything.
221
271
 
222
272
  The reference guide, in `docs/guide/`: [1 Quick start](docs/guide/01-quick-start.md) ·
223
273
  [2 The loop](docs/guide/02-the-loop.md) (the four steps, what a stage file controls, the two execution modes) ·
@@ -233,6 +283,12 @@ Design docs: [`docs/concept.md`](docs/concept.md) (why) · [`docs/spec.md`](docs
233
283
  open decisions) · [`docs/ROADMAP.md`](docs/ROADMAP.md) (next) · [`CHANGELOG.md`](CHANGELOG.md) (shipped) ·
234
284
  [`docs/dashboard-model.md`](docs/dashboard-model.md).
235
285
 
286
+ Contributing: **[`CONTRIBUTING.md`](CONTRIBUTING.md)** — the loop a change goes through, the four gates
287
+ and what CI actually runs, the red-first test rules, and
288
+ [how to contribute a model-provider config](CONTRIBUTING.md#contributing-a-model-provider-config)
289
+ (the `TLDRX_CLAUDE_BIN` seam, the `stream-json` transcript contract, and what a generic provider
290
+ would have to supply).
291
+
236
292
  ## Releases and status tags
237
293
 
238
294
  Install name is **`tldr-experts`**; it installs two commands, **`tldrx`** (short) and `tldr-experts` (same binary).
@@ -242,6 +298,8 @@ back on the registry is 0.3.0.
242
298
 
243
299
  | Version | Date | Status | Contains |
244
300
  |---|---|---|---|
301
+ | 0.6.0 | 2026-09-02 | `beta` | the dashboard suite — a "Now" hero strip, `--serve` live refresh, stage durations and gate notes on page and CLI, and a public demo generated from fixtures; honest dual-economy spend (metered + host, lower-bound named); gate provenance — `executed_by` + `authority` so a delegated signature never reads as a personal one, machine-signed reporting fixed; ONE `absent:` semantic shared by claim-sources and the auto gate; superseded stamps when an owner answer flips an earlier phase doc; run close commits its state and `ship` refuses a state-carrying epic; merge-wave gates `docs:build`, survives interruption and self-rewrite; a public-surface drift guard; env.yml validation (unique ids, tool cap) |
302
+ | 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
303
  | 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
304
  | 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
305
  | 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 +314,10 @@ path documented; `stable` = 1.0, semver from here on. The badge above shows the
256
314
 
257
315
  ## Releasing
258
316
 
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`.
317
+ **One command: `scripts/release.sh X.Y.Z --tag beta`.** The tag is not optional in practice: omit
318
+ `--tag` and the script writes `alpha`, which is no longer this project's status. It is the only
319
+ sanctioned path — a Claude Code hook denies hand-made `git tag` / `npm publish`, and `publish.yml`
320
+ runs `release-check.sh --ci` (the file checks only) plus its own typecheck, tests and build.
321
+ Checklist and judgement calls: `docs/RELEASING.md`.
260
322
 
261
323
  MIT, © 2026 Alan Martinez — a placeholder made while scaffolding; change it freely before anything ships.
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FactsStore
4
- } from "./chunk-t1ywrfr4.js";
4
+ } from "./chunk-17gv74sr.js";
5
5
  import {
6
6
  parseHookInput,
7
7
  readStdin
8
- } from "./chunk-b8kxzna2.js";
8
+ } from "./chunk-vrhczzxs.js";
9
9
  import {
10
- EventLog
11
- } from "./chunk-rz541e2b.js";
12
- import"./chunk-c6t5nx0r.js";
10
+ EventLog,
11
+ PHASE_ID_RE
12
+ } from "./chunk-h7151g8w.js";
13
+ import"./chunk-g4db8rbs.js";
14
+ import"./chunk-v4ay50nq.js";
13
15
  import {
14
16
  MAX_FACT_CHARS,
15
17
  detectAnswered,
@@ -17,16 +19,15 @@ import {
17
19
  recordAnswer,
18
20
  replaceBlock,
19
21
  serializeQuestions
20
- } from "./chunk-rpcxsqh3.js";
21
- import"./chunk-m3mewgnw.js";
22
+ } from "./chunk-97ncnegd.js";
22
23
  import"./chunk-39zh2e44.js";
23
24
  import {
24
25
  PROJECT_WORK_DIR,
25
26
  factsPath
26
- } from "./chunk-sq44k6g2.js";
27
+ } from "./chunk-9w64mxdh.js";
27
28
 
28
29
  // src/hooks/answer-capture.ts
29
- import { existsSync as existsSync2 } from "fs";
30
+ import { existsSync as existsSync3 } from "fs";
30
31
 
31
32
  // src/hooks/lib/decide.ts
32
33
  function allow() {
@@ -98,7 +99,95 @@ function nowRfc3339() {
98
99
  }
99
100
 
100
101
  // src/core/answers/captureAnswers.ts
101
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
102
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
103
+
104
+ // src/core/answers/stampSuperseded.ts
105
+ import { appendFileSync, existsSync, readFileSync, statSync } from "node:fs";
106
+ import { isAbsolute as isAbsolute2, join as join2, relative } from "node:path";
107
+ var STAMP_MARKER = "tldrx:superseded";
108
+ var AFFECTS_KEY = "affects";
109
+ function isStamped(text, fact) {
110
+ return text.includes(`${STAMP_MARKER} ${fact} `);
111
+ }
112
+ function stampText(fact, q, questionsRel, at) {
113
+ return `<!-- ${STAMP_MARKER} ${fact} | q: ${q} | at: ${at} | see: ${questionsRel} -->
114
+ ` + `> **Superseded in part by ${fact}** — ${q} was answered after this document was written; ` + `the answer is in \`${questionsRel}\` and \`.tldrx/memory/facts.yml\`. ` + `This document was not reconciled: where the two disagree, the fact is what the workspace believes.
115
+ `;
116
+ }
117
+ function affectedDocs(runDir, questionsPath, block) {
118
+ const self = runRelative(runDir, questionsPath);
119
+ const askedIn = self === null ? null : phaseIdOf(self);
120
+ const found = new Map;
121
+ const consider = (raw, by) => {
122
+ const rel = resolveInRun(runDir, raw);
123
+ if (rel === null || rel === self || !rel.endsWith(".md"))
124
+ return;
125
+ const phase = phaseIdOf(rel);
126
+ if (phase === null)
127
+ return;
128
+ if (by === "cited" && (askedIn === null || !isEarlier(phase, askedIn)))
129
+ return;
130
+ if (!found.has(rel))
131
+ found.set(rel, { rel, abs: join2(runDir, rel), by });
132
+ };
133
+ for (const ref of block.whySrc?.refs ?? []) {
134
+ if (ref.kind === "file")
135
+ consider(ref.path, "cited");
136
+ }
137
+ for (const raw of declaredAffects(block))
138
+ consider(raw, "affects");
139
+ return [...found.values()];
140
+ }
141
+ function stampSuperseded(runDir, questionsPath, block, fact, at) {
142
+ const questionsRel = runRelative(runDir, questionsPath) ?? "questions.md";
143
+ const stamped = [];
144
+ for (const doc of affectedDocs(runDir, questionsPath, block)) {
145
+ const text = readFileSync(doc.abs, "utf8");
146
+ if (isStamped(text, fact))
147
+ continue;
148
+ const lead = text === "" ? "" : text.endsWith(`
149
+ `) ? `
150
+ ` : `
151
+
152
+ `;
153
+ appendFileSync(doc.abs, lead + stampText(fact, block.id, questionsRel, at), "utf8");
154
+ stamped.push(doc);
155
+ }
156
+ return stamped;
157
+ }
158
+ function declaredAffects(block) {
159
+ const raw = block.metadata?.extra.find(([key]) => key === AFFECTS_KEY)?.[1] ?? "";
160
+ return raw.split(/[,\s]+/).map((part) => part.trim()).filter((part) => part !== "");
161
+ }
162
+ function resolveInRun(runDir, raw) {
163
+ if (raw === "" || isAbsolute2(raw))
164
+ return null;
165
+ const root = join2(runDir, "..", "..");
166
+ for (const candidate of [join2(runDir, raw), join2(root, raw)]) {
167
+ const rel = runRelative(runDir, candidate);
168
+ if (rel === null)
169
+ continue;
170
+ if (!existsSync(candidate) || !statSync(candidate).isFile())
171
+ continue;
172
+ return rel;
173
+ }
174
+ return null;
175
+ }
176
+ function runRelative(runDir, path) {
177
+ const rel = relative(runDir, path);
178
+ if (rel === "" || rel.startsWith("..") || isAbsolute2(rel))
179
+ return null;
180
+ return rel.split(/[\\/]/).join("/");
181
+ }
182
+ function phaseIdOf(rel) {
183
+ const first = rel.split("/")[0] ?? "";
184
+ return PHASE_ID_RE.test(first) ? first : null;
185
+ }
186
+ function isEarlier(phase, than) {
187
+ return Number(phase.slice(0, 2)) < Number(than.slice(0, 2));
188
+ }
189
+
190
+ // src/core/answers/captureAnswers.ts
102
191
  var TRUNCATION_MARK = " …";
103
192
  function factTextFor(title, answer) {
104
193
  const whole = `${title} — ${answer}`;
@@ -110,14 +199,15 @@ function factWasTruncated(title, answer) {
110
199
  return `${title} — ${answer}`.length > MAX_FACT_CHARS;
111
200
  }
112
201
  function captureAnswers(questionsPath, ctx) {
113
- if (!existsSync(questionsPath))
202
+ if (!existsSync2(questionsPath))
114
203
  return [];
115
- let doc = parseQuestions(readFileSync(questionsPath, "utf8"));
204
+ let doc = parseQuestions(readFileSync2(questionsPath, "utf8"));
116
205
  const answered = detectAnswered(doc.blocks);
117
206
  if (answered.length === 0)
118
207
  return [];
119
208
  const log = EventLog.forRun(ctx.runDir);
120
209
  const captured = [];
210
+ const recorded = [];
121
211
  FactsStore.update(factsPath(ctx.root), (store) => {
122
212
  for (const block of answered) {
123
213
  const area = block.metadata?.area ?? "unscoped";
@@ -151,11 +241,44 @@ function captureAnswers(questionsPath, ctx) {
151
241
  payload: { fact: fact.id, area: fact.area, kind: fact.kind, q: block.id }
152
242
  });
153
243
  captured.push({ q: block.id, fact: fact.id, answer: block.answer, area });
244
+ recorded.push({ block, fact: fact.id });
154
245
  }
155
246
  });
156
247
  writeFileSync(questionsPath, serializeQuestions(doc), "utf8");
248
+ for (const item of recorded) {
249
+ markSuperseded(log, ctx, questionsPath, item.block, item.fact);
250
+ }
157
251
  return captured;
158
252
  }
253
+ function markSuperseded(log, ctx, questionsPath, block, fact) {
254
+ try {
255
+ for (const doc of stampSuperseded(ctx.runDir, questionsPath, block, fact, ctx.at)) {
256
+ log.tryAppend({
257
+ ts: ctx.at,
258
+ run: ctx.run,
259
+ stage: null,
260
+ type: "doc.superseded",
261
+ actor: ctx.actor,
262
+ cost_usd: 0,
263
+ payload: { doc: doc.rel, fact, q: block.id, by: doc.by }
264
+ });
265
+ }
266
+ } catch (error) {
267
+ log.tryAppend({
268
+ ts: ctx.at,
269
+ run: ctx.run,
270
+ stage: null,
271
+ type: "error",
272
+ actor: ctx.actor,
273
+ cost_usd: 0,
274
+ payload: {
275
+ message: `could not stamp the documents ${block.id} supersedes: ` + (error instanceof Error ? error.message : String(error)),
276
+ q: block.id,
277
+ fact
278
+ }
279
+ });
280
+ }
281
+ }
159
282
 
160
283
  // src/hooks/answer-capture.ts
161
284
  await runHook("answer-capture", async () => {
@@ -164,7 +287,7 @@ await runHook("answer-capture", async () => {
164
287
  if (event === "PostToolUse" && payload.tool_name !== "Write" && payload.tool_name !== "Edit")
165
288
  return;
166
289
  const filePath = filePathOf(payload);
167
- if (!filePath.endsWith("questions.md") || !existsSync2(filePath))
290
+ if (!filePath.endsWith("questions.md") || !existsSync3(filePath))
168
291
  return;
169
292
  const location = locateWork(filePath);
170
293
  if (location === null)
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-9gb21660.js";
4
+ } from "./chunk-dhbxzjfs.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-x6n8b5fp.js";
12
+ import"./chunk-vrhczzxs.js";
13
13
  import {
14
14
  asRunBudget,
15
15
  currentActor,
@@ -29,16 +29,15 @@ import {
29
29
  validateRunBudget,
30
30
  wouldExceed,
31
31
  wouldExceedHostTokens
32
- } from "./chunk-4cp363kv.js";
32
+ } from "./chunk-7eqhfddw.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-h7151g8w.js";
36
+ import"./chunk-rrkdfk7s.js";
37
+ import"./chunk-g4db8rbs.js";
38
38
  import {
39
39
  noteDeprecations
40
- } from "./chunk-rpcxsqh3.js";
41
- import"./chunk-m3mewgnw.js";
40
+ } from "./chunk-97ncnegd.js";
42
41
  import"./chunk-39zh2e44.js";
43
42
  import {
44
43
  PROJECT_WORK_DIR,
@@ -46,7 +45,7 @@ import {
46
45
  locateWork,
47
46
  parseYaml,
48
47
  stageYamlPath
49
- } from "./chunk-sq44k6g2.js";
48
+ } from "./chunk-9w64mxdh.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;
@@ -3,7 +3,7 @@ import {
3
3
  withWorkspaceLock,
4
4
  workspaceRootOfFactsPath,
5
5
  writeAtomic
6
- } from "./chunk-c6t5nx0r.js";
6
+ } from "./chunk-v4ay50nq.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-97ncnegd.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-9w64mxdh.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" });