tldr-experts 0.12.0 → 0.13.1

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,163 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## 0.13.1 — 2026-09-08
5
+
6
+ ### Fixed
7
+
8
+ - **A stage prompt now opens by saying what to do (#196).** On a real workspace at 0.13.0 a
9
+ What sub-agent was handed the whole 66,452-byte prompt and answered: *"I don't see an actual
10
+ request in your message — only system context, tldrx state, and template/expert file dumps."*
11
+ It was right. `renderParts` prepended nothing, so the document opened on `stage.md` — which is
12
+ a fill-in HANDOFF TEMPLATE that describes a finished document without ever saying that writing
13
+ it is the job. Grepping that prompt found zero occurrences of "You are", "## Produce", "your
14
+ task" or "write the following files", while line ~89 of it forbade "sections beyond the ones
15
+ listed under **Produce**" — a rule pointing at a heading that was never assembled. The agent
16
+ asked a question instead, wrote none of its six declared outputs, and the stage failed with
17
+ $0.29 spent. The prompt now leads with a generated brief: who the reader is, which stage of
18
+ which run, that the template below is to be FILLED, the exact path of every declared output,
19
+ and that a question goes in the questions file rather than back to an operator who is not
20
+ there. Generated from the same `outputs:` list `pending.json` records, so it cannot name a
21
+ path the commit will not look for.
22
+ Not a regression — a 2026-08-30 run of the same stage on the same model passed. What changed
23
+ is the ratio: the spliced citation grammar grew the stage section 5,007 B → 13,180 B and this
24
+ run's seed was small, so work material fell from 93% to 76% of the prompt. The instruction was
25
+ always missing; it had never been this outnumbered.
26
+ - **The "N runs are open" nudge stops talking to sub-agents (#196).** It was the one
27
+ imperative-shaped sentence in that agent's window — `tldrx: 7 runs are open — pass a run id to
28
+ next/answer/approve/…` — and the agent answered it, which is the most reasonable thing it
29
+ could have done with what it was given. `spawnAgent` now marks every child it spawns
30
+ (`TLDRX_SUBAGENT=1`) and `session-start` emits nothing when it sees the marker: those lines
31
+ orient a human who opened a session and may be in the wrong run, and a sub-agent was handed
32
+ its run in its prompt. An absent marker is a human's session and behaves exactly as before.
33
+
34
+ ## 0.13.0 — 2026-09-08
35
+
36
+ ### Added
37
+
38
+ - **`tldrx expert rescore [<name>] [--area <a>]` — score the knowledge you already paid for.**
39
+ Its sibling `recompute` is arithmetic over the evidence rows already in `competencies.yml`;
40
+ `rescore` RE-READS `knowledge/*.md` and derives their evidence again under today's rules. It
41
+ exists because the gate fix below changes what the NEXT training run earns and can do nothing
42
+ for the files already on disk: for every expert the bug hit, `evidence:` is `[]`, so the only
43
+ other recovery is to buy the same readings a second time — **$9.47** of them in the measured
44
+ case. Like `recompute` it reads no code, spawns nothing, spends nothing, and leaves `status`
45
+ and `last_trained` exactly as it found them.
46
+ Rows are dated by the knowledge file's own `trained_at`, or by the expert's `last_trained`
47
+ when it has none — **never by the clock**, because §2.6 weighs recency and a reading taken in
48
+ August is not evidence gathered today. When neither date exists the file is skipped with that
49
+ as the reason, and so is a file that no longer validates: rescoring is a re-read under
50
+ today's rules, and keeping rows an older tldrx once accepted is the drift this refuses.
51
+ **A rescored row carries two dates, and the rescore writes itself into the ledger** — because
52
+ the workspaces this command exists to rescue are exactly the ones whose `training.jsonl`
53
+ already asserts `check.passed` with `evidence_added: 0`. Built and measured on the first cut:
54
+ a $0 rescore turned that ledger's own August date into real August-dated rows and moved the
55
+ level 0 → 1, leaving `training.jsonl` byte-identical — two files that contradict each other,
56
+ with nothing on disk saying a free re-derivation weeks later is what wrote the rows. So
57
+ `areas[].evidence[].rescored_at` is additive and optional beside `at`, and its ABSENCE keeps
58
+ the meaning every row already had: a paid training turn earned it. `at` still says when the
59
+ CLAIM was read and is still the only date the level formula weighs; `rescored_at` says when
60
+ the SCORING happened. And `training.jsonl` gains one `evidence.rescored` line per file whose
61
+ rescore actually moved something — `cost_usd: 0`, `spawned: 0`, dated when it RAN and never
62
+ backdated to match the rows, carrying `dated_at`/`dated_by` so the two clocks in one record
63
+ can never be read as one. A rescore that changes nothing writes no line: it contradicts
64
+ nothing, and a ledger that grows on every idempotent re-run is one nobody reads.
65
+
66
+ - **`scripts/merge-wave.sh` now refuses a branch that carries no review record (#192).** Every
67
+ other invariant this script protects got a mechanism — the lock (#44), the HEAD assertion, the
68
+ ref guard (#89), the unwind and orphan refusal (#116), the self-snapshot (#117). The pre-merge
69
+ review was still a paragraph, and prose loses to recency: measured over twelve maintenance
70
+ waves (2026-09-06 → 2026-09-07), **4 of the 12** pre-merge reviews found a real Important
71
+ defect the implementer then fixed before the branch merged, and the one wave that reviewed
72
+ *after* merging found one too — which then sat on `main` for ~2 hours, because once a commit is
73
+ published the only remedies are a follow-up merge or a revert and both wait for the next lock
74
+ window. That wave did not decide to skip the review; it simply did not remember at the moment
75
+ the branch was green, and nothing in the merge path could tell the difference afterwards.
76
+ So the record is now a FILE on the branch, `.review/<branch>.md`, asserted in the same preamble
77
+ block as the dirty-tree and orphan refusals: `verdict: merge` on the first line (anything else,
78
+ `verdict: fixes required` included, refuses), `reviewed-by:` naming who read it, and `against:`
79
+ naming the sha they read. A file rather than a commit trailer because it lands in the merge
80
+ commit's tree — `main` keeps answering "was this reviewed, by whom, against which diff" long
81
+ after the branch is deleted, which a trailer could not. **A stale record refuses rather than
82
+ warns**, and the refusal names both shas: a review of a different diff is exactly the hole the
83
+ honour system already allowed, and a warning would let it through. Staleness is measured as
84
+ "the code moved", not "the sha differs" — the named sha must be an ancestor of the branch head
85
+ with no path outside `.review/` changed since — because a record can only ever name the commit
86
+ the reviewer READ, and committing the record moves the head past exactly that sha, so a literal
87
+ sha-equals-head rule would be unsatisfiable by construction. A rebase does invalidate a record,
88
+ correctly: it is a different diff. The exit code is **10**, a new one: that script's table is
89
+ its own namespace with one code per condition, `2` there is already "merge conflict", and the
90
+ CLI refusal families in `src/cli/exitCodes.ts` — where 2 IS the gate refusal — do not reach a
91
+ shell script that never imports them. Every refusal prints the file path and the three
92
+ lines it wants, so a session that trips the gate never has to read the script to satisfy it.
93
+ There is **no escape hatch** — no flag, no env var — on the issue's own argument that a
94
+ documented one is a hole the moment it exists. Scoped to the MERGE PATH alone:
95
+ `scripts/release.sh` commits on `main` directly, waves nothing and names `merge-wave.sh`
96
+ nowhere (measured — `grep -n 'merge-wave' scripts/release.sh scripts/release-check.sh
97
+ docs/RELEASING.md` exits 1 with no output), so releasing is untouched and `docs/RELEASING.md`
98
+ needed no change. `AGENTS.md` §2 carries the rule and the `maintain` skill cites it rather
99
+ than restating it.
100
+
101
+ ### Fixed
102
+
103
+ - **The mutation check is asked of the developer, which can run it, instead of the reviewer,
104
+ which holds no pen.** Every stack pack's `## Checks` carried "Can each new test fail? verify:
105
+ change the line under test, re-run only that test's file … and confirm it goes red", and those
106
+ Checks are rendered verbatim into the reviewer prompt under `## Stack checks` — a hundred lines
107
+ above a Rule in that same prompt reading "You have no write tool." That was not a wording slip
108
+ in one of the two places: `REVIEWER_TOOLS` is `Read`, `Grep`, `Glob`, `Bash(git diff *)`, so the
109
+ role being asked held neither the pen the mutation needs nor a way to run a test. Measured over
110
+ a week of unattended runs on three real workspaces, hosts resolved the contradiction by hand
111
+ every time and nobody reported the check as unaskable — which is the worse outcome, because a
112
+ reviewer that cannot perform a check still answers it, from reading alone, and calls that an
113
+ answer. It is a producer's obligation, so it goes to the producer: the developer's contract now
114
+ carries it as step 5 of `## Investigate` (`MUTATION_PROOF_RULE`), naming the same instrument the
115
+ packs named — the one test file while iterating, the declared command once, at the Definition of
116
+ Done — and asking for the record it leaves. What is left in the packs is the reviewer's half,
117
+ and it is a read: *did the developer record, beside each new test, that it was seen to fail?* A
118
+ new test carrying none is a finding with a cited file. Where the record goes is the part that
119
+ had to be decided rather than assumed — the reviewer's allowance holds no `git log`, so a commit
120
+ message is a surface it cannot read, and beside the test, in the test file, is the one place
121
+ that is both the developer's to write and the reviewer's to cite. The developer prompt's bytes
122
+ change and `test/build-golden.test.ts` moves with them; the reviewer prompt's own bytes do not.
123
+ - **A role expert could never earn evidence — `--mode full` mines the run record and the domain
124
+ gate refused it** (gh #154). `roleTraining.ts` states the premise: full mode's pre-pass reads
125
+ `tldrx-work/<run>/**/{handoff,retro}.md`, "the record of how this workflow actually ran, which
126
+ IS a role's domain". `outsideDomain` then judged every one of those citations against the
127
+ `## Domain` bullets of `expert.md`, which name folders of CODE — so 100% of what the runs pass
128
+ produced was discarded as `outside domain`, in every workspace, every time.
129
+ Measured on a real workspace at 0.8.0: four role experts, `--mode full`, **$9.47 spent and one
130
+ evidence row** — and that row is a fact token, the one citation kind the gate never inspects.
131
+ And it was not fixable from the workspace: `domainPaths()` drops any single-segment bullet, so
132
+ `tldrx-work` — the only spelling the matcher would match — cannot be declared, while every
133
+ spelling that DOES register (`tldrx-work/**`, `tldrx-work/<run>`) is a literal the
134
+ no-globbing matcher can never reach. The two sets are disjoint.
135
+ Re-measured on 0.12.0 before the fix: `domainPaths()` over the five shipped role templates
136
+ still yields `.tldrx/map/**`, `.tldrx/process.yml` + `.tldrx/map/workspace.md`,
137
+ `.tldrx/conventions/shared.md`, `.tldrx/map/{repo}/gotchas.md` and `.tldrx/memory/facts.yml`,
138
+ and `pathsIntersect` is `false` against every one of them for a citation to
139
+ `tldrx-work/<run>/03-build/handoff.md` — so no role expert could earn a row from the runs
140
+ pass at 0.12.0 either.
141
+ The gate now treats the run record as in-domain for the file mined FROM it, scoped to the pass
142
+ and not to the expert's `kind:` — a light file citing a handoff is still out of domain, and
143
+ still says so.
144
+ - **Two shipped role templates declared a `## Domain` path that matched nothing at all.**
145
+ `pathsIntersect` is segment-prefix matching with no globbing, so architect's `.tldrx/map/**`
146
+ and operations' `.tldrx/map/{repo}/gotchas.md` were literal strings: even a citation to the
147
+ map itself was refused. Both are now spelled `.tldrx/map/`, which the matcher reaches; the
148
+ nuance each carried stays in the bullet's prose. Globs and `{repo}` placeholders remain
149
+ unsupported in a `## Domain` bullet — a test now refuses one in any shipped role template
150
+ rather than letting it look like it works.
151
+ - **A paid training pass that earned nothing said so nowhere durable.** The ledger recorded
152
+ `problems` only on the failure path, so a file that validated, cost $1.61 and added zero
153
+ evidence rows was written down as `check.passed` with `evidence_added: 0` and no reason beside
154
+ it — the operator's next signal being `tldrx status` telling them to train it again. The
155
+ `check.passed` record now carries its `warnings`, and the run prints `the level did not move —
156
+ $X.XX bought 0 evidence row(s)` with the reasons under it. It stays exit `0` and the file is
157
+ still kept: a warning is a way of being worth nothing, not a lie, and quarantining an honest
158
+ file would destroy the one thing that was paid for. (The sibling of #101 on the output side —
159
+ that one refuses an empty INPUT before the money; nothing asked the question after.)
160
+
4
161
  ## 0.12.0 — 2026-09-08
5
162
 
6
163
  ### Added
package/README.md CHANGED
@@ -316,6 +316,8 @@ back on the registry is 0.3.0.
316
316
 
317
317
  | Version | Date | Status | Contains |
318
318
  |---|---|---|---|
319
+ | 0.13.1 | 2026-09-08 | `beta` | a stage prompt that opens by saying what to do: every stage prompt now leads with a generated brief — who the reader is, which stage of which run, that the template below is to be FILLED, the exact path of every declared output, and that a question goes in the questions file rather than back to an operator who is not there — because on a real workspace at 0.13.0 a What sub-agent read its 66,452-byte prompt, found no request in it, wrote none of its six declared outputs and asked what to do, $0.29 spent; the brief is generated from the same `outputs:` list `pending.json` records, so it cannot name a path the commit will not look for, and the failure was never a regression — the spliced citation grammar grew the stage section 5,007 B → 13,180 B and the missing instruction was finally outnumbered; and the `N runs are open` nudge, the one imperative-shaped sentence in that agent's window and the one it duly answered, stops reaching sub-agents at all — `spawnAgent` marks every child it spawns and `session-start` emits nothing when it sees the marker, an absent marker still being a human's session and behaving exactly as before |
320
+ | 0.13.0 | 2026-09-08 | `beta` | evidence a role expert can actually earn, and a review that leaves a record: `--mode full`'s runs pass mines `tldrx-work/**` while the domain gate judged every citation it produced against folders of code — measured at four role experts, **$9.47 and one evidence row**, and unfixable from the workspace because the single spelling the matcher would reach is the one `domainPaths()` drops — so the gate now treats the run record as in-domain for the file mined FROM it, scoped to the pass and never to the expert's `kind:` (a light file citing a handoff is still out of domain and still says so), two shipped role templates stop declaring `.tldrx/map/**` and `.tldrx/map/{repo}/gotchas.md` paths that matched nothing at all, and a pass that validated, spent money and earned zero rows now prints `the level did not move — $X.XX bought 0 evidence row(s)` with its reasons carried into `check.passed` instead of a silent ledger; `tldrx expert rescore` recovers what was already bought for $0 by re-reading `knowledge/*.md` under today's rules, dating rows by the knowledge file's own `trained_at` and never by the clock, with `rescored_at` additive beside `at` — its ABSENCE keeping the meaning every existing row had — and one `evidence.rescored` line per file it actually moved, so a free re-derivation can never be read as a paid turn; `scripts/merge-wave.sh` refuses a branch carrying no `.review/<branch>.md` with **exit 10**, its own code because `2` in that script is already "merge conflict", a stale record refusing rather than warning and staleness measured as "the code moved" rather than "the sha differs", since committing the record moves the head past exactly the sha it names; and the mutation check moves from the reviewer, whose allowance is `Read`/`Grep`/`Glob`/`Bash(git diff *)` and holds no pen, to the developer's contract that can run it, leaving the reviewer the read it can actually perform |
319
321
  | 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 |
320
322
  | 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 |
321
323
  | 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 |
@@ -29,7 +29,7 @@ import {
29
29
  validateRunBudget,
30
30
  wouldExceed,
31
31
  wouldExceedHostTokens
32
- } from "./chunk-zrjgpgwy.js";
32
+ } from "./chunk-fsgsn8vb.js";
33
33
  import {
34
34
  EventLog
35
35
  } from "./chunk-f1w9sy07.js";
@@ -595,12 +595,14 @@ function readEvidenceRows(input) {
595
595
  continue;
596
596
  }
597
597
  const confidence = str2(row.confidence);
598
+ const rescoredAt = str2(row.rescored_at);
598
599
  evidence.push({
599
600
  kind,
600
601
  src,
601
602
  at,
602
603
  ...row.cross === true ? { cross: true } : {},
603
- ...isEvidenceConfidence(confidence) ? { confidence } : {}
604
+ ...isEvidenceConfidence(confidence) ? { confidence } : {},
605
+ ...rescoredAt === "" ? {} : { rescored_at: rescoredAt }
604
606
  });
605
607
  }
606
608
  return { evidence, ignored: [...counts.values()] };
@@ -10,7 +10,7 @@ import {
10
10
  spentBasis,
11
11
  tallyOf,
12
12
  validateRunBudget
13
- } from "./chunk-zrjgpgwy.js";
13
+ } from "./chunk-fsgsn8vb.js";
14
14
  import {
15
15
  EventLog,
16
16
  asRunFile,
@@ -18,7 +18,7 @@ import {
18
18
  openQuestions,
19
19
  runSnapshot,
20
20
  whatIsWaiting
21
- } from "./chunk-jd48713j.js";
21
+ } from "./chunk-w9r9p155.js";
22
22
  import {
23
23
  expertsDir,
24
24
  loadExperts,
@@ -26,7 +26,7 @@ import {
26
26
  pathsIntersect,
27
27
  readExpertDomain,
28
28
  stackExpertNames
29
- } from "./chunk-zrjgpgwy.js";
29
+ } from "./chunk-fsgsn8vb.js";
30
30
  import"./chunk-f1w9sy07.js";
31
31
  import"./chunk-yvgkw0gm.js";
32
32
  import {
@@ -981,11 +981,19 @@ function sessionStartLines(status, max) {
981
981
  }
982
982
  return lines;
983
983
  }
984
+ // src/core/facilitator/subagent.ts
985
+ var SUBAGENT_ENV_VAR = "TLDRX_SUBAGENT";
986
+ function isSubagentEnv(env = process.env) {
987
+ return (env[SUBAGENT_ENV_VAR] ?? "") !== "";
988
+ }
989
+
984
990
  // src/hooks/session-start.ts
985
991
  var MAX_LINES = 3;
986
992
  var MAX_PENDING_LINES = 3;
987
993
  var MAX_OPEN_LISTED = 8;
988
994
  await runHook("session-start", async () => {
995
+ if (isSubagentEnv())
996
+ return;
989
997
  const payload = await readPayload();
990
998
  const root = findWorkspaceRoot(payload.cwd ?? process.cwd());
991
999
  if (root === null)
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  bar,
4
4
  runSnapshot
5
- } from "./chunk-jd48713j.js";
6
- import"./chunk-zrjgpgwy.js";
5
+ } from "./chunk-w9r9p155.js";
6
+ import"./chunk-fsgsn8vb.js";
7
7
  import"./chunk-f1w9sy07.js";
8
8
  import"./chunk-yvgkw0gm.js";
9
9
  import"./chunk-1knfmj6j.js";