tldr-experts 0.22.0 → 0.24.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/dist/tldrx.js CHANGED
@@ -16033,7 +16033,7 @@ var ENTRIES = [
16033
16033
  {
16034
16034
  name: "until-done",
16035
16035
  arg: "[<n>]",
16036
- meaning: "Relaunch the loop IN-PROCESS after an exit it can do nothing else with, at most n times — bare means 5, the cap. `--retry-failed` bounds one stage's exit 5s INSIDE the loop; this bounds the loop ENDING: a stage failure past that bound (5), a thrown error that used to reach the shell as a bare 1 with nothing on the ledger, a refusal whose remedy is the same command typed again (2). Measured on one 28 h unattended run: five such exits, each a person reading it and typing `tldrx run auto` again, 18 h before a story ran. Every relaunch is a `run.relaunched` event with the exit it recovered from, the attempt and the bound, and `run.finished` / `run.failed` go out ONCE, from the last attempt. What it NEVER relaunches over: exit 4 (a person's — `--wait-answers` and `--wait-gates` own it), a `budget.blocked` or this loop's own `--max-usd` (nothing in-process moves a ceiling, and `--max-usd` spans every relaunch rather than resetting), and an attempt whose last line is the previous attempt's verbatim — a refusal that repeats is not one a relaunch moves. `0` is the default and is one launch, exactly as before. Put the run id before the flag, or write `--until-done=3`.",
16036
+ meaning: "Relaunch the loop IN-PROCESS after an exit it can do nothing else with, at most n times — bare means 5, the cap. `--retry-failed` bounds one stage's exit 5s INSIDE the loop; this bounds the loop ENDING: a stage failure past that bound (5), a thrown error that used to reach the shell as a bare 1 with nothing on the ledger, a refusal whose remedy is the same command typed again (2). Measured on one 28 h unattended run: five such exits, each a person reading it and typing `tldrx run auto` again, 18 h before a story ran. Every relaunch is a `run.relaunched` event with the exit it recovered from, the attempt and the bound, and `run.finished` / `run.failed` go out ONCE, from the last attempt. What it NEVER relaunches over: exit 4 (a person's — `--wait-answers` and `--wait-gates` own it), a `budget.blocked` or this loop's own `--max-usd` (nothing in-process moves a ceiling, and `--max-usd` spans every relaunch rather than resetting), and an attempt REFUSED BY THE SAME WORDS as the one before it — a refusal that repeats is not one a relaunch moves. The words compared are the ones the stop NAMES, never the last line printed: every stage death ends with the same advice about cost, and comparing that stopped runs that were making progress (#297). `0` is the default and is one launch, exactly as before. Put the run id before the flag, or write `--until-done=3`.",
16037
16037
  sub: "auto"
16038
16038
  },
16039
16039
  {
@@ -16100,7 +16100,7 @@ var ENTRIES = [
16100
16100
  exits: [EXIT_OK, EXIT_USAGE, EXIT_GATE_REFUSED, EXIT_NOT_FOUND, EXIT_AWAITING_HUMAN, EXIT_AGENT_FAILED],
16101
16101
  notes: [
16102
16102
  "`run attend host` is a LOCK, not an engine. It sets one field, spends nothing, runs no stage and touches no branch — and from then on THE FRAMEWORK WILL NOT SPAWN on that run: every turn is a `tldrx next --prepare` / `tldrx next --commit` handshake with the session driving it, the Build reviewer included. `run attend --none` hands it back.",
16103
- "`run auto` is an ENGINE, not a lock. It calls `next` HEADLESS over and over, so THE FRAMEWORK spawns a metered sub-agent stage after stage, and it stops at the first thing it may not decide: a human gate or an open question (4), a stage failure (5) — unless `--retry-failed <n>` lets it run that stage again, bounded, up to n times in a row — a phase ceiling or this loop's own --max-usd (2). `--until-done [<n>]` puts a bounded relauncher OVER those stops: after a 5 past the retry bound, a throw, or a 2 with no money behind it, the same process runs the loop again and writes `run.relaunched` — never over a person's 4, never over a `budget.blocked`, never twice over the same last line. It is REFUSED ON AN ATTENDED RUN (exit 1, before the event log is opened) — a lock and an engine are alternatives, never layers.",
16103
+ "`run auto` is an ENGINE, not a lock. It calls `next` HEADLESS over and over, so THE FRAMEWORK spawns a metered sub-agent stage after stage, and it stops at the first thing it may not decide: a human gate or an open question (4), a stage failure (5) — unless `--retry-failed <n>` lets it run that stage again, bounded, up to n times in a row — a phase ceiling or this loop's own --max-usd (2). `--until-done [<n>]` puts a bounded relauncher OVER those stops: after a 5 past the retry bound, a throw, or a 2 with no money behind it, the same process runs the loop again and writes `run.relaunched` — never over a person's 4, never over a `budget.blocked`, never twice over the same refusal. It is REFUSED ON AN ATTENDED RUN (exit 1, before the event log is opened) — a lock and an engine are alternatives, never layers.",
16104
16104
  "`run auto` can also TELL SOMEBODY. When `.tldrx/workspace.yml` declares a `notify:` command (§2.18), the loop hands that command one `version: 1` JSON object on stdin at every moment a person is needed — an open question with its options and the literal `tldrx answer` line, a gate with the literal approve line, a finished or failed run with its exit code and family — plus a periodic `status` under `--notify-every`. The framework names no chat tool: the command is the owner's own, run as argv with no shell, and its exit code is recorded as `notify.sent` / `notify.failed` and NEVER changes the run's outcome.",
16105
16105
  "Under `run auto`, a stage whose `gates_policy` is `agent` gets one bounded GATE-SIGNER turn of its own. When the stage's checks have passed, the engine spawns a single sub-agent at the stage's model and effort, on a quarter of the stage's per-agent ceiling, allowed to read and to write exactly one file: `.agent/<stage>/evidence.md`. The note then goes through the UNCHANGED `approve --as-agent` path — the same validator a person's note goes through — so `verdict: sign` plus every condition holding closes the gate under the note's own `by:`, and anything else leaves it pending for a person with the reasons named. The turn is recorded like any other (`agent.spawned` / `agent.result`, `role: gate-signer`) and shows up in `tldrx cost`. There is no flag for it: `gates_policy: agent` is already the owner's recorded decision that an agent may close this gate, and `human` gates are never touched.",
16106
16106
  "`run auto` walks the LAST MILE when the run was opened with `--ship` (#253): the moment the run reads `done` under it, it runs `tldrx ship` — push, PR, and under `merge` the auto-merge — and `run.finished` carries `pr_url` and `merge`. A ship that is refused is the loop's exit code (2), because the loop was asked for a PR and did not deliver one; the run's own work is still `done` on disk. A run closed by a hand `tldrx approve` outside the loop is shipped by typing `tldrx ship`, which reads the same block.",
@@ -16449,7 +16449,7 @@ var ENTRIES = [
16449
16449
  {
16450
16450
  name: "as-is",
16451
16451
  arg: null,
16452
- meaning: "Settle the story from its BRANCH AS IT STANDS: the next Build turn runs the dod, the review and the merge over the commits already on `story/<run>/<id>`, and spawns NO developer. For a story a person finished by hand — a rebase after a conflict, a fix typed directly on the branch — which the plain verb cannot land, because a developer handed finished work has nothing to do and the story blocks on the no-work rule. It is not a shortcut past a gate: a red dod blocks the story, and so does a branch carrying no commit its epic has not already got. Requires --note; refused together with --for-fix.",
16452
+ meaning: "Settle the story from its BRANCH AS IT STANDS: the next Build turn runs the dod, the review and the merge over the commits already on `story/<run>/<id>`, and spawns NO developer. For a story a person finished by hand — a rebase after a conflict, a fix typed directly on the branch — which the plain verb cannot land, because a developer handed finished work has nothing to do and the story blocks on the no-work rule. It is not a shortcut past a gate: a red dod blocks the story, and so does a branch carrying no commit its epic has not already got — with one named exception: a story whose work is ALREADY on the epic from an earlier turn and whose review never completed (the reviewer died, or the attempt after the merge died with no work) is settled review-only: nothing is merged, the dod runs on the epic head and the reviewer judges the range the story was merged as; a review that stands (`approve`, `changes`, `fixlist`) still refuses. Requires --note; refused together with --for-fix.",
16453
16453
  sub: "reopen"
16454
16454
  },
16455
16455
  runFlag(),
@@ -20652,10 +20652,17 @@ function describe4(exitCode, doc, stderr, timedOut, stdout, provider) {
20652
20652
  return `${name} exited ${exitCode} without a parseable result event: ${tail}`;
20653
20653
  }
20654
20654
  const errors2 = Array.isArray(doc.errors) ? doc.errors.filter((e) => typeof e === "string") : [];
20655
- const reason = errors2[0] ?? (typeof doc.subtype === "string" ? doc.subtype : "") ?? "";
20656
- const suffix = reason === "" ? "" : `: ${String(reason)}`;
20657
- return `${name} exited ${exitCode} with is_error=${String(doc.is_error === true)}${suffix}`;
20658
- }
20655
+ const verdict = `${name} exited ${exitCode} with is_error=${String(doc.is_error === true)}`;
20656
+ const named = typeof errors2[0] === "string" && errors2[0] !== "" ? errors2[0] : "";
20657
+ if (named !== "")
20658
+ return `${verdict}: ${named}`;
20659
+ const subtype = typeof doc.subtype === "string" ? doc.subtype : "";
20660
+ if (subtype !== "" && !SUCCESS_SUBTYPES.has(subtype))
20661
+ return `${verdict}: ${subtype}`;
20662
+ const contradiction = subtype === "" ? "" : ` (the provider's own subtype said "${subtype}")`;
20663
+ return `${verdict}: no reason named${contradiction}`;
20664
+ }
20665
+ var SUCCESS_SUBTYPES = new Set(["success"]);
20659
20666
  function firstLine3(text2) {
20660
20667
  const line = text2.split(`
20661
20668
  `).map((l) => l.trim()).find((l) => l !== "");
@@ -24906,7 +24913,7 @@ function effortFlag(args, name = "effort") {
24906
24913
  }
24907
24914
 
24908
24915
  // src/core/facilitator/runAuto.ts
24909
- import { readFileSync as readFileSync76 } from "node:fs";
24916
+ import { readFileSync as readFileSync77 } from "node:fs";
24910
24917
  import { join as join102 } from "node:path";
24911
24918
 
24912
24919
  // src/core/build/implicitPlan.ts
@@ -25937,6 +25944,7 @@ function carriedFindings(findings2) {
25937
25944
  return findings2.filter((f) => f.disposition === "defer-with-log" && !(f.resolved && f.resolvedSha !== null));
25938
25945
  }
25939
25946
  var CLAIMED_UNVERIFIED = "claimed-unverified";
25947
+ var FIXLIST_SETTLED_MARK = "every finding is routed away from `fix-now`, so the story is done without a fix round";
25940
25948
  function parseFixFindings(value) {
25941
25949
  const problems = [];
25942
25950
  const format = [];
@@ -26452,8 +26460,16 @@ function reviewStillOwed(outcome) {
26452
26460
  }
26453
26461
  var AS_IS_MARK = "the branch was taken AS IT STANDS — no developer was spawned for it";
26454
26462
  var AS_IS_NOT_AHEAD_MARK = "nothing to take as it stands";
26455
- function asIsNotAheadReason(branch, epicBranch, ahead) {
26456
- return ahead === null ? `${AS_IS_NOT_AHEAD_MARK}: git could not count what \`${branch}\` carries beyond ` + `\`${epicBranch}\`, and a settlement that spawns no developer will not merge on a ` + "measurement it does not have. Nothing was merged" : `${AS_IS_NOT_AHEAD_MARK}: \`${branch}\` carries no commit \`${epicBranch}\` has not already ` + "got, so there is no work to settle. Commit the fix to the story branch first, then reopen it " + "--as-is. Nothing was merged";
26463
+ var AS_IS_JUDGED_MARK = "the last review of that work stands";
26464
+ var AS_IS_REVIEW_ONLY_MARK = "the review was run AS IT STANDS — nothing was merged and no developer was spawned";
26465
+ function asIsNotAheadReason(branch, epicBranch, ahead, judged) {
26466
+ if (ahead === null) {
26467
+ return `${AS_IS_NOT_AHEAD_MARK}: git could not count what \`${branch}\` carries beyond ` + `\`${epicBranch}\`, and a settlement that spawns no developer will not merge on a ` + "measurement it does not have. Nothing was merged";
26468
+ }
26469
+ return `${AS_IS_NOT_AHEAD_MARK}: \`${branch}\` carries no commit \`${epicBranch}\` has not already ` + "got, so there is no work to settle" + (judged === undefined ? "" : `, and ${AS_IS_JUDGED_MARK} — \`${judged.commit.slice(0, 7)}\` is on \`${epicBranch}\` and its ` + `review said \`${judged.verdict}\`, so the review is not what is owed`) + ". Commit the fix to the story branch first, then reopen it --as-is. Nothing was merged";
26470
+ }
26471
+ function reviewNeverCompleted(verdict) {
26472
+ return verdict === "n-a" || verdict === "error";
26457
26473
  }
26458
26474
  function scopedNote(result2) {
26459
26475
  if (result2.scope !== "paths" || result2.rendered === undefined)
@@ -28997,6 +29013,7 @@ function dodLines(r) {
28997
29013
  ` \`${r.outputPath}\` [src: ${r.outputPath}:${String(r.outputLine ?? 1)}]. Read that before re-running anything.`
28998
29014
  ];
28999
29015
  }
29016
+ var WHY_NOT_DONE_HEADING = "## Why it is not done";
29000
29017
  function renderReviewLog(outcome) {
29001
29018
  const lines = [
29002
29019
  `# Review — ${outcome.id} · ${outcome.title}`,
@@ -29008,7 +29025,7 @@ function renderReviewLog(outcome) {
29008
29025
  `- Repo: \`${outcome.repo}\` · wave ${outcome.wave} · epic ${outcome.epic}`,
29009
29026
  `- Branch: \`${outcome.branch}\` → \`${outcome.epicBranch}\` (${mergeWord(outcome)})`,
29010
29027
  `- Commit: ${outcome.commit ?? "(none)"}`,
29011
- ...outcome.asIs == null ? [] : [`- Developer: **none** — ${AS_IS_MARK}. \`${outcome.branch}\` was settled as it stands, ` + `signed by ${outcome.asIs.actor}: ${outcome.asIs.note}. The Definition of Done below and ` + "the reviewer above judged that branch, unchanged."],
29028
+ ...outcome.asIs == null ? [] : outcome.asIs.reason === "review-only" ? [`- Developer: **none** — ${AS_IS_REVIEW_ONLY_MARK}. \`${outcome.asIs.reviewed?.commit ?? outcome.commit ?? "(none)"}\` ` + `was already on \`${outcome.epicBranch}\` from an earlier turn and nothing had judged it; ` + `signed by ${outcome.asIs.actor}: ${outcome.asIs.note}. The Definition of Done below ran on the ` + "epic head, and the reviewer above judged the range that merge was recorded as."] : [`- Developer: **none** — ${AS_IS_MARK}. \`${outcome.branch}\` was settled as it stands, ` + `signed by ${outcome.asIs.actor}: ${outcome.asIs.note}. The Definition of Done below and ` + "the reviewer above judged that branch, unchanged."],
29012
29029
  ...outcome.developerError === null ? [] : [`- Developer: **FAILED** — ${outcome.developerError}`],
29013
29030
  ...outcome.permissionRefused == null ? [] : [withCure(`- Developer: \`${outcome.permissionRefused}\` was refused for approval by the agent's own ` + "permission layer; the tree held committed work, so the Definition of Done below decided", outcome.permissionRefused, outcome.declaredCommands)],
29014
29031
  ...outcome.budgetDeath == null ? [] : [`- Developer: died on its per-story cap — ${outcome.budgetDeath}; the tree held work, ` + "so the Definition of Done below decided"],
@@ -29033,7 +29050,7 @@ function renderReviewLog(outcome) {
29033
29050
  lines.push("## Uncommitted work rescued", "", ...rescueLines(outcome), "");
29034
29051
  }
29035
29052
  if (outcome.reason !== null) {
29036
- lines.push("## Why it is not done", "", outcome.reason, "");
29053
+ lines.push(WHY_NOT_DONE_HEADING, "", outcome.reason, "");
29037
29054
  }
29038
29055
  return lines.join(`
29039
29056
  `);
@@ -29369,6 +29386,161 @@ function round22(n) {
29369
29386
  return Math.round(n * 100) / 100;
29370
29387
  }
29371
29388
 
29389
+ // src/core/build/caps.ts
29390
+ var MAX_ATTEMPTS2 = STAGE_TUNING_DEFAULTS.attempts;
29391
+ var REVIEWER_SHARE2 = STAGE_TUNING_DEFAULTS.reviewerShare;
29392
+ var REVIEWER_FLOOR_USD2 = 1;
29393
+ var STORY_CAP_MULTIPLIER2 = STAGE_TUNING_DEFAULTS.storyCapMultiplier;
29394
+ var STORY_CAP_FLOOR_USD2 = STAGE_TUNING_DEFAULTS.storyCapFloorUsd;
29395
+ function developerAttemptDivisor2(attempt, attempts = MAX_ATTEMPTS2) {
29396
+ return attempt <= 1 ? 1 : attempts;
29397
+ }
29398
+ var DEFAULT_PARALLEL = 1;
29399
+ function clampParallel(requested) {
29400
+ if (requested === undefined || !Number.isFinite(requested))
29401
+ return DEFAULT_PARALLEL;
29402
+ return Math.max(1, Math.min(MAX_STORIES_PER_WAVE, Math.trunc(requested)));
29403
+ }
29404
+ function attemptsOf(parts) {
29405
+ return parts.attempts ?? MAX_ATTEMPTS2;
29406
+ }
29407
+ function storyCapMultiplierOf(parts) {
29408
+ return parts.storyCapMultiplier ?? STORY_CAP_MULTIPLIER2;
29409
+ }
29410
+ function storyCapFloorOf(parts) {
29411
+ return parts.storyCapFloorUsd ?? STORY_CAP_FLOOR_USD2;
29412
+ }
29413
+ function reviewerShareOf(parts) {
29414
+ return parts.reviewerShare ?? REVIEWER_SHARE2;
29415
+ }
29416
+ function round23(n) {
29417
+ return Math.round(n * 100) / 100;
29418
+ }
29419
+ function developerCap(parts, storyId, attempt = 1) {
29420
+ const price = priceOf(parts, storyId);
29421
+ if (price === null)
29422
+ return parts.agentCap(1 / worstCaseShares(parts));
29423
+ return parts.agentCap(shareOf(parts, storyCeilingUsd(parts, price) / developerAttemptDivisor2(attempt, attemptsOf(parts))));
29424
+ }
29425
+ function storyCeilingUsd(parts, price) {
29426
+ const floor = parts.budgetUsd > 0 ? Math.min(storyCapFloorOf(parts), parts.budgetUsd) : storyCapFloorOf(parts);
29427
+ return Math.max(price * storyCapMultiplierOf(parts), floor);
29428
+ }
29429
+ function reviewerCap(parts, spentUsd, storyId) {
29430
+ const price = priceOf(parts, storyId);
29431
+ const share = reviewerShareOf(parts);
29432
+ const derived = price === null ? parts.agentCap(share / worstCaseShares(parts)) : parts.agentCap(shareOf(parts, price * share / (attemptsOf(parts) * (1 + share))));
29433
+ const floor = Math.min(REVIEWER_FLOOR_USD2, Math.max(parts.budgetUsd - spentUsd, 0));
29434
+ return round23(Math.min(Math.max(derived, floor), parts.maxBudgetUsd));
29435
+ }
29436
+ function stageRemainderUsd(parts, spentUsd) {
29437
+ if (!(parts.budgetUsd > 0))
29438
+ return null;
29439
+ return Math.max(round23(parts.budgetUsd - spentUsd), 0);
29440
+ }
29441
+ function reviewerUnderfunded(parts, spentUsd) {
29442
+ const left = stageRemainderUsd(parts, spentUsd);
29443
+ return left !== null && left < REVIEWER_FLOOR_USD2;
29444
+ }
29445
+ function priceOf(parts, storyId) {
29446
+ if (storyId === undefined)
29447
+ return null;
29448
+ const price = parts.prices.get(storyId);
29449
+ if (price === undefined || !Number.isFinite(price) || price <= 0)
29450
+ return null;
29451
+ return price * priceScale(parts);
29452
+ }
29453
+ function priceScale(parts) {
29454
+ if (parts.budgetUsd <= 0)
29455
+ return 1;
29456
+ const sum = plannedSumUsd(parts);
29457
+ return sum <= parts.budgetUsd ? 1 : parts.budgetUsd / sum;
29458
+ }
29459
+ function plannedSumUsd(parts) {
29460
+ let sum = 0;
29461
+ for (const price of parts.prices.values()) {
29462
+ if (Number.isFinite(price) && price > 0)
29463
+ sum += price;
29464
+ }
29465
+ return sum;
29466
+ }
29467
+ function shareOf(parts, usd) {
29468
+ return parts.budgetUsd <= 0 ? 1 : usd / parts.budgetUsd;
29469
+ }
29470
+ function worstCaseShares(parts) {
29471
+ return Math.max(parts.storyCount, 1) * attemptsOf(parts) * (1 + reviewerShareOf(parts));
29472
+ }
29473
+ function storyCapDerivation(parts, storyId, attempt = 1) {
29474
+ const scaled = priceOf(parts, storyId);
29475
+ if (scaled === null)
29476
+ return null;
29477
+ const planPriceUsd = parts.prices.get(storyId);
29478
+ const floorUsd = parts.budgetUsd > 0 ? Math.min(storyCapFloorOf(parts), parts.budgetUsd) : storyCapFloorOf(parts);
29479
+ return {
29480
+ storyId,
29481
+ attempt,
29482
+ planPriceUsd,
29483
+ plannedSumUsd: plannedSumUsd(parts),
29484
+ stageBudgetUsd: parts.budgetUsd,
29485
+ scale: priceScale(parts),
29486
+ scaledPriceUsd: scaled,
29487
+ multiplier: storyCapMultiplierOf(parts),
29488
+ floorUsd,
29489
+ ceilingUsd: storyCeilingUsd(parts, scaled),
29490
+ divisor: developerAttemptDivisor2(attempt, attemptsOf(parts)),
29491
+ capUsd: developerCap(parts, storyId, attempt)
29492
+ };
29493
+ }
29494
+ function usd(n) {
29495
+ return `$${n.toFixed(2)}`;
29496
+ }
29497
+ function scaleOf(scale) {
29498
+ return scale === 1 ? "1" : scale.toFixed(4);
29499
+ }
29500
+ function describeStoryCap(d) {
29501
+ const multiplied = d.scaledPriceUsd * d.multiplier;
29502
+ const scaleNote = d.scale === 1 ? ` (stage scale 1: ${usd(d.plannedSumUsd)} of plan prices fit the stage's ${usd(d.stageBudgetUsd)})` : ` (stage budget_usd ${usd(d.stageBudgetUsd)} over ${usd(d.plannedSumUsd)} of plan prices)`;
29503
+ const scaleTerm = d.scale === 1 ? "" : ` × stage scale ${scaleOf(d.scale)}${scaleNote}`;
29504
+ const ceiling = multiplied >= d.floorUsd ? `plan price ${usd(d.planPriceUsd)}${scaleTerm} × story_cap_multiplier ${String(d.multiplier)}` + ` = ${usd(multiplied)}${d.scale === 1 ? scaleNote : ""}, above the floor ${usd(d.floorUsd)}` : `story_cap_floor_usd ${usd(d.floorUsd)} (plan price ${usd(d.planPriceUsd)} × stage scale ${scaleOf(d.scale)}` + ` × story_cap_multiplier ${String(d.multiplier)} = ${usd(multiplied)} is below it)`;
29505
+ const perAttempt = round23(d.ceilingUsd / d.divisor);
29506
+ const contingency = d.divisor === 1 ? "" : `; attempt ${String(d.attempt)} is the contingency and gets that ÷ ${String(d.divisor)} = ${usd(perAttempt)}`;
29507
+ const clamp = d.capUsd < perAttempt ? `; clamped to ${usd(d.capUsd)} by per_agent_max_usd / --max-usd` : "";
29508
+ return `cap ${usd(d.capUsd)} = ${ceiling}${contingency}${clamp}`;
29509
+ }
29510
+ function capDeathReason(error, parts, storyId, attempt, lever) {
29511
+ const head2 = `the developer died on its per-story cap — ${error}: `;
29512
+ const d = storyCapDerivation(parts, storyId, attempt);
29513
+ if (d === null) {
29514
+ const cap2 = developerCap(parts, storyId, attempt);
29515
+ return head2 + `this story has no price in \`03-plan/budget.yml\`, so its cap ${usd(cap2)} is the uniform share of the ` + `stage's budget_usd ${usd(parts.budgetUsd)} over ${String(Math.max(parts.storyCount, 1))} story(ies)` + ` × ${String(attemptsOf(parts))} attempt(s) × (1 + reviewer share ${String(reviewerShareOf(parts))})` + ` — the stage's own \`budget_usd\` is what moves it (\`${lever.raiseCommand(parts.budgetUsd)}\` doubles it)`;
29516
+ }
29517
+ const formula = describeStoryCap(d);
29518
+ if (d.scale === 1) {
29519
+ return head2 + formula + "; a story that really costs more than the plan guessed wants a higher price in `03-plan/budget.yml` " + "or a higher `story_cap_multiplier:` on the stage";
29520
+ }
29521
+ const short2 = shortBy(d.plannedSumUsd, d.stageBudgetUsd);
29522
+ return head2 + formula + "; the plan's price cannot move this cap while the plan is scaled — raising every price keeps the " + "ratio and the sum still exceeds the stage — so raise the stage's own `budget_usd` " + `(\`${lever.raiseCommand(short2)}\` lifts the scale to 1) or \`story_cap_multiplier:\` on the stage`;
29523
+ }
29524
+ function planOverStageAdvisory(parts, lever) {
29525
+ const scale = priceScale(parts);
29526
+ if (scale >= 1)
29527
+ return null;
29528
+ const sum = plannedSumUsd(parts);
29529
+ const factor = (sum / parts.budgetUsd).toFixed(1);
29530
+ let largest = null;
29531
+ for (const [id, price] of parts.prices) {
29532
+ if (Number.isFinite(price) && price > 0 && (largest === null || price > (parts.prices.get(largest) ?? 0))) {
29533
+ largest = id;
29534
+ }
29535
+ }
29536
+ const example = largest === null ? null : storyCapDerivation(parts, largest);
29537
+ const worked = example === null ? "" : `: ${example.storyId} priced ${usd(example.planPriceUsd)} gets max(${usd(example.planPriceUsd)} × ${scaleOf(scale)}` + ` × ${String(example.multiplier)}, ${usd(example.floorUsd)}) = ${usd(example.capUsd)}`;
29538
+ return `advisory: 03-plan/budget.yml prices ${usd(sum)} of stories into a stage whose budget_usd is ` + `${usd(parts.budgetUsd)} — ${factor}× what the stage holds — so every per-story cap is derived from ` + `the plan price × ${scaleOf(scale)}, not the price as written${worked}. Raising the prices cannot lift a ` + `cap while their sum exceeds the stage; \`${lever.raiseCommand(shortBy(sum, parts.budgetUsd))}\` ` + "makes the stage hold the plan as priced, or raise `story_cap_multiplier:` on the stage";
29539
+ }
29540
+ function shortBy(estimate, remaining2) {
29541
+ return Math.max(0.01, Math.ceil((estimate - remaining2) * 100) / 100);
29542
+ }
29543
+
29372
29544
  // src/core/budget/budgetView.ts
29373
29545
  function buildBudgetView(run, budget, runDir) {
29374
29546
  const tally = runTally(run);
@@ -29428,8 +29600,8 @@ function grantLine(view) {
29428
29600
  if (by === null)
29429
29601
  return [];
29430
29602
  const scopes = [
29431
- ...view.authorized_usd === null ? [] : [`the run ${usd(view.authorized_usd)}`],
29432
- ...view.phases.filter((phase) => phase.authorized_usd !== null).map((phase) => `${phase.id} ${usd(phase.authorized_usd ?? 0)}`)
29603
+ ...view.authorized_usd === null ? [] : [`the run ${usd2(view.authorized_usd)}`],
29604
+ ...view.phases.filter((phase) => phase.authorized_usd !== null).map((phase) => `${phase.id} ${usd2(phase.authorized_usd ?? 0)}`)
29433
29605
  ];
29434
29606
  if (scopes.length === 0)
29435
29607
  return [];
@@ -29447,9 +29619,6 @@ function raiseCommand(runId, phaseId, amountUsd) {
29447
29619
  function stageRaiseCommand(runId, phaseId, stageId, amountUsd) {
29448
29620
  return `${raiseCommand(runId, phaseId, amountUsd)} --stage ${stageId}`;
29449
29621
  }
29450
- function shortBy(estimate, remaining2) {
29451
- return Math.max(0.01, Math.ceil((estimate - remaining2) * 100) / 100);
29452
- }
29453
29622
  function nextStageOf(stages) {
29454
29623
  return stages.find((stage2) => !isTerminal(stage2.status)) ?? null;
29455
29624
  }
@@ -29458,29 +29627,29 @@ function renderBudget(view) {
29458
29627
  const stageWidth = Math.max(...view.phases.map((p) => (p.next_stage ?? "—").length), 10);
29459
29628
  const lines = [
29460
29629
  `${view.run} · ${view.title}`,
29461
- `ceiling ${usd(view.ceiling_usd)} · spent ${spentFigure({
29630
+ `ceiling ${usd2(view.ceiling_usd)} · spent ${spentFigure({
29462
29631
  usd: view.spent_usd,
29463
29632
  unmetered: view.unmetered_tasks,
29464
29633
  metered: view.metered_tasks
29465
- })}` + ` · left ${usd(view.remaining_usd)} · ` + `per-agent max ${usd(view.per_agent_max_usd)} · on_exceed ${view.on_exceed}`,
29634
+ })}` + ` · left ${usd2(view.remaining_usd)} · ` + `per-agent max ${usd2(view.per_agent_max_usd)} · on_exceed ${view.on_exceed}`,
29466
29635
  ...view.unmetered_tasks === 0 ? [] : [unmeteredNote(view.unmetered_tasks)],
29467
29636
  ...grantLine(view),
29468
29637
  "",
29469
29638
  ` ${"phase".padEnd(width)} ${pad2("ceiling")} ${pad2("spent")} ${pad2("left")} ` + `${"next stage".padEnd(stageWidth)} ${pad2("est.")} next`
29470
29639
  ];
29471
29640
  for (const phase of view.phases) {
29472
- lines.push(`${phase.is_cursor ? ">" : " "} ${phase.id.padEnd(width)} ${pad2(usd(phase.ceiling_usd))} ` + `${pad2(usd(phase.spent_usd))} ${pad2(usd(phase.remaining_usd))} ` + `${(phase.next_stage ?? "—").padEnd(stageWidth)} ` + `${pad2(phase.next_stage === null ? "—" : usd(phase.next_estimate_usd))} ` + `${phase.next_stage === null ? "—" : phase.blocked ? "BLOCKED" : "ok"}`);
29641
+ lines.push(`${phase.is_cursor ? ">" : " "} ${phase.id.padEnd(width)} ${pad2(usd2(phase.ceiling_usd))} ` + `${pad2(usd2(phase.spent_usd))} ${pad2(usd2(phase.remaining_usd))} ` + `${(phase.next_stage ?? "—").padEnd(stageWidth)} ` + `${pad2(phase.next_stage === null ? "—" : usd2(phase.next_estimate_usd))} ` + `${phase.next_stage === null ? "—" : phase.blocked ? "BLOCKED" : "ok"}`);
29473
29642
  }
29474
29643
  for (const phase of view.phases) {
29475
29644
  if (phase.next_estimate_detail !== null) {
29476
- lines.push(` ${phase.id}: ${phase.next_estimate_detail} ` + `(stage estimate ${usd(phase.next_estimate_static_usd)})`);
29645
+ lines.push(` ${phase.id}: ${phase.next_estimate_detail} ` + `(stage estimate ${usd2(phase.next_estimate_static_usd)})`);
29477
29646
  }
29478
29647
  }
29479
29648
  const blocked = view.blocked;
29480
29649
  if (blocked === null) {
29481
29650
  lines.push("", "`tldrx next` is affordable in every phase that still has a stage to run.");
29482
29651
  } else {
29483
- lines.push("", `\`tldrx next\` is BLOCKED: phase ${blocked.id} has ${usd(blocked.remaining_usd)} left and stage ` + `\`${blocked.next_stage ?? "?"}\` estimates ${usd(blocked.next_estimate_usd)} ` + `(short by ${usd(blocked.short_by_usd)}).`, `Fix it with: ${view.fix_command ?? ""}`, `Or move the money instead of adding it: ${view.fix_command ?? ""} --take-from <phase>`);
29652
+ lines.push("", `\`tldrx next\` is BLOCKED: phase ${blocked.id} has ${usd2(blocked.remaining_usd)} left and stage ` + `\`${blocked.next_stage ?? "?"}\` estimates ${usd2(blocked.next_estimate_usd)} ` + `(short by ${usd2(blocked.short_by_usd)}).`, `Fix it with: ${view.fix_command ?? ""}`, `Or move the money instead of adding it: ${view.fix_command ?? ""} --take-from <phase>`);
29484
29653
  }
29485
29654
  return lines.join(`
29486
29655
  `);
@@ -29488,7 +29657,7 @@ function renderBudget(view) {
29488
29657
  function pad2(text3) {
29489
29658
  return text3.padStart(9);
29490
29659
  }
29491
- function usd(amount) {
29660
+ function usd2(amount) {
29492
29661
  return `$${amount.toFixed(2)}`;
29493
29662
  }
29494
29663
  function round7(n) {
@@ -29598,6 +29767,26 @@ import { join as join61 } from "node:path";
29598
29767
 
29599
29768
  // src/core/build/handoff.ts
29600
29769
  var MAX_CARRIED_BULLETS = 25;
29770
+ var LINE_BREAK_MARK = "⏎";
29771
+ var LINE_BREAK_NOTE = `\`${LINE_BREAK_MARK}\` marks a line break inside a quoted command; the review log beside this file ` + "keeps the command verbatim.";
29772
+ function ensureLineBreakNote(document) {
29773
+ if (!document.includes(LINE_BREAK_MARK) || document.includes(LINE_BREAK_NOTE))
29774
+ return document;
29775
+ const lines = document.split(`
29776
+ `);
29777
+ const at = lines.findIndex((line) => line.startsWith("## "));
29778
+ if (at === -1)
29779
+ return `${document.trimEnd()}
29780
+
29781
+ ${LINE_BREAK_NOTE}
29782
+ `;
29783
+ lines.splice(at, 0, LINE_BREAK_NOTE, "");
29784
+ return lines.join(`
29785
+ `);
29786
+ }
29787
+ function asOneLine(line) {
29788
+ return line.replace(/[ \t]*\r?\n[ \t]*/g, ` ${LINE_BREAK_MARK} `);
29789
+ }
29601
29790
  function renderBuildHandoff(parts) {
29602
29791
  const done = parts.outcomes.filter((o) => o.status === "done");
29603
29792
  const notDone = parts.outcomes.filter((o) => o.status !== "done");
@@ -29648,8 +29837,8 @@ function renderBuildHandoff(parts) {
29648
29837
  ...parts.epics.length === 0 ? ["- (no epic branch was written)"] : gateRows(parts.epics).map((e) => `- \`${e.branch}\` in ${e.repos.join(", ")} → \`${e.defaultBranches.join(", ")}\` ` + `(${mergeSummary(e)})`),
29649
29838
  ""
29650
29839
  ];
29651
- return lines.join(`
29652
- `);
29840
+ return ensureLineBreakNote(lines.map(asOneLine).join(`
29841
+ `));
29653
29842
  }
29654
29843
  function gateRows(epics) {
29655
29844
  const byBranch = new Map;
@@ -29913,12 +30102,412 @@ function statusWithOutcome(status, outcome) {
29913
30102
  }
29914
30103
 
29915
30104
  // src/core/run/autoGate.ts
29916
- import { existsSync as existsSync45, readFileSync as readFileSync40 } from "node:fs";
30105
+ import { existsSync as existsSync46, readFileSync as readFileSync42 } from "node:fs";
29917
30106
  import { join as join63 } from "node:path";
29918
30107
 
29919
30108
  // src/core/run/checks.ts
29920
- import { existsSync as existsSync44, readFileSync as readFileSync39 } from "node:fs";
30109
+ import { existsSync as existsSync45, readFileSync as readFileSync41 } from "node:fs";
29921
30110
  import { basename as basename10, join as join62 } from "node:path";
30111
+
30112
+ // src/core/facilitator/stageSpec.ts
30113
+ import { existsSync as existsSync44, readFileSync as readFileSync40 } from "node:fs";
30114
+
30115
+ // src/core/facilitator/seedInputs.ts
30116
+ import { readFileSync as readFileSync39, statSync as statSync16 } from "node:fs";
30117
+ var DEFAULT_INPUTS_MAX_BYTES = 256 * 1024;
30118
+ var MAX_SEED_INLINE_BYTES = 64 * 1024;
30119
+ var MIN_SLICE_BYTES = 2 * 1024;
30120
+ function inlineInputs(declared, options) {
30121
+ const budget = options.budgetBytes ?? DEFAULT_INPUTS_MAX_BYTES;
30122
+ const exempt = options.exempt ?? new Set;
30123
+ const inputs = [];
30124
+ const truncated = [];
30125
+ let spent = 0;
30126
+ let seedOverflowed = false;
30127
+ for (const path of declared) {
30128
+ const abs = resolveDeclared(path, options.ctx);
30129
+ if (exempt.has(path)) {
30130
+ inputs.push({ path, content: readOrEmpty3(abs) });
30131
+ continue;
30132
+ }
30133
+ const size = sizeOf4(abs);
30134
+ const room = budget - spent;
30135
+ if (size <= room) {
30136
+ spent += size;
30137
+ inputs.push({ path, content: readOrEmpty3(abs) });
30138
+ continue;
30139
+ }
30140
+ if (options.seed.has(path))
30141
+ seedOverflowed = true;
30142
+ if (room >= MIN_SLICE_BYTES) {
30143
+ spent += room;
30144
+ truncated.push({ path, totalBytes: size, inlinedBytes: room });
30145
+ inputs.push({ path, content: slice(abs, room), inlinedBytes: room, totalBytes: size });
30146
+ continue;
30147
+ }
30148
+ truncated.push({ path, totalBytes: size, inlinedBytes: 0 });
30149
+ inputs.push({ path, content: "", inlinedBytes: 0, totalBytes: size });
30150
+ }
30151
+ return {
30152
+ inputs,
30153
+ note: describe5(budget, truncated, seedOverflowed),
30154
+ truncated,
30155
+ inlinedBytes: spent,
30156
+ budgetBytes: budget
30157
+ };
30158
+ }
30159
+ function describe5(budget, truncated, seedOverflowed) {
30160
+ if (truncated.length === 0)
30161
+ return null;
30162
+ const listed = truncated.map((entry) => `${entry.path} (${entry.totalBytes.toLocaleString("en-US")} B` + `${entry.inlinedBytes === 0 ? ", none inlined" : `, first ${entry.inlinedBytes.toLocaleString("en-US")} B only`})`).join("; ");
30163
+ const fix = seedOverflowed ? "raise `inputs_max_bytes` in the stage file or split the seed (`tldrx seed triage`)" : "raise `inputs_max_bytes` in the stage file";
30164
+ return `truncated inputs: ${listed} — the ${budget.toLocaleString("en-US")}-byte ` + `\`inputs_max_bytes\` budget ran out. ${fix}. ` + "Work from what IS inlined and say in your handoff what you could not read.";
30165
+ }
30166
+ function describeTruncatedInputs(result2) {
30167
+ return result2.truncated.map((entry) => `truncated input: ${entry.path} (${entry.totalBytes.toLocaleString("en-US")} B` + `${entry.inlinedBytes === 0 ? ", none inlined" : `, ${entry.inlinedBytes.toLocaleString("en-US")} B inlined`})` + ` — raise inputs_max_bytes (now ${result2.budgetBytes.toLocaleString("en-US")}) or split the seed`);
30168
+ }
30169
+ function capInputs(declared, max = MAX_STAGE_INPUTS) {
30170
+ return declared.length <= max ? declared : declared.slice(0, max);
30171
+ }
30172
+ function sizeOf4(abs) {
30173
+ try {
30174
+ return statSync16(abs).size;
30175
+ } catch {
30176
+ return 0;
30177
+ }
30178
+ }
30179
+ function slice(abs, bytes) {
30180
+ try {
30181
+ return readFileSync39(abs).subarray(0, bytes).toString("utf8");
30182
+ } catch {
30183
+ return "";
30184
+ }
30185
+ }
30186
+ function readOrEmpty3(abs) {
30187
+ try {
30188
+ return readFileSync39(abs, "utf8");
30189
+ } catch {
30190
+ return "";
30191
+ }
30192
+ }
30193
+
30194
+ // src/core/budget/modelPrices.ts
30195
+ var CACHE_WRITE_MULTIPLIER = 1.25;
30196
+ var CACHE_READ_MULTIPLIER = 0.1;
30197
+ var BYTES_PER_TOKEN = 3.6;
30198
+ var MODEL_PRICES = [
30199
+ { id: "opus", contextTokens: 200000, inputUsdPerMTok: 5, outputUsdPerMTok: 25 },
30200
+ { id: "opus[1m]", contextTokens: 1e6, inputUsdPerMTok: 5, outputUsdPerMTok: 25 },
30201
+ { id: "sonnet", contextTokens: 200000, inputUsdPerMTok: 2, outputUsdPerMTok: 10 },
30202
+ { id: "sonnet[1m]", contextTokens: 1e6, inputUsdPerMTok: 2, outputUsdPerMTok: 10 },
30203
+ { id: "haiku", contextTokens: 200000, inputUsdPerMTok: 1, outputUsdPerMTok: 5 }
30204
+ ];
30205
+ var ONE_MILLION_MARKER = "[1m]";
30206
+ var ONE_MILLION_TOKENS = 1e6;
30207
+ function familyOf(id) {
30208
+ return id.endsWith(ONE_MILLION_MARKER) ? id.slice(0, -ONE_MILLION_MARKER.length) : id;
30209
+ }
30210
+ function priceFor(model2) {
30211
+ if (model2 === null)
30212
+ return null;
30213
+ const name = model2.toLowerCase();
30214
+ const oneMillion = name.includes(ONE_MILLION_MARKER);
30215
+ let best = null;
30216
+ for (const row2 of MODEL_PRICES) {
30217
+ const family = familyOf(row2.id);
30218
+ if (family !== row2.id && !oneMillion)
30219
+ continue;
30220
+ if (!name.includes(family))
30221
+ continue;
30222
+ if (best === null || row2.id.length > best.id.length)
30223
+ best = row2;
30224
+ }
30225
+ return best;
30226
+ }
30227
+ function knownContextTokensFor(model2) {
30228
+ const priced = priceFor(model2);
30229
+ if (priced !== null)
30230
+ return priced.contextTokens;
30231
+ if (model2 !== null && model2.toLowerCase().includes(ONE_MILLION_MARKER))
30232
+ return ONE_MILLION_TOKENS;
30233
+ return null;
30234
+ }
30235
+ function estimateTokensFromBytes(bytes) {
30236
+ return Math.round(bytes / BYTES_PER_TOKEN);
30237
+ }
30238
+
30239
+ // src/core/facilitator/contextLedger.ts
30240
+ var DEFAULT_PROMPT_MAX_BYTES = 400 * 1024;
30241
+ var CONTEXT_WARN_PCT = 80;
30242
+ function buildLedger(input) {
30243
+ const rows = [];
30244
+ let stage2 = 0;
30245
+ let inputs = 0;
30246
+ let expertBodies = 0;
30247
+ let expertKnowledge = 0;
30248
+ let dispatchNotes = 0;
30249
+ let projectSkills = 0;
30250
+ let previousAttempt = 0;
30251
+ for (const part of input.parts) {
30252
+ const bytes = byteLength(part.text);
30253
+ switch (part.kind) {
30254
+ case "preamble":
30255
+ stage2 += bytes;
30256
+ rows.push({ kind: part.kind, name: part.name, bytes });
30257
+ break;
30258
+ case "stage":
30259
+ stage2 += bytes;
30260
+ rows.push({ kind: part.kind, name: part.name, bytes });
30261
+ break;
30262
+ case "expert-body":
30263
+ expertBodies += bytes;
30264
+ rows.push({ kind: part.kind, name: part.name, bytes });
30265
+ break;
30266
+ case "expert-knowledge":
30267
+ expertKnowledge += bytes;
30268
+ rows.push({ kind: part.kind, name: part.name, bytes });
30269
+ break;
30270
+ case "dispatch-notes":
30271
+ dispatchNotes += bytes;
30272
+ rows.push({ kind: part.kind, name: part.name, bytes });
30273
+ break;
30274
+ case "project-skills":
30275
+ projectSkills += bytes;
30276
+ rows.push({ kind: part.kind, name: part.name, bytes });
30277
+ break;
30278
+ case "previous-attempt":
30279
+ previousAttempt += bytes;
30280
+ rows.push({ kind: part.kind, name: part.name, bytes });
30281
+ break;
30282
+ case "inputs":
30283
+ inputs += bytes;
30284
+ for (const file of input.inputBytes) {
30285
+ rows.push({ kind: "inputs", name: file.path, bytes: file.bytes });
30286
+ }
30287
+ break;
30288
+ }
30289
+ }
30290
+ const totalBytes = stage2 + inputs + expertBodies + expertKnowledge + dispatchNotes + projectSkills + previousAttempt;
30291
+ const estimatedTokens = estimateTokensFromBytes(totalBytes);
30292
+ const contextTokens = knownContextTokensFor(input.model);
30293
+ const contextPct = contextTokens === null || contextTokens === 0 ? null : estimatedTokens / contextTokens * 100;
30294
+ return {
30295
+ totalBytes,
30296
+ rows,
30297
+ groups: {
30298
+ stage: stage2,
30299
+ inputs,
30300
+ experts: expertBodies + expertKnowledge,
30301
+ expertBodies,
30302
+ expertKnowledge,
30303
+ dispatchNotes,
30304
+ projectSkills,
30305
+ previousAttempt,
30306
+ questions: input.questionsBytes ?? 0
30307
+ },
30308
+ truncatedInputs: input.truncatedInputs,
30309
+ limitBytes: input.limitBytes,
30310
+ overLimit: totalBytes > input.limitBytes,
30311
+ model: input.model,
30312
+ estimatedTokens,
30313
+ contextTokens,
30314
+ contextPct,
30315
+ contextWarns: contextPct !== null && contextPct >= CONTEXT_WARN_PCT
30316
+ };
30317
+ }
30318
+ function questionsBytesOf(stageMd) {
30319
+ const lines = stageMd.split(`
30320
+ `);
30321
+ const start = lines.findIndex((line) => line.trim() === "## Questions");
30322
+ if (start === -1)
30323
+ return 0;
30324
+ let end = lines.length;
30325
+ for (let i = start + 1;i < lines.length; i++) {
30326
+ if ((lines[i] ?? "").startsWith("## ")) {
30327
+ end = i;
30328
+ break;
30329
+ }
30330
+ }
30331
+ return byteLength(lines.slice(start, end).join(`
30332
+ `));
30333
+ }
30334
+ function renderLedger(ledger3, maxRows = 8) {
30335
+ const g = ledger3.groups;
30336
+ const lines = [
30337
+ `context ${bytes(ledger3.totalBytes)} of ${bytes(ledger3.limitBytes)} ` + `(~${tokens2(ledger3.estimatedTokens)} tok${windowClause(ledger3)})`,
30338
+ ` stage ${bytes(g.stage)}${g.questions === 0 ? "" : ` (questions ${bytes(g.questions)})`}` + ` · inputs ${bytes(g.inputs)} · experts ${bytes(g.experts)}` + ` (bodies ${bytes(g.expertBodies)}, knowledge ${bytes(g.expertKnowledge)})` + (g.dispatchNotes === 0 ? "" : ` · dispatch notes ${bytes(g.dispatchNotes)}`) + (g.projectSkills === 0 ? "" : ` · project skills ${bytes(g.projectSkills)}`) + (g.previousAttempt === 0 ? "" : ` · previous attempt ${bytes(g.previousAttempt)}`)
30339
+ ];
30340
+ const biggest = [...ledger3.rows].filter((row2) => row2.kind !== "inputs" || row2.name !== "Inputs").sort((a, b) => b.bytes - a.bytes).slice(0, maxRows);
30341
+ for (const row2 of biggest) {
30342
+ lines.push(` ${label(row2)} ${bytes(row2.bytes)}`);
30343
+ }
30344
+ return lines;
30345
+ }
30346
+ function renderRefusal(ledger3, stageId) {
30347
+ const g = ledger3.groups;
30348
+ const lines = [
30349
+ `[tldrx] context: refusing to start stage "${stageId}" — the prompt is ` + `${bytes(ledger3.totalBytes)} and prompt_max_bytes is ${bytes(ledger3.limitBytes)}.`,
30350
+ ...renderLedger(ledger3, 5),
30351
+ "Shrink it, then run again:"
30352
+ ];
30353
+ if (g.inputs >= g.experts) {
30354
+ lines.push(` inputs are the biggest section (${bytes(g.inputs)}) — lower \`inputs_max_bytes\` ` + "in the stage file, declare fewer inputs, or split the seed with `tldrx seed triage`.");
30355
+ }
30356
+ if (g.experts > 0) {
30357
+ lines.push(` experts are ${bytes(g.experts)} (${bytes(g.expertKnowledge)} of it trained knowledge) — ` + "lower `knowledge_max_bytes` in the stage file, or narrow the stage's `experts:` list.");
30358
+ }
30359
+ lines.push(` or raise the ceiling deliberately: \`prompt_max_bytes\` in the stage file, ` + "or `--prompt-max-bytes <n>` for this one invocation.");
30360
+ return lines;
30361
+ }
30362
+ function renderContextWarning(ledger3) {
30363
+ if (!ledger3.contextWarns || ledger3.contextTokens === null || ledger3.contextPct === null)
30364
+ return [];
30365
+ return [
30366
+ `note: this prompt is ~${tokens2(ledger3.estimatedTokens)} tokens, ` + `${ledger3.contextPct.toFixed(0)}% of ${shortModel2(ledger3.model)}'s ~${tokens2(ledger3.contextTokens)}-token ` + "window [assumption: both the window and the ~3.6 bytes/token ratio are estimates, " + "see src/core/budget/modelPrices.ts]. The sub-agent still has to fit its own reading and " + "its answer in what is left."
30367
+ ];
30368
+ }
30369
+ function windowClause(ledger3) {
30370
+ if (ledger3.contextTokens === null || ledger3.contextPct === null)
30371
+ return "";
30372
+ return `, ${ledger3.contextPct.toFixed(0)}% of ${shortModel2(ledger3.model)}'s ` + `~${tokens2(ledger3.contextTokens)} window`;
30373
+ }
30374
+ function label(row2) {
30375
+ switch (row2.kind) {
30376
+ case "stage":
30377
+ return "stage.md";
30378
+ case "inputs":
30379
+ return `input ${row2.name}`;
30380
+ case "expert-body":
30381
+ return `expert ${row2.name} body`;
30382
+ case "expert-knowledge":
30383
+ return `expert ${row2.name} knowledge`;
30384
+ case "preamble":
30385
+ return "stage preamble";
30386
+ case "dispatch-notes":
30387
+ return "dispatch notes";
30388
+ case "project-skills":
30389
+ return "project skills";
30390
+ case "previous-attempt":
30391
+ return "previous attempt";
30392
+ }
30393
+ }
30394
+ function shortModel2(model2) {
30395
+ return model2 === null || model2 === "" ? "the default model" : model2;
30396
+ }
30397
+ function bytes(count) {
30398
+ return count < 1024 ? `${String(count)} B` : `${(count / 1024).toFixed(1)} KB`;
30399
+ }
30400
+ function tokens2(count) {
30401
+ return count < 1000 ? String(count) : `${(count / 1000).toFixed(1)}k`;
30402
+ }
30403
+
30404
+ // src/core/facilitator/stageSpec.ts
30405
+ var DEFAULT_STACK_EXPERTS = true;
30406
+ var DEFAULT_DRY_RUN_ALLOWED = true;
30407
+ function loadStageSpec(root2, scope, stageId) {
30408
+ const preset = loadWorkflowPreset(root2, scope);
30409
+ const planned = preset.stages.find((s) => s.id === stageId);
30410
+ if (planned === undefined) {
30411
+ throw new PresetError(`stage '${stageId}' is not in workflow '${preset.name}' (${preset.source})`);
30412
+ }
30413
+ const over = overlay(root2, scope, stageId);
30414
+ return {
30415
+ planned,
30416
+ scope: preset.name,
30417
+ skips: preset.skips,
30418
+ ...over,
30419
+ tuning: { ...over.tuning, attempts: planned.attempts },
30420
+ ...inputSplit(root2, stageId)
30421
+ };
30422
+ }
30423
+ function overlay(root2, scope, stageId) {
30424
+ const stageDoc = readDoc(stagePath(root2, stageId));
30425
+ const workflowDoc = readDoc(workflowPath(root2, scope));
30426
+ let skipIf = null;
30427
+ const entries = isRecord(workflowDoc) && Array.isArray(workflowDoc.stages) ? workflowDoc.stages : [];
30428
+ for (const entry of entries) {
30429
+ if (!isRecord(entry) || entry.id !== stageId)
30430
+ continue;
30431
+ if (typeof entry.skip_if === "string" && entry.skip_if !== "")
30432
+ skipIf = entry.skip_if;
30433
+ }
30434
+ const questions = isRecord(stageDoc) && isRecord(stageDoc.questions) ? stageDoc.questions : null;
30435
+ return {
30436
+ stackExperts: isRecord(stageDoc) && typeof stageDoc.stack_experts === "boolean" ? stageDoc.stack_experts : DEFAULT_STACK_EXPERTS,
30437
+ knowledgeMaxBytes: byteKey(stageDoc, "knowledge_max_bytes") ?? byteKey(stageDoc, "expert_knowledge_bytes") ?? DEFAULT_KNOWLEDGE_MAX_BYTES,
30438
+ inputsMaxBytes: byteKey(stageDoc, "inputs_max_bytes") ?? DEFAULT_INPUTS_MAX_BYTES,
30439
+ promptMaxBytes: byteKey(stageDoc, "prompt_max_bytes") ?? DEFAULT_PROMPT_MAX_BYTES,
30440
+ maxReads: byteKey(stageDoc, "max_reads") ?? defaultMaxReads(stageId),
30441
+ parallel: parallelKey(workflowDoc, stageId) ?? byteKey(stageDoc, "parallel"),
30442
+ reviewer: reviewerOverride(isRecord(stageDoc) ? stageDoc.reviewer : undefined),
30443
+ reviewerByStakes: reviewerByStakesMap(isRecord(stageDoc) ? stageDoc.reviewer_by_stakes : undefined),
30444
+ tuning: readStageTuning(stageDoc),
30445
+ dryRunAllowed: isRecord(stageDoc) && typeof stageDoc.dry_run_allowed === "boolean" ? stageDoc.dry_run_allowed : DEFAULT_DRY_RUN_ALLOWED,
30446
+ skipIf,
30447
+ questionsMax: questions !== null && typeof questions.max === "number" ? questions.max : null
30448
+ };
30449
+ }
30450
+ function inputSplit(root2, stageId) {
30451
+ const doc = readDoc(stagePath(root2, stageId));
30452
+ const inputs = isRecord(doc) ? doc.inputs : undefined;
30453
+ const topLevelSeed = isRecord(doc) && doc.seed === true;
30454
+ if (isRecord(inputs)) {
30455
+ return {
30456
+ requiredInputs: strings2(inputs.required),
30457
+ optionalInputs: strings2(inputs.optional),
30458
+ seedInputs: inputs.seed === true || topLevelSeed
30459
+ };
30460
+ }
30461
+ return { requiredInputs: [], optionalInputs: strings2(inputs), seedInputs: topLevelSeed };
30462
+ }
30463
+ function reviewerOverride(value) {
30464
+ if (!isRecord(value))
30465
+ return null;
30466
+ const model2 = typeof value.model === "string" && value.model !== "" ? value.model : undefined;
30467
+ const effort2 = EFFORT_LEVELS.includes(String(value.effort)) ? value.effort : undefined;
30468
+ if (model2 === undefined && effort2 === undefined)
30469
+ return null;
30470
+ return { ...model2 === undefined ? {} : { model: model2 }, ...effort2 === undefined ? {} : { effort: effort2 } };
30471
+ }
30472
+ function reviewerByStakesMap(value) {
30473
+ if (!isRecord(value))
30474
+ return {};
30475
+ const out = {};
30476
+ for (const [key, entry] of Object.entries(value)) {
30477
+ const override = reviewerOverride(entry);
30478
+ if (override !== null)
30479
+ out[key] = override;
30480
+ }
30481
+ return out;
30482
+ }
30483
+ function parallelKey(workflowDoc, stageId) {
30484
+ if (!isRecord(workflowDoc))
30485
+ return null;
30486
+ const entry = workflowDoc[stageId];
30487
+ return isRecord(entry) ? byteKey(entry, "parallel") : null;
30488
+ }
30489
+ function byteKey(doc, key) {
30490
+ if (!isRecord(doc))
30491
+ return null;
30492
+ const value = doc[key];
30493
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
30494
+ return null;
30495
+ return Math.trunc(value);
30496
+ }
30497
+ function readDoc(path) {
30498
+ if (path === null || !existsSync44(path))
30499
+ return null;
30500
+ try {
30501
+ return parseYaml(readFileSync40(path, "utf8"));
30502
+ } catch {
30503
+ return null;
30504
+ }
30505
+ }
30506
+ function strings2(value) {
30507
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
30508
+ }
30509
+
30510
+ // src/core/run/checks.ts
29922
30511
  var WRITE_TIME_ONLY = ["no-reask", "budget-gate", "dod"];
29923
30512
  async function runChecks(checks, ctx) {
29924
30513
  const outcomes = [];
@@ -29964,12 +30553,12 @@ function checkClaimSources(ctx) {
29964
30553
  continue;
29965
30554
  }
29966
30555
  for (const hit of hits) {
29967
- if (!existsSync44(hit.absolute)) {
30556
+ if (!existsSync45(hit.absolute)) {
29968
30557
  if (isHandoffOutput)
29969
30558
  failures.push(`${hit.path} was declared but never written`);
29970
30559
  continue;
29971
30560
  }
29972
- const text3 = readFileSync39(hit.absolute, "utf8");
30561
+ const text3 = readFileSync41(hit.absolute, "utf8");
29973
30562
  if (isHandoffOutput || isHandoff(text3)) {
29974
30563
  handoffs++;
29975
30564
  const validation = validateHandoff(text3, srcCtx);
@@ -30120,12 +30709,63 @@ function checkPlan(ctx) {
30120
30709
  return { id: "plan", status: "failed", detail: describePlanIssues(issues) };
30121
30710
  }
30122
30711
  const model2 = branchModelFor(basename10(ctx.runDir), report2.epicChain);
30712
+ const over = planPricesOverStage(ctx, planDir);
30123
30713
  return {
30124
30714
  id: "plan",
30125
30715
  status: "passed",
30126
- detail: `${report2.epicCount} epic(s), ${report2.storyCount} story(ies), ${report2.waveCount} wave(s)` + ` — ${describeBranchModel(model2)}`
30716
+ detail: `${report2.epicCount} epic(s), ${report2.storyCount} story(ies), ${report2.waveCount} wave(s)` + ` — ${describeBranchModel(model2)}${over === null ? "" : ` — ${over}`}`
30127
30717
  };
30128
30718
  }
30719
+ function planPricesOverStage(ctx, planDir) {
30720
+ const run = rawYaml(join62(ctx.runDir, "run.yml"));
30721
+ if (run === null || typeof run.scope !== "string" || !Array.isArray(run.phases))
30722
+ return null;
30723
+ const stage2 = run.phases.find((phase) => phase.id === BUILD_PHASE2)?.stages?.[0];
30724
+ if (stage2 === undefined || typeof stage2.id !== "string" || typeof stage2.budget_usd !== "number")
30725
+ return null;
30726
+ const progress = buildProgress(ctx.runDir);
30727
+ if (progress === null || progress.implicit)
30728
+ return null;
30729
+ const ids = new Set(progress.waves.flatMap((wave) => wave.stories.map((story) => story.id)));
30730
+ const prices = loadPlanPrices(planDir, ids).prices;
30731
+ if (prices.size === 0)
30732
+ return null;
30733
+ let spec = null;
30734
+ try {
30735
+ spec = loadStageSpec(ctx.root, run.scope, stage2.id);
30736
+ } catch {
30737
+ spec = null;
30738
+ }
30739
+ const budget = rawYaml(join62(ctx.runDir, "budget.yml"));
30740
+ const perAgentMax = typeof budget?.per_agent_max_usd === "number" ? budget.per_agent_max_usd : Infinity;
30741
+ const budgetUsd = stage2.budget_usd;
30742
+ const parts = {
30743
+ prices,
30744
+ storyCount: ids.size,
30745
+ budgetUsd,
30746
+ maxBudgetUsd: Math.min(budgetUsd, perAgentMax),
30747
+ agentCap: (share = 1) => round23(Math.min(budgetUsd * share, perAgentMax)),
30748
+ ...spec === null ? {} : {
30749
+ attempts: spec.tuning.attempts,
30750
+ reviewerShare: spec.tuning.reviewerShare,
30751
+ storyCapMultiplier: spec.tuning.storyCapMultiplier,
30752
+ storyCapFloorUsd: spec.tuning.storyCapFloorUsd
30753
+ }
30754
+ };
30755
+ const runId = basename10(ctx.runDir);
30756
+ return planOverStageAdvisory(parts, {
30757
+ raiseCommand: (usd3) => stageRaiseCommand(runId, BUILD_PHASE2, stage2.id, usd3)
30758
+ });
30759
+ }
30760
+ function rawYaml(path) {
30761
+ if (!existsSync45(path))
30762
+ return null;
30763
+ try {
30764
+ return parseYaml(readFileSync41(path, "utf8"));
30765
+ } catch {
30766
+ return null;
30767
+ }
30768
+ }
30129
30769
  function checkSchema(ctx) {
30130
30770
  const pairs = [
30131
30771
  ["run.yml", validateRunFile],
@@ -30133,9 +30773,9 @@ function checkSchema(ctx) {
30133
30773
  ];
30134
30774
  for (const [name, validate2] of pairs) {
30135
30775
  const path = join62(ctx.runDir, name);
30136
- if (!existsSync44(path))
30776
+ if (!existsSync45(path))
30137
30777
  return { id: "schema", status: "failed", detail: `${name} is missing` };
30138
- const validation = validate2(parseYaml(readFileSync39(path, "utf8")));
30778
+ const validation = validate2(parseYaml(readFileSync41(path, "utf8")));
30139
30779
  if (!validation.ok) {
30140
30780
  const first2 = validation.issues[0];
30141
30781
  return { id: "schema", status: "failed", detail: `${name}: ${first2?.path ?? ""} ${first2?.message ?? ""}`.trim() };
@@ -30334,7 +30974,7 @@ var MISSING_QUESTIONS = "questions.md is a declared output of this stage and was
30334
30974
  function questionsCondition(runDir, phaseId, planned) {
30335
30975
  const path = join63(runDir, phaseId, "questions.md");
30336
30976
  if (declaresQuestions(planned)) {
30337
- if (!existsSync45(path)) {
30977
+ if (!existsSync46(path)) {
30338
30978
  return { id: "questions", ok: false, detail: MISSING_QUESTIONS };
30339
30979
  }
30340
30980
  const unreadable = unreadableHeadings(path);
@@ -30357,17 +30997,17 @@ function declaresQuestions(planned) {
30357
30997
  return planned.outputs.some((path) => path.endsWith("questions.md"));
30358
30998
  }
30359
30999
  function unreadableHeadings(path) {
30360
- if (!existsSync45(path))
31000
+ if (!existsSync46(path))
30361
31001
  return [];
30362
31002
  try {
30363
- return unreadableQuestionHeadings(readFileSync40(path, "utf8"));
31003
+ return unreadableQuestionHeadings(readFileSync42(path, "utf8"));
30364
31004
  } catch {
30365
31005
  return [];
30366
31006
  }
30367
31007
  }
30368
31008
  function parsedBlocks(path) {
30369
31009
  try {
30370
- return parseQuestions(readFileSync40(path, "utf8")).blocks.length;
31010
+ return parseQuestions(readFileSync42(path, "utf8")).blocks.length;
30371
31011
  } catch {
30372
31012
  return 0;
30373
31013
  }
@@ -30376,7 +31016,7 @@ function budgetCondition(input) {
30376
31016
  if (isHostTokens(input.budget, input.phaseId)) {
30377
31017
  return { id: "budget", ok: true, detail: "n/a (host-tokens economy)" };
30378
31018
  }
30379
- const spent = round23(input.stage.tasks.reduce((sum, task2) => sum + (task2.cost_usd ?? 0), 0));
31019
+ const spent = round24(input.stage.tasks.reduce((sum, task2) => sum + (task2.cost_usd ?? 0), 0));
30380
31020
  const unmetered = input.stage.tasks.filter((task2) => task2.cost_usd === null).length;
30381
31021
  const phase = input.budget.phases.find((entry) => entry.id === input.phaseId);
30382
31022
  const phaseSpent = phase === undefined ? 0 : phase.spent_usd;
@@ -30446,10 +31086,10 @@ async function boundaryCondition(input) {
30446
31086
  return { id: "boundary", ...verdict };
30447
31087
  }
30448
31088
  function openQuestions(path) {
30449
- if (!existsSync45(path))
31089
+ if (!existsSync46(path))
30450
31090
  return { blocking: [], advisory: [] };
30451
31091
  try {
30452
- const open = openBlocks(parseQuestions(readFileSync40(path, "utf8")).blocks);
31092
+ const open = openBlocks(parseQuestions(readFileSync42(path, "utf8")).blocks);
30453
31093
  return {
30454
31094
  blocking: open.filter((block2) => !isAdvisory(block2)).map((block2) => block2.id),
30455
31095
  advisory: open.filter((block2) => isAdvisory(block2)).map((block2) => block2.id)
@@ -30458,7 +31098,7 @@ function openQuestions(path) {
30458
31098
  return { blocking: ["(questions.md does not parse)"], advisory: [] };
30459
31099
  }
30460
31100
  }
30461
- function round23(n) {
31101
+ function round24(n) {
30462
31102
  return Math.round(n * 100) / 100;
30463
31103
  }
30464
31104
 
@@ -30527,7 +31167,7 @@ function describeGateSignature(sig) {
30527
31167
  import { basename as basename11, join as join65 } from "node:path";
30528
31168
 
30529
31169
  // src/core/facilitator/skipIf.ts
30530
- import { existsSync as existsSync46, readdirSync as readdirSync21, readFileSync as readFileSync41 } from "node:fs";
31170
+ import { existsSync as existsSync47, readdirSync as readdirSync21, readFileSync as readFileSync43 } from "node:fs";
30531
31171
  import { join as join64 } from "node:path";
30532
31172
  var SKIP_IF_RE = /^(stories|repos|questions)(<=|>=|==|<|>)(\d{1,4})$/;
30533
31173
 
@@ -30564,7 +31204,7 @@ function countSkipInputs(runDir, run) {
30564
31204
  }
30565
31205
  function countStories(runDir) {
30566
31206
  const dir = join64(runDir, "03-plan", "stories");
30567
- if (!existsSync46(dir))
31207
+ if (!existsSync47(dir))
30568
31208
  return 0;
30569
31209
  try {
30570
31210
  return readdirSync21(dir).filter((name) => name.endsWith(".md")).length;
@@ -30587,10 +31227,10 @@ function phaseDirs(runDir) {
30587
31227
  }
30588
31228
  }
30589
31229
  function blockingQuestionIds(path) {
30590
- if (!existsSync46(path))
31230
+ if (!existsSync47(path))
30591
31231
  return [];
30592
31232
  try {
30593
- return openBlocks(parseQuestions(readFileSync41(path, "utf8")).blocks).filter((block2) => !isAdvisory(block2)).map((block2) => block2.id);
31233
+ return openBlocks(parseQuestions(readFileSync43(path, "utf8")).blocks).filter((block2) => !isAdvisory(block2)).map((block2) => block2.id);
30594
31234
  } catch {
30595
31235
  return [];
30596
31236
  }
@@ -30702,7 +31342,7 @@ function cancelledMessage(cancelled) {
30702
31342
  }
30703
31343
 
30704
31344
  // src/core/run/operatorNote.ts
30705
- import { existsSync as existsSync47 } from "node:fs";
31345
+ import { existsSync as existsSync48 } from "node:fs";
30706
31346
  import { join as join66 } from "node:path";
30707
31347
  var EXIT_OK3 = 0;
30708
31348
  var EXIT_USAGE2 = 1;
@@ -30719,7 +31359,7 @@ function addOperatorNote(options) {
30719
31359
  ]
30720
31360
  };
30721
31361
  }
30722
- if (options.soleArgument === true && existsSync47(join66(options.root, PROJECT_WORK_DIR, text3))) {
31362
+ if (options.soleArgument === true && existsSync48(join66(options.root, PROJECT_WORK_DIR, text3))) {
30723
31363
  return {
30724
31364
  code: EXIT_USAGE2,
30725
31365
  lines: [
@@ -30963,8 +31603,8 @@ function renderGates(rows, verbose = false) {
30963
31603
  rows.forEach((row2, i) => {
30964
31604
  const head2 = ` ${(where[i] ?? "").padEnd(width)} ${row2.policy.padEnd(5)} `;
30965
31605
  const tail = tails[i] ?? "";
30966
- const describe5 = described[i] ?? "";
30967
- lines.push(tail === "" ? head2 + describe5 : `${head2}${describe5.padEnd(tailWidth)} ${tail}`);
31606
+ const describe6 = described[i] ?? "";
31607
+ lines.push(tail === "" ? head2 + describe6 : `${head2}${describe6.padEnd(tailWidth)} ${tail}`);
30968
31608
  if (verbose)
30969
31609
  lines.push(...gateDetail(row2));
30970
31610
  });
@@ -31079,7 +31719,7 @@ function width(rows, of) {
31079
31719
  }
31080
31720
 
31081
31721
  // src/core/run/decisionCards.ts
31082
- import { existsSync as existsSync48, readFileSync as readFileSync42 } from "node:fs";
31722
+ import { existsSync as existsSync49, readFileSync as readFileSync44 } from "node:fs";
31083
31723
  import { join as join67 } from "node:path";
31084
31724
  function questionsCard(ctx) {
31085
31725
  const blocks = openQuestionBlocks(join67(ctx.runDir, ctx.phaseId, "questions.md"));
@@ -31194,10 +31834,10 @@ function toRecommendation2(entry) {
31194
31834
  return { option: entry.option, why: entry.why, src: entry.src };
31195
31835
  }
31196
31836
  function openQuestionBlocks(path) {
31197
- if (!existsSync48(path))
31837
+ if (!existsSync49(path))
31198
31838
  return [];
31199
31839
  try {
31200
- return openBlocks(parseQuestions(readFileSync42(path, "utf8")).blocks);
31840
+ return openBlocks(parseQuestions(readFileSync44(path, "utf8")).blocks);
31201
31841
  } catch {
31202
31842
  return [];
31203
31843
  }
@@ -31225,17 +31865,17 @@ function readRecommendations(ctx) {
31225
31865
  return out;
31226
31866
  }
31227
31867
  function readOrNull(path) {
31228
- if (!existsSync48(path))
31868
+ if (!existsSync49(path))
31229
31869
  return null;
31230
31870
  try {
31231
- return readFileSync42(path, "utf8");
31871
+ return readFileSync44(path, "utf8");
31232
31872
  } catch {
31233
31873
  return null;
31234
31874
  }
31235
31875
  }
31236
31876
 
31237
31877
  // src/core/run/questionCards.ts
31238
- import { existsSync as existsSync49, readFileSync as readFileSync43 } from "node:fs";
31878
+ import { existsSync as existsSync50, readFileSync as readFileSync45 } from "node:fs";
31239
31879
  import { join as join68 } from "node:path";
31240
31880
  var QUESTION_PHASES = PHASE_IDS;
31241
31881
  var NEEDS_OPTIONS = "NEEDS OPTIONS";
@@ -31243,11 +31883,11 @@ function collectQuestionCards(runDir, runId) {
31243
31883
  const cards = [];
31244
31884
  for (const phase of QUESTION_PHASES) {
31245
31885
  const path = join68(runDir, phase, "questions.md");
31246
- if (!existsSync49(path))
31886
+ if (!existsSync50(path))
31247
31887
  continue;
31248
31888
  let text3;
31249
31889
  try {
31250
- text3 = readFileSync43(path, "utf8");
31890
+ text3 = readFileSync45(path, "utf8");
31251
31891
  } catch {
31252
31892
  continue;
31253
31893
  }
@@ -31261,7 +31901,7 @@ function collectQuestionCards(runDir, runId) {
31261
31901
  function countQuestionFiles(runDir) {
31262
31902
  let found = 0;
31263
31903
  for (const phase of QUESTION_PHASES) {
31264
- if (existsSync49(join68(runDir, phase, "questions.md")))
31904
+ if (existsSync50(join68(runDir, phase, "questions.md")))
31265
31905
  found++;
31266
31906
  }
31267
31907
  return found;
@@ -31340,14 +31980,14 @@ function cardBlock(card, n, total) {
31340
31980
  }
31341
31981
 
31342
31982
  // src/core/facilitator/autoAnswer.ts
31343
- import { existsSync as existsSync53, readFileSync as readFileSync47 } from "node:fs";
31983
+ import { existsSync as existsSync54, readFileSync as readFileSync49 } from "node:fs";
31344
31984
  import { join as join71 } from "node:path";
31345
31985
 
31346
31986
  // src/core/answers/captureAnswers.ts
31347
- import { existsSync as existsSync52, readFileSync as readFileSync46, writeFileSync as writeFileSync11 } from "node:fs";
31987
+ import { existsSync as existsSync53, readFileSync as readFileSync48, writeFileSync as writeFileSync11 } from "node:fs";
31348
31988
 
31349
31989
  // src/core/answers/stampSuperseded.ts
31350
- import { appendFileSync as appendFileSync2, existsSync as existsSync50, readFileSync as readFileSync44, statSync as statSync16 } from "node:fs";
31990
+ import { appendFileSync as appendFileSync2, existsSync as existsSync51, readFileSync as readFileSync46, statSync as statSync17 } from "node:fs";
31351
31991
  import { isAbsolute as isAbsolute6, join as join69, relative as relative11 } from "node:path";
31352
31992
  var STAMP_MARKER = "tldrx:superseded";
31353
31993
  var AFFECTS_KEY = "affects";
@@ -31387,7 +32027,7 @@ function stampSuperseded(runDir, questionsPath, block2, fact, at) {
31387
32027
  const questionsRel = runRelative(runDir, questionsPath) ?? "questions.md";
31388
32028
  const stamped = [];
31389
32029
  for (const doc of affectedDocs(runDir, questionsPath, block2)) {
31390
- const text3 = readFileSync44(doc.abs, "utf8");
32030
+ const text3 = readFileSync46(doc.abs, "utf8");
31391
32031
  if (isStamped(text3, fact))
31392
32032
  continue;
31393
32033
  const lead = text3 === "" ? "" : text3.endsWith(`
@@ -31412,7 +32052,7 @@ function resolveInRun(runDir, raw) {
31412
32052
  const rel = runRelative(runDir, candidate);
31413
32053
  if (rel === null)
31414
32054
  continue;
31415
- if (!existsSync50(candidate) || !statSync16(candidate).isFile())
32055
+ if (!existsSync51(candidate) || !statSync17(candidate).isFile())
31416
32056
  continue;
31417
32057
  return rel;
31418
32058
  }
@@ -31461,18 +32101,18 @@ function reposFromAffects(affects, repoNames) {
31461
32101
  }
31462
32102
 
31463
32103
  // src/core/answers/raiseConflict.ts
31464
- import { appendFileSync as appendFileSync3, existsSync as existsSync51, readFileSync as readFileSync45 } from "node:fs";
32104
+ import { appendFileSync as appendFileSync3, existsSync as existsSync52, readFileSync as readFileSync47 } from "node:fs";
31465
32105
  import { join as join70 } from "node:path";
31466
32106
  var RAISED_BY = "tldrx";
31467
32107
  function nextQuestionId(runDir) {
31468
32108
  let highest = 0;
31469
32109
  for (const phase of QUESTION_PHASES) {
31470
32110
  const path = join70(runDir, phase, "questions.md");
31471
- if (!existsSync51(path))
32111
+ if (!existsSync52(path))
31472
32112
  continue;
31473
32113
  let blocks;
31474
32114
  try {
31475
- blocks = parseQuestions(readFileSync45(path, "utf8")).blocks;
32115
+ blocks = parseQuestions(readFileSync47(path, "utf8")).blocks;
31476
32116
  } catch {
31477
32117
  continue;
31478
32118
  }
@@ -31552,9 +32192,9 @@ function factWasTruncated(title, answer) {
31552
32192
  return `${title} — ${answer}`.length > MAX_FACT_CHARS;
31553
32193
  }
31554
32194
  function captureAnswers(questionsPath, ctx) {
31555
- if (!existsSync52(questionsPath))
32195
+ if (!existsSync53(questionsPath))
31556
32196
  return [];
31557
- let doc = parseQuestions(readFileSync46(questionsPath, "utf8"));
32197
+ let doc = parseQuestions(readFileSync48(questionsPath, "utf8"));
31558
32198
  const answered = detectAnswered(doc.blocks);
31559
32199
  if (answered.length === 0)
31560
32200
  return [];
@@ -31684,12 +32324,12 @@ function markSuperseded(log, ctx, questionsPath, block2, fact) {
31684
32324
  }
31685
32325
  }
31686
32326
  function supersedeAnswer(questionsPath, qid, text3, ctx) {
31687
- if (!existsSync52(questionsPath))
32327
+ if (!existsSync53(questionsPath))
31688
32328
  throw new AnswerError(`no questions file at ${questionsPath}`);
31689
32329
  const answer = text3.trim();
31690
32330
  if (answer === "")
31691
32331
  throw new AnswerError("a superseding answer cannot be empty");
31692
- const doc = parseQuestions(readFileSync46(questionsPath, "utf8"));
32332
+ const doc = parseQuestions(readFileSync48(questionsPath, "utf8"));
31693
32333
  const block2 = doc.blocks.find((b) => b.id === qid);
31694
32334
  if (block2 === undefined)
31695
32335
  throw new AnswerError(`${qid} is not in ${questionsPath}`);
@@ -31767,11 +32407,11 @@ function supersedeAnswer(questionsPath, qid, text3, ctx) {
31767
32407
  return conflict === undefined ? result2 : { ...result2, conflict };
31768
32408
  }
31769
32409
  function writeAnswerSlot(questionsPath, qid, text3) {
31770
- if (!existsSync52(questionsPath))
32410
+ if (!existsSync53(questionsPath))
31771
32411
  throw new AnswerError(`no questions file at ${questionsPath}`);
31772
32412
  if (text3.trim() === "")
31773
32413
  throw new AnswerError("an answer cannot be empty");
31774
- const doc = parseQuestions(readFileSync46(questionsPath, "utf8"));
32414
+ const doc = parseQuestions(readFileSync48(questionsPath, "utf8"));
31775
32415
  const block2 = doc.blocks.find((b) => b.id === qid);
31776
32416
  if (block2 === undefined)
31777
32417
  throw new AnswerError(`${qid} is not in ${questionsPath}`);
@@ -31793,11 +32433,11 @@ function answerRecommended(ctx) {
31793
32433
  for (const phase of QUESTION_PHASES) {
31794
32434
  const rel = `${phase}/questions.md`;
31795
32435
  const path = join71(ctx.runDir, phase, "questions.md");
31796
- if (!existsSync53(path))
32436
+ if (!existsSync54(path))
31797
32437
  continue;
31798
32438
  const ids = blockingIds(path);
31799
32439
  for (const id of ids) {
31800
- const block2 = openBlocks(parseQuestions(readFileSync47(path, "utf8")).blocks).find((b) => b.id === id);
32440
+ const block2 = openBlocks(parseQuestions(readFileSync49(path, "utf8")).blocks).find((b) => b.id === id);
31801
32441
  if (block2 === undefined)
31802
32442
  continue;
31803
32443
  if (pinnedToPerson(block2)) {
@@ -31839,23 +32479,23 @@ function answerRecommended(ctx) {
31839
32479
  }
31840
32480
  function blockingIds(path) {
31841
32481
  try {
31842
- return openBlocks(parseQuestions(readFileSync47(path, "utf8")).blocks).filter((block2) => !isAdvisory(block2)).map((block2) => block2.id);
32482
+ return openBlocks(parseQuestions(readFileSync49(path, "utf8")).blocks).filter((block2) => !isAdvisory(block2)).map((block2) => block2.id);
31843
32483
  } catch {
31844
32484
  return [];
31845
32485
  }
31846
32486
  }
31847
32487
 
31848
32488
  // src/core/notify/declaration.ts
31849
- import { existsSync as existsSync54, readFileSync as readFileSync48 } from "node:fs";
32489
+ import { existsSync as existsSync55, readFileSync as readFileSync50 } from "node:fs";
31850
32490
  import { join as join72 } from "node:path";
31851
32491
  var NOTIFY_TIMEOUT_MS = 30000;
31852
32492
  function readNotifyDeclaration(root2) {
31853
32493
  const path = join72(root2, PROJECT_WORKSPACE_FILE);
31854
- if (!existsSync54(path))
32494
+ if (!existsSync55(path))
31855
32495
  return null;
31856
32496
  let parsed;
31857
32497
  try {
31858
- parsed = parseYaml(readFileSync48(path, "utf8"));
32498
+ parsed = parseYaml(readFileSync50(path, "utf8"));
31859
32499
  } catch {
31860
32500
  return null;
31861
32501
  }
@@ -32259,7 +32899,7 @@ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "node
32259
32899
  import { dirname as dirname12, join as join74 } from "node:path";
32260
32900
 
32261
32901
  // src/core/run/closeRun.ts
32262
- import { existsSync as existsSync55, readFileSync as readFileSync49 } from "node:fs";
32902
+ import { existsSync as existsSync56, readFileSync as readFileSync51 } from "node:fs";
32263
32903
  import { join as join73, relative as relative12, resolve as resolve10, sep as sep9 } from "node:path";
32264
32904
 
32265
32905
  // src/core/facts/decidedTally.ts
@@ -32304,11 +32944,11 @@ function collectOpenQuestions(runDir) {
32304
32944
  for (const phase of QUESTION_PHASES) {
32305
32945
  const rel = `${phase}/questions.md`;
32306
32946
  const abs = join73(runDir, phase, "questions.md");
32307
- if (!existsSync55(abs))
32947
+ if (!existsSync56(abs))
32308
32948
  continue;
32309
32949
  let text3;
32310
32950
  try {
32311
- text3 = readFileSync49(abs, "utf8");
32951
+ text3 = readFileSync51(abs, "utf8");
32312
32952
  } catch {
32313
32953
  continue;
32314
32954
  }
@@ -32647,11 +33287,11 @@ function event2(ts, run, stage2, type, actor, payload) {
32647
33287
 
32648
33288
  // src/core/facilitator/runNext.ts
32649
33289
  import { rmSync as rmSync10 } from "node:fs";
32650
- import { mkdirSync as mkdirSync19, readFileSync as readFileSync74, writeFileSync as writeFileSync18 } from "node:fs";
33290
+ import { mkdirSync as mkdirSync19, readFileSync as readFileSync75, writeFileSync as writeFileSync19 } from "node:fs";
32651
33291
  import { join as join100, relative as relative18 } from "node:path";
32652
33292
 
32653
33293
  // src/core/run/agentGate.ts
32654
- import { existsSync as existsSync56, readFileSync as readFileSync50 } from "node:fs";
33294
+ import { existsSync as existsSync57, readFileSync as readFileSync52 } from "node:fs";
32655
33295
  var BUDGET_DECISION_EVENTS = ["budget.raised", "budget.blocked"];
32656
33296
  async function evaluateAgentGate(input) {
32657
33297
  const auto = await evaluateAutoGate(input);
@@ -32740,17 +33380,17 @@ function describeAgentFallthroughs(fallthroughs) {
32740
33380
  return fallthroughs.map((f) => ` ${f.trigger}: ${f.detail}`);
32741
33381
  }
32742
33382
  function readOrNull2(path) {
32743
- if (!existsSync56(path))
33383
+ if (!existsSync57(path))
32744
33384
  return null;
32745
33385
  try {
32746
- return readFileSync50(path, "utf8");
33386
+ return readFileSync52(path, "utf8");
32747
33387
  } catch {
32748
33388
  return null;
32749
33389
  }
32750
33390
  }
32751
33391
 
32752
33392
  // src/core/build/carriedRows.ts
32753
- import { existsSync as existsSync57, readFileSync as readFileSync51, readdirSync as readdirSync22 } from "node:fs";
33393
+ import { existsSync as existsSync58, readFileSync as readFileSync53, readdirSync as readdirSync22 } from "node:fs";
32754
33394
  import { join as join75 } from "node:path";
32755
33395
 
32756
33396
  // src/core/build/unownedFindings.ts
@@ -32784,7 +33424,7 @@ function unownedFindings(carried, declared, repoNames) {
32784
33424
  function declaredPhases(runDir) {
32785
33425
  let doc;
32786
33426
  try {
32787
- doc = parseYaml(readFileSync51(join75(runDir, "run.yml"), "utf8"));
33427
+ doc = parseYaml(readFileSync53(join75(runDir, "run.yml"), "utf8"));
32788
33428
  } catch {
32789
33429
  return [];
32790
33430
  }
@@ -32810,7 +33450,7 @@ function scanStories(runDir) {
32810
33450
  const seen = new Set;
32811
33451
  for (const phase of phaseDirsOf(runDir)) {
32812
33452
  const dir = join75(runDir, phase, STORIES_DIR);
32813
- if (!existsSync57(dir))
33453
+ if (!existsSync58(dir))
32814
33454
  continue;
32815
33455
  let names;
32816
33456
  try {
@@ -32826,7 +33466,7 @@ function scanStories(runDir) {
32826
33466
  const rel = `${phase}/${STORIES_DIR}/${name}`;
32827
33467
  let text3;
32828
33468
  try {
32829
- text3 = readFileSync51(join75(dir, name), "utf8");
33469
+ text3 = readFileSync53(join75(dir, name), "utf8");
32830
33470
  } catch (error) {
32831
33471
  unreadable.push({ rel, reason: `the file could not be read — ${String(error)}` });
32832
33472
  continue;
@@ -32889,404 +33529,6 @@ function carriedDetailLines(report2) {
32889
33529
  return [...report2.rows.map(carriedCardLine), ...report2.unreadable.map(unreadableCardLine)];
32890
33530
  }
32891
33531
 
32892
- // src/core/facilitator/stageSpec.ts
32893
- import { existsSync as existsSync58, readFileSync as readFileSync53 } from "node:fs";
32894
-
32895
- // src/core/facilitator/seedInputs.ts
32896
- import { readFileSync as readFileSync52, statSync as statSync17 } from "node:fs";
32897
- var DEFAULT_INPUTS_MAX_BYTES = 256 * 1024;
32898
- var MAX_SEED_INLINE_BYTES = 64 * 1024;
32899
- var MIN_SLICE_BYTES = 2 * 1024;
32900
- function inlineInputs(declared, options) {
32901
- const budget = options.budgetBytes ?? DEFAULT_INPUTS_MAX_BYTES;
32902
- const exempt = options.exempt ?? new Set;
32903
- const inputs = [];
32904
- const truncated = [];
32905
- let spent = 0;
32906
- let seedOverflowed = false;
32907
- for (const path of declared) {
32908
- const abs = resolveDeclared(path, options.ctx);
32909
- if (exempt.has(path)) {
32910
- inputs.push({ path, content: readOrEmpty3(abs) });
32911
- continue;
32912
- }
32913
- const size = sizeOf4(abs);
32914
- const room = budget - spent;
32915
- if (size <= room) {
32916
- spent += size;
32917
- inputs.push({ path, content: readOrEmpty3(abs) });
32918
- continue;
32919
- }
32920
- if (options.seed.has(path))
32921
- seedOverflowed = true;
32922
- if (room >= MIN_SLICE_BYTES) {
32923
- spent += room;
32924
- truncated.push({ path, totalBytes: size, inlinedBytes: room });
32925
- inputs.push({ path, content: slice(abs, room), inlinedBytes: room, totalBytes: size });
32926
- continue;
32927
- }
32928
- truncated.push({ path, totalBytes: size, inlinedBytes: 0 });
32929
- inputs.push({ path, content: "", inlinedBytes: 0, totalBytes: size });
32930
- }
32931
- return {
32932
- inputs,
32933
- note: describe5(budget, truncated, seedOverflowed),
32934
- truncated,
32935
- inlinedBytes: spent,
32936
- budgetBytes: budget
32937
- };
32938
- }
32939
- function describe5(budget, truncated, seedOverflowed) {
32940
- if (truncated.length === 0)
32941
- return null;
32942
- const listed = truncated.map((entry) => `${entry.path} (${entry.totalBytes.toLocaleString("en-US")} B` + `${entry.inlinedBytes === 0 ? ", none inlined" : `, first ${entry.inlinedBytes.toLocaleString("en-US")} B only`})`).join("; ");
32943
- const fix = seedOverflowed ? "raise `inputs_max_bytes` in the stage file or split the seed (`tldrx seed triage`)" : "raise `inputs_max_bytes` in the stage file";
32944
- return `truncated inputs: ${listed} — the ${budget.toLocaleString("en-US")}-byte ` + `\`inputs_max_bytes\` budget ran out. ${fix}. ` + "Work from what IS inlined and say in your handoff what you could not read.";
32945
- }
32946
- function describeTruncatedInputs(result2) {
32947
- return result2.truncated.map((entry) => `truncated input: ${entry.path} (${entry.totalBytes.toLocaleString("en-US")} B` + `${entry.inlinedBytes === 0 ? ", none inlined" : `, ${entry.inlinedBytes.toLocaleString("en-US")} B inlined`})` + ` — raise inputs_max_bytes (now ${result2.budgetBytes.toLocaleString("en-US")}) or split the seed`);
32948
- }
32949
- function capInputs(declared, max = MAX_STAGE_INPUTS) {
32950
- return declared.length <= max ? declared : declared.slice(0, max);
32951
- }
32952
- function sizeOf4(abs) {
32953
- try {
32954
- return statSync17(abs).size;
32955
- } catch {
32956
- return 0;
32957
- }
32958
- }
32959
- function slice(abs, bytes) {
32960
- try {
32961
- return readFileSync52(abs).subarray(0, bytes).toString("utf8");
32962
- } catch {
32963
- return "";
32964
- }
32965
- }
32966
- function readOrEmpty3(abs) {
32967
- try {
32968
- return readFileSync52(abs, "utf8");
32969
- } catch {
32970
- return "";
32971
- }
32972
- }
32973
-
32974
- // src/core/budget/modelPrices.ts
32975
- var CACHE_WRITE_MULTIPLIER = 1.25;
32976
- var CACHE_READ_MULTIPLIER = 0.1;
32977
- var BYTES_PER_TOKEN = 3.6;
32978
- var MODEL_PRICES = [
32979
- { id: "opus", contextTokens: 200000, inputUsdPerMTok: 5, outputUsdPerMTok: 25 },
32980
- { id: "opus[1m]", contextTokens: 1e6, inputUsdPerMTok: 5, outputUsdPerMTok: 25 },
32981
- { id: "sonnet", contextTokens: 200000, inputUsdPerMTok: 2, outputUsdPerMTok: 10 },
32982
- { id: "sonnet[1m]", contextTokens: 1e6, inputUsdPerMTok: 2, outputUsdPerMTok: 10 },
32983
- { id: "haiku", contextTokens: 200000, inputUsdPerMTok: 1, outputUsdPerMTok: 5 }
32984
- ];
32985
- var ONE_MILLION_MARKER = "[1m]";
32986
- var ONE_MILLION_TOKENS = 1e6;
32987
- function familyOf(id) {
32988
- return id.endsWith(ONE_MILLION_MARKER) ? id.slice(0, -ONE_MILLION_MARKER.length) : id;
32989
- }
32990
- function priceFor(model2) {
32991
- if (model2 === null)
32992
- return null;
32993
- const name = model2.toLowerCase();
32994
- const oneMillion = name.includes(ONE_MILLION_MARKER);
32995
- let best = null;
32996
- for (const row2 of MODEL_PRICES) {
32997
- const family = familyOf(row2.id);
32998
- if (family !== row2.id && !oneMillion)
32999
- continue;
33000
- if (!name.includes(family))
33001
- continue;
33002
- if (best === null || row2.id.length > best.id.length)
33003
- best = row2;
33004
- }
33005
- return best;
33006
- }
33007
- function knownContextTokensFor(model2) {
33008
- const priced = priceFor(model2);
33009
- if (priced !== null)
33010
- return priced.contextTokens;
33011
- if (model2 !== null && model2.toLowerCase().includes(ONE_MILLION_MARKER))
33012
- return ONE_MILLION_TOKENS;
33013
- return null;
33014
- }
33015
- function estimateTokensFromBytes(bytes) {
33016
- return Math.round(bytes / BYTES_PER_TOKEN);
33017
- }
33018
-
33019
- // src/core/facilitator/contextLedger.ts
33020
- var DEFAULT_PROMPT_MAX_BYTES = 400 * 1024;
33021
- var CONTEXT_WARN_PCT = 80;
33022
- function buildLedger(input) {
33023
- const rows = [];
33024
- let stage2 = 0;
33025
- let inputs = 0;
33026
- let expertBodies = 0;
33027
- let expertKnowledge = 0;
33028
- let dispatchNotes = 0;
33029
- let projectSkills = 0;
33030
- let previousAttempt = 0;
33031
- for (const part of input.parts) {
33032
- const bytes = byteLength(part.text);
33033
- switch (part.kind) {
33034
- case "preamble":
33035
- stage2 += bytes;
33036
- rows.push({ kind: part.kind, name: part.name, bytes });
33037
- break;
33038
- case "stage":
33039
- stage2 += bytes;
33040
- rows.push({ kind: part.kind, name: part.name, bytes });
33041
- break;
33042
- case "expert-body":
33043
- expertBodies += bytes;
33044
- rows.push({ kind: part.kind, name: part.name, bytes });
33045
- break;
33046
- case "expert-knowledge":
33047
- expertKnowledge += bytes;
33048
- rows.push({ kind: part.kind, name: part.name, bytes });
33049
- break;
33050
- case "dispatch-notes":
33051
- dispatchNotes += bytes;
33052
- rows.push({ kind: part.kind, name: part.name, bytes });
33053
- break;
33054
- case "project-skills":
33055
- projectSkills += bytes;
33056
- rows.push({ kind: part.kind, name: part.name, bytes });
33057
- break;
33058
- case "previous-attempt":
33059
- previousAttempt += bytes;
33060
- rows.push({ kind: part.kind, name: part.name, bytes });
33061
- break;
33062
- case "inputs":
33063
- inputs += bytes;
33064
- for (const file of input.inputBytes) {
33065
- rows.push({ kind: "inputs", name: file.path, bytes: file.bytes });
33066
- }
33067
- break;
33068
- }
33069
- }
33070
- const totalBytes = stage2 + inputs + expertBodies + expertKnowledge + dispatchNotes + projectSkills + previousAttempt;
33071
- const estimatedTokens = estimateTokensFromBytes(totalBytes);
33072
- const contextTokens = knownContextTokensFor(input.model);
33073
- const contextPct = contextTokens === null || contextTokens === 0 ? null : estimatedTokens / contextTokens * 100;
33074
- return {
33075
- totalBytes,
33076
- rows,
33077
- groups: {
33078
- stage: stage2,
33079
- inputs,
33080
- experts: expertBodies + expertKnowledge,
33081
- expertBodies,
33082
- expertKnowledge,
33083
- dispatchNotes,
33084
- projectSkills,
33085
- previousAttempt,
33086
- questions: input.questionsBytes ?? 0
33087
- },
33088
- truncatedInputs: input.truncatedInputs,
33089
- limitBytes: input.limitBytes,
33090
- overLimit: totalBytes > input.limitBytes,
33091
- model: input.model,
33092
- estimatedTokens,
33093
- contextTokens,
33094
- contextPct,
33095
- contextWarns: contextPct !== null && contextPct >= CONTEXT_WARN_PCT
33096
- };
33097
- }
33098
- function questionsBytesOf(stageMd) {
33099
- const lines = stageMd.split(`
33100
- `);
33101
- const start = lines.findIndex((line) => line.trim() === "## Questions");
33102
- if (start === -1)
33103
- return 0;
33104
- let end = lines.length;
33105
- for (let i = start + 1;i < lines.length; i++) {
33106
- if ((lines[i] ?? "").startsWith("## ")) {
33107
- end = i;
33108
- break;
33109
- }
33110
- }
33111
- return byteLength(lines.slice(start, end).join(`
33112
- `));
33113
- }
33114
- function renderLedger(ledger3, maxRows = 8) {
33115
- const g = ledger3.groups;
33116
- const lines = [
33117
- `context ${bytes(ledger3.totalBytes)} of ${bytes(ledger3.limitBytes)} ` + `(~${tokens2(ledger3.estimatedTokens)} tok${windowClause(ledger3)})`,
33118
- ` stage ${bytes(g.stage)}${g.questions === 0 ? "" : ` (questions ${bytes(g.questions)})`}` + ` · inputs ${bytes(g.inputs)} · experts ${bytes(g.experts)}` + ` (bodies ${bytes(g.expertBodies)}, knowledge ${bytes(g.expertKnowledge)})` + (g.dispatchNotes === 0 ? "" : ` · dispatch notes ${bytes(g.dispatchNotes)}`) + (g.projectSkills === 0 ? "" : ` · project skills ${bytes(g.projectSkills)}`) + (g.previousAttempt === 0 ? "" : ` · previous attempt ${bytes(g.previousAttempt)}`)
33119
- ];
33120
- const biggest = [...ledger3.rows].filter((row2) => row2.kind !== "inputs" || row2.name !== "Inputs").sort((a, b) => b.bytes - a.bytes).slice(0, maxRows);
33121
- for (const row2 of biggest) {
33122
- lines.push(` ${label(row2)} ${bytes(row2.bytes)}`);
33123
- }
33124
- return lines;
33125
- }
33126
- function renderRefusal(ledger3, stageId) {
33127
- const g = ledger3.groups;
33128
- const lines = [
33129
- `[tldrx] context: refusing to start stage "${stageId}" — the prompt is ` + `${bytes(ledger3.totalBytes)} and prompt_max_bytes is ${bytes(ledger3.limitBytes)}.`,
33130
- ...renderLedger(ledger3, 5),
33131
- "Shrink it, then run again:"
33132
- ];
33133
- if (g.inputs >= g.experts) {
33134
- lines.push(` inputs are the biggest section (${bytes(g.inputs)}) — lower \`inputs_max_bytes\` ` + "in the stage file, declare fewer inputs, or split the seed with `tldrx seed triage`.");
33135
- }
33136
- if (g.experts > 0) {
33137
- lines.push(` experts are ${bytes(g.experts)} (${bytes(g.expertKnowledge)} of it trained knowledge) — ` + "lower `knowledge_max_bytes` in the stage file, or narrow the stage's `experts:` list.");
33138
- }
33139
- lines.push(` or raise the ceiling deliberately: \`prompt_max_bytes\` in the stage file, ` + "or `--prompt-max-bytes <n>` for this one invocation.");
33140
- return lines;
33141
- }
33142
- function renderContextWarning(ledger3) {
33143
- if (!ledger3.contextWarns || ledger3.contextTokens === null || ledger3.contextPct === null)
33144
- return [];
33145
- return [
33146
- `note: this prompt is ~${tokens2(ledger3.estimatedTokens)} tokens, ` + `${ledger3.contextPct.toFixed(0)}% of ${shortModel2(ledger3.model)}'s ~${tokens2(ledger3.contextTokens)}-token ` + "window [assumption: both the window and the ~3.6 bytes/token ratio are estimates, " + "see src/core/budget/modelPrices.ts]. The sub-agent still has to fit its own reading and " + "its answer in what is left."
33147
- ];
33148
- }
33149
- function windowClause(ledger3) {
33150
- if (ledger3.contextTokens === null || ledger3.contextPct === null)
33151
- return "";
33152
- return `, ${ledger3.contextPct.toFixed(0)}% of ${shortModel2(ledger3.model)}'s ` + `~${tokens2(ledger3.contextTokens)} window`;
33153
- }
33154
- function label(row2) {
33155
- switch (row2.kind) {
33156
- case "stage":
33157
- return "stage.md";
33158
- case "inputs":
33159
- return `input ${row2.name}`;
33160
- case "expert-body":
33161
- return `expert ${row2.name} body`;
33162
- case "expert-knowledge":
33163
- return `expert ${row2.name} knowledge`;
33164
- case "preamble":
33165
- return "stage preamble";
33166
- case "dispatch-notes":
33167
- return "dispatch notes";
33168
- case "project-skills":
33169
- return "project skills";
33170
- case "previous-attempt":
33171
- return "previous attempt";
33172
- }
33173
- }
33174
- function shortModel2(model2) {
33175
- return model2 === null || model2 === "" ? "the default model" : model2;
33176
- }
33177
- function bytes(count) {
33178
- return count < 1024 ? `${String(count)} B` : `${(count / 1024).toFixed(1)} KB`;
33179
- }
33180
- function tokens2(count) {
33181
- return count < 1000 ? String(count) : `${(count / 1000).toFixed(1)}k`;
33182
- }
33183
-
33184
- // src/core/facilitator/stageSpec.ts
33185
- var DEFAULT_STACK_EXPERTS = true;
33186
- var DEFAULT_DRY_RUN_ALLOWED = true;
33187
- function loadStageSpec(root2, scope, stageId) {
33188
- const preset = loadWorkflowPreset(root2, scope);
33189
- const planned = preset.stages.find((s) => s.id === stageId);
33190
- if (planned === undefined) {
33191
- throw new PresetError(`stage '${stageId}' is not in workflow '${preset.name}' (${preset.source})`);
33192
- }
33193
- const over = overlay(root2, scope, stageId);
33194
- return {
33195
- planned,
33196
- scope: preset.name,
33197
- skips: preset.skips,
33198
- ...over,
33199
- tuning: { ...over.tuning, attempts: planned.attempts },
33200
- ...inputSplit(root2, stageId)
33201
- };
33202
- }
33203
- function overlay(root2, scope, stageId) {
33204
- const stageDoc = readDoc(stagePath(root2, stageId));
33205
- const workflowDoc = readDoc(workflowPath(root2, scope));
33206
- let skipIf = null;
33207
- const entries = isRecord(workflowDoc) && Array.isArray(workflowDoc.stages) ? workflowDoc.stages : [];
33208
- for (const entry of entries) {
33209
- if (!isRecord(entry) || entry.id !== stageId)
33210
- continue;
33211
- if (typeof entry.skip_if === "string" && entry.skip_if !== "")
33212
- skipIf = entry.skip_if;
33213
- }
33214
- const questions = isRecord(stageDoc) && isRecord(stageDoc.questions) ? stageDoc.questions : null;
33215
- return {
33216
- stackExperts: isRecord(stageDoc) && typeof stageDoc.stack_experts === "boolean" ? stageDoc.stack_experts : DEFAULT_STACK_EXPERTS,
33217
- knowledgeMaxBytes: byteKey(stageDoc, "knowledge_max_bytes") ?? byteKey(stageDoc, "expert_knowledge_bytes") ?? DEFAULT_KNOWLEDGE_MAX_BYTES,
33218
- inputsMaxBytes: byteKey(stageDoc, "inputs_max_bytes") ?? DEFAULT_INPUTS_MAX_BYTES,
33219
- promptMaxBytes: byteKey(stageDoc, "prompt_max_bytes") ?? DEFAULT_PROMPT_MAX_BYTES,
33220
- maxReads: byteKey(stageDoc, "max_reads") ?? defaultMaxReads(stageId),
33221
- parallel: parallelKey(workflowDoc, stageId) ?? byteKey(stageDoc, "parallel"),
33222
- reviewer: reviewerOverride(isRecord(stageDoc) ? stageDoc.reviewer : undefined),
33223
- reviewerByStakes: reviewerByStakesMap(isRecord(stageDoc) ? stageDoc.reviewer_by_stakes : undefined),
33224
- tuning: readStageTuning(stageDoc),
33225
- dryRunAllowed: isRecord(stageDoc) && typeof stageDoc.dry_run_allowed === "boolean" ? stageDoc.dry_run_allowed : DEFAULT_DRY_RUN_ALLOWED,
33226
- skipIf,
33227
- questionsMax: questions !== null && typeof questions.max === "number" ? questions.max : null
33228
- };
33229
- }
33230
- function inputSplit(root2, stageId) {
33231
- const doc = readDoc(stagePath(root2, stageId));
33232
- const inputs = isRecord(doc) ? doc.inputs : undefined;
33233
- const topLevelSeed = isRecord(doc) && doc.seed === true;
33234
- if (isRecord(inputs)) {
33235
- return {
33236
- requiredInputs: strings2(inputs.required),
33237
- optionalInputs: strings2(inputs.optional),
33238
- seedInputs: inputs.seed === true || topLevelSeed
33239
- };
33240
- }
33241
- return { requiredInputs: [], optionalInputs: strings2(inputs), seedInputs: topLevelSeed };
33242
- }
33243
- function reviewerOverride(value) {
33244
- if (!isRecord(value))
33245
- return null;
33246
- const model2 = typeof value.model === "string" && value.model !== "" ? value.model : undefined;
33247
- const effort2 = EFFORT_LEVELS.includes(String(value.effort)) ? value.effort : undefined;
33248
- if (model2 === undefined && effort2 === undefined)
33249
- return null;
33250
- return { ...model2 === undefined ? {} : { model: model2 }, ...effort2 === undefined ? {} : { effort: effort2 } };
33251
- }
33252
- function reviewerByStakesMap(value) {
33253
- if (!isRecord(value))
33254
- return {};
33255
- const out = {};
33256
- for (const [key, entry] of Object.entries(value)) {
33257
- const override = reviewerOverride(entry);
33258
- if (override !== null)
33259
- out[key] = override;
33260
- }
33261
- return out;
33262
- }
33263
- function parallelKey(workflowDoc, stageId) {
33264
- if (!isRecord(workflowDoc))
33265
- return null;
33266
- const entry = workflowDoc[stageId];
33267
- return isRecord(entry) ? byteKey(entry, "parallel") : null;
33268
- }
33269
- function byteKey(doc, key) {
33270
- if (!isRecord(doc))
33271
- return null;
33272
- const value = doc[key];
33273
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
33274
- return null;
33275
- return Math.trunc(value);
33276
- }
33277
- function readDoc(path) {
33278
- if (path === null || !existsSync58(path))
33279
- return null;
33280
- try {
33281
- return parseYaml(readFileSync53(path, "utf8"));
33282
- } catch {
33283
- return null;
33284
- }
33285
- }
33286
- function strings2(value) {
33287
- return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
33288
- }
33289
-
33290
33532
  // src/core/plan/schemaContract.ts
33291
33533
  var PLAN_CONTRACT_HEADING = "Output schemas";
33292
33534
  var STORY_OPTIONAL_KEYS = ["stakes"];
@@ -34820,9 +35062,10 @@ async function watchExecutor(ctx) {
34820
35062
  tasks: [],
34821
35063
  costUsd: 0,
34822
35064
  outputs: [],
34823
- lines: incoherent,
35065
+ lines: incoherent.lines,
34824
35066
  error: null,
34825
- refused: true
35067
+ refused: true,
35068
+ signature: incoherent.signature
34826
35069
  };
34827
35070
  }
34828
35071
  prompts = features.map((feature, i) => featurePrompt(ctx, feature, diffs[i] ?? []));
@@ -34853,7 +35096,7 @@ async function watchExecutor(ctx) {
34853
35096
  tasks.push({
34854
35097
  key: feature.id,
34855
35098
  model: ctx.model ?? ctx.spec.planned.model,
34856
- costUsd: round24(outcome.costUsd),
35099
+ costUsd: round25(outcome.costUsd),
34857
35100
  sessionId: outcome.sessionId,
34858
35101
  error: outcome.error,
34859
35102
  outputs: outcome.envelope?.outputs ?? [],
@@ -34890,7 +35133,7 @@ async function watchExecutor(ctx) {
34890
35133
  ok: true,
34891
35134
  awaiting: false,
34892
35135
  tasks,
34893
- costUsd: round24(tasks.reduce((sum, task2) => sum + task2.costUsd, 0)),
35136
+ costUsd: round25(tasks.reduce((sum, task2) => sum + task2.costUsd, 0)),
34894
35137
  outputs: [...written.map((w) => w.path), HANDOFF_REL],
34895
35138
  lines: [
34896
35139
  `${ctx.phaseId}/${ctx.stageId}: ${String(written.length)} watcher card(s) — ` + `${String(verified)} verified, ${String(written.length - verified)} draft`,
@@ -34940,7 +35183,7 @@ function collectResults(ctx, features) {
34940
35183
  tasks.push({
34941
35184
  key: feature.id,
34942
35185
  model: ctx.model ?? ctx.spec.planned.model,
34943
- costUsd: round24(result2.cost_usd ?? 0),
35186
+ costUsd: round25(result2.cost_usd ?? 0),
34944
35187
  sessionId: result2.session_id,
34945
35188
  error: null,
34946
35189
  outputs: result2.outputs
@@ -35059,7 +35302,14 @@ function branchIncoherence(ctx, features, diffs) {
35059
35302
  const [opening, ...rest] = block2;
35060
35303
  out.push(i === 0 ? `${ctx.phaseId}/${ctx.stageId} refuses to start: ${opening ?? ""}` : ` and: ${opening ?? ""}`, ...rest);
35061
35304
  }
35062
- return out;
35305
+ const faults = [
35306
+ ...bases.map((row2) => `base \`${row2.diff.base}\` missing in \`${row2.diff.repo}\``),
35307
+ ...branches.map((row2) => `branch \`${row2.diff.branch}\` (${row2.owner}) missing in \`${row2.diff.repo}\``)
35308
+ ];
35309
+ return {
35310
+ lines: out,
35311
+ signature: `${ctx.phaseId}/${ctx.stageId} refuses to start: ${faults.join("; ")}`
35312
+ };
35063
35313
  }
35064
35314
  function dedupeByRepo(rows) {
35065
35315
  const seen = new Set;
@@ -35090,7 +35340,7 @@ function failed2(ctx, error, tasks) {
35090
35340
  ok: false,
35091
35341
  awaiting: false,
35092
35342
  tasks,
35093
- costUsd: round24(tasks.reduce((sum, task2) => sum + task2.costUsd, 0)),
35343
+ costUsd: round25(tasks.reduce((sum, task2) => sum + task2.costUsd, 0)),
35094
35344
  outputs: [],
35095
35345
  lines: [`${ctx.phaseId}/${ctx.stageId} failed: ${error}`],
35096
35346
  error
@@ -35101,21 +35351,21 @@ function taskKey(ctx, feature) {
35101
35351
  }
35102
35352
  function agentShare(ctx, features) {
35103
35353
  const share = features <= 1 ? ctx.maxBudgetUsd : ctx.maxBudgetUsd / features;
35104
- return round24(Math.max(MIN_AGENT_USD, share));
35354
+ return round25(Math.max(MIN_AGENT_USD, share));
35105
35355
  }
35106
35356
  function floorOverrun(ctx, features) {
35107
- const needed = round24(features * MIN_AGENT_USD);
35357
+ const needed = round25(features * MIN_AGENT_USD);
35108
35358
  if (needed <= ctx.budgetUsd + 0.001)
35109
35359
  return null;
35110
- const short2 = round24(Math.ceil((needed - ctx.budgetUsd) * 100) / 100);
35360
+ const short2 = round25(Math.ceil((needed - ctx.budgetUsd) * 100) / 100);
35111
35361
  return `${ctx.phaseId}/${ctx.stageId} refuses to start: ${features} feature(s) need at least ` + `$${MIN_AGENT_USD.toFixed(2)} each ($${needed.toFixed(2)} in total) and the stage ceiling is ` + `$${ctx.budgetUsd.toFixed(2)}. A spawn under the floor fails as \`error_max_budget_usd\` before it ` + `does any work, so splitting it further would just buy N failures. ` + `Run \`tldrx budget raise ${ctx.phaseId} ${short2.toFixed(2)}\` (add \`--take-from <phase>\` to move ` + "the money instead of adding it), or ship fewer features in one run.";
35112
35362
  }
35113
- function round24(n) {
35363
+ function round25(n) {
35114
35364
  return Math.round(n * 100) / 100;
35115
35365
  }
35116
35366
 
35117
35367
  // src/core/facilitator/executors/build.ts
35118
- import { existsSync as existsSync83, mkdirSync as mkdirSync18, readFileSync as readFileSync73, rmSync as rmSync9, writeFileSync as writeFileSync17 } from "node:fs";
35368
+ import { existsSync as existsSync83, mkdirSync as mkdirSync18, readFileSync as readFileSync74, rmSync as rmSync9, writeFileSync as writeFileSync18 } from "node:fs";
35119
35369
  import { isAbsolute as isAbsolute8, join as join99, relative as relative16 } from "node:path";
35120
35370
 
35121
35371
  // src/core/build/preflight.ts
@@ -35646,10 +35896,9 @@ async function redBaseRefusal(parts, stories) {
35646
35896
  }
35647
35897
  if (failures.length === 0)
35648
35898
  return null;
35649
- const first2 = failures[0];
35650
35899
  return {
35651
35900
  lines: [...baseRefusalLines(failures, parts.workspace)],
35652
- error: first2 === undefined ? "a workspace command fails on the base tree" : `\`${first2.command}\` exits ${String(first2.exitCode ?? "?")} on the base tree of ${first2.repo}`
35901
+ error: failures.length === 0 ? "a workspace command fails on the base tree" : failures.map((row2) => `\`${row2.command}\` exits ${String(row2.exitCode ?? "?")} on the base tree of ${row2.repo}`).join("; ")
35653
35902
  };
35654
35903
  }
35655
35904
  async function runStoryDod(parts) {
@@ -36176,7 +36425,7 @@ async function measureWorktree(parts, decl, dod, repoDir, baseRef, baseSha, decl
36176
36425
  import { basename as basename12 } from "node:path";
36177
36426
 
36178
36427
  // src/core/build/epicRelease.ts
36179
- import { appendFileSync as appendFileSync4, existsSync as existsSync73 } from "node:fs";
36428
+ import { existsSync as existsSync73, readFileSync as readFileSync64, writeFileSync as writeFileSync16 } from "node:fs";
36180
36429
  import { join as join88 } from "node:path";
36181
36430
  var ASIDE_SEPARATOR = "@";
36182
36431
  function asideBranchOf(epicBranch, ownerRunId) {
@@ -36302,14 +36551,15 @@ function appendToHandoff(runDir, released) {
36302
36551
  const path = join88(runDir, BUILD_PHASE, "handoff.md");
36303
36552
  if (!existsSync73(path))
36304
36553
  return;
36305
- const bullets = released.map((record2) => `- ${describeRelease(record2)} — ${record2.via} at ${record2.at}: ${record2.reason} ` + `[src: absent:${BUILD_PHASE}/log]`);
36554
+ const bullets = released.map((record2) => asOneLine(`- ${describeRelease(record2)} — ${record2.via} at ${record2.at}: ${record2.reason} ` + `[src: absent:${BUILD_PHASE}/log]`));
36306
36555
  try {
36307
- appendFileSync4(path, `
36556
+ const before = readFileSync64(path, "utf8");
36557
+ writeFileSync16(path, ensureLineBreakNote(`${before}
36308
36558
  ## Epic branch released
36309
36559
 
36310
36560
  ${bullets.join(`
36311
36561
  `)}
36312
- `, "utf8");
36562
+ `), "utf8");
36313
36563
  } catch {}
36314
36564
  }
36315
36565
 
@@ -36365,21 +36615,27 @@ async function foreignEpicRefusal(state, parts, stories) {
36365
36615
  `[tldrx] build: \`${branch}\` already exists in ${planned.story.repo} and run ${parts.runId} ` + "did not cut it — refusing to stack this run's commits onto someone else's epic.",
36366
36616
  " either delete or rename that branch, or run `tldrx next --reuse-epic` to work on it deliberately."
36367
36617
  ];
36368
- const error = `epic branch \`${branch}\` was not created by this run`;
36618
+ const about = `epic branch \`${branch}\` in ${planned.story.repo} was not created by this run`;
36369
36619
  const owners = epicClaimants(parts.root, branch, parts.runId);
36370
36620
  if (owners.unreadable.length > 0) {
36371
- return { lines: [...verbatim, ` · ${owners.unreadable.join(", ")} could not be read, so who owns it is unknown`], error };
36621
+ return {
36622
+ lines: [...verbatim, ` · ${owners.unreadable.join(", ")} could not be read, so who owns it is unknown`],
36623
+ error: `${about}; who owns it is unknown — unreadable claim(s): ${owners.unreadable.join(", ")}`
36624
+ };
36372
36625
  }
36373
36626
  const open = owners.claimants.filter((store) => !isFinished(store.run.status));
36374
36627
  if (open.length > 0) {
36375
36628
  const named = open.map((store) => `${store.runId} (${store.run.status})`).join(", ");
36376
- return { lines: [...verbatim, ` · claimed by run ${named}, which is still open`], error };
36629
+ return {
36630
+ lines: [...verbatim, ` · claimed by run ${named}, which is still open`],
36631
+ error: `${about}; claimed by open run(s) ${named}`
36632
+ };
36377
36633
  }
36378
36634
  const owner = owners.claimants[0];
36379
36635
  if (owner === undefined) {
36380
36636
  return {
36381
36637
  lines: [...verbatim, ` · no run under ${PROJECT_WORK_DIR}/ records cutting it, so it is nobody's leftover to move`],
36382
- error
36638
+ error: `${about}; no run records cutting it, so it is nobody's leftover to move`
36383
36639
  };
36384
36640
  }
36385
36641
  const status = owner.run.status;
@@ -36403,7 +36659,7 @@ async function foreignEpicRefusal(state, parts, stories) {
36403
36659
  `[tldrx] build: \`${branch}\` already exists in ${planned.story.repo}, left by run ${owner.runId} ` + `(${status}), and could not be moved aside — ${why}.`,
36404
36660
  verbatim[1] ?? ""
36405
36661
  ],
36406
- error
36662
+ error: `${about}; left by run ${owner.runId} (${status}) and could not be moved aside — ${why}`
36407
36663
  };
36408
36664
  }
36409
36665
  state.released.push({ record: record2, owner: owner.runId, ownerStatus: status });
@@ -36482,7 +36738,7 @@ async function dirtyRepoRefusal(parts, stories) {
36482
36738
  ` git -C ${shellQuote2(dir)} stash pop`,
36483
36739
  ...setAside.length === 0 ? [] : [` The other ${String(setAside.length)} change(s) here are nobody's story — ` + "the engine would have set those aside and given them back itself."]
36484
36740
  ],
36485
- error: `repo \`${name}\` has uncommitted changes a pending story declares`
36741
+ error: `repo \`${name}\` has uncommitted changes a pending story declares: ` + verdict2.overlapping.map((row2) => `${row2.entry.path} (${row2.why})`).join("; ")
36486
36742
  });
36487
36743
  }
36488
36744
  aside.push({ repo: name, repoDir: dir, paths: verdict2.foreign.map((entry) => entry.path) });
@@ -36509,92 +36765,11 @@ function epicRows(state, parts, outcomes) {
36509
36765
  }
36510
36766
 
36511
36767
  // src/core/build/reviewBundle.ts
36512
- import { existsSync as existsSync75, readFileSync as readFileSync65, renameSync as renameSync3, rmSync as rmSync8 } from "node:fs";
36768
+ import { existsSync as existsSync75, readFileSync as readFileSync66, renameSync as renameSync3, rmSync as rmSync8 } from "node:fs";
36513
36769
  import { join as join90, relative as relative15 } from "node:path";
36514
36770
 
36515
- // src/core/build/caps.ts
36516
- var MAX_ATTEMPTS2 = STAGE_TUNING_DEFAULTS.attempts;
36517
- var REVIEWER_SHARE2 = STAGE_TUNING_DEFAULTS.reviewerShare;
36518
- var REVIEWER_FLOOR_USD2 = 1;
36519
- var STORY_CAP_MULTIPLIER2 = STAGE_TUNING_DEFAULTS.storyCapMultiplier;
36520
- var STORY_CAP_FLOOR_USD2 = STAGE_TUNING_DEFAULTS.storyCapFloorUsd;
36521
- function developerAttemptDivisor2(attempt, attempts = MAX_ATTEMPTS2) {
36522
- return attempt <= 1 ? 1 : attempts;
36523
- }
36524
- var DEFAULT_PARALLEL = 1;
36525
- function clampParallel(requested) {
36526
- if (requested === undefined || !Number.isFinite(requested))
36527
- return DEFAULT_PARALLEL;
36528
- return Math.max(1, Math.min(MAX_STORIES_PER_WAVE, Math.trunc(requested)));
36529
- }
36530
- function attemptsOf(parts) {
36531
- return parts.attempts ?? MAX_ATTEMPTS2;
36532
- }
36533
- function storyCapMultiplierOf(parts) {
36534
- return parts.storyCapMultiplier ?? STORY_CAP_MULTIPLIER2;
36535
- }
36536
- function storyCapFloorOf(parts) {
36537
- return parts.storyCapFloorUsd ?? STORY_CAP_FLOOR_USD2;
36538
- }
36539
- function reviewerShareOf(parts) {
36540
- return parts.reviewerShare ?? REVIEWER_SHARE2;
36541
- }
36542
- function round25(n) {
36543
- return Math.round(n * 100) / 100;
36544
- }
36545
- function developerCap(parts, storyId, attempt = 1) {
36546
- const price = priceOf(parts, storyId);
36547
- if (price === null)
36548
- return parts.agentCap(1 / worstCaseShares(parts));
36549
- return parts.agentCap(shareOf(parts, storyCeilingUsd(parts, price) / developerAttemptDivisor2(attempt, attemptsOf(parts))));
36550
- }
36551
- function storyCeilingUsd(parts, price) {
36552
- const floor = parts.budgetUsd > 0 ? Math.min(storyCapFloorOf(parts), parts.budgetUsd) : storyCapFloorOf(parts);
36553
- return Math.max(price * storyCapMultiplierOf(parts), floor);
36554
- }
36555
- function reviewerCap(parts, spentUsd, storyId) {
36556
- const price = priceOf(parts, storyId);
36557
- const share = reviewerShareOf(parts);
36558
- const derived = price === null ? parts.agentCap(share / worstCaseShares(parts)) : parts.agentCap(shareOf(parts, price * share / (attemptsOf(parts) * (1 + share))));
36559
- const floor = Math.min(REVIEWER_FLOOR_USD2, Math.max(parts.budgetUsd - spentUsd, 0));
36560
- return round25(Math.min(Math.max(derived, floor), parts.maxBudgetUsd));
36561
- }
36562
- function stageRemainderUsd(parts, spentUsd) {
36563
- if (!(parts.budgetUsd > 0))
36564
- return null;
36565
- return Math.max(round25(parts.budgetUsd - spentUsd), 0);
36566
- }
36567
- function reviewerUnderfunded(parts, spentUsd) {
36568
- const left = stageRemainderUsd(parts, spentUsd);
36569
- return left !== null && left < REVIEWER_FLOOR_USD2;
36570
- }
36571
- function priceOf(parts, storyId) {
36572
- if (storyId === undefined)
36573
- return null;
36574
- const price = parts.prices.get(storyId);
36575
- if (price === undefined || !Number.isFinite(price) || price <= 0)
36576
- return null;
36577
- return price * priceScale(parts);
36578
- }
36579
- function priceScale(parts) {
36580
- if (parts.budgetUsd <= 0)
36581
- return 1;
36582
- let sum = 0;
36583
- for (const price of parts.prices.values()) {
36584
- if (Number.isFinite(price) && price > 0)
36585
- sum += price;
36586
- }
36587
- return sum <= parts.budgetUsd ? 1 : parts.budgetUsd / sum;
36588
- }
36589
- function shareOf(parts, usd2) {
36590
- return parts.budgetUsd <= 0 ? 1 : usd2 / parts.budgetUsd;
36591
- }
36592
- function worstCaseShares(parts) {
36593
- return Math.max(parts.storyCount, 1) * attemptsOf(parts) * (1 + reviewerShareOf(parts));
36594
- }
36595
-
36596
36771
  // src/core/build/reviewLedger.ts
36597
- import { existsSync as existsSync74, readFileSync as readFileSync64 } from "node:fs";
36772
+ import { existsSync as existsSync74, readFileSync as readFileSync65 } from "node:fs";
36598
36773
  import { join as join89 } from "node:path";
36599
36774
  function readReviewLedger(runDir, storyId) {
36600
36775
  const path = join89(runDir, "events.jsonl");
@@ -36610,6 +36785,7 @@ function readReviewLedger(runDir, storyId) {
36610
36785
  blockedWithNothingRun: false,
36611
36786
  reopened: null,
36612
36787
  asIs: null,
36788
+ lastMerge: null,
36613
36789
  fixRound: null,
36614
36790
  formatRetries: 0,
36615
36791
  formatRefusal: null,
@@ -36631,12 +36807,13 @@ function readReviewLedger(runDir, storyId) {
36631
36807
  let lastDodOutputPath = null;
36632
36808
  let reopened = null;
36633
36809
  let asIs = null;
36810
+ let lastMerge = null;
36634
36811
  let fixRound = null;
36635
36812
  let formatRetries = 0;
36636
36813
  let formatRefusal = null;
36637
36814
  let spawned = null;
36638
36815
  let reviewer = null;
36639
- for (const line of readFileSync64(path, "utf8").split(`
36816
+ for (const line of readFileSync65(path, "utf8").split(`
36640
36817
  `)) {
36641
36818
  if (line.trim() === "")
36642
36819
  continue;
@@ -36698,6 +36875,13 @@ function readReviewLedger(runDir, storyId) {
36698
36875
  commit2 = payload.commit;
36699
36876
  if (typeof payload.epic_base === "string" && payload.epic_base !== "")
36700
36877
  epicBase = payload.epic_base;
36878
+ if (typeof payload.commit === "string" && payload.commit !== "" && typeof payload.epic_base === "string" && payload.epic_base !== "") {
36879
+ lastMerge = {
36880
+ commit: payload.commit,
36881
+ epicBase: payload.epic_base,
36882
+ verdict: typeof payload.verdict === "string" && payload.verdict !== "" ? payload.verdict : "n-a"
36883
+ };
36884
+ }
36701
36885
  if (payload.status === "done")
36702
36886
  fixRound = null;
36703
36887
  asIs = null;
@@ -36761,6 +36945,7 @@ function readReviewLedger(runDir, storyId) {
36761
36945
  blockedWithNothingRun,
36762
36946
  reopened,
36763
36947
  asIs,
36948
+ lastMerge,
36764
36949
  fixRound,
36765
36950
  formatRetries,
36766
36951
  formatRefusal,
@@ -36864,7 +37049,7 @@ function reviewWorkFromBundle(runDir, key) {
36864
37049
  return null;
36865
37050
  let doc;
36866
37051
  try {
36867
- doc = JSON.parse(readFileSync65(path, "utf8"));
37052
+ doc = JSON.parse(readFileSync66(path, "utf8"));
36868
37053
  } catch {
36869
37054
  return null;
36870
37055
  }
@@ -36919,11 +37104,11 @@ function resumableReview(runDir, storyId, status, fresh) {
36919
37104
  }
36920
37105
 
36921
37106
  // src/core/retro/findings.ts
36922
- import { existsSync as existsSync81, readdirSync as readdirSync26, readFileSync as readFileSync71 } from "node:fs";
37107
+ import { existsSync as existsSync81, readdirSync as readdirSync26, readFileSync as readFileSync72 } from "node:fs";
36923
37108
  import { join as join97 } from "node:path";
36924
37109
 
36925
37110
  // src/core/replay/loadRun.ts
36926
- import { existsSync as existsSync76, readdirSync as readdirSync25, readFileSync as readFileSync66, statSync as statSync21 } from "node:fs";
37111
+ import { existsSync as existsSync76, readdirSync as readdirSync25, readFileSync as readFileSync67, statSync as statSync21 } from "node:fs";
36927
37112
  import { join as join91 } from "node:path";
36928
37113
  // src/core/replay/RunDocument.ts
36929
37114
  function toRunDocument(input, fallbackId) {
@@ -37171,7 +37356,7 @@ function loadRunResult(root2, id) {
37171
37356
  return { kind: "missing" };
37172
37357
  let doc;
37173
37358
  try {
37174
- doc = parseYamlRepairing(readFileSync66(runPath, "utf8")).doc;
37359
+ doc = parseYamlRepairing(readFileSync67(runPath, "utf8")).doc;
37175
37360
  } catch (error2) {
37176
37361
  return { kind: "unreadable", dir, error: error2 instanceof Error ? error2.message : String(error2) };
37177
37362
  }
@@ -37182,7 +37367,7 @@ function loadRunResult(root2, id) {
37182
37367
  let budget = null;
37183
37368
  if (existsSync76(budgetPath)) {
37184
37369
  try {
37185
- budget = toBudgetDocument(parseYamlRepairing(readFileSync66(budgetPath, "utf8")).doc);
37370
+ budget = toBudgetDocument(parseYamlRepairing(readFileSync67(budgetPath, "utf8")).doc);
37186
37371
  } catch {
37187
37372
  budget = null;
37188
37373
  }
@@ -37233,7 +37418,7 @@ function readEvents2(dir) {
37233
37418
  function loadPhaseArtefacts(loaded, phase) {
37234
37419
  const read2 = (name) => {
37235
37420
  const path = join91(loaded.dir, phase, name);
37236
- return existsSync76(path) ? readFileSync66(path, "utf8") : null;
37421
+ return existsSync76(path) ? readFileSync67(path, "utf8") : null;
37237
37422
  };
37238
37423
  return { phase, handoff: read2(HANDOFF_FILE2), questions: read2(QUESTIONS_FILE3) };
37239
37424
  }
@@ -37244,7 +37429,7 @@ function loadGateEvidence(loaded, relPath) {
37244
37429
  if (!existsSync76(path))
37245
37430
  return null;
37246
37431
  try {
37247
- return readFileSync66(path, "utf8");
37432
+ return readFileSync67(path, "utf8");
37248
37433
  } catch {
37249
37434
  return null;
37250
37435
  }
@@ -37257,7 +37442,7 @@ function runLevelEvents(loaded) {
37257
37442
  }
37258
37443
 
37259
37444
  // src/core/build/retroLog.ts
37260
- import { existsSync as existsSync77, mkdirSync as mkdirSync17, readFileSync as readFileSync67, writeFileSync as writeFileSync16 } from "node:fs";
37445
+ import { existsSync as existsSync77, mkdirSync as mkdirSync17, readFileSync as readFileSync68, writeFileSync as writeFileSync17 } from "node:fs";
37261
37446
  import { join as join92 } from "node:path";
37262
37447
  var RETRO_FILE = "retro.md";
37263
37448
  var BUILD_RETRO_SECTION = "Build feedback";
@@ -37306,7 +37491,7 @@ function gateRetroLines(runDir2, runId) {
37306
37491
  return [];
37307
37492
  let text3;
37308
37493
  try {
37309
- text3 = readFileSync67(path, "utf8");
37494
+ text3 = readFileSync68(path, "utf8");
37310
37495
  } catch {
37311
37496
  return [];
37312
37497
  }
@@ -37341,7 +37526,7 @@ function appendBuildRetro(runDir2, incoming) {
37341
37526
  if (incoming.length === 0)
37342
37527
  return [];
37343
37528
  const path = buildRetroPath(runDir2);
37344
- const existing = existsSync77(path) ? readFileSync67(path, "utf8") : "";
37529
+ const existing = existsSync77(path) ? readFileSync68(path, "utf8") : "";
37345
37530
  const { before, section: section2 } = splitBuildSection(existing);
37346
37531
  const already = new Set(section2.split(`
37347
37532
  `).map((line) => line.trim()));
@@ -37363,7 +37548,7 @@ function appendBuildRetro(runDir2, incoming) {
37363
37548
  ].join(`
37364
37549
  `);
37365
37550
  mkdirSync17(runDir2, { recursive: true });
37366
- writeFileSync16(path, out, "utf8");
37551
+ writeFileSync17(path, out, "utf8");
37367
37552
  return added;
37368
37553
  }
37369
37554
  function extractBuildSection(text3) {
@@ -37400,7 +37585,7 @@ function str7(value) {
37400
37585
  }
37401
37586
 
37402
37587
  // src/core/retro/renderRetro.ts
37403
- import { existsSync as existsSync79, readFileSync as readFileSync69 } from "node:fs";
37588
+ import { existsSync as existsSync79, readFileSync as readFileSync70 } from "node:fs";
37404
37589
  import { join as join95 } from "node:path";
37405
37590
 
37406
37591
  // src/core/retro/factsFromRun.ts
@@ -37447,7 +37632,7 @@ function str8(value) {
37447
37632
  }
37448
37633
 
37449
37634
  // src/core/retro/proposals.ts
37450
- import { existsSync as existsSync78, readFileSync as readFileSync68 } from "node:fs";
37635
+ import { existsSync as existsSync78, readFileSync as readFileSync69 } from "node:fs";
37451
37636
  import { join as join94 } from "node:path";
37452
37637
  var PROPOSE_STAGE_MARKER = "propose stage:";
37453
37638
  function practiceProposals(loaded) {
@@ -37526,7 +37711,7 @@ function questionsMax(root2, stage2) {
37526
37711
  if (!existsSync78(path))
37527
37712
  return null;
37528
37713
  try {
37529
- const doc = parseYaml(readFileSync68(path, "utf8"));
37714
+ const doc = parseYaml(readFileSync69(path, "utf8"));
37530
37715
  if (typeof doc !== "object" || doc === null)
37531
37716
  return null;
37532
37717
  const questions = doc.questions;
@@ -37618,7 +37803,7 @@ function carryBuildSection(dir) {
37618
37803
  if (!existsSync79(path))
37619
37804
  return "";
37620
37805
  try {
37621
- return extractBuildSection(readFileSync69(path, "utf8"));
37806
+ return extractBuildSection(readFileSync70(path, "utf8"));
37622
37807
  } catch {
37623
37808
  return "";
37624
37809
  }
@@ -37643,7 +37828,7 @@ var OTHER = "other";
37643
37828
  var RULED_CLASSES = FINDING_CLASSES.filter((cls) => cls !== OTHER);
37644
37829
 
37645
37830
  // src/core/retro/findingClasses.ts
37646
- import { existsSync as existsSync80, readFileSync as readFileSync70 } from "node:fs";
37831
+ import { existsSync as existsSync80, readFileSync as readFileSync71 } from "node:fs";
37647
37832
  import { join as join96 } from "node:path";
37648
37833
  var FINDING_CLASSES_FILE = `${PROJECT_FRAMEWORK_DIR}/memory/finding-classes.yml`;
37649
37834
  var MAX_CLASSES = 16;
@@ -37665,7 +37850,7 @@ function loadExtraClasses(root2) {
37665
37850
  return [];
37666
37851
  let text3;
37667
37852
  try {
37668
- text3 = readFileSync70(path, "utf8");
37853
+ text3 = readFileSync71(path, "utf8");
37669
37854
  } catch (error) {
37670
37855
  throw new FindingClassesError(`cannot be read — ${messageOf3(error)}`);
37671
37856
  }
@@ -37841,7 +38026,7 @@ function readText3(path) {
37841
38026
  if (!existsSync81(path))
37842
38027
  return null;
37843
38028
  try {
37844
- return readFileSync71(path, "utf8");
38029
+ return readFileSync72(path, "utf8");
37845
38030
  } catch {
37846
38031
  return null;
37847
38032
  }
@@ -38220,6 +38405,38 @@ function clipDetail(detail) {
38220
38405
  return text3.length <= 1200 ? text3 : `${text3.slice(0, 1197)}…`;
38221
38406
  }
38222
38407
 
38408
+ // src/core/build/dependencyHold.ts
38409
+ function dependencyIsPending(status) {
38410
+ return status === "review" || status === "in_progress";
38411
+ }
38412
+ function decidingHold(holds) {
38413
+ return holds.find((hold) => !dependencyIsPending(hold.status)) ?? holds[0] ?? null;
38414
+ }
38415
+ function dependencyHoldReason(held2) {
38416
+ return held2.status === "blocked" ? `dependency ${held2.id} blocked` : `dependency ${held2.id} is \`${held2.status}\`, not \`done\``;
38417
+ }
38418
+ function dependencyWaitReason(held2) {
38419
+ return `it waits on dependency ${held2.id}, which is \`${held2.status}\` and is offered again by the next ` + `invocation; ${held2.id} \`done\` is what releases it, and no reopen is owed`;
38420
+ }
38421
+ function dependencyWaitLine(storyId, held2) {
38422
+ return ` · ${storyId} waits on ${held2.id}, which is \`${held2.status}\` — left \`todo\`, not \`blocked\`: ` + `it is asked again once ${held2.id} settles`;
38423
+ }
38424
+ function dependencyNamedByHold(reason) {
38425
+ const found = /^dependency (\S+) (?:blocked|is `\w+`, not `done`)$/.exec(reason.trim());
38426
+ return found?.[1] ?? null;
38427
+ }
38428
+ function dependencyHoldOfLog(log) {
38429
+ const lines = log.split(`
38430
+ `);
38431
+ const start = lines.indexOf(WHY_NOT_DONE_HEADING);
38432
+ if (start === -1)
38433
+ return null;
38434
+ const rest = lines.slice(start + 1);
38435
+ const end = rest.findIndex((line) => line.startsWith("## "));
38436
+ const body = (end === -1 ? rest : rest.slice(0, end)).map((line) => line.trim()).filter((line) => line !== "");
38437
+ return body.length === 1 && body[0] !== undefined ? dependencyNamedByHold(body[0]) : null;
38438
+ }
38439
+
38223
38440
  // src/core/facilitator/reviewerModel.ts
38224
38441
  function resolveReviewer(layers) {
38225
38442
  const byStakes = layers.stakes === null ? undefined : layers.byStakes[layers.stakes];
@@ -38326,7 +38543,7 @@ function overShareSentence(ceilingUsd, measuredUsd, stories) {
38326
38543
  if (ratio <= 1)
38327
38544
  return null;
38328
38545
  const over = stories === 1 ? "1 story" : `${String(stories)} stories`;
38329
- return `${over} measured $${round25(measuredUsd).toFixed(2)} against ` + `$${round25(ceilingUsd).toFixed(2)} of spawn ceilings — ${ratio.toFixed(1)}x`;
38546
+ return `${over} measured $${round23(measuredUsd).toFixed(2)} against ` + `$${round23(ceilingUsd).toFixed(2)} of spawn ceilings — ${ratio.toFixed(1)}x`;
38330
38547
  }
38331
38548
  function sumOrNull(values) {
38332
38549
  if (values.length === 0)
@@ -38344,10 +38561,10 @@ function sumOrNull(values) {
38344
38561
  import { basename as basename13 } from "node:path";
38345
38562
 
38346
38563
  // src/core/budget/loadBudget.ts
38347
- import { existsSync as existsSync82, readFileSync as readFileSync72 } from "node:fs";
38564
+ import { existsSync as existsSync82, readFileSync as readFileSync73 } from "node:fs";
38348
38565
  import { join as join98 } from "node:path";
38349
38566
  function loadBudget(path) {
38350
- const doc = parseYaml(readFileSync72(path, "utf8"));
38567
+ const doc = parseYaml(readFileSync73(path, "utf8"));
38351
38568
  const validation = validateRunBudget(doc);
38352
38569
  noteDeprecations(path, validation);
38353
38570
  if (!validation.ok) {
@@ -38794,7 +39011,7 @@ function phaseCostToDate(runDir2, phaseId, stageId, invocationUsd, invocationTur
38794
39011
  turns = [
38795
39012
  ...turns,
38796
39013
  ...invocationTurns.map((task2) => ({
38797
- costUsd: task2.metered === false ? null : round25(task2.costUsd),
39014
+ costUsd: task2.metered === false ? null : round23(task2.costUsd),
38798
39015
  metered: task2.metered !== false,
38799
39016
  tokens: turnTokens({
38800
39017
  tokens: task2.tokens,
@@ -38812,12 +39029,12 @@ function phaseCostToDate(runDir2, phaseId, stageId, invocationUsd, invocationTur
38812
39029
  const tally = { unmetered, metered: turns.length - unmetered };
38813
39030
  if (recorded === null) {
38814
39031
  return {
38815
- usd: round25(invocationUsd),
39032
+ usd: round23(invocationUsd),
38816
39033
  note: "this invocation only — `run.yml` could not be read for what the stage spent before it" + (note === null ? "" : `; ${note}`),
38817
39034
  ...tally
38818
39035
  };
38819
39036
  }
38820
- return { usd: round25(recorded + invocationUsd), note, ...tally };
39037
+ return { usd: round23(recorded + invocationUsd), note, ...tally };
38821
39038
  }
38822
39039
 
38823
39040
  // src/core/facilitator/executors/build.ts
@@ -38947,7 +39164,7 @@ async function rederiveImplicitPlan(ctx, workspace, parts, lines) {
38947
39164
  const kept = (why) => {
38948
39165
  lines.push(` · kept ${IMPLICIT_PLAN_REL} (--discard-pending re-derives only an unbuilt plan): ${why}`);
38949
39166
  };
38950
- const blocker = implicitPlanIsStale(readFileSync73(implicitPlanPath(ctx.runDir), "utf8"));
39167
+ const blocker = implicitPlanIsStale(readFileSync74(implicitPlanPath(ctx.runDir), "utf8"));
38951
39168
  if (blocker !== null) {
38952
39169
  kept(blocker);
38953
39170
  return;
@@ -39015,6 +39232,7 @@ class BuildSession {
39015
39232
  aside = [];
39016
39233
  preflight;
39017
39234
  lanes;
39235
+ waits = new Map;
39018
39236
  constructor(ctx, workspace, plan, opening = []) {
39019
39237
  this.ctx = ctx;
39020
39238
  this.workspace = workspace;
@@ -39028,6 +39246,9 @@ class BuildSession {
39028
39246
  }
39029
39247
  if (plan.priceIssue !== null)
39030
39248
  this.advisories.push(plan.priceIssue);
39249
+ const over = planOverStageAdvisory(this.capParts, this.capLever);
39250
+ if (over !== null)
39251
+ this.advisories.push(over);
39031
39252
  }
39032
39253
  async runAll() {
39033
39254
  const refusal = await this.refuseOnDirtyRepos() ?? await this.refuseOnForeignEpic() ?? await this.setAsideForeign() ?? await this.refuseOnRedBase() ?? await this.refuseOnUnrunnableWorktree();
@@ -39044,13 +39265,22 @@ class BuildSession {
39044
39265
  pending.push(planned);
39045
39266
  continue;
39046
39267
  }
39268
+ const released = status === "blocked" ? this.staleDependencyHold(planned) : null;
39269
+ if (released !== null) {
39270
+ this.lines.push(` · ${planned.story.id} was \`blocked\` for dependency ${released}, which is now \`done\` — ` + "that was never an attempt, so it is offered again");
39271
+ pending.push(planned);
39272
+ continue;
39273
+ }
39047
39274
  if (status === "done" || status === "blocked") {
39048
39275
  this.lines.push(` · ${planned.story.id} is already \`${status}\` — left alone`);
39049
39276
  continue;
39050
39277
  }
39051
39278
  const held2 = this.blockingDependency(planned);
39052
39279
  if (held2 !== null) {
39053
- this.blockOnDependency(planned, held2);
39280
+ if (dependencyIsPending(held2.status))
39281
+ this.waitOnDependency(planned, held2);
39282
+ else
39283
+ this.blockOnDependency(planned, held2);
39054
39284
  continue;
39055
39285
  }
39056
39286
  this.noteIfReopened(planned, status);
@@ -39286,7 +39516,7 @@ class BuildSession {
39286
39516
  }
39287
39517
  const story = await this.openStory(planned);
39288
39518
  const declared2 = this.ctx.costUsd ?? result2.cost_usd;
39289
- const cost = declared2 === null ? null : round25(declared2);
39519
+ const cost = declared2 === null ? null : round23(declared2);
39290
39520
  this.tasks.push({
39291
39521
  key: planned.story.id,
39292
39522
  model: this.model(),
@@ -39430,7 +39660,22 @@ class BuildSession {
39430
39660
  };
39431
39661
  }
39432
39662
  const ahead = await commitsBetween(story.repoDir, story.epicBranch, story.branch);
39433
- if (ahead === null || ahead === 0) {
39663
+ if (ahead === 0) {
39664
+ const merge = readReviewLedger(this.ctx.runDir, id).lastMerge;
39665
+ if (merge !== null && reviewNeverCompleted(merge.verdict)) {
39666
+ return await this.reviewOnlyHalf(planned, asIs, story, before, merge);
39667
+ }
39668
+ return {
39669
+ story,
39670
+ cost: 0,
39671
+ dod: [],
39672
+ commit: null,
39673
+ failure: asIsNotAheadReason(story.branch, story.epicBranch, ahead, merge === null ? undefined : merge),
39674
+ developerError: null,
39675
+ before
39676
+ };
39677
+ }
39678
+ if (ahead === null) {
39434
39679
  return {
39435
39680
  story,
39436
39681
  cost: 0,
@@ -39468,6 +39713,27 @@ class BuildSession {
39468
39713
  }
39469
39714
  return { story, cost: 0, dod, commit: commit2, failure: null, developerError: null, before };
39470
39715
  }
39716
+ async reviewOnlyHalf(planned, asIs, story, before, merge) {
39717
+ const id = planned.story.id;
39718
+ const reviewed = { commit: merge.commit, epicBase: merge.epicBase };
39719
+ this.asIsSettlements.set(id, { ...asIs, reason: "review-only", reviewed });
39720
+ this.lines.push(` · ${id}: ${AS_IS_REVIEW_ONLY_MARK} — \`${merge.commit.slice(0, 7)}\` is already on ` + `\`${story.epicBranch}\` and nothing judged it; running the dod on the epic head and the review ` + `over \`${reviewDiffRange(merge.epicBase, story.epicBranch, story.branch)}\``);
39721
+ await this.writes.run(() => this.refreshStoryBase(planned, story.repoDir, story.worktree, story.branch, story.epicBranch));
39722
+ const dod = await this.runDod(story);
39723
+ if (!this.dodProves(dod)) {
39724
+ const failing = dodFailure(dod);
39725
+ return {
39726
+ story,
39727
+ cost: 0,
39728
+ dod,
39729
+ commit: null,
39730
+ failure: failing === undefined ? "the story declares no dod commands, so nothing could prove it" : dodFailureReason(failing, planned.story.repo),
39731
+ developerError: null,
39732
+ before
39733
+ };
39734
+ }
39735
+ return { story, cost: 0, dod, commit: merge.commit, failure: null, developerError: null, before, reviewOnly: reviewed };
39736
+ }
39471
39737
  async buildHalf(planned) {
39472
39738
  const asIs = this.asIsFor(planned);
39473
39739
  if (asIs !== null)
@@ -39507,7 +39773,7 @@ class BuildSession {
39507
39773
  let spent = 0;
39508
39774
  let separatorRetries = 0;
39509
39775
  for (;; ) {
39510
- spent = round25(spent + developer.cost);
39776
+ spent = round23(spent + developer.cost);
39511
39777
  const capDeath = developer.error !== null && diedOnCap(developer.error);
39512
39778
  if (developer.error !== null) {
39513
39779
  const proven2 = capDeath && await workSince({
@@ -39572,7 +39838,7 @@ class BuildSession {
39572
39838
  failure: [
39573
39839
  why,
39574
39840
  ...developer.refused === null ? [] : [permissionBlockReason(developer.refused, { declared: this.repoCommands(story.planned.story.repo) })],
39575
- ...budgetDeath === null ? [] : [capDeathReason(budgetDeath)]
39841
+ ...budgetDeath === null ? [] : [capDeathReason(budgetDeath, this.capParts, story.planned.story.id, story.attempt, this.capLever)]
39576
39842
  ].join("; and "),
39577
39843
  developerError: null,
39578
39844
  before
@@ -39606,6 +39872,10 @@ class BuildSession {
39606
39872
  await this.block(story, "the story produced no commit to review", half.cost, dod);
39607
39873
  return "settled";
39608
39874
  }
39875
+ if (half.reviewOnly !== undefined) {
39876
+ this.noteMerged(story, null);
39877
+ return await this.reviewAndSettle(story, dod, half.reviewOnly.commit, half.cost, null, half.reviewOnly.epicBase);
39878
+ }
39609
39879
  const update = await updateStoryBase({
39610
39880
  storyId: story.planned.story.id,
39611
39881
  repoDir: story.repoDir,
@@ -39665,14 +39935,14 @@ class BuildSession {
39665
39935
  epicBase,
39666
39936
  verdict: "n-a",
39667
39937
  review: review2,
39668
- cost: round25(priorCost),
39938
+ cost: round23(priorCost),
39669
39939
  reason: review2.summary
39670
39940
  });
39671
39941
  return "settled";
39672
39942
  }
39673
39943
  const outcome = supplied === undefined ? await this.spawnReviewer(story, dod, epicBase) : { review: supplied, cost: 0 };
39674
39944
  const review = outcome.review;
39675
- const cost = round25(priorCost + outcome.cost);
39945
+ const cost = round23(priorCost + outcome.cost);
39676
39946
  if (review.verdict === "error") {
39677
39947
  await this.settle(story, "review", {
39678
39948
  dod,
@@ -39690,6 +39960,21 @@ class BuildSession {
39690
39960
  if (review.verdict === "fixlist") {
39691
39961
  const rel = this.writeFixlistFor(story, review, commit2, epicBase);
39692
39962
  const open2 = openFindings(review.fixlist).length;
39963
+ if (open2 === 0) {
39964
+ this.lines.push(` · ${story.planned.story.id}: ${FIXLIST_SETTLED_MARK} (${rel})`);
39965
+ await this.settle(story, "done", {
39966
+ dod,
39967
+ commit: commit2,
39968
+ merged: true,
39969
+ carried,
39970
+ epicBase,
39971
+ verdict: "fixlist",
39972
+ review,
39973
+ cost,
39974
+ reason: null
39975
+ });
39976
+ return "settled";
39977
+ }
39693
39978
  await this.settle(story, "review", {
39694
39979
  dod,
39695
39980
  commit: commit2,
@@ -39789,17 +40074,17 @@ class BuildSession {
39789
40074
  }
39790
40075
  findings3.push({ ...finding2, resolved: false, resolvedSha: null });
39791
40076
  refused2.push(`#${String(finding2.n)} — ${why}`);
39792
- text3 = markUnverified(text3 ?? readFileSync73(fixlist.path, "utf8"), finding2.n, why);
40077
+ text3 = markUnverified(text3 ?? readFileSync74(fixlist.path, "utf8"), finding2.n, why);
39793
40078
  this.lines.push(` · ${id}: fix-list finding #${String(finding2.n)} claimed \`Resolved: yes\` and ` + `${why} — recorded as \`${CLAIMED_UNVERIFIED}\`, and it still blocks \`done\``);
39794
40079
  }
39795
- const canonicalized = await canonicalizeResolutions(story.repoDir, findings3, text3 ?? readFileSync73(fixlist.path, "utf8"));
40080
+ const canonicalized = await canonicalizeResolutions(story.repoDir, findings3, text3 ?? readFileSync74(fixlist.path, "utf8"));
39796
40081
  if (canonicalized.lines.length > 0) {
39797
40082
  text3 = canonicalized.text;
39798
40083
  for (const line of canonicalized.lines)
39799
40084
  this.lines.push(` · ${id}: ${line}`);
39800
40085
  }
39801
40086
  if (text3 !== null)
39802
- writeFileSync17(fixlist.path, text3, "utf8");
40087
+ writeFileSync18(fixlist.path, text3, "utf8");
39803
40088
  return { findings: canonicalized.findings, refused: refused2 };
39804
40089
  }
39805
40090
  async unverifiedBecause(story, sha) {
@@ -39891,19 +40176,36 @@ class BuildSession {
39891
40176
  return wave.stories.some((planned) => this.statusOf(planned) === "blocked");
39892
40177
  }
39893
40178
  blockingDependency(planned) {
40179
+ const holds = [];
39894
40180
  for (const id of planned.story.depends_on) {
39895
40181
  const dependency = this.plan.stories.get(id);
39896
40182
  if (dependency === undefined)
39897
40183
  continue;
39898
40184
  const status = this.statusOf(dependency);
39899
40185
  if (status !== "done")
39900
- return { id, status };
40186
+ holds.push({ id, status });
39901
40187
  }
39902
- return null;
40188
+ return decidingHold(holds);
40189
+ }
40190
+ waitOnDependency(planned, held2) {
40191
+ this.waits.set(planned.story.id, held2);
40192
+ this.lines.push(dependencyWaitLine(planned.story.id, held2));
40193
+ }
40194
+ staleDependencyHold(planned) {
40195
+ const path = join99(this.ctx.runDir, BUILD_PHASE, LOG_DIR, `${planned.story.id}.md`);
40196
+ let named;
40197
+ try {
40198
+ named = dependencyHoldOfLog(readFileSync74(path, "utf8"));
40199
+ } catch {
40200
+ return null;
40201
+ }
40202
+ if (named === null)
40203
+ return null;
40204
+ return this.blockingDependency(planned) === null ? named : null;
39903
40205
  }
39904
40206
  blockOnDependency(planned, held2) {
39905
40207
  const id = planned.story.id;
39906
- const reason = held2.status === "blocked" ? `dependency ${held2.id} blocked` : `dependency ${held2.id} is \`${held2.status}\`, not \`done\``;
40208
+ const reason = dependencyHoldReason(held2);
39907
40209
  const epic = this.plan.epics.get(planned.story.epic);
39908
40210
  const outcome = {
39909
40211
  id,
@@ -40076,7 +40378,7 @@ class BuildSession {
40076
40378
  this.tasks.push({
40077
40379
  key: story.planned.story.id,
40078
40380
  model: this.model(),
40079
- costUsd: round25(agent.costUsd),
40381
+ costUsd: round23(agent.costUsd),
40080
40382
  sessionId: agent.sessionId,
40081
40383
  error: agent.error,
40082
40384
  role: "developer",
@@ -40086,7 +40388,7 @@ class BuildSession {
40086
40388
  durationMs: agent.durationMs
40087
40389
  });
40088
40390
  if (agent.ok) {
40089
- return { cost: round25(agent.costUsd), error: null, refused: agent.permissionRefusal };
40391
+ return { cost: round23(agent.costUsd), error: null, refused: agent.permissionRefusal };
40090
40392
  }
40091
40393
  const error = (agent.error ?? "").trim() === "" ? DEVELOPER_FAILED : (agent.error ?? "").trim();
40092
40394
  this.ctx.emit("check.failed", {
@@ -40097,7 +40399,7 @@ class BuildSession {
40097
40399
  attempt: story.attempt,
40098
40400
  detail: error
40099
40401
  });
40100
- return { cost: round25(agent.costUsd), error, refused: null };
40402
+ return { cost: round23(agent.costUsd), error, refused: null };
40101
40403
  }
40102
40404
  dodProves(dod) {
40103
40405
  return dod.length === 0 ? dodIsSatisfiedEmpty(this.plan) : dodGreen({ dod });
@@ -40268,7 +40570,7 @@ class BuildSession {
40268
40570
  });
40269
40571
  const parsed = agent.ok ? parseReview(agent.structured, agent.result) : reviewerFailed(agent.error);
40270
40572
  const review = this.narrowFixlist(id, parsed);
40271
- const turn = round25(agent.costUsd);
40573
+ const turn = round23(agent.costUsd);
40272
40574
  const again = this.formatRetry(story, review, {
40273
40575
  costUsd: turn,
40274
40576
  sessionId: agent.sessionId,
@@ -40278,7 +40580,7 @@ class BuildSession {
40278
40580
  });
40279
40581
  if (again !== null) {
40280
40582
  refusal = again;
40281
- spent = round25(spent + turn);
40583
+ spent = round23(spent + turn);
40282
40584
  continue;
40283
40585
  }
40284
40586
  this.recordReview(story, review, {
@@ -40291,7 +40593,7 @@ class BuildSession {
40291
40593
  source: "agent",
40292
40594
  reviewer: { model: reviewer.model, effort: reviewer.effort, basis: "spawned" }
40293
40595
  });
40294
- return { review, cost: round25(spent + turn) };
40596
+ return { review, cost: round23(spent + turn) };
40295
40597
  }
40296
40598
  }
40297
40599
  formatRetry(story, review, task2) {
@@ -40492,17 +40794,18 @@ class BuildSession {
40492
40794
  ...outcome.permissionRefused == null ? {} : { permission_refused: outcome.permissionRefused },
40493
40795
  ...outcome.budgetDeath == null ? {} : { budget_death: outcome.budgetDeath },
40494
40796
  ...outcome.reviewerUnfunded == null ? {} : { reviewer_unfunded: outcome.reviewerUnfunded },
40495
- ...outcome.asIs == null ? {} : { as_is: true, as_is_by: outcome.asIs.actor, as_is_note: outcome.asIs.note }
40797
+ ...outcome.asIs == null ? {} : { as_is: true, as_is_by: outcome.asIs.actor, as_is_note: outcome.asIs.note },
40798
+ ...outcome.asIs?.reason === undefined ? {} : { as_is_reason: outcome.asIs.reason }
40496
40799
  });
40497
40800
  if (pruning && (rescued === null || rescued.worktree === null)) {
40498
40801
  await removeWorktree(story.repoDir, story.worktree);
40499
40802
  }
40500
- this.lines.push(` ${status === "done" ? "✓" : "·"} ${id} → \`${status}\`` + (outcome.reason === null ? "" : ` (${outcome.reason})`) + (outcome.asIs == null ? "" : ` — ${AS_IS_MARK}; ${outcome.asIs.actor} signed it`) + (outcome.permissionRefused == null ? "" : ` — ${withCure(`\`${outcome.permissionRefused}\` was refused for approval; the tree held committed work, so the DoD decided`, outcome.permissionRefused, outcome.declaredCommands)}`));
40803
+ this.lines.push(` ${status === "done" ? "✓" : "·"} ${id} → \`${status}\`` + (outcome.reason === null ? "" : ` (${outcome.reason})`) + (outcome.asIs == null ? "" : ` — ${outcome.asIs.reason === "review-only" ? AS_IS_REVIEW_ONLY_MARK : AS_IS_MARK}; ` + `${outcome.asIs.actor} signed it`) + (outcome.permissionRefused == null ? "" : ` — ${withCure(`\`${outcome.permissionRefused}\` was refused for approval; the tree held committed work, so the DoD decided`, outcome.permissionRefused, outcome.declaredCommands)}`));
40501
40804
  }
40502
40805
  writeLog(outcome) {
40503
40806
  const dir = join99(this.ctx.runDir, BUILD_PHASE, LOG_DIR);
40504
40807
  mkdirSync18(dir, { recursive: true });
40505
- writeFileSync17(join99(dir, `${outcome.id}.md`), renderReviewLog(outcome), "utf8");
40808
+ writeFileSync18(join99(dir, `${outcome.id}.md`), renderReviewLog(outcome), "utf8");
40506
40809
  }
40507
40810
  recordStoryFeedback(outcome) {
40508
40811
  appendBuildRetro(this.ctx.runDir, storyRetroLines(outcome, this.ctx.runId));
@@ -40546,11 +40849,12 @@ class BuildSession {
40546
40849
  for (const planned of wave.stories) {
40547
40850
  if (named.has(planned.story.id))
40548
40851
  continue;
40852
+ const wait = this.waits.get(planned.story.id);
40549
40853
  rows.push({
40550
40854
  id: planned.story.id,
40551
40855
  rel: this.plan.implicit ? IMPLICIT_PLAN_REL : planned.rel,
40552
40856
  status: this.statusOf(planned),
40553
- reason: "this stage recorded no attempt and no reason for it — " + "nothing here says the work was done, and nothing says why it was not"
40857
+ reason: wait !== undefined ? dependencyWaitReason(wait) : "this stage recorded no attempt and no reason for it — " + "nothing here says the work was done, and nothing says why it was not"
40554
40858
  });
40555
40859
  }
40556
40860
  }
@@ -40561,7 +40865,7 @@ class BuildSession {
40561
40865
  mkdirSync18(join99(path, ".."), { recursive: true });
40562
40866
  const cost = phaseCostToDate(this.ctx.runDir, this.ctx.phaseId, this.ctx.stageId, this.spent(), this.tasks, storySpendToDate(this.ctx.runDir, this.ctx.phaseId, this.ctx.stageId, this.tasks));
40563
40867
  const carried = this.carriedRows();
40564
- writeFileSync17(path, renderBuildHandoff({
40868
+ writeFileSync18(path, renderBuildHandoff({
40565
40869
  runId: this.ctx.runId,
40566
40870
  stageId: this.ctx.stageId,
40567
40871
  model: this.model(),
@@ -40594,7 +40898,7 @@ class BuildSession {
40594
40898
  }
40595
40899
  wideningRows() {
40596
40900
  try {
40597
- return wideningRows(readFileSync73(join99(this.ctx.runDir, EVENTS_FILE2), "utf8"));
40901
+ return wideningRows(readFileSync74(join99(this.ctx.runDir, EVENTS_FILE2), "utf8"));
40598
40902
  } catch {
40599
40903
  return [];
40600
40904
  }
@@ -40812,7 +41116,7 @@ class BuildSession {
40812
41116
  ` ${namePaths(row2.paths)}`,
40813
41117
  " Commit them, or set them aside by hand, then run " + `\`${relaunchCommand(this.ctx.mode, this.ctx.runId)}\`.`
40814
41118
  ],
40815
- error: `repo \`${row2.repo}\` has uncommitted changes that could not be set aside`
41119
+ error: `repo \`${row2.repo}\` has uncommitted changes that could not be set aside: ` + `${outcome.reason ?? "no reason given"}`
40816
41120
  };
40817
41121
  }
40818
41122
  this.ctx.emit(FOREIGN_ASIDE_EVENT, asidePayload(outcome.stash, reason));
@@ -40823,7 +41127,7 @@ class BuildSession {
40823
41127
  async restoreForeignWorkAside() {
40824
41128
  let open;
40825
41129
  try {
40826
- open = pendingAsides(readFileSync73(join99(this.ctx.runDir, EVENTS_FILE2), "utf8"), (repo) => {
41130
+ open = pendingAsides(readFileSync74(join99(this.ctx.runDir, EVENTS_FILE2), "utf8"), (repo) => {
40827
41131
  try {
40828
41132
  return repoDirOf(this.workspace, repo);
40829
41133
  } catch {
@@ -40852,7 +41156,7 @@ class BuildSession {
40852
41156
  const status = this.statusOf(planned);
40853
41157
  if (status === "done")
40854
41158
  continue;
40855
- if (status === "blocked" && this.blockedByFailedDeveloper(planned) === null)
41159
+ if (status === "blocked" && this.blockedByFailedDeveloper(planned) === null && this.staleDependencyHold(planned) === null)
40856
41160
  continue;
40857
41161
  rows.push(planned);
40858
41162
  }
@@ -40870,16 +41174,16 @@ class BuildSession {
40870
41174
  if (fresh !== undefined)
40871
41175
  return fresh.status;
40872
41176
  try {
40873
- const value = /^status\s*:\s*(\w+)\s*$/m.exec(readFileSync73(planned.path, "utf8"))?.[1];
41177
+ const value = /^status\s*:\s*(\w+)\s*$/m.exec(readFileSync74(planned.path, "utf8"))?.[1];
40874
41178
  return isPlanStatus2(value) ? value : planned.story.status;
40875
41179
  } catch {
40876
41180
  return planned.story.status;
40877
41181
  }
40878
41182
  }
40879
41183
  setStoryStatus(planned, status, evidence) {
40880
- const text3 = readFileSync73(planned.path, "utf8");
41184
+ const text3 = readFileSync74(planned.path, "utf8");
40881
41185
  const patched = this.plan.implicit ? updateImplicitPlan(text3, { status, evidence }) : updateStoryFront(text3, { status, evidence });
40882
- writeFileSync17(planned.path, patched, "utf8");
41186
+ writeFileSync18(planned.path, patched, "utf8");
40883
41187
  }
40884
41188
  updateEpicStatus(epic) {
40885
41189
  const statuses = epic.epic.stories.map((id) => {
@@ -40887,7 +41191,7 @@ class BuildSession {
40887
41191
  return planned === undefined ? "todo" : this.statusOf(planned);
40888
41192
  });
40889
41193
  const status = statuses.every((s) => s === "done") ? "done" : statuses.some((s) => s === "blocked") ? "blocked" : "in_progress";
40890
- writeFileSync17(epic.path, updateStoryFront(readFileSync73(epic.path, "utf8"), { status }), "utf8");
41194
+ writeFileSync18(epic.path, updateStoryFront(readFileSync74(epic.path, "utf8"), { status }), "utf8");
40891
41195
  return status;
40892
41196
  }
40893
41197
  noteMerged(story, carried) {
@@ -40957,7 +41261,7 @@ class BuildSession {
40957
41261
  if (!existsSync83(path))
40958
41262
  return null;
40959
41263
  try {
40960
- const doc = JSON.parse(readFileSync73(path, "utf8"));
41264
+ const doc = JSON.parse(readFileSync74(path, "utf8"));
40961
41265
  return typeof doc.session_id === "string" && doc.session_id !== "" ? doc.session_id : null;
40962
41266
  } catch {
40963
41267
  return null;
@@ -41021,6 +41325,11 @@ class BuildSession {
41021
41325
  storyCapFloorUsd: this.ctx.spec.tuning.storyCapFloorUsd
41022
41326
  };
41023
41327
  }
41328
+ get capLever() {
41329
+ return {
41330
+ raiseCommand: (usd3) => stageRaiseCommand(this.ctx.runId, this.ctx.phaseId, this.ctx.stageId, usd3)
41331
+ };
41332
+ }
41024
41333
  get attempts() {
41025
41334
  return this.ctx.spec.tuning.attempts;
41026
41335
  }
@@ -41028,7 +41337,7 @@ class BuildSession {
41028
41337
  return this.ctx.spec.tuning.fixlistRounds;
41029
41338
  }
41030
41339
  spent() {
41031
- return round25(this.tasks.reduce((sum, task2) => sum + task2.costUsd, 0));
41340
+ return round23(this.tasks.reduce((sum, task2) => sum + task2.costUsd, 0));
41032
41341
  }
41033
41342
  logPaths() {
41034
41343
  return [...this.outcomes.values()].map((outcome) => outcome.reviewRel);
@@ -41141,7 +41450,7 @@ class BuildSession {
41141
41450
  const path = join99(this.ctx.runDir, BUILD_PHASE, LOG_DIR, `${storyId}.md`);
41142
41451
  if (!this.hasPriorAttempt(storyId) || !existsSync83(path))
41143
41452
  return "";
41144
- return readFileSync73(path, "utf8").trimEnd().split(`
41453
+ return readFileSync74(path, "utf8").trimEnd().split(`
41145
41454
  `).map((line) => `> ${line}`).join(`
41146
41455
  `);
41147
41456
  }
@@ -41190,9 +41499,6 @@ function permissionBlockReason(command2, options = {}) {
41190
41499
  function diedOnCap(error) {
41191
41500
  return error.includes("Reached maximum budget");
41192
41501
  }
41193
- function capDeathReason(error) {
41194
- return `the developer died on its per-story cap — ${error}: the ceiling is derived from ` + "`03-plan/budget.yml`'s price for this story, so a story that really costs more than the " + "plan guessed wants a higher price there or a higher `story_cap_multiplier:` on the stage";
41195
- }
41196
41502
  var REVIEWER_TOOLS = ["Read", "Grep", "Glob", "Bash(git diff *)"];
41197
41503
  function summaryOf(envelope) {
41198
41504
  return typeof envelope.summary === "string" ? envelope.summary : "";
@@ -41221,7 +41527,7 @@ function failed3(ctx, error, tasks) {
41221
41527
  ok: false,
41222
41528
  awaiting: false,
41223
41529
  tasks,
41224
- costUsd: round25(tasks.reduce((sum, task2) => sum + task2.costUsd, 0)),
41530
+ costUsd: round23(tasks.reduce((sum, task2) => sum + task2.costUsd, 0)),
41225
41531
  outputs: [],
41226
41532
  lines: [`${ctx.phaseId}/${ctx.stageId} failed: ${error}`],
41227
41533
  error
@@ -41401,7 +41707,7 @@ function preparedRefusal(store, options, notes) {
41401
41707
  ` finish it: run ${where}/prompt.md, write ${where}/result.json, then \`tldrx next --commit ${store.runId}\``,
41402
41708
  ` drop it: \`tldrx reject --run ${store.runId} --note "…"\``,
41403
41709
  ` redo it: \`tldrx next --discard-pending\` (throws the bundle away and runs the stage again)`
41404
- ]);
41710
+ ], [], `${entry.phase.id}/${entry.stage.id} has a --prepare bundle waiting and nothing is holding the run`);
41405
41711
  }
41406
41712
  function demoteStaleRunning(store, deadPid) {
41407
41713
  const stuck = [];
@@ -41547,7 +41853,8 @@ async function runStage(store, options, phaseId, stageId, spec, notes) {
41547
41853
  if (options.mode === "prepare" || options.dryRun)
41548
41854
  notes.push(...renderLedger(ledger4));
41549
41855
  if (ledger4.overLimit) {
41550
- return out(EXIT_REFUSED2, [...notes, ...renderRefusal(ledger4, stageId)]);
41856
+ const refusal = renderRefusal(ledger4, stageId);
41857
+ return out(EXIT_REFUSED2, [...notes, ...refusal], [], refusal[0]);
41551
41858
  }
41552
41859
  const providerAdvisory = providerBudgetAdvisory(agentProvider(), cap2);
41553
41860
  const advisories = [
@@ -41715,7 +42022,7 @@ function saveOverflow(store, options, phaseId, type, field, text3) {
41715
42022
  const relPath = `${phaseId}/${LOG_DIR}/overflow/${stamp2}-${String(overflowSeq)}-${type}-${field}.txt`;
41716
42023
  try {
41717
42024
  mkdirSync19(join100(store.runDir, phaseId, LOG_DIR, "overflow"), { recursive: true });
41718
- writeFileSync18(join100(store.runDir, relPath), text3, "utf8");
42025
+ writeFileSync19(join100(store.runDir, relPath), text3, "utf8");
41719
42026
  return `omitted text saved at ${relPath}`;
41720
42027
  } catch (error) {
41721
42028
  return `could not be saved: ${error instanceof Error ? error.message : String(error)}`;
@@ -41804,7 +42111,7 @@ async function preconditionRefusal(store, options, phaseId, stageId, spec, notes
41804
42111
  `refusing to dispatch ${phaseId}/${stageId} — precondition \`${precondition.id}\` is red.`,
41805
42112
  ` ${ran.detail}`,
41806
42113
  `Fix it and run the same command again: the stage is still \`${stageStatusOf(store, phaseId, stageId)}\` and nothing was spent.`
41807
- ]);
42114
+ ], [], `${phaseId}/${stageId}: precondition \`${precondition.id}\` is red — ${ran.detail}`);
41808
42115
  }
41809
42116
  return null;
41810
42117
  }
@@ -41830,7 +42137,7 @@ function economyRefusal(store, options, phaseId, stageId, notes) {
41830
42137
  `refusing to spawn — ${phaseId} is priced in \`host-tokens\` ` + `($${ceiling.toFixed(2)} is not dollars a spawn may`,
41831
42138
  "spend) and this invocation is headless. Either run it in-session (tldrx next --prepare), or set the",
41832
42139
  `phase to \`economy: metered-usd\` and re-price it (tldrx budget raise ${phaseId} <usd>).`
41833
- ]);
42140
+ ], [], `${phaseId}/${stageId}: refusing to spawn — ${phaseId} is priced in \`host-tokens\` ($${ceiling.toFixed(2)}) and this invocation is headless`);
41834
42141
  }
41835
42142
  function budgetRefusal(store, options, phaseId, stageId, spec, notes) {
41836
42143
  const stage2 = requireStage(store, phaseId, stageId);
@@ -41870,7 +42177,7 @@ function budgetRefusal(store, options, phaseId, stageId, spec, notes) {
41870
42177
  ...work.basis === "plan" ? [renderRemainingWork(work), ...remainingWorkContext(work)] : [],
41871
42178
  `Run \`${fix}\` (add \`--take-from <phase>\` to move the money instead of adding it), ` + `lower budget_usd in the stage, or set on_exceed: warn.`,
41872
42179
  `See the whole picture first: \`tldrx budget show --run ${store.runId}\`.`
41873
- ]);
42180
+ ], [], `${phaseId}/${stageId}: phase ${phaseId} has $${phaseRemaining.toFixed(2)} left and the estimate is $${estimate.toFixed(2)}`);
41874
42181
  }
41875
42182
  warnOnce(store, options, phaseId, stageId, estimate, phaseRemaining, spec.tuning.attempts, notes);
41876
42183
  return null;
@@ -41986,7 +42293,7 @@ async function runExecutor(store, options, phaseId, stageId, spec, notes, execut
41986
42293
  mapStage2(store, phaseId, stageId, () => stageOnEntry);
41987
42294
  store.save();
41988
42295
  }
41989
- return out(EXIT_USAGE3, [...notes, ...outcome.lines]);
42296
+ return out(EXIT_USAGE3, [...notes, ...outcome.lines], [], executorSignature(outcome));
41990
42297
  }
41991
42298
  claimEpicBranches(store, outcome.epicBranches, outcome.branchModel);
41992
42299
  try {
@@ -42008,7 +42315,7 @@ async function runExecutor(store, options, phaseId, stageId, spec, notes, execut
42008
42315
  if (outcome.refused === true) {
42009
42316
  setStatus(store, phaseId, stageId, "ready");
42010
42317
  store.save();
42011
- return out(EXIT_REFUSED2, [...notes, ...outcome.lines]);
42318
+ return out(EXIT_REFUSED2, [...notes, ...outcome.lines], [], executorSignature(outcome));
42012
42319
  }
42013
42320
  if (!outcome.ok) {
42014
42321
  return failStage(store, options, phaseId, stageId, outcome.error ?? "the executor failed", notes);
@@ -42140,7 +42447,7 @@ async function commitStage(store, options, phaseId, stageId, spec, notes) {
42140
42447
  `${phaseId}/questions.md has ${unreadable.length} question(s) the parser cannot read ` + `(${unreadable.join(", ")}) — a heading must be \`## Qn · <title>\` with the ` + "`<!-- id: Qn | status: open | area: … | asked_by: … | asked_at: … -->` line under it.",
42141
42448
  'As written they are invisible: the gate would read this file as "0 open" and sign itself.',
42142
42449
  `Fix: \`tldrx questions lint --run ${store.runId} --fix\`, then \`tldrx next --commit\` again.`
42143
- ]);
42450
+ ], [], `${phaseId}/questions.md has ${String(unreadable.length)} unreadable question(s): ${unreadable.join(", ")}`);
42144
42451
  }
42145
42452
  return await finishStage(store, options, phaseId, stageId, spec, notes);
42146
42453
  }
@@ -42378,7 +42685,7 @@ function failStage(store, options, phaseId, stageId, reason, notes, tasksBefore)
42378
42685
  ...notes,
42379
42686
  `${phaseId}/${stageId} failed: ${oneLine7(reason)}`,
42380
42687
  `cost is recorded, not refunded — retry with \`tldrx next\`, or \`tldrx reject --note "…"\``
42381
- ]);
42688
+ ], [], `${phaseId}/${stageId} failed: ${oneLine7(reason)}`);
42382
42689
  }
42383
42690
  function seedInputsOf(spec, stage2, ctx) {
42384
42691
  if (!spec.seedInputs)
@@ -42541,11 +42848,11 @@ function priorOutputs(options) {
42541
42848
  return [...out, ...blocks];
42542
42849
  }
42543
42850
  function readStageMd(planned) {
42544
- return readFileSync74(stageMdPath(planned.id, planned.source), "utf8");
42851
+ return readFileSync75(stageMdPath(planned.id, planned.source), "utf8");
42545
42852
  }
42546
42853
  function readOrEmpty4(path) {
42547
42854
  try {
42548
- return readFileSync74(path, "utf8");
42855
+ return readFileSync75(path, "utf8");
42549
42856
  } catch {
42550
42857
  return "";
42551
42858
  }
@@ -42754,7 +43061,7 @@ function hostTokensNote(store, options, phaseId, stageId, notes) {
42754
43061
  ...notes,
42755
43062
  `[tldrx] budget: refusing to start stage "${stageId}" — phase ${phaseId} is priced in \`host-tokens\` ` + `and has declared ${String(tokens3.spent)} of ${String(tokens3.ceiling)} allowed.`,
42756
43063
  "Raise that phase's ceiling in budget.yml (under this economy the number is a TOKEN allowance), " + "or set `on_host_tokens_exceed: warn` to go back to a note."
42757
- ]);
43064
+ ], [], `${phaseId}/${stageId}: phase ${phaseId} has declared ${String(tokens3.spent)} of ${String(tokens3.ceiling)} allowed host-tokens`);
42758
43065
  }
42759
43066
  function declaredTokensIn(run, phaseId) {
42760
43067
  let tokens3 = 0;
@@ -42853,14 +43160,22 @@ function cacheSplit(cacheCreationTokens, cacheReadTokens) {
42853
43160
  ...reported(cacheReadTokens) ? { cache_read_input_tokens: cacheReadTokens } : {}
42854
43161
  };
42855
43162
  }
42856
- function out(code, lines, stderr = []) {
43163
+ function executorSignature(outcome) {
43164
+ return outcome.signature ?? outcome.error ?? undefined;
43165
+ }
43166
+ function out(code, lines, stderr = [], signature) {
42857
43167
  const held2 = lines.filter((line) => line.includes(NOT_RESTORED_MARK));
42858
43168
  const ordered = held2.length === 0 ? lines : [...lines.filter((line) => !line.includes(NOT_RESTORED_MARK)), ...held2];
42859
- return stderr.length === 0 ? { code, lines: ordered } : { code, lines: ordered, stderr };
43169
+ return {
43170
+ code,
43171
+ lines: ordered,
43172
+ ...stderr.length === 0 ? {} : { stderr },
43173
+ ...signature === undefined ? {} : { signature }
43174
+ };
42860
43175
  }
42861
43176
 
42862
43177
  // src/core/run/ship.ts
42863
- import { existsSync as existsSync84, mkdtempSync as mkdtempSync2, readFileSync as readFileSync75, rmSync as rmSync11, writeFileSync as writeFileSync19 } from "node:fs";
43178
+ import { existsSync as existsSync84, mkdtempSync as mkdtempSync2, readFileSync as readFileSync76, rmSync as rmSync11, writeFileSync as writeFileSync20 } from "node:fs";
42864
43179
  import { tmpdir as tmpdir2 } from "node:os";
42865
43180
  import { dirname as dirname13, join as join101, resolve as resolve11 } from "node:path";
42866
43181
 
@@ -43600,7 +43915,7 @@ function lastHandoff(runDir2, phases) {
43600
43915
  continue;
43601
43916
  let text3;
43602
43917
  try {
43603
- text3 = readFileSync75(path, "utf8");
43918
+ text3 = readFileSync76(path, "utf8");
43604
43919
  } catch {
43605
43920
  continue;
43606
43921
  }
@@ -43625,7 +43940,7 @@ function writeShipBody(store, branch, handoff, stories, repoNames) {
43625
43940
  outcome: describeShipOutcome(store)
43626
43941
  });
43627
43942
  const path = join101(mkdtempSync2(join101(tmpdir2(), "tldrx-ship-")), "pr-body.md");
43628
- writeFileSync19(path, text3, "utf8");
43943
+ writeFileSync20(path, text3, "utf8");
43629
43944
  return { path, bytes: Buffer.byteLength(text3, "utf8"), handoffRel: handoff.rel, open: rows.length };
43630
43945
  }
43631
43946
  function bodyRecipe(body) {
@@ -43702,6 +44017,9 @@ function clipForLedger(text3) {
43702
44017
  const tail2 = text3.slice(-(LAST_LINE_CHARS - CLIP_HEAD_CHARS - 1));
43703
44018
  return `${text3.slice(0, CLIP_HEAD_CHARS)}…${tail2}`;
43704
44019
  }
44020
+ function comparandOf(signature, lines) {
44021
+ return signature ?? lines[lines.length - 1] ?? "";
44022
+ }
43705
44023
  function relaunchVerdict(input) {
43706
44024
  const { code } = input;
43707
44025
  if (code === EXIT_OK5)
@@ -43724,14 +44042,14 @@ function relaunchVerdict(input) {
43724
44042
  reason: `the --until-done ${String(input.of)} bound is spent after ${String(input.attempt)} attempts`
43725
44043
  };
43726
44044
  }
43727
- if (input.previousLastLine !== null && input.lastLine === input.previousLastLine) {
44045
+ if (input.previousComparand !== null && input.comparand === input.previousComparand) {
43728
44046
  return {
43729
44047
  relaunch: false,
43730
- reason: "its last line is the same as the previous attempt's — a refusal that repeats " + "verbatim is not one a relaunch moves"
44048
+ reason: "its refusal is the same as the previous attempt's — a refusal that repeats " + "verbatim is not one a relaunch moves"
43731
44049
  };
43732
44050
  }
43733
44051
  const family = code === EXIT_AGENT_FAILED3 ? "a stage failed past --retry-failed" : code === EXIT_REFUSED3 ? "a refusal with no money behind it" : "the loop ended with exit 1";
43734
- return { relaunch: true, reason: `${family}: ${input.lastLine}` };
44052
+ return { relaunch: true, reason: `${family}: ${input.comparand}` };
43735
44053
  }
43736
44054
  function budgetBlockedReason(fresh) {
43737
44055
  for (let i = fresh.length - 1;i >= 0; i--) {
@@ -43757,11 +44075,11 @@ async function runAuto(options) {
43757
44075
  };
43758
44076
  let runId = options.runId;
43759
44077
  let baseline = null;
43760
- let previousLastLine = null;
44078
+ let previousComparand = null;
43761
44079
  for (let attempt = 1;; attempt++) {
43762
44080
  let outcome;
43763
44081
  try {
43764
- outcome = await runAutoOnce({ ...options, runId, onLine: say }, { attempt, of: bound, previousLastLine, baseline });
44082
+ outcome = await runAutoOnce({ ...options, runId, onLine: say }, { attempt, of: bound, previousComparand, baseline });
43765
44083
  } catch (error) {
43766
44084
  const line = `run auto threw: ${error instanceof Error ? error.message : String(error)}`;
43767
44085
  say(line);
@@ -43770,10 +44088,11 @@ async function runAuto(options) {
43770
44088
  runId = outcome.runId ?? runId;
43771
44089
  baseline = outcome.baseline ?? baseline;
43772
44090
  const lastLine2 = outcome.lines[outcome.lines.length - 1] ?? "";
44091
+ const comparand = comparandOf(outcome.signature, outcome.lines);
43773
44092
  const verdict2 = outcome.verdict ?? relaunchVerdict({
43774
44093
  code: outcome.code,
43775
- lastLine: lastLine2,
43776
- previousLastLine,
44094
+ comparand,
44095
+ previousComparand,
43777
44096
  held: null,
43778
44097
  attempt,
43779
44098
  of: bound,
@@ -43799,11 +44118,12 @@ async function runAuto(options) {
43799
44118
  exit: outcome.code,
43800
44119
  attempt,
43801
44120
  of: bound,
43802
- last_line: clipForLedger(lastLine2)
44121
+ last_line: clipForLedger(lastLine2),
44122
+ comparand: clipForLedger(comparand)
43803
44123
  }
43804
44124
  });
43805
44125
  say(`relaunching run auto after exit ${String(outcome.code)} — ${verdict2.reason} ` + `(relaunch ${String(attempt)} of ${String(bound)}, --until-done)`);
43806
- previousLastLine = lastLine2;
44126
+ previousComparand = comparand;
43807
44127
  }
43808
44128
  }
43809
44129
  async function runAutoOnce(options, supervision) {
@@ -43814,16 +44134,17 @@ async function runAutoOnce(options, supervision) {
43814
44134
  };
43815
44135
  let resolved = null;
43816
44136
  let baseline = supervision?.baseline ?? null;
43817
- const leave = (code, held2 = null) => ({
44137
+ const leave = (code, held2 = null, signature) => ({
43818
44138
  code,
43819
44139
  lines,
44140
+ ...signature === undefined ? {} : { signature },
43820
44141
  runId: resolved?.runId ?? null,
43821
44142
  runDir: resolved?.runDir ?? null,
43822
44143
  baseline,
43823
44144
  verdict: supervision === undefined ? null : relaunchVerdict({
43824
44145
  code,
43825
- lastLine: lines[lines.length - 1] ?? "",
43826
- previousLastLine: supervision.previousLastLine,
44146
+ comparand: comparandOf(signature, lines),
44147
+ previousComparand: supervision.previousComparand,
43827
44148
  held: held2,
43828
44149
  attempt: supervision.attempt,
43829
44150
  of: supervision.of,
@@ -43894,10 +44215,10 @@ async function runAutoOnce(options, supervision) {
43894
44215
  await notifier.send(statusNotification(notifyCtx(), text3, stillBlocking(runDir2), gate2, cutInputs(stageIdOf()), gate2 === null ? null : gateStories(runDir2, gate2.phase)), stageIdOf());
43895
44216
  })();
43896
44217
  }, options.notifyEveryMs);
43897
- const finish = async (code, spentUsd, held2 = null) => {
44218
+ const finish = async (code, spentUsd, held2 = null, signature) => {
43898
44219
  if (heartbeat !== null)
43899
44220
  clearInterval(heartbeat);
43900
- const outcome = leave(code, held2 ?? budgetBlockedReason(readEvents3(log).slice(attemptStart)));
44221
+ const outcome = leave(code, held2 ?? budgetBlockedReason(readEvents3(log).slice(attemptStart)), signature);
43901
44222
  if (outcome.verdict?.relaunch === true)
43902
44223
  return outcome;
43903
44224
  if (notifier !== null) {
@@ -43919,7 +44240,7 @@ async function runAutoOnce(options, supervision) {
43919
44240
  };
43920
44241
  const heldForeignWork = () => {
43921
44242
  try {
43922
- return notRestoredSummary(readFileSync76(join102(runDir2, "events.jsonl"), "utf8"));
44243
+ return notRestoredSummary(readFileSync77(join102(runDir2, "events.jsonl"), "utf8"));
43923
44244
  } catch {
43924
44245
  return null;
43925
44246
  }
@@ -44142,7 +44463,7 @@ async function runAutoOnce(options, supervision) {
44142
44463
  if (outcome.code === EXIT_AGENT_FAILED3 && retryBound > 0) {
44143
44464
  say(`stopped: ${String(consecutiveFailures)} consecutive stage failures at ${cursorBefore} ` + `— the --retry-failed ${String(retryBound)} bound is spent, and the exit code is still ` + "5, the failure's own");
44144
44465
  }
44145
- return await finish(outcome.code, spentByLoop);
44466
+ return await finish(outcome.code, spentByLoop, null, outcome.signature);
44146
44467
  }
44147
44468
  const after = RunStore.open(runDir2);
44148
44469
  if (fresh.length === 0 && `${after.run.cursor.phase}/${after.run.cursor.stage}` === cursorBefore) {
@@ -44491,7 +44812,7 @@ function event5(options, runId, payload2) {
44491
44812
  return { ts: options.at, run: runId, stage: null, type: "run.attended", actor: options.actor, cost_usd: 0, payload: payload2 };
44492
44813
  }
44493
44814
  // src/core/status/initQuestionsItem.ts
44494
- import { existsSync as existsSync85, readFileSync as readFileSync77 } from "node:fs";
44815
+ import { existsSync as existsSync85, readFileSync as readFileSync78 } from "node:fs";
44495
44816
  import { join as join103 } from "node:path";
44496
44817
  var MAX_LISTED = 5;
44497
44818
  function initQuestionsItem(root2) {
@@ -44500,7 +44821,7 @@ function initQuestionsItem(root2) {
44500
44821
  return null;
44501
44822
  let open;
44502
44823
  try {
44503
- open = openBlocks(parseQuestions(readFileSync77(path, "utf8")).blocks);
44824
+ open = openBlocks(parseQuestions(readFileSync78(path, "utf8")).blocks);
44504
44825
  } catch {
44505
44826
  return null;
44506
44827
  }
@@ -44520,7 +44841,7 @@ function initQuestionsItem(root2) {
44520
44841
  }
44521
44842
 
44522
44843
  // src/core/status/seedSplitItems.ts
44523
- import { closeSync as closeSync2, existsSync as existsSync87, openSync as openSync2, readSync, readdirSync as readdirSync27, readFileSync as readFileSync78, statSync as statSync23 } from "node:fs";
44844
+ import { closeSync as closeSync2, existsSync as existsSync87, openSync as openSync2, readSync, readdirSync as readdirSync27, readFileSync as readFileSync79, statSync as statSync23 } from "node:fs";
44524
44845
  import { basename as basename14, join as join105, relative as relative19 } from "node:path";
44525
44846
 
44526
44847
  // src/core/seed/triageInventory.ts
@@ -44720,7 +45041,7 @@ function proposedSplits(root2) {
44720
45041
  try {
44721
45042
  if (!statSync23(dir).isDirectory() || !existsSync87(path))
44722
45043
  continue;
44723
- const file = readSplitFile(parseYaml(readFileSync78(path, "utf8")));
45044
+ const file = readSplitFile(parseYaml(readFileSync79(path, "utf8")));
44724
45045
  if (file.status !== "proposed" && file.status !== "applying")
44725
45046
  continue;
44726
45047
  found.push({ path, rel: toRel(root2, path), dir, file });
@@ -44788,7 +45109,7 @@ function seedDocuments2(dir, file) {
44788
45109
  const inventory = join105(dir, "inventory.json");
44789
45110
  if (existsSync87(inventory)) {
44790
45111
  try {
44791
- const doc = JSON.parse(readFileSync78(inventory, "utf8"));
45112
+ const doc = JSON.parse(readFileSync79(inventory, "utf8"));
44792
45113
  const rels2 = (doc.documents ?? []).map((row2) => row2.rel).filter((rel) => typeof rel === "string");
44793
45114
  if (rels2.length > 0)
44794
45115
  return rels2;
@@ -44824,7 +45145,7 @@ function cachedStatuses(dir) {
44824
45145
  if (!existsSync87(inventory))
44825
45146
  return out2;
44826
45147
  try {
44827
- const doc = JSON.parse(readFileSync78(inventory, "utf8"));
45148
+ const doc = JSON.parse(readFileSync79(inventory, "utf8"));
44828
45149
  for (const row2 of doc.documents ?? []) {
44829
45150
  if (typeof row2.rel === "string" && typeof row2.adrStatus === "string")
44830
45151
  out2.set(row2.rel, row2.adrStatus);
@@ -45141,7 +45462,7 @@ function resolveWorkspaceRoot(explicit, start = process.cwd()) {
45141
45462
  return findWorkspaceRoot2(start) ?? resolve12(start);
45142
45463
  }
45143
45464
  // src/core/experts/stageCoverage.ts
45144
- import { existsSync as existsSync89, readdirSync as readdirSync28, readFileSync as readFileSync79, statSync as statSync24 } from "node:fs";
45465
+ import { existsSync as existsSync89, readdirSync as readdirSync28, readFileSync as readFileSync80, statSync as statSync24 } from "node:fs";
45145
45466
  import { join as join107 } from "node:path";
45146
45467
  function stagesLoadingExperts(root2) {
45147
45468
  const repos = workspaceRepos(root2);
@@ -45201,7 +45522,7 @@ function readStage(root2, id) {
45201
45522
  if (!existsSync89(path))
45202
45523
  return null;
45203
45524
  try {
45204
- const doc = parseYaml(readFileSync79(path, "utf8"));
45525
+ const doc = parseYaml(readFileSync80(path, "utf8"));
45205
45526
  return isRecord(doc) ? doc : null;
45206
45527
  } catch {
45207
45528
  return null;
@@ -45212,7 +45533,7 @@ function workspaceRepos(root2) {
45212
45533
  if (!existsSync89(path))
45213
45534
  return [];
45214
45535
  try {
45215
- const doc = parseYaml(readFileSync79(path, "utf8"));
45536
+ const doc = parseYaml(readFileSync80(path, "utf8"));
45216
45537
  const list = isRecord(doc) && Array.isArray(doc.repos) ? doc.repos : [];
45217
45538
  return list.map((row2) => isRecord(row2) && typeof row2.name === "string" ? row2.name : "").filter((name) => name !== "");
45218
45539
  } catch {
@@ -45305,7 +45626,7 @@ function expertListJson(experts, loads) {
45305
45626
  })), null, 2);
45306
45627
  }
45307
45628
  // src/core/experts/createExpert.ts
45308
- import { mkdirSync as mkdirSync20, existsSync as existsSync90, writeFileSync as writeFileSync20 } from "node:fs";
45629
+ import { mkdirSync as mkdirSync20, existsSync as existsSync90, writeFileSync as writeFileSync21 } from "node:fs";
45309
45630
  import { join as join108 } from "node:path";
45310
45631
  var EXPERT_NAME_RE = /^[a-z0-9-]{1,32}$/;
45311
45632
  function planAreas(options) {
@@ -45362,8 +45683,8 @@ async function createExpert(options) {
45362
45683
  repos
45363
45684
  });
45364
45685
  mkdirSync20(dir, { recursive: true });
45365
- writeFileSync20(expertPath, body, "utf8");
45366
- writeFileSync20(competenciesPath, "# Written by `tldrx expert create` (spec §2.6). `level` is computed, never hand-set.\n" + stringifyYaml(buildCompetenciesDocument(options.name, areas)), "utf8");
45686
+ writeFileSync21(expertPath, body, "utf8");
45687
+ writeFileSync21(competenciesPath, "# Written by `tldrx expert create` (spec §2.6). `level` is computed, never hand-set.\n" + stringifyYaml(buildCompetenciesDocument(options.name, areas)), "utf8");
45367
45688
  if (kind === "role")
45368
45689
  mkdirSync20(join108(dir, KNOWLEDGE_DIRNAME), { recursive: true });
45369
45690
  return {
@@ -45562,7 +45883,7 @@ function sharedCitationWarnings(shared) {
45562
45883
  return shared.map((row2) => `warning: shared citation ${row2.src} by ${row2.experts.join(",")} — check for contradiction`);
45563
45884
  }
45564
45885
  // src/core/training/mineRuns.ts
45565
- import { existsSync as existsSync91, readFileSync as readFileSync80, readdirSync as readdirSync29, statSync as statSync25 } from "node:fs";
45886
+ import { existsSync as existsSync91, readFileSync as readFileSync81, readdirSync as readdirSync29, statSync as statSync25 } from "node:fs";
45566
45887
  import { join as join110, relative as relative20 } from "node:path";
45567
45888
  var MAX_RUN_FILES = 24;
45568
45889
  var MAX_RUN_BYTES = 64 * 1024;
@@ -45599,7 +45920,7 @@ function mineRuns(options) {
45599
45920
  }
45600
45921
  let text3;
45601
45922
  try {
45602
- text3 = readFileSync80(candidate.abs, "utf8");
45923
+ text3 = readFileSync81(candidate.abs, "utf8");
45603
45924
  } catch {
45604
45925
  notRead.push(candidate.path);
45605
45926
  continue;
@@ -45663,7 +45984,7 @@ function runRepos(path) {
45663
45984
  if (!existsSync91(path))
45664
45985
  return [];
45665
45986
  try {
45666
- const doc = parseYaml(readFileSync80(path, "utf8"));
45987
+ const doc = parseYaml(readFileSync81(path, "utf8"));
45667
45988
  const repos = doc?.repos;
45668
45989
  if (!Array.isArray(repos))
45669
45990
  return [];
@@ -46383,7 +46704,7 @@ function estimateNextStage(root2, runId) {
46383
46704
  function runRemainingOf(store, phaseId, stageId, work) {
46384
46705
  const stages = [];
46385
46706
  let staticUsd = 0;
46386
- let usd2 = 0;
46707
+ let usd3 = 0;
46387
46708
  let done = 0;
46388
46709
  for (const entry of flatten(store.run)) {
46389
46710
  if (isTerminal(entry.stage.status)) {
@@ -46393,9 +46714,9 @@ function runRemainingOf(store, phaseId, stageId, work) {
46393
46714
  stages.push(`${entry.phase.id}/${entry.stage.id}`);
46394
46715
  staticUsd += entry.stage.budget_usd;
46395
46716
  const isCursor = entry.phase.id === phaseId && entry.stage.id === stageId;
46396
- usd2 += isCursor && work.basis === "plan" ? work.usd : entry.stage.budget_usd;
46717
+ usd3 += isCursor && work.basis === "plan" ? work.usd : entry.stage.budget_usd;
46397
46718
  }
46398
- return { stages, done, staticUsd: round10(staticUsd), usd: round10(usd2) };
46719
+ return { stages, done, staticUsd: round10(staticUsd), usd: round10(usd3) };
46399
46720
  }
46400
46721
  function renderEstimate(estimate) {
46401
46722
  const lines = [
@@ -46901,7 +47222,7 @@ var statusCommand = {
46901
47222
  };
46902
47223
 
46903
47224
  // src/core/seed/runTriage.ts
46904
- import { mkdirSync as mkdirSync21, readFileSync as readFileSync81, writeFileSync as writeFileSync21, existsSync as existsSync93 } from "node:fs";
47225
+ import { mkdirSync as mkdirSync21, readFileSync as readFileSync82, writeFileSync as writeFileSync22, existsSync as existsSync93 } from "node:fs";
46905
47226
  import { isAbsolute as isAbsolute10, join as join111, relative as relative21, resolve as resolve15 } from "node:path";
46906
47227
 
46907
47228
  // src/core/seed/renderInventory.ts
@@ -47161,8 +47482,8 @@ async function runTriage(options) {
47161
47482
  });
47162
47483
  const outDir = resolveOut(options);
47163
47484
  mkdirSync21(outDir, { recursive: true });
47164
- writeFileSync21(join111(outDir, INVENTORY_MD), renderInventory(inventory, options.seedPath, options.at), "utf8");
47165
- writeFileSync21(join111(outDir, INVENTORY_JSON), inventoryJson(inventory, options.seedPath, options.at), "utf8");
47485
+ writeFileSync22(join111(outDir, INVENTORY_MD), renderInventory(inventory, options.seedPath, options.at), "utf8");
47486
+ writeFileSync22(join111(outDir, INVENTORY_JSON), inventoryJson(inventory, options.seedPath, options.at), "utf8");
47166
47487
  if (options.propose !== true) {
47167
47488
  const where = display(options.root, outDir);
47168
47489
  const lines = options.json === true ? [inventoryJson(inventory, options.seedPath, options.at).replace(/\n$/, "")] : [
@@ -47318,8 +47639,8 @@ async function propose(options, workspaceCommands, inventory, seed, outDir) {
47318
47639
  created_at: options.at,
47319
47640
  ...validation.proposal
47320
47641
  };
47321
- writeFileSync21(join111(outDir, SPLIT_YML), emitSplitYaml(file), "utf8");
47322
- writeFileSync21(join111(outDir, SPLIT_MD), renderSplitMarkdown(file, where), "utf8");
47642
+ writeFileSync22(join111(outDir, SPLIT_YML), emitSplitYaml(file), "utf8");
47643
+ writeFileSync22(join111(outDir, SPLIT_MD), renderSplitMarkdown(file, where), "utf8");
47323
47644
  const over = metered && costUsd > ceiling + 0.000000001;
47324
47645
  return {
47325
47646
  code: EXIT_OK9,
@@ -47344,7 +47665,7 @@ function readTriageResult(outDir) {
47344
47665
  }
47345
47666
  let doc;
47346
47667
  try {
47347
- doc = JSON.parse(readFileSync81(path, "utf8"));
47668
+ doc = JSON.parse(readFileSync82(path, "utf8"));
47348
47669
  } catch (error) {
47349
47670
  throw new PendingError(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
47350
47671
  }
@@ -47385,7 +47706,7 @@ function round28(n) {
47385
47706
  }
47386
47707
 
47387
47708
  // src/core/seed/applySplit.ts
47388
- import { existsSync as existsSync94, readFileSync as readFileSync82, writeFileSync as writeFileSync22 } from "node:fs";
47709
+ import { existsSync as existsSync94, readFileSync as readFileSync83, writeFileSync as writeFileSync23 } from "node:fs";
47389
47710
  import { dirname as dirname15, isAbsolute as isAbsolute11, join as join112, relative as relative22, resolve as resolve16 } from "node:path";
47390
47711
  var EXIT_OK10 = 0;
47391
47712
  var EXIT_USAGE7 = 1;
@@ -47397,7 +47718,7 @@ function applySplit(options) {
47397
47718
  }
47398
47719
  let file;
47399
47720
  try {
47400
- file = readSplitFile(parseYaml(readFileSync82(path, "utf8")));
47721
+ file = readSplitFile(parseYaml(readFileSync83(path, "utf8")));
47401
47722
  } catch (error) {
47402
47723
  return fail2(EXIT_USAGE7, [error instanceof SplitError ? error.message : message5(error)]);
47403
47724
  }
@@ -47438,7 +47759,7 @@ function applySplit(options) {
47438
47759
  }
47439
47760
  const created = [];
47440
47761
  const stamp2 = (status) => {
47441
- writeFileSync22(path, emitSplitYaml({
47762
+ writeFileSync23(path, emitSplitYaml({
47442
47763
  ...file,
47443
47764
  ...validation.proposal,
47444
47765
  status,
@@ -47483,11 +47804,11 @@ function applySplit(options) {
47483
47804
  applied_at: rfc33393(options.now),
47484
47805
  created_runs: created
47485
47806
  };
47486
- writeFileSync22(path, emitSplitYaml(applied), "utf8");
47807
+ writeFileSync23(path, emitSplitYaml(applied), "utf8");
47487
47808
  const outRel = reference(options.root, dirname15(path));
47488
47809
  const markdown = join112(dirname15(path), SPLIT_MD);
47489
47810
  if (existsSync94(markdown))
47490
- writeFileSync22(markdown, renderSplitMarkdown(applied, outRel), "utf8");
47811
+ writeFileSync23(markdown, renderSplitMarkdown(applied, outRel), "utf8");
47491
47812
  return {
47492
47813
  code: EXIT_OK10,
47493
47814
  created,
@@ -47522,7 +47843,7 @@ function splitUniverse(splitPath, file) {
47522
47843
  const inventory = join112(dirname15(splitPath), "inventory.json");
47523
47844
  if (existsSync94(inventory)) {
47524
47845
  try {
47525
- const doc = JSON.parse(readFileSync82(inventory, "utf8"));
47846
+ const doc = JSON.parse(readFileSync83(inventory, "utf8"));
47526
47847
  const rels2 = (doc.documents ?? []).map((row2) => row2.rel).filter((rel) => typeof rel === "string");
47527
47848
  if (rels2.length > 0)
47528
47849
  return new Set(rels2);
@@ -47565,7 +47886,7 @@ function message5(error) {
47565
47886
  }
47566
47887
 
47567
47888
  // src/core/seed/answerSplitQuestion.ts
47568
- import { existsSync as existsSync95, readFileSync as readFileSync83, writeFileSync as writeFileSync23 } from "node:fs";
47889
+ import { existsSync as existsSync95, readFileSync as readFileSync84, writeFileSync as writeFileSync24 } from "node:fs";
47569
47890
  import { dirname as dirname16, isAbsolute as isAbsolute12, join as join113, relative as relative23, resolve as resolve17 } from "node:path";
47570
47891
  var EXIT_OK11 = 0;
47571
47892
  var EXIT_USAGE8 = 1;
@@ -47579,7 +47900,7 @@ function answerSplitQuestion(options) {
47579
47900
  return { code: EXIT_NOT_FOUND9, lines: [`no such file: ${options.splitPath}`] };
47580
47901
  let file;
47581
47902
  try {
47582
- file = readSplitFile(parseYaml(readFileSync83(path, "utf8")));
47903
+ file = readSplitFile(parseYaml(readFileSync84(path, "utf8")));
47583
47904
  } catch (error) {
47584
47905
  return { code: EXIT_USAGE8, lines: [error instanceof SplitError ? error.message : message6(error)] };
47585
47906
  }
@@ -47613,10 +47934,10 @@ function answerSplitQuestion(options) {
47613
47934
  ...validation.proposal,
47614
47935
  questions: questions.map((question) => question.id === options.id ? { ...question, answer: trimmed } : question)
47615
47936
  };
47616
- writeFileSync23(path, emitSplitYaml(answered), "utf8");
47937
+ writeFileSync24(path, emitSplitYaml(answered), "utf8");
47617
47938
  const markdown = join113(dirname16(path), SPLIT_MD);
47618
47939
  if (existsSync95(markdown)) {
47619
- writeFileSync23(markdown, renderSplitMarkdown(answered, reference2(options.root, dirname16(path))), "utf8");
47940
+ writeFileSync24(markdown, renderSplitMarkdown(answered, reference2(options.root, dirname16(path))), "utf8");
47620
47941
  }
47621
47942
  const open = answered.questions.filter((question) => question.answer === undefined).length;
47622
47943
  return {
@@ -47637,7 +47958,7 @@ function message6(error) {
47637
47958
  }
47638
47959
 
47639
47960
  // src/core/seed/checkSeed.ts
47640
- import { mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync3, rmSync as rmSync12, writeFileSync as writeFileSync24 } from "node:fs";
47961
+ import { mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync3, rmSync as rmSync12, writeFileSync as writeFileSync25 } from "node:fs";
47641
47962
  import { tmpdir as tmpdir3 } from "node:os";
47642
47963
  import { join as join114 } from "node:path";
47643
47964
  var MAX_SEED_BULLET_CHARS = 200;
@@ -47983,7 +48304,7 @@ function importerVerdict(seed, preset, workspace) {
47983
48304
  const headings2 = allSeedHeadings(seed.documents);
47984
48305
  const indexPath = join114(temp, ...phase.split("/"), SEED_INDEX);
47985
48306
  mkdirSync22(join114(temp, phase), { recursive: true });
47986
- writeFileSync24(indexPath, renderSeedIndex("seed-check", seed, phase), "utf8");
48307
+ writeFileSync25(indexPath, renderSeedIndex("seed-check", seed, phase), "utf8");
47987
48308
  const handoff = renderSeedHandoff({
47988
48309
  runId: "seed-check",
47989
48310
  stageId: stage2?.id ?? "what",
@@ -48024,7 +48345,7 @@ function budgetView(preset, ceiling, storyCount) {
48024
48345
  storyCount: Math.max(storyCount, 1),
48025
48346
  budgetUsd,
48026
48347
  maxBudgetUsd: Math.min(budgetUsd, plan.perAgentMax),
48027
- agentCap: (share = 1) => round25(budgetUsd * share),
48348
+ agentCap: (share = 1) => round23(budgetUsd * share),
48028
48349
  attempts: buildStage.attempts
48029
48350
  };
48030
48351
  build = {
@@ -48056,7 +48377,7 @@ function renderSeedCheck(report3) {
48056
48377
  lines.push(` ${stage2.id.padEnd(7)} ${`$${stage2.perAttemptUsd.toFixed(2)}`.padStart(8)} per attempt` + ` (${stage2.phase} holds $${stage2.phaseUsd.toFixed(2)} for ${String(stage2.attempts)} attempt(s))`);
48057
48378
  }
48058
48379
  if (b.build !== null) {
48059
- lines.push(` per story, ${String(b.build.storyCount)} story(ies), no plan price yet: developer cap $${b.build.developerUsd.toFixed(2)}` + ` per attempt, reviewer cap $${b.build.reviewerUsd.toFixed(2)} (#244/#289 — a priced story gets` + " max(price x 3, $4.00) at dispatch; raise --budget if the cap does not cover the largest story)");
48380
+ lines.push(` per story, ${String(b.build.storyCount)} story(ies), no plan price yet: developer cap $${b.build.developerUsd.toFixed(2)}` + ` per attempt, reviewer cap $${b.build.reviewerUsd.toFixed(2)} (#244/#289 — a priced story gets` + " max(price x scale x 3, $4.00) at dispatch, scale being 1 only while the plan's prices sum inside" + " the stage (#281); raise --budget if the cap does not cover the largest story)");
48060
48381
  }
48061
48382
  }
48062
48383
  const stories = report3.stories.length;
@@ -48282,7 +48603,7 @@ function scopeRepos(names, repoNames) {
48282
48603
  }
48283
48604
 
48284
48605
  // src/cli/commands/answer.ts
48285
- import { readFileSync as readFileSync84 } from "node:fs";
48606
+ import { readFileSync as readFileSync85 } from "node:fs";
48286
48607
  var QUESTION_ID_RE = /^Q\d{1,6}$/;
48287
48608
  var answerCommand2 = {
48288
48609
  name: "answer",
@@ -48397,7 +48718,7 @@ function locateQuestion(store, qid, status) {
48397
48718
  const path = join115(store.runDir, phase.id, "questions.md");
48398
48719
  if (!existsSync96(path))
48399
48720
  continue;
48400
- const block2 = parseQuestions(readFileSync84(path, "utf8")).blocks.find((b) => b.id === qid);
48721
+ const block2 = parseQuestions(readFileSync85(path, "utf8")).blocks.find((b) => b.id === qid);
48401
48722
  if (block2 !== undefined && block2.metadata?.status === status)
48402
48723
  return { path, block: block2 };
48403
48724
  }
@@ -48405,7 +48726,7 @@ function locateQuestion(store, qid, status) {
48405
48726
  }
48406
48727
 
48407
48728
  // src/cli/commands/approve.ts
48408
- import { existsSync as existsSync97, readFileSync as readFileSync85 } from "node:fs";
48729
+ import { existsSync as existsSync97, readFileSync as readFileSync86 } from "node:fs";
48409
48730
  import { relative as relative24 } from "node:path";
48410
48731
  var approveCommand2 = {
48411
48732
  name: "approve",
@@ -48490,7 +48811,7 @@ function readAgentEvidence(root2, store, override) {
48490
48811
  process.stderr.write(`tldrx approve: --as-agent refused — no evidence note at ${relative24(root2, path)}. ` + "Write one (`tldrx gate template` puts the blank form there), then sign with it.\n");
48491
48812
  return { kind: "stop", exit: EXIT_GATE_REFUSED };
48492
48813
  }
48493
- const text3 = readFileSync85(path, "utf8");
48814
+ const text3 = readFileSync86(path, "utf8");
48494
48815
  const gate2 = `${entry.phase.id}/${entry.stage.id}`;
48495
48816
  const validation = validateEvidence(text3, toSrcContext(loadWorkspace(root2), store.runDir, { epicRefs: true }), { gate: gate2 });
48496
48817
  const refusals = validation.issues.filter((issue) => issue.kind === "verdict");
@@ -48750,7 +49071,7 @@ function budgetRaise(argv) {
48750
49071
  ...phase,
48751
49072
  stages: phase.stages.map((stage2) => stage2.id !== stageId ? stage2 : {
48752
49073
  ...stage2,
48753
- budget_usd: round25(stage2.budget_usd + amountUsd)
49074
+ budget_usd: round23(stage2.budget_usd + amountUsd)
48754
49075
  })
48755
49076
  })
48756
49077
  }));
@@ -48773,7 +49094,7 @@ function budgetRaise(argv) {
48773
49094
  ...stageId === null || stageBefore === null ? {} : {
48774
49095
  stage: stageId,
48775
49096
  stage_budget_before: stageBefore,
48776
- stage_budget_after: round25(stageBefore + amountUsd)
49097
+ stage_budget_after: round23(stageBefore + amountUsd)
48777
49098
  },
48778
49099
  note: stringFlag(args, "note") ?? ""
48779
49100
  }
@@ -48899,7 +49220,7 @@ function stageRaiseLines(phaseId, stageId, before, amountUsd) {
48899
49220
  ];
48900
49221
  }
48901
49222
  return [
48902
- `${phaseId}/${stageId} budget_usd $${before.toFixed(2)} → $${round25(before + amountUsd).toFixed(2)} ` + `(+$${amountUsd.toFixed(2)}) — every per-story and reviewer cap is derived from this figure.`
49223
+ `${phaseId}/${stageId} budget_usd $${before.toFixed(2)} → $${round23(before + amountUsd).toFixed(2)} ` + `(+$${amountUsd.toFixed(2)}) — every per-story and reviewer cap is derived from this figure.`
48903
49224
  ];
48904
49225
  }
48905
49226
 
@@ -49062,7 +49383,7 @@ function resolveIncludingFinished(root2, wanted) {
49062
49383
  }
49063
49384
 
49064
49385
  // src/cli/commands/gate.ts
49065
- import { existsSync as existsSync98, mkdirSync as mkdirSync23, writeFileSync as writeFileSync25 } from "node:fs";
49386
+ import { existsSync as existsSync98, mkdirSync as mkdirSync23, writeFileSync as writeFileSync26 } from "node:fs";
49066
49387
  import { dirname as dirname17, relative as relative25 } from "node:path";
49067
49388
  var VALUE_FLAGS5 = ["run", "root"];
49068
49389
  var gateCommand = {
@@ -49111,7 +49432,7 @@ function template(argv) {
49111
49432
  touchesAudited: countDeclaredTouches(ctx)
49112
49433
  };
49113
49434
  mkdirSync23(dirname17(path), { recursive: true });
49114
- writeFileSync25(path, renderEvidenceTemplate(input), "utf8");
49435
+ writeFileSync26(path, renderEvidenceTemplate(input), "utf8");
49115
49436
  const lines = describeEvidenceTemplate(relative25(root2, path), input);
49116
49437
  process.stdout.write(`${lines.join(`
49117
49438
  `)}
@@ -49123,7 +49444,7 @@ function template(argv) {
49123
49444
  }
49124
49445
 
49125
49446
  // src/core/run/reopenStory.ts
49126
- import { existsSync as existsSync99, readFileSync as readFileSync86, writeFileSync as writeFileSync26 } from "node:fs";
49447
+ import { existsSync as existsSync99, readFileSync as readFileSync87, writeFileSync as writeFileSync27 } from "node:fs";
49127
49448
  import { join as join116 } from "node:path";
49128
49449
  var EXIT_OK12 = 0;
49129
49450
  var EXIT_REFUSED8 = 2;
@@ -49228,7 +49549,7 @@ function reopenStory(options) {
49228
49549
  }
49229
49550
  let patched;
49230
49551
  try {
49231
- const text3 = readFileSync86(path, "utf8");
49552
+ const text3 = readFileSync87(path, "utf8");
49232
49553
  patched = progress.implicit ? updateImplicitPlan(text3, { status: REOPENED_TO }) : updateStoryFront(text3, { status: REOPENED_TO });
49233
49554
  } catch (error) {
49234
49555
  if (error instanceof StoryWriteError) {
@@ -49245,7 +49566,7 @@ function reopenStory(options) {
49245
49566
  " nothing was written — the note is the only free text in it, so it is almost certainly too long"
49246
49567
  ]);
49247
49568
  }
49248
- writeFileSync26(path, patched, "utf8");
49569
+ writeFileSync27(path, patched, "utf8");
49249
49570
  store.append(event7);
49250
49571
  const kept = [
49251
49572
  " its branch is kept, so the next developer starts from the commits the last one made " + "(the worktree is reopened from that branch if the build had removed it)",
@@ -49275,6 +49596,7 @@ function reopenStory(options) {
49275
49596
  ` signed by ${options.actor}: ${options.note}`,
49276
49597
  " no developer will be spawned: the next Build turn runs the dod, the review and the merge " + "over the commits already on the story branch",
49277
49598
  " it is not a shortcut past either gate — a red dod BLOCKS the story, and so does a branch " + "that carries no commit its epic has not already got; the refusal says which",
49599
+ " one named exception: work that is ALREADY on the epic with a review that never completed is " + "settled review-only — nothing merged, the dod on the epic head, the reviewer over the recorded range",
49278
49600
  " the record will say the branch was taken as it stands and name you, so nothing reads as " + "though a developer delivered it",
49279
49601
  ...kept
49280
49602
  ]
@@ -49330,7 +49652,7 @@ function refuse4(lines) {
49330
49652
  }
49331
49653
 
49332
49654
  // src/core/run/widenStory.ts
49333
- import { existsSync as existsSync100, readFileSync as readFileSync87, writeFileSync as writeFileSync27 } from "node:fs";
49655
+ import { existsSync as existsSync100, readFileSync as readFileSync88, writeFileSync as writeFileSync28 } from "node:fs";
49334
49656
  import { join as join117 } from "node:path";
49335
49657
  var EXIT_OK13 = 0;
49336
49658
  var EXIT_REFUSED9 = 2;
@@ -49410,7 +49732,7 @@ function widenStory(options) {
49410
49732
  if (!existsSync100(path)) {
49411
49733
  return refuse5([`${id} is \`${row2.status}\` but ${path} is not on disk — refusing to write a file that is not there`]);
49412
49734
  }
49413
- const text3 = readFileSync87(path, "utf8");
49735
+ const text3 = readFileSync88(path, "utf8");
49414
49736
  const before = touchesIn(text3, progress.implicit);
49415
49737
  if (before === null) {
49416
49738
  return refuse5([
@@ -49466,7 +49788,7 @@ function widenStory(options) {
49466
49788
  " nothing was written — the note is the only free text in it, so it is almost certainly too long"
49467
49789
  ]);
49468
49790
  }
49469
- writeFileSync27(path, patched, "utf8");
49791
+ writeFileSync28(path, patched, "utf8");
49470
49792
  store.append(event7);
49471
49793
  return {
49472
49794
  code: EXIT_OK13,
@@ -49790,11 +50112,11 @@ var shipCommand = {
49790
50112
  import { join as join120 } from "node:path";
49791
50113
 
49792
50114
  // src/core/plan/syncDod.ts
49793
- import { existsSync as existsSync102, readdirSync as readdirSync30, readFileSync as readFileSync89 } from "node:fs";
50115
+ import { existsSync as existsSync102, readdirSync as readdirSync30, readFileSync as readFileSync90 } from "node:fs";
49794
50116
  import { join as join119 } from "node:path";
49795
50117
 
49796
50118
  // src/core/plan/workspaceHistory.ts
49797
- import { existsSync as existsSync101, readFileSync as readFileSync88, realpathSync } from "node:fs";
50119
+ import { existsSync as existsSync101, readFileSync as readFileSync89, realpathSync } from "node:fs";
49798
50120
  import { join as join118, relative as relative26, sep as sep10 } from "node:path";
49799
50121
  var WORKSPACE_REL = `${PROJECT_FRAMEWORK_DIR}/workspace.yml`;
49800
50122
  var MAX_VERSIONS = 50;
@@ -49802,7 +50124,7 @@ async function readWorkspaceHistory(root2) {
49802
50124
  const documents = [];
49803
50125
  const live2 = join118(root2, WORKSPACE_REL);
49804
50126
  if (existsSync101(live2)) {
49805
- const parsed = parse(readFileSync88(live2, "utf8"));
50127
+ const parsed = parse(readFileSync89(live2, "utf8"));
49806
50128
  if (parsed !== null)
49807
50129
  documents.push(parsed);
49808
50130
  }
@@ -49892,7 +50214,7 @@ function planSyncDod(input) {
49892
50214
  };
49893
50215
  }
49894
50216
  function syncOne(path, name, input) {
49895
- const text3 = readFileSync89(path, "utf8");
50217
+ const text3 = readFileSync90(path, "utf8");
49896
50218
  const id = name.replace(/\.md$/, "");
49897
50219
  const rel = `${STORIES_DIR}/${name}`;
49898
50220
  const repo = readStory(text3).repo ?? "";
@@ -50487,7 +50809,7 @@ function refusal(input, model2, share, expected) {
50487
50809
  ];
50488
50810
  }
50489
50811
  // src/core/training/ambientModel.ts
50490
- import { readFileSync as readFileSync90 } from "node:fs";
50812
+ import { readFileSync as readFileSync91 } from "node:fs";
50491
50813
  import { join as join122 } from "node:path";
50492
50814
  function ambientModelFiles(root2, home) {
50493
50815
  return [
@@ -50510,7 +50832,7 @@ function resolveAmbientModel(input) {
50510
50832
  function modelIn(path) {
50511
50833
  let parsed;
50512
50834
  try {
50513
- parsed = JSON.parse(readFileSync90(path, "utf8"));
50835
+ parsed = JSON.parse(readFileSync91(path, "utf8"));
50514
50836
  } catch {
50515
50837
  return null;
50516
50838
  }
@@ -50582,11 +50904,11 @@ function noEvidenceNote(costUsd, added, warnings) {
50582
50904
  return lines;
50583
50905
  }
50584
50906
  // src/core/training/rescoreExperts.ts
50585
- import { readFileSync as readFileSync93 } from "node:fs";
50907
+ import { readFileSync as readFileSync94 } from "node:fs";
50586
50908
  import { relative as relative27 } from "node:path";
50587
50909
 
50588
50910
  // src/core/training/competenciesWrite.ts
50589
- import { existsSync as existsSync104, mkdirSync as mkdirSync24, readFileSync as readFileSync91, writeFileSync as writeFileSync28 } from "node:fs";
50911
+ import { existsSync as existsSync104, mkdirSync as mkdirSync24, readFileSync as readFileSync92, writeFileSync as writeFileSync29 } from "node:fs";
50590
50912
  import { dirname as dirname18, join as join124 } from "node:path";
50591
50913
  var HEADER = "# Written by `tldrx expert train` (spec §2.6). `level` is computed, never hand-set.\n";
50592
50914
 
@@ -50596,7 +50918,7 @@ function readDocument(root2, expert) {
50596
50918
  const path = join124(expertDir(root2, expert), COMPETENCIES_FILE);
50597
50919
  if (!existsSync104(path))
50598
50920
  throw new CompetenciesError(`${path} is missing — \`tldrx expert create\` writes it`);
50599
- const text3 = readFileSync91(path, "utf8");
50921
+ const text3 = readFileSync92(path, "utf8");
50600
50922
  const parsed = parseYaml(text3);
50601
50923
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
50602
50924
  throw new CompetenciesError(`${path} is not a mapping`);
@@ -50656,7 +50978,7 @@ function writeCompetencies(options) {
50656
50978
  doc.status = options.status;
50657
50979
  doc.last_trained = options.lastTrained;
50658
50980
  mkdirSync24(dirname18(path), { recursive: true });
50659
- writeFileSync28(path, serialize(loaded, doc), "utf8");
50981
+ writeFileSync29(path, serialize(loaded, doc), "utf8");
50660
50982
  return { ...write2, warnings };
50661
50983
  }
50662
50984
  function rejectBadEvidence(expert, areaId2, evidence) {
@@ -50695,7 +51017,7 @@ function recomputeCompetencies(options) {
50695
51017
  if (next === loaded.text)
50696
51018
  return { path, written: false, areas: recomputed, warnings };
50697
51019
  mkdirSync24(dirname18(path), { recursive: true });
50698
- writeFileSync28(path, next, "utf8");
51020
+ writeFileSync29(path, next, "utf8");
50699
51021
  return { path, written: true, areas: recomputed, warnings };
50700
51022
  }
50701
51023
  function headerOf(text3) {
@@ -50709,7 +51031,7 @@ function str11(value) {
50709
51031
  }
50710
51032
 
50711
51033
  // src/core/training/trainingLog.ts
50712
- import { appendFileSync as appendFileSync5, existsSync as existsSync105, mkdirSync as mkdirSync25, readFileSync as readFileSync92, statSync as statSync27 } from "node:fs";
51034
+ import { appendFileSync as appendFileSync4, existsSync as existsSync105, mkdirSync as mkdirSync25, readFileSync as readFileSync93, statSync as statSync27 } from "node:fs";
50713
51035
  import { dirname as dirname19, join as join125 } from "node:path";
50714
51036
  var TRAINING_EVENT_TYPES = [
50715
51037
  "agent.spawned",
@@ -50745,7 +51067,7 @@ class TrainingLog {
50745
51067
  throw new Error(`training record is ${String(bytes3)} bytes (max ${String(MAX_LINE_BYTES2)})`);
50746
51068
  const before = this.sizeBytes;
50747
51069
  mkdirSync25(dirname19(this.path), { recursive: true });
50748
- appendFileSync5(this.path, `${line}
51070
+ appendFileSync4(this.path, `${line}
50749
51071
  `, "utf8");
50750
51072
  const after = this.sizeBytes;
50751
51073
  if (after < before + bytes3) {
@@ -50756,7 +51078,7 @@ class TrainingLog {
50756
51078
  if (!existsSync105(this.path))
50757
51079
  return [];
50758
51080
  const out2 = [];
50759
- for (const line of readFileSync92(this.path, "utf8").split(`
51081
+ for (const line of readFileSync93(this.path, "utf8").split(`
50760
51082
  `)) {
50761
51083
  if (line.trim() === "")
50762
51084
  continue;
@@ -50906,7 +51228,7 @@ function rescoreExperts(options) {
50906
51228
  }
50907
51229
  const shape = minesRunRecord ? RUNS_SHAPE : LIGHT_SHAPE;
50908
51230
  const scope = knowledgeScopeFor(options.root, expert, area);
50909
- const parsed = parseKnowledgeFile(readFileSync93(file.path, "utf8"), srcCtx, shape, scope);
51231
+ const parsed = parseKnowledgeFile(readFileSync94(file.path, "utf8"), srcCtx, shape, scope);
50910
51232
  if (!parsed.ok) {
50911
51233
  const errors2 = knowledgeErrors(parsed).length;
50912
51234
  skip(`the file does not validate under today's rules — ${String(errors2)} problem(s), so it is not evidence`);
@@ -50993,7 +51315,7 @@ function rescoreJson(rows) {
50993
51315
  return JSON.stringify(rows, null, 2);
50994
51316
  }
50995
51317
  // src/core/training/selectFiles.ts
50996
- import { existsSync as existsSync107, readFileSync as readFileSync94, statSync as statSync29 } from "node:fs";
51318
+ import { existsSync as existsSync107, readFileSync as readFileSync95, statSync as statSync29 } from "node:fs";
50997
51319
  import { join as join126 } from "node:path";
50998
51320
  var MAX_SCANNED_FILES2 = 4000;
50999
51321
  var MAX_SCAN_BYTES2 = 512 * 1024;
@@ -51133,7 +51455,7 @@ function readDomains(root2, repos) {
51133
51455
  const path = join126(root2, PROJECT_FRAMEWORK_DIR, "map", repo, "domains.md");
51134
51456
  if (!existsSync107(path))
51135
51457
  continue;
51136
- for (const line of readFileSync94(path, "utf8").split(`
51458
+ for (const line of readFileSync95(path, "utf8").split(`
51137
51459
  `)) {
51138
51460
  if (!line.startsWith("- "))
51139
51461
  continue;
@@ -51162,7 +51484,7 @@ function readCommunities(root2, repos, keywords) {
51162
51484
  }
51163
51485
  let text3;
51164
51486
  try {
51165
- text3 = readFileSync94(path, "utf8");
51487
+ text3 = readFileSync95(path, "utf8");
51166
51488
  } catch {
51167
51489
  notes.push(`${repo}: graph.json could not be read`);
51168
51490
  continue;
@@ -51241,7 +51563,7 @@ function matches2(label2, keywords) {
51241
51563
  function contentHits(abs, keywords) {
51242
51564
  let text3;
51243
51565
  try {
51244
- text3 = readFileSync94(abs, "utf8").toLowerCase();
51566
+ text3 = readFileSync95(abs, "utf8").toLowerCase();
51245
51567
  } catch {
51246
51568
  return [];
51247
51569
  }
@@ -51252,7 +51574,7 @@ function readCapped2(abs, remaining2) {
51252
51574
  try {
51253
51575
  if (statSync29(abs).size > MAX_SCAN_BYTES2)
51254
51576
  return null;
51255
- text3 = readFileSync94(abs, "utf8");
51577
+ text3 = readFileSync95(abs, "utf8");
51256
51578
  } catch {
51257
51579
  return null;
51258
51580
  }
@@ -51825,7 +52147,7 @@ function skipNoteLines(expert, area, reason) {
51825
52147
  return [`${expert}/${area}: skipped — ${first2 ?? ""}`, ...rest.map((line) => ` ${line}`)];
51826
52148
  }
51827
52149
  // src/core/training/strayWrite.ts
51828
- import { existsSync as existsSync108, mkdirSync as mkdirSync26, readdirSync as readdirSync33, readFileSync as readFileSync95, renameSync as renameSync4, rmdirSync as rmdirSync2, unlinkSync, writeFileSync as writeFileSync29 } from "node:fs";
52150
+ import { existsSync as existsSync108, mkdirSync as mkdirSync26, readdirSync as readdirSync33, readFileSync as readFileSync96, renameSync as renameSync4, rmdirSync as rmdirSync2, unlinkSync, writeFileSync as writeFileSync30 } from "node:fs";
51829
52151
  import { dirname as dirname20, join as join128, relative as relative28, resolve as resolve19 } from "node:path";
51830
52152
  function findStrayWrite(probe2) {
51831
52153
  const root2 = resolve19(probe2.root);
@@ -51848,7 +52170,7 @@ function recoverStrayWrite(stray, dest) {
51848
52170
  renameSync4(stray.path, dest);
51849
52171
  } catch {
51850
52172
  try {
51851
- writeFileSync29(dest, readFileSync95(stray.path, "utf8"), "utf8");
52173
+ writeFileSync30(dest, readFileSync96(stray.path, "utf8"), "utf8");
51852
52174
  unlinkSync(stray.path);
51853
52175
  } catch {
51854
52176
  return { stray, recovered: existsSync108(dest), leftBehind: stray.path };
@@ -51897,7 +52219,7 @@ function describeStrayRecovery(root2, recovery, rel) {
51897
52219
  return lines;
51898
52220
  }
51899
52221
  // src/core/training/runTraining.ts
51900
- import { existsSync as existsSync109, mkdirSync as mkdirSync27, readFileSync as readFileSync96, renameSync as renameSync5, rmSync as rmSync13, writeFileSync as writeFileSync30 } from "node:fs";
52222
+ import { existsSync as existsSync109, mkdirSync as mkdirSync27, readFileSync as readFileSync97, renameSync as renameSync5, rmSync as rmSync13, writeFileSync as writeFileSync31 } from "node:fs";
51901
52223
  import { homedir as homedir3 } from "node:os";
51902
52224
  import { join as join129, relative as relative29 } from "node:path";
51903
52225
  var EXIT_OK14 = 0;
@@ -52079,7 +52401,7 @@ async function trainWithPreflight(options, said2) {
52079
52401
  const previous = prompts.map((task2) => ({
52080
52402
  path: join129(dir, task2.final),
52081
52403
  partial: join129(dir, task2.output),
52082
- content: existsSync109(join129(dir, task2.final)) ? readFileSync96(join129(dir, task2.final), "utf8") : null
52404
+ content: existsSync109(join129(dir, task2.final)) ? readFileSync97(join129(dir, task2.final), "utf8") : null
52083
52405
  }));
52084
52406
  const tasks = [];
52085
52407
  for (const task2 of prompts) {
@@ -52193,7 +52515,7 @@ async function trainWithPreflight(options, said2) {
52193
52515
  " nothing was written to competencies.yml and the status is unchanged"
52194
52516
  ]);
52195
52517
  }
52196
- const text3 = readFileSync96(abs, "utf8");
52518
+ const text3 = readFileSync97(abs, "utf8");
52197
52519
  const shape = task2.key === CODE_TASK ? LIGHT_SHAPE : RUNS_SHAPE;
52198
52520
  let parsed = parseKnowledgeFile(text3, srcCtx, shape, scope);
52199
52521
  if (!parsed.ok && options.run === "headless") {
@@ -52364,14 +52686,14 @@ async function repairRound(input) {
52364
52686
  };
52365
52687
  }
52366
52688
  if (!existsSync109(abs)) {
52367
- writeFileSync30(abs, input.rejectedText, "utf8");
52689
+ writeFileSync31(abs, input.rejectedText, "utf8");
52368
52690
  return {
52369
52691
  lines: [opened, " the repair sub-agent removed the file instead of rewriting it"],
52370
52692
  spend,
52371
52693
  parsed: null
52372
52694
  };
52373
52695
  }
52374
- const parsed = parseKnowledgeFile(readFileSync96(abs, "utf8"), input.srcCtx, input.shape, input.scope);
52696
+ const parsed = parseKnowledgeFile(readFileSync97(abs, "utf8"), input.srcCtx, input.shape, input.scope);
52375
52697
  const left = knowledgeErrors(parsed).length;
52376
52698
  return {
52377
52699
  lines: [
@@ -52401,11 +52723,11 @@ function quarantine(abs, header2 = []) {
52401
52723
  try {
52402
52724
  const body = header2.length === 0 ? null : `${header2.join(`
52403
52725
  `)}
52404
- ${readFileSync96(abs, "utf8")}`;
52726
+ ${readFileSync97(abs, "utf8")}`;
52405
52727
  rmSync13(kept, { force: true });
52406
52728
  renameSync5(abs, kept);
52407
52729
  if (body !== null)
52408
- writeFileSync30(kept, body, "utf8");
52730
+ writeFileSync31(kept, body, "utf8");
52409
52731
  } catch {
52410
52732
  return abs;
52411
52733
  }
@@ -52435,7 +52757,7 @@ function rollback(previous) {
52435
52757
  quarantine(entry.partial);
52436
52758
  if (entry.content !== null) {
52437
52759
  mkdirSync27(join129(entry.path, ".."), { recursive: true });
52438
- writeFileSync30(entry.path, entry.content, "utf8");
52760
+ writeFileSync31(entry.path, entry.content, "utf8");
52439
52761
  continue;
52440
52762
  }
52441
52763
  if (existsSync109(entry.path))
@@ -52445,7 +52767,7 @@ function rollback(previous) {
52445
52767
  function setExpertStatus(path, status) {
52446
52768
  if (!existsSync109(path))
52447
52769
  return;
52448
- const lines = readFileSync96(path, "utf8").split(`
52770
+ const lines = readFileSync97(path, "utf8").split(`
52449
52771
  `);
52450
52772
  if ((lines[0] ?? "").trimEnd() !== "---")
52451
52773
  return;
@@ -52457,7 +52779,7 @@ function setExpertStatus(path, status) {
52457
52779
  continue;
52458
52780
  const comment = line.indexOf("#", line.indexOf(":") + 1);
52459
52781
  lines[i] = comment === -1 ? `status: ${status}` : `status: ${status} ${line.slice(comment)}`;
52460
- writeFileSync30(path, lines.join(`
52782
+ writeFileSync31(path, lines.join(`
52461
52783
  `), "utf8");
52462
52784
  return;
52463
52785
  }
@@ -52925,7 +53247,7 @@ function message8(error) {
52925
53247
  import { isAbsolute as isAbsolute13, join as join135 } from "node:path";
52926
53248
 
52927
53249
  // src/core/dashboard/model.ts
52928
- import { existsSync as existsSync111, readFileSync as readFileSync97, readdirSync as readdirSync34 } from "node:fs";
53250
+ import { existsSync as existsSync111, readFileSync as readFileSync98, readdirSync as readdirSync34 } from "node:fs";
52929
53251
  import { basename as basename17, join as join131 } from "node:path";
52930
53252
  // src/core/replay/renderReplay.ts
52931
53253
  function renderReplay(loaded) {
@@ -53819,7 +54141,7 @@ function readWaves(path, unreadable) {
53819
54141
  }
53820
54142
  function read2(path, unreadable, label2) {
53821
54143
  try {
53822
- return readFileSync97(path, "utf8");
54144
+ return readFileSync98(path, "utf8");
53823
54145
  } catch {
53824
54146
  unreadable.push(label2);
53825
54147
  return null;
@@ -55701,7 +56023,7 @@ function dashWavesView(model2) {
55701
56023
  return head2 + blocks.join("");
55702
56024
  }
55703
56025
  // src/core/dashboard/writeStatic.ts
55704
- import { mkdirSync as mkdirSync28, writeFileSync as writeFileSync31 } from "node:fs";
56026
+ import { mkdirSync as mkdirSync28, writeFileSync as writeFileSync32 } from "node:fs";
55705
56027
  import { join as join132 } from "node:path";
55706
56028
  var DEFAULT_OUT_DIR = join132(PROJECT_FRAMEWORK_DIR, "cache", "dashboard");
55707
56029
  var INDEX_FILE = "index.html";
@@ -55710,7 +56032,7 @@ function writeStaticDashboard(root2, outDir, generatedAt, now = new Date) {
55710
56032
  const html = renderDashboard(model2);
55711
56033
  const path = join132(outDir, INDEX_FILE);
55712
56034
  mkdirSync28(outDir, { recursive: true });
55713
- writeFileSync31(path, html, "utf8");
56035
+ writeFileSync32(path, html, "utf8");
55714
56036
  return {
55715
56037
  path,
55716
56038
  bytes: Buffer.byteLength(html, "utf8"),
@@ -56252,10 +56574,10 @@ function option2(argv, name) {
56252
56574
  }
56253
56575
 
56254
56576
  // src/cli/commands/retro.ts
56255
- import { writeFileSync as writeFileSync33 } from "node:fs";
56577
+ import { writeFileSync as writeFileSync34 } from "node:fs";
56256
56578
  import { join as join137 } from "node:path";
56257
56579
  // src/core/retro/applyPractices.ts
56258
- import { existsSync as existsSync113, mkdirSync as mkdirSync29, readFileSync as readFileSync98, writeFileSync as writeFileSync32 } from "node:fs";
56580
+ import { existsSync as existsSync113, mkdirSync as mkdirSync29, readFileSync as readFileSync99, writeFileSync as writeFileSync33 } from "node:fs";
56259
56581
  import { dirname as dirname21, join as join136 } from "node:path";
56260
56582
  var PRACTICES_FILE = "practices.md";
56261
56583
  var HEADER2 = [
@@ -56274,7 +56596,7 @@ function applyPractices(root2, run2, proposals, now = new Date) {
56274
56596
  if (proposals.length === 0) {
56275
56597
  return { path, appended: false, reason: "there are no practice proposals to apply" };
56276
56598
  }
56277
- const existing = existsSync113(path) ? readFileSync98(path, "utf8") : "";
56599
+ const existing = existsSync113(path) ? readFileSync99(path, "utf8") : "";
56278
56600
  const stamp4 = `— run ${run2}`;
56279
56601
  if (existing.includes(stamp4)) {
56280
56602
  return { path, appended: false, reason: `practices.md already carries a block for run ${run2}` };
@@ -56286,7 +56608,7 @@ function applyPractices(root2, run2, proposals, now = new Date) {
56286
56608
  `) ? existing : `${existing}
56287
56609
  `;
56288
56610
  mkdirSync29(dirname21(path), { recursive: true });
56289
- writeFileSync32(path, `${base2}
56611
+ writeFileSync33(path, `${base2}
56290
56612
  ${block2}`, "utf8");
56291
56613
  return { path, appended: true, reason: null };
56292
56614
  }
@@ -56440,7 +56762,7 @@ var retroCommand = {
56440
56762
  }
56441
56763
  const report3 = buildRetro(loaded);
56442
56764
  const path = join137(loaded.dir, RETRO_FILE);
56443
- writeFileSync33(path, report3.markdown, "utf8");
56765
+ writeFileSync34(path, report3.markdown, "utf8");
56444
56766
  const lines = [
56445
56767
  `wrote ${path}`,
56446
56768
  ` ${report3.facts.length} fact(s) · ${report3.practices.length} practice proposal(s) ` + `· ${report3.stages.length} proposed stage(s)`
@@ -56823,7 +57145,7 @@ function theOneOpenRun() {
56823
57145
  }
56824
57146
  }
56825
57147
  // src/core/watch/watchViews.ts
56826
- import { existsSync as existsSync114, readdirSync as readdirSync36, readFileSync as readFileSync99 } from "node:fs";
57148
+ import { existsSync as existsSync114, readdirSync as readdirSync36, readFileSync as readFileSync100 } from "node:fs";
56827
57149
  import { join as join138 } from "node:path";
56828
57150
  function watchersDir(runDir2) {
56829
57151
  return join138(runDir2, WATCH_PHASE, WATCHERS_DIR);
@@ -56835,7 +57157,7 @@ function loadCards(runDir2, ctx) {
56835
57157
  const cards = [];
56836
57158
  for (const name of readdirSync36(dir).filter((n) => n.endsWith(".md")).sort()) {
56837
57159
  const id = name.slice(0, -".md".length);
56838
- const text5 = readFileSync99(join138(dir, name), "utf8");
57160
+ const text5 = readFileSync100(join138(dir, name), "utf8");
56839
57161
  cards.push({
56840
57162
  id,
56841
57163
  text: text5,
@@ -57596,7 +57918,7 @@ function quote4(value) {
57596
57918
  return JSON.stringify(value);
57597
57919
  }
57598
57920
  // src/core/adapters/processConfig.ts
57599
- import { existsSync as existsSync116, readFileSync as readFileSync100 } from "node:fs";
57921
+ import { existsSync as existsSync116, readFileSync as readFileSync101 } from "node:fs";
57600
57922
  import { join as join140 } from "node:path";
57601
57923
  var PROCESS_FILE2 = "process.yml";
57602
57924
  var DISABLED = {
@@ -57615,7 +57937,7 @@ function readTicketToolConfig(root2) {
57615
57937
  return DISABLED;
57616
57938
  let doc;
57617
57939
  try {
57618
- doc = parseYaml(readFileSync100(path, "utf8"));
57940
+ doc = parseYaml(readFileSync101(path, "utf8"));
57619
57941
  } catch {
57620
57942
  return { ...DISABLED, path };
57621
57943
  }
@@ -57777,7 +58099,7 @@ function strip(url) {
57777
58099
  return at === -1 ? url : url.slice(at);
57778
58100
  }
57779
58101
  // src/core/adapters/collect.ts
57780
- import { existsSync as existsSync117, readdirSync as readdirSync37, readFileSync as readFileSync101 } from "node:fs";
58102
+ import { existsSync as existsSync117, readdirSync as readdirSync37, readFileSync as readFileSync102 } from "node:fs";
57781
58103
  import { join as join141 } from "node:path";
57782
58104
  var PLAN_PHASE3 = "03-plan";
57783
58105
  function collectMirrorItems(planDir, runId) {
@@ -57785,7 +58107,7 @@ function collectMirrorItems(planDir, runId) {
57785
58107
  const skipped = [];
57786
58108
  for (const name of markdownIn4(join141(planDir, EPICS_DIR))) {
57787
58109
  const path = join141(planDir, EPICS_DIR, name);
57788
- const text6 = readFileSync101(path, "utf8");
58110
+ const text6 = readFileSync102(path, "utf8");
57789
58111
  const parsed = validateEpicFile(text6);
57790
58112
  const epic = parsed.epic;
57791
58113
  if (epic === null) {
@@ -57818,7 +58140,7 @@ function collectMirrorItems(planDir, runId) {
57818
58140
  }
57819
58141
  for (const name of markdownIn4(join141(planDir, STORIES_DIR))) {
57820
58142
  const path = join141(planDir, STORIES_DIR, name);
57821
- const text6 = readFileSync101(path, "utf8");
58143
+ const text6 = readFileSync102(path, "utf8");
57822
58144
  const parsed = validateStoryFile(text6);
57823
58145
  const story = parsed.story;
57824
58146
  if (story === null) {
@@ -57863,7 +58185,7 @@ function reason(issues) {
57863
58185
  return first2.path === "" ? first2.message : `${first2.path}: ${first2.message}`;
57864
58186
  }
57865
58187
  // src/core/adapters/sync.ts
57866
- import { readFileSync as readFileSync102, writeFileSync as writeFileSync34 } from "node:fs";
58188
+ import { readFileSync as readFileSync103, writeFileSync as writeFileSync35 } from "node:fs";
57867
58189
  function planSync(options) {
57868
58190
  const collected = collectMirrorItems(options.planDir, options.runId);
57869
58191
  const actions = new Map;
@@ -57907,14 +58229,14 @@ async function syncTickets(options) {
57907
58229
  return { results, skipped: collected.skipped, created, updated, dryRun: options.dryRun };
57908
58230
  }
57909
58231
  function writeBack(item, ref, externalStatus, sync) {
57910
- const before2 = readFileSync102(item.path, "utf8");
58232
+ const before2 = readFileSync103(item.path, "utf8");
57911
58233
  const after = applyExternal(before2, {
57912
58234
  external: ref,
57913
58235
  ...sync === "two-way" ? { externalStatus } : {}
57914
58236
  });
57915
58237
  if (after === before2)
57916
58238
  return;
57917
- writeFileSync34(item.path, after, "utf8");
58239
+ writeFileSync35(item.path, after, "utf8");
57918
58240
  }
57919
58241
  // src/core/adapters/statusTable.ts
57920
58242
  var DONE_LIKE = ["done", "closed", "resolved", "complete", "completed", "shipped"];
@@ -58159,7 +58481,7 @@ function renderSync(runId, provider, config, outcome) {
58159
58481
  }
58160
58482
 
58161
58483
  // src/cli/commands/interview.ts
58162
- import { existsSync as existsSync120, readFileSync as readFileSync105 } from "node:fs";
58484
+ import { existsSync as existsSync120, readFileSync as readFileSync106 } from "node:fs";
58163
58485
  import { join as join144 } from "node:path";
58164
58486
 
58165
58487
  // src/core/interview/lineReader.ts
@@ -58273,7 +58595,7 @@ function renderNextSteps(hasRun) {
58273
58595
  }
58274
58596
 
58275
58597
  // src/core/interview/runInterview.ts
58276
- import { existsSync as existsSync118, readFileSync as readFileSync103 } from "node:fs";
58598
+ import { existsSync as existsSync118, readFileSync as readFileSync104 } from "node:fs";
58277
58599
 
58278
58600
  // src/core/interview/reply.ts
58279
58601
  var LETTER_RE = /^[A-Ea-e]$/;
@@ -58314,7 +58636,7 @@ async function interview(options) {
58314
58636
  const { questionsPath, out: out2, ctx } = options;
58315
58637
  if (!existsSync118(questionsPath))
58316
58638
  throw new InterviewError(`no questions file at ${questionsPath}`);
58317
- const blocks = openBlocks(parseQuestions(readFileSync103(questionsPath, "utf8")).blocks);
58639
+ const blocks = openBlocks(parseQuestions(readFileSync104(questionsPath, "utf8")).blocks);
58318
58640
  const answered = [];
58319
58641
  let skipped = 0;
58320
58642
  let quit = false;
@@ -58375,7 +58697,7 @@ function renderInterviewSummary(result2) {
58375
58697
  }
58376
58698
 
58377
58699
  // src/core/init/processAnswers.ts
58378
- import { existsSync as existsSync119, readFileSync as readFileSync104 } from "node:fs";
58700
+ import { existsSync as existsSync119, readFileSync as readFileSync105 } from "node:fs";
58379
58701
 
58380
58702
  // src/core/init/githubProject.ts
58381
58703
  import { join as join143 } from "node:path";
@@ -58425,7 +58747,7 @@ function hasProcessAnswer(answers) {
58425
58747
  function collectProcessAnswers(questionsPath, answered) {
58426
58748
  if (answered.length === 0 || !existsSync119(questionsPath))
58427
58749
  return NO_PROCESS_ANSWERS;
58428
- const titles = new Map(parseQuestions(readFileSync104(questionsPath, "utf8")).blocks.map((block2) => [block2.id, block2.title]));
58750
+ const titles = new Map(parseQuestions(readFileSync105(questionsPath, "utf8")).blocks.map((block2) => [block2.id, block2.title]));
58429
58751
  const find = (title) => answered.find((answer2) => titles.get(answer2.q) === title);
58430
58752
  const methodology = find(METHODOLOGY_QUESTION);
58431
58753
  const ticket = find(TICKET_QUESTION);
@@ -58463,7 +58785,7 @@ async function applyProcessAnswers(input) {
58463
58785
  }
58464
58786
  assertEnum(methodology, METHODOLOGIES, "methodology");
58465
58787
  assertEnum(ticketTool, TICKET_TOOLS, "ticket_tool.kind");
58466
- const before2 = existsSync119(path) ? readFileSync104(path, "utf8") : null;
58788
+ const before2 = existsSync119(path) ? readFileSync105(path, "utf8") : null;
58467
58789
  const seed = before2 ?? PROCESS_HEADER + stringifyYaml(buildProcessDocument({
58468
58790
  methodology: null,
58469
58791
  approver: await gitUserName(input.runner, input.root),
@@ -58700,11 +59022,11 @@ No open questions.
58700
59022
  return "no-open-questions";
58701
59023
  }
58702
59024
  function countOpen(path) {
58703
- return openBlocks(parseQuestions(readFileSync105(path, "utf8")).blocks).length;
59025
+ return openBlocks(parseQuestions(readFileSync106(path, "utf8")).blocks).length;
58704
59026
  }
58705
59027
 
58706
59028
  // src/cli/commands/questions.ts
58707
- import { existsSync as existsSync121, readFileSync as readFileSync106, writeFileSync as writeFileSync35 } from "node:fs";
59029
+ import { existsSync as existsSync121, readFileSync as readFileSync107, writeFileSync as writeFileSync36 } from "node:fs";
58708
59030
  import { join as join145 } from "node:path";
58709
59031
  var VALUE_FLAGS15 = ["run", "root", "area"];
58710
59032
  var questionsCommand = {
@@ -58761,7 +59083,7 @@ function lint(argv) {
58761
59083
  const needSource = [];
58762
59084
  for (const path of files) {
58763
59085
  const rel = path.slice(store.runDir.length + 1);
58764
- const text6 = readFileSync106(path, "utf8");
59086
+ const text6 = readFileSync107(path, "utf8");
58765
59087
  const readable = parseQuestions(text6).blocks.length;
58766
59088
  const lost = unreadableQuestionHeadings(text6);
58767
59089
  if (lost.length === 0) {
@@ -58782,7 +59104,7 @@ function lint(argv) {
58782
59104
  askedBy: currentActor(),
58783
59105
  askedAt: nowRfc3339()
58784
59106
  });
58785
- writeFileSync35(path, result2.text, "utf8");
59107
+ writeFileSync36(path, result2.text, "utf8");
58786
59108
  fixed += result2.converted.length;
58787
59109
  needSource.push(...result2.needSource);
58788
59110
  lines.push(` FIX ${rel} — converted ${result2.converted.join(", ")} to the §2.7 grammar`);
@@ -58926,11 +59248,11 @@ function renderFinale(ink) {
58926
59248
  }
58927
59249
 
58928
59250
  // src/core/learn/engine.ts
58929
- import { existsSync as existsSync124, readdirSync as readdirSync38, readFileSync as readFileSync107 } from "node:fs";
59251
+ import { existsSync as existsSync124, readdirSync as readdirSync38, readFileSync as readFileSync108 } from "node:fs";
58930
59252
  import { join as join148 } from "node:path";
58931
59253
 
58932
59254
  // src/core/learn/sandbox.ts
58933
- import { chmodSync, existsSync as existsSync123, mkdirSync as mkdirSync30, rmSync as rmSync14, writeFileSync as writeFileSync36 } from "node:fs";
59255
+ import { chmodSync, existsSync as existsSync123, mkdirSync as mkdirSync30, rmSync as rmSync14, writeFileSync as writeFileSync37 } from "node:fs";
58934
59256
  import { dirname as dirname22, join as join147, resolve as resolve20 } from "node:path";
58935
59257
  import { homedir as homedir4 } from "node:os";
58936
59258
  function defaultSandboxRoot(home = homedir4()) {
@@ -59002,14 +59324,14 @@ function claudeShimScript(selfCmd, scriptPath) {
59002
59324
  `);
59003
59325
  }
59004
59326
  function writeClaudeShim(sandbox, selfCmd) {
59005
- writeFileSync36(sandbox.claudeBin, claudeShimScript(selfCmd, sandbox.scriptPath), "utf8");
59327
+ writeFileSync37(sandbox.claudeBin, claudeShimScript(selfCmd, sandbox.scriptPath), "utf8");
59006
59328
  chmodSync(sandbox.claudeBin, 493);
59007
59329
  }
59008
59330
  function shellQuote3(text6) {
59009
59331
  return `'${text6.replaceAll("'", `'\\''`)}'`;
59010
59332
  }
59011
59333
  function writeScript(sandbox, script) {
59012
- writeFileSync36(sandbox.scriptPath, stringifyScript(script), "utf8");
59334
+ writeFileSync37(sandbox.scriptPath, stringifyScript(script), "utf8");
59013
59335
  rmSync14(`${sandbox.scriptPath}.tally.json`, { force: true });
59014
59336
  }
59015
59337
  function sandboxEnv(sandbox, base2 = process.env) {
@@ -59074,7 +59396,7 @@ async function makeToyRepo(dir) {
59074
59396
  for (const [rel, content] of Object.entries(TOY_FILES)) {
59075
59397
  const path = join147(dir, rel);
59076
59398
  mkdirSync30(dirname22(path), { recursive: true });
59077
- writeFileSync36(path, content, "utf8");
59399
+ writeFileSync37(path, content, "utf8");
59078
59400
  }
59079
59401
  await git3(dir, ["init", "-q"]);
59080
59402
  await git3(dir, ["symbolic-ref", "HEAD", "refs/heads/main"]);
@@ -59200,7 +59522,7 @@ function newestRunId(sandbox) {
59200
59522
  }
59201
59523
  function runIsFinished(runDir2) {
59202
59524
  try {
59203
- const status = /^status:\s*(\S+)/m.exec(readFileSync107(join148(runDir2, "run.yml"), "utf8"))?.[1] ?? "";
59525
+ const status = /^status:\s*(\S+)/m.exec(readFileSync108(join148(runDir2, "run.yml"), "utf8"))?.[1] ?? "";
59204
59526
  return isFinished(status);
59205
59527
  } catch {
59206
59528
  return false;
@@ -59241,7 +59563,7 @@ function sandboxHas(sandbox, rel) {
59241
59563
  }
59242
59564
 
59243
59565
  // src/core/learn/chapters.ts
59244
- import { existsSync as existsSync125, mkdirSync as mkdirSync31, readdirSync as readdirSync39, readFileSync as readFileSync108, writeFileSync as writeFileSync37 } from "node:fs";
59566
+ import { existsSync as existsSync125, mkdirSync as mkdirSync31, readdirSync as readdirSync39, readFileSync as readFileSync109, writeFileSync as writeFileSync38 } from "node:fs";
59245
59567
  import { dirname as dirname23, join as join149 } from "node:path";
59246
59568
  var LEARN_RUN_SLUG = "bulk-pricing";
59247
59569
  var WATCH_PHASE_DIR = "05-watch";
@@ -60169,7 +60491,7 @@ var CHAPTER_6 = {
60169
60491
  return;
60170
60492
  const path = join149(sandbox.workspace, PROJECT_WORK_DIR, run2, ".agent", "watch", "evidence.md");
60171
60493
  mkdirSync31(dirname23(path), { recursive: true });
60172
- writeFileSync37(path, evidenceNote2(), "utf8");
60494
+ writeFileSync38(path, evidenceNote2(), "utf8");
60173
60495
  },
60174
60496
  steps: [
60175
60497
  {
@@ -60492,11 +60814,11 @@ var CHAPTERS = [
60492
60814
  ];
60493
60815
  function writeInto(path, content) {
60494
60816
  mkdirSync31(dirname23(path), { recursive: true });
60495
- writeFileSync37(path, content, "utf8");
60817
+ writeFileSync38(path, content, "utf8");
60496
60818
  }
60497
60819
  function read3(sandbox, rel) {
60498
60820
  try {
60499
- return readFileSync108(join149(sandbox.workspace, rel), "utf8");
60821
+ return readFileSync109(join149(sandbox.workspace, rel), "utf8");
60500
60822
  } catch {
60501
60823
  return "";
60502
60824
  }
@@ -60527,14 +60849,14 @@ function runIdBySlug(sandbox, slug) {
60527
60849
  }
60528
60850
 
60529
60851
  // src/core/learn/progress.ts
60530
- import { existsSync as existsSync126, mkdirSync as mkdirSync32, readFileSync as readFileSync109, writeFileSync as writeFileSync38 } from "node:fs";
60852
+ import { existsSync as existsSync126, mkdirSync as mkdirSync32, readFileSync as readFileSync110, writeFileSync as writeFileSync39 } from "node:fs";
60531
60853
  import { dirname as dirname24 } from "node:path";
60532
60854
  var NO_PROGRESS = { version: 1, completed: [], updatedAt: null };
60533
60855
  function readProgress(path) {
60534
60856
  if (!existsSync126(path))
60535
60857
  return NO_PROGRESS;
60536
60858
  try {
60537
- const doc = JSON.parse(readFileSync109(path, "utf8"));
60859
+ const doc = JSON.parse(readFileSync110(path, "utf8"));
60538
60860
  const completed = doc.completed;
60539
60861
  if (!Array.isArray(completed))
60540
60862
  return NO_PROGRESS;
@@ -60551,7 +60873,7 @@ function readProgress(path) {
60551
60873
  }
60552
60874
  function writeProgress(path, progress) {
60553
60875
  mkdirSync32(dirname24(path), { recursive: true });
60554
- writeFileSync38(path, `${JSON.stringify(progress, null, 2)}
60876
+ writeFileSync39(path, `${JSON.stringify(progress, null, 2)}
60555
60877
  `, "utf8");
60556
60878
  }
60557
60879
  function markComplete(progress, chapter, at) {