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.
- package/CHANGELOG.md +2396 -0
- package/README.md +133 -17
- package/dist/hooks/answer-capture.js +7 -8
- package/dist/hooks/budget-gate.js +83 -16
- package/dist/hooks/{chunk-zdxgragg.js → chunk-14zn51kh.js} +171 -16
- package/dist/hooks/{chunk-xpxe94qe.js → chunk-3w55tp71.js} +75 -27
- package/dist/hooks/{chunk-afamdvyn.js → chunk-5556vjt5.js} +1 -1
- package/dist/hooks/{chunk-ytvmc5ns.js → chunk-889tybxc.js} +26 -28
- package/dist/hooks/{chunk-9gb21660.js → chunk-a5dq2dcp.js} +54 -14
- package/dist/hooks/{chunk-ztczwtj0.js → chunk-a6rpj2cp.js} +835 -16
- package/dist/hooks/{chunk-3g61yg59.js → chunk-bvm6vjrt.js} +1 -1
- package/dist/hooks/{chunk-0z27twdk.js → chunk-hcrbr430.js} +10 -5
- package/dist/hooks/{chunk-3t91gvpp.js → chunk-nadqsr3w.js} +8 -2
- package/dist/hooks/{chunk-458wgg9j.js → chunk-q8d3sff9.js} +86 -11
- package/dist/hooks/{chunk-9kkm6q0t.js → chunk-qw73rdbr.js} +35 -3
- package/dist/hooks/{chunk-ybacnpxd.js → chunk-sae7sqty.js} +5 -0
- package/dist/hooks/{chunk-s5qsb4k6.js → chunk-v1c1hpb8.js} +1 -1
- package/dist/hooks/claim-sources.js +32 -21
- package/dist/hooks/dod-gate.js +7 -6
- package/dist/hooks/no-reask.js +9 -9
- package/dist/hooks/session-start.js +46 -21
- package/dist/hooks/statusline.js +8 -9
- package/dist/tldrx.js +19667 -10978
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/skills/tldrx/SKILL.md +1 -1
- package/stages/build/stage.yml +2 -1
- package/stages/plan/stage.md +11 -0
- package/stages/watch/stage.md +4 -0
- package/templates/expert.md +13 -1
- package/templates/watcher.md +8 -1
- package/workflows/bugfix.yml +8 -4
- package/workflows/docs.yml +8 -4
- package/workflows/feature.yml +8 -4
- package/workflows/hotfix.yml +8 -4
- package/workflows/integration.yml +8 -4
- package/workflows/migration.yml +8 -4
- package/workflows/performance.yml +8 -4
- package/workflows/prototype.yml +8 -4
- package/workflows/refactor.yml +8 -4
- package/workflows/security-patch.yml +8 -4
- package/workflows/spike.yml +8 -4
- package/workflows/upgrade.yml +8 -4
- package/dist/hooks/chunk-sznsenee.js +0 -503
- package/templates/epic.md +0 -38
- 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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 / (
|
|
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-
|
|
19
|
+
} from "./chunk-14zn51kh.js";
|
|
18
20
|
import {
|
|
19
21
|
EventLog
|
|
20
|
-
} from "./chunk-
|
|
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-
|
|
31
|
+
} from "./chunk-qw73rdbr.js";
|
|
28
32
|
import {
|
|
29
33
|
noteDeprecations,
|
|
30
34
|
openBlocks,
|
|
31
35
|
parseQuestions
|
|
32
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-nadqsr3w.js";
|
|
33
37
|
import {
|
|
34
38
|
listRunDirs,
|
|
35
|
-
parseYaml
|
|
36
|
-
|
|
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,
|
|
44
|
-
import { basename,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
204
|
-
const budgetValidation = validateRunBudget(
|
|
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
|
-
|
|
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 : {
|
|
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
|
|
321
|
-
const
|
|
322
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
356
|
+
function parseStateFile(path) {
|
|
357
|
+
const text = readFileSync(path, "utf8");
|
|
323
358
|
try {
|
|
324
|
-
|
|
325
|
-
renameSync(temp, path);
|
|
359
|
+
return parseYamlRepairing(text);
|
|
326
360
|
} catch (error) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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:
|
|
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 [];
|