@turri-dev/cli 0.1.8-next.e553476 → 0.1.8

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 (66) hide show
  1. package/README.md +1 -3
  2. package/dist/cli.js +6 -2
  3. package/dist/config-edit.d.ts +1 -1
  4. package/dist/config-edit.js +4 -0
  5. package/dist/config.d.ts +27 -0
  6. package/dist/config.js +28 -1
  7. package/dist/extension.js +75 -9
  8. package/dist/genome.js +2 -0
  9. package/dist/host.d.ts +3 -1
  10. package/dist/host.js +4 -1
  11. package/dist/init-config.d.ts +3 -0
  12. package/dist/init-config.js +15 -0
  13. package/dist/ledger-cli.d.ts +1 -0
  14. package/dist/ledger-cli.js +50 -0
  15. package/dist/ledger-tools.d.ts +10 -0
  16. package/dist/ledger-tools.js +64 -0
  17. package/dist/loop-cli.js +4 -0
  18. package/dist/pi.d.ts +2 -0
  19. package/dist/pi.js +24 -4
  20. package/dist/regenerate/activity.d.ts +9 -0
  21. package/dist/regenerate/activity.js +11 -0
  22. package/dist/regenerate/agent-env.d.ts +7 -0
  23. package/dist/regenerate/agent-env.js +126 -5
  24. package/dist/regenerate/agent-shell.js +5 -0
  25. package/dist/regenerate/chunk-plan.d.ts +5 -6
  26. package/dist/regenerate/chunk-plan.js +43 -10
  27. package/dist/regenerate/conformance-agents.js +2 -2
  28. package/dist/regenerate/conformance.js +6 -3
  29. package/dist/regenerate/debug-log.d.ts +3 -1
  30. package/dist/regenerate/debug-log.js +10 -2
  31. package/dist/regenerate/diagnose.js +12 -2
  32. package/dist/regenerate/environment-error.d.ts +6 -0
  33. package/dist/regenerate/environment-error.js +19 -1
  34. package/dist/regenerate/job-log.d.ts +3 -1
  35. package/dist/regenerate/job-log.js +8 -5
  36. package/dist/regenerate/jobs-tree.d.ts +7 -3
  37. package/dist/regenerate/jobs-tree.js +21 -6
  38. package/dist/regenerate/jobs.d.ts +9 -0
  39. package/dist/regenerate/jobs.js +51 -5
  40. package/dist/regenerate/ledger-view.d.ts +92 -0
  41. package/dist/regenerate/ledger-view.js +345 -0
  42. package/dist/regenerate/ledger.d.ts +275 -0
  43. package/dist/regenerate/ledger.js +349 -0
  44. package/dist/regenerate/lineage.d.ts +9 -1
  45. package/dist/regenerate/lineage.js +64 -12
  46. package/dist/regenerate/loop.js +12 -3
  47. package/dist/regenerate/pulse-files.d.ts +14 -0
  48. package/dist/regenerate/pulse-files.js +63 -0
  49. package/dist/regenerate/regenerate.js +7 -5
  50. package/dist/regenerate/regression.js +4 -2
  51. package/dist/regenerate/services.js +3 -0
  52. package/dist/regenerate/side-panel.d.ts +2 -0
  53. package/dist/regenerate/side-panel.js +4 -3
  54. package/dist/regenerate/test-scripts.js +18 -5
  55. package/dist/regenerate/testing.js +3 -0
  56. package/dist/regenerate/unit-review.js +3 -0
  57. package/dist/run-report.js +9 -5
  58. package/docs/config.md +23 -2
  59. package/docs/development.md +3 -1
  60. package/docs/headless.md +5 -4
  61. package/docs/hosting.md +29 -0
  62. package/docs/installation.md +1 -0
  63. package/docs/regenerating.md +7 -7
  64. package/docs/sandbox.md +5 -5
  65. package/docs/using.md +36 -5
  66. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { defineTool } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
  import { ask, BEGIN, createExecutor } from "../pi.js";
4
+ import { debugMode } from "../config.js";
4
5
  import { confinedTools } from "./confine.js";
5
6
  /** How many tool calls the diagnosis may make before it is told to decide. */
6
7
  export const DIAGNOSIS_CALLS = 40;
@@ -49,8 +50,10 @@ export async function diagnoseFailure(options) {
49
50
  if (diagnosis === undefined)
50
51
  await ask(session, "You have not called submit_diagnosis. Call it now, with what you know.");
51
52
  }
52
- catch {
53
- // Without a diagnosis the person still gets both failures, as they were.
53
+ catch (error) {
54
+ // Without a diagnosis the person still gets both failures, as they were - and the ledger why there is none.
55
+ if (diagnosis === undefined)
56
+ throw error;
54
57
  }
55
58
  finally {
56
59
  session.dispose();
@@ -75,6 +78,13 @@ export function diagnosisPrompt(options) {
75
78
  "Find the root cause, not the last symptom: read the failing run's output and the agents' logs around where it",
76
79
  "failed, and follow it to the spec line, the code, the test, the environment or the harness it comes from. Then",
77
80
  "call submit_diagnosis: what happened, why, whose it is to fix, and exactly what to do.",
81
+ ...(debugMode(options.projectDir)
82
+ ? []
83
+ : [
84
+ "",
85
+ "The logs and the full outputs are removed as the job ends - the project is not in debug mode - so never send",
86
+ "the person to one of them: quote what in them shows the cause.",
87
+ ]),
78
88
  "",
79
89
  ...options.failures.flatMap((failure, index) => [`<attempt ${index + 1}>`, failure.trim().slice(-12_000), `</attempt ${index + 1}>`, ""]),
80
90
  ].join("\n");
@@ -128,6 +128,12 @@ export declare function missingToolchainError(run: {
128
128
  script: string;
129
129
  kind: "unit" | "conformance";
130
130
  }): EnvironmentError | undefined;
131
+ /**
132
+ * Work that passed could not be committed into the module's history: the next pulse, putting the module back to its last
133
+ * commit, would undo it while turri took it as built - so the pulse does not land, and is rolled back like any other that
134
+ * did not, and the person is told why. `said` is what git said.
135
+ */
136
+ export declare function historyError(moduleDir: string, said: string): EnvironmentError;
131
137
  /**
132
138
  * Before a pulse in `moduleDir` starts any agent: a credential `agent-env:` names that is not there stops it - every
133
139
  * command that needs it would fail, and no agent may make one up. Nothing when all are there, or the folder is no
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { basename, dirname, isAbsolute, sep } from "node:path";
3
+ import { basename, dirname, isAbsolute, join, sep } from "node:path";
4
4
  import { defineTool } from "@earendil-works/pi-coding-agent";
5
5
  import { Type } from "typebox";
6
6
  import { passedCredentials } from "./agent-env.js";
@@ -246,6 +246,24 @@ export function missingToolchainError(run, details) {
246
246
  const missing = missingToolchain(run);
247
247
  return missing !== undefined ? toolchainError(missing, details) : undefined;
248
248
  }
249
+ /**
250
+ * Work that passed could not be committed into the module's history: the next pulse, putting the module back to its last
251
+ * commit, would undo it while turri took it as built - so the pulse does not land, and is rolled back like any other that
252
+ * did not, and the person is told why. `said` is what git said.
253
+ */
254
+ export function historyError(moduleDir, said) {
255
+ const lock = /index\.lock/.test(said);
256
+ return new EnvironmentError({
257
+ moduleName: moduleNameOf(moduleDir),
258
+ missing: `a commit into the module's history, \`${join(moduleDir, ".plaingit")}\``,
259
+ neededBy: "the work that passed - without the commit, the next pulse would undo it",
260
+ shownBy: `git said: ${said.replace(/\s+/g, " ").trim().slice(0, 600)}`,
261
+ fix: lock
262
+ ? `if no git is still running on it, remove \`${join(moduleDir, ".plaingit", "index.lock")}\`, which a git stopped mid-commit left behind.`
263
+ : "fix what git said - a full disk, files it cannot read - so a commit can be made there.",
264
+ source: "harness",
265
+ });
266
+ }
249
267
  /** The module a folder is part of, by name: `plain_modules/<module>/...`. */
250
268
  function moduleNameOf(moduleDir) {
251
269
  return basename(moduleDir);
@@ -17,7 +17,9 @@ export interface JobLog {
17
17
  /** `agents`: a line per agent - its turns, compactions and cost. */
18
18
  end(status: string, report: string | undefined, agents?: string[]): void;
19
19
  }
20
- /** The folder a job's log goes in: the project of the first module it writes, from its lanes. */
20
+ /** The project a job works in: the one of the first module it writes, from its lanes. */
21
+ export declare function projectOfLanes(lanes: string[]): string | undefined;
22
+ /** The folder a job's log goes in: `.turri/jobs` in its project. */
21
23
  export declare function jobLogDir(lanes: string[]): string | undefined;
22
24
  export declare function startJobLog(job: {
23
25
  id: string;
@@ -2,12 +2,15 @@ import { appendFileSync, existsSync } from "node:fs";
2
2
  import { maskSecrets } from "./masking.js";
3
3
  import { dirname, isAbsolute, join, sep } from "node:path";
4
4
  import { ensureInScratch, MODULES_DIR, projectRootOf, scratchDir } from "./paths.js";
5
- /** The folder a job's log goes in: the project of the first module it writes, from its lanes. */
6
- export function jobLogDir(lanes) {
5
+ /** The project a job works in: the one of the first module it writes, from its lanes. */
6
+ export function projectOfLanes(lanes) {
7
7
  const lane = lanes.find((candidate) => isAbsolute(candidate) && candidate.split(sep).includes(MODULES_DIR));
8
- if (lane === undefined)
9
- return undefined;
10
- return join(scratchDir(join(projectRootOf(lane), MODULES_DIR)), "jobs");
8
+ return lane !== undefined ? projectRootOf(lane) : undefined;
9
+ }
10
+ /** The folder a job's log goes in: `.turri/jobs` in its project. */
11
+ export function jobLogDir(lanes) {
12
+ const project = projectOfLanes(lanes);
13
+ return project !== undefined ? join(scratchDir(join(project, MODULES_DIR)), "jobs") : undefined;
11
14
  }
12
15
  export function startJobLog(job, lanes) {
13
16
  const dir = jobLogDir(lanes);
@@ -3,15 +3,17 @@ import type { TranscriptStyle } from "./transcript-view.ts";
3
3
  /**
4
4
  * The side panel's Jobs view: the queue as a tree, read one job at a time.
5
5
  *
6
- * Three sections - running, queued, finished - each opened and closed on its own, and under them one line per job:
6
+ * Four sections - running, queued, finished, and earlier sessions' from the ledger - each opened and closed on its own,
7
+ * and under them one line per job:
7
8
  * its module, its time and its cost. A job opened shows all of what it builds - the diff, or the spec - and, running,
8
- * what it is doing; finished, what went on in it. So what is planned can be read without every diff on the screen at once.
9
+ * what it is doing; finished, what went on in it; earlier, what the ledger says of it. So what is planned can be read
10
+ * without every diff on the screen at once.
9
11
  *
10
12
  * A cursor moves over the sections and the jobs; it stays on its row, whatever moves under it - a job that finishes
11
13
  * leaves its row to the one after it. A job opened stays open while it stays where it is: moving to another section
12
14
  * it closes, and turns up there collapsed. The state is the conversation's, kept while the panel is closed.
13
15
  */
14
- export type JobSection = "running" | "queued" | "finished";
16
+ export type JobSection = "running" | "queued" | "finished" | "earlier";
15
17
  export declare const SECTIONS: JobSection[];
16
18
  export interface JobsTreeState {
17
19
  /** Which sections are open. */
@@ -24,6 +26,8 @@ export interface JobsTreeState {
24
26
  cursor: number;
25
27
  /** The first line shown. */
26
28
  top: number;
29
+ /** How many runs the ledger has beyond the earlier sessions shown: said under them. */
30
+ earlierMore?: number;
27
31
  }
28
32
  export declare function createJobsTreeState(): JobsTreeState;
29
33
  export declare function sectionOf(job: Job): JobSection;
@@ -3,11 +3,15 @@ import { describeAgents } from "./activity.js";
3
3
  import { didNotPass, finishedMark, pulseCost, spinner } from "./job-panel.js";
4
4
  import { formatDuration, pulseName } from "./jobs.js";
5
5
  import { diffFiles, fileHeader, PLAIN_PAINT } from "./diff-view.js";
6
- export const SECTIONS = ["running", "queued", "finished"];
6
+ export const SECTIONS = ["running", "queued", "finished", "earlier"];
7
+ /** A section as its header says it. */
8
+ const SECTION_NAMES = { running: "running", queued: "queued", finished: "finished", earlier: "earlier sessions" };
7
9
  export function createJobsTreeState() {
8
- return { open: { running: true, queued: true, finished: false }, openJobs: new Map(), openFiles: new Map(), cursor: 0, top: 0 };
10
+ return { open: { running: true, queued: true, finished: false, earlier: false }, openJobs: new Map(), openFiles: new Map(), cursor: 0, top: 0 };
9
11
  }
10
12
  export function sectionOf(job) {
13
+ if (job.earlier !== undefined)
14
+ return "earlier";
11
15
  return job.status === "running" ? "running" : job.status === "queued" ? "queued" : "finished";
12
16
  }
13
17
  /** The files of a job's diff, in the order they are shown - read once for each diff, not on every frame. */
@@ -47,7 +51,7 @@ export function selectJob(jobs, state, id) {
47
51
  const job = jobs.find((entry) => entry.id === id);
48
52
  if (job === undefined)
49
53
  return false;
50
- const section = job.status === "running" ? "running" : job.status === "queued" ? "queued" : "finished";
54
+ const section = sectionOf(job);
51
55
  state.open[section] = true;
52
56
  state.openJobs.set(id, section);
53
57
  const at = jobsTreeRows(jobs, state).findIndex((row) => row.kind === "job" && row.job.id === id);
@@ -60,13 +64,16 @@ export function jobsTreeRows(jobs, state) {
60
64
  const bySequence = jobs.filter((job) => job.replacedBy === undefined).sort((a, b) => a.sequence - b.sequence);
61
65
  const of = (section) => {
62
66
  const inSection = bySequence.filter((job) => sectionOf(job) === section);
63
- return section === "finished" ? inSection.sort((a, b) => (b.finishedAt ?? b.sequence) - (a.finishedAt ?? a.sequence) || b.sequence - a.sequence) : inSection;
67
+ return section === "finished" || section === "earlier" ? inSection.sort((a, b) => (b.finishedAt ?? b.sequence) - (a.finishedAt ?? a.sequence) || b.sequence - a.sequence) : inSection;
64
68
  };
65
69
  // Forget what is open for jobs that moved on - they close as they leave.
66
70
  for (const job of jobs)
67
71
  isOpen(state, job);
68
72
  return SECTIONS.flatMap((section) => {
69
73
  const inSection = of(section);
74
+ // Earlier sessions are there only when the ledger has some.
75
+ if (section === "earlier" && inSection.length === 0)
76
+ return [];
70
77
  const jobRows = (job) => [
71
78
  { kind: "job", section, job },
72
79
  // An open job's diff, a row a file: each opens and closes on its own.
@@ -82,7 +89,8 @@ export function jobsTreeRows(jobs, state) {
82
89
  const MARK = { running: spinner, queued: () => "·", succeeded: () => "✓", failed: () => "✗", stopped: () => "■" };
83
90
  /** A pulse's mark: running or queued as it is; finished, in the colour of how it ended - green it passed, red it failed or did not pass, stopped amber. */
84
91
  function markOf(job, style) {
85
- if (sectionOf(job) !== "finished")
92
+ const section = sectionOf(job);
93
+ if (section !== "finished" && section !== "earlier")
86
94
  return MARK[job.status]?.(Date.now()) ?? "·";
87
95
  return finishedMark(job, { success: style.success ?? style.added, error: style.error, warning: style.warning ?? style.accent });
88
96
  }
@@ -106,6 +114,9 @@ function named(jobs, id) {
106
114
  /** All of a job, opened: what it builds, and - running - what it is doing; waiting, what it waits on; done, how it ended. */
107
115
  function detailLines(job, jobs, width, style) {
108
116
  const indent = " ";
117
+ // An earlier session's: what the ledger says of it - its transcript is its log's alone.
118
+ if (job.earlier !== undefined)
119
+ return job.earlier.lines.flatMap((line) => wrap(line, width, indent, /FAILED|ended with/.test(line) ? style.error : style.muted));
109
120
  const lines = [];
110
121
  // A diff is shown a file at a time, in rows of their own after these.
111
122
  if ((job.diff === undefined || job.diff.trim() === "") && job.building !== undefined) {
@@ -170,7 +181,7 @@ export function jobsTreeLines(jobs, state, width, style) {
170
181
  lines.push("");
171
182
  rowStarts[index] = lines.length;
172
183
  }
173
- const header = `${pointer} ${state.open[row.section] ? "▾" : "▸"} ${row.section} (${row.count})`;
184
+ const header = `${pointer} ${state.open[row.section] ? "▾" : "▸"} ${SECTION_NAMES[row.section]} (${row.count})`;
174
185
  lines.push(here ? style.heading(header) : style.muted(header));
175
186
  return;
176
187
  }
@@ -197,6 +208,10 @@ export function jobsTreeLines(jobs, state, width, style) {
197
208
  if (open)
198
209
  lines.push(...detailLines(job, jobs, width, style));
199
210
  });
211
+ // The ledger has more than the runs shown: where to find them.
212
+ if (state.open.earlier && (state.earlierMore ?? 0) > 0) {
213
+ lines.push(style.muted(` ${state.earlierMore} earlier run${state.earlierMore === 1 ? "" : "s"} in the ledger - turri ledger lists them`));
214
+ }
200
215
  return { lines, rowStarts, rows };
201
216
  }
202
217
  /** How far a job's files, and their lines, are indented under it. */
@@ -84,6 +84,15 @@ export interface Job {
84
84
  announce?: string;
85
85
  error?: string;
86
86
  details?: unknown;
87
+ /**
88
+ * A pulse of an earlier session, as the ledger recorded it - not this session's registry's: shown, never run, halted or
89
+ * waited for. Its run, how and when that run was started, and what the ledger says of it, a line each.
90
+ */
91
+ earlier?: {
92
+ run: string;
93
+ runMode?: string;
94
+ lines: string[];
95
+ };
87
96
  }
88
97
  export interface JobOutcome {
89
98
  report: string;
@@ -3,8 +3,10 @@ import { availableParallelism } from "node:os";
3
3
  import { withPulse } from "./shell.js";
4
4
  import { describeEnvironmentError, EnvironmentError, missingCredentials } from "./environment-error.js";
5
5
  import { addAgentReport, describeAgents, JobStoppedError, withActivity } from "./activity.js";
6
- import { startJobLog } from "./job-log.js";
7
- import { chosenStack, withStack } from "../config.js";
6
+ import { projectOfLanes, startJobLog } from "./job-log.js";
7
+ import { firstLine, healing, record } from "./ledger.js";
8
+ import { chosenStack, debugMode, withStack } from "../config.js";
9
+ import { pulseEnded, pulseWrote } from "./pulse-files.js";
8
10
  const DEFAULT_TAIL_CHARS = 4000;
9
11
  const ACTIVITY_LINES = 10;
10
12
  /** A long render's full transcript is kept, up to about this much text. */
@@ -88,6 +90,20 @@ export function createJobRegistry(options) {
88
90
  };
89
91
  jobs.set(job.id, job);
90
92
  let transcriptChars = 0;
93
+ // Where what it does is recorded: its project, from the modules it writes.
94
+ const project = projectOfLanes(claimed);
95
+ const place = { job: id, ...(project !== undefined ? { project } : {}), ...(input.module !== undefined ? { module: input.module } : {}) };
96
+ record({
97
+ kind: "job.queued",
98
+ jobKind: input.kind,
99
+ summary: firstLine(input.summary, 300),
100
+ ...(input.pulse !== undefined ? { pulse: input.pulse } : {}),
101
+ ...(input.title !== undefined ? { title: input.title } : {}),
102
+ ...(input.model !== undefined ? { model: input.model } : {}),
103
+ ...(stack !== undefined ? { stack } : {}),
104
+ waitingOn: blockers,
105
+ ...(input.follows !== undefined ? { follows: input.follows } : {}),
106
+ }, place);
91
107
  const changed = () => {
92
108
  // Like onFinish: a display that throws must not take the job down.
93
109
  try {
@@ -105,6 +121,8 @@ export function createJobRegistry(options) {
105
121
  const sink = {
106
122
  signal: controller.signal,
107
123
  job: { id, kind: input.kind, summary: input.summary },
124
+ ...(project !== undefined ? { project } : {}),
125
+ ...(input.module !== undefined ? { module: input.module } : {}),
108
126
  specConflict(conflict) {
109
127
  job.specConflict = conflict;
110
128
  this.transcript({ kind: "stage", text: `the spec contradicts itself: functionalities ${conflict.specs.map((spec) => `${spec.module} ${spec.frid}`).join(" and ")} cannot both hold` });
@@ -112,6 +130,7 @@ export function createJobRegistry(options) {
112
130
  },
113
131
  stage(label) {
114
132
  job.stage = label;
133
+ record({ kind: "job.stage", label: firstLine(label, 300) }, place);
115
134
  this.transcript({ kind: "stage", text: label });
116
135
  changed();
117
136
  },
@@ -213,22 +232,24 @@ export function createJobRegistry(options) {
213
232
  // and the person is told. What the harness found itself is worked out at once; what an agent said goes as it is.
214
233
  if (error instanceof EnvironmentError) {
215
234
  job.failedForGood = true;
235
+ record({ kind: "environment.error", missing: firstLine(error.missing, 300), source: error.source, sandbox: error.sandbox !== undefined }, place);
216
236
  if (error.source === "harness" && options?.diagnose !== undefined) {
217
237
  sink.stage("the environment lacks something - working out what to do about it");
218
- job.diagnosis = await withActivity(sink, () => options.diagnose(job, [error.message, describeEnvironmentError(error, input.rerun)], log?.file)).catch(() => undefined);
238
+ job.diagnosis = await withActivity(sink, () => healing("diagnosis", () => options.diagnose(job, [error.message, describeEnvironmentError(error, input.rerun)], log?.file), (found) => found));
219
239
  }
220
240
  job.environment = describeEnvironmentError(error, input.rerun);
221
241
  throw new Error([job.environment, ...(job.diagnosis !== undefined ? ["", job.diagnosis] : [])].join("\n"));
222
242
  }
223
243
  failures.push(error !== undefined ? (error instanceof Error ? error.message : String(error)) : outcome.report);
224
244
  if (attempt < retries) {
245
+ record({ kind: "job.retried", attempt: attempt + 1, of: retries, failure: firstLine(failures.at(-1), 300) }, place);
225
246
  sink.stage(`the pulse failed - trying it once more (${attempt + 1} of ${retries})`);
226
247
  continue;
227
248
  }
228
249
  job.failedForGood = true;
229
250
  if (options?.diagnose !== undefined) {
230
251
  sink.stage("the pulse failed again - working out why, and what has to be done");
231
- job.diagnosis = await withActivity(sink, () => options.diagnose(job, failures, log?.file)).catch(() => undefined);
252
+ job.diagnosis = await withActivity(sink, () => healing("diagnosis", () => options.diagnose(job, failures, log?.file), (found) => found));
232
253
  }
233
254
  throw new Error([
234
255
  `Failed ${failures.length} time${failures.length === 1 ? "" : "s"}, so it and every pulse queued behind it have stopped.`,
@@ -241,6 +262,8 @@ export function createJobRegistry(options) {
241
262
  };
242
263
  /** It holds one of the run slots, to give back when it ends. */
243
264
  let holding = false;
265
+ /** It started, holding its lanes: what it wrote, and what its chain installed, are its to clear as it ends. */
266
+ let ran = false;
244
267
  const promise = turn
245
268
  .then(async () => {
246
269
  // Stopped while it waited: it never starts.
@@ -275,8 +298,14 @@ export function createJobRegistry(options) {
275
298
  changed();
276
299
  }
277
300
  log = startJobLog(job, claimed);
278
- if (log !== undefined)
301
+ ran = true;
302
+ if (log !== undefined) {
279
303
  job.logFile = log.file;
304
+ pulseWrote(log.file, id);
305
+ }
306
+ // Its log is named only where it is kept: otherwise it goes as the pulse ends.
307
+ const named = log !== undefined && project !== undefined && debugMode(project);
308
+ record({ kind: "job.started", waitedMs: job.startedAt - job.queuedAt, ...(named ? { logFile: log.file } : {}) }, place);
280
309
  // What the job's agents start runs until it ends, and no longer (withPulse) - with the stack it was queued
281
310
  // with, whatever is chosen while it waits or runs.
282
311
  return withStack(stack, () => (guarded ? runGuarded() : runOnce()));
@@ -303,6 +332,23 @@ export function createJobRegistry(options) {
303
332
  }
304
333
  finished.finishedAt = Date.now();
305
334
  log?.end(finished.status === "succeeded" ? "finished" : finished.status === "stopped" ? "stopped" : "finished - failed", finished.announce ?? finished.report ?? finished.error, describeAgents(finished.agents));
335
+ // What it wrote for a person goes now, outside debug mode - its log too, which nothing names from then on.
336
+ if (ran && project !== undefined && !pulseEnded({ job: id, project, ...(input.module !== undefined ? { module: input.module } : {}), lanes: claimed }))
337
+ delete finished.logFile;
338
+ const outcome = finished.details;
339
+ const stopReason = stopReasons.get(id);
340
+ record({
341
+ kind: "job.finished",
342
+ status: finished.status,
343
+ ...(typeof outcome?.landed === "boolean" ? { landed: outcome.landed } : {}),
344
+ ...(typeof outcome?.passed === "boolean" ? { passed: outcome.passed } : {}),
345
+ ms: finished.finishedAt - finished.startedAt,
346
+ usage: finished.usage,
347
+ ...(finished.failedForGood === true ? { failedForGood: true } : {}),
348
+ ...(finished.status === "stopped" ? { stoppedBy: stopReason !== undefined ? "turri" : "person" } : {}),
349
+ ...(finished.error !== undefined ? { error: firstLine(finished.error, 300) } : {}),
350
+ ...(finished.logFile !== undefined ? { logFile: finished.logFile } : {}),
351
+ }, place);
306
352
  // A failing listener must not take the job down with it; the result is
307
353
  // already recorded and can still be collected by asking.
308
354
  try {
@@ -0,0 +1,92 @@
1
+ import { type UsageReport } from "./activity.ts";
2
+ import { type Job } from "./jobs.ts";
3
+ import { type LedgerEntry } from "./ledger.ts";
4
+ /**
5
+ * What the ledger says of pulses, read back: one pulse - from this session or an earlier one - as the chat and
6
+ * `turri ledger` are told it, and the runs a project has had, each with its pulses.
7
+ */
8
+ type Of<K extends LedgerEntry["kind"]> = Extract<LedgerEntry, {
9
+ kind: K;
10
+ }>;
11
+ /** The projects whose ledgers a folder's pulses may be in: its own, and those of the projects in folders under it. */
12
+ export declare function ledgerProjects(cwd: string, depth?: number): string[];
13
+ /** One pulse, as its events tell it. */
14
+ export interface PulseView {
15
+ id: string;
16
+ run: string;
17
+ /** How its run was started - chat, host, regenerate - and when. */
18
+ runMode?: string;
19
+ queued?: Of<"job.queued"> & {
20
+ module?: string;
21
+ };
22
+ started?: Of<"job.started">;
23
+ finished?: Of<"job.finished">;
24
+ stages: string[];
25
+ retried: Array<Of<"job.retried">>;
26
+ rounds: Map<string, {
27
+ round: number;
28
+ of: number;
29
+ }>;
30
+ scripts: Array<Of<"script.run">>;
31
+ judge: Map<string, number>;
32
+ reviews: Array<Of<"review.verdict">>;
33
+ healers: Array<Of<"healer.ended"> | Of<"healer.failed">>;
34
+ provider: {
35
+ retries: number;
36
+ stalls: number;
37
+ errors: string[];
38
+ };
39
+ refused: Map<string, number>;
40
+ stopped: string[];
41
+ history: Array<Of<"history.committed"> | Of<"history.failed"> | Of<"history.reset"> | Of<"history.rolledBack">>;
42
+ environment?: Of<"environment.error">;
43
+ usage?: UsageReport;
44
+ }
45
+ /** A pulse's events, gathered: undefined when the ledger has none of it. `entries` are its run's, oldest first. */
46
+ export declare function pulseView(id: string, entries: LedgerEntry[]): PulseView | undefined;
47
+ /** A pulse from the ledger, for the chat and `turri ledger <pulse>`: what happened in it, as facts. */
48
+ export declare function describePulseView(view: PulseView): string;
49
+ /** A pulse the ledger of any of these projects knows, described - or undefined. */
50
+ export declare function describePastPulse(projects: string[], id: string): string | undefined;
51
+ /** A run of the project's, and its pulses: the most recent first. */
52
+ export interface RunView {
53
+ run: string;
54
+ started?: Of<"run.started">;
55
+ ended?: Of<"run.ended">;
56
+ pulses: PulseView[];
57
+ }
58
+ export declare function runViews(project: string): RunView[];
59
+ /** The project's last runs, a line a pulse: what `turri ledger` prints. */
60
+ export declare function describeRuns(runs: RunView[]): string;
61
+ /**
62
+ * The pulses of earlier sessions, for a list beside this session's: the last `runs` runs of these projects that are not
63
+ * this one, the most recent first, each pulse as a job of its own marked `earlier` - none `known` to this session's
64
+ * registry. `more`: how many runs the ledger has beyond them.
65
+ */
66
+ export declare function earlierPulses(projects: string[], known: Set<string>, runs?: number): {
67
+ jobs: Job[];
68
+ runs: RunView[];
69
+ more: number;
70
+ };
71
+ /** A pulse of the ledger's, as the lists show a job: finished as it ended, its time and cost - and nothing to run. */
72
+ export declare function asJob(view: PulseView): Job;
73
+ /** What to count in the ledger: kinds - whole, or a group by what comes before the dot - a module, since when. */
74
+ export interface EventQuery {
75
+ /** `healer.failed`, or `healer` for every healer event. */
76
+ kinds?: string[];
77
+ module?: string;
78
+ /** Only events at or after this time. */
79
+ since?: Date;
80
+ /** How many of the most recent to list, a line each. */
81
+ limit?: number;
82
+ }
83
+ /** One event in a line: when, its pulse and module, its kind, its own fields - the agent's role when it has one. */
84
+ export declare function eventLine(entry: LedgerEntry): string;
85
+ /**
86
+ * The events of these projects' ledgers that `query` asks for: how many of each kind, and the most recent, a line each -
87
+ * or, when no kind asked for is in the ledger, the kinds it has.
88
+ */
89
+ export declare function describeEvents(projects: string[], query?: EventQuery): string;
90
+ /** `24h`, `7d`, `30m`, or a date and time: the moment it means, or undefined when it means none. */
91
+ export declare function sinceOf(text: string, now?: number): Date | undefined;
92
+ export {};