tldr-experts 0.28.0 → 0.29.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.
@@ -1,9 +1,3 @@
1
- import {
2
- GatePolicyError,
3
- STAGE_TUNING_DEFAULTS,
4
- parseWorkflowGates,
5
- readStageTuning
6
- } from "./chunk-g7wz339n.js";
7
1
  import {
8
2
  PROJECT_FRAMEWORK_DIR,
9
3
  STAGES_DIR,
@@ -12,12 +6,109 @@ import {
12
6
  isRecord,
13
7
  loadWorkspace,
14
8
  parseYaml
15
- } from "./chunk-f6t1xc3f.js";
9
+ } from "./chunk-qmp5p77c.js";
16
10
 
17
11
  // src/core/run/workflowPreset.ts
18
12
  import { existsSync, readFileSync } from "node:fs";
19
13
  import { join } from "node:path";
20
14
 
15
+ // src/core/run/stagePolicy.ts
16
+ function validateStagePolicy(value, stageIds, issues, spec) {
17
+ if (value === undefined || value === null)
18
+ return;
19
+ if (!isRecord(value)) {
20
+ issues.push({
21
+ path: spec.field,
22
+ message: `expected a mapping of stage id -> ${spec.policies.join(" | ")}`
23
+ });
24
+ return;
25
+ }
26
+ const known = new Set(stageIds);
27
+ for (const [key, raw] of Object.entries(value)) {
28
+ if (typeof raw !== "string" || !spec.policies.includes(raw)) {
29
+ issues.push({
30
+ path: `${spec.field}.${key}`,
31
+ message: `expected one of ${spec.policies.join(" | ")}, got ${JSON.stringify(raw)}`
32
+ });
33
+ }
34
+ if (known.size > 0 && !known.has(key)) {
35
+ issues.push({ path: `${spec.field}.${key}`, message: `names no stage in this run` });
36
+ }
37
+ }
38
+ }
39
+
40
+ // src/core/run/gatePolicy.ts
41
+ var GATE_POLICIES = ["human", "auto", "agent"];
42
+ var RESERVED_GATE_KEYS = ["collapse"];
43
+
44
+ class GatePolicyError extends Error {
45
+ }
46
+ function isGatePolicy(value) {
47
+ return typeof value === "string" && GATE_POLICIES.includes(value);
48
+ }
49
+ function parseWorkflowGates(value, stageIds, source) {
50
+ if (value === undefined || value === null)
51
+ return {};
52
+ if (!isRecord(value)) {
53
+ throw new GatePolicyError(`${source}: gates must be a mapping of stage id -> ${GATE_POLICIES.join(" | ")}`);
54
+ }
55
+ const known = new Set(stageIds);
56
+ const out = {};
57
+ for (const [key, raw] of Object.entries(value)) {
58
+ if (RESERVED_GATE_KEYS.includes(key))
59
+ continue;
60
+ if (!known.has(key)) {
61
+ throw new GatePolicyError(`${source}: gates names '${key}', which is not one of this workflow's stages (${stageIds.join(", ")})`);
62
+ }
63
+ if (!isGatePolicy(raw)) {
64
+ throw new GatePolicyError(`${source}: gates.${key} must be ${GATE_POLICIES.join(" | ")}, got ${JSON.stringify(raw)}`);
65
+ }
66
+ out[key] = raw;
67
+ }
68
+ return out;
69
+ }
70
+ function validateGatesPolicy(value, stageIds, issues) {
71
+ validateStagePolicy(value, stageIds, issues, { field: "gates_policy", policies: GATE_POLICIES });
72
+ }
73
+
74
+ // src/core/schemas/stageTuning.ts
75
+ var STAGE_TUNING_DEFAULTS = {
76
+ attempts: 2,
77
+ fixlistRounds: 1,
78
+ reviewerShare: 0.25,
79
+ gateSignerShare: 0.25,
80
+ storyCapMultiplier: 3,
81
+ storyCapFloorUsd: 4
82
+ };
83
+ var STAGE_TUNING_RANGES = {
84
+ attempts: { key: "attempts", min: 1, max: 5, integer: true },
85
+ fixlistRounds: { key: "fixlist_rounds", min: 0, max: 3, integer: true },
86
+ reviewerShare: { key: "reviewer_share", min: 0, max: 1, integer: false },
87
+ gateSignerShare: { key: "gate_signer_share", min: 0, max: 1, integer: false },
88
+ storyCapMultiplier: { key: "story_cap_multiplier", min: 1, max: 20, integer: false },
89
+ storyCapFloorUsd: { key: "story_cap_floor_usd", min: 0, max: 200, integer: false }
90
+ };
91
+ var FIELDS = Object.keys(STAGE_TUNING_RANGES);
92
+ function inTuningRange(range, value) {
93
+ if (!Number.isFinite(value))
94
+ return false;
95
+ if (range.integer && !Number.isInteger(value))
96
+ return false;
97
+ return value >= range.min && value <= range.max;
98
+ }
99
+ function readStageTuning(doc) {
100
+ if (!isRecord(doc))
101
+ return STAGE_TUNING_DEFAULTS;
102
+ const out = { ...STAGE_TUNING_DEFAULTS };
103
+ for (const field of FIELDS) {
104
+ const range = STAGE_TUNING_RANGES[field];
105
+ const value = doc[range.key];
106
+ if (typeof value === "number" && inTuningRange(range, value))
107
+ out[field] = value;
108
+ }
109
+ return out;
110
+ }
111
+
21
112
  // src/core/schemas/stage.ts
22
113
  var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
23
114
  function isEffortLevel(value) {
@@ -30,11 +121,11 @@ var PRECONDITION_TIMEOUT_S = 60;
30
121
  var PHASE_IDS = ["01-what", "02-how", "03-plan", "04-build", "05-watch"];
31
122
  var PHASE_SEGMENT_RE = /^0[1-5]-[a-z]+$/;
32
123
  var DEFAULT_TIMEOUT_S = 7200;
33
- function buildStageDefaults(root, scope) {
124
+ function buildStageDefaults(root, scope, stageId) {
34
125
  const fallback = { attempts: STAGE_TUNING_DEFAULTS.attempts, timeoutS: DEFAULT_TIMEOUT_S };
35
126
  try {
36
127
  const preset = loadWorkflowPreset(root, scope);
37
- const stage = preset.stages.find((s) => s.phase === PHASE_IDS[3]);
128
+ const stage = (stageId === undefined ? undefined : preset.stages.find((s) => s.id === stageId)) ?? preset.stages.find((s) => s.phase === PHASE_IDS[3]);
38
129
  if (stage === undefined)
39
130
  return fallback;
40
131
  return { attempts: stage.attempts, timeoutS: stage.timeout_s };
@@ -275,4 +366,4 @@ function isString(value) {
275
366
  return typeof value === "string";
276
367
  }
277
368
 
278
- export { PHASE_IDS, DEFAULT_TIMEOUT_S, buildStageDefaults };
369
+ export { validateStagePolicy, GATE_POLICIES, validateGatesPolicy, STAGE_TUNING_DEFAULTS, PHASE_IDS, DEFAULT_TIMEOUT_S, buildStageDefaults };
@@ -9,7 +9,7 @@ import {
9
9
  spentBasis,
10
10
  tallyOf,
11
11
  validateRunBudget
12
- } from "./chunk-xhd77h0p.js";
12
+ } from "./chunk-w3d9kxcp.js";
13
13
  import {
14
14
  EventLog,
15
15
  OUTCOME_NOT_RECORDED,
@@ -22,12 +22,12 @@ import {
22
22
  isTerminal,
23
23
  stageAt,
24
24
  validateRunFile
25
- } from "./chunk-abswp1vt.js";
25
+ } from "./chunk-t6gbx3jg.js";
26
26
  import {
27
27
  cursorStage,
28
28
  isAttendedByHostView,
29
29
  openRunViews
30
- } from "./chunk-hws2gnxj.js";
30
+ } from "./chunk-0d4csx4s.js";
31
31
  import {
32
32
  backupPathFor,
33
33
  isAlive,
@@ -36,20 +36,21 @@ import {
36
36
  workspaceRootOfRunDir,
37
37
  writeAtomic,
38
38
  yamlScalar
39
- } from "./chunk-zvxgdssb.js";
39
+ } from "./chunk-857jhjmb.js";
40
40
  import {
41
41
  isAdvisory,
42
42
  noteDeprecations,
43
43
  openBlocks,
44
44
  parseQuestions
45
- } from "./chunk-khy21x6e.js";
45
+ } from "./chunk-yanmz0vz.js";
46
46
  import {
47
47
  FRAMEWORK_ROOT,
48
+ MAX_ITEM_CHARS,
48
49
  PLAN_STATUSES,
49
50
  listRunDirs,
50
51
  parseYaml,
51
52
  parseYamlRepairing
52
- } from "./chunk-f6t1xc3f.js";
53
+ } from "./chunk-qmp5p77c.js";
53
54
 
54
55
  // src/core/statusline/runSnapshot.ts
55
56
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
@@ -634,7 +635,7 @@ var WAVE_CAP_REASON_KEY = "wave_cap_reason";
634
635
  var PLAN_SHAPE_RULES = [
635
636
  {
636
637
  issue: "#316",
637
- text: `**At most ${String(MAX_WAVES_PER_RUN)} waves** (patch for ${RUN_SIZE_ISSUES} — the framework's measured ` + "carry today, not a design preference). A later wave waits on every earlier merge, so each extra wave is " + "a stall: a 4-wave plan left 2 stories never attempted. The `plan` check refuses more unless the root of " + `\`${WAVES_FILE}\` records \`${WAVE_CAP_REASON_KEY}: "<why these stories cannot share a wave>"\`.`
638
+ text: `**At most ${String(MAX_WAVES_PER_RUN)} waves** (patch for ${RUN_SIZE_ISSUES} — the framework's measured ` + "carry today, not a design preference). A later wave waits on every earlier merge, so each extra wave is " + "a stall: a 4-wave plan left 2 stories never attempted. The `plan` check refuses more unless the root of " + `\`${WAVES_FILE}\` records \`${WAVE_CAP_REASON_KEY}: "<why these stories cannot share a wave>"\` — one sentence ` + `on one line, at most ${String(MAX_ITEM_CHARS)} characters (#328).`
638
639
  },
639
640
  {
640
641
  issue: "#316",
@@ -663,6 +664,9 @@ var LINE_BREAK_NOTE = `\`${LINE_BREAK_MARK}\` marks a line break inside a quoted
663
664
  var FINDING_STATUS_RE = new RegExp(`—\\s+(${PLAN_STATUSES.join("|")})\\s+—`);
664
665
  var FINDING_REASON_RE = new RegExp(`—\\s+(?:${PLAN_STATUSES.join("|")})\\s+—\\s+[^:]*:\\s*(\\S.*)$`);
665
666
 
667
+ // src/core/build/dependencyHold.ts
668
+ var NOT_A_HOLD_REASON = "its block is not a dependency hold — a reviewer, a DoD or a developer recorded it, " + "and reopening a dependency does not answer that; `tldrx story reopen <id>` does";
669
+
666
670
  // src/core/run/runOutcome.ts
667
671
  var BUILD_HANDOFF = join3(BUILD_PHASE, "handoff.md");
668
672
  var NO_BUILD_PHASE = "this run's workflow has no Build phase, so no story could be delivered";
@@ -3,7 +3,7 @@ import {
3
3
  withWorkspaceLock,
4
4
  workspaceRootOfFactsPath,
5
5
  writeAtomic
6
- } from "./chunk-zvxgdssb.js";
6
+ } from "./chunk-857jhjmb.js";
7
7
  import {
8
8
  FACT_CONFIDENCES,
9
9
  FACT_DECIDERS,
@@ -15,7 +15,7 @@ import {
15
15
  isLive,
16
16
  isRetired,
17
17
  noteDeprecations
18
- } from "./chunk-khy21x6e.js";
18
+ } from "./chunk-yanmz0vz.js";
19
19
  import {
20
20
  SRC_PATTERNS,
21
21
  asDocument,
@@ -28,7 +28,7 @@ import {
28
28
  requireString,
29
29
  requireVersion,
30
30
  result
31
- } from "./chunk-f6t1xc3f.js";
31
+ } from "./chunk-qmp5p77c.js";
32
32
 
33
33
  // src/core/facts/FactsStore.ts
34
34
  import { existsSync, readFileSync } from "node:fs";
@@ -3,7 +3,7 @@ import {
3
3
  EMPTY_SECTION_RULE,
4
4
  noneBullet,
5
5
  srcRule
6
- } from "./chunk-f6t1xc3f.js";
6
+ } from "./chunk-qmp5p77c.js";
7
7
 
8
8
  // src/hooks/lib/messages.ts
9
9
  var GRAMMAR = "[src: <repo:path:line> | https://… | Q<n> | F<n> | $ <cmd> → exit <n> | graph:<node> | absent:<path>]";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parseHookInput,
3
3
  readStdin
4
- } from "./chunk-khjm356r.js";
4
+ } from "./chunk-z9hv47ps.js";
5
5
 
6
6
  // src/hooks/lib/decide.ts
7
7
  function deny(reason) {
@@ -7548,7 +7548,7 @@ function requireStringList(value, path, issues, rules = {}) {
7548
7548
  if (item.length > MAX_ITEM_CHARS) {
7549
7549
  issues.push({
7550
7550
  path: at,
7551
- message: `${item.length} characters exceeds the ${MAX_ITEM_CHARS}-character cap on one list item ` + "— split it into several items"
7551
+ message: `${item.length} characters (cap ${MAX_ITEM_CHARS}) — split it into several items`
7552
7552
  });
7553
7553
  }
7554
7554
  if (rules.pattern !== undefined && !rules.pattern.test(item)) {
@@ -9202,4 +9202,4 @@ function noneBullet(lookedAt) {
9202
9202
  return `- none [src: absent:${lookedAt}]`;
9203
9203
  }
9204
9204
 
9205
- export { runtime, parseYaml, parseYamlRepairing, FRAMEWORK_ROOT, STAGES_DIR, WORKFLOWS_DIR, TEMPLATES_DIR, PROJECT_FRAMEWORK_DIR, PROJECT_WORK_DIR, SRC_SEPARATOR, srcToken, readableSource, SRC_PATTERNS, srcRule, parseSrcToken, withoutSrcToken, classifySrc, result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, requireRecord, asDocument, STORY_ID_RE, WAVE_ID_RE, PLAN_STATUSES, MAX_WAVES, MAX_STORIES_PER_WAVE, MAX_PLAN_STORIES, requireStringList, requirePattern, requireVersion1, parseFrontMatter, allowlistIssue, ITERATION_ONLY_SLOT, scopedSlotOf, isScopedTemplate, readStory, DodCommandRefused, runDodCommand, commandProbeIssues, locateWork, findWorkspaceRoot, loadWorkspace, toSrcContext, repoPath, factsPath, stageYamlPath, listRunDirs, parseHandoff, isHandoff, BULLET_RULE, EMPTY_SECTION_RULE, validateHandoff, validateCitations, noneBullet };
9205
+ export { runtime, parseYaml, parseYamlRepairing, FRAMEWORK_ROOT, STAGES_DIR, WORKFLOWS_DIR, TEMPLATES_DIR, PROJECT_FRAMEWORK_DIR, PROJECT_WORK_DIR, SRC_SEPARATOR, srcToken, readableSource, SRC_PATTERNS, srcRule, parseSrcToken, withoutSrcToken, classifySrc, result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, requireRecord, asDocument, STORY_ID_RE, WAVE_ID_RE, PLAN_STATUSES, MAX_ITEM_CHARS, MAX_WAVES, MAX_STORIES_PER_WAVE, MAX_PLAN_STORIES, requireStringList, requirePattern, requireVersion1, parseFrontMatter, allowlistIssue, ITERATION_ONLY_SLOT, scopedSlotOf, isScopedTemplate, readStory, DodCommandRefused, runDodCommand, commandProbeIssues, locateWork, findWorkspaceRoot, loadWorkspace, toSrcContext, repoPath, factsPath, stageYamlPath, listRunDirs, parseHandoff, isHandoff, BULLET_RULE, EMPTY_SECTION_RULE, validateHandoff, validateCitations, noneBullet };
@@ -2,7 +2,7 @@ import {
2
2
  GATE_POLICIES,
3
3
  validateGatesPolicy,
4
4
  validateStagePolicy
5
- } from "./chunk-g7wz339n.js";
5
+ } from "./chunk-g4d2v4jc.js";
6
6
  import {
7
7
  asDocument,
8
8
  isRecord,
@@ -15,7 +15,7 @@ import {
15
15
  requireString,
16
16
  requireVersion,
17
17
  result
18
- } from "./chunk-f6t1xc3f.js";
18
+ } from "./chunk-qmp5p77c.js";
19
19
 
20
20
  // src/core/events/EventLog.ts
21
21
  import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  STAGE_TUNING_DEFAULTS
3
- } from "./chunk-g7wz339n.js";
3
+ } from "./chunk-g4d2v4jc.js";
4
4
  import {
5
5
  MAX_PLAN_STORIES,
6
6
  MAX_STORIES_PER_WAVE,
@@ -25,7 +25,7 @@ import {
25
25
  requireVersion,
26
26
  requireVersion1,
27
27
  result
28
- } from "./chunk-f6t1xc3f.js";
28
+ } from "./chunk-qmp5p77c.js";
29
29
 
30
30
  // src/hooks/lib/actor.ts
31
31
  function currentActor() {
@@ -737,6 +737,9 @@ var KINDS = {
737
737
  }
738
738
  };
739
739
 
740
+ // src/core/build/git.ts
741
+ var MARKER_SCAN_MAX_BYTES = 4 * 1024 * 1024;
742
+
740
743
  // src/core/build/fixlist.ts
741
744
  var MAX_FIXLIST_ROUNDS = STAGE_TUNING_DEFAULTS.fixlistRounds;
742
745
  var FINDING_KINDS = ["correctness", "security", "docs", "style"];
@@ -1245,12 +1248,194 @@ function wouldExceedHostTokens(budget, phaseId, spentTokens) {
1245
1248
  };
1246
1249
  }
1247
1250
 
1248
- // src/core/budget/rebalance.ts
1249
- var SETTLED = new Set(["done", "skipped"]);
1251
+ // src/core/budget/grant.ts
1252
+ function grantFor(budget, phaseId) {
1253
+ if (phaseId !== undefined && phaseId !== null) {
1254
+ const phase = budget.phases.find((p) => p.id === phaseId);
1255
+ if (phase?.authorized_usd != null && budget.authorized_by !== null) {
1256
+ return { usd: phase.authorized_usd, factId: budget.authorized_by, level: "phase" };
1257
+ }
1258
+ }
1259
+ if (budget.authorized_usd === null || budget.authorized_by === null)
1260
+ return null;
1261
+ return { usd: budget.authorized_usd, factId: budget.authorized_by, level: "run" };
1262
+ }
1263
+ function wouldExceedGrant(budget, phaseId, resultingCeilingUsd) {
1264
+ const grant = grantFor(budget, phaseId);
1265
+ if (grant === null) {
1266
+ return { exceeds: false, blocked: false, grant: null, sentence: null };
1267
+ }
1268
+ const exceeds = resultingCeilingUsd > grant.usd + 0.000000001;
1269
+ const policy = budget.on_grant_exceed;
1270
+ if (!exceeds)
1271
+ return { exceeds: false, blocked: false, grant, sentence: null };
1272
+ return {
1273
+ exceeds: true,
1274
+ blocked: policy === "block",
1275
+ grant,
1276
+ sentence: `$${resultingCeilingUsd.toFixed(2)} is above the $${grant.usd.toFixed(2)} authorized ` + `by ${grant.factId} (${grant.level} grant)` + (policy === "block" ? " — refused; raise the grant first, or record a new decision." : ".")
1277
+ };
1278
+ }
1279
+
1280
+ // src/core/budget/raiseBudget.ts
1281
+ class BudgetRaiseError extends Error {
1282
+ }
1283
+ var MIN_RAISE_USD = 0.01;
1284
+ var MAX_RAISE_USD = 1000;
1285
+ function raiseBudget(budget, request) {
1286
+ const amount = round5(request.amountUsd);
1287
+ if (!Number.isFinite(amount) || amount < MIN_RAISE_USD) {
1288
+ throw new BudgetRaiseError(`the amount must be at least ${money(MIN_RAISE_USD)}, got ${money(request.amountUsd)}`);
1289
+ }
1290
+ if (amount > MAX_RAISE_USD) {
1291
+ throw new BudgetRaiseError(`${money(amount)} exceeds the ${money(MAX_RAISE_USD)} cap for a single raise`);
1292
+ }
1293
+ const target = budget.phases.find((p) => p.id === request.phaseId);
1294
+ if (target === undefined) {
1295
+ throw new BudgetRaiseError(`no phase \`${request.phaseId}\` in budget.yml — it has ${budget.phases.map((p) => p.id).join(", ")}`);
1296
+ }
1297
+ const takeFrom = request.takeFrom ?? null;
1298
+ if (takeFrom !== null && takeFrom === request.phaseId) {
1299
+ throw new BudgetRaiseError(`--take-from ${takeFrom} is the same phase — that moves nothing`);
1300
+ }
1301
+ let donor = null;
1302
+ if (takeFrom !== null) {
1303
+ donor = budget.phases.find((p) => p.id === takeFrom) ?? null;
1304
+ if (donor === null) {
1305
+ throw new BudgetRaiseError(`no phase \`${takeFrom}\` in budget.yml — it has ${budget.phases.map((p) => p.id).join(", ")}`);
1306
+ }
1307
+ const left = round5(donor.ceiling_usd - donor.spent_usd);
1308
+ if (amount > left) {
1309
+ throw new BudgetRaiseError(`${takeFrom} has only ${money(left)} unspent of its ${money(donor.ceiling_usd)} ceiling — ` + `taking ${money(amount)} would put its ceiling below what it has already spent`);
1310
+ }
1311
+ }
1312
+ const phases = budget.phases.map((phase) => {
1313
+ if (phase.id === request.phaseId)
1314
+ return { ...phase, ceiling_usd: round5(phase.ceiling_usd + amount) };
1315
+ if (donor !== null && phase.id === donor.id)
1316
+ return { ...phase, ceiling_usd: round5(phase.ceiling_usd - amount) };
1317
+ return phase;
1318
+ });
1319
+ const sum = round5(phases.reduce((total, p) => total + p.ceiling_usd, 0));
1320
+ const runCeiling = sum > budget.ceiling_usd ? sum : budget.ceiling_usd;
1321
+ const next = { ...budget, ceiling_usd: runCeiling, phases };
1322
+ if (round5(totalSpent(next)) > runCeiling) {
1323
+ throw new BudgetRaiseError("the run has already spent more than the resulting ceiling — raise the run ceiling first");
1324
+ }
1325
+ return {
1326
+ budget: next,
1327
+ phaseId: request.phaseId,
1328
+ amountUsd: amount,
1329
+ takeFrom,
1330
+ phaseCeilingBefore: target.ceiling_usd,
1331
+ phaseCeilingAfter: round5(target.ceiling_usd + amount),
1332
+ takeFromCeilingBefore: donor === null ? null : donor.ceiling_usd,
1333
+ takeFromCeilingAfter: donor === null ? null : round5(donor.ceiling_usd - amount),
1334
+ runCeilingBefore: budget.ceiling_usd,
1335
+ runCeilingAfter: runCeiling,
1336
+ runCeilingGrew: runCeiling > budget.ceiling_usd
1337
+ };
1338
+ }
1339
+ function raiseGrantVerdict(budget, outcome) {
1340
+ const phaseGrant = grantFor(budget, outcome.phaseId);
1341
+ return phaseGrant !== null && phaseGrant.level === "phase" ? wouldExceedGrant(budget, outcome.phaseId, outcome.phaseCeilingAfter) : wouldExceedGrant(budget, null, outcome.runCeilingAfter);
1342
+ }
1343
+ function money(amount) {
1344
+ return `$${amount.toFixed(2)}`;
1345
+ }
1346
+ function round5(n) {
1347
+ return Math.round(n * 100) / 100;
1348
+ }
1250
1349
 
1251
1350
  // src/core/budget/budgetView.ts
1252
1351
  function raiseCommand(runId, phaseId, amountUsd) {
1253
1352
  return `tldrx budget raise ${phaseId} ${amountUsd.toFixed(2)} --run ${runId}`;
1254
1353
  }
1255
1354
 
1256
- export { currentActor, nowRfc3339, spentBasis, tallyOf, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ON_GRANT_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, stackExpertNames, WAVES_FILE, BUILD_PHASE2 as BUILD_PHASE, shortBy, remainingWork, wouldExceed, wouldExceedHostTokens, raiseCommand };
1355
+ // src/core/budget/rebalance.ts
1356
+ var REBALANCE_SOURCE = "run auto --rebalance-finished";
1357
+ var SETTLED = new Set(["done", "skipped"]);
1358
+ function planRebalance(budget, run, targetPhaseId, shortUsd) {
1359
+ const donors = [];
1360
+ const excluded = [];
1361
+ for (const phase of budget.phases) {
1362
+ if (phase.id === targetPhaseId)
1363
+ continue;
1364
+ const reason = notADonor(budget, run, phase.id);
1365
+ if (reason !== null) {
1366
+ excluded.push({ phaseId: phase.id, reason });
1367
+ continue;
1368
+ }
1369
+ const unspent = round6(phase.ceiling_usd - phase.spent_usd);
1370
+ if (unspent < 0.01) {
1371
+ excluded.push({ phaseId: phase.id, reason: `finished with no unspent ceiling ($${phase.spent_usd.toFixed(2)} of $${phase.ceiling_usd.toFixed(2)})` });
1372
+ continue;
1373
+ }
1374
+ donors.push({ phaseId: phase.id, unspentUsd: unspent });
1375
+ }
1376
+ const short = round6(shortUsd);
1377
+ const finishedUnspentUsd = round6(donors.reduce((sum, d) => sum + d.unspentUsd, 0));
1378
+ const uncoveredUsd = round6(Math.max(0, short - finishedUnspentUsd));
1379
+ const moves = [];
1380
+ if (uncoveredUsd === 0 && short > 0) {
1381
+ let left = short;
1382
+ for (const donor of donors) {
1383
+ if (left <= 0)
1384
+ break;
1385
+ const take = round6(Math.min(donor.unspentUsd, left));
1386
+ moves.push({ takeFrom: donor.phaseId, amountUsd: take });
1387
+ left = round6(left - take);
1388
+ }
1389
+ }
1390
+ return { targetPhaseId, shortUsd: short, donors, excluded, finishedUnspentUsd, moves, uncoveredUsd };
1391
+ }
1392
+ function applyRebalance(budget, plan) {
1393
+ let current = budget;
1394
+ const outcomes = [];
1395
+ for (const move of plan.moves) {
1396
+ const outcome = raiseBudget(current, { phaseId: plan.targetPhaseId, amountUsd: move.amountUsd, takeFrom: move.takeFrom });
1397
+ outcomes.push(outcome);
1398
+ current = outcome.budget;
1399
+ }
1400
+ return { budget: current, outcomes };
1401
+ }
1402
+ function describeRebalance(plan) {
1403
+ if (plan.donors.length === 0) {
1404
+ return `no finished phase holds unspent ceiling` + (plan.excluded.length === 0 ? "" : ` (${plan.excluded.map((e) => `${e.phaseId}: ${e.reason}`).join("; ")})`);
1405
+ }
1406
+ const held = plan.donors.map((d) => `${d.phaseId} $${d.unspentUsd.toFixed(2)}`).join(", ");
1407
+ return `finished phase(s) hold $${plan.finishedUnspentUsd.toFixed(2)} unspent (${held})`;
1408
+ }
1409
+ function unfinishedReason(run, phaseId) {
1410
+ const phase = run.phases.find((p) => p.id === phaseId);
1411
+ if (phase === undefined || phase.stages.length === 0)
1412
+ return null;
1413
+ const open = phase.stages.filter((s) => !SETTLED.has(s.status));
1414
+ if (open.length > 0) {
1415
+ return `not finished — ${open.map((s) => `${s.id} is ${s.status}`).join(", ")}`;
1416
+ }
1417
+ const stale = phase.stages.filter((s) => s.stale === true);
1418
+ if (stale.length > 0)
1419
+ return `not finished — ${stale.map((s) => s.id).join(", ")} is stale and will run again`;
1420
+ return null;
1421
+ }
1422
+ function notADonor(budget, run, phaseId) {
1423
+ const phase = run.phases.find((p) => p.id === phaseId);
1424
+ if (phase === undefined || phase.stages.length === 0)
1425
+ return "no stage of it in run.yml, so nothing proves it finished";
1426
+ const unfinished = unfinishedReason(run, phaseId);
1427
+ if (unfinished !== null)
1428
+ return unfinished;
1429
+ if (economyFor(budget, phaseId) !== "metered-usd")
1430
+ return "priced in host-tokens, which are not dollars";
1431
+ const unmetered = tallyOf(phase.stages.flatMap((s) => s.tasks)).unmetered;
1432
+ if (unmetered > 0) {
1433
+ return `${String(unmetered)} unmetered turn(s), so its spend is a lower bound and its unspent figure is not a proof`;
1434
+ }
1435
+ return null;
1436
+ }
1437
+ function round6(n) {
1438
+ return Math.round(n * 100) / 100;
1439
+ }
1440
+
1441
+ export { currentActor, nowRfc3339, spentBasis, tallyOf, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ON_GRANT_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect, stackExpertNames, WAVES_FILE, BUILD_PHASE2 as BUILD_PHASE, shortBy, remainingWork, wouldExceed, wouldExceedHostTokens, raiseGrantVerdict, REBALANCE_SOURCE, planRebalance, applyRebalance, describeRebalance, raiseCommand };
@@ -3,7 +3,7 @@ import {
3
3
  parseSrcToken,
4
4
  srcToken,
5
5
  withoutSrcToken
6
- } from "./chunk-f6t1xc3f.js";
6
+ } from "./chunk-qmp5p77c.js";
7
7
 
8
8
  // src/core/text/questions.ts
9
9
  var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runtime
3
- } from "./chunk-f6t1xc3f.js";
3
+ } from "./chunk-qmp5p77c.js";
4
4
 
5
5
  // src/core/hooks/passthrough.ts
6
6
  async function readStdin() {
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  wouldBeContent
4
- } from "./chunk-zrwq6r7k.js";
4
+ } from "./chunk-7xascvee.js";
5
5
  import {
6
6
  claimSourcesDeny,
7
7
  claimSourcesEmptySectionDeny,
8
8
  claimSourcesMalformedDeny,
9
9
  claimSourcesUnresolvedDeny
10
- } from "./chunk-q3d4xdhb.js";
10
+ } from "./chunk-mg3aj0sa.js";
11
11
  import {
12
12
  allow,
13
13
  deny,
@@ -16,8 +16,8 @@ import {
16
16
  postContext,
17
17
  readPayload,
18
18
  runHook
19
- } from "./chunk-tna1bz8r.js";
20
- import"./chunk-khjm356r.js";
19
+ } from "./chunk-pne7afnf.js";
20
+ import"./chunk-z9hv47ps.js";
21
21
  import {
22
22
  isHandoff,
23
23
  loadWorkspace,
@@ -25,7 +25,7 @@ import {
25
25
  toSrcContext,
26
26
  validateCitations,
27
27
  validateHandoff
28
- } from "./chunk-f6t1xc3f.js";
28
+ } from "./chunk-qmp5p77c.js";
29
29
 
30
30
  // src/hooks/claim-sources.ts
31
31
  await runHook("claim-sources", async () => {
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  wouldBeContent
4
- } from "./chunk-zrwq6r7k.js";
4
+ } from "./chunk-7xascvee.js";
5
5
  import {
6
6
  dodGateDeny,
7
7
  dodGateInternalErrorDeny,
8
8
  dodGateMissingBlockDeny,
9
9
  dodGateRefusedCommandDeny
10
- } from "./chunk-q3d4xdhb.js";
10
+ } from "./chunk-mg3aj0sa.js";
11
11
  import {
12
12
  allow,
13
13
  deny,
@@ -15,16 +15,15 @@ import {
15
15
  filePathOf,
16
16
  isWriteOrEdit,
17
17
  readPayload
18
- } from "./chunk-tna1bz8r.js";
19
- import"./chunk-khjm356r.js";
18
+ } from "./chunk-pne7afnf.js";
19
+ import"./chunk-z9hv47ps.js";
20
+ import {
21
+ loadRunView
22
+ } from "./chunk-0d4csx4s.js";
20
23
  import {
21
24
  DEFAULT_TIMEOUT_S,
22
25
  buildStageDefaults
23
- } from "./chunk-veffvzw5.js";
24
- import {
25
- loadRunView
26
- } from "./chunk-hws2gnxj.js";
27
- import"./chunk-g7wz339n.js";
26
+ } from "./chunk-g4d2v4jc.js";
28
27
  import {
29
28
  DodCommandRefused,
30
29
  loadWorkspace,
@@ -32,7 +31,7 @@ import {
32
31
  readStory,
33
32
  repoPath,
34
33
  runDodCommand
35
- } from "./chunk-f6t1xc3f.js";
34
+ } from "./chunk-qmp5p77c.js";
36
35
 
37
36
  // src/hooks/dod-gate.ts
38
37
  import { basename, join } from "node:path";
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  wouldBeContent
4
- } from "./chunk-zrwq6r7k.js";
4
+ } from "./chunk-7xascvee.js";
5
5
  import {
6
6
  noReAskDeny
7
- } from "./chunk-q3d4xdhb.js";
7
+ } from "./chunk-mg3aj0sa.js";
8
8
  import {
9
9
  FactsStore
10
- } from "./chunk-48ctk154.js";
10
+ } from "./chunk-gr83pq7x.js";
11
11
  import {
12
12
  allow,
13
13
  deny,
@@ -15,17 +15,17 @@ import {
15
15
  isWriteOrEdit,
16
16
  readPayload,
17
17
  runHook
18
- } from "./chunk-tna1bz8r.js";
19
- import"./chunk-khjm356r.js";
20
- import"./chunk-zvxgdssb.js";
18
+ } from "./chunk-pne7afnf.js";
19
+ import"./chunk-z9hv47ps.js";
20
+ import"./chunk-857jhjmb.js";
21
21
  import {
22
22
  openBlocks,
23
23
  parseQuestions
24
- } from "./chunk-khy21x6e.js";
24
+ } from "./chunk-yanmz0vz.js";
25
25
  import {
26
26
  factsPath,
27
27
  locateWork
28
- } from "./chunk-f6t1xc3f.js";
28
+ } from "./chunk-qmp5p77c.js";
29
29
 
30
30
  // src/hooks/no-reask.ts
31
31
  import { existsSync, readFileSync } from "node:fs";