tldr-experts 0.14.2 → 0.14.3

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
@@ -7285,6 +7285,8 @@ var EVENT_TYPES = [
7285
7285
  "story.review_retried",
7286
7286
  "story.work_rescued",
7287
7287
  "story.touches_widened",
7288
+ "worktree.foreign_work_aside",
7289
+ "worktree.foreign_work_restored",
7288
7290
  "result.unreadable",
7289
7291
  "operator_note",
7290
7292
  "check.passed",
@@ -24219,65 +24221,9 @@ function effortFlag(args, name = "effort") {
24219
24221
  }
24220
24222
 
24221
24223
  // src/core/facilitator/runAuto.ts
24224
+ import { readFileSync as readFileSync70 } from "node:fs";
24222
24225
  import { join as join93 } from "node:path";
24223
24226
 
24224
- // src/core/budget/wouldExceed.ts
24225
- function totalSpent(budget) {
24226
- return budget.phases.reduce((sum, p) => sum + p.spent_usd, 0);
24227
- }
24228
- function remaining(budget, phaseId) {
24229
- if (phaseId !== undefined) {
24230
- const phase = budget.phases.find((p) => p.id === phaseId);
24231
- if (phase !== undefined)
24232
- return round5(phase.ceiling_usd - phase.spent_usd);
24233
- }
24234
- return round5(budget.ceiling_usd - totalSpent(budget));
24235
- }
24236
- function wouldExceed(budget, phaseId, estimate) {
24237
- const phase = phaseId === null ? undefined : budget.phases.find((p) => p.id === phaseId);
24238
- const scope = phase === undefined ? "run" : "phase";
24239
- const ceiling = phase === undefined ? budget.ceiling_usd : phase.ceiling_usd;
24240
- const spent = phase === undefined ? totalSpent(budget) : phase.spent_usd;
24241
- const left = round5(ceiling - spent);
24242
- const exceeds = round5(spent + estimate) > ceiling;
24243
- const pct = ceiling === 0 ? 100 : (spent + estimate) / ceiling * 100;
24244
- return {
24245
- exceeds,
24246
- blocked: exceeds && budget.on_exceed === "block",
24247
- scope,
24248
- phaseId: phase === undefined ? null : phase.id,
24249
- remaining: left,
24250
- ceiling,
24251
- estimate,
24252
- warns: !exceeds && pct >= budget.warn_at_pct
24253
- };
24254
- }
24255
- function round5(n) {
24256
- return Math.round(n * 1e6) / 1e6;
24257
- }
24258
- function wouldExceedHostTokens(budget, phaseId, spentTokens) {
24259
- const ceiling = hostTokenCeiling(budget, phaseId);
24260
- if (ceiling === null)
24261
- return null;
24262
- const phase = phaseId === null ? undefined : budget.phases.find((p) => p.id === phaseId);
24263
- const over = ceiling > 0 && spentTokens > ceiling;
24264
- return {
24265
- over,
24266
- blocked: over && budget.on_host_tokens_exceed === "block",
24267
- scope: phase === undefined ? "run" : "phase",
24268
- spent: spentTokens,
24269
- ceiling
24270
- };
24271
- }
24272
-
24273
- // src/core/budget/remainingWork.ts
24274
- import { existsSync as existsSync39, readFileSync as readFileSync35 } from "node:fs";
24275
- import { join as join58 } from "node:path";
24276
-
24277
- // src/core/run/buildProgress.ts
24278
- import { existsSync as existsSync37, readFileSync as readFileSync33 } from "node:fs";
24279
- import { join as join56 } from "node:path";
24280
-
24281
24227
  // src/core/build/implicitPlan.ts
24282
24228
  import { existsSync as existsSync36, mkdirSync as mkdirSync10, readFileSync as readFileSync32, readdirSync as readdirSync18, rmSync as rmSync7, statSync as statSync15, writeFileSync as writeFileSync9 } from "node:fs";
24283
24229
  import { join as join55 } from "node:path";
@@ -24918,13 +24864,125 @@ function repoDirOf(workspace, name) {
24918
24864
  throw new GitError(`repo \`${name}\` resolves to ${resolved}, which does not exist`);
24919
24865
  return resolved;
24920
24866
  }
24921
- async function dirtyPaths(cwd) {
24922
- const result2 = await git(["status", "--porcelain"], cwd);
24867
+ async function dirtyEntries(cwd) {
24868
+ const result2 = await git(["status", "--porcelain", "-z"], cwd);
24923
24869
  if (!result2.ok)
24924
24870
  throw new GitError(`\`git status\` failed in ${cwd}: ${firstLine7(result2.stderr)}`);
24871
+ const fields = result2.stdout.split("\x00");
24872
+ const out = [];
24873
+ for (let i = 0;i < fields.length; i += 1) {
24874
+ const record2 = fields[i] ?? "";
24875
+ if (record2 === "")
24876
+ continue;
24877
+ const code = record2.slice(0, 2);
24878
+ const path = record2.slice(3);
24879
+ if (code.startsWith("R") || code.startsWith("C") || code[1] === "R" || code[1] === "C")
24880
+ i += 1;
24881
+ out.push({ code, path, entry: `${code} ${path}`.trim() });
24882
+ }
24883
+ return out;
24884
+ }
24885
+ async function dirtyPaths(cwd) {
24886
+ return (await dirtyEntries(cwd)).map((entry) => entry.entry);
24887
+ }
24888
+ function literalPathspec(path) {
24889
+ return `:(literal)${path}`;
24890
+ }
24891
+ async function submodulePaths(cwd) {
24892
+ const result2 = await git(["config", "--file", ".gitmodules", "--get-regexp", "^submodule\\..*\\.path$"], cwd);
24893
+ const paths = new Set;
24894
+ if (!result2.ok)
24895
+ return paths;
24896
+ for (const line of result2.stdout.split(`
24897
+ `)) {
24898
+ const at = line.indexOf(" ");
24899
+ if (at === -1)
24900
+ continue;
24901
+ const path = line.slice(at + 1).trim();
24902
+ if (path !== "")
24903
+ paths.add(path);
24904
+ }
24905
+ return paths;
24906
+ }
24907
+ async function operationInProgress(cwd) {
24908
+ const gitDir = await git(["rev-parse", "--git-dir"], cwd);
24909
+ if (!gitDir.ok)
24910
+ return null;
24911
+ const dir = resolve9(cwd, gitDir.stdout.trim());
24912
+ for (const [name, what] of [
24913
+ ["MERGE_HEAD", "a merge"],
24914
+ ["rebase-merge", "a rebase"],
24915
+ ["rebase-apply", "a rebase or `git am`"],
24916
+ ["CHERRY_PICK_HEAD", "a cherry-pick"],
24917
+ ["REVERT_HEAD", "a revert"],
24918
+ ["BISECT_LOG", "a bisect"]
24919
+ ]) {
24920
+ if (existsSync32(join51(dir, name)))
24921
+ return what;
24922
+ }
24923
+ return null;
24924
+ }
24925
+ async function unmergedPaths(cwd) {
24926
+ const result2 = await git(["diff", "--name-only", "--diff-filter=U"], cwd);
24925
24927
  return result2.stdout.split(`
24926
24928
  `).map((line) => line.trim()).filter((line) => line !== "");
24927
24929
  }
24930
+ async function stashPushPaths(cwd, message4, paths) {
24931
+ if (paths.length === 0)
24932
+ return { ok: false, hash: "", message: message4, detail: "no paths to set aside" };
24933
+ const pushed = await git(["stash", "push", "-u", "-m", message4, "--", ...paths.map(literalPathspec)], cwd);
24934
+ if (!pushed.ok) {
24935
+ return { ok: false, hash: "", message: message4, detail: firstLine7(pushed.stderr) || firstLine7(pushed.stdout) };
24936
+ }
24937
+ const hash = await fullShaOf(cwd, "refs/stash");
24938
+ if (hash === "")
24939
+ return { ok: false, hash: "", message: message4, detail: "`git stash push` left no refs/stash to name" };
24940
+ return { ok: true, hash, message: message4, detail: firstLine7(pushed.stdout) };
24941
+ }
24942
+ async function stashRefFor(cwd, hash) {
24943
+ const listed = await git(["stash", "list", "--format=%H %gd"], cwd);
24944
+ if (!listed.ok)
24945
+ return "";
24946
+ for (const line of listed.stdout.split(`
24947
+ `)) {
24948
+ const [sha, ref] = line.trim().split(" ");
24949
+ if (sha === hash && ref !== undefined)
24950
+ return ref;
24951
+ }
24952
+ return "";
24953
+ }
24954
+ async function stashPop(cwd, hash) {
24955
+ const ref = await stashRefFor(cwd, hash);
24956
+ if (ref === "") {
24957
+ return {
24958
+ restored: false,
24959
+ indexRestored: false,
24960
+ conflicts: [],
24961
+ ref: "",
24962
+ detail: "the stash entry is no longer in `git stash list`"
24963
+ };
24964
+ }
24965
+ const withIndex = await git(["stash", "pop", "--index", ref], cwd);
24966
+ if (withIndex.ok)
24967
+ return { restored: true, indexRestored: true, conflicts: [], ref, detail: firstLine7(withIndex.stdout) };
24968
+ const plain = await git(["stash", "pop", ref], cwd);
24969
+ if (plain.ok) {
24970
+ return {
24971
+ restored: true,
24972
+ indexRestored: false,
24973
+ conflicts: [],
24974
+ ref,
24975
+ detail: `${firstLine7(plain.stdout)} — the index could not be reinstated (${firstLine7(withIndex.stderr)})`
24976
+ };
24977
+ }
24978
+ return {
24979
+ restored: false,
24980
+ indexRestored: false,
24981
+ conflicts: await unmergedPaths(cwd),
24982
+ ref: await stashRefFor(cwd, hash),
24983
+ detail: firstLine7(plain.stderr) || firstLine7(plain.stdout) || `git stash pop exited ${String(plain.exitCode)}`
24984
+ };
24985
+ }
24928
24986
  function stateDirPrefixes(workspaceRoot, repoDir) {
24929
24987
  const base = resolve9(repoDir);
24930
24988
  const prefixes = [];
@@ -26621,7 +26679,13 @@ function readOrEmpty2(path) {
26621
26679
  }
26622
26680
  }
26623
26681
 
26682
+ // src/core/run/boundary.ts
26683
+ import { existsSync as existsSync38, readFileSync as readFileSync34, readdirSync as readdirSync19 } from "node:fs";
26684
+ import { join as join57 } from "node:path";
26685
+
26624
26686
  // src/core/run/buildProgress.ts
26687
+ import { existsSync as existsSync37, readFileSync as readFileSync33 } from "node:fs";
26688
+ import { join as join56 } from "node:path";
26625
26689
  var PLAN_DIR = "03-plan";
26626
26690
  var BUILD_PHASE2 = "04-build";
26627
26691
  var STATUS_RE2 = /^status\s*:\s*(\w+)\s*$/m;
@@ -26658,7 +26722,7 @@ function buildProgress(runDir) {
26658
26722
  }
26659
26723
  waves.push({ id: wave.id, stories });
26660
26724
  }
26661
- return { waves, done, total, cost_usd: round6(cost), implicit };
26725
+ return { waves, done, total, cost_usd: round5(cost), implicit };
26662
26726
  }
26663
26727
  function renderBuildProgress(progress) {
26664
26728
  return progress.waves.map((wave) => `${wave.id} [${wave.stories.map((s) => `${s.id} ${s.status}`).join(", ")}]`).join(" ");
@@ -26713,17 +26777,569 @@ function costByStory(runDir) {
26713
26777
  const story = typeof payload.story === "string" ? payload.story : payload.key;
26714
26778
  if (typeof story !== "string" || story === "")
26715
26779
  continue;
26716
- costs.set(story, round6((costs.get(story) ?? 0) + (typeof event2.cost_usd === "number" ? event2.cost_usd : 0)));
26780
+ costs.set(story, round5((costs.get(story) ?? 0) + (typeof event2.cost_usd === "number" ? event2.cost_usd : 0)));
26717
26781
  }
26718
26782
  return costs;
26719
26783
  }
26720
- function round6(n) {
26784
+ function round5(n) {
26721
26785
  return Math.round(n * 100) / 100;
26722
26786
  }
26723
26787
 
26788
+ // src/core/run/boundary.ts
26789
+ var OUTSIDE_SURFACE = "work outside the declared surface is a boundary change — a human decides whether to widen the scope";
26790
+ var NAMED_PATHS = 8;
26791
+ var SURFACE_HANDOFFS = ["01-what/handoff.md", "02-how/handoff.md"];
26792
+ var STORIES_DIR2 = "stories";
26793
+ var EPICS_DIR2 = "epics";
26794
+ function normalisePath2(path) {
26795
+ let out = path.trim().split("\\").join("/");
26796
+ while (out.startsWith("./"))
26797
+ out = out.slice(2);
26798
+ while (out.endsWith("/"))
26799
+ out = out.slice(0, -1);
26800
+ return out;
26801
+ }
26802
+ function inSurface(changed, surface) {
26803
+ const path = normalisePath2(changed);
26804
+ for (const raw of surface) {
26805
+ const entry = normalisePath2(raw);
26806
+ if (entry === "")
26807
+ continue;
26808
+ if (path === entry || path.startsWith(`${entry}/`))
26809
+ return true;
26810
+ }
26811
+ return false;
26812
+ }
26813
+ function unqualifiedCitedPaths(handoffText, repos) {
26814
+ const out = [];
26815
+ const seen = new Set;
26816
+ if (handoffText === "")
26817
+ return out;
26818
+ for (const line of handoffText.split(`
26819
+ `)) {
26820
+ const token = parseSrcToken(line, repos);
26821
+ if (token === null)
26822
+ continue;
26823
+ for (const ref of token.refs) {
26824
+ if (ref.kind !== "file" || ref.repo !== null)
26825
+ continue;
26826
+ const path = normalisePath2(ref.path);
26827
+ if (path === "" || seen.has(path))
26828
+ continue;
26829
+ seen.add(path);
26830
+ out.push(path);
26831
+ }
26832
+ }
26833
+ return out;
26834
+ }
26835
+ function storyTouches(runDir) {
26836
+ const dir = join57(runDir, PLAN_DIR, STORIES_DIR2);
26837
+ if (!existsSync38(dir))
26838
+ return [];
26839
+ const out = [];
26840
+ let names;
26841
+ try {
26842
+ names = readdirSync19(dir).filter((name) => name.endsWith(".md")).sort();
26843
+ } catch {
26844
+ return [];
26845
+ }
26846
+ for (const name of names) {
26847
+ const front2 = readFront(join57(dir, name));
26848
+ if (front2 === null)
26849
+ continue;
26850
+ const repo = typeof front2.repo === "string" ? front2.repo : "";
26851
+ const id = typeof front2.id === "string" && front2.id !== "" ? front2.id : name.replace(/\.md$/, "");
26852
+ out.push({ story: id, repo, touches: stringList2(front2.touches) });
26853
+ }
26854
+ return out;
26855
+ }
26856
+ function declaredTouchesFor(runDir, storyId) {
26857
+ for (const row2 of storyTouches(runDir)) {
26858
+ if (row2.story === storyId)
26859
+ return row2.touches;
26860
+ }
26861
+ const implicit = implicitStorySurface(runDir);
26862
+ return implicit !== null && implicit.story === storyId ? implicit.touches : null;
26863
+ }
26864
+ function implicitStorySurface(runDir) {
26865
+ const doc = readYaml(join57(runDir, BUILD_PHASE2, IMPLICIT_PLAN_FILE));
26866
+ const story = doc?.story;
26867
+ if (story === null || typeof story !== "object")
26868
+ return null;
26869
+ const row2 = story;
26870
+ return {
26871
+ story: typeof row2.id === "string" && row2.id !== "" ? row2.id : IMPLICIT_STORY_ID,
26872
+ repo: typeof row2.repo === "string" ? row2.repo : "",
26873
+ touches: stringList2(row2.touches)
26874
+ };
26875
+ }
26876
+ function deriveSurface(runDir, workspace) {
26877
+ const byRepo = new Map;
26878
+ const unqualified = [];
26879
+ const excluded = [];
26880
+ let cited = 0;
26881
+ let declared = 0;
26882
+ const add = (repo, path, count) => {
26883
+ const entry = normalisePath2(path);
26884
+ if (entry === "")
26885
+ return;
26886
+ if (isStatePath(entry)) {
26887
+ if (!excluded.includes(entry))
26888
+ excluded.push(entry);
26889
+ return;
26890
+ }
26891
+ count();
26892
+ if (repo === "") {
26893
+ if (!unqualified.includes(entry))
26894
+ unqualified.push(entry);
26895
+ return;
26896
+ }
26897
+ const list = byRepo.get(repo) ?? [];
26898
+ if (!list.includes(entry))
26899
+ list.push(entry);
26900
+ byRepo.set(repo, list);
26901
+ };
26902
+ const repoNames = new Set(workspace.repos.keys());
26903
+ for (const rel of SURFACE_HANDOFFS) {
26904
+ const text2 = readText2(join57(runDir, rel));
26905
+ if (text2 === "")
26906
+ continue;
26907
+ for (const entry of citedRepoPaths(text2, workspace)) {
26908
+ add(entry.repo, entry.path, () => {
26909
+ cited += 1;
26910
+ });
26911
+ }
26912
+ for (const path of unqualifiedCitedPaths(text2, repoNames)) {
26913
+ add("", path, () => {
26914
+ cited += 1;
26915
+ });
26916
+ }
26917
+ }
26918
+ const stories = storyTouches(runDir);
26919
+ const sources = stories.length > 0 ? stories : [implicitStorySurface(runDir)].filter((s) => s !== null);
26920
+ for (const story of sources) {
26921
+ for (const path of story.touches) {
26922
+ add(story.repo, path, () => {
26923
+ declared += 1;
26924
+ });
26925
+ }
26926
+ }
26927
+ return { byRepo, unqualified, cited, declared, excluded };
26928
+ }
26929
+ function epicTargets2(runDir, workspace) {
26930
+ const out = [];
26931
+ const seen = new Set;
26932
+ const push2 = (epic2, repo, branch) => {
26933
+ if (repo === "" || branch === "")
26934
+ return;
26935
+ const key = `${repo}\x00${branch}`;
26936
+ if (seen.has(key))
26937
+ return;
26938
+ seen.add(key);
26939
+ const rel = repoPath(workspace, repo);
26940
+ out.push({
26941
+ epic: epic2,
26942
+ repo,
26943
+ branch,
26944
+ base: workspace.defaultBranches.get(repo) ?? "main",
26945
+ dir: rel
26946
+ });
26947
+ };
26948
+ const dir = join57(runDir, PLAN_DIR, EPICS_DIR2);
26949
+ if (existsSync38(dir)) {
26950
+ let names = [];
26951
+ try {
26952
+ names = readdirSync19(dir).filter((name) => name.endsWith(".md")).sort();
26953
+ } catch {
26954
+ names = [];
26955
+ }
26956
+ for (const name of names) {
26957
+ const front2 = readFront(join57(dir, name));
26958
+ if (front2 === null)
26959
+ continue;
26960
+ const id = typeof front2.id === "string" ? front2.id : name.replace(/\.md$/, "");
26961
+ const branch = typeof front2.branch === "string" ? front2.branch : "";
26962
+ const repos = stringList2(front2.repos);
26963
+ const fallback = storyTouches(runDir).map((story) => story.repo).filter((repo) => repo !== "");
26964
+ for (const repo of repos.length > 0 ? repos : [...new Set(fallback)])
26965
+ push2(id, repo, branch);
26966
+ }
26967
+ if (out.length > 0)
26968
+ return out;
26969
+ }
26970
+ const doc = readYaml(join57(runDir, BUILD_PHASE2, IMPLICIT_PLAN_FILE));
26971
+ const epic = doc?.epic;
26972
+ if (epic !== null && typeof epic === "object") {
26973
+ const row2 = epic;
26974
+ const id = typeof row2.id === "string" ? row2.id : "E1";
26975
+ const branch = typeof row2.branch === "string" ? row2.branch : "";
26976
+ for (const repo of stringList2(row2.repos))
26977
+ push2(id, repo, branch);
26978
+ }
26979
+ return out;
26980
+ }
26981
+ async function diffRepo(target) {
26982
+ if (target.dir === null)
26983
+ return { changed: null, reason: `${target.repo} is not in ${PROJECT_WORKSPACE_FILE}` };
26984
+ if (!existsSync38(target.dir))
26985
+ return { changed: null, reason: `${target.repo} is not on disk` };
26986
+ const baseCheck = await git(["rev-parse", "--verify", "--quiet", `${target.base}^{commit}`], target.dir);
26987
+ if (!baseCheck.ok) {
26988
+ return {
26989
+ changed: null,
26990
+ reason: `\`${target.base}\`, the \`default_branch\` ${PROJECT_WORKSPACE_FILE} records for ${target.repo}, does not ` + "resolve there — the record is stale, so nothing in this repo has a base to be diffed against " + "(run `tldrx doctor`)"
26991
+ };
26992
+ }
26993
+ const branchCheck = await git(["rev-parse", "--verify", "--quiet", `${target.branch}^{commit}`], target.dir);
26994
+ if (!branchCheck.ok) {
26995
+ return { changed: null, reason: `\`${target.branch}\` does not resolve in ${target.repo}` };
26996
+ }
26997
+ const range = `${target.base}...${target.branch}`;
26998
+ const diff = await git(["diff", "--name-only", range], target.dir);
26999
+ if (!diff.ok)
27000
+ return { changed: null, reason: `\`git diff ${range}\` failed in ${target.repo}` };
27001
+ const changed = diff.stdout.split(`
27002
+ `).map(normalisePath2).filter((path) => path !== "");
27003
+ return { changed, reason: null };
27004
+ }
27005
+ async function evaluateBoundary(input) {
27006
+ if (input.phaseId !== BUILD_PHASE2)
27007
+ return { ok: true, detail: "n/a (not a build stage)" };
27008
+ const workspace = loadWorkspace(input.root);
27009
+ const targets = epicTargets2(input.runDir, workspace);
27010
+ if (targets.length === 0)
27011
+ return { ok: true, detail: "n/a (no plan naming an epic branch)" };
27012
+ const surface = deriveSurface(input.runDir, workspace);
27013
+ const excludedPart = surface.excluded.length === 0 ? "" : `; ${String(surface.excluded.length)} state path(s) excluded from the surface: ` + surface.excluded.slice(0, NAMED_PATHS).join(", ") + (surface.excluded.length > NAMED_PATHS ? `, +${String(surface.excluded.length - NAMED_PATHS)} more` : "");
27014
+ if (surface.cited + surface.declared === 0) {
27015
+ return {
27016
+ ok: true,
27017
+ detail: `n/a (the run declares no surface: no cited path, no \`touches:\`)${excludedPart}`
27018
+ };
27019
+ }
27020
+ const repos = [];
27021
+ for (const target of targets) {
27022
+ const { changed, reason } = await diffRepo(target);
27023
+ if (changed === null) {
27024
+ repos.push({ target, changed: null, reason, outside: [], state: [] });
27025
+ continue;
27026
+ }
27027
+ const allowed = [...surface.byRepo.get(target.repo) ?? [], ...surface.unqualified];
27028
+ const state = changed.filter(isStatePath);
27029
+ const product = changed.filter((path) => !isStatePath(path));
27030
+ repos.push({
27031
+ target,
27032
+ changed: product,
27033
+ reason: null,
27034
+ outside: product.filter((path) => !inSurface(path, allowed)),
27035
+ state
27036
+ });
27037
+ }
27038
+ const measured = repos.filter((repo) => repo.changed !== null);
27039
+ if (measured.length === 0) {
27040
+ const why = repos.map((repo) => repo.reason ?? "unavailable").join("; ");
27041
+ return { ok: true, detail: `n/a (nothing could be diffed: ${why})${excludedPart}` };
27042
+ }
27043
+ const audited = measured.reduce((sum, repo) => sum + (repo.changed?.length ?? 0), 0);
27044
+ const outside = measured.flatMap((repo) => repo.outside.map((path) => `${repo.target.repo}:${path}`));
27045
+ const unread = repos.filter((repo) => repo.changed === null);
27046
+ const unreadPart = unread.length === 0 ? "" : `; not diffed: ${unread.map((repo) => repo.reason ?? "unavailable").join(", ")}`;
27047
+ const range = measured.map((repo) => `${repo.target.repo} ${repo.target.base}...${repo.target.branch}`).join(", ");
27048
+ if (outside.length === 0) {
27049
+ return {
27050
+ ok: true,
27051
+ detail: `${String(audited)} changed path(s), 0 outside the surface (${range})${unreadPart}${excludedPart}`
27052
+ };
27053
+ }
27054
+ const named = outside.slice(0, NAMED_PATHS);
27055
+ const rest = outside.length - named.length;
27056
+ return {
27057
+ ok: false,
27058
+ detail: `${String(audited)} changed path(s), ${String(outside.length)} outside the surface: ` + `${named.join(", ")}${rest > 0 ? `, +${String(rest)} more` : ""}` + `${unreadPart}${excludedPart}; ${OUTSIDE_SURFACE}`
27059
+ };
27060
+ }
27061
+ function readText2(path) {
27062
+ if (!existsSync38(path))
27063
+ return "";
27064
+ try {
27065
+ return readFileSync34(path, "utf8");
27066
+ } catch {
27067
+ return "";
27068
+ }
27069
+ }
27070
+ function readYaml(path) {
27071
+ const text2 = readText2(path);
27072
+ if (text2 === "")
27073
+ return null;
27074
+ try {
27075
+ return parseYaml(text2);
27076
+ } catch {
27077
+ return null;
27078
+ }
27079
+ }
27080
+ function readFront(path) {
27081
+ const text2 = readText2(path);
27082
+ if (text2 === "")
27083
+ return null;
27084
+ const parsed = parseFrontMatter(text2);
27085
+ const doc = parsed.doc;
27086
+ if (doc === null || typeof doc !== "object" || Array.isArray(doc))
27087
+ return null;
27088
+ return doc;
27089
+ }
27090
+ function stringList2(value) {
27091
+ if (!Array.isArray(value))
27092
+ return [];
27093
+ return value.filter((item) => typeof item === "string");
27094
+ }
27095
+
27096
+ // src/core/build/foreignWork.ts
27097
+ var FOREIGN_ASIDE_EVENT = "worktree.foreign_work_aside";
27098
+ var FOREIGN_RESTORED_EVENT = "worktree.foreign_work_restored";
27099
+ var NAMED_PATHS2 = 5;
27100
+ function classifyDirty(input) {
27101
+ const own = [];
27102
+ const foreign = [];
27103
+ const overlapping = [];
27104
+ for (const entry of input.entries) {
27105
+ if (input.statePrefixes.some((p) => entry.path === p || entry.path.startsWith(`${p}/`))) {
27106
+ own.push(entry);
27107
+ continue;
27108
+ }
27109
+ if (isStatePath(entry.path)) {
27110
+ overlapping.push({
27111
+ entry,
27112
+ why: "this is your own directory that happens to be named like tldrx state — the framework " + "never stashes a `tldrx-work/`, `.tldrx/` or `.agent/` path, so this one is yours to move"
27113
+ });
27114
+ continue;
27115
+ }
27116
+ const submodule = [...input.submodules].find((path) => entry.path === path || entry.path.startsWith(`${path}/`));
27117
+ if (submodule !== undefined) {
27118
+ overlapping.push({
27119
+ entry,
27120
+ why: `\`${submodule}\` is a submodule — its uncommitted state is another repository's, ` + "and a stash here does not carry it"
27121
+ });
27122
+ continue;
27123
+ }
27124
+ if (inSurface(entry.path, input.touches)) {
27125
+ overlapping.push({
27126
+ entry,
27127
+ why: "a pending story declares this path in its `touches:` — setting your version of it " + "aside would hand the developer a tree you did not agree to"
27128
+ });
27129
+ continue;
27130
+ }
27131
+ foreign.push(entry);
27132
+ }
27133
+ return { own, foreign, overlapping };
27134
+ }
27135
+ function stashMessage(runId) {
27136
+ return `tldrx ${runId} foreign work`;
27137
+ }
27138
+ function shellQuote2(word) {
27139
+ return `'${word.split("'").join(`'\\''`)}'`;
27140
+ }
27141
+ function stashCommand(repoDir, runId, paths) {
27142
+ const pathspecs = paths.map((path) => shellQuote2(literalPathspec(path))).join(" ");
27143
+ return `git -C ${shellQuote2(repoDir)} stash push -u -m ${shellQuote2(stashMessage(runId))} -- ${pathspecs}`;
27144
+ }
27145
+ async function setAsideForeignWork(repo, repoDir, runId, paths) {
27146
+ if (paths.length === 0)
27147
+ return { stash: null, reason: null };
27148
+ const busy = await operationInProgress(repoDir);
27149
+ if (busy !== null)
27150
+ return { stash: null, reason: `${repo} is in the middle of ${busy}` };
27151
+ const pushed = await stashPushPaths(repoDir, stashMessage(runId), paths);
27152
+ if (!pushed.ok)
27153
+ return { stash: null, reason: pushed.detail };
27154
+ return { stash: { repo, repoDir, paths, hash: pushed.hash, message: pushed.message }, reason: null };
27155
+ }
27156
+ async function restoreForeignWork(stash) {
27157
+ const popped = await stashPop(stash.repoDir, stash.hash);
27158
+ return {
27159
+ stash,
27160
+ restored: popped.restored,
27161
+ indexRestored: popped.indexRestored,
27162
+ conflicts: popped.conflicts,
27163
+ command: popped.ref === "" ? "" : `git -C ${stash.repoDir} stash pop ${popped.ref}`,
27164
+ detail: popped.detail
27165
+ };
27166
+ }
27167
+ function namePaths(paths, max = NAMED_PATHS2) {
27168
+ const named = paths.slice(0, max).join(", ");
27169
+ return paths.length > max ? `${named}, +${String(paths.length - max)} more` : named;
27170
+ }
27171
+ function notRestoredLine(outcome) {
27172
+ return notRestoredSentence({
27173
+ repo: outcome.stash.repo,
27174
+ hash: outcome.stash.hash,
27175
+ paths: outcome.conflicts.length > 0 ? outcome.conflicts : outcome.stash.paths,
27176
+ command: outcome.command,
27177
+ detail: outcome.detail
27178
+ });
27179
+ }
27180
+ var NOT_RESTORED_MARK = "foreign work NOT restored";
27181
+ function notRestoredSentence(row2) {
27182
+ const how = row2.command === "" ? row2.detail : row2.command;
27183
+ return `${NOT_RESTORED_MARK} in ${row2.repo} — stash ${row2.hash.slice(0, 12)} ` + `still holds ${namePaths(row2.paths)}: ${how}`;
27184
+ }
27185
+ function notRestoredSummary(eventsText) {
27186
+ const failed2 = new Map;
27187
+ for (const line of eventsText.split(`
27188
+ `)) {
27189
+ if (line.trim() === "")
27190
+ continue;
27191
+ let event2;
27192
+ try {
27193
+ event2 = JSON.parse(line);
27194
+ } catch {
27195
+ continue;
27196
+ }
27197
+ if (event2.type !== FOREIGN_RESTORED_EVENT)
27198
+ continue;
27199
+ const payload = typeof event2.payload === "object" && event2.payload !== null ? event2.payload : {};
27200
+ const hash = typeof payload.stash_ref === "string" ? payload.stash_ref : "";
27201
+ if (hash === "")
27202
+ continue;
27203
+ if (payload.restored === true) {
27204
+ failed2.delete(hash);
27205
+ continue;
27206
+ }
27207
+ failed2.set(hash, notRestoredSentence({
27208
+ repo: typeof payload.repo === "string" ? payload.repo : "a repo",
27209
+ hash,
27210
+ paths: Array.isArray(payload.paths) ? payload.paths.filter((p) => typeof p === "string") : [],
27211
+ command: typeof payload.command === "string" ? payload.command : "",
27212
+ detail: typeof payload.detail === "string" ? payload.detail : "git refused the pop"
27213
+ }));
27214
+ }
27215
+ return failed2.size === 0 ? null : [...failed2.values()].join(" ");
27216
+ }
27217
+ function restoredLine(outcome) {
27218
+ const head2 = `${outcome.stash.repo}: foreign work restored from stash ` + `${outcome.stash.hash.slice(0, 12)} (${namePaths(outcome.stash.paths)})`;
27219
+ return outcome.indexRestored ? head2 : `${head2} — the staged snapshot could NOT be reinstated, so those paths are back UNSTAGED; ` + "`git add` them as you had them";
27220
+ }
27221
+ function unrestored(outcomes) {
27222
+ return outcomes.filter((outcome) => !outcome.restored);
27223
+ }
27224
+ var MAX_RECORDED_PATHS = 40;
27225
+ function asidePayload(stash, reason) {
27226
+ const omitted = stash.paths.length - MAX_RECORDED_PATHS;
27227
+ return {
27228
+ repo: stash.repo,
27229
+ paths: stash.paths.slice(0, MAX_RECORDED_PATHS),
27230
+ ...omitted > 0 ? { paths_omitted: omitted } : {},
27231
+ stash_ref: stash.hash,
27232
+ reason
27233
+ };
27234
+ }
27235
+ function restoredPayload(outcome) {
27236
+ const paths = outcome.restored ? outcome.stash.paths : outcome.conflicts.length > 0 ? outcome.conflicts : outcome.stash.paths;
27237
+ const omitted = paths.length - MAX_RECORDED_PATHS;
27238
+ return {
27239
+ repo: outcome.stash.repo,
27240
+ paths: paths.slice(0, MAX_RECORDED_PATHS),
27241
+ ...omitted > 0 ? { paths_omitted: omitted } : {},
27242
+ stash_ref: outcome.stash.hash,
27243
+ restored: outcome.restored,
27244
+ index_restored: outcome.indexRestored,
27245
+ ...outcome.restored ? {} : { conflicts: outcome.conflicts.slice(0, MAX_RECORDED_PATHS) },
27246
+ ...outcome.restored ? {} : { command: outcome.command },
27247
+ ...outcome.restored ? {} : { detail: outcome.detail }
27248
+ };
27249
+ }
27250
+ function pendingAsides(eventsText, repoDirOf2) {
27251
+ const open = new Map;
27252
+ for (const line of eventsText.split(`
27253
+ `)) {
27254
+ if (line.trim() === "")
27255
+ continue;
27256
+ let event2;
27257
+ try {
27258
+ event2 = JSON.parse(line);
27259
+ } catch {
27260
+ continue;
27261
+ }
27262
+ const payload = typeof event2.payload === "object" && event2.payload !== null ? event2.payload : {};
27263
+ const hash = typeof payload.stash_ref === "string" ? payload.stash_ref : "";
27264
+ if (hash === "")
27265
+ continue;
27266
+ if (event2.type === FOREIGN_RESTORED_EVENT && payload.restored === true) {
27267
+ open.delete(hash);
27268
+ continue;
27269
+ }
27270
+ if (event2.type !== FOREIGN_ASIDE_EVENT)
27271
+ continue;
27272
+ const repo = typeof payload.repo === "string" ? payload.repo : "";
27273
+ const repoDir = repo === "" ? null : repoDirOf2(repo);
27274
+ if (repoDir === null)
27275
+ continue;
27276
+ open.set(hash, {
27277
+ repo,
27278
+ repoDir,
27279
+ paths: Array.isArray(payload.paths) ? payload.paths.filter((p) => typeof p === "string") : [],
27280
+ hash,
27281
+ message: ""
27282
+ });
27283
+ }
27284
+ return [...open.values()];
27285
+ }
27286
+
27287
+ // src/core/budget/wouldExceed.ts
27288
+ function totalSpent(budget) {
27289
+ return budget.phases.reduce((sum, p) => sum + p.spent_usd, 0);
27290
+ }
27291
+ function remaining(budget, phaseId) {
27292
+ if (phaseId !== undefined) {
27293
+ const phase = budget.phases.find((p) => p.id === phaseId);
27294
+ if (phase !== undefined)
27295
+ return round6(phase.ceiling_usd - phase.spent_usd);
27296
+ }
27297
+ return round6(budget.ceiling_usd - totalSpent(budget));
27298
+ }
27299
+ function wouldExceed(budget, phaseId, estimate) {
27300
+ const phase = phaseId === null ? undefined : budget.phases.find((p) => p.id === phaseId);
27301
+ const scope = phase === undefined ? "run" : "phase";
27302
+ const ceiling = phase === undefined ? budget.ceiling_usd : phase.ceiling_usd;
27303
+ const spent = phase === undefined ? totalSpent(budget) : phase.spent_usd;
27304
+ const left = round6(ceiling - spent);
27305
+ const exceeds = round6(spent + estimate) > ceiling;
27306
+ const pct = ceiling === 0 ? 100 : (spent + estimate) / ceiling * 100;
27307
+ return {
27308
+ exceeds,
27309
+ blocked: exceeds && budget.on_exceed === "block",
27310
+ scope,
27311
+ phaseId: phase === undefined ? null : phase.id,
27312
+ remaining: left,
27313
+ ceiling,
27314
+ estimate,
27315
+ warns: !exceeds && pct >= budget.warn_at_pct
27316
+ };
27317
+ }
27318
+ function round6(n) {
27319
+ return Math.round(n * 1e6) / 1e6;
27320
+ }
27321
+ function wouldExceedHostTokens(budget, phaseId, spentTokens) {
27322
+ const ceiling = hostTokenCeiling(budget, phaseId);
27323
+ if (ceiling === null)
27324
+ return null;
27325
+ const phase = phaseId === null ? undefined : budget.phases.find((p) => p.id === phaseId);
27326
+ const over = ceiling > 0 && spentTokens > ceiling;
27327
+ return {
27328
+ over,
27329
+ blocked: over && budget.on_host_tokens_exceed === "block",
27330
+ scope: phase === undefined ? "run" : "phase",
27331
+ spent: spentTokens,
27332
+ ceiling
27333
+ };
27334
+ }
27335
+
27336
+ // src/core/budget/remainingWork.ts
27337
+ import { existsSync as existsSync40, readFileSync as readFileSync36 } from "node:fs";
27338
+ import { join as join59 } from "node:path";
27339
+
26724
27340
  // src/core/build/fixlist.ts
26725
- import { existsSync as existsSync38, mkdirSync as mkdirSync11, readdirSync as readdirSync19, readFileSync as readFileSync34, writeFileSync as writeFileSync10 } from "node:fs";
26726
- import { join as join57 } from "node:path";
27341
+ import { existsSync as existsSync39, mkdirSync as mkdirSync11, readdirSync as readdirSync20, readFileSync as readFileSync35, writeFileSync as writeFileSync10 } from "node:fs";
27342
+ import { join as join58 } from "node:path";
26727
27343
  var FIXLIST_DIR = "fixlist";
26728
27344
  var MAX_FIXLIST_ROUNDS = 1;
26729
27345
  var DISPOSITIONS = ["fix-now", "defer-with-log", "refuted", "out-of-scope"];
@@ -26998,21 +27614,21 @@ function fixlistRel(phaseDir, storyId, round4) {
26998
27614
  return `${phaseDir}/${FIXLIST_DIR}/${storyId}-${String(round4)}.md`;
26999
27615
  }
27000
27616
  function fixlistDir(runDir, phaseDir) {
27001
- return join57(runDir, phaseDir, FIXLIST_DIR);
27617
+ return join58(runDir, phaseDir, FIXLIST_DIR);
27002
27618
  }
27003
27619
  function writeFixlist(runDir, phaseDir, parts) {
27004
27620
  const dir = fixlistDir(runDir, phaseDir);
27005
27621
  mkdirSync11(dir, { recursive: true });
27006
- writeFileSync10(join57(dir, `${parts.storyId}-${String(parts.round)}.md`), renderFixlist(parts), "utf8");
27622
+ writeFileSync10(join58(dir, `${parts.storyId}-${String(parts.round)}.md`), renderFixlist(parts), "utf8");
27007
27623
  return fixlistRel(phaseDir, parts.storyId, parts.round);
27008
27624
  }
27009
27625
  function fixlistRounds(runDir, phaseDir, storyId) {
27010
27626
  const dir = fixlistDir(runDir, phaseDir);
27011
- if (!existsSync38(dir))
27627
+ if (!existsSync39(dir))
27012
27628
  return [];
27013
27629
  let entries;
27014
27630
  try {
27015
- entries = readdirSync19(dir);
27631
+ entries = readdirSync20(dir);
27016
27632
  } catch {
27017
27633
  return [];
27018
27634
  }
@@ -27022,7 +27638,7 @@ function fixlistRounds(runDir, phaseDir, storyId) {
27022
27638
  if (match === null)
27023
27639
  continue;
27024
27640
  const round4 = Number(match[1] ?? "0");
27025
- const path = join57(dir, entry);
27641
+ const path = join58(dir, entry);
27026
27642
  rows.push({ round: round4, path, rel: fixlistRel(phaseDir, storyId, round4), findings: readFindings(path) });
27027
27643
  }
27028
27644
  return rows.sort((a, b) => a.round - b.round);
@@ -27032,11 +27648,11 @@ function latestFixlist(runDir, phaseDir, storyId) {
27032
27648
  return rounds[rounds.length - 1] ?? null;
27033
27649
  }
27034
27650
  function readFixlistAt(path, rel) {
27035
- if (!existsSync38(path))
27651
+ if (!existsSync39(path))
27036
27652
  return null;
27037
27653
  let text2;
27038
27654
  try {
27039
- text2 = readFileSync34(path, "utf8");
27655
+ text2 = readFileSync35(path, "utf8");
27040
27656
  } catch {
27041
27657
  return null;
27042
27658
  }
@@ -27048,7 +27664,7 @@ function readFixlistAt(path, rel) {
27048
27664
  }
27049
27665
  function readFindings(path) {
27050
27666
  try {
27051
- return parseFixlistFile(readFileSync34(path, "utf8"));
27667
+ return parseFixlistFile(readFileSync35(path, "utf8"));
27052
27668
  } catch {
27053
27669
  return [];
27054
27670
  }
@@ -27412,7 +28028,7 @@ function measure(input) {
27412
28028
  if (all.length === 0)
27413
28029
  return fallback;
27414
28030
  const ids = new Set(all.map((story) => story.id));
27415
- const prices = progress.implicit ? new Map : loadPlanPrices(join58(input.runDir, PLAN_DIR), ids).prices;
28031
+ const prices = progress.implicit ? new Map : loadPlanPrices(join59(input.runDir, PLAN_DIR), ids).prices;
27416
28032
  const verdicts = reviewVerdictsByStory(input.runDir);
27417
28033
  const caps = new CapMath(input, prices, all.length);
27418
28034
  const stories = [];
@@ -27561,12 +28177,12 @@ class CapMath {
27561
28177
  }
27562
28178
  function reviewVerdictsByStory(runDir) {
27563
28179
  const counts = new Map;
27564
- const path = join58(runDir, "events.jsonl");
27565
- if (!existsSync39(path))
28180
+ const path = join59(runDir, "events.jsonl");
28181
+ if (!existsSync40(path))
27566
28182
  return counts;
27567
28183
  let text3;
27568
28184
  try {
27569
- text3 = readFileSync35(path, "utf8");
28185
+ text3 = readFileSync36(path, "utf8");
27570
28186
  } catch {
27571
28187
  return counts;
27572
28188
  }
@@ -27733,11 +28349,11 @@ function round7(n) {
27733
28349
  }
27734
28350
 
27735
28351
  // src/core/run/attempts.ts
27736
- import { existsSync as existsSync40, readFileSync as readFileSync36 } from "node:fs";
27737
- import { join as join59 } from "node:path";
28352
+ import { existsSync as existsSync41, readFileSync as readFileSync37 } from "node:fs";
28353
+ import { join as join60 } from "node:path";
27738
28354
  function stageAttempts(runDir, phase, stage2) {
27739
28355
  const costs = [];
27740
- for (const event2 of readEvents(join59(runDir, "events.jsonl"))) {
28356
+ for (const event2 of readEvents(join60(runDir, "events.jsonl"))) {
27741
28357
  if (event2.type !== "agent.result" || event2.stage !== stage2)
27742
28358
  continue;
27743
28359
  const eventPhase = event2.payload.phase;
@@ -27754,12 +28370,12 @@ function renderAttempts(attempts) {
27754
28370
  return `attempts: ${String(attempts.costs.length)} · ${each}`;
27755
28371
  }
27756
28372
  function readEvents(path) {
27757
- if (!existsSync40(path))
28373
+ if (!existsSync41(path))
27758
28374
  return [];
27759
28375
  const events = [];
27760
28376
  let text3;
27761
28377
  try {
27762
- text3 = readFileSync36(path, "utf8");
28378
+ text3 = readFileSync37(path, "utf8");
27763
28379
  } catch {
27764
28380
  return events;
27765
28381
  }
@@ -27782,8 +28398,8 @@ import { existsSync as existsSync43, readFileSync as readFileSync39 } from "node
27782
28398
  import { join as join62 } from "node:path";
27783
28399
 
27784
28400
  // src/core/run/checks.ts
27785
- import { existsSync as existsSync41, readFileSync as readFileSync37 } from "node:fs";
27786
- import { basename as basename9, join as join60 } from "node:path";
28401
+ import { existsSync as existsSync42, readFileSync as readFileSync38 } from "node:fs";
28402
+ import { basename as basename9, join as join61 } from "node:path";
27787
28403
  var WRITE_TIME_ONLY = ["no-reask", "budget-gate", "dod"];
27788
28404
  async function runChecks(checks, ctx) {
27789
28405
  const outcomes = [];
@@ -27829,12 +28445,12 @@ function checkClaimSources(ctx) {
27829
28445
  continue;
27830
28446
  }
27831
28447
  for (const hit of hits) {
27832
- if (!existsSync41(hit.absolute)) {
28448
+ if (!existsSync42(hit.absolute)) {
27833
28449
  if (isHandoffOutput)
27834
28450
  failures.push(`${hit.path} was declared but never written`);
27835
28451
  continue;
27836
28452
  }
27837
- const text3 = readFileSync37(hit.absolute, "utf8");
28453
+ const text3 = readFileSync38(hit.absolute, "utf8");
27838
28454
  if (isHandoffOutput || isHandoff(text3)) {
27839
28455
  handoffs++;
27840
28456
  const validation = validateHandoff(text3, srcCtx);
@@ -27977,7 +28593,7 @@ function checkPlan(ctx) {
27977
28593
  if (!writesPlanArtefacts(ctx.stage.outputs)) {
27978
28594
  return { id: "plan", status: "skipped", detail: "the stage declares no waves.yml output" };
27979
28595
  }
27980
- const planDir = join60(ctx.runDir, ctx.stage.phase);
28596
+ const planDir = join61(ctx.runDir, ctx.stage.phase);
27981
28597
  const workspace = loadWorkspace(ctx.root);
27982
28598
  const report2 = validatePlan(planDir, workspace.commands, workspace.iterationCommands);
27983
28599
  if (!report2.ok) {
@@ -27996,10 +28612,10 @@ function checkSchema(ctx) {
27996
28612
  ["budget.yml", validateRunBudget]
27997
28613
  ];
27998
28614
  for (const [name, validate2] of pairs) {
27999
- const path = join60(ctx.runDir, name);
28000
- if (!existsSync41(path))
28615
+ const path = join61(ctx.runDir, name);
28616
+ if (!existsSync42(path))
28001
28617
  return { id: "schema", status: "failed", detail: `${name} is missing` };
28002
- const validation = validate2(parseYaml(readFileSync37(path, "utf8")));
28618
+ const validation = validate2(parseYaml(readFileSync38(path, "utf8")));
28003
28619
  if (!validation.ok) {
28004
28620
  const first2 = validation.issues[0];
28005
28621
  return { id: "schema", status: "failed", detail: `${name}: ${first2?.path ?? ""} ${first2?.message ?? ""}`.trim() };
@@ -28086,316 +28702,6 @@ ${stderr}`.split(`
28086
28702
  return last.length > max ? `${last.slice(0, max - 1)}…` : last;
28087
28703
  }
28088
28704
 
28089
- // src/core/run/boundary.ts
28090
- import { existsSync as existsSync42, readFileSync as readFileSync38, readdirSync as readdirSync20 } from "node:fs";
28091
- import { join as join61 } from "node:path";
28092
- var OUTSIDE_SURFACE = "work outside the declared surface is a boundary change — a human decides whether to widen the scope";
28093
- var NAMED_PATHS = 8;
28094
- var SURFACE_HANDOFFS = ["01-what/handoff.md", "02-how/handoff.md"];
28095
- var STORIES_DIR2 = "stories";
28096
- var EPICS_DIR2 = "epics";
28097
- function normalisePath2(path) {
28098
- let out = path.trim().split("\\").join("/");
28099
- while (out.startsWith("./"))
28100
- out = out.slice(2);
28101
- while (out.endsWith("/"))
28102
- out = out.slice(0, -1);
28103
- return out;
28104
- }
28105
- function inSurface(changed, surface) {
28106
- const path = normalisePath2(changed);
28107
- for (const raw of surface) {
28108
- const entry = normalisePath2(raw);
28109
- if (entry === "")
28110
- continue;
28111
- if (path === entry || path.startsWith(`${entry}/`))
28112
- return true;
28113
- }
28114
- return false;
28115
- }
28116
- function unqualifiedCitedPaths(handoffText, repos) {
28117
- const out = [];
28118
- const seen = new Set;
28119
- if (handoffText === "")
28120
- return out;
28121
- for (const line of handoffText.split(`
28122
- `)) {
28123
- const token = parseSrcToken(line, repos);
28124
- if (token === null)
28125
- continue;
28126
- for (const ref of token.refs) {
28127
- if (ref.kind !== "file" || ref.repo !== null)
28128
- continue;
28129
- const path = normalisePath2(ref.path);
28130
- if (path === "" || seen.has(path))
28131
- continue;
28132
- seen.add(path);
28133
- out.push(path);
28134
- }
28135
- }
28136
- return out;
28137
- }
28138
- function storyTouches(runDir) {
28139
- const dir = join61(runDir, PLAN_DIR, STORIES_DIR2);
28140
- if (!existsSync42(dir))
28141
- return [];
28142
- const out = [];
28143
- let names;
28144
- try {
28145
- names = readdirSync20(dir).filter((name) => name.endsWith(".md")).sort();
28146
- } catch {
28147
- return [];
28148
- }
28149
- for (const name of names) {
28150
- const front2 = readFront(join61(dir, name));
28151
- if (front2 === null)
28152
- continue;
28153
- const repo = typeof front2.repo === "string" ? front2.repo : "";
28154
- const id = typeof front2.id === "string" && front2.id !== "" ? front2.id : name.replace(/\.md$/, "");
28155
- out.push({ story: id, repo, touches: stringList2(front2.touches) });
28156
- }
28157
- return out;
28158
- }
28159
- function declaredTouchesFor(runDir, storyId) {
28160
- for (const row2 of storyTouches(runDir)) {
28161
- if (row2.story === storyId)
28162
- return row2.touches;
28163
- }
28164
- const implicit = implicitStorySurface(runDir);
28165
- return implicit !== null && implicit.story === storyId ? implicit.touches : null;
28166
- }
28167
- function implicitStorySurface(runDir) {
28168
- const doc = readYaml(join61(runDir, BUILD_PHASE2, IMPLICIT_PLAN_FILE));
28169
- const story = doc?.story;
28170
- if (story === null || typeof story !== "object")
28171
- return null;
28172
- const row2 = story;
28173
- return {
28174
- story: typeof row2.id === "string" && row2.id !== "" ? row2.id : IMPLICIT_STORY_ID,
28175
- repo: typeof row2.repo === "string" ? row2.repo : "",
28176
- touches: stringList2(row2.touches)
28177
- };
28178
- }
28179
- function deriveSurface(runDir, workspace) {
28180
- const byRepo = new Map;
28181
- const unqualified = [];
28182
- const excluded = [];
28183
- let cited = 0;
28184
- let declared = 0;
28185
- const add = (repo, path, count) => {
28186
- const entry = normalisePath2(path);
28187
- if (entry === "")
28188
- return;
28189
- if (isStatePath(entry)) {
28190
- if (!excluded.includes(entry))
28191
- excluded.push(entry);
28192
- return;
28193
- }
28194
- count();
28195
- if (repo === "") {
28196
- if (!unqualified.includes(entry))
28197
- unqualified.push(entry);
28198
- return;
28199
- }
28200
- const list = byRepo.get(repo) ?? [];
28201
- if (!list.includes(entry))
28202
- list.push(entry);
28203
- byRepo.set(repo, list);
28204
- };
28205
- const repoNames = new Set(workspace.repos.keys());
28206
- for (const rel of SURFACE_HANDOFFS) {
28207
- const text3 = readText2(join61(runDir, rel));
28208
- if (text3 === "")
28209
- continue;
28210
- for (const entry of citedRepoPaths(text3, workspace)) {
28211
- add(entry.repo, entry.path, () => {
28212
- cited += 1;
28213
- });
28214
- }
28215
- for (const path of unqualifiedCitedPaths(text3, repoNames)) {
28216
- add("", path, () => {
28217
- cited += 1;
28218
- });
28219
- }
28220
- }
28221
- const stories = storyTouches(runDir);
28222
- const sources = stories.length > 0 ? stories : [implicitStorySurface(runDir)].filter((s) => s !== null);
28223
- for (const story of sources) {
28224
- for (const path of story.touches) {
28225
- add(story.repo, path, () => {
28226
- declared += 1;
28227
- });
28228
- }
28229
- }
28230
- return { byRepo, unqualified, cited, declared, excluded };
28231
- }
28232
- function epicTargets2(runDir, workspace) {
28233
- const out = [];
28234
- const seen = new Set;
28235
- const push2 = (epic2, repo, branch) => {
28236
- if (repo === "" || branch === "")
28237
- return;
28238
- const key = `${repo}\x00${branch}`;
28239
- if (seen.has(key))
28240
- return;
28241
- seen.add(key);
28242
- const rel = repoPath(workspace, repo);
28243
- out.push({
28244
- epic: epic2,
28245
- repo,
28246
- branch,
28247
- base: workspace.defaultBranches.get(repo) ?? "main",
28248
- dir: rel
28249
- });
28250
- };
28251
- const dir = join61(runDir, PLAN_DIR, EPICS_DIR2);
28252
- if (existsSync42(dir)) {
28253
- let names = [];
28254
- try {
28255
- names = readdirSync20(dir).filter((name) => name.endsWith(".md")).sort();
28256
- } catch {
28257
- names = [];
28258
- }
28259
- for (const name of names) {
28260
- const front2 = readFront(join61(dir, name));
28261
- if (front2 === null)
28262
- continue;
28263
- const id = typeof front2.id === "string" ? front2.id : name.replace(/\.md$/, "");
28264
- const branch = typeof front2.branch === "string" ? front2.branch : "";
28265
- const repos = stringList2(front2.repos);
28266
- const fallback = storyTouches(runDir).map((story) => story.repo).filter((repo) => repo !== "");
28267
- for (const repo of repos.length > 0 ? repos : [...new Set(fallback)])
28268
- push2(id, repo, branch);
28269
- }
28270
- if (out.length > 0)
28271
- return out;
28272
- }
28273
- const doc = readYaml(join61(runDir, BUILD_PHASE2, IMPLICIT_PLAN_FILE));
28274
- const epic = doc?.epic;
28275
- if (epic !== null && typeof epic === "object") {
28276
- const row2 = epic;
28277
- const id = typeof row2.id === "string" ? row2.id : "E1";
28278
- const branch = typeof row2.branch === "string" ? row2.branch : "";
28279
- for (const repo of stringList2(row2.repos))
28280
- push2(id, repo, branch);
28281
- }
28282
- return out;
28283
- }
28284
- async function diffRepo(target) {
28285
- if (target.dir === null)
28286
- return { changed: null, reason: `${target.repo} is not in ${PROJECT_WORKSPACE_FILE}` };
28287
- if (!existsSync42(target.dir))
28288
- return { changed: null, reason: `${target.repo} is not on disk` };
28289
- const baseCheck = await git(["rev-parse", "--verify", "--quiet", `${target.base}^{commit}`], target.dir);
28290
- if (!baseCheck.ok) {
28291
- return {
28292
- changed: null,
28293
- reason: `\`${target.base}\`, the \`default_branch\` ${PROJECT_WORKSPACE_FILE} records for ${target.repo}, does not ` + "resolve there — the record is stale, so nothing in this repo has a base to be diffed against " + "(run `tldrx doctor`)"
28294
- };
28295
- }
28296
- const branchCheck = await git(["rev-parse", "--verify", "--quiet", `${target.branch}^{commit}`], target.dir);
28297
- if (!branchCheck.ok) {
28298
- return { changed: null, reason: `\`${target.branch}\` does not resolve in ${target.repo}` };
28299
- }
28300
- const range = `${target.base}...${target.branch}`;
28301
- const diff = await git(["diff", "--name-only", range], target.dir);
28302
- if (!diff.ok)
28303
- return { changed: null, reason: `\`git diff ${range}\` failed in ${target.repo}` };
28304
- const changed = diff.stdout.split(`
28305
- `).map(normalisePath2).filter((path) => path !== "");
28306
- return { changed, reason: null };
28307
- }
28308
- async function evaluateBoundary(input) {
28309
- if (input.phaseId !== BUILD_PHASE2)
28310
- return { ok: true, detail: "n/a (not a build stage)" };
28311
- const workspace = loadWorkspace(input.root);
28312
- const targets = epicTargets2(input.runDir, workspace);
28313
- if (targets.length === 0)
28314
- return { ok: true, detail: "n/a (no plan naming an epic branch)" };
28315
- const surface = deriveSurface(input.runDir, workspace);
28316
- const excludedPart = surface.excluded.length === 0 ? "" : `; ${String(surface.excluded.length)} state path(s) excluded from the surface: ` + surface.excluded.slice(0, NAMED_PATHS).join(", ") + (surface.excluded.length > NAMED_PATHS ? `, +${String(surface.excluded.length - NAMED_PATHS)} more` : "");
28317
- if (surface.cited + surface.declared === 0) {
28318
- return {
28319
- ok: true,
28320
- detail: `n/a (the run declares no surface: no cited path, no \`touches:\`)${excludedPart}`
28321
- };
28322
- }
28323
- const repos = [];
28324
- for (const target of targets) {
28325
- const { changed, reason } = await diffRepo(target);
28326
- if (changed === null) {
28327
- repos.push({ target, changed: null, reason, outside: [], state: [] });
28328
- continue;
28329
- }
28330
- const allowed = [...surface.byRepo.get(target.repo) ?? [], ...surface.unqualified];
28331
- const state = changed.filter(isStatePath);
28332
- const product = changed.filter((path) => !isStatePath(path));
28333
- repos.push({
28334
- target,
28335
- changed: product,
28336
- reason: null,
28337
- outside: product.filter((path) => !inSurface(path, allowed)),
28338
- state
28339
- });
28340
- }
28341
- const measured = repos.filter((repo) => repo.changed !== null);
28342
- if (measured.length === 0) {
28343
- const why = repos.map((repo) => repo.reason ?? "unavailable").join("; ");
28344
- return { ok: true, detail: `n/a (nothing could be diffed: ${why})${excludedPart}` };
28345
- }
28346
- const audited = measured.reduce((sum, repo) => sum + (repo.changed?.length ?? 0), 0);
28347
- const outside = measured.flatMap((repo) => repo.outside.map((path) => `${repo.target.repo}:${path}`));
28348
- const unread = repos.filter((repo) => repo.changed === null);
28349
- const unreadPart = unread.length === 0 ? "" : `; not diffed: ${unread.map((repo) => repo.reason ?? "unavailable").join(", ")}`;
28350
- const range = measured.map((repo) => `${repo.target.repo} ${repo.target.base}...${repo.target.branch}`).join(", ");
28351
- if (outside.length === 0) {
28352
- return {
28353
- ok: true,
28354
- detail: `${String(audited)} changed path(s), 0 outside the surface (${range})${unreadPart}${excludedPart}`
28355
- };
28356
- }
28357
- const named = outside.slice(0, NAMED_PATHS);
28358
- const rest = outside.length - named.length;
28359
- return {
28360
- ok: false,
28361
- detail: `${String(audited)} changed path(s), ${String(outside.length)} outside the surface: ` + `${named.join(", ")}${rest > 0 ? `, +${String(rest)} more` : ""}` + `${unreadPart}${excludedPart}; ${OUTSIDE_SURFACE}`
28362
- };
28363
- }
28364
- function readText2(path) {
28365
- if (!existsSync42(path))
28366
- return "";
28367
- try {
28368
- return readFileSync38(path, "utf8");
28369
- } catch {
28370
- return "";
28371
- }
28372
- }
28373
- function readYaml(path) {
28374
- const text3 = readText2(path);
28375
- if (text3 === "")
28376
- return null;
28377
- try {
28378
- return parseYaml(text3);
28379
- } catch {
28380
- return null;
28381
- }
28382
- }
28383
- function readFront(path) {
28384
- const text3 = readText2(path);
28385
- if (text3 === "")
28386
- return null;
28387
- const parsed = parseFrontMatter(text3);
28388
- const doc = parsed.doc;
28389
- if (doc === null || typeof doc !== "object" || Array.isArray(doc))
28390
- return null;
28391
- return doc;
28392
- }
28393
- function stringList2(value) {
28394
- if (!Array.isArray(value))
28395
- return [];
28396
- return value.filter((item) => typeof item === "string");
28397
- }
28398
-
28399
28705
  // src/core/run/autoGate.ts
28400
28706
  var AUTO_GATE_ACTOR = "auto";
28401
28707
  async function evaluateAutoGate(input) {
@@ -29696,11 +30002,11 @@ function gateTimeoutNotification(ctx, costUsd, waitedMs, policy = null) {
29696
30002
  }
29697
30003
  };
29698
30004
  }
29699
- function stageDoneNotification(ctx, costUsd, unmetered = 0) {
30005
+ function stageDoneNotification(ctx, costUsd, unmetered = 0, note = null) {
29700
30006
  const figure = spentFigure({ usd: costUsd, unmetered, metered: unmetered > 0 && costUsd === 0 ? 0 : 1 });
29701
30007
  return {
29702
30008
  ...base(ctx, "stage.done"),
29703
- summary: `${ctx.runId} finished ${ctx.stage ?? "a stage"} for ${figure} and moved on. ` + "No decision is waiting on you.",
30009
+ summary: `${ctx.runId} finished ${ctx.stage ?? "a stage"} for ${figure} and moved on. ` + (note === null ? "No decision is waiting on you." : note),
29704
30010
  command: null,
29705
30011
  detail: { cost_usd: costUsd, ...unmetered === 0 ? {} : { unmetered_tasks: unmetered } }
29706
30012
  };
@@ -29718,12 +30024,12 @@ function budgetNotification(ctx, spentUsd, ceilingUsd, tally = { usd: spentUsd,
29718
30024
  }
29719
30025
  };
29720
30026
  }
29721
- function runEndNotification(ctx, exitCode, spentUsd, lastLine2, tally = { usd: spentUsd, unmetered: 0, metered: 1 }) {
30027
+ function runEndNotification(ctx, exitCode, spentUsd, lastLine2, tally = { usd: spentUsd, unmetered: 0, metered: 1 }, note = null) {
29722
30028
  const kind = exitCode === 0 ? "run.finished" : "run.failed";
29723
30029
  const verb = exitCode === 0 ? "finished" : "stopped";
29724
30030
  return {
29725
30031
  ...base(ctx, kind),
29726
- summary: `${ctx.runId}: the loop ${verb} with exit ${String(exitCode)} ` + `(${exitFamily(exitCode)}), ${spentFigure(tally)} spent by this loop. ${lastLine2}`,
30032
+ summary: `${ctx.runId}: the loop ${verb} with exit ${String(exitCode)} ` + `(${exitFamily(exitCode)}), ${spentFigure(tally)} spent by this loop. ${lastLine2}` + `${note === null ? "" : ` ${note}`}`,
29727
30033
  command: exitCode === 0 ? null : `tldrx run status ${ctx.runId}`,
29728
30034
  detail: {
29729
30035
  exit_code: exitCode,
@@ -32762,10 +33068,11 @@ function renderBuildHandoff(parts) {
32762
33068
  "",
32763
33069
  "## Unknowns",
32764
33070
  "",
32765
- ...notDone.length === 0 && (parts.carried ?? []).length === 0 && (parts.unreadableStories ?? []).length === 0 ? [`- none — every scheduled story reached \`done\` and no carried finding is unowned ` + `[src: absent:04-build/log]`] : [],
33071
+ ...notDone.length === 0 && (parts.carried ?? []).length === 0 && (parts.unreadableStories ?? []).length === 0 && (parts.foreignWork ?? []).length === 0 ? [`- none — every scheduled story reached \`done\` and no carried finding is unowned ` + `[src: absent:04-build/log]`] : [],
32766
33072
  ...notDone.map((o) => `- ${o.id} is \`${o.status}\` and needs a human: ${o.reason ?? "see the review"} ` + `[src: ${o.reviewRel}:1]`),
32767
33073
  ...carriedBullets(parts.carried ?? []),
32768
33074
  ...(parts.unreadableStories ?? []).map((row2) => `- a story file could not be read, so its carried findings were not checked: ` + `\`${row2.rel}\` — ${row2.reason} [src: absent:${row2.rel}]`),
33075
+ ...(parts.foreignWork ?? []).map((row2) => `- uncommitted work this stage set aside in \`${row2.repo}\` was NOT restored and needs a human: ` + `stash \`${row2.stashRef.slice(0, 12)}\` still holds ${row2.paths.join(", ")} — ${row2.detail}. ` + `Take it back with \`${row2.command === "" ? `git -C <${row2.repo}> stash list` : row2.command}\` ` + `[src: absent:04-build/log]`),
32769
33076
  "",
32770
33077
  "## Evidence ledger",
32771
33078
  "",
@@ -33324,36 +33631,65 @@ async function foreignEpicRefusal(state, parts, stories) {
33324
33631
  }
33325
33632
  return null;
33326
33633
  }
33634
+ function relaunchCommand(mode, runId) {
33635
+ return mode === "headless" ? `tldrx run auto ${runId}` : "tldrx next";
33636
+ }
33327
33637
  async function dirtyRepoRefusal(parts, stories) {
33328
33638
  const seen = new Set;
33639
+ const aside = [];
33329
33640
  let ignored = 0;
33641
+ const refuse2 = (refusal) => ({ refusal, ignored, aside: [] });
33330
33642
  for (const planned of stories) {
33331
33643
  const name = planned.story.repo;
33332
33644
  if (seen.has(name))
33333
33645
  continue;
33334
33646
  seen.add(name);
33335
33647
  const dir = repoDirOf(parts.workspace, name);
33336
- const split = partitionDirty(await dirtyPaths(dir), stateDirPrefixes(parts.workspace.root, dir));
33337
- ignored += split.state.length;
33338
- const dirty = split.product;
33339
- if (dirty.length === 0)
33648
+ const entries = await dirtyEntries(dir);
33649
+ const touches = stories.filter((row2) => row2.story.repo === name).flatMap((row2) => [...row2.story.touches]);
33650
+ const verdict2 = classifyDirty({
33651
+ entries,
33652
+ statePrefixes: stateDirPrefixes(parts.workspace.root, dir),
33653
+ touches,
33654
+ submodules: await submodulePaths(dir)
33655
+ });
33656
+ ignored += verdict2.own.length;
33657
+ if (verdict2.foreign.length === 0 && verdict2.overlapping.length === 0)
33340
33658
  continue;
33341
33659
  const branch = await currentBranch(dir);
33342
- return { ignored, refusal: {
33343
- lines: [
33344
- `[tldrx] build: repo \`${name}\` has ${String(dirty.length)} uncommitted change(s) on ` + `\`${branch}\` — refusing to cut an epic branch from a dirty tree.`,
33345
- ` ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? `, +${String(dirty.length - 5)} more` : ""}`,
33346
- " Why: the base pre-flight runs in this checkout, so uncommitted product changes here",
33347
- " land inside the measurement that decides whether a red DoD is the story's fault.",
33348
- " Commit them, or set them aside and take them back afterwards:",
33349
- ` git -C ${dir} stash push -u -m "tldrx ${parts.runId} foreign work"`,
33350
- " tldrx next",
33351
- ` git -C ${dir} stash pop`
33352
- ],
33353
- error: `repo \`${name}\` has uncommitted changes`
33354
- } };
33660
+ const busy = await operationInProgress(dir);
33661
+ if (busy !== null) {
33662
+ return refuse2({
33663
+ lines: [
33664
+ `[tldrx] build: repo \`${name}\` is in the middle of ${busy} on \`${branch}\` — refusing to cut ` + "an epic branch, and refusing to stash anything into that state.",
33665
+ ` ${namePaths(entries.map((entry) => entry.entry))}`,
33666
+ ` Finish or abort ${busy} first, then run \`${relaunchCommand(parts.mode, parts.runId)}\`.`
33667
+ ],
33668
+ error: `repo \`${name}\` is in the middle of ${busy}`
33669
+ });
33670
+ }
33671
+ if (verdict2.overlapping.length > 0) {
33672
+ const paths = verdict2.overlapping.map((row2) => row2.entry.path);
33673
+ const setAside = verdict2.foreign.map((entry) => entry.path);
33674
+ return refuse2({
33675
+ lines: [
33676
+ `[tldrx] build: repo \`${name}\` has ${String(paths.length)} uncommitted change(s) on ` + `\`${branch}\` that a pending story is about to write — refusing to cut an epic branch ` + "from a dirty tree.",
33677
+ ` ${namePaths(verdict2.overlapping.map((row2) => row2.entry.entry))}`,
33678
+ ...verdict2.overlapping.slice(0, NAMED_PATHS2).map((row2) => ` · ${row2.entry.path}: ${row2.why}`),
33679
+ " Why: the base pre-flight runs in this checkout, so uncommitted product changes here",
33680
+ " land inside the measurement that decides whether a red DoD is the story's fault.",
33681
+ " Commit them, or set exactly these paths aside and take them back afterwards:",
33682
+ ` ${stashCommand(dir, parts.runId, paths)}`,
33683
+ ` ${relaunchCommand(parts.mode, parts.runId)}`,
33684
+ ` git -C ${shellQuote2(dir)} stash pop`,
33685
+ ...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."]
33686
+ ],
33687
+ error: `repo \`${name}\` has uncommitted changes a pending story declares`
33688
+ });
33689
+ }
33690
+ aside.push({ repo: name, repoDir: dir, paths: verdict2.foreign.map((entry) => entry.path) });
33355
33691
  }
33356
- return { refusal: null, ignored };
33692
+ return { refusal: null, ignored, aside };
33357
33693
  }
33358
33694
  function epicRows(state, parts, outcomes) {
33359
33695
  const rows = [];
@@ -35659,20 +35995,28 @@ async function buildExecutor(ctx) {
35659
35995
  epicBranches: [...session.claimedEpics],
35660
35996
  branchModel: session.branchModel.kind
35661
35997
  };
35998
+ const withRestore = async (outcome) => {
35999
+ const lines = await session.restoreForeignWorkAside();
36000
+ return withClaims(lines.length === 0 ? outcome : { ...outcome, lines: [...outcome.lines, ...lines] });
36001
+ };
35662
36002
  try {
35663
36003
  if (ctx.mode === "prepare") {
35664
- return withClaims(ctx.review ? await session.prepareReviewOnly() : await session.prepare());
36004
+ return await withRestore(ctx.review ? await session.prepareReviewOnly() : await session.prepare());
35665
36005
  }
35666
36006
  if (ctx.mode === "commit") {
35667
- return withClaims(ctx.review ? await session.commitReview() : await session.commit());
36007
+ return await withRestore(ctx.review ? await session.commitReview() : await session.commit());
35668
36008
  }
35669
- return withClaims(await session.runAll());
36009
+ return await withRestore(await session.runAll());
35670
36010
  } catch (error) {
35671
36011
  if (error instanceof BaseGateFailure)
35672
- return withClaims(refusedOnBase(session, error));
36012
+ return await withRestore(refusedOnBase(session, error));
35673
36013
  if (error instanceof GitError || error instanceof PlanLoadError) {
35674
- return withClaims(failed3(ctx, error.message, session.tasks));
36014
+ const outcome = failed3(ctx, error.message, session.tasks);
36015
+ return await withRestore({ ...outcome, lines: [...session.reportLines, ...outcome.lines] });
35675
36016
  }
36017
+ try {
36018
+ await session.restoreForeignWorkAside();
36019
+ } catch {}
35676
36020
  throw error;
35677
36021
  }
35678
36022
  }
@@ -35686,6 +36030,7 @@ function refusedOnBase(session, error) {
35686
36030
  costUsd: session.tasks.reduce((sum, task2) => sum + task2.costUsd, 0),
35687
36031
  outputs: [],
35688
36032
  lines: [
36033
+ ...session.reportLines,
35689
36034
  `[tldrx] build: ${who} was not blocked — its Definition of Done failed for a reason the base tree shares.`,
35690
36035
  ...baseRefusalLines([error.result])
35691
36036
  ],
@@ -35774,6 +36119,9 @@ class BuildSession {
35774
36119
  branchModel;
35775
36120
  writes = new SerialQueue;
35776
36121
  advisories = [];
36122
+ restores = [];
36123
+ restoreAttempted = new Set;
36124
+ aside = [];
35777
36125
  preflight;
35778
36126
  lanes;
35779
36127
  constructor(ctx, workspace, plan, opening = []) {
@@ -35791,7 +36139,7 @@ class BuildSession {
35791
36139
  this.advisories.push(plan.priceIssue);
35792
36140
  }
35793
36141
  async runAll() {
35794
- const refusal = await this.refuseOnDirtyRepos() ?? await this.refuseOnForeignEpic() ?? await this.refuseOnRedBase();
36142
+ const refusal = await this.refuseOnDirtyRepos() ?? await this.refuseOnForeignEpic() ?? await this.setAsideForeign() ?? await this.refuseOnRedBase();
35795
36143
  if (refusal !== null)
35796
36144
  return refusal;
35797
36145
  this.recordGateFeedback();
@@ -35832,7 +36180,7 @@ class BuildSession {
35832
36180
  const planned = this.nextPending();
35833
36181
  if (planned === null)
35834
36182
  return await this.finish();
35835
- const refusal = await this.refuseOnDirtyRepos() ?? await this.refuseOnForeignEpic() ?? await this.refuseOnRedBase();
36183
+ const refusal = await this.refuseOnDirtyRepos() ?? await this.refuseOnForeignEpic() ?? await this.setAsideForeign() ?? await this.refuseOnRedBase();
35836
36184
  if (refusal !== null)
35837
36185
  return refusal;
35838
36186
  const review = this.reviewWorkFor(planned);
@@ -35914,7 +36262,7 @@ class BuildSession {
35914
36262
  const planned = this.nextPending();
35915
36263
  if (planned === null)
35916
36264
  return await this.finish();
35917
- const refusal = await this.refuseOnDirtyRepos() ?? await this.refuseOnForeignEpic();
36265
+ const refusal = await this.refuseOnDirtyRepos() ?? await this.refuseOnForeignEpic() ?? await this.setAsideForeign();
35918
36266
  if (refusal !== null)
35919
36267
  return refusal;
35920
36268
  const work = this.reviewWorkFor(planned) ?? this.reviewWorkFromLedger(planned);
@@ -36878,6 +37226,7 @@ class BuildSession {
36878
37226
  return existsSync74(buildRetroPath(this.ctx.runDir)) ? [RETRO_REL] : [];
36879
37227
  }
36880
37228
  async finish() {
37229
+ const restored = await this.restoreForeignWorkAside();
36881
37230
  const outcomes = this.orderedOutcomes();
36882
37231
  const done = outcomes.filter((o) => o.status === "done").length;
36883
37232
  this.writeHandoff(outcomes);
@@ -36891,7 +37240,8 @@ class BuildSession {
36891
37240
  lines: [
36892
37241
  `${this.ctx.phaseId}/${this.ctx.stageId}: ${String(done)} of ${String(outcomes.length)} story(ies) done ` + `across ${String(this.plan.waves.length)} wave(s)`,
36893
37242
  ...this.lines,
36894
- `wrote ${HANDOFF_REL2}`
37243
+ `wrote ${HANDOFF_REL2}`,
37244
+ ...restored
36895
37245
  ],
36896
37246
  stderr: [...this.advisories],
36897
37247
  error: null
@@ -36918,7 +37268,14 @@ class BuildSession {
36918
37268
  storiesRel: this.plan.implicit ? IMPLICIT_PLAN_REL : null,
36919
37269
  carried: carried.rows,
36920
37270
  unreadableStories: carried.unreadable,
36921
- widenings: this.wideningRows()
37271
+ widenings: this.wideningRows(),
37272
+ foreignWork: unrestored(this.restores).map((outcome) => ({
37273
+ repo: outcome.stash.repo,
37274
+ paths: outcome.conflicts.length > 0 ? outcome.conflicts : outcome.stash.paths,
37275
+ stashRef: outcome.stash.hash,
37276
+ command: outcome.command,
37277
+ detail: outcome.detail
37278
+ }))
36922
37279
  }), "utf8");
36923
37280
  }
36924
37281
  carriedRows() {
@@ -37066,13 +37423,17 @@ class BuildSession {
37066
37423
  tasks: [],
37067
37424
  costUsd: 0,
37068
37425
  outputs: [],
37069
- lines: refusal.lines,
37426
+ lines: [...this.lines, ...refusal.lines],
37070
37427
  error: refusal.error
37071
37428
  };
37072
37429
  }
37430
+ get reportLines() {
37431
+ return this.lines;
37432
+ }
37073
37433
  async refuseOnDirtyRepos() {
37074
- const { refusal, ignored } = await dirtyRepoRefusal(this.claimParts, this.pendingStories());
37075
- if (refusal !== null) {
37434
+ const plan = await dirtyRepoRefusal({ ...this.claimParts, mode: this.ctx.mode }, this.pendingStories());
37435
+ this.aside = plan.aside;
37436
+ if (plan.refusal !== null) {
37076
37437
  return {
37077
37438
  ok: false,
37078
37439
  refused: true,
@@ -37080,15 +37441,65 @@ class BuildSession {
37080
37441
  tasks: [],
37081
37442
  costUsd: 0,
37082
37443
  outputs: [],
37083
- lines: refusal.lines,
37084
- error: refusal.error
37444
+ lines: plan.refusal.lines,
37445
+ error: plan.refusal.error
37085
37446
  };
37086
37447
  }
37087
- if (ignored > 0) {
37088
- this.lines.push(` · ignoring ${String(ignored)} tldrx state file(s) in the dirty-tree check`);
37448
+ if (plan.ignored > 0) {
37449
+ this.lines.push(` · ignoring ${String(plan.ignored)} tldrx state file(s) in the dirty-tree check`);
37450
+ }
37451
+ return null;
37452
+ }
37453
+ async setAsideForeign() {
37454
+ for (const row2 of this.aside) {
37455
+ const reason = "uncommitted work no pending story declares in its `touches:` — set aside so the " + "base pre-flight measures the tree the stories are cut from";
37456
+ const outcome = await setAsideForeignWork(row2.repo, row2.repoDir, this.ctx.runId, row2.paths);
37457
+ if (outcome.stash === null) {
37458
+ return {
37459
+ ok: false,
37460
+ refused: true,
37461
+ awaiting: false,
37462
+ tasks: [],
37463
+ costUsd: 0,
37464
+ outputs: [],
37465
+ lines: [
37466
+ `[tldrx] build: repo \`${row2.repo}\` has ${String(row2.paths.length)} uncommitted change(s) that ` + `are nobody's story, and they could not be set aside: ${outcome.reason ?? "no reason given"}.`,
37467
+ ` ${namePaths(row2.paths)}`,
37468
+ " Commit them, or set them aside by hand, then run " + `\`${relaunchCommand(this.ctx.mode, this.ctx.runId)}\`.`
37469
+ ],
37470
+ error: `repo \`${row2.repo}\` has uncommitted changes that could not be set aside`
37471
+ };
37472
+ }
37473
+ this.ctx.emit(FOREIGN_ASIDE_EVENT, asidePayload(outcome.stash, reason));
37474
+ this.lines.push(` · ${row2.repo}: ${String(row2.paths.length)} uncommitted change(s) nobody's story declares — ` + `set aside in stash ${outcome.stash.hash.slice(0, 12)} (${namePaths(row2.paths)}) and ` + "given back when this stage ends");
37089
37475
  }
37090
37476
  return null;
37091
37477
  }
37478
+ async restoreForeignWorkAside() {
37479
+ let open;
37480
+ try {
37481
+ open = pendingAsides(readFileSync68(join91(this.ctx.runDir, EVENTS_FILE2), "utf8"), (repo) => {
37482
+ try {
37483
+ return repoDirOf(this.workspace, repo);
37484
+ } catch {
37485
+ return null;
37486
+ }
37487
+ });
37488
+ } catch {
37489
+ return [];
37490
+ }
37491
+ const lines = [];
37492
+ for (const stash of open) {
37493
+ if (this.restoreAttempted.has(stash.hash))
37494
+ continue;
37495
+ this.restoreAttempted.add(stash.hash);
37496
+ const outcome = await restoreForeignWork(stash);
37497
+ this.restores.push(outcome);
37498
+ this.ctx.emit(FOREIGN_RESTORED_EVENT, restoredPayload(outcome));
37499
+ lines.push(outcome.restored ? ` · ${restoredLine(outcome)}` : `[tldrx] build: ${notRestoredLine(outcome)}`);
37500
+ }
37501
+ return lines;
37502
+ }
37092
37503
  pendingStories() {
37093
37504
  const rows = [];
37094
37505
  for (const wave of this.plan.waves) {
@@ -39004,7 +39415,9 @@ function tokenSplit(inputTokens, outputTokens) {
39004
39415
  return {};
39005
39416
  }
39006
39417
  function out(code, lines, stderr = []) {
39007
- return stderr.length === 0 ? { code, lines } : { code, lines, stderr };
39418
+ const held2 = lines.filter((line) => line.includes(NOT_RESTORED_MARK));
39419
+ const ordered = held2.length === 0 ? lines : [...lines.filter((line) => !line.includes(NOT_RESTORED_MARK)), ...held2];
39420
+ return stderr.length === 0 ? { code, lines: ordered } : { code, lines: ordered, stderr };
39008
39421
  }
39009
39422
 
39010
39423
  // src/core/facilitator/runAuto.ts
@@ -39081,11 +39494,18 @@ async function runAuto(options) {
39081
39494
  if (heartbeat !== null)
39082
39495
  clearInterval(heartbeat);
39083
39496
  if (notifier !== null) {
39084
- await notifier.send(runEndNotification(notifyCtx(), code, spentUsd, lines[lines.length - 1] ?? "", loopTally(RunStore.open(runDir2).run, spentUsd)), stageIdOf());
39497
+ await notifier.send(runEndNotification(notifyCtx(), code, spentUsd, lines[lines.length - 1] ?? "", loopTally(RunStore.open(runDir2).run, spentUsd), heldForeignWork()), stageIdOf());
39085
39498
  await notifier.drain();
39086
39499
  }
39087
39500
  return { code, lines };
39088
39501
  };
39502
+ const heldForeignWork = () => {
39503
+ try {
39504
+ return notRestoredSummary(readFileSync70(join93(runDir2, "events.jsonl"), "utf8"));
39505
+ } catch {
39506
+ return null;
39507
+ }
39508
+ };
39089
39509
  const openQuestions2 = () => {
39090
39510
  const ctx = cursorContext(runDir2, runId);
39091
39511
  return ctx === null ? null : questionsCard(ctx);
@@ -39126,7 +39546,7 @@ async function runAuto(options) {
39126
39546
  if (event4.type === "stage.done") {
39127
39547
  const stage2 = flatten(RunStore.open(runDir2).run).find((entry) => entry.stage.id === event4.stage);
39128
39548
  const unmetered = stage2 === undefined ? 0 : stage2.stage.tasks.filter((task2) => task2.metered === false).length;
39129
- await notifier.send(stageDoneNotification(notifyCtx(), number2(payload(event4, "cost_usd")), unmetered), event4.stage);
39549
+ await notifier.send(stageDoneNotification(notifyCtx(), number2(payload(event4, "cost_usd")), unmetered, heldForeignWork()), event4.stage);
39130
39550
  }
39131
39551
  }
39132
39552
  return { costUsd: null, deferredGate: null };
@@ -39537,7 +39957,7 @@ function event4(options, runId, payload2) {
39537
39957
  return { ts: options.at, run: runId, stage: null, type: "run.attended", actor: options.actor, cost_usd: 0, payload: payload2 };
39538
39958
  }
39539
39959
  // src/core/status/initQuestionsItem.ts
39540
- import { existsSync as existsSync75, readFileSync as readFileSync70 } from "node:fs";
39960
+ import { existsSync as existsSync75, readFileSync as readFileSync71 } from "node:fs";
39541
39961
  import { join as join94 } from "node:path";
39542
39962
  var MAX_LISTED = 5;
39543
39963
  function initQuestionsItem(root2) {
@@ -39546,7 +39966,7 @@ function initQuestionsItem(root2) {
39546
39966
  return null;
39547
39967
  let open;
39548
39968
  try {
39549
- open = openBlocks(parseQuestions(readFileSync70(path, "utf8")).blocks);
39969
+ open = openBlocks(parseQuestions(readFileSync71(path, "utf8")).blocks);
39550
39970
  } catch {
39551
39971
  return null;
39552
39972
  }
@@ -39566,7 +39986,7 @@ function initQuestionsItem(root2) {
39566
39986
  }
39567
39987
 
39568
39988
  // src/core/status/seedSplitItems.ts
39569
- import { closeSync as closeSync2, existsSync as existsSync77, openSync as openSync2, readSync, readdirSync as readdirSync27, readFileSync as readFileSync71, statSync as statSync21 } from "node:fs";
39989
+ import { closeSync as closeSync2, existsSync as existsSync77, openSync as openSync2, readSync, readdirSync as readdirSync27, readFileSync as readFileSync72, statSync as statSync21 } from "node:fs";
39570
39990
  import { basename as basename12, join as join96, relative as relative18 } from "node:path";
39571
39991
 
39572
39992
  // src/core/seed/triageInventory.ts
@@ -39766,7 +40186,7 @@ function proposedSplits(root2) {
39766
40186
  try {
39767
40187
  if (!statSync21(dir).isDirectory() || !existsSync77(path))
39768
40188
  continue;
39769
- const file = readSplitFile(parseYaml(readFileSync71(path, "utf8")));
40189
+ const file = readSplitFile(parseYaml(readFileSync72(path, "utf8")));
39770
40190
  if (file.status !== "proposed" && file.status !== "applying")
39771
40191
  continue;
39772
40192
  found.push({ path, rel: toRel(root2, path), dir, file });
@@ -39834,7 +40254,7 @@ function seedDocuments2(dir, file) {
39834
40254
  const inventory = join96(dir, "inventory.json");
39835
40255
  if (existsSync77(inventory)) {
39836
40256
  try {
39837
- const doc = JSON.parse(readFileSync71(inventory, "utf8"));
40257
+ const doc = JSON.parse(readFileSync72(inventory, "utf8"));
39838
40258
  const rels2 = (doc.documents ?? []).map((row2) => row2.rel).filter((rel) => typeof rel === "string");
39839
40259
  if (rels2.length > 0)
39840
40260
  return rels2;
@@ -39870,7 +40290,7 @@ function cachedStatuses(dir) {
39870
40290
  if (!existsSync77(inventory))
39871
40291
  return out2;
39872
40292
  try {
39873
- const doc = JSON.parse(readFileSync71(inventory, "utf8"));
40293
+ const doc = JSON.parse(readFileSync72(inventory, "utf8"));
39874
40294
  for (const row2 of doc.documents ?? []) {
39875
40295
  if (typeof row2.rel === "string" && typeof row2.adrStatus === "string")
39876
40296
  out2.set(row2.rel, row2.adrStatus);
@@ -40186,7 +40606,7 @@ function resolveWorkspaceRoot(explicit, start = process.cwd()) {
40186
40606
  return findWorkspaceRoot2(start) ?? resolve11(start);
40187
40607
  }
40188
40608
  // src/core/experts/stageCoverage.ts
40189
- import { existsSync as existsSync79, readdirSync as readdirSync28, readFileSync as readFileSync72, statSync as statSync22 } from "node:fs";
40609
+ import { existsSync as existsSync79, readdirSync as readdirSync28, readFileSync as readFileSync73, statSync as statSync22 } from "node:fs";
40190
40610
  import { join as join98 } from "node:path";
40191
40611
  function stagesLoadingExperts(root2) {
40192
40612
  const repos = workspaceRepos(root2);
@@ -40246,7 +40666,7 @@ function readStage(root2, id) {
40246
40666
  if (!existsSync79(path))
40247
40667
  return null;
40248
40668
  try {
40249
- const doc = parseYaml(readFileSync72(path, "utf8"));
40669
+ const doc = parseYaml(readFileSync73(path, "utf8"));
40250
40670
  return isRecord(doc) ? doc : null;
40251
40671
  } catch {
40252
40672
  return null;
@@ -40257,7 +40677,7 @@ function workspaceRepos(root2) {
40257
40677
  if (!existsSync79(path))
40258
40678
  return [];
40259
40679
  try {
40260
- const doc = parseYaml(readFileSync72(path, "utf8"));
40680
+ const doc = parseYaml(readFileSync73(path, "utf8"));
40261
40681
  const list = isRecord(doc) && Array.isArray(doc.repos) ? doc.repos : [];
40262
40682
  return list.map((row2) => isRecord(row2) && typeof row2.name === "string" ? row2.name : "").filter((name) => name !== "");
40263
40683
  } catch {
@@ -40607,7 +41027,7 @@ function sharedCitationWarnings(shared) {
40607
41027
  return shared.map((row2) => `warning: shared citation ${row2.src} by ${row2.experts.join(",")} — check for contradiction`);
40608
41028
  }
40609
41029
  // src/core/training/mineRuns.ts
40610
- import { existsSync as existsSync81, readFileSync as readFileSync73, readdirSync as readdirSync29, statSync as statSync23 } from "node:fs";
41030
+ import { existsSync as existsSync81, readFileSync as readFileSync74, readdirSync as readdirSync29, statSync as statSync23 } from "node:fs";
40611
41031
  import { join as join101, relative as relative19 } from "node:path";
40612
41032
  var MAX_RUN_FILES = 24;
40613
41033
  var MAX_RUN_BYTES = 64 * 1024;
@@ -40644,7 +41064,7 @@ function mineRuns(options) {
40644
41064
  }
40645
41065
  let text3;
40646
41066
  try {
40647
- text3 = readFileSync73(candidate.abs, "utf8");
41067
+ text3 = readFileSync74(candidate.abs, "utf8");
40648
41068
  } catch {
40649
41069
  notRead.push(candidate.path);
40650
41070
  continue;
@@ -40708,7 +41128,7 @@ function runRepos(path) {
40708
41128
  if (!existsSync81(path))
40709
41129
  return [];
40710
41130
  try {
40711
- const doc = parseYaml(readFileSync73(path, "utf8"));
41131
+ const doc = parseYaml(readFileSync74(path, "utf8"));
40712
41132
  const repos = doc?.repos;
40713
41133
  if (!Array.isArray(repos))
40714
41134
  return [];
@@ -41823,7 +42243,7 @@ var statusCommand = {
41823
42243
  };
41824
42244
 
41825
42245
  // src/core/seed/runTriage.ts
41826
- import { mkdirSync as mkdirSync20, readFileSync as readFileSync74, writeFileSync as writeFileSync18, existsSync as existsSync83 } from "node:fs";
42246
+ import { mkdirSync as mkdirSync20, readFileSync as readFileSync75, writeFileSync as writeFileSync18, existsSync as existsSync83 } from "node:fs";
41827
42247
  import { isAbsolute as isAbsolute8, join as join102, relative as relative20, resolve as resolve14 } from "node:path";
41828
42248
 
41829
42249
  // src/core/seed/renderInventory.ts
@@ -42266,7 +42686,7 @@ function readTriageResult(outDir) {
42266
42686
  }
42267
42687
  let doc;
42268
42688
  try {
42269
- doc = JSON.parse(readFileSync74(path, "utf8"));
42689
+ doc = JSON.parse(readFileSync75(path, "utf8"));
42270
42690
  } catch (error) {
42271
42691
  throw new PendingError(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
42272
42692
  }
@@ -42307,7 +42727,7 @@ function round28(n) {
42307
42727
  }
42308
42728
 
42309
42729
  // src/core/seed/applySplit.ts
42310
- import { existsSync as existsSync84, readFileSync as readFileSync75, writeFileSync as writeFileSync19 } from "node:fs";
42730
+ import { existsSync as existsSync84, readFileSync as readFileSync76, writeFileSync as writeFileSync19 } from "node:fs";
42311
42731
  import { dirname as dirname14, isAbsolute as isAbsolute9, join as join103, relative as relative21, resolve as resolve15 } from "node:path";
42312
42732
  var EXIT_OK10 = 0;
42313
42733
  var EXIT_USAGE7 = 1;
@@ -42319,7 +42739,7 @@ function applySplit(options) {
42319
42739
  }
42320
42740
  let file;
42321
42741
  try {
42322
- file = readSplitFile(parseYaml(readFileSync75(path, "utf8")));
42742
+ file = readSplitFile(parseYaml(readFileSync76(path, "utf8")));
42323
42743
  } catch (error) {
42324
42744
  return fail2(EXIT_USAGE7, [error instanceof SplitError ? error.message : message5(error)]);
42325
42745
  }
@@ -42444,7 +42864,7 @@ function splitUniverse(splitPath, file) {
42444
42864
  const inventory = join103(dirname14(splitPath), "inventory.json");
42445
42865
  if (existsSync84(inventory)) {
42446
42866
  try {
42447
- const doc = JSON.parse(readFileSync75(inventory, "utf8"));
42867
+ const doc = JSON.parse(readFileSync76(inventory, "utf8"));
42448
42868
  const rels2 = (doc.documents ?? []).map((row2) => row2.rel).filter((rel) => typeof rel === "string");
42449
42869
  if (rels2.length > 0)
42450
42870
  return new Set(rels2);
@@ -42487,7 +42907,7 @@ function message5(error) {
42487
42907
  }
42488
42908
 
42489
42909
  // src/core/seed/answerSplitQuestion.ts
42490
- import { existsSync as existsSync85, readFileSync as readFileSync76, writeFileSync as writeFileSync20 } from "node:fs";
42910
+ import { existsSync as existsSync85, readFileSync as readFileSync77, writeFileSync as writeFileSync20 } from "node:fs";
42491
42911
  import { dirname as dirname15, isAbsolute as isAbsolute10, join as join104, relative as relative22, resolve as resolve16 } from "node:path";
42492
42912
  var EXIT_OK11 = 0;
42493
42913
  var EXIT_USAGE8 = 1;
@@ -42501,7 +42921,7 @@ function answerSplitQuestion(options) {
42501
42921
  return { code: EXIT_NOT_FOUND9, lines: [`no such file: ${options.splitPath}`] };
42502
42922
  let file;
42503
42923
  try {
42504
- file = readSplitFile(parseYaml(readFileSync76(path, "utf8")));
42924
+ file = readSplitFile(parseYaml(readFileSync77(path, "utf8")));
42505
42925
  } catch (error) {
42506
42926
  return { code: EXIT_USAGE8, lines: [error instanceof SplitError ? error.message : message6(error)] };
42507
42927
  }
@@ -42728,10 +43148,10 @@ import { existsSync as existsSync89 } from "node:fs";
42728
43148
  import { join as join107 } from "node:path";
42729
43149
 
42730
43150
  // src/core/answers/captureAnswers.ts
42731
- import { existsSync as existsSync88, readFileSync as readFileSync79, writeFileSync as writeFileSync21 } from "node:fs";
43151
+ import { existsSync as existsSync88, readFileSync as readFileSync80, writeFileSync as writeFileSync21 } from "node:fs";
42732
43152
 
42733
43153
  // src/core/answers/stampSuperseded.ts
42734
- import { appendFileSync as appendFileSync2, existsSync as existsSync86, readFileSync as readFileSync77, statSync as statSync24 } from "node:fs";
43154
+ import { appendFileSync as appendFileSync2, existsSync as existsSync86, readFileSync as readFileSync78, statSync as statSync24 } from "node:fs";
42735
43155
  import { isAbsolute as isAbsolute11, join as join105, relative as relative23 } from "node:path";
42736
43156
  var STAMP_MARKER = "tldrx:superseded";
42737
43157
  var AFFECTS_KEY = "affects";
@@ -42771,7 +43191,7 @@ function stampSuperseded(runDir2, questionsPath, block2, fact, at) {
42771
43191
  const questionsRel = runRelative(runDir2, questionsPath) ?? "questions.md";
42772
43192
  const stamped = [];
42773
43193
  for (const doc of affectedDocs(runDir2, questionsPath, block2)) {
42774
- const text3 = readFileSync77(doc.abs, "utf8");
43194
+ const text3 = readFileSync78(doc.abs, "utf8");
42775
43195
  if (isStamped(text3, fact))
42776
43196
  continue;
42777
43197
  const lead = text3 === "" ? "" : text3.endsWith(`
@@ -42845,7 +43265,7 @@ function reposFromAffects(affects, repoNames) {
42845
43265
  }
42846
43266
 
42847
43267
  // src/core/answers/raiseConflict.ts
42848
- import { appendFileSync as appendFileSync3, existsSync as existsSync87, readFileSync as readFileSync78 } from "node:fs";
43268
+ import { appendFileSync as appendFileSync3, existsSync as existsSync87, readFileSync as readFileSync79 } from "node:fs";
42849
43269
  import { join as join106 } from "node:path";
42850
43270
  var RAISED_BY = "tldrx";
42851
43271
  function nextQuestionId(runDir2) {
@@ -42856,7 +43276,7 @@ function nextQuestionId(runDir2) {
42856
43276
  continue;
42857
43277
  let blocks;
42858
43278
  try {
42859
- blocks = parseQuestions(readFileSync78(path, "utf8")).blocks;
43279
+ blocks = parseQuestions(readFileSync79(path, "utf8")).blocks;
42860
43280
  } catch {
42861
43281
  continue;
42862
43282
  }
@@ -42934,7 +43354,7 @@ function factWasTruncated(title, answer2) {
42934
43354
  function captureAnswers(questionsPath, ctx) {
42935
43355
  if (!existsSync88(questionsPath))
42936
43356
  return [];
42937
- let doc = parseQuestions(readFileSync79(questionsPath, "utf8"));
43357
+ let doc = parseQuestions(readFileSync80(questionsPath, "utf8"));
42938
43358
  const answered = detectAnswered(doc.blocks);
42939
43359
  if (answered.length === 0)
42940
43360
  return [];
@@ -43068,7 +43488,7 @@ function supersedeAnswer(questionsPath, qid, text3, ctx) {
43068
43488
  const answer2 = text3.trim();
43069
43489
  if (answer2 === "")
43070
43490
  throw new AnswerError("a superseding answer cannot be empty");
43071
- const doc = parseQuestions(readFileSync79(questionsPath, "utf8"));
43491
+ const doc = parseQuestions(readFileSync80(questionsPath, "utf8"));
43072
43492
  const block2 = doc.blocks.find((b) => b.id === qid);
43073
43493
  if (block2 === undefined)
43074
43494
  throw new AnswerError(`${qid} is not in ${questionsPath}`);
@@ -43149,7 +43569,7 @@ function writeAnswerSlot(questionsPath, qid, text3) {
43149
43569
  throw new AnswerError(`no questions file at ${questionsPath}`);
43150
43570
  if (text3.trim() === "")
43151
43571
  throw new AnswerError("an answer cannot be empty");
43152
- const doc = parseQuestions(readFileSync79(questionsPath, "utf8"));
43572
+ const doc = parseQuestions(readFileSync80(questionsPath, "utf8"));
43153
43573
  const block2 = doc.blocks.find((b) => b.id === qid);
43154
43574
  if (block2 === undefined)
43155
43575
  throw new AnswerError(`${qid} is not in ${questionsPath}`);
@@ -43180,7 +43600,7 @@ function scopeRepos(names, repoNames) {
43180
43600
  }
43181
43601
 
43182
43602
  // src/cli/commands/answer.ts
43183
- import { readFileSync as readFileSync80 } from "node:fs";
43603
+ import { readFileSync as readFileSync81 } from "node:fs";
43184
43604
  var QUESTION_ID_RE = /^Q\d{1,6}$/;
43185
43605
  var answerCommand2 = {
43186
43606
  name: "answer",
@@ -43295,7 +43715,7 @@ function locateQuestion(store, qid, status) {
43295
43715
  const path = join107(store.runDir, phase.id, "questions.md");
43296
43716
  if (!existsSync89(path))
43297
43717
  continue;
43298
- const block2 = parseQuestions(readFileSync80(path, "utf8")).blocks.find((b) => b.id === qid);
43718
+ const block2 = parseQuestions(readFileSync81(path, "utf8")).blocks.find((b) => b.id === qid);
43299
43719
  if (block2 !== undefined && block2.metadata?.status === status)
43300
43720
  return { path, block: block2 };
43301
43721
  }
@@ -43303,7 +43723,7 @@ function locateQuestion(store, qid, status) {
43303
43723
  }
43304
43724
 
43305
43725
  // src/cli/commands/approve.ts
43306
- import { existsSync as existsSync90, readFileSync as readFileSync81 } from "node:fs";
43726
+ import { existsSync as existsSync90, readFileSync as readFileSync82 } from "node:fs";
43307
43727
  import { relative as relative24 } from "node:path";
43308
43728
  var approveCommand2 = {
43309
43729
  name: "approve",
@@ -43388,7 +43808,7 @@ function readAgentEvidence(root2, store, override) {
43388
43808
  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");
43389
43809
  return { kind: "stop", exit: EXIT_GATE_REFUSED };
43390
43810
  }
43391
- const text3 = readFileSync81(path, "utf8");
43811
+ const text3 = readFileSync82(path, "utf8");
43392
43812
  const gate2 = `${entry.phase.id}/${entry.stage.id}`;
43393
43813
  const validation = validateEvidence(text3, toSrcContext(loadWorkspace(root2), store.runDir, { epicRefs: true }), { gate: gate2 });
43394
43814
  const refusals = validation.issues.filter((issue) => issue.kind === "verdict");
@@ -43964,7 +44384,7 @@ function template(argv) {
43964
44384
  }
43965
44385
 
43966
44386
  // src/core/run/reopenStory.ts
43967
- import { existsSync as existsSync92, readFileSync as readFileSync82, writeFileSync as writeFileSync23 } from "node:fs";
44387
+ import { existsSync as existsSync92, readFileSync as readFileSync83, writeFileSync as writeFileSync23 } from "node:fs";
43968
44388
  import { join as join108 } from "node:path";
43969
44389
  var EXIT_OK12 = 0;
43970
44390
  var EXIT_REFUSED8 = 2;
@@ -44064,7 +44484,7 @@ function reopenStory(options) {
44064
44484
  }
44065
44485
  let patched;
44066
44486
  try {
44067
- const text3 = readFileSync82(path, "utf8");
44487
+ const text3 = readFileSync83(path, "utf8");
44068
44488
  patched = progress.implicit ? updateImplicitPlan(text3, { status: REOPENED_TO }) : updateStoryFront(text3, { status: REOPENED_TO });
44069
44489
  } catch (error) {
44070
44490
  if (error instanceof StoryWriteError) {
@@ -44152,7 +44572,7 @@ function refuse3(lines) {
44152
44572
  }
44153
44573
 
44154
44574
  // src/core/run/widenStory.ts
44155
- import { existsSync as existsSync93, readFileSync as readFileSync83, writeFileSync as writeFileSync24 } from "node:fs";
44575
+ import { existsSync as existsSync93, readFileSync as readFileSync84, writeFileSync as writeFileSync24 } from "node:fs";
44156
44576
  import { join as join109 } from "node:path";
44157
44577
  var EXIT_OK13 = 0;
44158
44578
  var EXIT_REFUSED9 = 2;
@@ -44232,7 +44652,7 @@ function widenStory(options) {
44232
44652
  if (!existsSync93(path)) {
44233
44653
  return refuse4([`${id} is \`${row2.status}\` but ${path} is not on disk — refusing to write a file that is not there`]);
44234
44654
  }
44235
- const text3 = readFileSync83(path, "utf8");
44655
+ const text3 = readFileSync84(path, "utf8");
44236
44656
  const before = touchesIn(text3, progress.implicit);
44237
44657
  if (before === null) {
44238
44658
  return refuse4([
@@ -44557,7 +44977,7 @@ function firstProblem(text3, area, kind, confidence, decidedBy) {
44557
44977
  }
44558
44978
 
44559
44979
  // src/core/run/ship.ts
44560
- import { existsSync as existsSync94, mkdtempSync as mkdtempSync2, readFileSync as readFileSync84, rmSync as rmSync11, writeFileSync as writeFileSync25 } from "node:fs";
44980
+ import { existsSync as existsSync94, mkdtempSync as mkdtempSync2, readFileSync as readFileSync85, rmSync as rmSync11, writeFileSync as writeFileSync25 } from "node:fs";
44561
44981
  import { tmpdir as tmpdir2 } from "node:os";
44562
44982
  import { dirname as dirname17, join as join110, resolve as resolve17 } from "node:path";
44563
44983
 
@@ -45048,7 +45468,7 @@ function lastHandoff(runDir2, phases) {
45048
45468
  continue;
45049
45469
  let text3;
45050
45470
  try {
45051
- text3 = readFileSync84(path, "utf8");
45471
+ text3 = readFileSync85(path, "utf8");
45052
45472
  } catch {
45053
45473
  continue;
45054
45474
  }
@@ -45171,11 +45591,11 @@ var shipCommand = {
45171
45591
  import { join as join113 } from "node:path";
45172
45592
 
45173
45593
  // src/core/plan/syncDod.ts
45174
- import { existsSync as existsSync96, readdirSync as readdirSync30, readFileSync as readFileSync86 } from "node:fs";
45594
+ import { existsSync as existsSync96, readdirSync as readdirSync30, readFileSync as readFileSync87 } from "node:fs";
45175
45595
  import { join as join112 } from "node:path";
45176
45596
 
45177
45597
  // src/core/plan/workspaceHistory.ts
45178
- import { existsSync as existsSync95, readFileSync as readFileSync85, realpathSync } from "node:fs";
45598
+ import { existsSync as existsSync95, readFileSync as readFileSync86, realpathSync } from "node:fs";
45179
45599
  import { join as join111, relative as relative26, sep as sep10 } from "node:path";
45180
45600
  var WORKSPACE_REL = `${PROJECT_FRAMEWORK_DIR}/workspace.yml`;
45181
45601
  var MAX_VERSIONS = 50;
@@ -45183,7 +45603,7 @@ async function readWorkspaceHistory(root2) {
45183
45603
  const documents = [];
45184
45604
  const live2 = join111(root2, WORKSPACE_REL);
45185
45605
  if (existsSync95(live2)) {
45186
- const parsed = parse(readFileSync85(live2, "utf8"));
45606
+ const parsed = parse(readFileSync86(live2, "utf8"));
45187
45607
  if (parsed !== null)
45188
45608
  documents.push(parsed);
45189
45609
  }
@@ -45273,7 +45693,7 @@ function planSyncDod(input) {
45273
45693
  };
45274
45694
  }
45275
45695
  function syncOne(path, name, input) {
45276
- const text3 = readFileSync86(path, "utf8");
45696
+ const text3 = readFileSync87(path, "utf8");
45277
45697
  const id = name.replace(/\.md$/, "");
45278
45698
  const rel = `${STORIES_DIR}/${name}`;
45279
45699
  const repo = readStory(text3).repo ?? "";
@@ -45868,7 +46288,7 @@ function refusal(input, model2, share, expected) {
45868
46288
  ];
45869
46289
  }
45870
46290
  // src/core/training/ambientModel.ts
45871
- import { readFileSync as readFileSync87 } from "node:fs";
46291
+ import { readFileSync as readFileSync88 } from "node:fs";
45872
46292
  import { join as join115 } from "node:path";
45873
46293
  function ambientModelFiles(root2, home) {
45874
46294
  return [
@@ -45891,7 +46311,7 @@ function resolveAmbientModel(input) {
45891
46311
  function modelIn(path) {
45892
46312
  let parsed;
45893
46313
  try {
45894
- parsed = JSON.parse(readFileSync87(path, "utf8"));
46314
+ parsed = JSON.parse(readFileSync88(path, "utf8"));
45895
46315
  } catch {
45896
46316
  return null;
45897
46317
  }
@@ -45963,11 +46383,11 @@ function noEvidenceNote(costUsd, added, warnings) {
45963
46383
  return lines;
45964
46384
  }
45965
46385
  // src/core/training/rescoreExperts.ts
45966
- import { readFileSync as readFileSync90 } from "node:fs";
46386
+ import { readFileSync as readFileSync91 } from "node:fs";
45967
46387
  import { relative as relative27 } from "node:path";
45968
46388
 
45969
46389
  // src/core/training/competenciesWrite.ts
45970
- import { existsSync as existsSync98, mkdirSync as mkdirSync22, readFileSync as readFileSync88, writeFileSync as writeFileSync26 } from "node:fs";
46390
+ import { existsSync as existsSync98, mkdirSync as mkdirSync22, readFileSync as readFileSync89, writeFileSync as writeFileSync26 } from "node:fs";
45971
46391
  import { dirname as dirname18, join as join117 } from "node:path";
45972
46392
  var HEADER = "# Written by `tldrx expert train` (spec §2.6). `level` is computed, never hand-set.\n";
45973
46393
 
@@ -45977,7 +46397,7 @@ function readDocument(root2, expert) {
45977
46397
  const path = join117(expertDir(root2, expert), COMPETENCIES_FILE);
45978
46398
  if (!existsSync98(path))
45979
46399
  throw new CompetenciesError(`${path} is missing — \`tldrx expert create\` writes it`);
45980
- const text3 = readFileSync88(path, "utf8");
46400
+ const text3 = readFileSync89(path, "utf8");
45981
46401
  const parsed = parseYaml(text3);
45982
46402
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
45983
46403
  throw new CompetenciesError(`${path} is not a mapping`);
@@ -46090,7 +46510,7 @@ function str11(value) {
46090
46510
  }
46091
46511
 
46092
46512
  // src/core/training/trainingLog.ts
46093
- import { appendFileSync as appendFileSync4, existsSync as existsSync99, mkdirSync as mkdirSync23, readFileSync as readFileSync89, statSync as statSync26 } from "node:fs";
46513
+ import { appendFileSync as appendFileSync4, existsSync as existsSync99, mkdirSync as mkdirSync23, readFileSync as readFileSync90, statSync as statSync26 } from "node:fs";
46094
46514
  import { dirname as dirname19, join as join118 } from "node:path";
46095
46515
  var TRAINING_EVENT_TYPES = [
46096
46516
  "agent.spawned",
@@ -46137,7 +46557,7 @@ class TrainingLog {
46137
46557
  if (!existsSync99(this.path))
46138
46558
  return [];
46139
46559
  const out2 = [];
46140
- for (const line of readFileSync89(this.path, "utf8").split(`
46560
+ for (const line of readFileSync90(this.path, "utf8").split(`
46141
46561
  `)) {
46142
46562
  if (line.trim() === "")
46143
46563
  continue;
@@ -46287,7 +46707,7 @@ function rescoreExperts(options) {
46287
46707
  }
46288
46708
  const shape = minesRunRecord ? RUNS_SHAPE : LIGHT_SHAPE;
46289
46709
  const scope = knowledgeScopeFor(options.root, expert, area);
46290
- const parsed = parseKnowledgeFile(readFileSync90(file.path, "utf8"), srcCtx, shape, scope);
46710
+ const parsed = parseKnowledgeFile(readFileSync91(file.path, "utf8"), srcCtx, shape, scope);
46291
46711
  if (!parsed.ok) {
46292
46712
  const errors2 = knowledgeErrors(parsed).length;
46293
46713
  skip(`the file does not validate under today's rules — ${String(errors2)} problem(s), so it is not evidence`);
@@ -46374,7 +46794,7 @@ function rescoreJson(rows) {
46374
46794
  return JSON.stringify(rows, null, 2);
46375
46795
  }
46376
46796
  // src/core/training/selectFiles.ts
46377
- import { existsSync as existsSync101, readFileSync as readFileSync91, statSync as statSync28 } from "node:fs";
46797
+ import { existsSync as existsSync101, readFileSync as readFileSync92, statSync as statSync28 } from "node:fs";
46378
46798
  import { join as join119 } from "node:path";
46379
46799
  var MAX_SCANNED_FILES2 = 4000;
46380
46800
  var MAX_SCAN_BYTES2 = 512 * 1024;
@@ -46514,7 +46934,7 @@ function readDomains(root2, repos) {
46514
46934
  const path = join119(root2, PROJECT_FRAMEWORK_DIR, "map", repo, "domains.md");
46515
46935
  if (!existsSync101(path))
46516
46936
  continue;
46517
- for (const line of readFileSync91(path, "utf8").split(`
46937
+ for (const line of readFileSync92(path, "utf8").split(`
46518
46938
  `)) {
46519
46939
  if (!line.startsWith("- "))
46520
46940
  continue;
@@ -46543,7 +46963,7 @@ function readCommunities(root2, repos, keywords) {
46543
46963
  }
46544
46964
  let text3;
46545
46965
  try {
46546
- text3 = readFileSync91(path, "utf8");
46966
+ text3 = readFileSync92(path, "utf8");
46547
46967
  } catch {
46548
46968
  notes.push(`${repo}: graph.json could not be read`);
46549
46969
  continue;
@@ -46622,7 +47042,7 @@ function matches2(label2, keywords) {
46622
47042
  function contentHits(abs, keywords) {
46623
47043
  let text3;
46624
47044
  try {
46625
- text3 = readFileSync91(abs, "utf8").toLowerCase();
47045
+ text3 = readFileSync92(abs, "utf8").toLowerCase();
46626
47046
  } catch {
46627
47047
  return [];
46628
47048
  }
@@ -46633,7 +47053,7 @@ function readCapped2(abs, remaining2) {
46633
47053
  try {
46634
47054
  if (statSync28(abs).size > MAX_SCAN_BYTES2)
46635
47055
  return null;
46636
- text3 = readFileSync91(abs, "utf8");
47056
+ text3 = readFileSync92(abs, "utf8");
46637
47057
  } catch {
46638
47058
  return null;
46639
47059
  }
@@ -47206,7 +47626,7 @@ function skipNoteLines(expert, area, reason) {
47206
47626
  return [`${expert}/${area}: skipped — ${first2 ?? ""}`, ...rest.map((line) => ` ${line}`)];
47207
47627
  }
47208
47628
  // src/core/training/strayWrite.ts
47209
- import { existsSync as existsSync102, mkdirSync as mkdirSync24, readdirSync as readdirSync33, readFileSync as readFileSync92, renameSync as renameSync4, rmdirSync as rmdirSync2, unlinkSync, writeFileSync as writeFileSync27 } from "node:fs";
47629
+ import { existsSync as existsSync102, mkdirSync as mkdirSync24, readdirSync as readdirSync33, readFileSync as readFileSync93, renameSync as renameSync4, rmdirSync as rmdirSync2, unlinkSync, writeFileSync as writeFileSync27 } from "node:fs";
47210
47630
  import { dirname as dirname20, join as join121, relative as relative28, resolve as resolve19 } from "node:path";
47211
47631
  function findStrayWrite(probe2) {
47212
47632
  const root2 = resolve19(probe2.root);
@@ -47229,7 +47649,7 @@ function recoverStrayWrite(stray, dest) {
47229
47649
  renameSync4(stray.path, dest);
47230
47650
  } catch {
47231
47651
  try {
47232
- writeFileSync27(dest, readFileSync92(stray.path, "utf8"), "utf8");
47652
+ writeFileSync27(dest, readFileSync93(stray.path, "utf8"), "utf8");
47233
47653
  unlinkSync(stray.path);
47234
47654
  } catch {
47235
47655
  return { stray, recovered: existsSync102(dest), leftBehind: stray.path };
@@ -47278,7 +47698,7 @@ function describeStrayRecovery(root2, recovery, rel) {
47278
47698
  return lines;
47279
47699
  }
47280
47700
  // src/core/training/runTraining.ts
47281
- import { existsSync as existsSync103, mkdirSync as mkdirSync25, readFileSync as readFileSync93, renameSync as renameSync5, rmSync as rmSync12, writeFileSync as writeFileSync28 } from "node:fs";
47701
+ import { existsSync as existsSync103, mkdirSync as mkdirSync25, readFileSync as readFileSync94, renameSync as renameSync5, rmSync as rmSync12, writeFileSync as writeFileSync28 } from "node:fs";
47282
47702
  import { homedir as homedir3 } from "node:os";
47283
47703
  import { join as join122, relative as relative29 } from "node:path";
47284
47704
  var EXIT_OK14 = 0;
@@ -47460,7 +47880,7 @@ async function trainWithPreflight(options, said2) {
47460
47880
  const previous = prompts.map((task2) => ({
47461
47881
  path: join122(dir, task2.final),
47462
47882
  partial: join122(dir, task2.output),
47463
- content: existsSync103(join122(dir, task2.final)) ? readFileSync93(join122(dir, task2.final), "utf8") : null
47883
+ content: existsSync103(join122(dir, task2.final)) ? readFileSync94(join122(dir, task2.final), "utf8") : null
47464
47884
  }));
47465
47885
  const tasks = [];
47466
47886
  for (const task2 of prompts) {
@@ -47574,7 +47994,7 @@ async function trainWithPreflight(options, said2) {
47574
47994
  " nothing was written to competencies.yml and the status is unchanged"
47575
47995
  ]);
47576
47996
  }
47577
- const text3 = readFileSync93(abs, "utf8");
47997
+ const text3 = readFileSync94(abs, "utf8");
47578
47998
  const shape = task2.key === CODE_TASK ? LIGHT_SHAPE : RUNS_SHAPE;
47579
47999
  let parsed = parseKnowledgeFile(text3, srcCtx, shape, scope);
47580
48000
  if (!parsed.ok && options.run === "headless") {
@@ -47752,7 +48172,7 @@ async function repairRound(input) {
47752
48172
  parsed: null
47753
48173
  };
47754
48174
  }
47755
- const parsed = parseKnowledgeFile(readFileSync93(abs, "utf8"), input.srcCtx, input.shape, input.scope);
48175
+ const parsed = parseKnowledgeFile(readFileSync94(abs, "utf8"), input.srcCtx, input.shape, input.scope);
47756
48176
  const left = knowledgeErrors(parsed).length;
47757
48177
  return {
47758
48178
  lines: [
@@ -47782,7 +48202,7 @@ function quarantine(abs, header2 = []) {
47782
48202
  try {
47783
48203
  const body = header2.length === 0 ? null : `${header2.join(`
47784
48204
  `)}
47785
- ${readFileSync93(abs, "utf8")}`;
48205
+ ${readFileSync94(abs, "utf8")}`;
47786
48206
  rmSync12(kept, { force: true });
47787
48207
  renameSync5(abs, kept);
47788
48208
  if (body !== null)
@@ -47826,7 +48246,7 @@ function rollback(previous) {
47826
48246
  function setExpertStatus(path, status) {
47827
48247
  if (!existsSync103(path))
47828
48248
  return;
47829
- const lines = readFileSync93(path, "utf8").split(`
48249
+ const lines = readFileSync94(path, "utf8").split(`
47830
48250
  `);
47831
48251
  if ((lines[0] ?? "").trimEnd() !== "---")
47832
48252
  return;
@@ -48306,7 +48726,7 @@ function message8(error) {
48306
48726
  import { isAbsolute as isAbsolute12, join as join128 } from "node:path";
48307
48727
 
48308
48728
  // src/core/dashboard/model.ts
48309
- import { existsSync as existsSync105, readFileSync as readFileSync94, readdirSync as readdirSync34 } from "node:fs";
48729
+ import { existsSync as existsSync105, readFileSync as readFileSync95, readdirSync as readdirSync34 } from "node:fs";
48310
48730
  import { basename as basename15, join as join124 } from "node:path";
48311
48731
  // src/core/replay/renderReplay.ts
48312
48732
  function renderReplay(loaded) {
@@ -49192,7 +49612,7 @@ function readWaves(path, unreadable) {
49192
49612
  }
49193
49613
  function read2(path, unreadable, label2) {
49194
49614
  try {
49195
- return readFileSync94(path, "utf8");
49615
+ return readFileSync95(path, "utf8");
49196
49616
  } catch {
49197
49617
  unreadable.push(label2);
49198
49618
  return null;
@@ -51184,6 +51604,7 @@ var LOOPBACK = "127.0.0.1";
51184
51604
  var LOOPBACK_HOSTNAMES = ["127.0.0.1", "localhost", "::1"];
51185
51605
  var DEBOUNCE_MS = 300;
51186
51606
  var POLL_MS = 500;
51607
+ var SWEEP_MS = 2000;
51187
51608
  var AGE_TICK_MS = 25000;
51188
51609
  var PROBE_NOW = new Date(0);
51189
51610
  var PROBE_STAMP = "1970-01-01T00:00:00Z";
@@ -51207,7 +51628,7 @@ async function startDashboardServer(options) {
51207
51628
  const tail2 = createLedgerTail(root2);
51208
51629
  tail2.poll();
51209
51630
  let shown = probe2(root2);
51210
- const watcher = watchWorkspace(root2, debounceMs, options.watch ?? "auto", () => {
51631
+ const watcher = watchWorkspace(root2, debounceMs, options.watch ?? "auto", options.sweepMs ?? SWEEP_MS, () => {
51211
51632
  const change = tail2.poll();
51212
51633
  const next = probe2(root2);
51213
51634
  if (next !== null && shown !== null && next === shown)
@@ -51233,6 +51654,9 @@ async function startDashboardServer(options) {
51233
51654
  port,
51234
51655
  url: `http://${host}:${String(port)}`,
51235
51656
  watchMode: watcher.mode,
51657
+ simulateWatcherLoss: () => {
51658
+ watcher.dropWatchers();
51659
+ },
51236
51660
  close: async () => {
51237
51661
  clearInterval(tick);
51238
51662
  watcher.close();
@@ -51348,12 +51772,13 @@ function probe2(root2) {
51348
51772
  return null;
51349
51773
  }
51350
51774
  }
51351
- function watchWorkspace(root2, debounceMs, requested, onChange) {
51775
+ function watchWorkspace(root2, debounceMs, requested, sweepMs, onChange) {
51352
51776
  const targets = [join127(root2, PROJECT_FRAMEWORK_DIR), join127(root2, PROJECT_WORK_DIR)];
51353
51777
  const watchers = new Map;
51354
51778
  let timer = null;
51355
51779
  let closed = false;
51356
51780
  let mode = "watch";
51781
+ let swept = "";
51357
51782
  const fire = () => {
51358
51783
  if (closed)
51359
51784
  return;
@@ -51362,6 +51787,7 @@ function watchWorkspace(root2, debounceMs, requested, onChange) {
51362
51787
  timer = setTimeout(() => {
51363
51788
  timer = null;
51364
51789
  arm();
51790
+ swept = fingerprint(root2, targets);
51365
51791
  onChange();
51366
51792
  }, debounceMs);
51367
51793
  timer.unref();
@@ -51392,26 +51818,27 @@ function watchWorkspace(root2, debounceMs, requested, onChange) {
51392
51818
  mode = "poll";
51393
51819
  else
51394
51820
  arm();
51395
- let poller = null;
51396
- if (mode === "poll") {
51397
- let previous = fingerprint(root2, targets);
51398
- poller = setInterval(() => {
51399
- const next = fingerprint(root2, targets);
51400
- if (next !== previous) {
51401
- previous = next;
51402
- fire();
51403
- }
51404
- }, POLL_MS);
51405
- poller.unref();
51406
- }
51821
+ swept = fingerprint(root2, targets);
51822
+ const poller = setInterval(() => {
51823
+ const next = fingerprint(root2, targets);
51824
+ if (next === swept)
51825
+ return;
51826
+ swept = next;
51827
+ fire();
51828
+ }, mode === "poll" ? POLL_MS : sweepMs);
51829
+ poller.unref();
51407
51830
  return {
51408
51831
  mode,
51832
+ dropWatchers: () => {
51833
+ for (const watcher of watchers.values())
51834
+ watcher.close();
51835
+ watchers.clear();
51836
+ },
51409
51837
  close: () => {
51410
51838
  closed = true;
51411
51839
  if (timer !== null)
51412
51840
  clearTimeout(timer);
51413
- if (poller !== null)
51414
- clearInterval(poller);
51841
+ clearInterval(poller);
51415
51842
  for (const watcher of watchers.values())
51416
51843
  watcher.close();
51417
51844
  watchers.clear();
@@ -51621,7 +52048,7 @@ function option2(argv, name) {
51621
52048
  import { writeFileSync as writeFileSync31 } from "node:fs";
51622
52049
  import { join as join130 } from "node:path";
51623
52050
  // src/core/retro/applyPractices.ts
51624
- import { existsSync as existsSync107, mkdirSync as mkdirSync27, readFileSync as readFileSync95, writeFileSync as writeFileSync30 } from "node:fs";
52051
+ import { existsSync as existsSync107, mkdirSync as mkdirSync27, readFileSync as readFileSync96, writeFileSync as writeFileSync30 } from "node:fs";
51625
52052
  import { dirname as dirname21, join as join129 } from "node:path";
51626
52053
  var PRACTICES_FILE = "practices.md";
51627
52054
  var HEADER2 = [
@@ -51640,7 +52067,7 @@ function applyPractices(root2, run2, proposals, now = new Date) {
51640
52067
  if (proposals.length === 0) {
51641
52068
  return { path, appended: false, reason: "there are no practice proposals to apply" };
51642
52069
  }
51643
- const existing = existsSync107(path) ? readFileSync95(path, "utf8") : "";
52070
+ const existing = existsSync107(path) ? readFileSync96(path, "utf8") : "";
51644
52071
  const stamp4 = `— run ${run2}`;
51645
52072
  if (existing.includes(stamp4)) {
51646
52073
  return { path, appended: false, reason: `practices.md already carries a block for run ${run2}` };
@@ -52189,7 +52616,7 @@ function theOneOpenRun() {
52189
52616
  }
52190
52617
  }
52191
52618
  // src/core/watch/watchViews.ts
52192
- import { existsSync as existsSync108, readdirSync as readdirSync36, readFileSync as readFileSync96 } from "node:fs";
52619
+ import { existsSync as existsSync108, readdirSync as readdirSync36, readFileSync as readFileSync97 } from "node:fs";
52193
52620
  import { join as join131 } from "node:path";
52194
52621
  function watchersDir(runDir2) {
52195
52622
  return join131(runDir2, WATCH_PHASE, WATCHERS_DIR);
@@ -52201,7 +52628,7 @@ function loadCards(runDir2, ctx) {
52201
52628
  const cards = [];
52202
52629
  for (const name of readdirSync36(dir).filter((n) => n.endsWith(".md")).sort()) {
52203
52630
  const id = name.slice(0, -".md".length);
52204
- const text5 = readFileSync96(join131(dir, name), "utf8");
52631
+ const text5 = readFileSync97(join131(dir, name), "utf8");
52205
52632
  cards.push({
52206
52633
  id,
52207
52634
  text: text5,
@@ -52972,7 +53399,7 @@ function quote4(value) {
52972
53399
  return JSON.stringify(value);
52973
53400
  }
52974
53401
  // src/core/adapters/processConfig.ts
52975
- import { existsSync as existsSync110, readFileSync as readFileSync97 } from "node:fs";
53402
+ import { existsSync as existsSync110, readFileSync as readFileSync98 } from "node:fs";
52976
53403
  import { join as join133 } from "node:path";
52977
53404
  var PROCESS_FILE2 = "process.yml";
52978
53405
  var DISABLED = {
@@ -52991,7 +53418,7 @@ function readTicketToolConfig(root2) {
52991
53418
  return DISABLED;
52992
53419
  let doc;
52993
53420
  try {
52994
- doc = parseYaml(readFileSync97(path, "utf8"));
53421
+ doc = parseYaml(readFileSync98(path, "utf8"));
52995
53422
  } catch {
52996
53423
  return { ...DISABLED, path };
52997
53424
  }
@@ -53153,7 +53580,7 @@ function strip(url) {
53153
53580
  return at === -1 ? url : url.slice(at);
53154
53581
  }
53155
53582
  // src/core/adapters/collect.ts
53156
- import { existsSync as existsSync111, readdirSync as readdirSync37, readFileSync as readFileSync98 } from "node:fs";
53583
+ import { existsSync as existsSync111, readdirSync as readdirSync37, readFileSync as readFileSync99 } from "node:fs";
53157
53584
  import { join as join134 } from "node:path";
53158
53585
  var PLAN_PHASE3 = "03-plan";
53159
53586
  function collectMirrorItems(planDir, runId) {
@@ -53161,7 +53588,7 @@ function collectMirrorItems(planDir, runId) {
53161
53588
  const skipped = [];
53162
53589
  for (const name of markdownIn4(join134(planDir, EPICS_DIR))) {
53163
53590
  const path = join134(planDir, EPICS_DIR, name);
53164
- const text6 = readFileSync98(path, "utf8");
53591
+ const text6 = readFileSync99(path, "utf8");
53165
53592
  const parsed = validateEpicFile(text6);
53166
53593
  const epic = parsed.epic;
53167
53594
  if (epic === null) {
@@ -53194,7 +53621,7 @@ function collectMirrorItems(planDir, runId) {
53194
53621
  }
53195
53622
  for (const name of markdownIn4(join134(planDir, STORIES_DIR))) {
53196
53623
  const path = join134(planDir, STORIES_DIR, name);
53197
- const text6 = readFileSync98(path, "utf8");
53624
+ const text6 = readFileSync99(path, "utf8");
53198
53625
  const parsed = validateStoryFile(text6);
53199
53626
  const story = parsed.story;
53200
53627
  if (story === null) {
@@ -53239,7 +53666,7 @@ function reason(issues) {
53239
53666
  return first2.path === "" ? first2.message : `${first2.path}: ${first2.message}`;
53240
53667
  }
53241
53668
  // src/core/adapters/sync.ts
53242
- import { readFileSync as readFileSync99, writeFileSync as writeFileSync32 } from "node:fs";
53669
+ import { readFileSync as readFileSync100, writeFileSync as writeFileSync32 } from "node:fs";
53243
53670
  function planSync(options) {
53244
53671
  const collected = collectMirrorItems(options.planDir, options.runId);
53245
53672
  const actions = new Map;
@@ -53283,7 +53710,7 @@ async function syncTickets(options) {
53283
53710
  return { results, skipped: collected.skipped, created, updated, dryRun: options.dryRun };
53284
53711
  }
53285
53712
  function writeBack(item, ref, externalStatus, sync) {
53286
- const before2 = readFileSync99(item.path, "utf8");
53713
+ const before2 = readFileSync100(item.path, "utf8");
53287
53714
  const after = applyExternal(before2, {
53288
53715
  external: ref,
53289
53716
  ...sync === "two-way" ? { externalStatus } : {}
@@ -53535,7 +53962,7 @@ function renderSync(runId, provider, config, outcome) {
53535
53962
  }
53536
53963
 
53537
53964
  // src/cli/commands/interview.ts
53538
- import { existsSync as existsSync114, readFileSync as readFileSync102 } from "node:fs";
53965
+ import { existsSync as existsSync114, readFileSync as readFileSync103 } from "node:fs";
53539
53966
  import { join as join137 } from "node:path";
53540
53967
 
53541
53968
  // src/core/interview/lineReader.ts
@@ -53649,7 +54076,7 @@ function renderNextSteps(hasRun) {
53649
54076
  }
53650
54077
 
53651
54078
  // src/core/interview/runInterview.ts
53652
- import { existsSync as existsSync112, readFileSync as readFileSync100 } from "node:fs";
54079
+ import { existsSync as existsSync112, readFileSync as readFileSync101 } from "node:fs";
53653
54080
 
53654
54081
  // src/core/interview/reply.ts
53655
54082
  var LETTER_RE = /^[A-Ea-e]$/;
@@ -53690,7 +54117,7 @@ async function interview(options) {
53690
54117
  const { questionsPath, out: out2, ctx } = options;
53691
54118
  if (!existsSync112(questionsPath))
53692
54119
  throw new InterviewError(`no questions file at ${questionsPath}`);
53693
- const blocks = openBlocks(parseQuestions(readFileSync100(questionsPath, "utf8")).blocks);
54120
+ const blocks = openBlocks(parseQuestions(readFileSync101(questionsPath, "utf8")).blocks);
53694
54121
  const answered = [];
53695
54122
  let skipped = 0;
53696
54123
  let quit = false;
@@ -53751,7 +54178,7 @@ function renderInterviewSummary(result2) {
53751
54178
  }
53752
54179
 
53753
54180
  // src/core/init/processAnswers.ts
53754
- import { existsSync as existsSync113, readFileSync as readFileSync101 } from "node:fs";
54181
+ import { existsSync as existsSync113, readFileSync as readFileSync102 } from "node:fs";
53755
54182
 
53756
54183
  // src/core/init/githubProject.ts
53757
54184
  import { join as join136 } from "node:path";
@@ -53801,7 +54228,7 @@ function hasProcessAnswer(answers) {
53801
54228
  function collectProcessAnswers(questionsPath, answered) {
53802
54229
  if (answered.length === 0 || !existsSync113(questionsPath))
53803
54230
  return NO_PROCESS_ANSWERS;
53804
- const titles = new Map(parseQuestions(readFileSync101(questionsPath, "utf8")).blocks.map((block2) => [block2.id, block2.title]));
54231
+ const titles = new Map(parseQuestions(readFileSync102(questionsPath, "utf8")).blocks.map((block2) => [block2.id, block2.title]));
53805
54232
  const find = (title) => answered.find((answer2) => titles.get(answer2.q) === title);
53806
54233
  const methodology = find(METHODOLOGY_QUESTION);
53807
54234
  const ticket = find(TICKET_QUESTION);
@@ -53839,7 +54266,7 @@ async function applyProcessAnswers(input) {
53839
54266
  }
53840
54267
  assertEnum(methodology, METHODOLOGIES, "methodology");
53841
54268
  assertEnum(ticketTool, TICKET_TOOLS, "ticket_tool.kind");
53842
- const before2 = existsSync113(path) ? readFileSync101(path, "utf8") : null;
54269
+ const before2 = existsSync113(path) ? readFileSync102(path, "utf8") : null;
53843
54270
  const seed = before2 ?? PROCESS_HEADER + stringifyYaml(buildProcessDocument({
53844
54271
  methodology: null,
53845
54272
  approver: await gitUserName(input.runner, input.root),
@@ -54076,11 +54503,11 @@ No open questions.
54076
54503
  return "no-open-questions";
54077
54504
  }
54078
54505
  function countOpen(path) {
54079
- return openBlocks(parseQuestions(readFileSync102(path, "utf8")).blocks).length;
54506
+ return openBlocks(parseQuestions(readFileSync103(path, "utf8")).blocks).length;
54080
54507
  }
54081
54508
 
54082
54509
  // src/cli/commands/questions.ts
54083
- import { existsSync as existsSync115, readFileSync as readFileSync103, writeFileSync as writeFileSync33 } from "node:fs";
54510
+ import { existsSync as existsSync115, readFileSync as readFileSync104, writeFileSync as writeFileSync33 } from "node:fs";
54084
54511
  import { join as join138 } from "node:path";
54085
54512
  var VALUE_FLAGS15 = ["run", "root", "area"];
54086
54513
  var questionsCommand = {
@@ -54137,7 +54564,7 @@ function lint(argv) {
54137
54564
  const needSource = [];
54138
54565
  for (const path of files) {
54139
54566
  const rel = path.slice(store.runDir.length + 1);
54140
- const text6 = readFileSync103(path, "utf8");
54567
+ const text6 = readFileSync104(path, "utf8");
54141
54568
  const readable = parseQuestions(text6).blocks.length;
54142
54569
  const lost = unreadableQuestionHeadings(text6);
54143
54570
  if (lost.length === 0) {
@@ -54302,7 +54729,7 @@ function renderFinale(ink) {
54302
54729
  }
54303
54730
 
54304
54731
  // src/core/learn/engine.ts
54305
- import { existsSync as existsSync118, readdirSync as readdirSync38, readFileSync as readFileSync104 } from "node:fs";
54732
+ import { existsSync as existsSync118, readdirSync as readdirSync38, readFileSync as readFileSync105 } from "node:fs";
54306
54733
  import { join as join141 } from "node:path";
54307
54734
 
54308
54735
  // src/core/learn/sandbox.ts
@@ -54370,9 +54797,9 @@ function claudeShimScript(selfCmd, scriptPath) {
54370
54797
  "#!/bin/sh",
54371
54798
  "# The tutorial's stand-in for `claude`, written by `tldrx learn`.",
54372
54799
  "# It is not the real CLI, it never reaches a network, and it never costs anything.",
54373
- `${SCRIPT_ENV}=${shellQuote2(scriptPath)}`,
54800
+ `${SCRIPT_ENV}=${shellQuote3(scriptPath)}`,
54374
54801
  `export ${SCRIPT_ENV}`,
54375
- `exec ${shellQuote2(interpreter)} ${shellQuote2(entry)} ${LEARN_AGENT_ARGV0} "$@"`,
54802
+ `exec ${shellQuote3(interpreter)} ${shellQuote3(entry)} ${LEARN_AGENT_ARGV0} "$@"`,
54376
54803
  ""
54377
54804
  ].join(`
54378
54805
  `);
@@ -54381,7 +54808,7 @@ function writeClaudeShim(sandbox, selfCmd) {
54381
54808
  writeFileSync34(sandbox.claudeBin, claudeShimScript(selfCmd, sandbox.scriptPath), "utf8");
54382
54809
  chmodSync(sandbox.claudeBin, 493);
54383
54810
  }
54384
- function shellQuote2(text6) {
54811
+ function shellQuote3(text6) {
54385
54812
  return `'${text6.replaceAll("'", `'\\''`)}'`;
54386
54813
  }
54387
54814
  function writeScript(sandbox, script) {
@@ -54576,7 +55003,7 @@ function newestRunId(sandbox) {
54576
55003
  }
54577
55004
  function runIsFinished(runDir2) {
54578
55005
  try {
54579
- const status = /^status:\s*(\S+)/m.exec(readFileSync104(join141(runDir2, "run.yml"), "utf8"))?.[1] ?? "";
55006
+ const status = /^status:\s*(\S+)/m.exec(readFileSync105(join141(runDir2, "run.yml"), "utf8"))?.[1] ?? "";
54580
55007
  return isFinished(status);
54581
55008
  } catch {
54582
55009
  return false;
@@ -54617,7 +55044,7 @@ function sandboxHas(sandbox, rel) {
54617
55044
  }
54618
55045
 
54619
55046
  // src/core/learn/chapters.ts
54620
- import { existsSync as existsSync119, mkdirSync as mkdirSync29, readdirSync as readdirSync39, readFileSync as readFileSync105, writeFileSync as writeFileSync35 } from "node:fs";
55047
+ import { existsSync as existsSync119, mkdirSync as mkdirSync29, readdirSync as readdirSync39, readFileSync as readFileSync106, writeFileSync as writeFileSync35 } from "node:fs";
54621
55048
  import { dirname as dirname23, join as join142 } from "node:path";
54622
55049
  var LEARN_RUN_SLUG = "bulk-pricing";
54623
55050
  var WATCH_PHASE_DIR = "05-watch";
@@ -55862,7 +56289,7 @@ function writeInto(path, content) {
55862
56289
  }
55863
56290
  function read3(sandbox, rel) {
55864
56291
  try {
55865
- return readFileSync105(join142(sandbox.workspace, rel), "utf8");
56292
+ return readFileSync106(join142(sandbox.workspace, rel), "utf8");
55866
56293
  } catch {
55867
56294
  return "";
55868
56295
  }
@@ -55893,14 +56320,14 @@ function runIdBySlug(sandbox, slug) {
55893
56320
  }
55894
56321
 
55895
56322
  // src/core/learn/progress.ts
55896
- import { existsSync as existsSync120, mkdirSync as mkdirSync30, readFileSync as readFileSync106, writeFileSync as writeFileSync36 } from "node:fs";
56323
+ import { existsSync as existsSync120, mkdirSync as mkdirSync30, readFileSync as readFileSync107, writeFileSync as writeFileSync36 } from "node:fs";
55897
56324
  import { dirname as dirname24 } from "node:path";
55898
56325
  var NO_PROGRESS = { version: 1, completed: [], updatedAt: null };
55899
56326
  function readProgress(path) {
55900
56327
  if (!existsSync120(path))
55901
56328
  return NO_PROGRESS;
55902
56329
  try {
55903
- const doc = JSON.parse(readFileSync106(path, "utf8"));
56330
+ const doc = JSON.parse(readFileSync107(path, "utf8"));
55904
56331
  const completed = doc.completed;
55905
56332
  if (!Array.isArray(completed))
55906
56333
  return NO_PROGRESS;