tldr-experts 0.9.0 → 0.9.2

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 CHANGED
@@ -1,6 +1,187 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## 0.9.2 — 2026-09-07
5
+
6
+ ### Changed
7
+
8
+ - **The Build executor is no longer one 4,351-line file.** `BuildSession` was a single class
9
+ (`:520-3833`) over five subjects — the story pipeline, the reviewer handshake, the fix-list
10
+ round, the base-tree pre-flight and the worktree/branch mechanics — and the methods that
11
+ reached three or more of them were the ones every change had to touch. That is not an
12
+ aesthetic complaint: every remaining hardening fix lands inside this file, and a 4,000-line
13
+ diff context is exactly where the review loop is weakest, because the reviewer reads the
14
+ change and cannot see what the change sits next to. Eight modules now hold the machinery —
15
+ `build/reviewLedger.ts` (`events.jsonl` read once into the bounds a fresh process cannot
16
+ remember), `build/phaseCost.ts` (the handoff's cost line), `build/caps.ts` (the money
17
+ constants and every ceiling), `build/dodRunner.ts` (the story DoD and the #41 base
18
+ pre-flight), `build/worktrees.ts` (the git side of a story), `build/branchClaims.ts` (which
19
+ epic branch this run owns and the two refusals that protect the tree), `build/reviewBundle.ts`
20
+ (the reviewer's bundle on disk) and `build/reviewRound.ts` (one review round, one reviewer
21
+ prompt) — each taking DATA rather than the session, which is the rule the rest of
22
+ `src/core/build/` already kept without exception. The mutable state the orchestrator used to
23
+ hide in private maps is now explicit and passed in: `PreflightCache`, `EpicState` and
24
+ `ReviewCounters` — three counters of three different things, reset on three different events,
25
+ still never merged. `executors/build.ts` is down to ~2.9k lines and keeps the orchestration: the
26
+ entry points, the wave drivers, the story-state cluster, the log and handoff cluster, the
27
+ refusal helpers and `SerialQueue`. Nothing moved that a caller can see — every symbol anything
28
+ imports is still exported from `executors/build.ts`, so the nine importing test files,
29
+ `run/reopenStory.ts` and `facilitator/index.ts` did not change one line of import (the
30
+ re-exports nothing imported were dropped rather than carried, which is the opposite of drift).
31
+ The one test edit is a widening: `build-executor.test.ts`'s #134 pin that no file builds a
32
+ `story/${…}` name by hand now reads `worktrees.ts` and `branchClaims.ts` too, because a
33
+ NEGATIVE pin on one file gets weaker every time code leaves it.
34
+ Zero behaviour change, and it is proved rather than argued. A golden guard captured before the
35
+ first move — `test/build-golden.test.ts`, 18 committed artifacts across three scenarios that
36
+ cannot stand in for one another — freezes byte for byte the developer prompt, the reviewer
37
+ prompt, the `--prepare` bundle's own prompt, the ordered event stream with its payload keys
38
+ and values, `run.yml`'s task rows and the exit codes of a real fake-agent build, and every one
39
+ of the five move steps — and the surface-polish pass after them — had to keep it identical. It
40
+ is kept, because it is the cheapest regression net this file has ever had. The rule while the
41
+ wave ran, and the rule now: a golden byte change means REVERT — never "update the golden" to
42
+ make a diff go away.
43
+
44
+ ## 0.9.1 — 2026-09-06
45
+
46
+ ### Added
47
+
48
+ - **The docs site carries the whole CLI, generated from the registry.** The site's
49
+ `reference/cli.md` was a curated map that named about half the flags and pointed at
50
+ `docs/guide/08-cli-reference.md` for the rest — a file the site never builds, because
51
+ `docs/` is not in its `srcDir`. So the "exhaustive version" was not on the website at all,
52
+ and the owner went looking for `--yolo`, the flag that drops per-tool permission prompts,
53
+ and found it only in the changelog. `docs-site/scripts/gen-cli.ts` now writes
54
+ `/reference/cli-flags` and `/es/reference/cli-flags` at build time out of
55
+ `src/cli/helpText.ts` — all 33 commands, every flag with its meaning, every allowed value
56
+ spelled out from where it is enforced, every exit code, the environment variables, and the
57
+ rules that hold everywhere. Generated, gitignored and never hand-edited, for the same
58
+ reason the changelog page is: a second copy of 180 flags drifts on its first edit. The
59
+ Spanish page is framed in Spanish and keeps the meanings in English, like the release notes
60
+ and the sample CLI strings.
61
+ - **`test/docs-cli-coverage.test.ts` — the docs are now a gate.** Nothing asserted that a
62
+ declared flag was EXPLAINED anywhere a reader who is not typing `--help` would look. It now
63
+ asserts both surfaces: the repo guide names every flag inside its own command's section,
64
+ and the generated page carries every command, flag, allowed value and exit code, with
65
+ `--yolo` explained on each of the four commands that take it rather than left bare in a
66
+ usage line. It also holds the env-var table against a grep of `src/` in both directions.
67
+ - **`tldrx facts add` — the command the drive mandate has been naming since 0.8.0 (#161).** The
68
+ mandate tells the driver "a fact that must outlive the turn is `tldrx facts add`, which every
69
+ later prompt DOES read", and no such command was dispatched. What drivers did instead was edit
70
+ `.tldrx/memory/facts.yml` by hand, which walks past `FactsStore.append`'s 2000-character cap,
71
+ past its `…` marker and its `truncated: true` flag, and past `save()`'s validation — and a
72
+ fact cut mid-word with no marker is a record that does not know it is incomplete. It writes
73
+ through the store, under the workspace lock, with `--area` and `--decided-by owner|driver`
74
+ both required — a driver's default is never cited as the owner's decision, so the command
75
+ never lets the caller skip saying which of the two it was. The underlying `source.decided_by`
76
+ field stays additive (rows written before it existed, or by another writer, still validate)
77
+ and renders in every `{{facts}}` block as `· decided by owner` / `· decided by driver`,
78
+ appended only when present. Without `--run` it uses the one open run and refuses to guess
79
+ between several, recording the absence with its reason.
80
+ - **The provider's token split on the `run.yml` task row (#159).** `input_tokens` and `output_tokens`
81
+ were parsed on every provider turn and reached the event log only, so `run.yml` — the file
82
+ every cost report and every resumed run reads — carried a dollar figure with no token figure
83
+ beside it, which is a number nobody can check against a price table. Both fields are additive,
84
+ written only when a turn reported them and only together — a half-reported split is exactly
85
+ as unverifiable as an absent one, so it is dropped rather than have the parse's own zero
86
+ default stand in for a real count — and distinct from `tokens`, which keeps its meaning as a
87
+ host declaration. `version: 1` is unchanged and every older row loads.
88
+
89
+ ### Fixed
90
+
91
+ - **Two commands the docs told you to run that do not exist.** `tldrx facts add` was
92
+ instructed three times, in `07-claude-code.md`, `10-unattended-mode.md` and
93
+ `08-cli-reference.md`, as the way to record something that must outlive a turn — measured:
94
+ `tldrx facts` is `unknown command 'facts'`, exit 1. A row in `.tldrx/memory/facts.yml` is
95
+ written by answering an open question, so all three now say `tldrx answer <Qid> "…"`. And
96
+ the environment-variable section told you to check that `TLDRX_CLAUDE_BIN` took with
97
+ `tldrx run --dry-run`; `--dry-run` is `next`'s flag, not `run`'s. The guard that found them
98
+ is now part of the suite: every `tldrx …` line inside a code span or a fenced block, on
99
+ every page of the site and the guide plus the README, is parsed and held against the
100
+ registry — an unknown command or a flag its command does not declare fails the build.
101
+ - **Two flags the guide never documented**, found by that test on its first run:
102
+ `tldrx dashboard --serve` (the default mode, and half of the two-modes refusal that
103
+ `--static` is the other half of) and `tldrx run status --verbose` (the two instants behind
104
+ a gate row). Both were real, both were reachable, and neither appeared in the page that
105
+ claims to be exhaustive. The shared spawn flags — `--model`, `--effort`, `--max-usd`,
106
+ `--ui`, `--prepare`/`--commit`, `--yolo` — now point from `run auto`, `seed triage` and
107
+ `expert train` at the one place they are explained, instead of appearing as bare tokens in
108
+ a usage line.
109
+ - **A turn with no provider USD figure is unmetered, not a metered `$0.00` (#159).** `interpret`
110
+ wrote `cost_usd: 0, metered: true` for a Claude result document that carried no
111
+ `total_cost_usd` — contradicting the `metered` field's own contract three lines above it, and
112
+ letting a stage, and then a whole run, read `$0.00` after real turns had run. `metered` is now
113
+ derived from the presence of the figure, so those turns reach `cost_usd: null` +
114
+ `metered: false` and every report that already knows how to say "LOWER BOUND, not a total"
115
+ says it. No new `spendBasis` word: `absent` already meant this, and the fix is to make more
116
+ turns reach it honestly. Codex keeps its pin — its synthesized result document carries a
117
+ `total_cost_usd: 0` that is a placeholder, and reading it as a measurement would be the same
118
+ lie under a different provider.
119
+ - **A refusal no longer discards a cost (#159).** `tldrx next --commit` returned
120
+ `EXIT_AGENT_FAILED` on a `questions.md` the §2.7 parser cannot read BEFORE it recorded the
121
+ task — so a turn that had already been paid for left no row anywhere, and the run's own ledger
122
+ was short by exactly the amount nobody could see. The row lands first and the refusal exits
123
+ after it. Because that refusal deliberately leaves the stage `running` so the operator can fix
124
+ the file and re-run, the banked row is marked `banked_before_refusal`, and the re-run is
125
+ matched to it by session id, declared cost and outputs — against MARKED rows only, so an
126
+ ordinary attempt a gate sent back is never mistaken for a re-read. Two things it deliberately
127
+ does NOT do. A result with no `session_id` is never fingerprinted at all: a null id identifies
128
+ nothing, so the second turn gets its own row carrying `dedupe: "none — no session id"` and the
129
+ reason is in `run.yml`, not only on the console — recording it twice is a smaller lie than
130
+ dropping a turn that ran. And the marker is single-use: the re-run that matches it stamps the
131
+ row `matched by the re-run committed at <at> — the marker is spent`, because a marker left
132
+ armed matched every later turn of the same shape for the life of the stage, and a real retry
133
+ after a gate reject — same session, same outputs, the same `null` cost — was silently dropped
134
+ as already recorded.
135
+ - **An oversized reviewer verdict no longer takes the whole invocation's ledger with it
136
+ (#160).** The 4096-byte payload cap was enforced by `EventLog.append` throwing, nothing
137
+ wrapped the executor call, and the reviewer's verdict prose is the field that overflows — so
138
+ one wordy review escaped past `recordExecutorTasks` and `store.save()`, leaving the epic merge
139
+ on disk and every task's cost gone from `run.yml`. The cap is honoured, never raised: at the
140
+ emit seam the oversized `detail` becomes `detail_omitted`, carrying its own byte count and
141
+ pointing at a sidecar the seam wrote FIRST — `<phase>/log/overflow/<stamp>-<n>-<type>-detail.txt`,
142
+ one file per omission — while the verdict itself survives. It is not the review log: that file
143
+ does not exist yet when the event is built, it later holds only the final verdict's prose, and
144
+ a story that does not settle never gets one, so pointing at it was a promise about a file that
145
+ might never arrive. Around all of it, the executor call is now wrapped: ANY throw out of an
146
+ executor exits 5 with the stage failed by name and the loss said plainly — which rows this
147
+ invocation could not recover — where before the throw simply escaped, leaving the stage
148
+ `running` in a file nobody saved. It attributes that failure to nothing it did not do, either:
149
+ the row it marks `failed` is one THIS invocation recorded, so a retry whose executor throws
150
+ leaves the previous attempt's `done` turn exactly as it was rather than repainting it with an
151
+ error it never produced.
152
+ - **A cached red base is no longer trusted forever (#162).** `04-build/preflight.yml` was
153
+ invalidated only by a base-sha comparison that no-ops when either sha is empty, and
154
+ `checked_at` was written and never read — so a red measured once came back from every later
155
+ `--prepare` in 0 seconds, over a base a live probe would show green, and the
156
+ `.tldrx/workspace.yml` fix that the refusal itself asks the operator to make was the one thing
157
+ the cache could not see. A red is now re-probed when the command hash differs, when the row is
158
+ older than 30 minutes, or always under `--prepare`. The hash covers the command together with
159
+ the workspace's whole declared command list, because the command string was already the join
160
+ key and hashing it alone would have changed nothing. A cached green keeps the rule it had, and
161
+ both new row fields are additive — an absent one invalidates nothing.
162
+ - **`tldrx facts add --run <id>` no longer invents an absence.** `RunStore.resolve` answers
163
+ `{kind: "none"}` both to "no run is open" and to "there is no run by that id", and the command
164
+ took the one branch for both: a typo'd `--run` wrote the fact with `source.run: null` under the
165
+ stdout line "no run recorded: no open run to attribute it to" — a sentence that is false
166
+ whenever a run IS open, over provenance the operator had asked for by name and silently did not
167
+ get. An id nothing in `tldrx-work/` answers to is now refused before the store is opened: exit
168
+ 3, nothing written, no event, and the id named back. The other two branches are unchanged —
169
+ one open run is used, several are still never guessed between.
170
+ - **A fix list records the canonical 40-hex sha (#163).** `Resolved: yes <sha>` accepted 7-40
171
+ hex, so a sha that had lost a character read as a deliberate abbreviation: git resolved it,
172
+ the claim verified, and the audit record kept a form no later reader can tell from a prefix of
173
+ a different commit. The grammar is unchanged — demanding 40 would refuse the abbreviation a
174
+ person legitimately types — and the full object id is written back after the verification,
175
+ which is strictly stronger and refuses nobody.
176
+
177
+ ### Changed
178
+
179
+ - **A command's subcommands are declared once.** They were written in the command module AND
180
+ needed by the docs generator, which would have made three copies of the same list. They now
181
+ live in `helpText.ts` beside the flags that are scoped by them, `subcommandsOf(name)` reads
182
+ them, and `Command.subcommands` is gone — one derivation, one implementation, per the house
183
+ invariant that the rest of the repo is already held to.
184
+
4
185
  ## 0.9.0 — 2026-09-06
5
186
 
6
187
  ### Added
package/README.md CHANGED
@@ -251,7 +251,7 @@ Most run-targeting commands take the id either way, a positional `<run>` or `--r
251
251
  `cost`, `note`, `gate template`, `questions`, `budget show`, `ship`, `tickets`, and `run attend` ·
252
252
  `status` · `estimate` · `auto` · `unlock` · `cancel`. `replay` and `retro` take the positional only
253
253
  — `--run` there is an unknown flag. `approve`, `reject`, `answer`, `interview`, `plan`,
254
- `story reopen`, `watch` and `run gates set` take `--run <id>` only.
254
+ `story reopen`, `facts add`, `watch` and `run gates set` take `--run <id>` only.
255
255
 
256
256
  `tldrx retro --all` goes the other way: it reads **every** run in the workspace and prints one
257
257
  table of what keeps catching you — finding class × count × how many runs × one example with its
@@ -314,6 +314,8 @@ back on the registry is 0.3.0.
314
314
 
315
315
  | Version | Date | Status | Contains |
316
316
  |---|---|---|---|
317
+ | 0.9.2 | 2026-09-07 | `beta` | the Build executor decomposed: 4,351 lines became an orchestrator plus eight modules under src/core/build/ (review ledger and phase cost, money caps, the DoD runner with its preflight cache, worktrees and epic-branch claims, the reviewer bundle and review round) — a pure refactor proven byte-for-byte by a golden guard over prompts, ordered events, run.yml rows and exit codes across three scenarios; no behaviour changed, every remaining hardening fix now lands in a file a reviewer can hold |
318
+ | 0.9.1 | 2026-09-06 | `beta` | records and money that do not lie: a turn without a provider USD figure is unmetered, never a metered $0.00; a refusal no longer discards the turn's cost (banked first, deduped only against marked rows); an event over the 4096-byte cap spills its text beside the run and names it instead of losing every row of the invocation; preflight rows carry `command_hash` and their own `checked_at`, so a cached red is re-probed when the command changed, after 30 minutes, or under --prepare; the fix list writes back git's canonical 40-hex sha; `tldrx facts add` exists with owner/driver attribution rendered in prompts; run.yml rows carry the provider's token split only when reported |
317
319
  | 0.9.0 | 2026-09-06 | `beta` | opt-in stack packs: four language pack bodies and thirteen framework overlays detected from manifests, interrogative by default (Defaults yield to any repo signal, Checks are questions with `verify:` hints), behind one per-project switch (`tldrx expert packs enable`) that materialises into `.tldrx/experts/<lang>-stack/` without touching trained knowledge; the Build reviewer gets `## Stack checks (the repo's own conventions win)`; every stage names the project's `.claude/skills` and the developer may invoke them; workspace.yml records overlays with evidence and skills with a tracked flag, `version: 1` unchanged |
318
320
  | 0.8.0 | 2026-09-04 | `beta` | the unattended mandate learns to keep going: a `## Do not stop` section that defines the one thing allowed to end a run early (a strict blocker, named against the work it does not block), product questions parked as **guided** ones with lettered options and a pre-declared fallback, and a budget stop that asks instead of halting — written against 26 `budget.raised` and 26 `question.answered` events on a real ten-run workspace where the owner had to re-authorise "unattended" mid-run. Plus `tldrx drive --tldr`: a reporting contract for runs nobody will audit — the `run status` block plus three bullets of delta, no operator notes, minimal gate evidence, handoffs trimmed of prose but never of the citations `claim-sources` gates on. The ask channel stays the console and the framework names no chat vendor |
319
321
  | 0.7.0 | 2026-09-03 | `beta` | Codex as a second honest automated runner: recorded JSONL contract, structured envelopes, role-based sandboxes, token/session provenance, and explicitly unmetered USD accounting; Claude remains the default and the pilot harness. Also absorbs the citation-honesty work previously staged as 0.6.2: a `file` src resolves against the branches the run RECORDED and NAMES the unmerged ref instead of passing in silence or breaking with the temp dir (#140), watcher cards name it the same way (#143), and every run close reports the questions nobody answered (#141) |
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FactsStore
4
- } from "./chunk-ecfqpeyj.js";
4
+ } from "./chunk-f6484ykg.js";
5
5
  import {
6
6
  parseHookInput,
7
7
  readStdin
@@ -9,9 +9,9 @@ import {
9
9
  import {
10
10
  EventLog,
11
11
  PHASE_ID_RE
12
- } from "./chunk-b9rjx4qc.js";
12
+ } from "./chunk-fv66tdsm.js";
13
13
  import"./chunk-5w5eg00k.js";
14
- import"./chunk-6405vkzk.js";
14
+ import"./chunk-8awygmqw.js";
15
15
  import {
16
16
  MAX_FACT_CHARS,
17
17
  detectAnswered,
@@ -19,7 +19,7 @@ import {
19
19
  recordAnswer,
20
20
  replaceBlock,
21
21
  serializeQuestions
22
- } from "./chunk-37fwvg3x.js";
22
+ } from "./chunk-vyvjbbmb.js";
23
23
  import"./chunk-39zh2e44.js";
24
24
  import {
25
25
  PROJECT_WORK_DIR,
@@ -29,15 +29,15 @@ import {
29
29
  validateRunBudget,
30
30
  wouldExceed,
31
31
  wouldExceedHostTokens
32
- } from "./chunk-c4p5x1b4.js";
32
+ } from "./chunk-hg43zexg.js";
33
33
  import {
34
34
  EventLog
35
- } from "./chunk-b9rjx4qc.js";
35
+ } from "./chunk-fv66tdsm.js";
36
36
  import"./chunk-rrkdfk7s.js";
37
37
  import"./chunk-5w5eg00k.js";
38
38
  import {
39
39
  noteDeprecations
40
- } from "./chunk-37fwvg3x.js";
40
+ } from "./chunk-vyvjbbmb.js";
41
41
  import"./chunk-39zh2e44.js";
42
42
  import {
43
43
  PROJECT_WORK_DIR,
@@ -7,7 +7,7 @@ import {
7
7
  nowRfc3339,
8
8
  openRunViews,
9
9
  validateRunBudget
10
- } from "./chunk-c4p5x1b4.js";
10
+ } from "./chunk-hg43zexg.js";
11
11
  import {
12
12
  EventLog,
13
13
  asRunFile,
@@ -19,7 +19,7 @@ import {
19
19
  isTerminal,
20
20
  stageAt,
21
21
  validateRunFile
22
- } from "./chunk-b9rjx4qc.js";
22
+ } from "./chunk-fv66tdsm.js";
23
23
  import {
24
24
  backupPathFor,
25
25
  isAlive,
@@ -28,12 +28,12 @@ import {
28
28
  workspaceRootOfRunDir,
29
29
  writeAtomic,
30
30
  yamlScalar
31
- } from "./chunk-6405vkzk.js";
31
+ } from "./chunk-8awygmqw.js";
32
32
  import {
33
33
  noteDeprecations,
34
34
  openBlocks,
35
35
  parseQuestions
36
- } from "./chunk-37fwvg3x.js";
36
+ } from "./chunk-vyvjbbmb.js";
37
37
  import {
38
38
  listRunDirs,
39
39
  parseYaml,
@@ -80,11 +80,15 @@ function task(t, indent) {
80
80
  const cost = t.cost_usd === null ? "null" : money(t.cost_usd);
81
81
  const metered = t.metered === false ? ", metered: false" : "";
82
82
  const tokens = t.tokens === undefined ? "" : `, tokens: ${String(t.tokens)}`;
83
+ const inTokens = t.input_tokens === undefined ? "" : `, input_tokens: ${String(t.input_tokens)}`;
84
+ const outTokens = t.output_tokens === undefined ? "" : `, output_tokens: ${String(t.output_tokens)}`;
83
85
  return [
84
- `${indent} - {id: ${yamlScalar(t.id)}, status: ${yamlScalar(t.status)}, expert: ${yamlScalar(t.expert)}, ` + `model: ${yamlScalar(t.model)}, cost_usd: ${cost}${metered}${tokens},`,
86
+ `${indent} - {id: ${yamlScalar(t.id)}, status: ${yamlScalar(t.status)}, expert: ${yamlScalar(t.expert)}, ` + `model: ${yamlScalar(t.model)}, cost_usd: ${cost}${metered}${tokens}${inTokens}${outTokens},`,
85
87
  `${inner}error: ${yamlScalar(t.error)}, session_id: ${yamlScalar(t.session_id)},`,
86
88
  `${inner}started_at: ${yamlScalar(t.started_at)}, ended_at: ${yamlScalar(t.ended_at)},`,
87
89
  ...t.stopped_by === undefined || t.stopped_by === null ? [] : [`${inner}stopped_by: ${yamlScalar(t.stopped_by)},`],
90
+ ...t.banked_before_refusal === undefined ? [] : [`${inner}banked_before_refusal: true,`],
91
+ ...t.dedupe === undefined ? [] : [`${inner}dedupe: ${yamlScalar(t.dedupe)},`],
88
92
  `${inner}outputs: ${inlineList(t.outputs)}}`
89
93
  ].join(`
90
94
  `);
@@ -143,6 +143,7 @@ function inlineList(values) {
143
143
  }
144
144
  function emitFact(fact, indent = " ") {
145
145
  const inner = `${indent} `;
146
+ const decidedBy = fact.source.decided_by === undefined ? "" : `, decided_by: ${yamlScalar(fact.source.decided_by)}`;
146
147
  const lines = [
147
148
  `${indent}- id: ${yamlScalar(fact.id)}`,
148
149
  `${inner}fact: ${yamlScalar(fact.fact)}`,
@@ -150,7 +151,7 @@ function emitFact(fact, indent = " ") {
150
151
  `${inner}repos: ${inlineList(fact.repos)}`,
151
152
  `${inner}kind: ${yamlScalar(fact.kind)}`,
152
153
  `${inner}confidence: ${yamlScalar(fact.confidence)}`,
153
- `${inner}source: {who: ${yamlScalar(fact.source.who)}, when: ${yamlScalar(fact.source.when)}, ` + `run: ${yamlScalar(fact.source.run)}, q: ${yamlScalar(fact.source.q)}}`,
154
+ `${inner}source: {who: ${yamlScalar(fact.source.who)}, when: ${yamlScalar(fact.source.when)}, ` + `run: ${yamlScalar(fact.source.run)}, q: ${yamlScalar(fact.source.q)}${decidedBy}}`,
154
155
  `${inner}supersedes: ${yamlScalar(fact.supersedes)}`,
155
156
  `${inner}superseded_by: ${yamlScalar(fact.superseded_by)}`
156
157
  ];
@@ -3,7 +3,7 @@ import {
3
3
  withWorkspaceLock,
4
4
  workspaceRootOfFactsPath,
5
5
  writeAtomic
6
- } from "./chunk-6405vkzk.js";
6
+ } from "./chunk-8awygmqw.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-37fwvg3x.js";
17
+ } from "./chunk-vyvjbbmb.js";
18
18
  import {
19
19
  asDocument,
20
20
  isRecord,
@@ -86,6 +86,13 @@ function validateFactsFile(input) {
86
86
  if (typeof q === "string" && !Q_RE.test(q)) {
87
87
  issues.push({ path: `${path}.source.q`, message: `expected ${readableSource(Q_RE)} or null` });
88
88
  }
89
+ const decidedBy = row.source.decided_by;
90
+ if (decidedBy !== undefined && decidedBy !== "owner" && decidedBy !== "driver") {
91
+ issues.push({
92
+ path: `${path}.source.decided_by`,
93
+ message: "expected owner, driver or absent"
94
+ });
95
+ }
89
96
  } else if (row.source !== undefined) {
90
97
  issues.push({ path: `${path}.source`, message: "expected a mapping" });
91
98
  }
@@ -698,6 +698,20 @@ function validateRunFile(input) {
698
698
  if (task.cost_usd === null && task.metered !== false) {
699
699
  issues.push({ path: `${tp}.metered`, message: "a null cost_usd must be marked `metered: false`" });
700
700
  }
701
+ if (task.banked_before_refusal !== undefined && task.banked_before_refusal !== true) {
702
+ issues.push({ path: `${tp}.banked_before_refusal`, message: "expected `true` or absent" });
703
+ }
704
+ if (task.dedupe !== undefined && typeof task.dedupe !== "string") {
705
+ issues.push({ path: `${tp}.dedupe`, message: "expected a string" });
706
+ }
707
+ for (const key of ["input_tokens", "output_tokens"]) {
708
+ const value = task[key];
709
+ if (value === undefined)
710
+ continue;
711
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
712
+ issues.push({ path: `${tp}.${key}`, message: "expected a number >= 0" });
713
+ }
714
+ }
701
715
  if (typeof task.cost_usd === "number")
702
716
  spentFromTasks += task.cost_usd;
703
717
  checkOrder(task.started_at, task.ended_at, tp, issues);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  EVIDENCE_FILE,
3
3
  gateEvidenceRelPath
4
- } from "./chunk-b9rjx4qc.js";
4
+ } from "./chunk-fv66tdsm.js";
5
5
  import {
6
6
  MAX_PLAN_STORIES,
7
7
  MAX_STORIES_PER_WAVE,
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-cf4az0j5.js";
8
8
  import {
9
9
  FactsStore
10
- } from "./chunk-ecfqpeyj.js";
10
+ } from "./chunk-f6484ykg.js";
11
11
  import {
12
12
  allow,
13
13
  deny,
@@ -17,11 +17,11 @@ import {
17
17
  runHook
18
18
  } from "./chunk-2y3kwfrx.js";
19
19
  import"./chunk-x3zy7zrn.js";
20
- import"./chunk-6405vkzk.js";
20
+ import"./chunk-8awygmqw.js";
21
21
  import {
22
22
  openBlocks,
23
23
  parseQuestions
24
- } from "./chunk-37fwvg3x.js";
24
+ } from "./chunk-vyvjbbmb.js";
25
25
  import"./chunk-39zh2e44.js";
26
26
  import {
27
27
  factsPath,
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import"./chunk-ecfqpeyj.js";
2
+ import"./chunk-f6484ykg.js";
3
3
  import {
4
4
  allow,
5
5
  readPayload,
@@ -15,7 +15,7 @@ import {
15
15
  openQuestions,
16
16
  runSnapshot,
17
17
  whatIsWaiting
18
- } from "./chunk-6ytv36jd.js";
18
+ } from "./chunk-4w03q2kj.js";
19
19
  import {
20
20
  evidencePath,
21
21
  expertsDir,
@@ -25,17 +25,17 @@ import {
25
25
  pathsIntersect,
26
26
  readExpertDomain,
27
27
  stackExpertNames
28
- } from "./chunk-c4p5x1b4.js";
28
+ } from "./chunk-hg43zexg.js";
29
29
  import {
30
30
  parseEvidence
31
- } from "./chunk-b9rjx4qc.js";
31
+ } from "./chunk-fv66tdsm.js";
32
32
  import"./chunk-rrkdfk7s.js";
33
33
  import"./chunk-5w5eg00k.js";
34
- import"./chunk-6405vkzk.js";
34
+ import"./chunk-8awygmqw.js";
35
35
  import {
36
36
  openBlocks,
37
37
  parseQuestions
38
- } from "./chunk-37fwvg3x.js";
38
+ } from "./chunk-vyvjbbmb.js";
39
39
  import {
40
40
  isRecord
41
41
  } from "./chunk-39zh2e44.js";
@@ -2,13 +2,13 @@
2
2
  import {
3
3
  bar,
4
4
  runSnapshot
5
- } from "./chunk-6ytv36jd.js";
6
- import"./chunk-c4p5x1b4.js";
7
- import"./chunk-b9rjx4qc.js";
5
+ } from "./chunk-4w03q2kj.js";
6
+ import"./chunk-hg43zexg.js";
7
+ import"./chunk-fv66tdsm.js";
8
8
  import"./chunk-rrkdfk7s.js";
9
9
  import"./chunk-5w5eg00k.js";
10
- import"./chunk-6405vkzk.js";
11
- import"./chunk-37fwvg3x.js";
10
+ import"./chunk-8awygmqw.js";
11
+ import"./chunk-vyvjbbmb.js";
12
12
  import"./chunk-39zh2e44.js";
13
13
  import {
14
14
  findWorkspaceRoot,