tldr-experts 0.11.1 → 0.12.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 CHANGED
@@ -1,6 +1,130 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## 0.12.0 — 2026-09-08
5
+
6
+ ### Added
7
+
8
+ - **The reviewer can run on a different model than the developer, and every verdict now says
9
+ which model produced it.** Measured this week across three real workspaces and 168 Build
10
+ stories: `model:` and `effort:` in `stage.yml` are per STAGE, one accessor served both
11
+ spawns, and all 168 stories ran the same model at the same effort for the developer AND the
12
+ reviewer that judged its diff. Zero reviewers ran on anything stronger — so *does a stronger
13
+ reviewer find more* was not a question the data could answer, while hosts were already
14
+ answering it by hand, upgrading the reviewer on stories whose own text said they were
15
+ security-bearing. Three additive keys close that: `reviewer: {model?, effort?}` in
16
+ `stage.yml` is the reviewer role's own pin; `reviewer_by_stakes:` is the same thing keyed on
17
+ a story's new optional `stakes:` field (`security | money | data | correctness | routine`, a
18
+ closed enum the Plan agent emits and nothing infers from prose); and both resolve FIELD BY
19
+ FIELD under `--model`/`--effort`, which stay the operator's word. Absent everywhere ⇒ the
20
+ reviewer runs on the stage's own two lines, byte for byte. **No opus default ships**:
21
+ `stages/build/stage.yml` declares `reviewer_by_stakes: {}` and a commented example, because
22
+ there is no evidence yet that a stronger reviewer finds more and shipping the answer as a
23
+ default would spend money asserting the thing these keys exist to measure. What ships is the
24
+ record that makes the evidence accumulate — a spawned reviewer's arguments were always on
25
+ its `agent.spawned`, a host review's are now read off `--model`/`--effort` on `tldrx next
26
+ --commit --review` and written as `basis: host-declared`, `04-build/log/<story>.md` carries
27
+ a `- Reviewer:` line, and `tldrx replay` names the model on every review round. A host that
28
+ declared nothing reads `not recorded` rather than the bundle's suggested model: that
29
+ suggestion is one tldrx made, and quoting it back would be a record lying in the dangerous
30
+ direction. (#178)
31
+ - **`touches` was declared and never measured, so the boundary audit was answering against a
32
+ forecast.** Measured on three real workspaces: one story changed 21 files and 18 of them fell
33
+ outside its declared `touches`, and the other two stories of the same run under-declared as
34
+ well — none of it a mistake anybody could point at, because the 18 were the Application-layer
35
+ files the work genuinely required and no compiler runs at Plan time to predict them. The
36
+ consequence is the part worth fixing: the check that exists to say "this epic changed something
37
+ no story claimed" was comparing a diff against a guess, which is the shape of check that gets
38
+ switched off. Build now takes the second reading, at the moment a story's work is final and the
39
+ answer is free: it diffs the story over exactly the range the reviewer was shown (the
40
+ `epic_base` recorded on `task.done` since 0.10.0 — one definition of "the story's diff", not a
41
+ second `git diff` derivation) and, when paths landed outside the declared list, appends ONE
42
+ `story.touches_widened` with `basis: "measured"` and `actor: framework`, naming the paths and
43
+ the count. It is the SAME event `tldrx story widen` writes, deliberately: "the surface grew" is
44
+ one fact, and a sibling type would make every reader ask two questions to learn it. `basis` is
45
+ additive and **absent means `declared`**, so every row written before it existed keeps its
46
+ meaning and reads unchanged. It is advisory and cannot refuse — nothing is written when the
47
+ work stayed inside the forecast, an unreadable diff is simply no measurement, and the story's
48
+ own `touches:` is never rewritten, because declaring is the operator's verb and a framework
49
+ that back-dated a declaration would make the plan claim it declared a path it did not. Both
50
+ bases render labelled in the Build handoff, in the `tldrx ship` PR body that embeds it, and in
51
+ `tldrx replay`, so the paths outside BOTH readings are the ones a human is pointed at.
52
+
53
+
54
+ - **A run now records which tldrx wrote it (#183).** `run.yml`'s `version: 1` is the FILE
55
+ FORMAT's number; nothing anywhere carried the framework's, so across 23 unattended runs on
56
+ three real workspaces no record could be attributed to the release that produced it — in a
57
+ week that shipped ten of them and moved behaviour in three (the DoD refusal record grew
58
+ `status`, the reviewer's diff base moved, a mandate check was reworded). `run.yml` gains
59
+ two additive keys beside the format's: `created_with`, stamped once at `run new` and never
60
+ rewritten, and `last_written_by`, rewritten by every save — so a run that outlived an
61
+ upgrade carries both ends of the range that drove it. `agent.spawned` and `agent.result`
62
+ carry `tldrx_version` for the same reason at the turn level: a spawn that never produced a
63
+ result is still a turn a release is answerable for. All of it reads from the one source
64
+ `tldrx --version` reads, so a record and the CLI cannot disagree. `run status` prints both
65
+ stamps; a run.yml written before the keys reads `not recorded`, which is a different fact
66
+ from a guess at what happened to be installed that day.
67
+
68
+ - **A task row now carries a duration it actually measured, and says which span it is
69
+ (#184).** `started_at` is the INVOCATION's clock and `ended_at` is the instant the row was
70
+ written, so every task of one parallel Build shares a start and subtracting the two yields
71
+ close to the whole invocation for each of them — three unattended runs measured 34.5 h,
72
+ 43.2 h and 36.8 h of span that no record could attribute to a phase or a sub-agent. Those
73
+ two fields keep their meaning; `duration_ms` is the new one, and it never travels without
74
+ `duration_basis`. `spawned` is the wall clock around the sub-agent's own process, measured
75
+ in `spawnAgent.ts` and written on the task row and on `agent.result`. `prepare-to-commit`
76
+ is the gap between a `--prepare` bundle's `prepared_at` and the `--commit` that recorded
77
+ the turn: the only span the framework can see for an in-session turn, and one that INCLUDES
78
+ the host's own time — a ceiling on the sub-agent's span the way `spent_usd` is a floor on
79
+ the money, and the spec says so rather than letting the number be read as the sub-agent's.
80
+ `tldrx cost` gains a duration column per attempt and a per-stage sum that names a mixed
81
+ basis instead of adding two different quantities, and counts the attempts it could not
82
+ time. A row from before this reads `not recorded`, never `0s`: "it took no time" and
83
+ "nobody timed it" are different facts and only one is a measurement.
84
+
85
+ - **A `maintain` skill turns a session into this repo's maintainer, and the circuit it encodes
86
+ is the one that was measured, not the one that sounds right.** Twelve maintenance waves ran
87
+ over two days by hand, and three of their lessons were costing time every cycle. First,
88
+ issues go stale in days: one asked for a fix to sequential Build execution while `--parallel`
89
+ had already shipped, so the skill reproduces an issue's measurement on current `origin/main`
90
+ BEFORE anything is touched, and a measured refutation closes it. Second, a fresh reviewer who
91
+ did not write the code found a real Important defect in four of those waves, each one
92
+ checkable from `git log`: `2a6413f` fixed a defect review found in `cbd5c4b`, and the fixes
93
+ that pre-merge review forced landed inside `28a987e` (the B0 mandate reword), `674049a` (the
94
+ heartbeat fix) and `103ff96` (the `runAuto` $0.00 fix). The one wave that reviewed AFTER
95
+ merging left its defect on `main` for two hours. So review is a mandatory step BEFORE
96
+ `scripts/merge-wave.sh`, run by a separate sub-agent on a mid-tier model, which is where the
97
+ defects were actually found. Third, concurrency: at most 3 issues a
98
+ cycle with non-overlapping file sets and at most 2 implementers at once, because the flaky
99
+ tests this repo already knows about redden under machine load and a red gate you caused
100
+ yourself costs a full re-run to disprove. The skill is a workflow and cites `AGENTS.md` and
101
+ `docs/RELEASING.md` by section rather than restating them; `test/maintain-skill.test.ts`
102
+ holds that premise mechanically — every `tldrx` command and `scripts/*.sh` it names must
103
+ exist in the real surface, every `§N` it cites must resolve to a real heading, and no private
104
+ workspace name or chat-product name may appear anywhere in the skill tree (#191). The review
105
+ step itself is still prose, which is the one thing the skill cannot fix about itself — #192
106
+ proposes making `scripts/merge-wave.sh` refuse a branch that carries no review record.
107
+
108
+ ### Fixed
109
+
110
+ - **No surface prints a bare `$0.00` over work nobody metered.** Measured across 23 real runs:
111
+ 45 % of 845 task rows are in-session turns recorded `cost_usd: null, metered: false`, which
112
+ every sum treats as contributing nothing — the only honest arithmetic there is. Two of those
113
+ runs then RENDERED `spent_usd: 0.00` against $3,000 and $200 ceilings after 30 and 9
114
+ stories. The counting and the caveat already existed (#103, #139); what did not was a rule
115
+ for the FIGURE, so each screen printed `$${n.toFixed(2)}` in its own words and three printed
116
+ it with no caveat at all. One implementation now writes it everywhere: `≥ $12.40 (7 tasks
117
+ unmetered)` when some of the work was metered, and `not measured: 9 in-session tasks, 0
118
+ metered` when none of it was and there is no floor worth printing. `run status`, `budget
119
+ show`, the dashboard's row and its hero, `replay`, `run auto`'s per-loop lines, the Build
120
+ handoff's `Cost:` header (and so the ship PR body, which embeds it), the `budget.warned`
121
+ note and every notification payload all read from it. `budget.yml` gains `unmetered_tasks`
122
+ and `spent_basis: lower-bound | complete` so an archived file alone still tells the truth —
123
+ emitted only when there IS something unmetered, so a fully metered run and every budget.yml
124
+ written before them stay byte-identical. `spent_usd` is unchanged and still what the ceiling
125
+ arithmetic enforces; a run that really did meter everything keeps its plain figure, because
126
+ a caveat on every screen is a caveat nobody reads.
127
+
4
128
  ## 0.11.1 — 2026-09-08
5
129
 
6
130
  ### Changed
package/README.md CHANGED
@@ -316,6 +316,7 @@ back on the registry is 0.3.0.
316
316
 
317
317
  | Version | Date | Status | Contains |
318
318
  |---|---|---|---|
319
+ | 0.12.0 | 2026-09-08 | `beta` | records that can be attributed and spans that were actually measured: the reviewer can be pinned to its own model and effort per role (`reviewer:`) and per story stakes (`reviewer_by_stakes:`, keyed on a story's new optional `stakes:` enum), resolved field by field under `--model`/`--effort` and shipping NO opus default — because there is no evidence yet that a stronger reviewer finds more, only the record that lets the evidence accumulate: every verdict now names the model that produced it, a host review reading `basis: host-declared` off its own flags and a host that declared nothing reading `not recorded` rather than the bundle's suggestion; `run.yml` gains `created_with` and `last_written_by` beside the file format's own `version: 1`, and `agent.spawned`/`agent.result` carry `tldrx_version`, so a run that outlived an upgrade carries both ends of the range that drove it; a task row carries `duration_ms` that never travels without `duration_basis` — `spawned` is the wall clock around the sub-agent's process, `prepare-to-commit` is a ceiling that includes the host's own time — and `tldrx cost` shows a duration per attempt and a per-stage sum that names a mixed basis instead of adding two different quantities, with a pre-existing row reading `not recorded` and never `0s`; no surface prints a bare `$0.00` over work nobody metered — one implementation writes `≥ $12.40 (7 tasks unmetered)` or `not measured: 9 in-session tasks, 0 metered` across `run status`, `budget show`, the dashboard, `replay`, `run auto`, the Build handoff and every notification, and `budget.yml` gains `unmetered_tasks` and `spent_basis`; the surface a story actually changed is measured off its own diff over the range the reviewer was shown and appended as one `story.touches_widened` with `basis: "measured"` beside what was declared, advisory and never rewriting the operator's `touches:`; and a `maintain` skill encodes the maintenance circuit twelve hand-run waves actually measured — reproduce an issue on current `origin/main` before touching anything, a fresh reviewer before `merge-wave.sh` and not after, at most 3 issues and 2 implementers a cycle — pinned by a test that every command and `§N` it cites resolves |
319
320
  | 0.11.1 | 2026-09-08 | `beta` | operating a run when nobody is watching: the drive mandate now names the host's own context as the costliest instrument and tells the driver what to READ back — a sub-agent's outcome from its `result.json` and the ledger, never its transcript, each sub-agent briefed to report its turn in ten lines, and a cited file re-opened to VERIFY at a gate rather than re-read for a claim already made — paid for inside the unchanged line budgets rather than appended; `.tldrx/workspace.yml` takes one optional `notify:` block whose command is handed a `version: 1` JSON object on stdin at each moment a person is needed (an open question with its options, its recommendation and the literal `tldrx answer` line; a gate with its approve line; a finished or failed run with what its exit code's family means; a budget warning with both numbers), split to argv and never shelled, with a notifier that will not split, is not there, exits non-zero or hangs recorded as `notify.failed` instead of failing the run; `tldrx run auto --wait-answers <duration>` polls the question files and resumes when the answer lands instead of exiting 4 the moment a stage parks, and `--notify-every <duration>` sends a timer-driven heartbeat that reminds rather than reassures while a run is parked; and a new EN+ES guide page walks the whole unattended loop — host mode versus the engine, the payload keys, a dependency-free Node adapter, a first-run checklist and the four ways it silently does nothing — naming no messaging service, by the same reasoning the mandate has always given |
320
321
  | 0.11.0 | 2026-09-08 | `beta` | governance that leaves a record: every answer says who decided it and what repo it binds, an advisory contradiction check raises a question that never stops a run and states its own limit, the close and the Build handoff count decided vs not-stated; `tldrx story widen` is the sanctioned way to grow a story's surface (a done story needs `reopen --for-fix` first) and a defect no story owns is named in the handoff, the PR body and the boundary card instead of absorbed; a budget grant is a recorded number in `budget.yml` that survives every writer, `budget raise` warns or refuses against it, presets are labelled as the assumptions they are, `triage.budget_basis` says where a figure came from and `tldrx cost --stories` measures each story against the ceiling its spawn was given, honest about lower bounds |
321
322
  | 0.10.0 | 2026-09-07 | `beta` | the mechanical blockers measured in real runs, fixed: a refused DoD command is recorded as refused (never a fabricated exit 126) in the event, the handoff, the review and retro logs, and the readers that used to recover it as green; the dirty-tree refusal prints the exact stash and pop commands and its true reason; the reviewer diffs the epic as it was before the merge (`epic_base` recorded on the story and the bundle) instead of an empty range; `tldrx init` probes each declared command once and records `command_probes` beside a `commands:` allowlist it never guesses; `tldrx ship` opens a PR whose body describes the change and its known defects, with the handoff folded underneath, and a state refusal that honours settled stories' declared touches; reviewer turns carry their token split and the spend basis reads it, so a whole provider no longer reads as absent |
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  conflictOf
4
- } from "./chunk-qn8qnrgw.js";
4
+ } from "./chunk-fyr7f9t6.js";
5
5
  import {
6
6
  FactsStore,
7
7
  formatJaccard
8
- } from "./chunk-gj1ckg0c.js";
8
+ } from "./chunk-nwzevx0j.js";
9
9
  import {
10
10
  parseHookInput,
11
11
  readStdin
12
- } from "./chunk-8d0t69pk.js";
12
+ } from "./chunk-mvz7jwt8.js";
13
13
  import {
14
14
  EventLog,
15
15
  PHASE_ID_RE
16
- } from "./chunk-rw3n3z6t.js";
17
- import"./chunk-b26rvgjd.js";
16
+ } from "./chunk-f1w9sy07.js";
17
+ import"./chunk-yvgkw0gm.js";
18
18
  import {
19
19
  ADVISORY_KEY,
20
20
  MAX_FACT_CHARS,
@@ -24,7 +24,7 @@ import {
24
24
  renderQuestionBlock,
25
25
  replaceBlock,
26
26
  serializeQuestions
27
- } from "./chunk-h23db062.js";
27
+ } from "./chunk-1knfmj6j.js";
28
28
  import {
29
29
  ITERATION_ONLY_SLOT,
30
30
  PROJECT_FRAMEWORK_DIR,
@@ -32,7 +32,7 @@ import {
32
32
  commandProbeIssues,
33
33
  factsPath,
34
34
  parseYaml
35
- } from "./chunk-5hyd1rk1.js";
35
+ } from "./chunk-6fywd8xd.js";
36
36
 
37
37
  // src/hooks/answer-capture.ts
38
38
  import { existsSync as existsSync5 } from "fs";
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-zs84arc1.js";
4
+ } from "./chunk-qefqmgjn.js";
5
5
  import {
6
6
  allow,
7
7
  deny,
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-hx0jgrmh.js";
12
- import"./chunk-8d0t69pk.js";
11
+ } from "./chunk-v2xzr4qd.js";
12
+ import"./chunk-mvz7jwt8.js";
13
13
  import {
14
14
  asRunBudget,
15
15
  currentActor,
@@ -29,20 +29,20 @@ import {
29
29
  validateRunBudget,
30
30
  wouldExceed,
31
31
  wouldExceedHostTokens
32
- } from "./chunk-jmypgf6s.js";
32
+ } from "./chunk-zrjgpgwy.js";
33
33
  import {
34
34
  EventLog
35
- } from "./chunk-rw3n3z6t.js";
35
+ } from "./chunk-f1w9sy07.js";
36
36
  import {
37
37
  noteDeprecations
38
- } from "./chunk-h23db062.js";
38
+ } from "./chunk-1knfmj6j.js";
39
39
  import {
40
40
  PROJECT_WORK_DIR,
41
41
  findWorkspaceRoot,
42
42
  locateWork,
43
43
  parseYaml,
44
44
  stageYamlPath
45
- } from "./chunk-5hyd1rk1.js";
45
+ } from "./chunk-6fywd8xd.js";
46
46
 
47
47
  // src/hooks/budget-gate.ts
48
48
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseSrcToken
3
- } from "./chunk-5hyd1rk1.js";
3
+ } from "./chunk-6fywd8xd.js";
4
4
 
5
5
  // src/core/text/questions.ts
6
6
  var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
@@ -9122,4 +9122,4 @@ function noneBullet(lookedAt) {
9122
9122
  return `- none [src: absent:${lookedAt}]`;
9123
9123
  }
9124
9124
 
9125
- export { runtime, parseYaml, parseYamlRepairing, STAGES_DIR, TEMPLATES_DIR, PROJECT_FRAMEWORK_DIR, PROJECT_WORK_DIR, result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, requireRecord, asDocument, STORY_ID_RE, WAVE_ID_RE, MAX_WAVES, MAX_STORIES_PER_WAVE, MAX_PLAN_STORIES, requireStringList, requirePattern, requireVersion1, parseFrontMatter, ITERATION_ONLY_SLOT, readStory, DodCommandRefused, runDodCommand, commandProbeIssues, readableSource, SRC_PATTERNS, srcRule, parseSrcToken, classifySrc, locateWork, findWorkspaceRoot, loadWorkspace, toSrcContext, repoPath, factsPath, stageYamlPath, listRunDirs, parseHandoff, isHandoff, BULLET_RULE, EMPTY_SECTION_RULE, validateHandoff, validateCitations, noneBullet };
9125
+ export { runtime, parseYaml, parseYamlRepairing, FRAMEWORK_ROOT, STAGES_DIR, TEMPLATES_DIR, PROJECT_FRAMEWORK_DIR, PROJECT_WORK_DIR, result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, requireRecord, asDocument, STORY_ID_RE, WAVE_ID_RE, MAX_WAVES, MAX_STORIES_PER_WAVE, MAX_PLAN_STORIES, requireStringList, requirePattern, requireVersion1, parseFrontMatter, ITERATION_ONLY_SLOT, readStory, DodCommandRefused, runDodCommand, commandProbeIssues, readableSource, SRC_PATTERNS, srcRule, parseSrcToken, classifySrc, locateWork, findWorkspaceRoot, loadWorkspace, toSrcContext, repoPath, factsPath, stageYamlPath, listRunDirs, parseHandoff, isHandoff, BULLET_RULE, EMPTY_SECTION_RULE, validateHandoff, validateCitations, noneBullet };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  toolInput
3
- } from "./chunk-hx0jgrmh.js";
3
+ } from "./chunk-v2xzr4qd.js";
4
4
 
5
5
  // src/hooks/lib/wouldBe.ts
6
6
  import { existsSync, readFileSync } from "node:fs";
@@ -10,7 +10,7 @@ import {
10
10
  requireString,
11
11
  requireVersion,
12
12
  result
13
- } from "./chunk-5hyd1rk1.js";
13
+ } from "./chunk-6fywd8xd.js";
14
14
 
15
15
  // src/core/events/EventLog.ts
16
16
  import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -448,6 +448,9 @@ function describe(value) {
448
448
  return typeof value === "string" ? `\`${value}\`` : String(value);
449
449
  }
450
450
 
451
+ // src/core/run/duration.ts
452
+ var DURATION_BASES = ["spawned", "prepare-to-commit"];
453
+
451
454
  // src/core/run/RunFile.ts
452
455
  var STAGE_STATUSES = [
453
456
  "pending",
@@ -596,6 +599,10 @@ function validateRunFile(input) {
596
599
  }
597
600
  if (doc.attended_by !== undefined)
598
601
  requireEnum(doc.attended_by, ATTENDED_BY, "attended_by", issues);
602
+ for (const key of ["created_with", "last_written_by"]) {
603
+ if (doc[key] !== undefined)
604
+ requireString(doc[key], key, issues);
605
+ }
599
606
  if (doc.keep_worktrees !== undefined && typeof doc.keep_worktrees !== "boolean") {
600
607
  issues.push({ path: "keep_worktrees", message: `expected a boolean, got ${typeof doc.keep_worktrees}` });
601
608
  }
@@ -717,6 +724,17 @@ function validateRunFile(input) {
717
724
  issues.push({ path: `${tp}.${key}`, message: "expected a number >= 0" });
718
725
  }
719
726
  }
727
+ if (task.duration_ms !== undefined) {
728
+ if (typeof task.duration_ms !== "number" || !Number.isFinite(task.duration_ms) || task.duration_ms < 0) {
729
+ issues.push({ path: `${tp}.duration_ms`, message: "expected a number >= 0" });
730
+ }
731
+ requireEnum(task.duration_basis, DURATION_BASES, `${tp}.duration_basis`, issues);
732
+ } else if (task.duration_basis !== undefined) {
733
+ issues.push({
734
+ path: `${tp}.duration_basis`,
735
+ message: "a duration_basis with no duration_ms names the basis of nothing"
736
+ });
737
+ }
720
738
  if (typeof task.cost_usd === "number")
721
739
  spentFromTasks += task.cost_usd;
722
740
  checkOrder(task.started_at, task.ended_at, tp, issues);
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  findDuplicate
3
- } from "./chunk-gj1ckg0c.js";
3
+ } from "./chunk-nwzevx0j.js";
4
4
  import {
5
5
  evidencePath,
6
6
  gateEvidencePath,
7
7
  parseEvidence
8
- } from "./chunk-rw3n3z6t.js";
8
+ } from "./chunk-f1w9sy07.js";
9
9
  import {
10
10
  openBlocks,
11
11
  parseQuestions
12
- } from "./chunk-h23db062.js";
12
+ } from "./chunk-1knfmj6j.js";
13
13
 
14
14
  // src/core/distill/distill.ts
15
15
  var CONFLICT_THRESHOLD = 0.6;
@@ -7,8 +7,10 @@ import {
7
7
  isAttendedByHostView,
8
8
  nowRfc3339,
9
9
  openRunViews,
10
+ spentBasis,
11
+ tallyOf,
10
12
  validateRunBudget
11
- } from "./chunk-jmypgf6s.js";
13
+ } from "./chunk-zrjgpgwy.js";
12
14
  import {
13
15
  EventLog,
14
16
  asRunFile,
@@ -20,7 +22,7 @@ import {
20
22
  isTerminal,
21
23
  stageAt,
22
24
  validateRunFile
23
- } from "./chunk-rw3n3z6t.js";
25
+ } from "./chunk-f1w9sy07.js";
24
26
  import {
25
27
  backupPathFor,
26
28
  isAlive,
@@ -29,26 +31,43 @@ import {
29
31
  workspaceRootOfRunDir,
30
32
  writeAtomic,
31
33
  yamlScalar
32
- } from "./chunk-b26rvgjd.js";
34
+ } from "./chunk-yvgkw0gm.js";
33
35
  import {
34
36
  isAdvisory,
35
37
  noteDeprecations,
36
38
  openBlocks,
37
39
  parseQuestions
38
- } from "./chunk-h23db062.js";
40
+ } from "./chunk-1knfmj6j.js";
39
41
  import {
42
+ FRAMEWORK_ROOT,
40
43
  listRunDirs,
41
44
  parseYaml,
42
45
  parseYamlRepairing
43
- } from "./chunk-5hyd1rk1.js";
46
+ } from "./chunk-6fywd8xd.js";
44
47
 
45
48
  // src/core/statusline/runSnapshot.ts
46
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
47
- import { join as join2 } from "node:path";
49
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
50
+ import { join as join3 } from "node:path";
48
51
 
49
52
  // src/core/run/RunStore.ts
50
- import { existsSync, readFileSync } from "node:fs";
51
- import { basename, join } from "node:path";
53
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
54
+ import { basename, join as join2 } from "node:path";
55
+
56
+ // src/core/frameworkVersion.ts
57
+ import { readFileSync } from "node:fs";
58
+ import { join } from "node:path";
59
+ var PACKAGE_JSON = join(FRAMEWORK_ROOT, "package.json");
60
+ function versionOf(pkg) {
61
+ const version = pkg?.version;
62
+ return typeof version === "string" ? version : "0.0.0";
63
+ }
64
+ function frameworkVersionSync() {
65
+ try {
66
+ return versionOf(JSON.parse(readFileSync(PACKAGE_JSON, "utf8")));
67
+ } catch {
68
+ return "0.0.0";
69
+ }
70
+ }
52
71
 
53
72
  // src/core/run/emitRunYaml.ts
54
73
  function inlineList(values) {
@@ -91,6 +110,7 @@ function task(t, indent) {
91
110
  ...t.stopped_by === undefined || t.stopped_by === null ? [] : [`${inner}stopped_by: ${yamlScalar(t.stopped_by)},`],
92
111
  ...t.banked_before_refusal === undefined ? [] : [`${inner}banked_before_refusal: true,`],
93
112
  ...t.dedupe === undefined ? [] : [`${inner}dedupe: ${yamlScalar(t.dedupe)},`],
113
+ ...t.duration_ms === undefined ? [] : [`${inner}duration_ms: ${String(Math.round(t.duration_ms))}, ` + `duration_basis: ${yamlScalar(t.duration_basis ?? null)},`],
94
114
  `${inner}outputs: ${inlineList(t.outputs)}}`
95
115
  ].join(`
96
116
  `);
@@ -126,6 +146,8 @@ function emitRunYaml(run) {
126
146
  "# tldrx-work/<run>/run.yml — the execution path and the only resume point (spec §2.2).",
127
147
  "# Written by the facilitator alone. Hand-edit at your own risk: every write revalidates.",
128
148
  `version: ${run.version}`,
149
+ ...run.created_with === undefined ? [] : [`created_with: ${yamlScalar(run.created_with)}`],
150
+ ...run.last_written_by === undefined ? [] : [`last_written_by: ${yamlScalar(run.last_written_by)}`],
129
151
  `run: ${yamlScalar(run.run)}`,
130
152
  `title: ${yamlScalar(run.title)}`,
131
153
  `scope: ${yamlScalar(run.scope)}`,
@@ -190,6 +212,10 @@ function emitBudgetYaml(budget) {
190
212
  ...budget.authorized_by === null ? [] : [`authorized_by: ${yamlScalar(budget.authorized_by)}`],
191
213
  ...budget.authorized_at === null ? [] : [`authorized_at: ${yamlScalar(budget.authorized_at)}`],
192
214
  ...budget.on_grant_exceed === DEFAULT_ON_GRANT_EXCEED ? [] : [`on_grant_exceed: ${yamlScalar(budget.on_grant_exceed)}`],
215
+ ...budget.unmetered_tasks > 0 ? [
216
+ `unmetered_tasks: ${String(budget.unmetered_tasks)}`,
217
+ `spent_basis: ${yamlScalar(budget.spent_basis)}`
218
+ ] : [],
193
219
  "phases:"
194
220
  ];
195
221
  for (const phase of budget.phases) {
@@ -223,7 +249,7 @@ class RunStore {
223
249
  this.currentBudget = budget;
224
250
  }
225
251
  static open(runDir) {
226
- const runPath = join(runDir, "run.yml");
252
+ const runPath = join2(runDir, "run.yml");
227
253
  if (!existsSync(runPath))
228
254
  throw new RunStoreError(`no run.yml in ${runDir}`);
229
255
  const run = parseStateFile(runPath);
@@ -233,7 +259,7 @@ class RunStore {
233
259
  const first = validation.issues[0];
234
260
  throw new RunStoreError(`invalid run.yml (${runPath}): ${first?.path ?? ""} ${first?.message ?? "schema error"}`);
235
261
  }
236
- const budgetPath = join(runDir, "budget.yml");
262
+ const budgetPath = join2(runDir, "budget.yml");
237
263
  if (!existsSync(budgetPath))
238
264
  throw new RunStoreError(`no budget.yml in ${runDir}`);
239
265
  const budget = parseStateFile(budgetPath);
@@ -252,9 +278,9 @@ class RunStore {
252
278
  try {
253
279
  withWorkspaceLock(workspaceRootOfRunDir(this.runDir), () => {
254
280
  if (runRepaired)
255
- writeAtomic(join(this.runDir, "run.yml"), emitRunYaml(this.current));
281
+ writeAtomic(join2(this.runDir, "run.yml"), emitRunYaml(this.current));
256
282
  if (budgetRepaired)
257
- writeAtomic(join(this.runDir, "budget.yml"), emitBudgetYaml(this.currentBudget));
283
+ writeAtomic(join2(this.runDir, "budget.yml"), emitBudgetYaml(this.currentBudget));
258
284
  });
259
285
  const which = [runRepaired ? "run.yml" : "", budgetRepaired ? "budget.yml" : ""].filter((name) => name !== "").join(" and ");
260
286
  process.stderr.write(`tldrx: ${this.runDir}: ${which} held text broken across lines by an emitter bug ` + "since fixed. It was repaired in memory and rewritten correctly; the version that was " + `on disk is beside it as .bak. Nothing else about the run was changed.
@@ -338,8 +364,8 @@ class RunStore {
338
364
  const first = budgetValidation.issues[0];
339
365
  throw new RunStoreError(`refusing to write an invalid budget.yml: ${first?.path ?? ""} ${first?.message ?? ""}`);
340
366
  }
341
- writeAtomic(join(this.runDir, "budget.yml"), emitBudgetYaml(budget));
342
- writeAtomic(join(this.runDir, "run.yml"), emitRunYaml(rolled));
367
+ writeAtomic(join2(this.runDir, "budget.yml"), emitBudgetYaml(budget));
368
+ writeAtomic(join2(this.runDir, "run.yml"), emitRunYaml(rolled));
343
369
  this.current = rolled;
344
370
  this.currentBudget = budget;
345
371
  this.budgetMutated = false;
@@ -348,11 +374,11 @@ class RunStore {
348
374
  ceilingsToWrite() {
349
375
  if (this.budgetMutated)
350
376
  return this.currentBudget;
351
- const path = join(this.runDir, "budget.yml");
377
+ const path = join2(this.runDir, "budget.yml");
352
378
  if (!existsSync(path))
353
379
  return this.currentBudget;
354
380
  try {
355
- const doc = parseYaml(readFileSync(path, "utf8"));
381
+ const doc = parseYaml(readFileSync2(path, "utf8"));
356
382
  const validation = validateRunBudget(doc);
357
383
  if (!validation.ok)
358
384
  return this.currentBudget;
@@ -376,7 +402,7 @@ class RunStore {
376
402
  }
377
403
  }
378
404
  function parseStateFile(path) {
379
- const text = readFileSync(path, "utf8");
405
+ const text = readFileSync2(path, "utf8");
380
406
  try {
381
407
  return parseYamlRepairing(text);
382
408
  } catch (error) {
@@ -406,7 +432,8 @@ function rollUp(run) {
406
432
  ...run,
407
433
  phases,
408
434
  budget: { ...run.budget, spent_usd: spent },
409
- updated_at: nowRfc3339()
435
+ updated_at: nowRfc3339(),
436
+ last_written_by: frameworkVersionSync()
410
437
  };
411
438
  return { ...next, status: deriveRunStatus(next) };
412
439
  }
@@ -415,8 +442,11 @@ function rollUpBudget(budget, run) {
415
442
  for (const phase of run.phases) {
416
443
  spentByPhase.set(phase.id, round(phase.stages.reduce((sum, s) => sum + s.cost_usd, 0)));
417
444
  }
445
+ const tally = tallyOf(run.phases.flatMap((p) => p.stages.flatMap((s) => s.tasks)));
418
446
  return {
419
447
  ...budget,
448
+ unmetered_tasks: tally.unmetered,
449
+ spent_basis: spentBasis(tally.unmetered),
420
450
  phases: budget.phases.map((p) => ({ ...p, spent_usd: spentByPhase.get(p.id) ?? p.spent_usd }))
421
451
  };
422
452
  }
@@ -456,7 +486,7 @@ function describeGateSignature(sig) {
456
486
  }
457
487
 
458
488
  // src/core/facilitator/skipIf.ts
459
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
489
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
460
490
  function phaseDirs(runDir) {
461
491
  try {
462
492
  return readdirSync(runDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^0[1-9]-/.test(entry.name)).map((entry) => entry.name).sort();
@@ -468,7 +498,7 @@ function openQuestionIds(path) {
468
498
  if (!existsSync2(path))
469
499
  return [];
470
500
  try {
471
- return openBlocks(parseQuestions(readFileSync2(path, "utf8")).blocks).map((block) => block.id);
501
+ return openBlocks(parseQuestions(readFileSync3(path, "utf8")).blocks).map((block) => block.id);
472
502
  } catch {
473
503
  return [];
474
504
  }
@@ -477,7 +507,7 @@ function blockingQuestionIds(path) {
477
507
  if (!existsSync2(path))
478
508
  return [];
479
509
  try {
480
- return openBlocks(parseQuestions(readFileSync2(path, "utf8")).blocks).filter((block) => !isAdvisory(block)).map((block) => block.id);
510
+ return openBlocks(parseQuestions(readFileSync3(path, "utf8")).blocks).filter((block) => !isAdvisory(block)).map((block) => block.id);
481
511
  } catch {
482
512
  return [];
483
513
  }
@@ -490,7 +520,7 @@ function runSnapshot(root) {
490
520
  function openQuestions(snapshot) {
491
521
  const ids = [];
492
522
  for (const phase of phaseDirs(snapshot.runDir)) {
493
- ids.push(...openQuestionIds(join2(snapshot.runDir, phase, "questions.md")));
523
+ ids.push(...openQuestionIds(join3(snapshot.runDir, phase, "questions.md")));
494
524
  }
495
525
  return ids;
496
526
  }
@@ -558,11 +588,11 @@ function fromTolerantRead(root) {
558
588
  };
559
589
  }
560
590
  function readBudgetMirror(runDir) {
561
- const path = join2(runDir, "run.yml");
591
+ const path = join3(runDir, "run.yml");
562
592
  if (!existsSync3(path))
563
593
  return { ceiling_usd: 0, spent_usd: 0 };
564
594
  try {
565
- const text = readFileSync3(path, "utf8");
595
+ const text = readFileSync4(path, "utf8");
566
596
  return {
567
597
  ceiling_usd: numberAfter(text, "ceiling_usd"),
568
598
  spent_usd: numberAfter(text, "spent_usd")
@@ -577,18 +607,18 @@ function numberAfter(text, key) {
577
607
  }
578
608
 
579
609
  // src/core/run/waiting.ts
580
- import { basename as basename2, join as join4 } from "node:path";
610
+ import { basename as basename2, join as join5 } from "node:path";
581
611
 
582
612
  // src/core/run/prepared.ts
583
613
  import { existsSync as existsSync4, readdirSync as readdirSync2, statSync } from "node:fs";
584
- import { join as join3 } from "node:path";
614
+ import { join as join4 } from "node:path";
585
615
  var PENDING_JSON = "pending.json";
586
616
  function preparedBundles(runDir, stageId) {
587
- const dir = join3(runDir, ".agent", stageId);
617
+ const dir = join4(runDir, ".agent", stageId);
588
618
  if (!existsSync4(dir))
589
619
  return [];
590
620
  const found = [];
591
- if (existsSync4(join3(dir, PENDING_JSON)))
621
+ if (existsSync4(join4(dir, PENDING_JSON)))
592
622
  found.push(dir);
593
623
  let entries;
594
624
  try {
@@ -597,14 +627,14 @@ function preparedBundles(runDir, stageId) {
597
627
  return found;
598
628
  }
599
629
  for (const entry of entries) {
600
- const child = join3(dir, entry);
630
+ const child = join4(dir, entry);
601
631
  try {
602
632
  if (!statSync(child).isDirectory())
603
633
  continue;
604
634
  } catch {
605
635
  continue;
606
636
  }
607
- if (existsSync4(join3(child, PENDING_JSON)))
637
+ if (existsSync4(join4(child, PENDING_JSON)))
608
638
  found.push(child);
609
639
  }
610
640
  return found;
@@ -651,7 +681,7 @@ function waitingFor(run, runDir) {
651
681
  questions: []
652
682
  };
653
683
  }
654
- const open = blockingQuestionIds(join4(runDir, cursor.phase, "questions.md"));
684
+ const open = blockingQuestionIds(join5(runDir, cursor.phase, "questions.md"));
655
685
  switch (entry.stage.status) {
656
686
  case "awaiting_gate":
657
687
  return {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runtime
3
- } from "./chunk-5hyd1rk1.js";
3
+ } from "./chunk-6fywd8xd.js";
4
4
 
5
5
  // src/core/hooks/passthrough.ts
6
6
  async function readStdin() {
@@ -3,7 +3,7 @@ import {
3
3
  withWorkspaceLock,
4
4
  workspaceRootOfFactsPath,
5
5
  writeAtomic
6
- } from "./chunk-b26rvgjd.js";
6
+ } from "./chunk-yvgkw0gm.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-h23db062.js";
17
+ } from "./chunk-1knfmj6j.js";
18
18
  import {
19
19
  SRC_PATTERNS,
20
20
  asDocument,
@@ -27,7 +27,7 @@ import {
27
27
  requireString,
28
28
  requireVersion,
29
29
  result
30
- } from "./chunk-5hyd1rk1.js";
30
+ } from "./chunk-6fywd8xd.js";
31
31
 
32
32
  // src/core/facts/FactsStore.ts
33
33
  import { existsSync, readFileSync } from "node:fs";
@@ -3,7 +3,7 @@ import {
3
3
  EMPTY_SECTION_RULE,
4
4
  noneBullet,
5
5
  srcRule
6
- } from "./chunk-5hyd1rk1.js";
6
+ } from "./chunk-6fywd8xd.js";
7
7
 
8
8
  // src/hooks/lib/messages.ts
9
9
  var GRAMMAR = "[src: <repo:path:line> | https://… | Q<n> | F<n> | $ <cmd> → exit <n> | graph:<node> | absent:<path>]";