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.
- package/CHANGELOG.md +913 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/hooks/answer-capture.js +174 -0
- package/dist/hooks/budget-gate.js +173 -0
- package/dist/hooks/chunk-0bt6yb2q.js +88 -0
- package/dist/hooks/chunk-1zwcxd3f.js +66 -0
- package/dist/hooks/chunk-9gb21660.js +66 -0
- package/dist/hooks/chunk-a8p2rc94.js +20 -0
- package/dist/hooks/chunk-ae6bkfs5.js +0 -0
- package/dist/hooks/chunk-azctppjh.js +198 -0
- package/dist/hooks/chunk-g395gk7e.js +503 -0
- package/dist/hooks/chunk-j234zf0t.js +339 -0
- package/dist/hooks/chunk-kw4tffzf.js +139 -0
- package/dist/hooks/chunk-p274ckxv.js +7435 -0
- package/dist/hooks/chunk-sdjnnmzz.js +497 -0
- package/dist/hooks/chunk-t56k6146.js +14 -0
- package/dist/hooks/chunk-t8tdv11p.js +35 -0
- package/dist/hooks/chunk-x98qs959.js +302 -0
- package/dist/hooks/chunk-y0jdr3et.js +627 -0
- package/dist/hooks/claim-sources.js +72 -0
- package/dist/hooks/dod-gate.js +198 -0
- package/dist/hooks/no-reask.js +67 -0
- package/dist/hooks/session-start.js +1348 -0
- package/dist/hooks/statusline.js +116 -0
- package/dist/tldrx.js +34219 -0
- package/env.yml +79 -0
- package/package.json +60 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/README.md +113 -0
- package/plugin/agents/README.md +22 -0
- package/plugin/hooks/hooks.json +107 -0
- package/plugin/skills/tldrx/SKILL.md +170 -0
- package/stages/build/stage.md +70 -0
- package/stages/build/stage.yml +54 -0
- package/stages/how/stage.md +89 -0
- package/stages/how/stage.yml +53 -0
- package/stages/plan/stage.md +89 -0
- package/stages/plan/stage.yml +48 -0
- package/stages/watch/stage.md +85 -0
- package/stages/watch/stage.yml +61 -0
- package/stages/what/stage.md +89 -0
- package/stages/what/stage.yml +61 -0
- package/templates/budget.yml +17 -0
- package/templates/competencies.yml +14 -0
- package/templates/env.yml +19 -0
- package/templates/epic.md +38 -0
- package/templates/expert.md +51 -0
- package/templates/experts/architect.md +77 -0
- package/templates/experts/delivery.md +76 -0
- package/templates/experts/developer.md +78 -0
- package/templates/experts/operations.md +74 -0
- package/templates/experts/product.md +78 -0
- package/templates/facts.yml +18 -0
- package/templates/handoff.md +63 -0
- package/templates/process.yml +33 -0
- package/templates/questions.md +63 -0
- package/templates/run.yml +24 -0
- package/templates/story.md +55 -0
- package/templates/watcher.md +68 -0
- package/templates/waves.yml +17 -0
- package/templates/workspace.yml +30 -0
- package/workflows/bugfix.yml +26 -0
- package/workflows/docs.yml +23 -0
- package/workflows/feature.yml +22 -0
- package/workflows/hotfix.yml +24 -0
- package/workflows/integration.yml +24 -0
- package/workflows/migration.yml +24 -0
- package/workflows/performance.yml +23 -0
- package/workflows/prototype.yml +24 -0
- package/workflows/refactor.yml +24 -0
- package/workflows/retro.yml +20 -0
- package/workflows/security-patch.yml +23 -0
- package/workflows/spike.yml +23 -0
- package/workflows/upgrade.yml +23 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asRunBudget,
|
|
3
|
+
asRunFile,
|
|
4
|
+
cursorStage,
|
|
5
|
+
derivePhaseStatus,
|
|
6
|
+
deriveRunStatus,
|
|
7
|
+
flatten,
|
|
8
|
+
isFinished,
|
|
9
|
+
isTerminal,
|
|
10
|
+
nowRfc3339,
|
|
11
|
+
openRunViews,
|
|
12
|
+
stageAt,
|
|
13
|
+
validateRunBudget,
|
|
14
|
+
validateRunFile
|
|
15
|
+
} from "./chunk-sdjnnmzz.js";
|
|
16
|
+
import {
|
|
17
|
+
EventLog
|
|
18
|
+
} from "./chunk-azctppjh.js";
|
|
19
|
+
import {
|
|
20
|
+
isAlive,
|
|
21
|
+
openBlocks,
|
|
22
|
+
parseQuestions,
|
|
23
|
+
readLock,
|
|
24
|
+
withWorkspaceLock,
|
|
25
|
+
workspaceRootOfRunDir,
|
|
26
|
+
yamlScalar
|
|
27
|
+
} from "./chunk-j234zf0t.js";
|
|
28
|
+
import {
|
|
29
|
+
noteDeprecations
|
|
30
|
+
} from "./chunk-t56k6146.js";
|
|
31
|
+
import {
|
|
32
|
+
listRunDirs,
|
|
33
|
+
parseYaml
|
|
34
|
+
} from "./chunk-p274ckxv.js";
|
|
35
|
+
|
|
36
|
+
// src/core/statusline/runSnapshot.ts
|
|
37
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
38
|
+
import { join as join2 } from "node:path";
|
|
39
|
+
|
|
40
|
+
// src/core/run/RunStore.ts
|
|
41
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
42
|
+
import { basename, dirname, join } from "node:path";
|
|
43
|
+
|
|
44
|
+
// src/core/run/emitRunYaml.ts
|
|
45
|
+
function inlineList(values) {
|
|
46
|
+
return `[${values.map((v) => yamlScalar(v)).join(", ")}]`;
|
|
47
|
+
}
|
|
48
|
+
function money(n) {
|
|
49
|
+
return n.toFixed(2);
|
|
50
|
+
}
|
|
51
|
+
function gatesPolicy(policy) {
|
|
52
|
+
const entries = Object.entries(policy).map(([id, value]) => `${yamlScalar(id)}: ${yamlScalar(value)}`);
|
|
53
|
+
return `{${entries.join(", ")}}`;
|
|
54
|
+
}
|
|
55
|
+
function gate(g) {
|
|
56
|
+
return `{type: ${yamlScalar(g.type)}, status: ${yamlScalar(g.status)}, by: ${yamlScalar(g.by)}, ` + `at: ${yamlScalar(g.at)}, note: ${yamlScalar(g.note)}}`;
|
|
57
|
+
}
|
|
58
|
+
function task(t, indent) {
|
|
59
|
+
const inner = `${indent} `;
|
|
60
|
+
const cost = t.cost_usd === null ? "null" : money(t.cost_usd);
|
|
61
|
+
const metered = t.metered === false ? ", metered: false" : "";
|
|
62
|
+
const tokens = t.tokens === undefined ? "" : `, tokens: ${String(t.tokens)}`;
|
|
63
|
+
return [
|
|
64
|
+
`${indent} - {id: ${yamlScalar(t.id)}, status: ${yamlScalar(t.status)}, expert: ${yamlScalar(t.expert)}, ` + `model: ${yamlScalar(t.model)}, cost_usd: ${cost}${metered}${tokens},`,
|
|
65
|
+
`${inner}error: ${yamlScalar(t.error)}, session_id: ${yamlScalar(t.session_id)},`,
|
|
66
|
+
`${inner}started_at: ${yamlScalar(t.started_at)}, ended_at: ${yamlScalar(t.ended_at)},`,
|
|
67
|
+
...t.stopped_by === undefined || t.stopped_by === null ? [] : [`${inner}stopped_by: ${yamlScalar(t.stopped_by)},`],
|
|
68
|
+
`${inner}outputs: ${inlineList(t.outputs)}}`
|
|
69
|
+
].join(`
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
function stage(s) {
|
|
73
|
+
const lines = [
|
|
74
|
+
` - id: ${yamlScalar(s.id)}`,
|
|
75
|
+
` status: ${yamlScalar(s.status)}`,
|
|
76
|
+
` expert: ${yamlScalar(s.expert)}`,
|
|
77
|
+
` model: ${yamlScalar(s.model)}`,
|
|
78
|
+
` budget_usd: ${money(s.budget_usd)}`,
|
|
79
|
+
` cost_usd: ${money(s.cost_usd)}`,
|
|
80
|
+
` started_at: ${yamlScalar(s.started_at)}`,
|
|
81
|
+
` ended_at: ${yamlScalar(s.ended_at)}`,
|
|
82
|
+
` inputs: ${inlineList(s.inputs)}`,
|
|
83
|
+
` outputs: ${inlineList(s.outputs)}`,
|
|
84
|
+
` gate: ${gate(s.gate)}`
|
|
85
|
+
];
|
|
86
|
+
if (s.stale === true)
|
|
87
|
+
lines.push(" stale: true");
|
|
88
|
+
if (s.tasks.length === 0) {
|
|
89
|
+
lines.push(" tasks: []");
|
|
90
|
+
} else {
|
|
91
|
+
lines.push(" tasks:");
|
|
92
|
+
for (const t of s.tasks)
|
|
93
|
+
lines.push(task(t, " "));
|
|
94
|
+
}
|
|
95
|
+
return lines.join(`
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
function emitRunYaml(run) {
|
|
99
|
+
const lines = [
|
|
100
|
+
"# tldrx-work/<run>/run.yml — the execution path and the only resume point (spec §2.2).",
|
|
101
|
+
"# Written by the facilitator alone. Hand-edit at your own risk: every write revalidates.",
|
|
102
|
+
`version: ${run.version}`,
|
|
103
|
+
`run: ${yamlScalar(run.run)}`,
|
|
104
|
+
`title: ${yamlScalar(run.title)}`,
|
|
105
|
+
`scope: ${yamlScalar(run.scope)}`,
|
|
106
|
+
`workflow: ${yamlScalar(run.workflow)}`,
|
|
107
|
+
`repos: ${inlineList(run.repos)}`,
|
|
108
|
+
`created_at: ${yamlScalar(run.created_at)}`,
|
|
109
|
+
`updated_at: ${yamlScalar(run.updated_at)}`,
|
|
110
|
+
`status: ${yamlScalar(run.status)}`,
|
|
111
|
+
`cursor: {phase: ${yamlScalar(run.cursor.phase)}, stage: ${yamlScalar(run.cursor.stage)}, ` + `task: ${yamlScalar(run.cursor.task)}}`,
|
|
112
|
+
`budget: {ceiling_usd: ${money(run.budget.ceiling_usd)}, spent_usd: ${money(run.budget.spent_usd)}, ` + `per_agent_max_usd: ${money(run.budget.per_agent_max_usd)}}`
|
|
113
|
+
];
|
|
114
|
+
if (run.triage !== undefined) {
|
|
115
|
+
lines.push(`triage: {split: ${yamlScalar(run.triage.split)}, depends_on: ${inlineList(run.triage.depends_on)}}`);
|
|
116
|
+
}
|
|
117
|
+
if (run.build !== undefined && run.build.epic_branch.length > 0) {
|
|
118
|
+
lines.push(`build: {epic_branch: ${inlineList(run.build.epic_branch)}}`);
|
|
119
|
+
}
|
|
120
|
+
if (run.cancelled !== undefined) {
|
|
121
|
+
lines.push(`cancelled: {by: ${yamlScalar(run.cancelled.by)}, at: ${yamlScalar(run.cancelled.at)}, ` + `note: ${yamlScalar(run.cancelled.note)}}`);
|
|
122
|
+
}
|
|
123
|
+
if (run.gates_policy !== undefined && Object.keys(run.gates_policy).length > 0) {
|
|
124
|
+
lines.push(`gates_policy: ${gatesPolicy(run.gates_policy)}`);
|
|
125
|
+
}
|
|
126
|
+
lines.push("phases:");
|
|
127
|
+
for (const phase of run.phases) {
|
|
128
|
+
lines.push(` - id: ${yamlScalar(phase.id)}`);
|
|
129
|
+
lines.push(` status: ${yamlScalar(phase.status)}`);
|
|
130
|
+
if (phase.stages.length === 0) {
|
|
131
|
+
lines.push(" stages: []");
|
|
132
|
+
} else {
|
|
133
|
+
lines.push(" stages:");
|
|
134
|
+
for (const s of phase.stages)
|
|
135
|
+
lines.push(stage(s));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return `${lines.join(`
|
|
139
|
+
`)}
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
function emitBudgetYaml(budget) {
|
|
143
|
+
const lines = [
|
|
144
|
+
"# tldrx-work/<run>/budget.yml — the ceiling the facilitator refuses to exceed (spec §2.11).",
|
|
145
|
+
"# Actuals are rolled up from run.yml task costs; never typed by hand.",
|
|
146
|
+
`version: ${budget.version}`,
|
|
147
|
+
`run: ${yamlScalar(budget.run)}`,
|
|
148
|
+
`ceiling_usd: ${money(budget.ceiling_usd)}`,
|
|
149
|
+
`per_agent_max_usd: ${money(budget.per_agent_max_usd)}`,
|
|
150
|
+
`warn_at_pct: ${budget.warn_at_pct}`,
|
|
151
|
+
`on_exceed: ${yamlScalar(budget.on_exceed)}`,
|
|
152
|
+
"phases:"
|
|
153
|
+
];
|
|
154
|
+
for (const phase of budget.phases) {
|
|
155
|
+
lines.push(` - {id: ${yamlScalar(phase.id)}, ceiling_usd: ${money(phase.ceiling_usd)}, ` + `spent_usd: ${money(phase.spent_usd)}}`);
|
|
156
|
+
}
|
|
157
|
+
return `${lines.join(`
|
|
158
|
+
`)}
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/core/run/RunStore.ts
|
|
163
|
+
class RunStoreError extends Error {
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
class RunStore {
|
|
167
|
+
runDir;
|
|
168
|
+
events;
|
|
169
|
+
current;
|
|
170
|
+
currentBudget;
|
|
171
|
+
budgetMutated = false;
|
|
172
|
+
constructor(runDir, run, budget, events) {
|
|
173
|
+
this.runDir = runDir;
|
|
174
|
+
this.events = events;
|
|
175
|
+
this.current = run;
|
|
176
|
+
this.currentBudget = budget;
|
|
177
|
+
}
|
|
178
|
+
static open(runDir) {
|
|
179
|
+
const runPath = join(runDir, "run.yml");
|
|
180
|
+
if (!existsSync(runPath))
|
|
181
|
+
throw new RunStoreError(`no run.yml in ${runDir}`);
|
|
182
|
+
const doc = parseYaml(readFileSync(runPath, "utf8"));
|
|
183
|
+
const validation = validateRunFile(doc);
|
|
184
|
+
noteDeprecations(runPath, validation);
|
|
185
|
+
if (!validation.ok) {
|
|
186
|
+
const first = validation.issues[0];
|
|
187
|
+
throw new RunStoreError(`invalid run.yml (${runPath}): ${first?.path ?? ""} ${first?.message ?? "schema error"}`);
|
|
188
|
+
}
|
|
189
|
+
const budgetPath = join(runDir, "budget.yml");
|
|
190
|
+
if (!existsSync(budgetPath))
|
|
191
|
+
throw new RunStoreError(`no budget.yml in ${runDir}`);
|
|
192
|
+
const budgetDoc = parseYaml(readFileSync(budgetPath, "utf8"));
|
|
193
|
+
const budgetValidation = validateRunBudget(budgetDoc);
|
|
194
|
+
noteDeprecations(budgetPath, budgetValidation);
|
|
195
|
+
if (!budgetValidation.ok) {
|
|
196
|
+
const first = budgetValidation.issues[0];
|
|
197
|
+
throw new RunStoreError(`invalid budget.yml (${budgetPath}): ${first?.path ?? ""} ${first?.message ?? "schema error"}`);
|
|
198
|
+
}
|
|
199
|
+
return new RunStore(runDir, asRunFile(doc), asRunBudget(budgetDoc), EventLog.forRun(runDir));
|
|
200
|
+
}
|
|
201
|
+
static find(root, runId) {
|
|
202
|
+
const dir = listRunDirs(root).find((d) => basename(d) === runId);
|
|
203
|
+
if (dir === undefined)
|
|
204
|
+
return null;
|
|
205
|
+
return RunStore.open(dir);
|
|
206
|
+
}
|
|
207
|
+
static findOpen(root) {
|
|
208
|
+
const open = [];
|
|
209
|
+
for (const dir of listRunDirs(root)) {
|
|
210
|
+
let store;
|
|
211
|
+
try {
|
|
212
|
+
store = RunStore.open(dir);
|
|
213
|
+
} catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (isFinished(store.run.status))
|
|
217
|
+
continue;
|
|
218
|
+
open.push(store);
|
|
219
|
+
}
|
|
220
|
+
return open.sort((a, b) => compareDesc(a.run.updated_at, b.run.updated_at));
|
|
221
|
+
}
|
|
222
|
+
static resolve(root, runId) {
|
|
223
|
+
if (runId !== undefined && runId !== "") {
|
|
224
|
+
const store = RunStore.find(root, runId);
|
|
225
|
+
return store === null ? { kind: "none" } : { kind: "one", store };
|
|
226
|
+
}
|
|
227
|
+
const open = RunStore.findOpen(root);
|
|
228
|
+
const only = open[0];
|
|
229
|
+
if (only === undefined)
|
|
230
|
+
return { kind: "none" };
|
|
231
|
+
if (open.length === 1)
|
|
232
|
+
return { kind: "one", store: only };
|
|
233
|
+
return { kind: "ambiguous", open };
|
|
234
|
+
}
|
|
235
|
+
get run() {
|
|
236
|
+
return this.current;
|
|
237
|
+
}
|
|
238
|
+
get budget() {
|
|
239
|
+
return this.currentBudget;
|
|
240
|
+
}
|
|
241
|
+
get runId() {
|
|
242
|
+
return this.current.run;
|
|
243
|
+
}
|
|
244
|
+
cursorEntry() {
|
|
245
|
+
return stageAt(this.current, this.current.cursor);
|
|
246
|
+
}
|
|
247
|
+
nextEntry() {
|
|
248
|
+
const all = flatten(this.current);
|
|
249
|
+
const at = all.findIndex((e) => e.phase.id === this.current.cursor.phase && e.stage.id === this.current.cursor.stage);
|
|
250
|
+
if (at === -1)
|
|
251
|
+
return null;
|
|
252
|
+
return all[at + 1] ?? null;
|
|
253
|
+
}
|
|
254
|
+
mutate(fn) {
|
|
255
|
+
this.current = fn(structuredClone(this.current));
|
|
256
|
+
}
|
|
257
|
+
mutateBudget(fn) {
|
|
258
|
+
this.currentBudget = fn(structuredClone(this.currentBudget));
|
|
259
|
+
this.budgetMutated = true;
|
|
260
|
+
}
|
|
261
|
+
append(event) {
|
|
262
|
+
this.events.append(event);
|
|
263
|
+
}
|
|
264
|
+
save() {
|
|
265
|
+
const rolled = rollUp(this.current);
|
|
266
|
+
const runValidation = validateRunFile(rolled);
|
|
267
|
+
if (!runValidation.ok) {
|
|
268
|
+
const first = runValidation.issues[0];
|
|
269
|
+
throw new RunStoreError(`refusing to write an invalid run.yml: ${first?.path ?? ""} ${first?.message ?? ""}`);
|
|
270
|
+
}
|
|
271
|
+
withWorkspaceLock(workspaceRootOfRunDir(this.runDir), () => {
|
|
272
|
+
const budget = rollUpBudget(this.ceilingsToWrite(), rolled);
|
|
273
|
+
const budgetValidation = validateRunBudget(budget);
|
|
274
|
+
if (!budgetValidation.ok) {
|
|
275
|
+
const first = budgetValidation.issues[0];
|
|
276
|
+
throw new RunStoreError(`refusing to write an invalid budget.yml: ${first?.path ?? ""} ${first?.message ?? ""}`);
|
|
277
|
+
}
|
|
278
|
+
writeAtomic(join(this.runDir, "budget.yml"), emitBudgetYaml(budget));
|
|
279
|
+
writeAtomic(join(this.runDir, "run.yml"), emitRunYaml(rolled));
|
|
280
|
+
this.current = rolled;
|
|
281
|
+
this.currentBudget = budget;
|
|
282
|
+
this.budgetMutated = false;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
ceilingsToWrite() {
|
|
286
|
+
if (this.budgetMutated)
|
|
287
|
+
return this.currentBudget;
|
|
288
|
+
const path = join(this.runDir, "budget.yml");
|
|
289
|
+
if (!existsSync(path))
|
|
290
|
+
return this.currentBudget;
|
|
291
|
+
try {
|
|
292
|
+
const doc = parseYaml(readFileSync(path, "utf8"));
|
|
293
|
+
const validation = validateRunBudget(doc);
|
|
294
|
+
if (!validation.ok)
|
|
295
|
+
return this.currentBudget;
|
|
296
|
+
const onDisk = asRunBudget(doc);
|
|
297
|
+
return {
|
|
298
|
+
...onDisk,
|
|
299
|
+
phases: this.currentBudget.phases.map((mine) => {
|
|
300
|
+
const theirs = onDisk.phases.find((p) => p.id === mine.id);
|
|
301
|
+
return theirs === undefined ? mine : { ...mine, ceiling_usd: theirs.ceiling_usd };
|
|
302
|
+
})
|
|
303
|
+
};
|
|
304
|
+
} catch {
|
|
305
|
+
return this.currentBudget;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function writeAtomic(path, content) {
|
|
310
|
+
const temp = `${path}.tmp-${String(process.pid)}`;
|
|
311
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
312
|
+
try {
|
|
313
|
+
writeFileSync(temp, content, "utf8");
|
|
314
|
+
renameSync(temp, path);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
try {
|
|
317
|
+
if (existsSync(temp))
|
|
318
|
+
rmSync(temp, { force: true });
|
|
319
|
+
} catch {}
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function rollUp(run) {
|
|
324
|
+
const phases = run.phases.map((phase) => {
|
|
325
|
+
const stages = phase.stages.map((stage2) => ({
|
|
326
|
+
...stage2,
|
|
327
|
+
cost_usd: round(stage2.tasks.reduce((sum, t) => sum + (t.cost_usd ?? 0), 0))
|
|
328
|
+
}));
|
|
329
|
+
const withStages = { ...phase, stages, status: "pending" };
|
|
330
|
+
return { ...withStages, status: derivePhaseStatus(withStages) };
|
|
331
|
+
});
|
|
332
|
+
const spent = round(phases.reduce((sum, p) => sum + p.stages.reduce((s, st) => s + st.cost_usd, 0), 0));
|
|
333
|
+
const next = {
|
|
334
|
+
...run,
|
|
335
|
+
phases,
|
|
336
|
+
budget: { ...run.budget, spent_usd: spent },
|
|
337
|
+
updated_at: nowRfc3339()
|
|
338
|
+
};
|
|
339
|
+
return { ...next, status: deriveRunStatus(next) };
|
|
340
|
+
}
|
|
341
|
+
function rollUpBudget(budget, run) {
|
|
342
|
+
const spentByPhase = new Map;
|
|
343
|
+
for (const phase of run.phases) {
|
|
344
|
+
spentByPhase.set(phase.id, round(phase.stages.reduce((sum, s) => sum + s.cost_usd, 0)));
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
...budget,
|
|
348
|
+
phases: budget.phases.map((p) => ({ ...p, spent_usd: spentByPhase.get(p.id) ?? p.spent_usd }))
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function compareDesc(a, b) {
|
|
352
|
+
if (a > b)
|
|
353
|
+
return -1;
|
|
354
|
+
return a < b ? 1 : 0;
|
|
355
|
+
}
|
|
356
|
+
function round(n) {
|
|
357
|
+
return Math.round(n * 100) / 100;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/core/facilitator/skipIf.ts
|
|
361
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
362
|
+
function phaseDirs(runDir) {
|
|
363
|
+
try {
|
|
364
|
+
return readdirSync(runDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^0[1-9]-/.test(entry.name)).map((entry) => entry.name).sort();
|
|
365
|
+
} catch {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function openQuestionIds(path) {
|
|
370
|
+
if (!existsSync2(path))
|
|
371
|
+
return [];
|
|
372
|
+
try {
|
|
373
|
+
return openBlocks(parseQuestions(readFileSync2(path, "utf8")).blocks).map((block) => block.id);
|
|
374
|
+
} catch {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/core/statusline/runSnapshot.ts
|
|
380
|
+
var AUTO = "auto";
|
|
381
|
+
function runSnapshot(root) {
|
|
382
|
+
return fromStore(root) ?? fromTolerantRead(root);
|
|
383
|
+
}
|
|
384
|
+
function openQuestions(snapshot) {
|
|
385
|
+
const ids = [];
|
|
386
|
+
for (const phase of phaseDirs(snapshot.runDir)) {
|
|
387
|
+
ids.push(...openQuestionIds(join2(snapshot.runDir, phase, "questions.md")));
|
|
388
|
+
}
|
|
389
|
+
return ids;
|
|
390
|
+
}
|
|
391
|
+
function fromStore(root) {
|
|
392
|
+
let open;
|
|
393
|
+
try {
|
|
394
|
+
open = RunStore.findOpen(root);
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
const store = open[0];
|
|
399
|
+
if (store === undefined)
|
|
400
|
+
return null;
|
|
401
|
+
const run = store.run;
|
|
402
|
+
const entry = store.cursorEntry();
|
|
403
|
+
const stages = run.phases.flatMap((phase) => phase.stages);
|
|
404
|
+
return {
|
|
405
|
+
runDir: store.runDir,
|
|
406
|
+
run: run.run,
|
|
407
|
+
title: run.title,
|
|
408
|
+
scope: run.scope,
|
|
409
|
+
status: run.status,
|
|
410
|
+
phase: run.cursor.phase,
|
|
411
|
+
stage: run.cursor.stage,
|
|
412
|
+
stageStatus: entry?.stage.status ?? run.status,
|
|
413
|
+
expert: entry?.stage.expert ?? null,
|
|
414
|
+
done: stages.filter((stage2) => isTerminal(stage2.status)).length,
|
|
415
|
+
total: stages.length,
|
|
416
|
+
ceilingUsd: run.budget.ceiling_usd,
|
|
417
|
+
spentUsd: run.budget.spent_usd,
|
|
418
|
+
openCount: open.length,
|
|
419
|
+
autoGates: stages.filter((s) => s.gate.status === "approved" && s.gate.by === AUTO).length,
|
|
420
|
+
staleStages: stages.filter((s) => s.stale === true).length,
|
|
421
|
+
source: "run-store"
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function fromTolerantRead(root) {
|
|
425
|
+
const open = openRunViews(root);
|
|
426
|
+
const view = open[0];
|
|
427
|
+
if (view === undefined)
|
|
428
|
+
return null;
|
|
429
|
+
const stage2 = cursorStage(view);
|
|
430
|
+
const stages = view.phases.flatMap((phase) => phase.stages);
|
|
431
|
+
const budget = readBudgetMirror(view.dir);
|
|
432
|
+
return {
|
|
433
|
+
runDir: view.dir,
|
|
434
|
+
run: view.run,
|
|
435
|
+
title: view.title,
|
|
436
|
+
scope: view.scope,
|
|
437
|
+
status: view.status,
|
|
438
|
+
phase: view.cursor?.phase ?? "",
|
|
439
|
+
stage: view.cursor?.stage ?? "",
|
|
440
|
+
stageStatus: stage2?.status === undefined || stage2.status === "" ? view.status : stage2.status,
|
|
441
|
+
expert: stage2?.expert ?? null,
|
|
442
|
+
done: stages.filter((s) => isTerminal(s.status)).length,
|
|
443
|
+
total: stages.length,
|
|
444
|
+
ceilingUsd: budget.ceiling_usd,
|
|
445
|
+
spentUsd: budget.spent_usd,
|
|
446
|
+
openCount: open.length,
|
|
447
|
+
autoGates: 0,
|
|
448
|
+
staleStages: 0,
|
|
449
|
+
source: "tolerant"
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function readBudgetMirror(runDir) {
|
|
453
|
+
const path = join2(runDir, "run.yml");
|
|
454
|
+
if (!existsSync3(path))
|
|
455
|
+
return { ceiling_usd: 0, spent_usd: 0 };
|
|
456
|
+
try {
|
|
457
|
+
const text = readFileSync3(path, "utf8");
|
|
458
|
+
return {
|
|
459
|
+
ceiling_usd: numberAfter(text, "ceiling_usd"),
|
|
460
|
+
spent_usd: numberAfter(text, "spent_usd")
|
|
461
|
+
};
|
|
462
|
+
} catch {
|
|
463
|
+
return { ceiling_usd: 0, spent_usd: 0 };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function numberAfter(text, key) {
|
|
467
|
+
const match = new RegExp(`${key}:\\s*([0-9]+(?:\\.[0-9]+)?)`).exec(text);
|
|
468
|
+
return match === null ? 0 : Number(match[1]);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/core/run/waiting.ts
|
|
472
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
473
|
+
import { basename as basename2, join as join4 } from "node:path";
|
|
474
|
+
|
|
475
|
+
// src/core/run/prepared.ts
|
|
476
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
477
|
+
import { join as join3 } from "node:path";
|
|
478
|
+
var PENDING_JSON = "pending.json";
|
|
479
|
+
function preparedBundles(runDir, stageId) {
|
|
480
|
+
const dir = join3(runDir, ".agent", stageId);
|
|
481
|
+
if (!existsSync4(dir))
|
|
482
|
+
return [];
|
|
483
|
+
const found = [];
|
|
484
|
+
if (existsSync4(join3(dir, PENDING_JSON)))
|
|
485
|
+
found.push(dir);
|
|
486
|
+
let entries;
|
|
487
|
+
try {
|
|
488
|
+
entries = readdirSync2(dir);
|
|
489
|
+
} catch {
|
|
490
|
+
return found;
|
|
491
|
+
}
|
|
492
|
+
for (const entry of entries) {
|
|
493
|
+
const child = join3(dir, entry);
|
|
494
|
+
try {
|
|
495
|
+
if (!statSync(child).isDirectory())
|
|
496
|
+
continue;
|
|
497
|
+
} catch {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (existsSync4(join3(child, PENDING_JSON)))
|
|
501
|
+
found.push(child);
|
|
502
|
+
}
|
|
503
|
+
return found;
|
|
504
|
+
}
|
|
505
|
+
function hasPreparedBundle(runDir, stageId) {
|
|
506
|
+
return preparedBundles(runDir, stageId).length > 0;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/core/run/waiting.ts
|
|
510
|
+
var MOVABLE_KINDS = ["gate", "answer", "ready", "failed", "prepared"];
|
|
511
|
+
function isMovable(kind) {
|
|
512
|
+
return MOVABLE_KINDS.includes(kind);
|
|
513
|
+
}
|
|
514
|
+
function failureReason(stage2) {
|
|
515
|
+
const error = [...stage2.tasks].reverse().find((task2) => task2.error !== null)?.error ?? null;
|
|
516
|
+
return error === null || error.trim() === "" ? null : oneLine(error);
|
|
517
|
+
}
|
|
518
|
+
function oneLine(text) {
|
|
519
|
+
return text.split(`
|
|
520
|
+
`)[0]?.trim() ?? "";
|
|
521
|
+
}
|
|
522
|
+
function stageAtCursor(run, cursor) {
|
|
523
|
+
for (const phase of run.phases) {
|
|
524
|
+
for (const stage2 of phase.stages) {
|
|
525
|
+
if (phase.id === cursor.phase && stage2.id === cursor.stage)
|
|
526
|
+
return { phase, stage: stage2 };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
function waitingFor(run, runDir) {
|
|
532
|
+
const cursor = run.cursor;
|
|
533
|
+
if (cursor === null) {
|
|
534
|
+
return { kind: "blocked", message: "run.yml records no cursor, so nothing can say where it is", questions: [] };
|
|
535
|
+
}
|
|
536
|
+
const entry = stageAtCursor(run, cursor);
|
|
537
|
+
if (entry === null) {
|
|
538
|
+
return {
|
|
539
|
+
kind: "blocked",
|
|
540
|
+
message: `cursor ${cursor.phase}/${cursor.stage} does not resolve to a stage`,
|
|
541
|
+
questions: []
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const open = openQuestionIds2(join4(runDir, cursor.phase, "questions.md"));
|
|
545
|
+
switch (entry.stage.status) {
|
|
546
|
+
case "awaiting_gate":
|
|
547
|
+
return {
|
|
548
|
+
kind: "gate",
|
|
549
|
+
message: `gate on ${entry.phase.id}/${entry.stage.id} — \`tldrx approve\` or \`tldrx reject --note "…"\``,
|
|
550
|
+
questions: open
|
|
551
|
+
};
|
|
552
|
+
case "awaiting_answer":
|
|
553
|
+
return {
|
|
554
|
+
kind: "answer",
|
|
555
|
+
message: open.length === 0 ? `stage ${entry.stage.id} is waiting on an answer, but ${cursor.phase}/questions.md has no open block` : `${open.length} open question(s) in ${cursor.phase}/questions.md — \`tldrx answer ${open[0] ?? "Q1"} "…"\``,
|
|
556
|
+
questions: open
|
|
557
|
+
};
|
|
558
|
+
case "failed": {
|
|
559
|
+
const reason = failureReason(entry.stage);
|
|
560
|
+
return {
|
|
561
|
+
kind: "failed",
|
|
562
|
+
message: `${entry.phase.id}/${entry.stage.id} FAILED${reason === null ? "" : `: ${reason}`} — ` + 'retry: `tldrx next` · or: `tldrx reject --note "…"`',
|
|
563
|
+
questions: open
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
case "running": {
|
|
567
|
+
const holder = readLock(runDir);
|
|
568
|
+
if (holder !== null && isAlive(holder.pid)) {
|
|
569
|
+
return {
|
|
570
|
+
kind: "running",
|
|
571
|
+
message: `stage is running (pid ${String(holder.pid)}) — wait, or ` + `\`tldrx run unlock ${basename2(runDir)}\` if it died`,
|
|
572
|
+
questions: open
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (hasPreparedBundle(runDir, entry.stage.id)) {
|
|
576
|
+
return {
|
|
577
|
+
kind: "prepared",
|
|
578
|
+
message: "a --prepare bundle is waiting — run the prompt and " + `\`tldrx next --commit ${basename2(runDir)}\`, or ` + `\`tldrx reject --run ${basename2(runDir)} --note …\` to discard`,
|
|
579
|
+
questions: open
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
kind: "ready",
|
|
584
|
+
message: `next up: ${entry.phase.id}/${entry.stage.id} (running, but nothing holds it) — \`tldrx next\``,
|
|
585
|
+
questions: open
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
case "done":
|
|
589
|
+
case "skipped":
|
|
590
|
+
case "cancelled":
|
|
591
|
+
return { kind: "done", message: "every stage is terminal — nothing is waiting", questions: open };
|
|
592
|
+
case "blocked":
|
|
593
|
+
return { kind: "blocked", message: `stage ${entry.stage.id} is blocked`, questions: open };
|
|
594
|
+
default:
|
|
595
|
+
return {
|
|
596
|
+
kind: "ready",
|
|
597
|
+
message: `next up: ${entry.phase.id}/${entry.stage.id} (${entry.stage.status}) — \`tldrx next\``,
|
|
598
|
+
questions: open
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function openQuestionIds2(path) {
|
|
603
|
+
if (!existsSync5(path))
|
|
604
|
+
return [];
|
|
605
|
+
try {
|
|
606
|
+
return openBlocks(parseQuestions(readFileSync4(path, "utf8")).blocks).map((b) => b.id);
|
|
607
|
+
} catch {
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/core/run/runStatus.ts
|
|
613
|
+
var BAR_CELLS = 5;
|
|
614
|
+
function bar(done, total, failed = 0) {
|
|
615
|
+
if (failed > 0) {
|
|
616
|
+
const cells = BAR_CELLS - 1;
|
|
617
|
+
const filled2 = total === 0 ? 0 : Math.min(cells, Math.round(done / total * cells));
|
|
618
|
+
return `✗${"▓".repeat(filled2)}${"░".repeat(cells - filled2)}`;
|
|
619
|
+
}
|
|
620
|
+
const filled = total === 0 ? 0 : Math.round(done / total * BAR_CELLS);
|
|
621
|
+
return "▓".repeat(filled) + "░".repeat(BAR_CELLS - filled);
|
|
622
|
+
}
|
|
623
|
+
function whatIsWaiting(run, runDir) {
|
|
624
|
+
return waitingFor(run, runDir);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export { RunStore, runSnapshot, openQuestions, isMovable, bar, whatIsWaiting };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
wouldBeContent
|
|
4
|
+
} from "./chunk-t8tdv11p.js";
|
|
5
|
+
import {
|
|
6
|
+
claimSourcesDeny,
|
|
7
|
+
claimSourcesEmptySectionDeny,
|
|
8
|
+
claimSourcesMalformedDeny,
|
|
9
|
+
claimSourcesUnresolvedDeny
|
|
10
|
+
} from "./chunk-9gb21660.js";
|
|
11
|
+
import {
|
|
12
|
+
isHandoff,
|
|
13
|
+
validateHandoff
|
|
14
|
+
} from "./chunk-kw4tffzf.js";
|
|
15
|
+
import {
|
|
16
|
+
allow,
|
|
17
|
+
deny,
|
|
18
|
+
filePathOf,
|
|
19
|
+
isWriteOrEdit,
|
|
20
|
+
postContext,
|
|
21
|
+
readPayload,
|
|
22
|
+
runHook
|
|
23
|
+
} from "./chunk-1zwcxd3f.js";
|
|
24
|
+
import"./chunk-a8p2rc94.js";
|
|
25
|
+
import"./chunk-g395gk7e.js";
|
|
26
|
+
import {
|
|
27
|
+
loadWorkspace,
|
|
28
|
+
locateWork,
|
|
29
|
+
toSrcContext
|
|
30
|
+
} from "./chunk-p274ckxv.js";
|
|
31
|
+
|
|
32
|
+
// src/hooks/claim-sources.ts
|
|
33
|
+
await runHook("claim-sources", async () => {
|
|
34
|
+
const payload = await readPayload();
|
|
35
|
+
if (!isWriteOrEdit(payload))
|
|
36
|
+
return;
|
|
37
|
+
const filePath = filePathOf(payload);
|
|
38
|
+
if (!filePath.endsWith(".md"))
|
|
39
|
+
return;
|
|
40
|
+
const location = locateWork(filePath);
|
|
41
|
+
if (location === null)
|
|
42
|
+
return;
|
|
43
|
+
const wouldBe = wouldBeContent(payload, filePath);
|
|
44
|
+
if (wouldBe.kind !== "content")
|
|
45
|
+
return;
|
|
46
|
+
const looksLikeHandoff = filePath.endsWith("handoff.md") || isHandoff(wouldBe.text);
|
|
47
|
+
if (!looksLikeHandoff)
|
|
48
|
+
return;
|
|
49
|
+
const workspace = loadWorkspace(location.root);
|
|
50
|
+
const report = validateHandoff(wouldBe.text, toSrcContext(workspace, location.runDir));
|
|
51
|
+
if (report.ok)
|
|
52
|
+
return;
|
|
53
|
+
const relPath = `tldrx-work/${location.run}/${location.relative}`;
|
|
54
|
+
const parts = [];
|
|
55
|
+
if (report.unsourced.length > 0)
|
|
56
|
+
parts.push(claimSourcesDeny(relPath, report.unsourced));
|
|
57
|
+
if (report.malformed.length > 0)
|
|
58
|
+
parts.push(claimSourcesMalformedDeny(relPath, report.malformed));
|
|
59
|
+
if (report.emptySections.length > 0) {
|
|
60
|
+
parts.push(claimSourcesEmptySectionDeny(relPath, [...report.emptySections]));
|
|
61
|
+
}
|
|
62
|
+
if (report.unresolved.length > 0)
|
|
63
|
+
parts.push(claimSourcesUnresolvedDeny(relPath, report.unresolved));
|
|
64
|
+
if (parts.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const message = parts.join(`
|
|
67
|
+
`);
|
|
68
|
+
if (payload.hook_event_name === "PostToolUse")
|
|
69
|
+
postContext(message);
|
|
70
|
+
deny(message);
|
|
71
|
+
});
|
|
72
|
+
allow();
|