tldr-experts 0.13.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,36 @@
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
+
4
34
  ## 0.13.0 — 2026-09-08
5
35
 
6
36
  ### Added
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.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 |
319
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 |
320
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 |
321
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 |
@@ -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)
package/dist/tldrx.js CHANGED
@@ -19707,6 +19707,12 @@ function millis(value) {
19707
19707
  return Number.isFinite(at) ? at : null;
19708
19708
  }
19709
19709
 
19710
+ // src/core/facilitator/subagent.ts
19711
+ var SUBAGENT_ENV_VAR = "TLDRX_SUBAGENT";
19712
+ function subagentEnv(base) {
19713
+ return { ...base ?? process.env, [SUBAGENT_ENV_VAR]: "1" };
19714
+ }
19715
+
19710
19716
  // src/core/facilitator/envelope.ts
19711
19717
  var ENVELOPE_SCHEMA = {
19712
19718
  type: "object",
@@ -19859,7 +19865,7 @@ async function spawnAgent(request) {
19859
19865
  stdin: request.prompt,
19860
19866
  timeoutMs: request.timeoutMs,
19861
19867
  ...cap > 0 ? { signal: controller.signal } : {},
19862
- env: request.env ?? { ...process.env },
19868
+ env: subagentEnv(request.env),
19863
19869
  onStdoutLine: (line) => {
19864
19870
  for (const event2 of stream.push(line)) {
19865
19871
  if (!capped && event2.kind === "tool-done" && (event2.countsAsRead === true || isReadTool(event2.name))) {
@@ -24503,6 +24509,24 @@ function isNotInlined(input) {
24503
24509
  return true;
24504
24510
  return input.totalBytes !== undefined && (input.inlinedBytes ?? 0) === 0;
24505
24511
  }
24512
+ var STAGE_PREAMBLE_MARKER = "You are the tldrx stage sub-agent";
24513
+ function renderStagePreamble(preamble) {
24514
+ const questions = preamble.outputs.find((path) => path.split("/").pop() === QUESTIONS_FILE2);
24515
+ const lines = [
24516
+ `${STAGE_PREAMBLE_MARKER} for stage \`${preamble.stage}\` of run \`${preamble.run}\`.`,
24517
+ "This prompt is the entire request: there is no other message to find, and no operator to reply to.",
24518
+ "",
24519
+ "Do this now:",
24520
+ "",
24521
+ "1. Fill in the template below — replace every `<…>` placeholder with real, sourced content, and keep the sections it declares.",
24522
+ "2. Write the result to the files this stage declares, all of them, at exactly these paths:",
24523
+ ...preamble.outputs.map((path) => ` - \`${path}\``),
24524
+ questions === undefined ? "3. Do not reply with a question. Record anything you cannot settle as an explicit unknown inside the outputs above." : `3. Do not reply with a question. Record anything you cannot settle in \`${questions}\`, in the shape the template gives.`
24525
+ ];
24526
+ return lines.join(`
24527
+ `);
24528
+ }
24529
+ var QUESTIONS_FILE2 = "questions.md";
24506
24530
  var INPUTS_HEADING = "Inputs";
24507
24531
  var PREVIOUS_ATTEMPT_HEADING = "Previous attempt";
24508
24532
  function buildPrompt(parts) {
@@ -24510,10 +24534,19 @@ function buildPrompt(parts) {
24510
24534
  }
24511
24535
  function renderParts(parts) {
24512
24536
  const substituted = cutSection(cutSection(cutSection(cutSection(substitute(parts.stageMd, parts.values), INPUTS_HEADING), DISPATCH_NOTES_HEADING), PROJECT_SKILLS_HEADING), PREVIOUS_ATTEMPT_HEADING);
24513
- const out = [
24514
- { kind: "stage", name: "stage.md", text: `${substituted.trimEnd()}
24515
- ` }
24516
- ];
24537
+ const out = [];
24538
+ if (parts.preamble !== undefined) {
24539
+ out.push({
24540
+ kind: "preamble",
24541
+ name: "preamble",
24542
+ text: `${renderStagePreamble(parts.preamble)}
24543
+
24544
+ ---
24545
+ `
24546
+ });
24547
+ }
24548
+ out.push({ kind: "stage", name: "stage.md", text: `${substituted.trimEnd()}
24549
+ ` });
24517
24550
  for (const expert of parts.experts) {
24518
24551
  out.push({
24519
24552
  kind: "expert-body",
@@ -30365,6 +30398,10 @@ function buildLedger(input) {
30365
30398
  for (const part of input.parts) {
30366
30399
  const bytes = byteLength(part.text);
30367
30400
  switch (part.kind) {
30401
+ case "preamble":
30402
+ stage2 += bytes;
30403
+ rows.push({ kind: part.kind, name: part.name, bytes });
30404
+ break;
30368
30405
  case "stage":
30369
30406
  stage2 += bytes;
30370
30407
  rows.push({ kind: part.kind, name: part.name, bytes });
@@ -30486,6 +30523,8 @@ function label(row2) {
30486
30523
  return `expert ${row2.name} body`;
30487
30524
  case "expert-knowledge":
30488
30525
  return `expert ${row2.name} knowledge`;
30526
+ case "preamble":
30527
+ return "stage preamble";
30489
30528
  case "dispatch-notes":
30490
30529
  return "dispatch notes";
30491
30530
  case "project-skills":
@@ -32128,6 +32167,11 @@ function featurePrompt(ctx, feature, diffs) {
32128
32167
  knowledgeBytes: ctx.spec.knowledgeMaxBytes
32129
32168
  });
32130
32169
  const body = buildPrompt({
32170
+ preamble: {
32171
+ stage: ctx.stageId,
32172
+ run: ctx.runId,
32173
+ outputs: [relative12(ctx.root, join78(ctx.runDir, watcherRelPath(feature.id)))]
32174
+ },
32131
32175
  stageMd,
32132
32176
  values: {
32133
32177
  run: ctx.runId,
@@ -33656,7 +33700,7 @@ function strings4(value) {
33656
33700
  var RUN_FILE = "run.yml";
33657
33701
  var BUDGET_FILE = "budget.yml";
33658
33702
  var EVENTS_FILE = "events.jsonl";
33659
- var QUESTIONS_FILE2 = "questions.md";
33703
+ var QUESTIONS_FILE3 = "questions.md";
33660
33704
  var HANDOFF_FILE2 = "handoff.md";
33661
33705
  function workDir(root2) {
33662
33706
  return join83(root2, PROJECT_WORK_DIR);
@@ -33751,7 +33795,7 @@ function loadPhaseArtefacts(loaded, phase) {
33751
33795
  const path = join83(loaded.dir, phase, name);
33752
33796
  return existsSync66(path) ? readFileSync60(path, "utf8") : null;
33753
33797
  };
33754
- return { phase, handoff: read2(HANDOFF_FILE2), questions: read2(QUESTIONS_FILE2) };
33798
+ return { phase, handoff: read2(HANDOFF_FILE2), questions: read2(QUESTIONS_FILE3) };
33755
33799
  }
33756
33800
  function loadGateEvidence(loaded, relPath) {
33757
33801
  if (relPath === "" || relPath.includes("..") || relPath.startsWith("/"))
@@ -38300,6 +38344,11 @@ function assemblePrompt(store, options, spec, stage2, inputs, ctx, seed) {
38300
38344
  const dispatchNotes = loadDispatchNotes(store.runDir, [stage2.id]);
38301
38345
  const absentInputs = absentDeclaredInputs(store, spec, ctx);
38302
38346
  const parts = renderParts({
38347
+ preamble: {
38348
+ stage: stage2.id,
38349
+ run: store.runId,
38350
+ outputs: expandAll(spec.planned.outputs, store.run.repos).map((declared2) => relative17(options.root, resolveDeclared(declared2, ctx)))
38351
+ },
38303
38352
  stageMd,
38304
38353
  absentInputs,
38305
38354
  dispatchNotes: dispatchNotes.body,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tldr-experts",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "tldr-experts: an evidence-first, file-based AI development framework - five stages, a gate on every one, and every claim cited or refused. Installs the `tldrx` (and `tldr-experts`) command. Beta.",
5
5
  "license": "MIT",
6
6
  "author": "Alan Martinez",
@@ -2,7 +2,7 @@
2
2
  "$doc": "Shape verified from https://code.claude.com/docs/en/plugins.md (Quickstart > Create the plugin manifest). Fields used here: name, description, version, author.name. Only plugin.json goes inside .claude-plugin/; skills/, agents/ and hooks/ live at the plugin root.",
3
3
  "name": "tldrx",
4
4
  "description": "tldr-experts: an evidence-first, file-based AI development framework. Five stages, a gate on every one, every claim cited or refused. Beta.",
5
- "version": "0.13.0",
5
+ "version": "0.13.1",
6
6
  "author": {
7
7
  "name": "Alan Martinez"
8
8
  }