tldr-experts 0.3.0 → 0.3.1
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 +994 -0
- package/README.md +26 -7
- package/dist/hooks/answer-capture.js +20 -11
- package/dist/hooks/budget-gate.js +28 -8
- package/dist/hooks/chunk-0z27twdk.js +159 -0
- package/dist/hooks/{chunk-0bt6yb2q.js → chunk-39zh2e44.js} +10 -1
- package/dist/hooks/{chunk-1zwcxd3f.js → chunk-3g61yg59.js} +1 -1
- package/dist/hooks/chunk-3t91gvpp.js +193 -0
- package/dist/hooks/{chunk-kw4tffzf.js → chunk-458wgg9j.js} +30 -24
- package/dist/hooks/{chunk-j234zf0t.js → chunk-9kkm6q0t.js} +4 -165
- package/dist/hooks/{chunk-t8tdv11p.js → chunk-afamdvyn.js} +1 -1
- package/dist/hooks/{chunk-a8p2rc94.js → chunk-s5qsb4k6.js} +1 -1
- package/dist/hooks/{chunk-g395gk7e.js → chunk-sznsenee.js} +1 -1
- package/dist/hooks/{chunk-y0jdr3et.js → chunk-xpxe94qe.js} +23 -10
- package/dist/hooks/{chunk-azctppjh.js → chunk-ybacnpxd.js} +4 -1
- package/dist/hooks/{chunk-x98qs959.js → chunk-ytvmc5ns.js} +19 -22
- package/dist/hooks/chunk-zdxgragg.js +1655 -0
- package/dist/hooks/{chunk-p274ckxv.js → chunk-ztczwtj0.js} +6 -1
- package/dist/hooks/claim-sources.js +9 -9
- package/dist/hooks/dod-gate.js +12 -37
- package/dist/hooks/no-reask.js +9 -9
- package/dist/hooks/session-start.js +176 -422
- package/dist/hooks/statusline.js +14 -11
- package/dist/tldrx.js +12029 -6330
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/tldrx/SKILL.md +16 -3
- package/stages/build/stage.md +5 -0
- package/templates/budget.yml +5 -0
- package/dist/hooks/chunk-ae6bkfs5.js +0 -0
- package/dist/hooks/chunk-sdjnnmzz.js +0 -497
- package/dist/hooks/chunk-t56k6146.js +0 -14
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$doc": "Shape verified from https://code.claude.com/docs/en/plugins.md (Quickstart > Create the plugin manifest). Fields used here: name, description, version, author.name. Only plugin.json goes inside .claude-plugin/; skills/, agents/ and hooks/ live at the plugin root.",
|
|
3
3
|
"name": "tldrx",
|
|
4
4
|
"description": "tldr-experts: a file-based AI development workflow. One loop, five phases, everything on disk. Alpha.",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alan Martinez"
|
|
8
8
|
}
|
|
@@ -21,6 +21,11 @@ A facilitator, not an author. You read files, hand ONE sub-agent ONE task, write
|
|
|
21
21
|
the result back, and stop at the gate. You hold no state between steps — the files
|
|
22
22
|
are the state. You do not decide anything that is the human's to decide.
|
|
23
23
|
|
|
24
|
+
The framework's `stages/`, `workflows/` and `templates/` live in the installed package —
|
|
25
|
+
`$(npm root -g)/tldr-experts/` globally, `node_modules/tldr-experts/` locally, and
|
|
26
|
+
`tldrx doctor` prints the exact path on its `framework` line. This project's overrides
|
|
27
|
+
live in `.tldrx/stages/` and `.tldrx/workflows/`, and win. Never `find /` for them.
|
|
28
|
+
|
|
24
29
|
## Step 1, always
|
|
25
30
|
|
|
26
31
|
```
|
|
@@ -55,8 +60,11 @@ Never answer on their behalf, never pick a default, never "assume yes":
|
|
|
55
60
|
edit `split.yml` if they want different runs.
|
|
56
61
|
- a gate, an answer, or any ADR/decision document — quote it and ask.
|
|
57
62
|
- a gate `tldrx status` reports as signed `by: auto` that they disagree with. It signs
|
|
58
|
-
only when its
|
|
59
|
-
the note — it carries all
|
|
63
|
+
only when its seven conditions hold and cannot judge whether a decision was RIGHT: quote
|
|
64
|
+
the note — it carries all seven measured values — and ask before revoking.
|
|
65
|
+
- a Build story left `blocked` that they want built anyway. Two reviewers judged the diff
|
|
66
|
+
and were not necessarily wrong; overruling them costs another two turns and is theirs to
|
|
67
|
+
decide. Quote `04-build/log/<id>.md` — it says what each verdict was and why — and ask.
|
|
60
68
|
|
|
61
69
|
**You act** once they have said what they want — these are mechanical:
|
|
62
70
|
|
|
@@ -65,6 +73,11 @@ Never answer on their behalf, never pick a default, never "assume yes":
|
|
|
65
73
|
after they said yes · `tldrx reject --run <id> --note "<their reason>"`, and
|
|
66
74
|
`tldrx reject --run <id> --stage <phase>/<stage> --note "…"` to take back an
|
|
67
75
|
approval already given (including one the harness signed `by: auto`).
|
|
76
|
+
- `tldrx story reopen <id> --run <id> --note "<their reason>"` once they have said a
|
|
77
|
+
blocked story must be built anyway: it puts that one story back to `todo` and restarts
|
|
78
|
+
its attempt counter at 1 of 2, keeping every earlier attempt on the record. It does NOT
|
|
79
|
+
send the stage back — if the Build stage is at its gate, `tldrx reject` does that first,
|
|
80
|
+
and the reopen's own output names which one applies.
|
|
68
81
|
- `tldrx questions lint --run <id>` when a stage wrote a `questions.md`: it exits 2
|
|
69
82
|
if any block is invisible to the parser, and `--fix` converts it without changing
|
|
70
83
|
a word. An unreadable file reads as "no questions" to everything downstream.
|
|
@@ -152,7 +165,7 @@ retries are where the money is) and `tldrx budget show`. Quote them, never the a
|
|
|
152
165
|
from disk. A sub-agent reporting "ok" is not evidence.
|
|
153
166
|
6. **Say which you are in: measured, inferred, or assumed.** Never blur them.
|
|
154
167
|
7. **Stop at gates.** You never close one — not even an `auto` gate, which the
|
|
155
|
-
framework closes itself and only when all
|
|
168
|
+
framework closes itself and only when all seven of its conditions hold.
|
|
156
169
|
8. **A checked section holds at least one item.** Nothing to report is still an
|
|
157
170
|
item: `- none [src: absent:<what you looked at>]`. Prose is refused.
|
|
158
171
|
9. **`.tldrx/` and `tldrx-work/` are committed** — the files ARE the state. `init`
|
package/stages/build/stage.md
CHANGED
|
@@ -56,6 +56,11 @@ Per story, in `waves.yml` order:
|
|
|
56
56
|
evidence and is never recorded as any.
|
|
57
57
|
- A reviewer's `changes` requeues the story ONCE, with the review under
|
|
58
58
|
`## Previous attempt`. A second `changes` blocks it.
|
|
59
|
+
- An acceptance criterion that embeds a literal command or pattern is validated BEFORE
|
|
60
|
+
the edit — run against the current tree first, and a criterion that reports zero while
|
|
61
|
+
the goal says the work exists is broken, so the real inventory becomes the completion
|
|
62
|
+
test and the discrepancy goes in the handoff (the criterion text is not the story's to
|
|
63
|
+
edit). The developer prompt carries this rule verbatim.
|
|
59
64
|
- A DoD failure or a merge conflict blocks that story only — the wave carries on.
|
|
60
65
|
- Nothing pushes. No epic is merged into a default branch.
|
|
61
66
|
|
package/templates/budget.yml
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
# ceiling_usd is a refusal, not a warning: a stage whose estimate exceeds what
|
|
5
5
|
# remains does not start. spent_usd is rolled up from events.jsonl — never typed
|
|
6
6
|
# by hand, never estimated by an agent.
|
|
7
|
+
#
|
|
8
|
+
# `economy:` is optional and says what these numbers are DENOMINATED IN:
|
|
9
|
+
# metered-usd (the default) dollars a spawn may spend
|
|
10
|
+
# host-tokens a host-session token allowance — not money, never a spawn cap
|
|
11
|
+
# Leave it out and everything behaves as it always has. See spec §2.11.
|
|
7
12
|
version: 1
|
|
8
13
|
run: "260828-example-slug"
|
|
9
14
|
ceiling_usd: 25
|
|
File without changes
|
|
@@ -1,497 +0,0 @@
|
|
|
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 };
|
|
@@ -1,14 +0,0 @@
|
|
|
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 };
|