tldr-experts 0.3.1 → 0.5.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +2396 -0
  2. package/README.md +133 -17
  3. package/dist/hooks/answer-capture.js +7 -8
  4. package/dist/hooks/budget-gate.js +83 -16
  5. package/dist/hooks/{chunk-zdxgragg.js → chunk-14zn51kh.js} +171 -16
  6. package/dist/hooks/{chunk-xpxe94qe.js → chunk-3w55tp71.js} +75 -27
  7. package/dist/hooks/{chunk-afamdvyn.js → chunk-5556vjt5.js} +1 -1
  8. package/dist/hooks/{chunk-ytvmc5ns.js → chunk-889tybxc.js} +26 -28
  9. package/dist/hooks/{chunk-9gb21660.js → chunk-a5dq2dcp.js} +54 -14
  10. package/dist/hooks/{chunk-ztczwtj0.js → chunk-a6rpj2cp.js} +835 -16
  11. package/dist/hooks/{chunk-3g61yg59.js → chunk-bvm6vjrt.js} +1 -1
  12. package/dist/hooks/{chunk-0z27twdk.js → chunk-hcrbr430.js} +10 -5
  13. package/dist/hooks/{chunk-3t91gvpp.js → chunk-nadqsr3w.js} +8 -2
  14. package/dist/hooks/{chunk-458wgg9j.js → chunk-q8d3sff9.js} +86 -11
  15. package/dist/hooks/{chunk-9kkm6q0t.js → chunk-qw73rdbr.js} +35 -3
  16. package/dist/hooks/{chunk-ybacnpxd.js → chunk-sae7sqty.js} +5 -0
  17. package/dist/hooks/{chunk-s5qsb4k6.js → chunk-v1c1hpb8.js} +1 -1
  18. package/dist/hooks/claim-sources.js +32 -21
  19. package/dist/hooks/dod-gate.js +7 -6
  20. package/dist/hooks/no-reask.js +9 -9
  21. package/dist/hooks/session-start.js +46 -21
  22. package/dist/hooks/statusline.js +8 -9
  23. package/dist/tldrx.js +19667 -10978
  24. package/package.json +4 -2
  25. package/plugin/.claude-plugin/plugin.json +2 -2
  26. package/plugin/skills/tldrx/SKILL.md +1 -1
  27. package/stages/build/stage.yml +2 -1
  28. package/stages/plan/stage.md +11 -0
  29. package/stages/watch/stage.md +4 -0
  30. package/templates/expert.md +13 -1
  31. package/templates/watcher.md +8 -1
  32. package/workflows/bugfix.yml +8 -4
  33. package/workflows/docs.yml +8 -4
  34. package/workflows/feature.yml +8 -4
  35. package/workflows/hotfix.yml +8 -4
  36. package/workflows/integration.yml +8 -4
  37. package/workflows/migration.yml +8 -4
  38. package/workflows/performance.yml +8 -4
  39. package/workflows/prototype.yml +8 -4
  40. package/workflows/refactor.yml +8 -4
  41. package/workflows/security-patch.yml +8 -4
  42. package/workflows/spike.yml +8 -4
  43. package/workflows/upgrade.yml +8 -4
  44. package/dist/hooks/chunk-sznsenee.js +0 -503
  45. package/templates/epic.md +0 -38
  46. package/templates/story.md +0 -55
@@ -1,6 +1,3 @@
1
- import {
2
- parseHandoff
3
- } from "./chunk-458wgg9j.js";
4
1
  import {
5
2
  MAX_PLAN_STORIES,
6
3
  MAX_STORIES_PER_WAVE,
@@ -11,10 +8,7 @@ import {
11
8
  requirePattern,
12
9
  requireStringList,
13
10
  requireVersion1
14
- } from "./chunk-0z27twdk.js";
15
- import {
16
- classifySrc
17
- } from "./chunk-sznsenee.js";
11
+ } from "./chunk-hcrbr430.js";
18
12
  import {
19
13
  asDocument,
20
14
  isRecord,
@@ -27,11 +21,17 @@ import {
27
21
  requireVersion,
28
22
  result
29
23
  } from "./chunk-39zh2e44.js";
24
+ import {
25
+ parseHandoff
26
+ } from "./chunk-q8d3sff9.js";
30
27
  import {
31
28
  PROJECT_FRAMEWORK_DIR,
29
+ SRC_PATTERNS,
30
+ classifySrc,
32
31
  listRunDirs,
33
- parseYaml
34
- } from "./chunk-ztczwtj0.js";
32
+ parseYaml,
33
+ readableSource
34
+ } from "./chunk-a6rpj2cp.js";
35
35
 
36
36
  // src/hooks/lib/runFile.ts
37
37
  import { existsSync, readFileSync } from "node:fs";
@@ -67,7 +67,8 @@ function loadRunView(runDir) {
67
67
  status: str(s?.status),
68
68
  expert: typeof s?.expert === "string" ? s.expert : null,
69
69
  budget_usd: typeof s?.budget_usd === "number" ? s.budget_usd : null,
70
- cost_usd: typeof s?.cost_usd === "number" ? s.cost_usd : null
70
+ cost_usd: typeof s?.cost_usd === "number" ? s.cost_usd : null,
71
+ tasks: tasksOf(s?.tasks)
71
72
  });
72
73
  }
73
74
  }
@@ -81,10 +82,69 @@ function loadRunView(runDir) {
81
82
  scope: str(doc.scope),
82
83
  status: str(doc.status),
83
84
  updated_at: str(doc.updated_at),
85
+ attended_by: typeof doc.attended_by === "string" && doc.attended_by !== "" ? doc.attended_by : null,
84
86
  cursor,
85
87
  phases
86
88
  };
87
89
  }
90
+ function tasksOf(value) {
91
+ if (!Array.isArray(value))
92
+ return [];
93
+ return value.map((task) => ({
94
+ cost_usd: typeof task?.cost_usd === "number" ? task.cost_usd : null,
95
+ metered: task?.metered !== false,
96
+ tokens: typeof task?.tokens === "number" ? task.tokens : null
97
+ }));
98
+ }
99
+ function isAttendedByHostView(view) {
100
+ return view.attended_by === "host";
101
+ }
102
+ function runSpend(view) {
103
+ let meteredUsd = 0;
104
+ let hostTokens = 0;
105
+ let unmeteredTasks = 0;
106
+ for (const phase of view.phases) {
107
+ for (const stage of phase.stages) {
108
+ for (const task of stage.tasks) {
109
+ if (task.cost_usd !== null)
110
+ meteredUsd += task.cost_usd;
111
+ if (!task.metered || task.cost_usd === null)
112
+ unmeteredTasks += 1;
113
+ if (task.tokens !== null)
114
+ hostTokens += task.tokens;
115
+ }
116
+ }
117
+ }
118
+ return { meteredUsd: Math.round(meteredUsd * 100) / 100, hostTokens, unmeteredTasks };
119
+ }
120
+ function hostTokensIn(view, phaseId) {
121
+ let tokens = 0;
122
+ for (const phase of view.phases) {
123
+ if (phaseId !== null && phase.id !== phaseId)
124
+ continue;
125
+ for (const stage of phase.stages) {
126
+ for (const task of stage.tasks) {
127
+ if (task.tokens !== null)
128
+ tokens += task.tokens;
129
+ }
130
+ }
131
+ }
132
+ return tokens;
133
+ }
134
+ function renderRunEconomies(view) {
135
+ const spend = runSpend(view);
136
+ const attended = isAttendedByHostView(view);
137
+ if (spend.hostTokens === 0 && spend.unmeteredTasks === 0 && !attended)
138
+ return null;
139
+ const parts = [`$${spend.meteredUsd.toFixed(2)} metered`];
140
+ if (spend.hostTokens > 0)
141
+ parts.push(`${String(spend.hostTokens)} host tokens`);
142
+ if (spend.unmeteredTasks > 0) {
143
+ parts.push(`${String(spend.unmeteredTasks)} unmetered turn${spend.unmeteredTasks === 1 ? "" : "s"}`);
144
+ }
145
+ const who = attended ? " (attended_by: host — the framework does not spawn on this run)" : "";
146
+ return `spend so far: ${parts.join(" + ")}${who}. ` + "The dollar figure is METERED spend only; host tokens are a different currency and are never converted.";
147
+ }
88
148
  function isTerminal(status) {
89
149
  return TERMINAL_STATUSES.includes(status);
90
150
  }
@@ -126,6 +186,8 @@ function nowRfc3339() {
126
186
 
127
187
  // src/core/budget/RunBudget.ts
128
188
  var ON_EXCEED = ["block", "warn"];
189
+ var ON_HOST_TOKENS_EXCEED = ["warn", "block"];
190
+ var DEFAULT_ON_HOST_TOKENS_EXCEED = "warn";
129
191
  var ECONOMIES = ["metered-usd", "host-tokens"];
130
192
  var DEFAULT_ECONOMY = "metered-usd";
131
193
  var DEFAULT_WARN_AT_PCT = 80;
@@ -158,6 +220,12 @@ function validateRunBudget(input) {
158
220
  if (doc.economy !== undefined && doc.economy !== null) {
159
221
  requireEnum(doc.economy, ECONOMIES, "economy", issues);
160
222
  }
223
+ if (doc.on_host_tokens_exceed !== undefined && doc.on_host_tokens_exceed !== null) {
224
+ requireEnum(doc.on_host_tokens_exceed, ON_HOST_TOKENS_EXCEED, "on_host_tokens_exceed", issues);
225
+ }
226
+ if (doc.ceiling_host_tokens !== undefined && doc.ceiling_host_tokens !== null) {
227
+ requireNumber(doc.ceiling_host_tokens, "ceiling_host_tokens", issues);
228
+ }
161
229
  if (doc.warn_at_pct !== undefined) {
162
230
  requireNumber(doc.warn_at_pct, "warn_at_pct", issues);
163
231
  const pct = doc.warn_at_pct;
@@ -171,7 +239,9 @@ function validateRunBudget(input) {
171
239
  if (phases.length > MAX_PHASES) {
172
240
  issues.push({ path: "phases", message: `${phases.length} phases exceeds the ${MAX_PHASES} cap` });
173
241
  }
242
+ const runEconomy = ECONOMIES.includes(doc.economy) ? doc.economy : DEFAULT_ECONOMY;
174
243
  let sum = 0;
244
+ let tokenSum = 0;
175
245
  phases.forEach((phase, i) => {
176
246
  const path = `phases[${i}]`;
177
247
  if (!isRecord(phase)) {
@@ -185,12 +255,27 @@ function validateRunBudget(input) {
185
255
  if (phase.economy !== undefined && phase.economy !== null) {
186
256
  requireEnum(phase.economy, ECONOMIES, `${path}.economy`, issues);
187
257
  }
258
+ if (phase.ceiling_host_tokens !== undefined && phase.ceiling_host_tokens !== null) {
259
+ requireNumber(phase.ceiling_host_tokens, `${path}.ceiling_host_tokens`, issues);
260
+ }
261
+ const economy = ECONOMIES.includes(phase.economy) ? phase.economy : runEconomy;
262
+ if (economy === "host-tokens") {
263
+ const tokens = typeof phase.ceiling_host_tokens === "number" ? phase.ceiling_host_tokens : typeof phase.ceiling_usd === "number" ? phase.ceiling_usd : 0;
264
+ tokenSum += tokens;
265
+ return;
266
+ }
188
267
  if (typeof phase.ceiling_usd === "number")
189
268
  sum += phase.ceiling_usd;
190
269
  });
191
270
  if (typeof doc.ceiling_usd === "number" && sum > doc.ceiling_usd + 0.000000001) {
192
271
  issues.push({ path: "phases", message: `phase ceilings sum to ${sum} > ceiling_usd ${doc.ceiling_usd}` });
193
272
  }
273
+ if (typeof doc.ceiling_host_tokens === "number" && tokenSum > doc.ceiling_host_tokens + 0.000000001) {
274
+ issues.push({
275
+ path: "phases",
276
+ message: `phase host-token ceilings sum to ${tokenSum} > ceiling_host_tokens ${doc.ceiling_host_tokens}`
277
+ });
278
+ }
194
279
  return result(issues, deprecations);
195
280
  }
196
281
  function asRunBudget(input) {
@@ -203,14 +288,27 @@ function asRunBudget(input) {
203
288
  warn_at_pct: doc.warn_at_pct ?? DEFAULT_WARN_AT_PCT,
204
289
  on_exceed: doc.on_exceed ?? "block",
205
290
  economy: doc.economy ?? DEFAULT_ECONOMY,
291
+ on_host_tokens_exceed: doc.on_host_tokens_exceed ?? DEFAULT_ON_HOST_TOKENS_EXCEED,
292
+ ceiling_host_tokens: doc.ceiling_host_tokens ?? null,
206
293
  phases: (doc.phases ?? []).map((phase) => ({
207
294
  id: phase.id,
208
295
  ceiling_usd: phase.ceiling_usd,
209
296
  spent_usd: phase.spent_usd,
210
- economy: phase.economy ?? null
297
+ economy: phase.economy ?? null,
298
+ ceiling_host_tokens: phase.ceiling_host_tokens ?? null
211
299
  }))
212
300
  };
213
301
  }
302
+ function hostTokenCeiling(budget, phaseId) {
303
+ if (budget === null || !isHostTokens(budget, phaseId))
304
+ return null;
305
+ if (phaseId !== undefined && phaseId !== null) {
306
+ const phase = budget.phases.find((entry) => entry.id === phaseId);
307
+ if (phase !== undefined)
308
+ return phase.ceiling_host_tokens ?? phase.ceiling_usd;
309
+ }
310
+ return budget.ceiling_host_tokens ?? budget.ceiling_usd;
311
+ }
214
312
 
215
313
  // src/core/budget/remainingWork.ts
216
314
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
@@ -326,6 +424,12 @@ var SKIPPED_DIRS = new Set([
326
424
  "test-results"
327
425
  ]);
328
426
 
427
+ // src/core/plan/branchModel.ts
428
+ var BRANCH_MODELS = ["per-epic", "integration"];
429
+ function isBranchModelKind(value) {
430
+ return typeof value === "string" && BRANCH_MODELS.includes(value);
431
+ }
432
+
329
433
  // src/core/experts/expertDocument.ts
330
434
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
331
435
  import { join as join3 } from "node:path";
@@ -889,6 +993,21 @@ function stackExpertNames(root, repos) {
889
993
  return names;
890
994
  }
891
995
 
996
+ // src/core/text/srcGrammarContract.ts
997
+ var KINDS = {
998
+ file: { shape: "`[repo:]path:line[-line]`", example: "api:src/Selector.ts:241" },
999
+ doc: { shape: "`https://` + a non-space URL", example: "https://example.com/spec" },
1000
+ answer: { shape: `\`Q<n>\` (${readableSource(SRC_PATTERNS.answer)})`, example: "Q3" },
1001
+ fact: { shape: `\`F<nnn>\` (${readableSource(SRC_PATTERNS.fact)})`, example: "F102" },
1002
+ cmd: { shape: `\`$ <command> → exit <n>\` (${readableSource(SRC_PATTERNS.cmd)})`, example: "$ bun test → exit 0" },
1003
+ graph: { shape: "`graph:<node id>`", example: "graph:hunt-engine" },
1004
+ absent: { shape: "`absent:<the path you looked at>`", example: "absent:docs/retention.md" },
1005
+ aidlc: {
1006
+ shape: `\`aidlc:<file>:<line>\` or \`aidlc:<file>#Q<n>\` (${readableSource(SRC_PATTERNS.aidlcLine)})`,
1007
+ example: "aidlc:intents/260821/design.md:14"
1008
+ }
1009
+ };
1010
+
892
1011
  // src/core/build/prompts.ts
893
1012
  var MAX_TOUCHED_BYTES = 64 * 1024;
894
1013
 
@@ -1082,6 +1201,8 @@ function round(n) {
1082
1201
  }
1083
1202
 
1084
1203
  // src/core/build/review.ts
1204
+ var VERDICT_WORDS = ["approve", "fixlist", "changes"];
1205
+ var VERDICT_ENUM = VERDICT_WORDS.join("|");
1085
1206
  var REVIEWER_FAILED = "the reviewer sub-agent failed";
1086
1207
  function looksLikeReviewerError(detail) {
1087
1208
  const text = detail.trim();
@@ -1096,6 +1217,9 @@ function looksLikeSpawnError(detail) {
1096
1217
  var MAX_ATTEMPTS = 2;
1097
1218
  var REVIEWER_SHARE = 0.25;
1098
1219
  var REVIEWER_FLOOR_USD = 1;
1220
+ function developerPriceDivisor(attempt) {
1221
+ return attempt <= 1 ? 1 + REVIEWER_SHARE : MAX_ATTEMPTS * (1 + REVIEWER_SHARE);
1222
+ }
1099
1223
  function remainingWork(input) {
1100
1224
  try {
1101
1225
  return measure(input);
@@ -1111,6 +1235,7 @@ function staticOnly(input) {
1111
1235
  staticUsd: input.stageBudgetUsd,
1112
1236
  clamped: false,
1113
1237
  economy: input.economy ?? DEFAULT_ECONOMY,
1238
+ attended: input.attended === true,
1114
1239
  stories: [],
1115
1240
  done: 0,
1116
1241
  total: 0,
@@ -1119,6 +1244,7 @@ function staticOnly(input) {
1119
1244
  }
1120
1245
  function measure(input) {
1121
1246
  const economy = input.economy ?? DEFAULT_ECONOMY;
1247
+ const hostPaysDeveloper = economy === "host-tokens" || input.attended === true;
1122
1248
  const fallback = {
1123
1249
  basis: "static",
1124
1250
  usd: input.stageBudgetUsd,
@@ -1126,6 +1252,7 @@ function measure(input) {
1126
1252
  staticUsd: input.stageBudgetUsd,
1127
1253
  clamped: false,
1128
1254
  economy,
1255
+ attended: input.attended === true,
1129
1256
  stories: [],
1130
1257
  done: 0,
1131
1258
  total: 0,
@@ -1157,9 +1284,12 @@ function measure(input) {
1157
1284
  if (attemptsLeft === 0)
1158
1285
  continue;
1159
1286
  const developerTurns = Math.max(attemptsLeft - (story.status === "review" ? 1 : 0), 0);
1160
- const developerCapUsd = economy === "host-tokens" ? 0 : caps.developer(story.id);
1287
+ const firstTurn = MAX_ATTEMPTS - developerTurns + 1;
1288
+ const developerCapsUsd = Array.from({ length: developerTurns }, (_unused, i) => hostPaysDeveloper ? 0 : caps.developer(story.id, firstTurn + i));
1289
+ const developerCapUsd = developerCapsUsd[0] ?? 0;
1161
1290
  const reviewerCapUsd = caps.reviewer(story.id);
1162
- const usd = round2(developerCapUsd * developerTurns + reviewerCapUsd * attemptsLeft);
1291
+ const developerUsd = developerCapsUsd.reduce((sum, cap) => sum + cap, 0);
1292
+ const usd = round2(developerUsd + reviewerCapUsd * attemptsLeft);
1163
1293
  if (usd <= 0 && developerTurns === 0 && attemptsLeft === 0)
1164
1294
  continue;
1165
1295
  stories.push({
@@ -1168,6 +1298,7 @@ function measure(input) {
1168
1298
  developerTurns,
1169
1299
  reviews: attemptsLeft,
1170
1300
  developerCapUsd,
1301
+ developerCapsUsd,
1171
1302
  reviewerCapUsd,
1172
1303
  usd
1173
1304
  });
@@ -1182,6 +1313,7 @@ function measure(input) {
1182
1313
  staticUsd: input.stageBudgetUsd,
1183
1314
  clamped,
1184
1315
  economy,
1316
+ attended: input.attended === true,
1185
1317
  stories,
1186
1318
  done: progress.done,
1187
1319
  total: progress.total,
@@ -1206,11 +1338,11 @@ class CapMath {
1206
1338
  this.scale = input.stageBudgetUsd <= 0 || sum <= input.stageBudgetUsd ? 1 : input.stageBudgetUsd / sum;
1207
1339
  this.maxBudgetUsd = this.agentCap(1);
1208
1340
  }
1209
- developer(storyId) {
1341
+ developer(storyId, attempt) {
1210
1342
  const price = this.priceOf(storyId);
1211
1343
  if (price === null)
1212
1344
  return this.agentCap(1 / this.worstCaseShares());
1213
- return this.agentCap(this.shareOf(price / (MAX_ATTEMPTS * (1 + REVIEWER_SHARE))));
1345
+ return this.agentCap(this.shareOf(price / developerPriceDivisor(attempt)));
1214
1346
  }
1215
1347
  reviewer(storyId) {
1216
1348
  const price = this.priceOf(storyId);
@@ -1314,6 +1446,20 @@ function wouldExceed(budget, phaseId, estimate) {
1314
1446
  function round3(n) {
1315
1447
  return Math.round(n * 1e6) / 1e6;
1316
1448
  }
1449
+ function wouldExceedHostTokens(budget, phaseId, spentTokens) {
1450
+ const ceiling = hostTokenCeiling(budget, phaseId);
1451
+ if (ceiling === null)
1452
+ return null;
1453
+ const phase = phaseId === null ? undefined : budget.phases.find((p) => p.id === phaseId);
1454
+ const over = ceiling > 0 && spentTokens > ceiling;
1455
+ return {
1456
+ over,
1457
+ blocked: over && budget.on_host_tokens_exceed === "block",
1458
+ scope: phase === undefined ? "run" : "phase",
1459
+ spent: spentTokens,
1460
+ ceiling
1461
+ };
1462
+ }
1317
1463
 
1318
1464
  // src/core/run/gatePolicy.ts
1319
1465
  var GATE_POLICIES = ["human", "auto", "agent"];
@@ -1450,6 +1596,12 @@ function validateRunFile(input) {
1450
1596
  requireString(branch, `build.epic_branch[${i}]`, issues);
1451
1597
  });
1452
1598
  }
1599
+ if (doc.build.branch_model !== undefined && !isBranchModelKind(doc.build.branch_model)) {
1600
+ issues.push({
1601
+ path: "build.branch_model",
1602
+ message: `expected one of ${BRANCH_MODELS.join(" | ")}`
1603
+ });
1604
+ }
1453
1605
  } else {
1454
1606
  issues.push({ path: "build", message: "expected a mapping" });
1455
1607
  }
@@ -1479,6 +1631,9 @@ function validateRunFile(input) {
1479
1631
  }
1480
1632
  if (doc.attended_by !== undefined)
1481
1633
  requireEnum(doc.attended_by, ATTENDED_BY, "attended_by", issues);
1634
+ if (doc.keep_worktrees !== undefined && typeof doc.keep_worktrees !== "boolean") {
1635
+ issues.push({ path: "keep_worktrees", message: `expected a boolean, got ${typeof doc.keep_worktrees}` });
1636
+ }
1482
1637
  if (!requireArray(doc.phases, "phases", issues))
1483
1638
  return result(issues, deprecations);
1484
1639
  const phases = doc.phases;
@@ -1652,4 +1807,4 @@ function shortBy(estimate, remaining) {
1652
1807
  return Math.max(0.01, Math.ceil((estimate - remaining) * 100) / 100);
1653
1808
  }
1654
1809
 
1655
- export { loadRunView, cursorStage, openRunViews, newestActiveRun, currentActor, nowRfc3339, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, parseEvidence, evidencePath, gateEvidencePath, stackExpertNames, remainingWork, wouldExceed, isTerminal2 as isTerminal, isFinished, isAttendedByHost, flatten, stageAt, deriveRunStatus, derivePhaseStatus, validateRunFile, asRunFile, raiseCommand, shortBy };
1810
+ export { loadRunView, isAttendedByHostView, runSpend, hostTokensIn, renderRunEconomies, cursorStage, openRunViews, newestActiveRun, currentActor, nowRfc3339, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, parseEvidence, evidencePath, gateEvidencePath, stackExpertNames, remainingWork, wouldExceed, wouldExceedHostTokens, isTerminal2 as isTerminal, isFinished, isAttendedByHost, flatten, stageAt, deriveRunStatus, derivePhaseStatus, validateRunFile, asRunFile, raiseCommand, shortBy };
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  DEFAULT_ECONOMY,
3
+ DEFAULT_ON_HOST_TOKENS_EXCEED,
3
4
  asRunBudget,
4
5
  asRunFile,
5
6
  cursorStage,
@@ -7,6 +8,7 @@ import {
7
8
  deriveRunStatus,
8
9
  flatten,
9
10
  isAttendedByHost,
11
+ isAttendedByHostView,
10
12
  isFinished,
11
13
  isTerminal,
12
14
  nowRfc3339,
@@ -14,34 +16,37 @@ import {
14
16
  stageAt,
15
17
  validateRunBudget,
16
18
  validateRunFile
17
- } from "./chunk-zdxgragg.js";
19
+ } from "./chunk-14zn51kh.js";
18
20
  import {
19
21
  EventLog
20
- } from "./chunk-ybacnpxd.js";
22
+ } from "./chunk-sae7sqty.js";
21
23
  import {
24
+ backupPathFor,
22
25
  isAlive,
23
26
  readLock,
24
27
  withWorkspaceLock,
25
28
  workspaceRootOfRunDir,
29
+ writeAtomic,
26
30
  yamlScalar
27
- } from "./chunk-9kkm6q0t.js";
31
+ } from "./chunk-qw73rdbr.js";
28
32
  import {
29
33
  noteDeprecations,
30
34
  openBlocks,
31
35
  parseQuestions
32
- } from "./chunk-3t91gvpp.js";
36
+ } from "./chunk-nadqsr3w.js";
33
37
  import {
34
38
  listRunDirs,
35
- parseYaml
36
- } from "./chunk-ztczwtj0.js";
39
+ parseYaml,
40
+ parseYamlRepairing
41
+ } from "./chunk-a6rpj2cp.js";
37
42
 
38
43
  // src/core/statusline/runSnapshot.ts
39
44
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
40
45
  import { join as join2 } from "node:path";
41
46
 
42
47
  // src/core/run/RunStore.ts
43
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
44
- import { basename, dirname, join } from "node:path";
48
+ import { existsSync, readFileSync } from "node:fs";
49
+ import { basename, join } from "node:path";
45
50
 
46
51
  // src/core/run/emitRunYaml.ts
47
52
  function inlineList(values) {
@@ -121,7 +126,8 @@ function emitRunYaml(run) {
121
126
  lines.push(`triage: {split: ${yamlScalar(run.triage.split)}, depends_on: ${inlineList(run.triage.depends_on)}}`);
122
127
  }
123
128
  if (run.build !== undefined && run.build.epic_branch.length > 0) {
124
- lines.push(`build: {epic_branch: ${inlineList(run.build.epic_branch)}}`);
129
+ const model = run.build.branch_model === undefined ? "" : `, branch_model: ${yamlScalar(run.build.branch_model)}`;
130
+ lines.push(`build: {epic_branch: ${inlineList(run.build.epic_branch)}${model}}`);
125
131
  }
126
132
  if (run.cancelled !== undefined) {
127
133
  lines.push(`cancelled: {by: ${yamlScalar(run.cancelled.by)}, at: ${yamlScalar(run.cancelled.at)}, ` + `note: ${yamlScalar(run.cancelled.note)}}`);
@@ -129,6 +135,9 @@ function emitRunYaml(run) {
129
135
  if (run.attended_by !== undefined) {
130
136
  lines.push(`attended_by: ${yamlScalar(run.attended_by)}`);
131
137
  }
138
+ if (run.keep_worktrees === true) {
139
+ lines.push("keep_worktrees: true");
140
+ }
132
141
  if (run.gates_policy !== undefined && Object.keys(run.gates_policy).length > 0) {
133
142
  lines.push(`gates_policy: ${gatesPolicy(run.gates_policy)}`);
134
143
  }
@@ -159,16 +168,22 @@ function emitBudgetYaml(budget) {
159
168
  `warn_at_pct: ${budget.warn_at_pct}`,
160
169
  `on_exceed: ${yamlScalar(budget.on_exceed)}`,
161
170
  ...budget.economy === DEFAULT_ECONOMY ? [] : [`economy: ${yamlScalar(budget.economy)}`],
171
+ ...budget.on_host_tokens_exceed === DEFAULT_ON_HOST_TOKENS_EXCEED ? [] : [`on_host_tokens_exceed: ${yamlScalar(budget.on_host_tokens_exceed)}`],
172
+ ...budget.ceiling_host_tokens === null ? [] : [`ceiling_host_tokens: ${tokens(budget.ceiling_host_tokens)}`],
162
173
  "phases:"
163
174
  ];
164
175
  for (const phase of budget.phases) {
165
176
  const economy = phase.economy === null ? "" : `, economy: ${yamlScalar(phase.economy)}`;
166
- lines.push(` - {id: ${yamlScalar(phase.id)}, ceiling_usd: ${money(phase.ceiling_usd)}, ` + `spent_usd: ${money(phase.spent_usd)}${economy}}`);
177
+ const hostTokens = phase.ceiling_host_tokens === null ? "" : `, ceiling_host_tokens: ${tokens(phase.ceiling_host_tokens)}`;
178
+ lines.push(` - {id: ${yamlScalar(phase.id)}, ceiling_usd: ${money(phase.ceiling_usd)}, ` + `spent_usd: ${money(phase.spent_usd)}${economy}${hostTokens}}`);
167
179
  }
168
180
  return `${lines.join(`
169
181
  `)}
170
182
  `;
171
183
  }
184
+ function tokens(value) {
185
+ return String(Math.trunc(value));
186
+ }
172
187
 
173
188
  // src/core/run/RunStore.ts
174
189
  class RunStoreError extends Error {
@@ -190,8 +205,8 @@ class RunStore {
190
205
  const runPath = join(runDir, "run.yml");
191
206
  if (!existsSync(runPath))
192
207
  throw new RunStoreError(`no run.yml in ${runDir}`);
193
- const doc = parseYaml(readFileSync(runPath, "utf8"));
194
- const validation = validateRunFile(doc);
208
+ const run = parseStateFile(runPath);
209
+ const validation = validateRunFile(run.doc);
195
210
  noteDeprecations(runPath, validation);
196
211
  if (!validation.ok) {
197
212
  const first = validation.issues[0];
@@ -200,14 +215,30 @@ class RunStore {
200
215
  const budgetPath = join(runDir, "budget.yml");
201
216
  if (!existsSync(budgetPath))
202
217
  throw new RunStoreError(`no budget.yml in ${runDir}`);
203
- const budgetDoc = parseYaml(readFileSync(budgetPath, "utf8"));
204
- const budgetValidation = validateRunBudget(budgetDoc);
218
+ const budget = parseStateFile(budgetPath);
219
+ const budgetValidation = validateRunBudget(budget.doc);
205
220
  noteDeprecations(budgetPath, budgetValidation);
206
221
  if (!budgetValidation.ok) {
207
222
  const first = budgetValidation.issues[0];
208
223
  throw new RunStoreError(`invalid budget.yml (${budgetPath}): ${first?.path ?? ""} ${first?.message ?? "schema error"}`);
209
224
  }
210
- return new RunStore(runDir, asRunFile(doc), asRunBudget(budgetDoc), EventLog.forRun(runDir));
225
+ const store = new RunStore(runDir, asRunFile(run.doc), asRunBudget(budget.doc), EventLog.forRun(runDir));
226
+ if (run.repaired || budget.repaired)
227
+ store.healOnDisk(run.repaired, budget.repaired);
228
+ return store;
229
+ }
230
+ healOnDisk(runRepaired, budgetRepaired) {
231
+ try {
232
+ withWorkspaceLock(workspaceRootOfRunDir(this.runDir), () => {
233
+ if (runRepaired)
234
+ writeAtomic(join(this.runDir, "run.yml"), emitRunYaml(this.current));
235
+ if (budgetRepaired)
236
+ writeAtomic(join(this.runDir, "budget.yml"), emitBudgetYaml(this.currentBudget));
237
+ });
238
+ const which = [runRepaired ? "run.yml" : "", budgetRepaired ? "budget.yml" : ""].filter((name) => name !== "").join(" and ");
239
+ process.stderr.write(`tldrx: ${this.runDir}: ${which} held text broken across lines by an emitter bug ` + "since fixed. It was repaired in memory and rewritten correctly; the version that was " + `on disk is beside it as .bak. Nothing else about the run was changed.
240
+ `);
241
+ } catch {}
211
242
  }
212
243
  static find(root, runId) {
213
244
  const dir = listRunDirs(root).find((d) => basename(d) === runId);
@@ -309,7 +340,12 @@ class RunStore {
309
340
  ...onDisk,
310
341
  phases: this.currentBudget.phases.map((mine) => {
311
342
  const theirs = onDisk.phases.find((p) => p.id === mine.id);
312
- return theirs === undefined ? mine : { ...mine, ceiling_usd: theirs.ceiling_usd };
343
+ return theirs === undefined ? mine : {
344
+ ...mine,
345
+ ceiling_usd: theirs.ceiling_usd,
346
+ ceiling_host_tokens: theirs.ceiling_host_tokens,
347
+ economy: theirs.economy
348
+ };
313
349
  })
314
350
  };
315
351
  } catch {
@@ -317,18 +353,21 @@ class RunStore {
317
353
  }
318
354
  }
319
355
  }
320
- function writeAtomic(path, content) {
321
- const temp = `${path}.tmp-${String(process.pid)}`;
322
- mkdirSync(dirname(path), { recursive: true });
356
+ function parseStateFile(path) {
357
+ const text = readFileSync(path, "utf8");
323
358
  try {
324
- writeFileSync(temp, content, "utf8");
325
- renameSync(temp, path);
359
+ return parseYamlRepairing(text);
326
360
  } catch (error) {
327
- try {
328
- if (existsSync(temp))
329
- rmSync(temp, { force: true });
330
- } catch {}
331
- throw error;
361
+ const backup = backupPathFor(path);
362
+ const detail = error instanceof Error ? error.message : String(error);
363
+ throw new RunStoreError([
364
+ `${path} does not parse: ${detail}`,
365
+ " every tldrx command on this run reads that file first, so all of them fail until it does",
366
+ existsSync(backup) ? ` the version the last save replaced is at ${backup} — recovery is MANUAL and tldrx ` + "will not do it for you:" + `
367
+ diff it, and if that version is the one you want, copy it back over ${basename(path)}` : ` there is no ${basename(backup)} beside it — this file has not been written since ` + "backups arrived, so there is no previous version to go back to",
368
+ " a hand-edit is the other way out, and it is the only one that keeps work the backup predates"
369
+ ].join(`
370
+ `));
332
371
  }
333
372
  }
334
373
  function rollUp(run) {
@@ -458,7 +497,7 @@ function fromTolerantRead(root) {
458
497
  openCount: open.length,
459
498
  autoGates: 0,
460
499
  staleStages: 0,
461
- attendedByHost: false,
500
+ attendedByHost: isAttendedByHostView(view),
462
501
  source: "tolerant"
463
502
  };
464
503
  }
@@ -542,6 +581,9 @@ function stageAtCursor(run, cursor) {
542
581
  return null;
543
582
  }
544
583
  function waitingFor(run, runDir) {
584
+ if (run.cancelled !== undefined && run.cancelled !== null) {
585
+ return { kind: "cancelled", message: cancelledMessage(run.cancelled), questions: [] };
586
+ }
545
587
  const cursor = run.cursor;
546
588
  if (cursor === null) {
547
589
  return { kind: "blocked", message: "run.yml records no cursor, so nothing can say where it is", questions: [] };
@@ -612,6 +654,12 @@ function waitingFor(run, runDir) {
612
654
  };
613
655
  }
614
656
  }
657
+ function cancelledMessage(cancelled) {
658
+ const by = cancelled.by.trim() === "" ? "someone" : cancelled.by.trim();
659
+ const when = cancelled.at.trim() === "" ? "" : ` at ${cancelled.at.trim()}`;
660
+ const note = cancelled.note.trim();
661
+ return `run cancelled by ${by}${when}${note === "" ? "" : ` — ${note}`}`;
662
+ }
615
663
  function openQuestionIds2(path) {
616
664
  if (!existsSync5(path))
617
665
  return [];
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  toolInput
3
- } from "./chunk-3g61yg59.js";
3
+ } from "./chunk-bvm6vjrt.js";
4
4
 
5
5
  // src/hooks/lib/wouldBe.ts
6
6
  import { existsSync, readFileSync } from "node:fs";