tldr-experts 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tldrx.js CHANGED
@@ -7362,24 +7362,34 @@ function capPayload(payload, save) {
7362
7362
  const where = (text, field) => save === undefined ? "the full text was not preserved and is not recoverable from this event" : save(text, field);
7363
7363
  const named = (size, text, field) => `${String(size)} bytes exceeds the ${String(MAX_PAYLOAD_BYTES)}-byte cap — ${where(text, field)}`;
7364
7364
  let current = payload;
7365
- if (typeof current.detail === "string") {
7366
- const { detail: dropped, ...rest } = current;
7367
- current = { ...rest, detail_omitted: named(byteSize(current), dropped, "detail") };
7365
+ for (const field of DROPPABLE_PROSE) {
7366
+ const dropped = current[field];
7367
+ if (typeof dropped !== "string")
7368
+ continue;
7369
+ const { [field]: _gone, ...rest } = current;
7370
+ current = { ...rest, [`${field}_omitted`]: named(byteSize(current), dropped, field) };
7368
7371
  if (byteSize(current) <= MAX_PAYLOAD_BYTES)
7369
7372
  return current;
7370
7373
  }
7371
- if (Array.isArray(current.outputs) && current.outputs.length > 0) {
7372
- const { outputs: dropped, ...rest } = current;
7373
- const list = dropped;
7374
+ for (const field of DROPPABLE_LISTS) {
7375
+ if (byteSize(current) <= MAX_PAYLOAD_BYTES)
7376
+ return current;
7377
+ const list = current[field];
7378
+ if (!Array.isArray(list) || list.length === 0)
7379
+ continue;
7380
+ const { [field]: dropped, ...rest } = current;
7381
+ const already = current[`${field}_omitted`];
7374
7382
  current = {
7375
7383
  ...rest,
7376
- outputs_omitted: list.length,
7377
- outputs_omitted_reason: named(byteSize(current), list.map((p) => String(p)).join(`
7378
- `), "outputs")
7384
+ [`${field}_omitted`]: list.length + (typeof already === "number" ? already : 0),
7385
+ [`${field}_omitted_reason`]: named(byteSize(current), dropped.map((p) => String(p)).join(`
7386
+ `), field)
7379
7387
  };
7380
7388
  }
7381
7389
  return current;
7382
7390
  }
7391
+ var DROPPABLE_LISTS = ["outputs", "after", "before", "paths"];
7392
+ var DROPPABLE_PROSE = ["recording_error", "detail"];
7383
7393
  function byteSize(payload) {
7384
7394
  return Buffer.byteLength(JSON.stringify(payload), "utf8");
7385
7395
  }
@@ -17140,6 +17150,7 @@ import { basename as basename4 } from "node:path";
17140
17150
 
17141
17151
  // src/core/run/RunStore.ts
17142
17152
  import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
17153
+ import { isDeepStrictEqual } from "node:util";
17143
17154
  import { basename as basename3, join as join14 } from "node:path";
17144
17155
 
17145
17156
  // src/hooks/lib/actor.ts
@@ -17160,11 +17171,14 @@ class RunStore {
17160
17171
  events;
17161
17172
  current;
17162
17173
  currentBudget;
17174
+ loaded;
17175
+ cancelledUnderFlag = false;
17163
17176
  budgetMutated = false;
17164
17177
  constructor(runDir, run, budget, events) {
17165
17178
  this.runDir = runDir;
17166
17179
  this.events = events;
17167
17180
  this.current = run;
17181
+ this.loaded = run;
17168
17182
  this.currentBudget = budget;
17169
17183
  }
17170
17184
  static open(runDir) {
@@ -17249,6 +17263,20 @@ class RunStore {
17249
17263
  get runId() {
17250
17264
  return this.current.run;
17251
17265
  }
17266
+ get cancelledUnder() {
17267
+ return this.cancelledUnderFlag;
17268
+ }
17269
+ static cancelledOnDisk(runDir) {
17270
+ const path = join14(runDir, "run.yml");
17271
+ if (!existsSync12(path))
17272
+ return false;
17273
+ try {
17274
+ const doc = parseYaml(readFileSync12(path, "utf8"));
17275
+ return validateRunFile(doc).ok && asRunFile(doc).cancelled !== undefined;
17276
+ } catch {
17277
+ return false;
17278
+ }
17279
+ }
17252
17280
  cursorEntry() {
17253
17281
  return stageAt(this.current, this.current.cursor);
17254
17282
  }
@@ -17270,13 +17298,14 @@ class RunStore {
17270
17298
  this.events.append(event2);
17271
17299
  }
17272
17300
  save() {
17273
- const rolled = rollUp(this.current);
17274
- const runValidation = validateRunFile(rolled);
17275
- if (!runValidation.ok) {
17276
- const first = runValidation.issues[0];
17277
- throw new RunStoreError(`refusing to write an invalid run.yml: ${first?.path ?? ""} ${first?.message ?? ""}`);
17278
- }
17279
- withWorkspaceLock(workspaceRootOfRunDir(this.runDir), () => {
17301
+ return withWorkspaceLock(workspaceRootOfRunDir(this.runDir), () => {
17302
+ const base = this.runToWrite();
17303
+ const rolled = rollUp(base.run);
17304
+ const runValidation = validateRunFile(rolled);
17305
+ if (!runValidation.ok) {
17306
+ const first = runValidation.issues[0];
17307
+ throw new RunStoreError(`refusing to write an invalid run.yml: ${first?.path ?? ""} ${first?.message ?? ""}`);
17308
+ }
17280
17309
  const budget = rollUpBudget(this.ceilingsToWrite(), rolled);
17281
17310
  const budgetValidation = validateRunBudget(budget);
17282
17311
  if (!budgetValidation.ok) {
@@ -17284,12 +17313,56 @@ class RunStore {
17284
17313
  throw new RunStoreError(`refusing to write an invalid budget.yml: ${first?.path ?? ""} ${first?.message ?? ""}`);
17285
17314
  }
17286
17315
  writeAtomic(join14(this.runDir, "budget.yml"), emitBudgetYaml(budget));
17287
- writeAtomic(join14(this.runDir, "run.yml"), emitRunYaml(rolled));
17316
+ const runPath = join14(this.runDir, "run.yml");
17317
+ writeAtomic(runPath, emitRunYaml(rolled));
17288
17318
  this.current = rolled;
17319
+ this.loaded = rolled;
17289
17320
  this.currentBudget = budget;
17290
17321
  this.budgetMutated = false;
17322
+ if (base.fallback !== null) {
17323
+ process.stderr.write(`tldrx: ${runPath} ${base.fallback}, so it could not be merged with: this process's copy of ` + `the run was written whole, and the version that was on disk is beside it as ${backupPathFor(runPath)}. ` + "A change another command wrote there since this process read the run (a budget raise, a " + `cancel, a rejection) may be in that backup and not in run.yml.
17324
+ `);
17325
+ }
17326
+ if (base.cancelledUnder) {
17327
+ this.cancelledUnderFlag = true;
17328
+ const who = rolled.cancelled;
17329
+ process.stderr.write(`tldrx: run ${rolled.run} was cancelled` + (who === undefined ? "" : ` by ${who.by} at ${who.at} (${who.note})`) + " while this process held it: the cancel's statuses were kept, this process's task rows were " + `recorded beside them, and nothing more may be spawned for it.
17330
+ `);
17331
+ }
17332
+ return { cancelledUnder: base.cancelledUnder };
17291
17333
  });
17292
17334
  }
17335
+ runToWrite() {
17336
+ const path = join14(this.runDir, "run.yml");
17337
+ if (!existsSync12(path))
17338
+ return { run: this.current, fallback: null, cancelledUnder: false };
17339
+ let doc;
17340
+ try {
17341
+ doc = parseYaml(readFileSync12(path, "utf8"));
17342
+ } catch (error) {
17343
+ const detail = error instanceof Error ? error.message : String(error);
17344
+ return { run: this.current, fallback: `could not be read or parsed (${detail})`, cancelledUnder: false };
17345
+ }
17346
+ const validation = validateRunFile(doc);
17347
+ if (!validation.ok) {
17348
+ const first = validation.issues[0];
17349
+ return {
17350
+ run: this.current,
17351
+ fallback: `does not validate (${first?.path ?? ""} ${first?.message ?? "schema error"})`,
17352
+ cancelledUnder: false
17353
+ };
17354
+ }
17355
+ const onDisk = asRunFile(doc);
17356
+ if (onDisk.run !== this.current.run) {
17357
+ throw new RunStoreError(`refusing to write ${path}: it records run ${onDisk.run}, and this store holds run ` + `${this.current.run} — a save never overwrites another run's record`);
17358
+ }
17359
+ const cancelledUnder = onDisk.cancelled !== undefined && this.loaded.cancelled === undefined && this.current.cancelled === undefined;
17360
+ return {
17361
+ run: carryChanges(onDisk, this.current, this.loaded, { cancelledWins: this.cancelledUnderFlag || cancelledUnder }),
17362
+ fallback: null,
17363
+ cancelledUnder
17364
+ };
17365
+ }
17293
17366
  ceilingsToWrite() {
17294
17367
  if (this.budgetMutated)
17295
17368
  return this.currentBudget;
@@ -17337,6 +17410,41 @@ function parseStateFile(path) {
17337
17410
  `));
17338
17411
  }
17339
17412
  }
17413
+ function carryChanges(onDisk, mine, loaded, rules = { cancelledWins: false }) {
17414
+ const top = carryLevel(onDisk, mine, loaded, rules.cancelledWins ? ["phases", "cursor"] : ["phases"]);
17415
+ const phases = mine.phases.map((phase) => {
17416
+ const theirs = onDisk.phases.find((p) => p.id === phase.id);
17417
+ const was = loaded.phases.find((p) => p.id === phase.id);
17418
+ if (theirs === undefined || was === undefined)
17419
+ return phase;
17420
+ const stages = phase.stages.map((stage2) => {
17421
+ const theirStage = theirs.stages.find((s) => s.id === stage2.id);
17422
+ const wasStage = was.stages.find((s) => s.id === stage2.id);
17423
+ if (theirStage === undefined || wasStage === undefined)
17424
+ return stage2;
17425
+ const kept = rules.cancelledWins && theirStage.status === "cancelled" ? ["status", "started_at", "ended_at"] : [];
17426
+ return carryLevel(theirStage, stage2, wasStage, kept);
17427
+ });
17428
+ return { ...carryLevel(theirs, phase, was, ["stages"]), stages };
17429
+ });
17430
+ return { ...top, phases };
17431
+ }
17432
+ function carryLevel(onDisk, mine, loaded, skip) {
17433
+ const out = { ...onDisk };
17434
+ const theirs = mine;
17435
+ const was = loaded;
17436
+ for (const key of new Set([...Object.keys(theirs), ...Object.keys(was)])) {
17437
+ if (skip.includes(key))
17438
+ continue;
17439
+ if (isDeepStrictEqual(theirs[key], was[key]))
17440
+ continue;
17441
+ if (theirs[key] === undefined)
17442
+ delete out[key];
17443
+ else
17444
+ out[key] = theirs[key];
17445
+ }
17446
+ return out;
17447
+ }
17340
17448
  function rollUp(run) {
17341
17449
  const phases = run.phases.map((phase) => {
17342
17450
  const stages = phase.stages.map((stage2) => ({
@@ -25206,6 +25314,8 @@ function renderStagePreamble(preamble) {
25206
25314
  var QUESTIONS_FILE2 = "questions.md";
25207
25315
  var INPUTS_HEADING = "Inputs";
25208
25316
  var PREVIOUS_ATTEMPT_HEADING = "Previous attempt";
25317
+ var PREVIOUS_ATTEMPT_EDIT_HEADING = "Previous attempt — edit, do not restart";
25318
+ var MAX_PREVIOUS_ATTEMPT_BYTES = 32 * 1024;
25209
25319
  function buildPrompt(parts) {
25210
25320
  return renderParts(parts).map((part) => part.text).join("");
25211
25321
  }
@@ -29938,8 +30048,9 @@ function wideningBullets(rows) {
29938
30048
  for (const row2 of rows.slice(0, MAX_WIDENING_BULLETS)) {
29939
30049
  const named = row2.paths.slice(0, NAMED_WIDENED_PATHS).map((path) => `\`${path}\``).join(", ");
29940
30050
  const more = row2.paths.length > NAMED_WIDENED_PATHS ? `, +${String(row2.paths.length - NAMED_WIDENED_PATHS)} more` : "";
30051
+ const unlisted = (row2.pathsOmitted ?? 0) > 0 ? `${row2.paths.length === 0 ? "" : ", "}${String(row2.pathsOmitted)} path(s) not listed on the event` : "";
29941
30052
  const opening = row2.basis === "measured" ? `${row2.story}'s declared surface was under the work it did (measured)` : `${row2.story}'s surface was widened by a person (declared)`;
29942
- out.push(`- ${opening}: +${named}${more} ` + `(${String(row2.before)} → ${String(row2.after)} path(s))` + `${row2.note === "" ? "" : ` — ${row2.note}`} [src: events.jsonl:${String(row2.line)}]`);
30053
+ out.push(`- ${opening}: +${named}${more}${unlisted} ` + `(${String(row2.before)} → ${String(row2.after)} path(s))` + `${row2.note === "" ? "" : ` — ${row2.note}`} [src: events.jsonl:${String(row2.line)}]`);
29943
30054
  }
29944
30055
  if (rows.length > MAX_WIDENING_BULLETS) {
29945
30056
  const next = rows[MAX_WIDENING_BULLETS]?.line ?? 1;
@@ -34508,94 +34619,6 @@ function quoted(values) {
34508
34619
  // src/core/watch/watchPrompt.ts
34509
34620
  import { existsSync as existsSync66, readFileSync as readFileSync60 } from "node:fs";
34510
34621
  import { join as join80 } from "node:path";
34511
- var WATCH_FACT_AREAS = ["observability", "deploy", "ownership"];
34512
- function featureInputs(options) {
34513
- const inputs = [];
34514
- for (const story of options.feature.stories) {
34515
- inputs.push({ path: story.path, content: story.text });
34516
- }
34517
- const epicPath = `${PLAN_PHASE2}/${EPICS_DIR}/${options.feature.epicId}.md`;
34518
- const epicAbs = join80(options.runDir, epicPath);
34519
- if (existsSync66(epicAbs))
34520
- inputs.push({ path: epicPath, content: readFileSync60(epicAbs, "utf8") });
34521
- inputs.push({ path: `(git) ${options.feature.epicId} branch vs default branch`, content: renderDiffs(options.diffs) });
34522
- inputs.push({ path: `(facts) area: ${WATCH_FACT_AREAS.join(", ")}`, content: renderWatchFacts(options.facts, options.feature.repos) });
34523
- for (const repo of options.feature.repos) {
34524
- const rel = `${PROJECT_FRAMEWORK_DIR}/map/${repo}/gotchas.md`;
34525
- const abs = join80(options.root, rel);
34526
- if (existsSync66(abs))
34527
- inputs.push({ path: rel, content: readFileSync60(abs, "utf8") });
34528
- }
34529
- return inputs.slice(0, MAX_STAGE_INPUTS);
34530
- }
34531
- function renderWatchFacts(facts, repos) {
34532
- const areas = new Set(WATCH_FACT_AREAS);
34533
- const relevant = facts.filter((fact) => isLive(fact) && areas.has(fact.area) && (fact.repos.length === 0 || fact.repos.some((r) => repos.includes(r))));
34534
- if (relevant.length === 0) {
34535
- return `_No live fact is tagged ${WATCH_FACT_AREAS.join(" or ")} for these repos. ` + "Cite `absent:.tldrx/memory/facts.yml` rather than inventing where a signal is read._";
34536
- }
34537
- return relevant.map((fact) => `- [${fact.id}] ${fact.fact} (${fact.area} · ${fact.confidence})`).join(`
34538
- `);
34539
- }
34540
- function watcherRelPath(featureId2) {
34541
- return `${WATCH_PHASE}/${WATCHERS_DIR}/${featureId2}.md`;
34542
- }
34543
- function featureBrief(feature) {
34544
- const path = watcherRelPath(feature.id);
34545
- const stories = feature.stories.map((s) => s.story.id);
34546
- return [
34547
- `Write exactly ONE file: \`${path}\`. Do not write, edit or delete anything else.`,
34548
- "",
34549
- "Its YAML front matter is fixed — copy it verbatim:",
34550
- "",
34551
- "```yaml",
34552
- "---",
34553
- "version: 1",
34554
- `id: ${feature.id}`,
34555
- `epic: ${feature.epicId}`,
34556
- `title: ${JSON.stringify(feature.title)}`,
34557
- `stories: [${stories.join(", ")}]`,
34558
- `repos: [${feature.repos.join(", ")}]`,
34559
- "status: draft",
34560
- "---",
34561
- "```",
34562
- "",
34563
- `Then these H2 sections, in this order: ${WATCHER_SECTIONS.map((s) => `\`## ${s}\``).join(" · ")}.`,
34564
- "",
34565
- "- Every list item under **Signal**, **Where**, **Healthy baseline** and **Looks broken when**",
34566
- " ends with a `[src: …]` token: `<repo>:<path>:<line>` for a line in the built code, `F<n>` for",
34567
- " a recorded fact, or `absent:<what you looked at>` when the code emits nothing.",
34568
- "- **Signal** names the log line, metric or event that is IN the diff above, at the line it is on.",
34569
- " If nothing is emitted, say so with an `absent:` source and say what to instrument. Do not",
34570
- " describe a signal that would be nice to have as though it exists.",
34571
- "- **Query** is one fenced block, copy-pasteable in whatever place **Where** names.",
34572
- " If — and ONLY if — the code emits nothing at all, so there is no place to paste a query into,",
34573
- " write ONE line instead of the block, sourced like every other claim on the card:",
34574
- "",
34575
- " Query: none — <why nothing is queryable> [src: absent:<what you looked at>]",
34576
- "",
34577
- " This is CHECKED, not trusted: it is refused unless **Signal** above cites `absent:` and the",
34578
- " reason's own source is `absent:` too. A card that names a real signal has somewhere to point a",
34579
- " query at, so `none` on one is a shortcut and will fail the stage. An unsourced `none` is refused",
34580
- " exactly like an unsourced bullet, and describing a query in prose is still refused either way.",
34581
- "- **Sources** is prose: each citation above, once, with what it establishes.",
34582
- "",
34583
- "Owner (optional, gh #70). A Signal item may name WHO to ask about it, as `(owner: <name>)`",
34584
- "placed BEFORE its `[src: …]` token — the token is still the last thing on the line:",
34585
- "",
34586
- " - `checkout.completed` is written on every order (owner: alice) [src: api:src/Checkout.cs:88]",
34587
- "",
34588
- "Write it ONLY from an `ownership` fact inlined above, and write the SAME name that fact uses.",
34589
- "Put it in the front matter as `owner: <name>` when one name covers every item on the card, or on",
34590
- "the individual items when they differ. Do not invent a name, do not put a repo name there (the",
34591
- "framework already derives that from your citation), and leave it off entirely when no fact says.",
34592
- "An `(owner: )` with nothing in it is refused — it loses the name it was trying to write.",
34593
- "",
34594
- "Leave `status: draft`. The framework sets it: a card is stamped `verified` only when nothing",
34595
- "under **Signal** cites `absent:`. Writing `verified` yourself changes nothing and will be overwritten."
34596
- ].join(`
34597
- `);
34598
- }
34599
34622
 
34600
34623
  // src/core/watch/itemOwner.ts
34601
34624
  var MAX_OWNER_CHARS = 64;
@@ -34632,7 +34655,8 @@ function itemOwner(text3) {
34632
34655
  }
34633
34656
 
34634
34657
  // src/core/watch/watcherFile.ts
34635
- var NO_SRC_TOKEN_ISSUE = "no `[src: …]` token — every item on a card is sourced";
34658
+ var NON_FILE_SOURCE_CURE = "a table, queue, dashboard or console is not a line of code, so cite the file that defines it " + "(the migration, model or config, as `<repo>:<path>:<line>`) or the `F<n>` fact that names it";
34659
+ var NO_SRC_TOKEN_ISSUE = `no \`[src: …]\` token — every item on a card is sourced; ${NON_FILE_SOURCE_CURE}`;
34636
34660
  var QUERY_NONE_NOT_EARNED_ISSUE = "`Query: none` is only for a card whose `## Signal` is itself `absent:` — this one names a real signal";
34637
34661
  var QUERY_NOT_PASTEABLE_ISSUE = "`## Query` holds no fenced block — the query has to be copy-pasteable, not described";
34638
34662
  var SRC_SECTION2 = "Evidence ledger";
@@ -34820,12 +34844,147 @@ function setWatcherStatus(text3, status) {
34820
34844
  }
34821
34845
  return text3;
34822
34846
  }
34847
+ function describeWatcherIssue(issue) {
34848
+ return `L${String(issue.line)}${issue.path === "" ? "" : ` ${issue.path}`}: ${issue.message}`;
34849
+ }
34823
34850
  function describeWatcherIssues(issues, max = 5) {
34824
- const shown = issues.slice(0, max).map((issue) => ` L${String(issue.line)}${issue.path === "" ? "" : ` ${issue.path}`}: ${issue.message}`);
34851
+ const shown = issues.slice(0, max).map((issue) => ` ${describeWatcherIssue(issue)}`);
34825
34852
  const rest = issues.length - shown.length;
34826
34853
  return rest > 0 ? [...shown, ` (+${String(rest)} more)`] : shown;
34827
34854
  }
34828
34855
 
34856
+ // src/core/watch/watchPrompt.ts
34857
+ var WATCH_FACT_AREAS = ["observability", "deploy", "ownership"];
34858
+ function featureInputs(options) {
34859
+ const inputs = [];
34860
+ for (const story of options.feature.stories) {
34861
+ inputs.push({ path: story.path, content: story.text });
34862
+ }
34863
+ const epicPath = `${PLAN_PHASE2}/${EPICS_DIR}/${options.feature.epicId}.md`;
34864
+ const epicAbs = join80(options.runDir, epicPath);
34865
+ if (existsSync66(epicAbs))
34866
+ inputs.push({ path: epicPath, content: readFileSync60(epicAbs, "utf8") });
34867
+ inputs.push({ path: `(git) ${options.feature.epicId} branch vs default branch`, content: renderDiffs(options.diffs) });
34868
+ inputs.push({ path: `(facts) area: ${WATCH_FACT_AREAS.join(", ")}`, content: renderWatchFacts(options.facts, options.feature.repos) });
34869
+ for (const repo of options.feature.repos) {
34870
+ const rel = `${PROJECT_FRAMEWORK_DIR}/map/${repo}/gotchas.md`;
34871
+ const abs = join80(options.root, rel);
34872
+ if (existsSync66(abs))
34873
+ inputs.push({ path: rel, content: readFileSync60(abs, "utf8") });
34874
+ }
34875
+ return inputs.slice(0, MAX_STAGE_INPUTS);
34876
+ }
34877
+ function renderWatchFacts(facts, repos) {
34878
+ const areas = new Set(WATCH_FACT_AREAS);
34879
+ const relevant = facts.filter((fact) => isLive(fact) && areas.has(fact.area) && (fact.repos.length === 0 || fact.repos.some((r) => repos.includes(r))));
34880
+ if (relevant.length === 0) {
34881
+ return `_No live fact is tagged ${WATCH_FACT_AREAS.join(" or ")} for these repos. ` + "Cite `absent:.tldrx/memory/facts.yml` rather than inventing where a signal is read._";
34882
+ }
34883
+ return relevant.map((fact) => `- [${fact.id}] ${fact.fact} (${fact.area} · ${fact.confidence})`).join(`
34884
+ `);
34885
+ }
34886
+ function watcherRelPath(featureId2) {
34887
+ return `${WATCH_PHASE}/${WATCHERS_DIR}/${featureId2}.md`;
34888
+ }
34889
+ function featureBrief(feature) {
34890
+ const path = watcherRelPath(feature.id);
34891
+ const stories = feature.stories.map((s) => s.story.id);
34892
+ return [
34893
+ `Write exactly ONE file: \`${path}\`. Do not write, edit or delete anything else.`,
34894
+ "",
34895
+ "Its YAML front matter is fixed — copy it verbatim:",
34896
+ "",
34897
+ "```yaml",
34898
+ "---",
34899
+ "version: 1",
34900
+ `id: ${feature.id}`,
34901
+ `epic: ${feature.epicId}`,
34902
+ `title: ${JSON.stringify(feature.title)}`,
34903
+ `stories: [${stories.join(", ")}]`,
34904
+ `repos: [${feature.repos.join(", ")}]`,
34905
+ "status: draft",
34906
+ "---",
34907
+ "```",
34908
+ "",
34909
+ `Then these H2 sections, in this order: ${WATCHER_SECTIONS.map((s) => `\`## ${s}\``).join(" · ")}.`,
34910
+ "",
34911
+ "- Every list item under **Signal**, **Where**, **Healthy baseline** and **Looks broken when**",
34912
+ " ends with a `[src: …]` token: `<repo>:<path>:<line>` for a line in the built code, `F<n>` for",
34913
+ " a recorded fact, or `absent:<what you looked at>` when the code emits nothing.",
34914
+ `- **Where** names a PLACE, and a place is cited like a line: ${NON_FILE_SOURCE_CURE}.`,
34915
+ " An item with no token at all is refused, however obvious the place. One example, for a table",
34916
+ " that no fact and no dashboard names yet:",
34917
+ "",
34918
+ " - PostgreSQL `leaderboard_refreshes` table, read with `psql` [src: api:db/migrations/0007_leaderboard_refreshes.sql:1]",
34919
+ "",
34920
+ "- **Signal** names the log line, metric or event that is IN the diff above, at the line it is on.",
34921
+ " If nothing is emitted, say so with an `absent:` source and say what to instrument. Do not",
34922
+ " describe a signal that would be nice to have as though it exists.",
34923
+ "- **Query** is one fenced block, copy-pasteable in whatever place **Where** names.",
34924
+ " If — and ONLY if — the code emits nothing at all, so there is no place to paste a query into,",
34925
+ " write ONE line instead of the block, sourced like every other claim on the card:",
34926
+ "",
34927
+ " Query: none — <why nothing is queryable> [src: absent:<what you looked at>]",
34928
+ "",
34929
+ " This is CHECKED, not trusted: it is refused unless **Signal** above cites `absent:` and the",
34930
+ " reason's own source is `absent:` too. A card that names a real signal has somewhere to point a",
34931
+ " query at, so `none` on one is a shortcut and will fail the stage. An unsourced `none` is refused",
34932
+ " exactly like an unsourced bullet, and describing a query in prose is still refused either way.",
34933
+ "- **Sources** is prose: each citation above, once, with what it establishes.",
34934
+ "",
34935
+ "Owner (optional, gh #70). A Signal item may name WHO to ask about it, as `(owner: <name>)`",
34936
+ "placed BEFORE its `[src: …]` token — the token is still the last thing on the line:",
34937
+ "",
34938
+ " - `checkout.completed` is written on every order (owner: alice) [src: api:src/Checkout.cs:88]",
34939
+ "",
34940
+ "Write it ONLY from an `ownership` fact inlined above, and write the SAME name that fact uses.",
34941
+ "Put it in the front matter as `owner: <name>` when one name covers every item on the card, or on",
34942
+ "the individual items when they differ. Do not invent a name, do not put a repo name there (the",
34943
+ "framework already derives that from your citation), and leave it off entirely when no fact says.",
34944
+ "An `(owner: )` with nothing in it is refused — it loses the name it was trying to write.",
34945
+ "",
34946
+ "Leave `status: draft`. The framework sets it: a card is stamped `verified` only when nothing",
34947
+ "under **Signal** cites `absent:`. Writing `verified` yourself changes nothing and will be overwritten."
34948
+ ].join(`
34949
+ `);
34950
+ }
34951
+ function previousCard(options) {
34952
+ const rel = watcherRelPath(options.feature.id);
34953
+ const abs = join80(options.runDir, rel);
34954
+ if (!existsSync66(abs))
34955
+ return "";
34956
+ const text3 = readFileSync60(abs, "utf8");
34957
+ if (text3.trim() === "")
34958
+ return "";
34959
+ const card = parseWatcherCard(text3, options.ctx, options.feature.id);
34960
+ const lines = text3.split(`
34961
+ `);
34962
+ const out = [];
34963
+ if (card.ok) {
34964
+ out.push(`The previous attempt at this stage wrote \`${rel}\` and it validates. The stage is being run`, "again for another reason; keep this card as it is unless the evidence above contradicts it.");
34965
+ } else {
34966
+ out.push(`The previous attempt at this stage wrote \`${rel}\` and it was REFUSED — ` + `${String(card.issues.length)} line(s) do not validate. Each one, with the line as written:`, "");
34967
+ for (const issue of card.issues) {
34968
+ out.push(`- ${describeWatcherIssue(issue)}`);
34969
+ const quoted2 = issue.line > 0 ? lines[issue.line - 1] : undefined;
34970
+ if (quoted2 !== undefined && quoted2.trim() !== "")
34971
+ out.push(` > ${quoted2}`);
34972
+ }
34973
+ out.push("", "Fix what is described above. Everything else in this prompt still applies.");
34974
+ }
34975
+ out.push("", `### ${PREVIOUS_ATTEMPT_EDIT_HEADING}`, "", "This card is on disk RIGHT NOW, exactly as the last attempt left it. It is the draft you are", "being paid to fix, not history: keep every item that already carries a `[src: …]` token, cure", "the lines marked above, and write the file back at the same path. Starting from a blank page", "throws away paid-for work — and, measured, moves a refusal to a different line instead of", "curing it.", "");
34976
+ const budget = options.maxBytes ?? MAX_PREVIOUS_ATTEMPT_BYTES;
34977
+ const size = Buffer.byteLength(text3, "utf8");
34978
+ if (size > budget) {
34979
+ out.push(`_Not inlined (past the ${budget.toLocaleString("en-US")}-byte previous-attempt budget): ` + `${rel} (${size.toLocaleString("en-US")} B). It is on disk; read it before you rewrite it._`);
34980
+ } else {
34981
+ const fence = fenceFor(text3);
34982
+ out.push(`#### \`${rel}\``, "", fence, text3.replace(/\n$/, ""), fence);
34983
+ }
34984
+ return out.join(`
34985
+ `);
34986
+ }
34987
+
34829
34988
  // src/core/watch/renderWatchHandoff.ts
34830
34989
  var NO_STORIES_SRC = `absent:${PLAN_PHASE2}/stories`;
34831
34990
  function renderWatchHandoff(cards, ctx) {
@@ -35050,6 +35209,7 @@ async function watchExecutor(ctx) {
35050
35209
  refused: true
35051
35210
  };
35052
35211
  }
35212
+ const srcCtx = toSrcContext(loadWorkspace(ctx.root), ctx.runDir, { epicRefs: true });
35053
35213
  let prompts = [];
35054
35214
  if (ctx.mode !== "commit") {
35055
35215
  const build = recordedBuild(ctx.runDir);
@@ -35068,7 +35228,7 @@ async function watchExecutor(ctx) {
35068
35228
  signature: incoherent.signature
35069
35229
  };
35070
35230
  }
35071
- prompts = features.map((feature, i) => featurePrompt(ctx, feature, diffs[i] ?? []));
35231
+ prompts = features.map((feature, i) => featurePrompt(ctx, feature, diffs[i] ?? [], srcCtx));
35072
35232
  }
35073
35233
  if (ctx.mode === "prepare")
35074
35234
  return prepare2(ctx, features, prompts);
@@ -35109,7 +35269,6 @@ async function watchExecutor(ctx) {
35109
35269
  }
35110
35270
  }
35111
35271
  }
35112
- const srcCtx = toSrcContext(loadWorkspace(ctx.root), ctx.runDir, { epicRefs: true });
35113
35272
  const written = [];
35114
35273
  for (const feature of features) {
35115
35274
  const rel = watcherRelPath(feature.id);
@@ -35196,7 +35355,7 @@ function collectResults(ctx, features) {
35196
35355
  }
35197
35356
  return { tasks, error: null };
35198
35357
  }
35199
- function featurePrompt(ctx, feature, diffs) {
35358
+ function featurePrompt(ctx, feature, diffs, srcCtx) {
35200
35359
  const facts = FactsStore.loadOrEmpty(factsPath(ctx.root)).facts;
35201
35360
  const stageMd = applyCheckContracts(readFileSync62(stageMdPath(ctx.spec.planned.id, ctx.spec.planned.source), "utf8"), {
35202
35361
  checks: ctx.spec.planned.checks.map((check) => check.id),
@@ -35227,7 +35386,8 @@ function featurePrompt(ctx, feature, diffs) {
35227
35386
  budget_usd: agentShare(ctx, 1).toFixed(2)
35228
35387
  },
35229
35388
  experts: bundles.experts,
35230
- inputs: featureInputs({ root: ctx.root, runDir: ctx.runDir, feature, diffs, facts })
35389
+ inputs: featureInputs({ root: ctx.root, runDir: ctx.runDir, feature, diffs, facts }),
35390
+ previousAttempt: previousCard({ runDir: ctx.runDir, feature, ctx: srcCtx })
35231
35391
  });
35232
35392
  return replaceSection(body, "Feature", featureBrief(feature));
35233
35393
  }
@@ -35649,6 +35809,11 @@ var MEASURED_NOTE_TAIL = " — measured off the story's own diff, the range the
35649
35809
  function basisOf(payload) {
35650
35810
  return payload.basis === "measured" ? "measured" : "declared";
35651
35811
  }
35812
+ function listCount(payload, field) {
35813
+ const list = payload[field];
35814
+ const omitted = payload[`${field}_omitted`];
35815
+ return (Array.isArray(list) ? list.length : 0) + (typeof omitted === "number" ? omitted : 0);
35816
+ }
35652
35817
  function stringList3(value) {
35653
35818
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
35654
35819
  }
@@ -35673,8 +35838,9 @@ function wideningRows(eventsText) {
35673
35838
  story: typeof payload.story === "string" ? payload.story : "?",
35674
35839
  basis: basisOf(payload),
35675
35840
  paths: stringList3(payload.paths),
35676
- before: stringList3(payload.before).length,
35677
- after: stringList3(payload.after).length,
35841
+ ...typeof payload.paths_omitted === "number" && payload.paths_omitted > 0 ? { pathsOmitted: payload.paths_omitted } : {},
35842
+ before: listCount(payload, "before"),
35843
+ after: listCount(payload, "after"),
35678
35844
  note: typeof payload.note === "string" ? payload.note : "",
35679
35845
  line: i + 1
35680
35846
  });
@@ -38436,6 +38602,27 @@ function dependencyHoldOfLog(log) {
38436
38602
  const body = (end === -1 ? rest : rest.slice(0, end)).map((line) => line.trim()).filter((line) => line !== "");
38437
38603
  return body.length === 1 && body[0] !== undefined ? dependencyNamedByHold(body[0]) : null;
38438
38604
  }
38605
+ function dependencyHoldCure(held2) {
38606
+ if (dependencyIsPending(held2.status)) {
38607
+ return `${held2.id} \`done\` is what releases it — it is offered again by the next \`tldrx next --prepare\``;
38608
+ }
38609
+ return held2.status === "blocked" ? `${held2.id} will not land in this run as it stands — \`tldrx story reopen ${held2.id} --note "…"\` gives it its turn` : `${held2.id} is \`todo\` after its wave ran, so its developer never delivered — \`tldrx next --prepare\` offers ${held2.id} first`;
38610
+ }
38611
+ function dependencyPrepareRefusal(storyId, held2) {
38612
+ if (dependencyIsPending(held2.status)) {
38613
+ return `nothing to prepare yet: ${storyId} waits on dependency ${held2.id}, which is \`${held2.status}\`, ` + `not \`done\` — ${storyId} is left \`todo\`, not \`blocked\`; ${dependencyHoldCure(held2)}`;
38614
+ }
38615
+ return `nothing to review: ${dependencyHoldReason(held2)}, so ${storyId} has no developer turn to review and ` + `none is owed — \`tldrx next --prepare\` records ${storyId} \`blocked\` with that reason; ${dependencyHoldCure(held2)}`;
38616
+ }
38617
+ function dependencyCommitRefusal(storyId, held2, work) {
38618
+ return `${storyId} is not settled: ${dependencyHoldReason(held2)}, and a story does not land over a dependency ` + `that has not. Nothing is lost — the bundle stays at ${work.bundleDir}, the work stays on \`${work.branch}\` ` + `in ${work.worktree}, and ${storyId} stays \`in_progress\`; ${dependencyHoldCure(held2)}, ` + `then \`tldrx next --commit\` settles ${storyId}`;
38619
+ }
38620
+ function dependencyNextLine(storyId, held2) {
38621
+ if (dependencyIsPending(held2.status)) {
38622
+ return `nothing is next yet — ${storyId} waits on dependency ${held2.id}, which is \`${held2.status}\`, ` + `not \`done\`; ${dependencyHoldCure(held2)}`;
38623
+ }
38624
+ return `nothing is next — ${dependencyHoldReason(held2)}, so \`tldrx next --prepare\` records ${storyId} ` + `\`blocked\` with that reason and closes the stage at its gate; ${dependencyHoldCure(held2)}`;
38625
+ }
38439
38626
 
38440
38627
  // src/core/facilitator/reviewerModel.ts
38441
38628
  function resolveReviewer(layers) {
@@ -38660,6 +38847,9 @@ function storyLedger(runDir2, scope = null, extraTurns = []) {
38660
38847
  const ceilings = new Map;
38661
38848
  const measured = new Map;
38662
38849
  const unmetered = new Map;
38850
+ const open = new Map;
38851
+ const openStage = new Map;
38852
+ const lost = new Map;
38663
38853
  const storyKeys = new Set;
38664
38854
  const storyPhases = new Set;
38665
38855
  let excluded = 0;
@@ -38708,6 +38898,39 @@ function storyLedger(runDir2, scope = null, extraTurns = []) {
38708
38898
  else
38709
38899
  measured.set(turn.key, round9((measured.get(turn.key) ?? 0) + turn.costUsd));
38710
38900
  }
38901
+ for (const event4 of events) {
38902
+ if (event4.type === "agent.spawned" && inScope(event4)) {
38903
+ const story = str10(event4.payload.story);
38904
+ if (story === null)
38905
+ continue;
38906
+ open.set(story, (open.get(story) ?? 0) + 1);
38907
+ openStage.set(story, event4.stage);
38908
+ continue;
38909
+ }
38910
+ if (event4.type === "agent.result" && inScope(event4)) {
38911
+ const key = str10(event4.payload.key);
38912
+ if (key === null || !isStory(key, str10(event4.payload.phase)))
38913
+ continue;
38914
+ const opened = open.get(key) ?? 0;
38915
+ if (opened > 0)
38916
+ open.set(key, opened - 1);
38917
+ else if ((lost.get(key) ?? 0) > 0)
38918
+ lost.set(key, (lost.get(key) ?? 0) - 1);
38919
+ continue;
38920
+ }
38921
+ if (!closesAttempt(event4))
38922
+ continue;
38923
+ for (const [story, count] of open) {
38924
+ if (count === 0 || openStage.get(story) !== event4.stage)
38925
+ continue;
38926
+ lost.set(story, (lost.get(story) ?? 0) + count);
38927
+ open.set(story, 0);
38928
+ }
38929
+ }
38930
+ for (const [story, count] of lost) {
38931
+ if (count > 0)
38932
+ unmetered.set(story, (unmetered.get(story) ?? 0) + count);
38933
+ }
38711
38934
  return {
38712
38935
  rows: order.map((story) => ({
38713
38936
  story,
@@ -38719,6 +38942,14 @@ function storyLedger(runDir2, scope = null, extraTurns = []) {
38719
38942
  events: events.length
38720
38943
  };
38721
38944
  }
38945
+ function closesAttempt(event4) {
38946
+ if (event4.type === "stage.started" || event4.type === "stage.done" || event4.type === "stage.failed" || event4.type === "stage.skipped")
38947
+ return true;
38948
+ if (event4.type !== "error")
38949
+ return false;
38950
+ const where = str10(event4.payload.where);
38951
+ return where === "executor" || where === "record-tasks";
38952
+ }
38722
38953
  function buildStoryCost(runDir2) {
38723
38954
  let title = "";
38724
38955
  let runId = basename13(runDir2);
@@ -38836,7 +39067,7 @@ function storyVerdict(cost) {
38836
39067
  return "no total: a story above is missing one side, and an absent figure is not summed as zero.";
38837
39068
  }
38838
39069
  if (floor) {
38839
- return `no verdict: ${turns} unmetered, so at least one story's measurement is a LOWER BOUND ` + "— an unmetered turn could put it over a spawn ceiling this cannot see.";
39070
+ return `no verdict: ${turns} unmetered, so at least one story's measurement is a ${LOWER_BOUND_MARK} ` + "— an unmetered turn could put it over a spawn ceiling this cannot see.";
38840
39071
  }
38841
39072
  return "every story measured inside the spawn ceilings it was given.";
38842
39073
  }
@@ -38865,7 +39096,7 @@ function renderStoryCost(cost) {
38865
39096
  lines.push(" measured not recorded — no metered `agent.result` carried a cost for that story. " + "A turn this process never metered is a cost it did not see, never a free one.");
38866
39097
  }
38867
39098
  if (cost.unmeteredStories.length > 0) {
38868
- lines.push(` ${cost.unmeteredStories.join(", ")}: ${plural3(cost.unmeteredTurns, "turn")} UNMETERED, so ` + "that measurement is a LOWER BOUND, not a total.");
39099
+ lines.push(` ${cost.unmeteredStories.join(", ")}: ${plural3(cost.unmeteredTurns, "turn")} UNMETERED, so ` + `that measurement is a ${LOWER_BOUND_MARK}, not a total.`);
38869
39100
  }
38870
39101
  if (cost.excludedKeyedResults > 0)
38871
39102
  lines.push(` ${excludedLine(cost)}`);
@@ -38934,6 +39165,7 @@ function readBudget(runDir2) {
38934
39165
  var ECONOMY_WIDTH = 12;
38935
39166
  var DASH = "—";
38936
39167
  var NOT_RECORDED = "not recorded";
39168
+ var LOWER_BOUND_MARK = "LOWER BOUND";
38937
39169
  function tokenColumns(t, declared2 = 0) {
38938
39170
  const measured = `${bigTokens(t.input)} in · ${bigTokens(t.output)} out · ` + `${bigTokens(t.cacheCreation)} cache write · ${bigTokens(t.cacheRead)} cache read`;
38939
39171
  if (declared2 <= 0)
@@ -39117,7 +39349,7 @@ async function buildExecutor(ctx) {
39117
39349
  try {
39118
39350
  await session.restoreForeignWorkAside();
39119
39351
  } catch {}
39120
- throw error;
39352
+ throw withPartialTasks(error, session.tasks);
39121
39353
  }
39122
39354
  }
39123
39355
  function refusedOnBase(session, error) {
@@ -39302,12 +39534,18 @@ class BuildSession {
39302
39534
  return await this.finish();
39303
39535
  }
39304
39536
  async prepare() {
39305
- const planned = this.nextPending();
39537
+ let planned = this.nextPending();
39306
39538
  if (planned === null)
39307
39539
  return await this.finish();
39308
39540
  const refusal = await this.refuseOnDirtyRepos() ?? await this.refuseOnForeignEpic() ?? await this.setAsideForeign() ?? await this.refuseOnRedBase() ?? await this.refuseOnUnrunnableWorktree();
39309
39541
  if (refusal !== null)
39310
39542
  return refusal;
39543
+ const offered = this.offerAtFrontier();
39544
+ if (offered.kind === "wait")
39545
+ return refusedOnSequence(this.ctx, dependencyPrepareRefusal(offered.story, offered.held));
39546
+ if (offered.kind === "none")
39547
+ return await this.finish();
39548
+ planned = offered.planned;
39311
39549
  const review = this.reviewWorkFor(planned);
39312
39550
  if (review !== null)
39313
39551
  return await this.prepareReview(planned, review);
@@ -39399,6 +39637,9 @@ class BuildSession {
39399
39637
  const refusal = await this.refuseOnDirtyRepos() ?? await this.refuseOnForeignEpic() ?? await this.setAsideForeign();
39400
39638
  if (refusal !== null)
39401
39639
  return refusal;
39640
+ const held2 = this.blockingDependency(planned);
39641
+ if (held2 !== null)
39642
+ return refusedOnSequence(this.ctx, dependencyPrepareRefusal(planned.story.id, held2));
39402
39643
  const work = this.reviewWorkFor(planned) ?? this.reviewWorkFromLedger(planned);
39403
39644
  if (work === null) {
39404
39645
  return refusedOnSequence(this.ctx, `${planned.story.id} has no merged commit to review — a story is reviewed after its developer turn, ` + "not instead of one. Run `tldrx next --prepare` for the developer half first.");
@@ -39493,7 +39734,7 @@ class BuildSession {
39493
39734
  lines: [
39494
39735
  ...this.lines,
39495
39736
  `${planned.story.id} → \`${outcome?.status ?? "?"}\` (host review, unmetered)`,
39496
- `${this.nextPending()?.story.id ?? "?"} is next — run \`tldrx next --prepare\``
39737
+ this.nextAtFrontier()
39497
39738
  ],
39498
39739
  stderr: [...this.advisories],
39499
39740
  error: null
@@ -39505,6 +39746,14 @@ class BuildSession {
39505
39746
  if (planned === null) {
39506
39747
  return refusedOnSequence(this.ctx, "no story is `in_progress` — run `tldrx next --prepare` first");
39507
39748
  }
39749
+ const held2 = this.blockingDependency(planned);
39750
+ if (held2 !== null) {
39751
+ return refusedOnSequence(this.ctx, dependencyCommitRefusal(planned.story.id, held2, {
39752
+ bundleDir: relative16(this.ctx.root, agentDir(this.ctx.runDir, this.bundleKey(planned.story.id))),
39753
+ branch: storyBranchOf(this.ctx.runId, planned.story.id),
39754
+ worktree: relative16(this.ctx.root, this.storyWorktree(planned))
39755
+ }));
39756
+ }
39508
39757
  const key = this.bundleKey(planned.story.id);
39509
39758
  let result2;
39510
39759
  try {
@@ -39553,11 +39802,14 @@ class BuildSession {
39553
39802
  lines: [
39554
39803
  ...this.lines,
39555
39804
  `${planned.story.id} → \`${outcome?.status ?? "?"}\``,
39556
- `${this.nextPending()?.story.id ?? "?"} is next — run \`tldrx next --prepare\``
39805
+ this.nextAtFrontier()
39557
39806
  ],
39558
39807
  error: null
39559
39808
  };
39560
39809
  }
39810
+ cancelledUnder() {
39811
+ return RunStore.cancelledOnDisk(this.ctx.runDir);
39812
+ }
39561
39813
  async driveStory(planned) {
39562
39814
  const resume = this.resumableReview(planned);
39563
39815
  if (resume !== null) {
@@ -39565,6 +39817,10 @@ class BuildSession {
39565
39817
  return;
39566
39818
  }
39567
39819
  for (let i = 0;i < this.attempts; i++) {
39820
+ if (this.cancelledUnder()) {
39821
+ this.lines.push(` · ${planned.story.id}: ${i === 0 ? "not started" : "not requeued"} — the run was cancelled ` + "(tldrx run cancel) while this stage held it");
39822
+ return;
39823
+ }
39568
39824
  await this.settleHalf(await this.buildHalf(planned));
39569
39825
  const outcome = this.outcomes.get(planned.story.id);
39570
39826
  if (outcome !== undefined && outcome.developerError !== null)
@@ -39614,6 +39870,10 @@ class BuildSession {
39614
39870
  const planned = queue[cursor++];
39615
39871
  if (planned === undefined)
39616
39872
  return;
39873
+ if (this.cancelledUnder()) {
39874
+ this.lines.push(` · ${planned.story.id}: not started — the run was cancelled (tldrx run cancel) while this stage held it`);
39875
+ return;
39876
+ }
39617
39877
  halves.set(planned.story.id, await this.buildHalf(planned));
39618
39878
  }
39619
39879
  };
@@ -39940,6 +40200,29 @@ class BuildSession {
39940
40200
  });
39941
40201
  return "settled";
39942
40202
  }
40203
+ if (supplied === undefined && this.cancelledUnder()) {
40204
+ const review2 = reviewerUnfunded("the run was cancelled (tldrx run cancel) before the review — the diff is merged on the epic " + "branch and nobody has judged it");
40205
+ this.ctx.emit("check.failed", {
40206
+ phase: this.ctx.phaseId,
40207
+ check: "review",
40208
+ story: story.planned.story.id,
40209
+ verdict: review2.verdict,
40210
+ attempt: story.attempt,
40211
+ detail: review2.summary
40212
+ });
40213
+ await this.settle(story, "review", {
40214
+ dod,
40215
+ commit: commit2,
40216
+ merged: true,
40217
+ carried,
40218
+ epicBase,
40219
+ verdict: "n-a",
40220
+ review: review2,
40221
+ cost: round23(priorCost),
40222
+ reason: review2.summary
40223
+ });
40224
+ return "settled";
40225
+ }
39943
40226
  const outcome = supplied === undefined ? await this.spawnReviewer(story, dod, epicBase) : { review: supplied, cost: 0 };
39944
40227
  const review = outcome.review;
39945
40228
  const cost = round23(priorCost + outcome.cost);
@@ -40187,6 +40470,41 @@ class BuildSession {
40187
40470
  }
40188
40471
  return decidingHold(holds);
40189
40472
  }
40473
+ offerAtFrontier() {
40474
+ const walked = this.walkFrontier(true);
40475
+ if (walked.kind === "held" && !walked.recorded)
40476
+ return { kind: "wait", story: walked.story, held: walked.held };
40477
+ if (walked.kind === "held")
40478
+ return { kind: "none" };
40479
+ return walked;
40480
+ }
40481
+ nextAtFrontier() {
40482
+ const walked = this.walkFrontier(false);
40483
+ if (walked.kind === "offer")
40484
+ return `${walked.planned.story.id} is next — run \`tldrx next --prepare\``;
40485
+ if (walked.kind === "held")
40486
+ return dependencyNextLine(walked.story, walked.held);
40487
+ return "nothing is next — run `tldrx next --prepare` to close the stage at its gate";
40488
+ }
40489
+ walkFrontier(record3) {
40490
+ let recorded = false;
40491
+ let first2 = null;
40492
+ for (const planned of this.pendingStories()) {
40493
+ const held2 = this.blockingDependency(planned);
40494
+ if (held2 === null)
40495
+ return { kind: "offer", planned };
40496
+ first2 ??= { story: planned.story.id, held: held2 };
40497
+ if (dependencyIsPending(held2.status)) {
40498
+ if (record3)
40499
+ this.waitOnDependency(planned, held2);
40500
+ continue;
40501
+ }
40502
+ if (record3)
40503
+ this.blockOnDependency(planned, held2);
40504
+ recorded = true;
40505
+ }
40506
+ return first2 === null ? { kind: "none" } : { kind: "held", ...first2, recorded };
40507
+ }
40190
40508
  waitOnDependency(planned, held2) {
40191
40509
  this.waits.set(planned.story.id, held2);
40192
40510
  this.lines.push(dependencyWaitLine(planned.story.id, held2));
@@ -40917,14 +41235,33 @@ class BuildSession {
40917
41235
  const measured = measuredWidening(changed, declared2);
40918
41236
  if (measured === null)
40919
41237
  return;
40920
- this.ctx.emit("story.touches_widened", {
40921
- story: story.planned.story.id,
40922
- paths: [...measured.paths],
40923
- note: measured.note,
40924
- before: [...measured.before],
40925
- after: [...measured.after],
40926
- basis: "measured"
40927
- }, 0, "framework");
41238
+ const id = story.planned.story.id;
41239
+ const actor = "framework";
41240
+ try {
41241
+ this.ctx.emit("story.touches_widened", {
41242
+ story: id,
41243
+ paths: [...measured.paths],
41244
+ note: measured.note,
41245
+ before: [...measured.before],
41246
+ after: [...measured.after],
41247
+ basis: "measured"
41248
+ }, 0, actor);
41249
+ return;
41250
+ } catch (error) {
41251
+ const why = firstLine7(error instanceof Error ? error.message : String(error));
41252
+ try {
41253
+ this.ctx.emit("story.touches_widened", {
41254
+ story: id,
41255
+ paths_omitted: measured.paths.length,
41256
+ before_omitted: measured.before.length,
41257
+ after_omitted: measured.after.length,
41258
+ note: `${measured.note} — the lists could not be recorded on this event: ${why}`,
41259
+ basis: "measured"
41260
+ }, 0, actor);
41261
+ } catch (again) {
41262
+ this.advisories.push(`${id}: the measured widening of its surface (${String(measured.paths.length)} path(s) outside ` + `\`touches:\`) could not be recorded in events.jsonl — ` + firstLine7(again instanceof Error ? again.message : String(again)));
41263
+ }
41264
+ }
40928
41265
  }
40929
41266
  orderedOutcomes() {
40930
41267
  const rows = [];
@@ -41535,6 +41872,20 @@ function failed3(ctx, error, tasks) {
41535
41872
  }
41536
41873
 
41537
41874
  // src/core/facilitator/executors/index.ts
41875
+ var PARTIAL_TASKS = Symbol.for("tldrx.partialTasks");
41876
+ function withPartialTasks(error, tasks) {
41877
+ if (tasks.length === 0)
41878
+ return error;
41879
+ const carrier = typeof error === "object" && error !== null ? error : new Error(String(error));
41880
+ Object.defineProperty(carrier, PARTIAL_TASKS, { value: [...tasks], enumerable: false, configurable: true });
41881
+ return carrier;
41882
+ }
41883
+ function partialTasksOf(error) {
41884
+ if (typeof error !== "object" || error === null)
41885
+ return [];
41886
+ const tasks = error[PARTIAL_TASKS];
41887
+ return Array.isArray(tasks) ? tasks : [];
41888
+ }
41538
41889
  var EXECUTORS = new Map([
41539
41890
  [BUILD_PHASE, buildExecutor],
41540
41891
  [WATCH_PHASE, watchExecutor]
@@ -42279,14 +42630,33 @@ async function runExecutor(store, options, phaseId, stageId, spec, notes, execut
42279
42630
  outcome = await executor(executorCtx);
42280
42631
  } catch (error) {
42281
42632
  const why = error instanceof Error ? error.message : String(error);
42282
- appendCapped("error", {
42633
+ const partial = partialTasksOf(error);
42634
+ const progress2 = { rows: 0, events: 0 };
42635
+ let recordingFailed = null;
42636
+ if (partial.length > 0) {
42637
+ try {
42638
+ recordExecutorTasks(store, options, phaseId, stageId, spec, partial, progress2);
42639
+ } catch (again) {
42640
+ recordingFailed = again instanceof Error ? again.message : String(again);
42641
+ }
42642
+ }
42643
+ const written = requireStage(store, phaseId, stageId).tasks.length - tasksBefore;
42644
+ const whole = partial.length > 0 && written === partial.length;
42645
+ store.save();
42646
+ const extra = appendOrSay(() => appendCapped("error", {
42283
42647
  phase: phaseId,
42284
42648
  where: "executor",
42285
42649
  detail: why,
42286
- tasks_recorded: false
42287
- }, 0);
42288
- store.save();
42289
- return failStage(store, options, phaseId, stageId, `the executor threw before returning its task rows — this invocation's turn costs are not in run.yml: ${why}`, notes, tasksBefore);
42650
+ tasks_recorded: whole,
42651
+ rows_written: written,
42652
+ rows_expected: partial.length,
42653
+ events_written: progress2.events,
42654
+ ...recordingFailed === null ? {} : { recording_error: recordingFailed }
42655
+ }, 0));
42656
+ const n = String(partial.length);
42657
+ const eventsShort = recordingFailed === null ? "" : `; ${String(progress2.events)} of ${n} agent.result events could not be appended: ${recordingFailed}`;
42658
+ const reason = partial.length === 0 ? `the executor threw before returning its task rows — this invocation's turn costs are not in run.yml: ${why}` : whole ? `the executor threw after ${n} turn(s) — their rows and costs are in run.yml${eventsShort}: ${why}` : `the executor threw after ${n} turn(s) and recording their rows threw too — ` + `${String(written)} of ${n} rows are in run.yml and the rest are not: ${why}; recording: ${recordingFailed ?? "unknown"}`;
42659
+ return failStage(store, options, phaseId, stageId, reason, [...notes, ...extra], tasksBefore + written);
42290
42660
  }
42291
42661
  if (isSequencingRefusal(outcome)) {
42292
42662
  if (!started) {
@@ -42296,22 +42666,34 @@ async function runExecutor(store, options, phaseId, stageId, spec, notes, execut
42296
42666
  return out(EXIT_USAGE3, [...notes, ...outcome.lines], [], executorSignature(outcome));
42297
42667
  }
42298
42668
  claimEpicBranches(store, outcome.epicBranches, outcome.branchModel);
42669
+ const progress = { rows: 0, events: 0 };
42299
42670
  try {
42300
- recordExecutorTasks(store, options, phaseId, stageId, spec, outcome);
42671
+ recordExecutorTasks(store, options, phaseId, stageId, spec, outcome.tasks, progress);
42301
42672
  } catch (error) {
42302
42673
  const why = error instanceof Error ? error.message : String(error);
42303
42674
  const recorded = requireStage(store, phaseId, stageId).tasks.length;
42304
- appendCappedEvent(store, options, phaseId, stageId, "error", {
42675
+ const written = recorded - tasksBefore;
42676
+ const expected = outcome.tasks.length;
42677
+ store.save();
42678
+ const extra = appendOrSay(() => appendCappedEvent(store, options, phaseId, stageId, "error", {
42305
42679
  phase: phaseId,
42306
42680
  where: "record-tasks",
42307
42681
  detail: why,
42308
- rows_written: recorded - tasksBefore,
42309
- rows_expected: outcome.tasks.length
42310
- }, 0);
42311
- store.save();
42312
- return failStage(store, options, phaseId, stageId, `recording this invocation's task rows threw — ${String(recorded - tasksBefore)} of ` + `${String(outcome.tasks.length)} rows are in run.yml and the rest are not: ${why}`, notes, recorded);
42682
+ rows_written: written,
42683
+ rows_expected: expected,
42684
+ events_written: progress.events
42685
+ }, 0));
42686
+ const rows = written === expected ? `all ${String(expected)} rows are in run.yml and ${String(progress.events)} of ${String(expected)} ` + "agent.result events were appended" : `${String(written)} of ${String(expected)} rows are in run.yml and the rest are not`;
42687
+ return failStage(store, options, phaseId, stageId, `recording this invocation's task rows threw — ${rows}: ${why}`, [...notes, ...extra], recorded);
42313
42688
  }
42314
42689
  store.save();
42690
+ if (store.cancelledUnder) {
42691
+ return out(EXIT_OK4, [
42692
+ ...notes,
42693
+ ...outcome.lines,
42694
+ `run ${store.runId} was cancelled while ${phaseId}/${stageId} ran — stopped here: ` + `${String(outcome.tasks.length)} task row(s) from this invocation are recorded, and nothing more is spawned`
42695
+ ], outcome.stderr ?? []);
42696
+ }
42315
42697
  if (outcome.refused === true) {
42316
42698
  setStatus(store, phaseId, stageId, "ready");
42317
42699
  store.save();
@@ -42332,11 +42714,22 @@ async function runExecutor(store, options, phaseId, stageId, spec, notes, execut
42332
42714
  function isSequencingRefusal(outcome) {
42333
42715
  return outcome.sequencing === true && outcome.refused === true && outcome.tasks.length === 0 && outcome.costUsd === 0 && (outcome.epicBranches?.length ?? 0) === 0;
42334
42716
  }
42335
- function recordExecutorTasks(store, options, phaseId, stageId, spec, outcome) {
42717
+ function appendOrSay(append) {
42718
+ try {
42719
+ append();
42720
+ return [];
42721
+ } catch (error) {
42722
+ const why = error instanceof Error ? error.message : String(error);
42723
+ return [`the error event for this failure could not be appended to events.jsonl — ${why}; run.yml is saved regardless`];
42724
+ }
42725
+ }
42726
+ function recordExecutorTasks(store, options, phaseId, stageId, spec, tasks, progress = { rows: 0, events: 0 }) {
42336
42727
  const version = frameworkVersionSync();
42337
- for (const task2 of outcome.tasks) {
42728
+ const rows = [];
42729
+ for (const task2 of tasks) {
42338
42730
  const id = nextTaskId(store, phaseId, stageId);
42339
42731
  const metered = task2.metered !== false;
42732
+ rows.push({ id, task: task2, metered });
42340
42733
  recordTask(store, phaseId, stageId, {
42341
42734
  id,
42342
42735
  status: task2.error === null ? "done" : "failed",
@@ -42355,6 +42748,9 @@ function recordExecutorTasks(store, options, phaseId, stageId, spec, outcome) {
42355
42748
  ...cacheSplit(task2.usage?.cache_creation_input_tokens, task2.usage?.cache_read_input_tokens),
42356
42749
  ...task2.durationMs === undefined ? {} : { duration_ms: task2.durationMs, duration_basis: "spawned" }
42357
42750
  });
42751
+ progress.rows += 1;
42752
+ }
42753
+ for (const { id, task: task2, metered } of rows) {
42358
42754
  appendCappedEvent(store, options, phaseId, stageId, "agent.result", {
42359
42755
  phase: phaseId,
42360
42756
  task: id,
@@ -42369,6 +42765,7 @@ function recordExecutorTasks(store, options, phaseId, stageId, spec, outcome) {
42369
42765
  ...task2.tokens === undefined ? {} : { tokens: task2.tokens },
42370
42766
  ...task2.usage === undefined ? {} : { usage: usagePayload(task2.usage) }
42371
42767
  }, metered ? round26(task2.costUsd) : 0);
42768
+ progress.events += 1;
42372
42769
  }
42373
42770
  }
42374
42771
  async function commitStage(store, options, phaseId, stageId, spec, notes) {
@@ -42790,7 +43187,6 @@ function assemblePrompt(store, options, spec, stage2, inputs, ctx, seed) {
42790
43187
  dispatchNotes
42791
43188
  };
42792
43189
  }
42793
- var MAX_PREVIOUS_ATTEMPT_BYTES = 32 * 1024;
42794
43190
  function describePreviousAttempt(stage2, options) {
42795
43191
  const lines = [];
42796
43192
  const failure = [...stage2.tasks].reverse().find((task2) => task2.error !== null)?.error ?? null;
@@ -42833,7 +43229,7 @@ function priorOutputs(options) {
42833
43229
  return [];
42834
43230
  const out = [
42835
43231
  "",
42836
- "### Previous attempt — edit, do not restart",
43232
+ `### ${PREVIOUS_ATTEMPT_EDIT_HEADING}`,
42837
43233
  "",
42838
43234
  "These files are on disk RIGHT NOW, exactly as the last attempt left them. They are",
42839
43235
  "not a suggestion and they are not history: they are the draft you are being paid to",
@@ -53250,6 +53646,8 @@ import { isAbsolute as isAbsolute13, join as join135 } from "node:path";
53250
53646
  import { existsSync as existsSync111, readFileSync as readFileSync98, readdirSync as readdirSync34 } from "node:fs";
53251
53647
  import { basename as basename17, join as join131 } from "node:path";
53252
53648
  // src/core/replay/renderReplay.ts
53649
+ var STAGE_FAILED_MARKER = "FAILED: ";
53650
+ var NO_REASON_RECORDED = "no reason recorded";
53253
53651
  function renderReplay(loaded) {
53254
53652
  const run2 = loaded.run;
53255
53653
  const out2 = [
@@ -53377,9 +53775,9 @@ function bullet2(item, trail) {
53377
53775
  case "stage.done":
53378
53776
  return `${prefix5}ended`;
53379
53777
  case "stage.failed":
53380
- return `${prefix5}FAILED: ${text3(payload2.error) || "no reason recorded"}`;
53778
+ return `${prefix5}${STAGE_FAILED_MARKER}${text3(payload2.reason) || NO_REASON_RECORDED}`;
53381
53779
  case "stage.skipped":
53382
- return `${prefix5}skipped: ${text3(payload2.reason) || "no reason recorded"}`;
53780
+ return `${prefix5}skipped: ${text3(payload2.reason) || NO_REASON_RECORDED}`;
53383
53781
  case "question.asked":
53384
53782
  return `${prefix5}${q || "a question"} asked by ${actor}: ${text3(payload2.question)}`.trimEnd();
53385
53783
  case "question.answered":
@@ -53395,7 +53793,7 @@ function bullet2(item, trail) {
53395
53793
  case "story.reopened":
53396
53794
  return `${prefix5}story ${text3(payload2.story) || "?"} REOPENED by ${actor}` + ` — back to \`${text3(payload2.to_status) || "todo"}\` from \`${text3(payload2.from_status) || "?"}\`` + `${note(payload2.note)}`;
53397
53795
  case "story.touches_widened":
53398
- return `${prefix5}story ${text3(payload2.story) || "?"}'s \`touches:\` WIDENED by ${actor}` + ` (${basisOf(payload2)})` + ` — +${pathList(payload2.paths)}` + ` (${String(lengthOf(payload2.before))} → ${String(lengthOf(payload2.after))} path(s))` + `${note(payload2.note)}`;
53796
+ return `${prefix5}story ${text3(payload2.story) || "?"}'s \`touches:\` WIDENED by ${actor}` + ` (${basisOf(payload2)})` + ` — +${widenedPaths(payload2)}` + ` (${String(listCount(payload2, "before"))} → ${String(listCount(payload2, "after"))} path(s))` + `${note(payload2.note)}`;
53399
53797
  case "story.base_fastforwarded":
53400
53798
  return `${prefix5}story ${text3(payload2.story) || "?"}'s base fast-forwarded to` + ` \`${text3(payload2.base) || "?"}\` — ${text3(payload2.from) || "?"} → ${text3(payload2.to) || "?"}` + ` (${String(payload2.commits ?? "?")} commit(s))`;
53401
53799
  case "story.base_updated":
@@ -53432,8 +53830,11 @@ function bullet2(item, trail) {
53432
53830
  return `${prefix5}map refreshed`;
53433
53831
  case "result.unreadable":
53434
53832
  return `${prefix5}UNREADABLE ${text3(payload2.path) || "result.json"}` + ` — ${text3(payload2.error) || "no parse error recorded"}` + " (nothing failed; rewrite it and run the same command again)";
53435
- case "error":
53436
- return `${prefix5}error: ${text3(payload2.message) || "no message recorded"}`;
53833
+ case "error": {
53834
+ const what = text3(payload2.detail) || text3(payload2.detail_omitted) || text3(payload2.message) || "no message recorded";
53835
+ const rows = typeof payload2.rows_expected === "number" ? ` (${String(payload2.rows_written ?? "?")} of ${String(payload2.rows_expected)} task rows recorded)` : "";
53836
+ return `${prefix5}error: ${what}${rows}`;
53837
+ }
53437
53838
  default:
53438
53839
  return null;
53439
53840
  }
@@ -53491,8 +53892,12 @@ function pathList(value) {
53491
53892
  const items = value.filter((item) => typeof item === "string");
53492
53893
  return items.length === 0 ? "?" : items.join(", ");
53493
53894
  }
53494
- function lengthOf(value) {
53495
- return Array.isArray(value) ? value.length : 0;
53895
+ function widenedPaths(payload2) {
53896
+ const listed = pathList(payload2.paths);
53897
+ const omitted = typeof payload2.paths_omitted === "number" && payload2.paths_omitted > 0 ? `${String(payload2.paths_omitted)} path(s) not listed on the event` : "";
53898
+ if (omitted === "")
53899
+ return listed;
53900
+ return listed === "?" ? omitted : `${listed}, ${omitted}`;
53496
53901
  }
53497
53902
  function signature(actor, payload2) {
53498
53903
  const executed = payload2.executed_by;