tldr-experts 0.3.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 (75) hide show
  1. package/CHANGELOG.md +913 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -0
  4. package/dist/hooks/answer-capture.js +174 -0
  5. package/dist/hooks/budget-gate.js +173 -0
  6. package/dist/hooks/chunk-0bt6yb2q.js +88 -0
  7. package/dist/hooks/chunk-1zwcxd3f.js +66 -0
  8. package/dist/hooks/chunk-9gb21660.js +66 -0
  9. package/dist/hooks/chunk-a8p2rc94.js +20 -0
  10. package/dist/hooks/chunk-ae6bkfs5.js +0 -0
  11. package/dist/hooks/chunk-azctppjh.js +198 -0
  12. package/dist/hooks/chunk-g395gk7e.js +503 -0
  13. package/dist/hooks/chunk-j234zf0t.js +339 -0
  14. package/dist/hooks/chunk-kw4tffzf.js +139 -0
  15. package/dist/hooks/chunk-p274ckxv.js +7435 -0
  16. package/dist/hooks/chunk-sdjnnmzz.js +497 -0
  17. package/dist/hooks/chunk-t56k6146.js +14 -0
  18. package/dist/hooks/chunk-t8tdv11p.js +35 -0
  19. package/dist/hooks/chunk-x98qs959.js +302 -0
  20. package/dist/hooks/chunk-y0jdr3et.js +627 -0
  21. package/dist/hooks/claim-sources.js +72 -0
  22. package/dist/hooks/dod-gate.js +198 -0
  23. package/dist/hooks/no-reask.js +67 -0
  24. package/dist/hooks/session-start.js +1348 -0
  25. package/dist/hooks/statusline.js +116 -0
  26. package/dist/tldrx.js +34219 -0
  27. package/env.yml +79 -0
  28. package/package.json +60 -0
  29. package/plugin/.claude-plugin/plugin.json +9 -0
  30. package/plugin/README.md +113 -0
  31. package/plugin/agents/README.md +22 -0
  32. package/plugin/hooks/hooks.json +107 -0
  33. package/plugin/skills/tldrx/SKILL.md +170 -0
  34. package/stages/build/stage.md +70 -0
  35. package/stages/build/stage.yml +54 -0
  36. package/stages/how/stage.md +89 -0
  37. package/stages/how/stage.yml +53 -0
  38. package/stages/plan/stage.md +89 -0
  39. package/stages/plan/stage.yml +48 -0
  40. package/stages/watch/stage.md +85 -0
  41. package/stages/watch/stage.yml +61 -0
  42. package/stages/what/stage.md +89 -0
  43. package/stages/what/stage.yml +61 -0
  44. package/templates/budget.yml +17 -0
  45. package/templates/competencies.yml +14 -0
  46. package/templates/env.yml +19 -0
  47. package/templates/epic.md +38 -0
  48. package/templates/expert.md +51 -0
  49. package/templates/experts/architect.md +77 -0
  50. package/templates/experts/delivery.md +76 -0
  51. package/templates/experts/developer.md +78 -0
  52. package/templates/experts/operations.md +74 -0
  53. package/templates/experts/product.md +78 -0
  54. package/templates/facts.yml +18 -0
  55. package/templates/handoff.md +63 -0
  56. package/templates/process.yml +33 -0
  57. package/templates/questions.md +63 -0
  58. package/templates/run.yml +24 -0
  59. package/templates/story.md +55 -0
  60. package/templates/watcher.md +68 -0
  61. package/templates/waves.yml +17 -0
  62. package/templates/workspace.yml +30 -0
  63. package/workflows/bugfix.yml +26 -0
  64. package/workflows/docs.yml +23 -0
  65. package/workflows/feature.yml +22 -0
  66. package/workflows/hotfix.yml +24 -0
  67. package/workflows/integration.yml +24 -0
  68. package/workflows/migration.yml +24 -0
  69. package/workflows/performance.yml +23 -0
  70. package/workflows/prototype.yml +24 -0
  71. package/workflows/refactor.yml +24 -0
  72. package/workflows/retro.yml +20 -0
  73. package/workflows/security-patch.yml +23 -0
  74. package/workflows/spike.yml +23 -0
  75. package/workflows/upgrade.yml +23 -0
@@ -0,0 +1,497 @@
1
+ import {
2
+ asDocument,
3
+ isRecord,
4
+ requireArray,
5
+ requireEnum,
6
+ requireKeys,
7
+ requireNumber,
8
+ requireString,
9
+ requireVersion,
10
+ result
11
+ } from "./chunk-0bt6yb2q.js";
12
+ import {
13
+ listRunDirs,
14
+ parseYaml
15
+ } from "./chunk-p274ckxv.js";
16
+
17
+ // src/hooks/lib/runFile.ts
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ var TERMINAL_STATUSES = ["done", "failed", "skipped", "cancelled"];
21
+ function loadRunView(runDir) {
22
+ const path = join(runDir, "run.yml");
23
+ if (!existsSync(path))
24
+ return null;
25
+ let doc;
26
+ try {
27
+ const parsed = parseYaml(readFileSync(path, "utf8"));
28
+ if (typeof parsed !== "object" || parsed === null)
29
+ return null;
30
+ doc = parsed;
31
+ } catch {
32
+ return null;
33
+ }
34
+ const rawCursor = doc.cursor;
35
+ const cursor = rawCursor === undefined || rawCursor === null ? null : {
36
+ phase: str(rawCursor.phase),
37
+ stage: str(rawCursor.stage),
38
+ task: typeof rawCursor.task === "string" ? rawCursor.task : null
39
+ };
40
+ const phases = [];
41
+ if (Array.isArray(doc.phases)) {
42
+ for (const entry of doc.phases) {
43
+ const stages = [];
44
+ if (Array.isArray(entry?.stages)) {
45
+ for (const s of entry.stages) {
46
+ stages.push({
47
+ id: str(s?.id),
48
+ status: str(s?.status),
49
+ expert: typeof s?.expert === "string" ? s.expert : null,
50
+ budget_usd: typeof s?.budget_usd === "number" ? s.budget_usd : null
51
+ });
52
+ }
53
+ }
54
+ phases.push({ id: str(entry?.id), status: str(entry?.status), stages });
55
+ }
56
+ }
57
+ return {
58
+ dir: runDir,
59
+ run: str(doc.run) || str(doc.run_id),
60
+ title: str(doc.title),
61
+ scope: str(doc.scope),
62
+ status: str(doc.status),
63
+ updated_at: str(doc.updated_at),
64
+ cursor,
65
+ phases
66
+ };
67
+ }
68
+ function isTerminal(status) {
69
+ return TERMINAL_STATUSES.includes(status);
70
+ }
71
+ function cursorStage(view) {
72
+ if (view.cursor === null)
73
+ return null;
74
+ for (const phase of view.phases) {
75
+ if (phase.id !== view.cursor.phase)
76
+ continue;
77
+ return phase.stages.find((s) => s.id === view.cursor?.stage) ?? null;
78
+ }
79
+ return null;
80
+ }
81
+ function openRunViews(root) {
82
+ const open = [];
83
+ for (const dir of listRunDirs(root)) {
84
+ const view = loadRunView(dir);
85
+ if (view === null || isTerminal(view.status))
86
+ continue;
87
+ open.push(view);
88
+ }
89
+ return open.sort((a, b) => a.updated_at > b.updated_at ? -1 : a.updated_at < b.updated_at ? 1 : 0);
90
+ }
91
+ function newestActiveRun(root) {
92
+ return openRunViews(root)[0] ?? null;
93
+ }
94
+ function str(value) {
95
+ return typeof value === "string" ? value : "";
96
+ }
97
+
98
+ // src/hooks/lib/actor.ts
99
+ function currentActor() {
100
+ const user = process.env.USER ?? process.env.USERNAME ?? "";
101
+ return user.trim() === "" ? "unknown" : user.trim();
102
+ }
103
+ function nowRfc3339() {
104
+ return `${new Date().toISOString().slice(0, 19)}Z`;
105
+ }
106
+
107
+ // src/core/budget/wouldExceed.ts
108
+ function totalSpent(budget) {
109
+ return budget.phases.reduce((sum, p) => sum + p.spent_usd, 0);
110
+ }
111
+ function wouldExceed(budget, phaseId, estimate) {
112
+ const phase = phaseId === null ? undefined : budget.phases.find((p) => p.id === phaseId);
113
+ const scope = phase === undefined ? "run" : "phase";
114
+ const ceiling = phase === undefined ? budget.ceiling_usd : phase.ceiling_usd;
115
+ const spent = phase === undefined ? totalSpent(budget) : phase.spent_usd;
116
+ const left = round(ceiling - spent);
117
+ const exceeds = round(spent + estimate) > ceiling;
118
+ const pct = ceiling === 0 ? 100 : (spent + estimate) / ceiling * 100;
119
+ return {
120
+ exceeds,
121
+ blocked: exceeds && budget.on_exceed === "block",
122
+ scope,
123
+ phaseId: phase === undefined ? null : phase.id,
124
+ remaining: left,
125
+ ceiling,
126
+ estimate,
127
+ warns: !exceeds && pct >= budget.warn_at_pct
128
+ };
129
+ }
130
+ function round(n) {
131
+ return Math.round(n * 1e6) / 1e6;
132
+ }
133
+
134
+ // src/core/run/gatePolicy.ts
135
+ var GATE_POLICIES = ["human", "auto"];
136
+ function isGatePolicy(value) {
137
+ return typeof value === "string" && GATE_POLICIES.includes(value);
138
+ }
139
+ function validateGatesPolicy(value, stageIds, issues) {
140
+ if (value === undefined || value === null)
141
+ return;
142
+ if (!isRecord(value)) {
143
+ issues.push({ path: "gates_policy", message: "expected a mapping of stage id -> human | auto" });
144
+ return;
145
+ }
146
+ const known = new Set(stageIds);
147
+ for (const [key, raw] of Object.entries(value)) {
148
+ if (!isGatePolicy(raw)) {
149
+ issues.push({
150
+ path: `gates_policy.${key}`,
151
+ message: `expected one of ${GATE_POLICIES.join(" | ")}, got ${JSON.stringify(raw)}`
152
+ });
153
+ }
154
+ if (known.size > 0 && !known.has(key)) {
155
+ issues.push({ path: `gates_policy.${key}`, message: `names no stage in this run` });
156
+ }
157
+ }
158
+ }
159
+
160
+ // src/core/run/RunFile.ts
161
+ var STAGE_STATUSES = [
162
+ "pending",
163
+ "ready",
164
+ "running",
165
+ "awaiting_answer",
166
+ "awaiting_gate",
167
+ "blocked",
168
+ "done",
169
+ "failed",
170
+ "skipped",
171
+ "cancelled"
172
+ ];
173
+ var TERMINAL_STATUSES2 = ["done", "failed", "skipped", "cancelled"];
174
+ var GATE_TYPES = ["approve", "checks", "auto"];
175
+ var GATE_STATUSES = ["pending", "approved", "rejected", "n-a"];
176
+ var RUN_ID_RE = /^\d{6}-[a-z0-9-]{1,40}$/;
177
+ var PHASE_ID_RE = /^0[1-5]-[a-z]+$/;
178
+ var MAX_PHASES = 5;
179
+ var MAX_STAGES = 40;
180
+ var MAX_TASKS = 200;
181
+ function isTerminal2(status) {
182
+ return TERMINAL_STATUSES2.includes(status);
183
+ }
184
+ function isFinished(status) {
185
+ return status === "done" || status === "cancelled";
186
+ }
187
+ function flatten(run) {
188
+ const out = [];
189
+ for (const phase of run.phases)
190
+ for (const stage of phase.stages)
191
+ out.push({ phase, stage });
192
+ return out;
193
+ }
194
+ function stageAt(run, cursor) {
195
+ return flatten(run).find((e) => e.phase.id === cursor.phase && e.stage.id === cursor.stage) ?? null;
196
+ }
197
+ function deriveRunStatus(run) {
198
+ const all = flatten(run);
199
+ if (run.cancelled !== undefined)
200
+ return "cancelled";
201
+ if (all.some((e) => e.stage.status === "failed"))
202
+ return "failed";
203
+ if (all.length > 0 && all.every((e) => isTerminal2(e.stage.status)))
204
+ return "done";
205
+ return stageAt(run, run.cursor)?.stage.status ?? "pending";
206
+ }
207
+ function derivePhaseStatus(phase) {
208
+ if (phase.stages.length === 0)
209
+ return "skipped";
210
+ if (phase.stages.some((s) => s.status === "failed"))
211
+ return "failed";
212
+ if (phase.stages.every((s) => isTerminal2(s.status)))
213
+ return "done";
214
+ const live = phase.stages.find((s) => !isTerminal2(s.status));
215
+ return live?.status ?? "pending";
216
+ }
217
+ function validateRunFile(input) {
218
+ const issues = [];
219
+ const deprecations = [];
220
+ const doc = asDocument(input, issues);
221
+ if (!doc)
222
+ return result(issues);
223
+ requireVersion(doc, issues, deprecations);
224
+ requireKeys(doc, ["run", "title", "scope", "workflow", "repos", "created_at", "updated_at", "status", "cursor", "budget", "phases"], "", issues);
225
+ if (typeof doc.run !== "string" || !RUN_ID_RE.test(doc.run)) {
226
+ issues.push({ path: "run", message: "run id must match ^\\d{6}-[a-z0-9-]{1,40}$" });
227
+ }
228
+ requireString(doc.title, "title", issues);
229
+ requireString(doc.scope, "scope", issues);
230
+ requireString(doc.workflow, "workflow", issues);
231
+ requireString(doc.created_at, "created_at", issues);
232
+ requireString(doc.updated_at, "updated_at", issues);
233
+ requireEnum(doc.status, STAGE_STATUSES, "status", issues);
234
+ requireArray(doc.repos, "repos", issues);
235
+ let cursor = null;
236
+ if (isRecord(doc.cursor)) {
237
+ requireKeys(doc.cursor, ["phase", "stage", "task"], "cursor", issues);
238
+ cursor = {
239
+ phase: String(doc.cursor.phase ?? ""),
240
+ stage: String(doc.cursor.stage ?? ""),
241
+ task: typeof doc.cursor.task === "string" ? doc.cursor.task : null
242
+ };
243
+ } else if (doc.cursor !== undefined) {
244
+ issues.push({ path: "cursor", message: "expected a mapping" });
245
+ }
246
+ if (isRecord(doc.budget)) {
247
+ requireKeys(doc.budget, ["ceiling_usd", "spent_usd", "per_agent_max_usd"], "budget", issues);
248
+ requireNumber(doc.budget.ceiling_usd, "budget.ceiling_usd", issues);
249
+ requireNumber(doc.budget.spent_usd, "budget.spent_usd", issues);
250
+ requireNumber(doc.budget.per_agent_max_usd, "budget.per_agent_max_usd", issues);
251
+ } else if (doc.budget !== undefined) {
252
+ issues.push({ path: "budget", message: "expected a mapping" });
253
+ }
254
+ if (doc.build !== undefined) {
255
+ if (isRecord(doc.build)) {
256
+ requireKeys(doc.build, ["epic_branch"], "build", issues);
257
+ if (requireArray(doc.build.epic_branch, "build.epic_branch", issues)) {
258
+ doc.build.epic_branch.forEach((branch, i) => {
259
+ requireString(branch, `build.epic_branch[${i}]`, issues);
260
+ });
261
+ }
262
+ } else {
263
+ issues.push({ path: "build", message: "expected a mapping" });
264
+ }
265
+ }
266
+ if (doc.cancelled !== undefined) {
267
+ if (isRecord(doc.cancelled)) {
268
+ requireKeys(doc.cancelled, ["by", "at", "note"], "cancelled", issues);
269
+ requireString(doc.cancelled.by, "cancelled.by", issues);
270
+ requireString(doc.cancelled.at, "cancelled.at", issues);
271
+ requireString(doc.cancelled.note, "cancelled.note", issues);
272
+ } else {
273
+ issues.push({ path: "cancelled", message: "expected a mapping" });
274
+ }
275
+ }
276
+ if (doc.triage !== undefined) {
277
+ if (isRecord(doc.triage)) {
278
+ requireKeys(doc.triage, ["split", "depends_on"], "triage", issues);
279
+ requireString(doc.triage.split, "triage.split", issues);
280
+ if (requireArray(doc.triage.depends_on, "triage.depends_on", issues)) {
281
+ doc.triage.depends_on.forEach((slug, i) => {
282
+ requireString(slug, `triage.depends_on[${i}]`, issues);
283
+ });
284
+ }
285
+ } else {
286
+ issues.push({ path: "triage", message: "expected a mapping" });
287
+ }
288
+ }
289
+ if (!requireArray(doc.phases, "phases", issues))
290
+ return result(issues, deprecations);
291
+ const phases = doc.phases;
292
+ if (phases.length > MAX_PHASES) {
293
+ issues.push({ path: "phases", message: `${phases.length} phases exceeds the ${MAX_PHASES} cap` });
294
+ }
295
+ const declaredStageIds = [];
296
+ for (const phase of phases) {
297
+ if (!isRecord(phase) || !Array.isArray(phase.stages))
298
+ continue;
299
+ for (const stage of phase.stages) {
300
+ if (isRecord(stage) && typeof stage.id === "string")
301
+ declaredStageIds.push(stage.id);
302
+ }
303
+ }
304
+ validateGatesPolicy(doc.gates_policy, declaredStageIds, issues);
305
+ let stageCount = 0;
306
+ let taskCount = 0;
307
+ let running = 0;
308
+ let cursorResolves = false;
309
+ let spentFromTasks = 0;
310
+ const phaseIds = new Set;
311
+ phases.forEach((phase, i) => {
312
+ const base = `phases[${i}]`;
313
+ if (!isRecord(phase)) {
314
+ issues.push({ path: base, message: "expected a mapping" });
315
+ return;
316
+ }
317
+ requireKeys(phase, ["id", "status", "stages"], base, issues);
318
+ const phaseId = typeof phase.id === "string" ? phase.id : "";
319
+ if (!PHASE_ID_RE.test(phaseId)) {
320
+ issues.push({ path: `${base}.id`, message: "phase id must match ^0[1-5]-[a-z]+$" });
321
+ }
322
+ if (phaseIds.has(phaseId))
323
+ issues.push({ path: `${base}.id`, message: `duplicate phase id ${phaseId}` });
324
+ phaseIds.add(phaseId);
325
+ requireEnum(phase.status, STAGE_STATUSES, `${base}.status`, issues);
326
+ if (!requireArray(phase.stages, `${base}.stages`, issues))
327
+ return;
328
+ const stageIds = new Set;
329
+ phase.stages.forEach((stage, j) => {
330
+ const path = `${base}.stages[${j}]`;
331
+ stageCount++;
332
+ if (!isRecord(stage)) {
333
+ issues.push({ path, message: "expected a mapping" });
334
+ return;
335
+ }
336
+ requireKeys(stage, ["id", "status", "expert", "model", "budget_usd", "cost_usd", "started_at", "ended_at", "inputs", "outputs", "gate", "tasks"], path, issues);
337
+ const stageId = typeof stage.id === "string" ? stage.id : "";
338
+ if (stageIds.has(stageId))
339
+ issues.push({ path: `${path}.id`, message: `duplicate stage id ${stageId}` });
340
+ stageIds.add(stageId);
341
+ requireEnum(stage.status, STAGE_STATUSES, `${path}.status`, issues);
342
+ if (stage.status === "running")
343
+ running++;
344
+ requireNumber(stage.budget_usd, `${path}.budget_usd`, issues);
345
+ requireNumber(stage.cost_usd, `${path}.cost_usd`, issues);
346
+ requireArray(stage.inputs, `${path}.inputs`, issues);
347
+ requireArray(stage.outputs, `${path}.outputs`, issues);
348
+ if (stage.stale !== undefined && typeof stage.stale !== "boolean") {
349
+ issues.push({ path: `${path}.stale`, message: "expected true or false" });
350
+ }
351
+ checkOrder(stage.started_at, stage.ended_at, path, issues);
352
+ if (cursor !== null && cursor.phase === phaseId && cursor.stage === stageId)
353
+ cursorResolves = true;
354
+ if (isRecord(stage.gate)) {
355
+ const gate = stage.gate;
356
+ requireKeys(gate, ["type", "status", "by", "at", "note"], `${path}.gate`, issues);
357
+ requireEnum(gate.type, GATE_TYPES, `${path}.gate.type`, issues);
358
+ requireEnum(gate.status, GATE_STATUSES, `${path}.gate.status`, issues);
359
+ if (gate.status === "approved" && (typeof gate.by !== "string" || typeof gate.at !== "string")) {
360
+ issues.push({ path: `${path}.gate`, message: "an approved gate needs both `by` and `at`" });
361
+ }
362
+ } else if (stage.gate !== undefined) {
363
+ issues.push({ path: `${path}.gate`, message: "expected a mapping" });
364
+ }
365
+ if (requireArray(stage.tasks, `${path}.tasks`, issues)) {
366
+ const taskIds = new Set;
367
+ stage.tasks.forEach((task, k) => {
368
+ const tp = `${path}.tasks[${k}]`;
369
+ taskCount++;
370
+ if (!isRecord(task)) {
371
+ issues.push({ path: tp, message: "expected a mapping" });
372
+ return;
373
+ }
374
+ requireKeys(task, ["id", "status", "cost_usd", "error", "session_id"], tp, issues);
375
+ const taskId = typeof task.id === "string" ? task.id : "";
376
+ if (!/^t\d+$/.test(taskId))
377
+ issues.push({ path: `${tp}.id`, message: "task id must match ^t\\d+$" });
378
+ if (taskIds.has(taskId))
379
+ issues.push({ path: `${tp}.id`, message: `duplicate task id ${taskId}` });
380
+ taskIds.add(taskId);
381
+ requireEnum(task.status, STAGE_STATUSES, `${tp}.status`, issues);
382
+ if (task.cost_usd !== null)
383
+ requireNumber(task.cost_usd, `${tp}.cost_usd`, issues);
384
+ if (task.metered !== undefined && typeof task.metered !== "boolean") {
385
+ issues.push({ path: `${tp}.metered`, message: "expected true or false" });
386
+ }
387
+ if (task.cost_usd === null && task.metered !== false) {
388
+ issues.push({ path: `${tp}.metered`, message: "a null cost_usd must be marked `metered: false`" });
389
+ }
390
+ if (typeof task.cost_usd === "number")
391
+ spentFromTasks += task.cost_usd;
392
+ checkOrder(task.started_at, task.ended_at, tp, issues);
393
+ });
394
+ }
395
+ });
396
+ });
397
+ if (stageCount > MAX_STAGES)
398
+ issues.push({ path: "phases", message: `${stageCount} stages exceeds the ${MAX_STAGES} cap` });
399
+ if (taskCount > MAX_TASKS)
400
+ issues.push({ path: "phases", message: `${taskCount} tasks exceeds the ${MAX_TASKS} cap` });
401
+ if (running > 1)
402
+ issues.push({ path: "phases", message: `${running} stages are running; tldrx is single-writer` });
403
+ if (cursor !== null && !cursorResolves) {
404
+ issues.push({ path: "cursor", message: `cursor ${cursor.phase}/${cursor.stage} does not resolve to a stage` });
405
+ }
406
+ if (isRecord(doc.budget) && typeof doc.budget.spent_usd === "number") {
407
+ if (Math.abs(doc.budget.spent_usd - spentFromTasks) > 0.01) {
408
+ issues.push({
409
+ path: "budget.spent_usd",
410
+ message: `${doc.budget.spent_usd} does not match the task total ${round2(spentFromTasks)} (tolerance 0.01)`
411
+ });
412
+ }
413
+ }
414
+ return result(issues, deprecations);
415
+ }
416
+ function checkOrder(started, ended, path, issues) {
417
+ if (typeof started === "string" && typeof ended === "string" && ended < started) {
418
+ issues.push({ path: `${path}.ended_at`, message: "ended_at is before started_at" });
419
+ }
420
+ }
421
+ function round2(n) {
422
+ return Math.round(n * 100) / 100;
423
+ }
424
+ function asRunFile(input) {
425
+ return input;
426
+ }
427
+
428
+ // src/core/budget/budgetView.ts
429
+ function raiseCommand(runId, phaseId, amountUsd) {
430
+ return `tldrx budget raise ${phaseId} ${amountUsd.toFixed(2)} --run ${runId}`;
431
+ }
432
+ function shortBy(estimate, remaining) {
433
+ return Math.max(0.01, Math.ceil((estimate - remaining) * 100) / 100);
434
+ }
435
+
436
+ // src/core/budget/RunBudget.ts
437
+ var ON_EXCEED = ["block", "warn"];
438
+ var DEFAULT_WARN_AT_PCT = 80;
439
+ var MAX_PHASES2 = 5;
440
+ function validateRunBudget(input) {
441
+ const issues = [];
442
+ const deprecations = [];
443
+ const doc = asDocument(input, issues);
444
+ if (!doc)
445
+ return result(issues);
446
+ requireVersion(doc, issues, deprecations);
447
+ requireKeys(doc, ["run", "ceiling_usd", "per_agent_max_usd", "on_exceed", "phases"], "", issues);
448
+ requireString(doc.run, "run", issues);
449
+ requireNumber(doc.ceiling_usd, "ceiling_usd", issues);
450
+ requireNumber(doc.per_agent_max_usd, "per_agent_max_usd", issues);
451
+ requireEnum(doc.on_exceed, ON_EXCEED, "on_exceed", issues);
452
+ if (doc.warn_at_pct !== undefined) {
453
+ requireNumber(doc.warn_at_pct, "warn_at_pct", issues);
454
+ const pct = doc.warn_at_pct;
455
+ if (typeof pct === "number" && (pct < 1 || pct > 99)) {
456
+ issues.push({ path: "warn_at_pct", message: "expected 1–99" });
457
+ }
458
+ }
459
+ if (!requireArray(doc.phases, "phases", issues))
460
+ return result(issues, deprecations);
461
+ const phases = doc.phases;
462
+ if (phases.length > MAX_PHASES2) {
463
+ issues.push({ path: "phases", message: `${phases.length} phases exceeds the ${MAX_PHASES2} cap` });
464
+ }
465
+ let sum = 0;
466
+ phases.forEach((phase, i) => {
467
+ const path = `phases[${i}]`;
468
+ if (!isRecord(phase)) {
469
+ issues.push({ path, message: "expected a mapping" });
470
+ return;
471
+ }
472
+ requireKeys(phase, ["id", "ceiling_usd", "spent_usd"], path, issues);
473
+ requireString(phase.id, `${path}.id`, issues);
474
+ requireNumber(phase.ceiling_usd, `${path}.ceiling_usd`, issues);
475
+ requireNumber(phase.spent_usd, `${path}.spent_usd`, issues);
476
+ if (typeof phase.ceiling_usd === "number")
477
+ sum += phase.ceiling_usd;
478
+ });
479
+ if (typeof doc.ceiling_usd === "number" && sum > doc.ceiling_usd + 0.000000001) {
480
+ issues.push({ path: "phases", message: `phase ceilings sum to ${sum} > ceiling_usd ${doc.ceiling_usd}` });
481
+ }
482
+ return result(issues, deprecations);
483
+ }
484
+ function asRunBudget(input) {
485
+ const doc = input;
486
+ return {
487
+ version: doc.version ?? 1,
488
+ run: doc.run ?? "",
489
+ ceiling_usd: doc.ceiling_usd ?? 0,
490
+ per_agent_max_usd: doc.per_agent_max_usd ?? 0,
491
+ warn_at_pct: doc.warn_at_pct ?? DEFAULT_WARN_AT_PCT,
492
+ on_exceed: doc.on_exceed ?? "block",
493
+ phases: doc.phases ?? []
494
+ };
495
+ }
496
+
497
+ export { loadRunView, cursorStage, openRunViews, newestActiveRun, currentActor, nowRfc3339, validateRunBudget, asRunBudget, wouldExceed, isTerminal2 as isTerminal, isFinished, flatten, stageAt, deriveRunStatus, derivePhaseStatus, validateRunFile, asRunFile, raiseCommand, shortBy };
@@ -0,0 +1,14 @@
1
+ // src/core/schemas/deprecationNotice.ts
2
+ var said = new Set;
3
+ function noteDeprecations(file, outcome) {
4
+ for (const note of outcome.deprecations ?? []) {
5
+ const line = `${file}: ${note}`;
6
+ if (said.has(line))
7
+ continue;
8
+ said.add(line);
9
+ process.stderr.write(`${line}
10
+ `);
11
+ }
12
+ }
13
+
14
+ export { noteDeprecations };
@@ -0,0 +1,35 @@
1
+ import {
2
+ toolInput
3
+ } from "./chunk-1zwcxd3f.js";
4
+
5
+ // src/hooks/lib/wouldBe.ts
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ function wouldBeContent(payload, filePath) {
8
+ const input = toolInput(payload);
9
+ if (payload.tool_name === "Write") {
10
+ return typeof input.content === "string" ? { kind: "content", text: input.content } : { kind: "not-applicable", why: "Write payload carried no content" };
11
+ }
12
+ if (payload.tool_name === "Edit") {
13
+ const oldString = input.old_string;
14
+ const newString = input.new_string;
15
+ if (typeof oldString !== "string" || typeof newString !== "string") {
16
+ return { kind: "not-applicable", why: "Edit payload carried no old_string/new_string" };
17
+ }
18
+ if (!existsSync(filePath)) {
19
+ return { kind: "not-applicable", why: "Edit target does not exist yet" };
20
+ }
21
+ if (oldString === "") {
22
+ return { kind: "not-applicable", why: "Edit payload carried an empty old_string" };
23
+ }
24
+ const current = readFileSync(filePath, "utf8");
25
+ const at = current.indexOf(oldString);
26
+ if (at === -1) {
27
+ return { kind: "not-applicable", why: "old_string is not in the file; the Edit will fail on its own" };
28
+ }
29
+ const text = input.replace_all === true ? current.split(oldString).join(newString) : current.slice(0, at) + newString + current.slice(at + oldString.length);
30
+ return { kind: "content", text };
31
+ }
32
+ return { kind: "not-applicable", why: `tool ${String(payload.tool_name)} does not write files` };
33
+ }
34
+
35
+ export { wouldBeContent };