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.
- package/CHANGELOG.md +2225 -0
- package/README.md +78 -16
- package/dist/hooks/answer-capture.js +136 -13
- package/dist/hooks/budget-gate.js +14 -11
- package/dist/hooks/{chunk-t1ywrfr4.js → chunk-17gv74sr.js} +10 -8
- package/dist/hooks/{chunk-s1c5h7yx.js → chunk-5k4dggq0.js} +72 -19
- package/dist/hooks/{chunk-4cp363kv.js → chunk-7eqhfddw.js} +81 -578
- package/dist/hooks/{chunk-rpcxsqh3.js → chunk-97ncnegd.js} +1 -1
- package/dist/hooks/{chunk-sq44k6g2.js → chunk-9w64mxdh.js} +1007 -3
- package/dist/hooks/{chunk-9gb21660.js → chunk-dhbxzjfs.js} +52 -14
- package/dist/hooks/{chunk-phmdk72a.js → chunk-g4db8rbs.js} +2 -30
- package/dist/hooks/chunk-h7151g8w.js +803 -0
- package/dist/hooks/{chunk-7y2dq0pj.js → chunk-ka6bkb64.js} +1 -1
- package/dist/hooks/chunk-rrkdfk7s.js +30 -0
- package/dist/hooks/{chunk-c6t5nx0r.js → chunk-v4ay50nq.js} +1 -1
- package/dist/hooks/{chunk-b8kxzna2.js → chunk-vrhczzxs.js} +1 -1
- package/dist/hooks/{chunk-tzzwddct.js → chunk-x6n8b5fp.js} +1 -1
- package/dist/hooks/claim-sources.js +21 -20
- package/dist/hooks/dod-gate.js +7 -6
- package/dist/hooks/no-reask.js +8 -9
- package/dist/hooks/session-start.js +58 -28
- package/dist/hooks/statusline.js +10 -11
- package/dist/tldrx.js +8491 -3059
- package/env.yml +9 -2
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/skills/tldrx/SKILL.md +9 -2
- package/stages/watch/stage.md +4 -0
- package/templates/expert.md +13 -1
- package/templates/questions.md +8 -0
- 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-9zsqxr6y.js +0 -213
- package/dist/hooks/chunk-m3mewgnw.js +0 -522
- package/dist/hooks/chunk-rz541e2b.js +0 -204
- 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. 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
|
-
**
|
|
40
|
-
dependencies,
|
|
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
|
|
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-
|
|
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,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
213
|
-
else, because those
|
|
214
|
-
`.tldrx/worktrees/`, `tldrx-work/*/.lock`,
|
|
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
|
|
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-
|
|
4
|
+
} from "./chunk-17gv74sr.js";
|
|
5
5
|
import {
|
|
6
6
|
parseHookInput,
|
|
7
7
|
readStdin
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-vrhczzxs.js";
|
|
9
9
|
import {
|
|
10
|
-
EventLog
|
|
11
|
-
|
|
12
|
-
|
|
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-
|
|
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-
|
|
27
|
+
} from "./chunk-9w64mxdh.js";
|
|
27
28
|
|
|
28
29
|
// src/hooks/answer-capture.ts
|
|
29
|
-
import { existsSync as
|
|
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 (!
|
|
202
|
+
if (!existsSync2(questionsPath))
|
|
114
203
|
return [];
|
|
115
|
-
let doc = parseQuestions(
|
|
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") || !
|
|
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-
|
|
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-
|
|
12
|
-
import"./chunk-
|
|
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-
|
|
32
|
+
} from "./chunk-7eqhfddw.js";
|
|
33
33
|
import {
|
|
34
34
|
EventLog
|
|
35
|
-
} from "./chunk-
|
|
36
|
-
import"./chunk-
|
|
37
|
-
import"./chunk-
|
|
35
|
+
} from "./chunk-h7151g8w.js";
|
|
36
|
+
import"./chunk-rrkdfk7s.js";
|
|
37
|
+
import"./chunk-g4db8rbs.js";
|
|
38
38
|
import {
|
|
39
39
|
noteDeprecations
|
|
40
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
30
|
-
|
|
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 =
|
|
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" });
|