@turri-dev/cli 0.1.8-next.bcca62a → 0.1.8-next.bfcb24e

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 (49) hide show
  1. package/README.md +0 -2
  2. package/dist/cli.js +2 -1
  3. package/dist/config-edit.d.ts +1 -1
  4. package/dist/config-edit.js +2 -0
  5. package/dist/config.d.ts +6 -0
  6. package/dist/config.js +5 -1
  7. package/dist/extension.js +75 -9
  8. package/dist/host.d.ts +2 -0
  9. package/dist/host.js +3 -0
  10. package/dist/init-config.d.ts +1 -0
  11. package/dist/init-config.js +5 -0
  12. package/dist/ledger-cli.d.ts +1 -0
  13. package/dist/ledger-cli.js +50 -0
  14. package/dist/ledger-tools.d.ts +10 -0
  15. package/dist/ledger-tools.js +64 -0
  16. package/dist/loop-cli.js +4 -0
  17. package/dist/pi.js +17 -0
  18. package/dist/regenerate/activity.d.ts +9 -0
  19. package/dist/regenerate/activity.js +11 -0
  20. package/dist/regenerate/agent-shell.js +5 -0
  21. package/dist/regenerate/chunk-plan.d.ts +0 -6
  22. package/dist/regenerate/chunk-plan.js +16 -4
  23. package/dist/regenerate/conformance.js +3 -0
  24. package/dist/regenerate/diagnose.js +4 -2
  25. package/dist/regenerate/job-log.d.ts +3 -1
  26. package/dist/regenerate/job-log.js +8 -5
  27. package/dist/regenerate/jobs-tree.d.ts +7 -3
  28. package/dist/regenerate/jobs-tree.js +21 -6
  29. package/dist/regenerate/jobs.d.ts +9 -0
  30. package/dist/regenerate/jobs.js +38 -3
  31. package/dist/regenerate/ledger-view.d.ts +92 -0
  32. package/dist/regenerate/ledger-view.js +345 -0
  33. package/dist/regenerate/ledger.d.ts +275 -0
  34. package/dist/regenerate/ledger.js +349 -0
  35. package/dist/regenerate/lineage.js +18 -1
  36. package/dist/regenerate/loop.js +10 -1
  37. package/dist/regenerate/regenerate.js +4 -2
  38. package/dist/regenerate/regression.js +2 -0
  39. package/dist/regenerate/side-panel.d.ts +2 -0
  40. package/dist/regenerate/side-panel.js +4 -3
  41. package/dist/regenerate/test-scripts.js +18 -5
  42. package/dist/regenerate/unit-review.js +3 -0
  43. package/docs/config.md +3 -1
  44. package/docs/development.md +1 -0
  45. package/docs/hosting.md +28 -0
  46. package/docs/installation.md +1 -0
  47. package/docs/regenerating.md +1 -1
  48. package/docs/using.md +29 -2
  49. package/package.json +1 -1
@@ -0,0 +1,345 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { describeUsage } from "./activity.js";
4
+ import { formatElapsed } from "./jobs.js";
5
+ import { LEDGER_DIR, ledgerRun, readLedger } from "./ledger.js";
6
+ /** The projects whose ledgers a folder's pulses may be in: its own, and those of the projects in folders under it. */
7
+ export function ledgerProjects(cwd, depth = 3) {
8
+ const found = [];
9
+ const visit = (dir, left) => {
10
+ if (existsSync(join(dir, ".turri", LEDGER_DIR)))
11
+ found.push(dir);
12
+ if (left === 0)
13
+ return;
14
+ let names;
15
+ try {
16
+ names = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
17
+ }
18
+ catch {
19
+ return;
20
+ }
21
+ for (const name of names) {
22
+ if (name.startsWith(".") || name === "node_modules" || name === "plain_modules" || name === "dist")
23
+ continue;
24
+ visit(join(dir, name), left - 1);
25
+ }
26
+ };
27
+ visit(resolve(cwd), depth);
28
+ return found;
29
+ }
30
+ /** A pulse's events, gathered: undefined when the ledger has none of it. `entries` are its run's, oldest first. */
31
+ export function pulseView(id, entries) {
32
+ const own = entries.filter((entry) => entry.job !== undefined && entry.job.startsWith(id));
33
+ if (own.length === 0)
34
+ return undefined;
35
+ const fullId = own[0].job;
36
+ const view = {
37
+ id: fullId,
38
+ run: own[0].run,
39
+ stages: [],
40
+ retried: [],
41
+ rounds: new Map(),
42
+ scripts: [],
43
+ judge: new Map(),
44
+ reviews: [],
45
+ healers: [],
46
+ provider: { retries: 0, stalls: 0, errors: [] },
47
+ refused: new Map(),
48
+ stopped: [],
49
+ history: [],
50
+ };
51
+ const started = entries.find((entry) => entry.kind === "run.started" && entry.run === view.run);
52
+ if (started !== undefined)
53
+ view.runMode = `${started.mode}, ${started.ts.slice(0, 16).replace("T", " ")}`;
54
+ for (const entry of own) {
55
+ if (entry.job !== fullId)
56
+ continue;
57
+ switch (entry.kind) {
58
+ case "job.queued":
59
+ view.queued = { ...entry, ...(entry.module !== undefined ? { module: entry.module } : {}) };
60
+ break;
61
+ case "job.started":
62
+ view.started = entry;
63
+ break;
64
+ case "job.finished":
65
+ view.finished = entry;
66
+ view.usage = entry.usage;
67
+ break;
68
+ case "job.stage":
69
+ view.stages.push(entry.label);
70
+ break;
71
+ case "job.retried":
72
+ view.retried.push(entry);
73
+ break;
74
+ case "gate.round":
75
+ view.rounds.set(entry.gate, { round: Math.max(entry.round, view.rounds.get(entry.gate)?.round ?? 0), of: entry.of });
76
+ break;
77
+ case "script.run":
78
+ view.scripts.push(entry);
79
+ break;
80
+ case "judge.verdict":
81
+ view.judge.set(entry.culprit, (view.judge.get(entry.culprit) ?? 0) + 1);
82
+ break;
83
+ case "review.verdict":
84
+ view.reviews.push(entry);
85
+ break;
86
+ case "healer.ended":
87
+ case "healer.failed":
88
+ view.healers.push(entry);
89
+ break;
90
+ case "provider.retry":
91
+ view.provider.retries += 1;
92
+ break;
93
+ case "provider.stall":
94
+ view.provider.stalls += 1;
95
+ break;
96
+ case "provider.error":
97
+ view.provider.errors.push(entry.error);
98
+ break;
99
+ case "guard.refused":
100
+ view.refused.set(entry.rule, (view.refused.get(entry.rule) ?? 0) + 1);
101
+ break;
102
+ case "command.refused":
103
+ view.refused.set("command check", (view.refused.get("command check") ?? 0) + 1);
104
+ break;
105
+ case "turn.stopped":
106
+ view.stopped.push(entry.reason);
107
+ break;
108
+ case "history.committed":
109
+ case "history.failed":
110
+ case "history.reset":
111
+ case "history.rolledBack":
112
+ view.history.push(entry);
113
+ break;
114
+ case "environment.error":
115
+ view.environment = entry;
116
+ break;
117
+ }
118
+ }
119
+ return view;
120
+ }
121
+ /** What a pulse is called: its module, kind and number - as pulseName says it for a live one. */
122
+ function nameOf(view) {
123
+ const queued = view.queued;
124
+ if (queued === undefined)
125
+ return view.id;
126
+ if (queued.module === undefined)
127
+ return queued.summary;
128
+ return queued.pulse !== undefined ? `${queued.module} · ${queued.jobKind} pulse ${queued.pulse}` : `${queued.module} ${queued.jobKind}`;
129
+ }
130
+ function outcomeOf(view) {
131
+ const finished = view.finished;
132
+ if (finished === undefined)
133
+ return view.started !== undefined ? "never ended - its run stopped while it ran" : "never started";
134
+ const landed = finished.landed === true ? ", landed" : finished.landed === false ? ", did not land" : "";
135
+ const stopped = finished.stoppedBy !== undefined ? ` by ${finished.stoppedBy === "turri" ? "turri" : "the person"}` : "";
136
+ return `${finished.status}${stopped}${landed}, after ${formatElapsed(finished.ms)}`;
137
+ }
138
+ /** A pulse from the ledger, for the chat and `turri ledger <pulse>`: what happened in it, as facts. */
139
+ export function describePulseView(view) {
140
+ const lines = [`${view.id} · ${nameOf(view)}${view.queued?.title !== undefined ? ` - ${view.queued.title}` : ""}`];
141
+ lines.push(` run ${view.run}${view.runMode !== undefined ? ` (${view.runMode})` : ""}`);
142
+ lines.push(` ${outcomeOf(view)}${view.usage !== undefined && describeUsage(view.usage) !== "" ? ` · ${describeUsage(view.usage)}` : ""}${view.queued?.model !== undefined ? ` · ${view.queued.model}` : ""}`);
143
+ if (view.finished?.error !== undefined)
144
+ lines.push(` ended with: ${view.finished.error}`);
145
+ if (view.environment !== undefined)
146
+ lines.push(` the environment lacks ${view.environment.missing} (found by ${view.environment.source === "harness" ? "turri" : "an agent"})`);
147
+ for (const retry of view.retried)
148
+ lines.push(` tried again (${retry.attempt} of ${retry.of}) after: ${retry.failure}`);
149
+ if (view.rounds.size > 0)
150
+ lines.push(` rounds: ${[...view.rounds].map(([gate, { round, of }]) => `${gate} ${round} of ${of}`).join("; ")}`);
151
+ // can_it_fail's runs, on code changed to break it, apart: one failing is a test that caught the bug.
152
+ const mutations = view.scripts.filter((run) => run.mutated === true);
153
+ const tests = view.scripts.filter((run) => run.mutated !== true);
154
+ if (tests.length > 0) {
155
+ const failing = tests.filter((run) => run.exit !== 0);
156
+ const signatures = new Map();
157
+ for (const run of failing)
158
+ if (run.signature !== undefined)
159
+ signatures.set(run.signature, (signatures.get(run.signature) ?? 0) + 1);
160
+ const most = Math.max(0, ...signatures.values());
161
+ const timedOut = tests.filter((run) => run.timedOut).length;
162
+ lines.push(` test runs: ${tests.length}, ${failing.length} failing` +
163
+ (most > 1 ? ` - ${most} of them the same way` : "") +
164
+ (timedOut > 0 ? `, ${timedOut} stopped at the time limit` : ""));
165
+ }
166
+ if (mutations.length > 0) {
167
+ const caught = mutations.filter((run) => run.exit !== 0 && !run.timedOut).length;
168
+ lines.push(` can_it_fail: ${mutations.length} run${mutations.length === 1 ? "" : "s"} on code broken on purpose, ${caught} caught by the tests`);
169
+ }
170
+ if (view.judge.size > 0)
171
+ lines.push(` the judge blamed: ${[...view.judge].map(([culprit, times]) => `${culprit} ${times}x`).join(", ")}`);
172
+ if (view.reviews.length > 0)
173
+ lines.push(` reviews: ${view.reviews.map((review) => (review.pass ? "passed" : `${review.problems} problem${review.problems === 1 ? "" : "s"}`)).join(", ")}`);
174
+ // The same outcome again is counted, not said again: a debugger failing for want of a key fails every time.
175
+ const healed = new Map();
176
+ for (const healer of view.healers) {
177
+ const line = healer.kind === "healer.failed" ? `the ${healer.healer} FAILED: ${healer.error}` : `the ${healer.healer}${healer.found !== undefined ? `: ${healer.found}` : " found nothing"}`;
178
+ healed.set(line, (healed.get(line) ?? 0) + 1);
179
+ }
180
+ for (const [line, times] of healed)
181
+ lines.push(` ${times > 1 ? line.replace(/^(the \S+( FAILED)?)/, `$1 ${times}x`) : line}`);
182
+ const provider = view.provider;
183
+ if (provider.retries + provider.stalls + provider.errors.length > 0) {
184
+ lines.push(` the model provider: ${provider.retries} retries, ${provider.stalls} stalls${provider.errors.length > 0 ? `, ${provider.errors.length} errors - last: ${provider.errors.at(-1)}` : ""}`);
185
+ }
186
+ if (view.refused.size > 0)
187
+ lines.push(` turri refused calls: ${[...view.refused].map(([rule, times]) => `${rule} ${times}x`).join(", ")}`);
188
+ if (view.stopped.length > 0)
189
+ lines.push(` turri stopped ${view.stopped.length} turn${view.stopped.length === 1 ? "" : "s"} - last: ${view.stopped.at(-1)}`);
190
+ for (const event of view.history) {
191
+ if (event.kind === "history.committed")
192
+ lines.push(` committed: ${event.subject}`);
193
+ else if (event.kind === "history.failed")
194
+ lines.push(` a commit FAILED${event.landing ? " (landing work)" : ""}: ${event.said}`);
195
+ else if (event.kind === "history.reset")
196
+ lines.push(` put back to the last commit: ${event.undone} change${event.undone === 1 ? "" : "s"} undone`);
197
+ else
198
+ lines.push(` rolled back: ${event.why}`);
199
+ }
200
+ if (view.stages.length > 0)
201
+ lines.push(" last stages:", ...view.stages.slice(-8).map((stage) => ` ${stage}`));
202
+ const log = view.finished?.logFile ?? view.started?.logFile;
203
+ if (log !== undefined)
204
+ lines.push(` log: ${log}`);
205
+ return lines.join("\n");
206
+ }
207
+ /** A pulse the ledger of any of these projects knows, described - or undefined. */
208
+ export function describePastPulse(projects, id) {
209
+ for (const project of projects) {
210
+ const entries = readLedger(project);
211
+ const view = pulseView(id, entries);
212
+ if (view !== undefined)
213
+ return describePulseView(view);
214
+ }
215
+ return undefined;
216
+ }
217
+ export function runViews(project) {
218
+ const entries = readLedger(project);
219
+ const runs = new Map();
220
+ for (const entry of entries)
221
+ runs.set(entry.run, [...(runs.get(entry.run) ?? []), entry]);
222
+ return [...runs]
223
+ .map(([run, own]) => {
224
+ const ids = [...new Set(own.filter((entry) => entry.kind === "job.queued").map((entry) => entry.job))];
225
+ const started = own.find((entry) => entry.kind === "run.started");
226
+ const ended = own.find((entry) => entry.kind === "run.ended");
227
+ return { run, ...(started !== undefined ? { started } : {}), ...(ended !== undefined ? { ended } : {}), pulses: ids.map((id) => pulseView(id, own)).filter(Boolean) };
228
+ })
229
+ .reverse();
230
+ }
231
+ /** The project's last runs, a line a pulse: what `turri ledger` prints. */
232
+ export function describeRuns(runs) {
233
+ if (runs.length === 0)
234
+ return "The ledger is empty: nothing has run here since it began.";
235
+ return runs
236
+ .map((view) => {
237
+ const started = view.started;
238
+ const when = started?.ts !== undefined ? started.ts.slice(0, 19).replace("T", " ") : "";
239
+ const version = started?.version !== undefined && started.version !== null ? ` · turri ${started.version}${started.commit !== null && started.commit !== undefined ? ` (${started.commit.slice(0, 7)}${started.dirty === true ? ", changed" : ""})` : ""}` : "";
240
+ const head = `run ${view.run} · ${started?.mode ?? "?"} · ${when}${view.ended !== undefined ? ` · ${formatElapsed(view.ended.ms)}` : " · still running, or killed"}${version}`;
241
+ const pulses = view.pulses.map((pulse) => ` ${pulse.id} ${nameOf(pulse).padEnd(32)} ${outcomeOf(pulse)}${pulse.usage !== undefined && describeUsage(pulse.usage) !== "" ? ` · ${describeUsage(pulse.usage)}` : ""}`);
242
+ return [head, ...(pulses.length > 0 ? pulses : [" no pulses"])].join("\n");
243
+ })
244
+ .join("\n\n");
245
+ }
246
+ /**
247
+ * The pulses of earlier sessions, for a list beside this session's: the last `runs` runs of these projects that are not
248
+ * this one, the most recent first, each pulse as a job of its own marked `earlier` - none `known` to this session's
249
+ * registry. `more`: how many runs the ledger has beyond them.
250
+ */
251
+ export function earlierPulses(projects, known, runs = 3) {
252
+ const own = ledgerRun();
253
+ const all = projects
254
+ .flatMap((project) => runViews(project))
255
+ .filter((view) => view.run !== own && view.pulses.some((pulse) => !known.has(pulse.id)))
256
+ .sort((a, b) => (b.started?.ts ?? "").localeCompare(a.started?.ts ?? ""));
257
+ const shown = all.slice(0, runs);
258
+ return { jobs: shown.flatMap((view) => view.pulses.filter((pulse) => !known.has(pulse.id)).map(asJob)), runs: shown, more: all.length - shown.length };
259
+ }
260
+ /** A pulse of the ledger's, as the lists show a job: finished as it ended, its time and cost - and nothing to run. */
261
+ export function asJob(view) {
262
+ const at = (entry) => (entry?.ts !== undefined ? Date.parse(entry.ts) : undefined);
263
+ const queuedAt = at(view.queued) ?? 0;
264
+ const startedAt = at(view.started) ?? queuedAt;
265
+ const finished = view.finished;
266
+ // A pulse whose run ended before it did never finished: it was stopped with its run.
267
+ const status = finished !== undefined && ["succeeded", "failed", "stopped"].includes(finished.status) ? finished.status : "stopped";
268
+ const logFile = finished?.logFile ?? view.started?.logFile;
269
+ return {
270
+ id: view.id,
271
+ kind: view.queued?.jobKind ?? "regenerate",
272
+ summary: view.queued?.summary ?? view.id,
273
+ ...(view.queued?.module !== undefined ? { module: view.queued.module } : {}),
274
+ ...(view.queued?.pulse !== undefined ? { pulse: view.queued.pulse } : {}),
275
+ ...(view.queued?.title !== undefined ? { title: view.queued.title } : {}),
276
+ ...(view.queued?.model !== undefined ? { model: view.queued.model } : {}),
277
+ status,
278
+ sequence: 0,
279
+ queuedAt,
280
+ startedAt,
281
+ finishedAt: finished !== undefined ? startedAt + finished.ms : startedAt,
282
+ waitingOn: [],
283
+ tail: "",
284
+ activity: [],
285
+ usage: view.usage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 },
286
+ agents: [],
287
+ transcript: [],
288
+ ...(logFile !== undefined ? { logFile } : {}),
289
+ ...(finished?.error !== undefined ? { error: finished.error } : {}),
290
+ details: { ...(finished?.landed !== undefined ? { landed: finished.landed } : {}), ...(finished?.passed !== undefined ? { passed: finished.passed } : {}) },
291
+ earlier: { run: view.run, ...(view.runMode !== undefined ? { runMode: view.runMode } : {}), lines: describePulseView(view).split("\n").slice(1).map((line) => line.replace(/^ /, "")) },
292
+ };
293
+ }
294
+ /** The envelope's fields, said apart from an event's own. */
295
+ const ENVELOPE = new Set(["v", "ts", "seq", "run", "job", "module", "agent", "kind"]);
296
+ /** One event in a line: when, its pulse and module, its kind, its own fields - the agent's role when it has one. */
297
+ export function eventLine(entry) {
298
+ const fields = Object.entries(entry)
299
+ .filter(([key]) => !ENVELOPE.has(key))
300
+ .map(([key, value]) => {
301
+ const text = typeof value === "string" ? JSON.stringify(value.length > 160 ? `${value.slice(0, 159)}…` : value) : JSON.stringify(value);
302
+ return `${key}=${text}`;
303
+ });
304
+ const where = [entry.job, entry.module, entry.agent?.role ?? entry.agent?.label].filter((part) => part !== undefined);
305
+ return [entry.ts.slice(0, 19).replace("T", " "), ...where, entry.kind, ...fields].join(" · ");
306
+ }
307
+ /**
308
+ * The events of these projects' ledgers that `query` asks for: how many of each kind, and the most recent, a line each -
309
+ * or, when no kind asked for is in the ledger, the kinds it has.
310
+ */
311
+ export function describeEvents(projects, query = {}) {
312
+ const all = projects.flatMap((project) => readLedger(project, query.since !== undefined ? { since: query.since } : {}));
313
+ const inModule = query.module !== undefined ? all.filter((entry) => entry.module === query.module) : all;
314
+ const wanted = query.kinds?.map((kind) => kind.trim()).filter((kind) => kind !== "");
315
+ const matches = (kind) => wanted === undefined || wanted.length === 0 || wanted.some((asked) => kind === asked || kind.startsWith(`${asked}.`));
316
+ const found = inModule.filter((entry) => matches(entry.kind));
317
+ const scope = [query.module !== undefined ? `in ${query.module}` : "", query.since !== undefined ? `since ${query.since.toISOString().slice(0, 16).replace("T", " ")}` : ""].filter(Boolean).join(", ");
318
+ if (found.length === 0) {
319
+ const kinds = [...new Set(inModule.map((entry) => entry.kind))].sort();
320
+ return `No ${wanted !== undefined && wanted.length > 0 ? wanted.join(", ") : ""} events${scope !== "" ? ` ${scope}` : ""} in the ledger.${kinds.length > 0 ? ` It has: ${kinds.join(", ")}.` : ""}`;
321
+ }
322
+ const counts = new Map();
323
+ for (const entry of found)
324
+ counts.set(entry.kind, (counts.get(entry.kind) ?? 0) + 1);
325
+ const pulses = new Set(found.map((entry) => entry.job).filter((job) => job !== undefined)).size;
326
+ const runs = new Set(found.map((entry) => entry.run)).size;
327
+ const limit = Math.max(1, query.limit ?? 20);
328
+ const recent = found.slice(-limit);
329
+ return [
330
+ `${found.length} event${found.length === 1 ? "" : "s"}${scope !== "" ? ` ${scope}` : ""}, in ${runs} run${runs === 1 ? "" : "s"} and ${pulses} pulse${pulses === 1 ? "" : "s"}: ${[...counts].sort((a, b) => b[1] - a[1]).map(([kind, times]) => `${kind} ${times}`).join(", ")}`,
331
+ "",
332
+ recent.length === found.length ? "Each, oldest first:" : `The last ${recent.length}, oldest first:`,
333
+ ...recent.map(eventLine),
334
+ ].join("\n");
335
+ }
336
+ /** `24h`, `7d`, `30m`, or a date and time: the moment it means, or undefined when it means none. */
337
+ export function sinceOf(text, now = Date.now()) {
338
+ const relative = /^(\d+)\s*([mhd])$/i.exec(text.trim());
339
+ if (relative !== null) {
340
+ const unit = { m: 60_000, h: 3_600_000, d: 86_400_000 }[relative[2].toLowerCase()];
341
+ return new Date(now - Number(relative[1]) * unit);
342
+ }
343
+ const date = new Date(text.trim());
344
+ return Number.isNaN(date.getTime()) ? undefined : date;
345
+ }
@@ -0,0 +1,275 @@
1
+ import { type UsageReport } from "./activity.ts";
2
+ /**
3
+ * The ledger: what turri does, as typed facts, one JSON line each, written as it happens - to
4
+ * `.turri/ledger/` in the project, one file a run (a conversation, a headless run, a host's session).
5
+ *
6
+ * The job and debug logs keep the bodies - every prompt, reply and tool result, whole - for reading one
7
+ * pulse closely. The ledger keeps the facts about them, across pulses, sessions and runs: that a pulse
8
+ * was queued, started, went through its stages and ended, and how; what each agent's model calls and
9
+ * turns cost; each tool call and test run, how long it took and how it ended; the gates' rounds and
10
+ * verdicts; what turri refused or stopped; what its healers did; the history's commits. What a pulse
11
+ * from an earlier session did is looked up here (`pulses`, `diagnose`, `turri ledger`). Never stored:
12
+ * prompts, replies, thinking, tool arguments and results, file contents - and what strings an event
13
+ * carries are masked as the logs are.
14
+ *
15
+ * A host hears each event as it is recorded (`onLedgerEvent`), with the project it is about, rather than reading the
16
+ * file again - written or not, so a project that keeps no ledger is heard all the same.
17
+ *
18
+ * Observation, never control: turri may react to events as they happen, but never reads the ledger on
19
+ * disk to decide what to build - the code and tests records are that (records.ts). Deleting
20
+ * `.turri/ledger/` loses only what can be looked up. A ledger that cannot be written stops, and never
21
+ * fails the work.
22
+ *
23
+ * Closed runs are gzipped, and the oldest pruned past `ledger-keep:` megabytes (100 unset; 0 keeps none),
24
+ * when a run first writes to the project. `.turri` ignores itself, so none of it is ever committed.
25
+ */
26
+ export declare const LEDGER_DIR = "ledger";
27
+ /** Megabytes of ledger a project keeps when `ledger-keep:` does not say. */
28
+ export declare const DEFAULT_LEDGER_KEEP_MB = 100;
29
+ /** Who did it: the agent an event is about. */
30
+ export interface AgentRef {
31
+ role?: string;
32
+ label?: string;
33
+ model?: string;
34
+ }
35
+ /** Every event's own fields, by kind. */
36
+ export type LedgerEvent = {
37
+ kind: "run.started";
38
+ mode: string;
39
+ version: string | null;
40
+ commit: string | null;
41
+ dirty: boolean | null;
42
+ pid: number;
43
+ platform: string;
44
+ node: string;
45
+ } | {
46
+ kind: "run.ended";
47
+ ms: number;
48
+ } | {
49
+ kind: "job.queued";
50
+ jobKind: string;
51
+ summary: string;
52
+ pulse?: number;
53
+ title?: string;
54
+ model?: string;
55
+ stack?: string;
56
+ waitingOn: string[];
57
+ follows?: string;
58
+ } | {
59
+ kind: "job.started";
60
+ waitedMs: number;
61
+ logFile?: string;
62
+ } | {
63
+ kind: "job.stage";
64
+ label: string;
65
+ } | {
66
+ kind: "job.retried";
67
+ attempt: number;
68
+ of: number;
69
+ failure: string;
70
+ } | {
71
+ kind: "job.finished";
72
+ status: string;
73
+ landed?: boolean;
74
+ passed?: boolean;
75
+ ms: number;
76
+ usage: UsageReport;
77
+ failedForGood?: boolean;
78
+ /** Who stopped it, for one stopped: the person, or turri itself, and why. */
79
+ stoppedBy?: "person" | "turri";
80
+ error?: string;
81
+ logFile?: string;
82
+ } | {
83
+ kind: "loop.halted";
84
+ why: string;
85
+ track: "code" | "conformance";
86
+ } | {
87
+ kind: "loop.stopped";
88
+ why: string;
89
+ } | {
90
+ kind: "planner.failed";
91
+ step: "group" | "place";
92
+ error: string;
93
+ } | {
94
+ kind: "planner.fellBack";
95
+ step: "group" | "place";
96
+ why: string;
97
+ } | {
98
+ kind: "model.call";
99
+ ms?: number;
100
+ usage?: UsageReport;
101
+ } | {
102
+ kind: "model.turn";
103
+ } | {
104
+ kind: "model.compaction";
105
+ } | {
106
+ kind: "provider.retry";
107
+ error: string;
108
+ attempt: number;
109
+ of: number;
110
+ waitMs: number;
111
+ } | {
112
+ kind: "provider.stall";
113
+ waitMs: number;
114
+ } | {
115
+ kind: "provider.error";
116
+ error: string;
117
+ transient: boolean;
118
+ } | {
119
+ kind: "turn.stopped";
120
+ reason: string;
121
+ } | {
122
+ kind: "tool.call";
123
+ tool: string;
124
+ ms: number;
125
+ ok: boolean;
126
+ resultChars: number;
127
+ } | {
128
+ kind: "script.run";
129
+ script: string;
130
+ exit: number;
131
+ ms: number;
132
+ timedOut: boolean;
133
+ inPlace: boolean;
134
+ mutated?: boolean;
135
+ signature?: string;
136
+ } | {
137
+ kind: "gate.round";
138
+ gate: "unit tests" | "conformance" | "regression";
139
+ round: number;
140
+ of: number;
141
+ signature?: string;
142
+ } | {
143
+ kind: "review.verdict";
144
+ reviewer: "unit";
145
+ review: number;
146
+ pass: boolean;
147
+ problems: number;
148
+ } | {
149
+ kind: "arbiter.ruling";
150
+ upheld: number;
151
+ dismissed: number;
152
+ } | {
153
+ kind: "judge.verdict";
154
+ culprit: string;
155
+ round: number;
156
+ reason: string;
157
+ } | {
158
+ kind: "guard.refused";
159
+ rule: "repeat" | "budget" | "restricted";
160
+ tool: string;
161
+ times: number;
162
+ } | {
163
+ kind: "command.refused";
164
+ rule: string;
165
+ } | {
166
+ kind: "audit.putBack";
167
+ putBack: number;
168
+ left: number;
169
+ lost: number;
170
+ conversation: number;
171
+ } | {
172
+ kind: "healer.started";
173
+ healer: string;
174
+ } | {
175
+ kind: "healer.ended";
176
+ healer: string;
177
+ ms: number;
178
+ found?: string;
179
+ } | {
180
+ kind: "healer.failed";
181
+ healer: string;
182
+ ms: number;
183
+ error: string;
184
+ } | {
185
+ kind: "history.committed";
186
+ subject: string;
187
+ commit?: string;
188
+ } | {
189
+ kind: "history.failed";
190
+ subject: string;
191
+ said: string;
192
+ landing: boolean;
193
+ } | {
194
+ kind: "history.reset";
195
+ undone: number;
196
+ paths: string[];
197
+ } | {
198
+ kind: "history.rolledBack";
199
+ why: string;
200
+ } | {
201
+ kind: "environment.error";
202
+ missing: string;
203
+ source: string;
204
+ sandbox: boolean;
205
+ };
206
+ export type LedgerKind = LedgerEvent["kind"];
207
+ /** One line of the ledger: the event, and where and when it happened. */
208
+ export type LedgerEntry = LedgerEvent & {
209
+ v: 1;
210
+ ts: string;
211
+ seq: number;
212
+ run: string;
213
+ job?: string;
214
+ module?: string;
215
+ agent?: AgentRef;
216
+ };
217
+ /** Where an event happened, when not where the code recording it runs: a job's own place is found by itself. */
218
+ export interface LedgerPlace {
219
+ project?: string;
220
+ job?: string;
221
+ module?: string;
222
+ agent?: AgentRef;
223
+ }
224
+ /**
225
+ * Hear every event from now on, as it is recorded, with the project it is about - while the ledger is on. Returns
226
+ * what stops it. A listener that throws is left out of that event, never the work.
227
+ */
228
+ export declare function onLedgerEvent(listener: (entry: LedgerEntry, project: string) => void): () => void;
229
+ /** Whether turri turns the ledger on as it starts: always, unless TURRI_LEDGER=off. */
230
+ export declare function ledgerWanted(): boolean;
231
+ /** This run's id, while the ledger is on. */
232
+ export declare function ledgerRun(): string | undefined;
233
+ /**
234
+ * Start the ledger for this process: from now on, what is recorded is written. `project` is the one events about no
235
+ * project of their own go to. `warn` is told, once a project, when its ledger cannot be written. Returns what stops it.
236
+ */
237
+ export declare function startLedger(options: {
238
+ mode: string;
239
+ project?: string;
240
+ warn?: (message: string) => void;
241
+ }): () => void;
242
+ /** Stop the ledger: each project this run wrote to is told it ended. */
243
+ export declare function stopLedger(): void;
244
+ /** Record an event: where it happened is the job this code runs for, unless `place` says. Nothing while the ledger is off. */
245
+ export declare function record(event: LedgerEvent, place?: LedgerPlace): void;
246
+ /**
247
+ * Run a healer - the debugger, the diagnosis of a failed pulse - and record it: started, ended with what it found, or
248
+ * failed and why. A healer that fails gives undefined, as it did before it was recorded, but is no longer silent.
249
+ */
250
+ export declare function healing<T>(healer: string, work: () => Promise<T>, found?: (result: T) => string | undefined): Promise<T | undefined>;
251
+ /** A failure's fingerprint, short: two runs failing the same way have the same one. */
252
+ export declare function signatureHash(signature: string): string;
253
+ /** The first line of a text, at most `max` characters: what an event keeps of an error or a reason. */
254
+ export declare function firstLine(text: string, max?: number): string;
255
+ /** The project's ledger folder. */
256
+ export declare function ledgerDir(project: string): string;
257
+ /**
258
+ * Keep the ledger folder within `keepBytes`: the files of runs that have ended gzipped, then the oldest pruned until
259
+ * the rest fit. A run still going - this one, or another turri's in the same project - is never touched.
260
+ */
261
+ export declare function tidy(dir: string, keepBytes: number): void;
262
+ /** What to read of the ledger. */
263
+ export interface LedgerQuery {
264
+ /** A pulse, by its id or the start of it. */
265
+ job?: string;
266
+ run?: string;
267
+ kinds?: LedgerKind[];
268
+ /** Only events at or after this time. */
269
+ since?: Date;
270
+ }
271
+ /**
272
+ * The project's ledger, oldest first - every run's, the gzipped ones too, as `query` narrows it. A line that does not
273
+ * parse, or a kind this turri does not know, is skipped: the ledger is read as it is, whatever wrote it.
274
+ */
275
+ export declare function readLedger(project: string, query?: LedgerQuery): LedgerEntry[];