@turri-dev/cli 0.1.18-next.85984f7 → 0.1.19-next.ee6bc61

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 (42) hide show
  1. package/dist/chat-terminal.js +4 -0
  2. package/dist/chat-waiting.d.ts +64 -0
  3. package/dist/chat-waiting.js +86 -0
  4. package/dist/chats.d.ts +2 -0
  5. package/dist/chats.js +1 -0
  6. package/dist/extension.js +36 -2
  7. package/dist/genome.d.ts +12 -0
  8. package/dist/genome.js +17 -0
  9. package/dist/new-project.js +1 -1
  10. package/dist/pi-launch.js +7 -4
  11. package/dist/regenerate/conformance-agents.d.ts +6 -2
  12. package/dist/regenerate/conformance-agents.js +40 -14
  13. package/dist/regenerate/conformance.d.ts +0 -8
  14. package/dist/regenerate/conformance.js +17 -14
  15. package/dist/regenerate/debugger.d.ts +5 -0
  16. package/dist/regenerate/debugger.js +21 -2
  17. package/dist/regenerate/environment-error.d.ts +25 -0
  18. package/dist/regenerate/environment-error.js +46 -0
  19. package/dist/regenerate/job-panel.d.ts +2 -2
  20. package/dist/regenerate/job-panel.js +12 -11
  21. package/dist/regenerate/jobs-tree.d.ts +1 -1
  22. package/dist/regenerate/jobs-tree.js +5 -6
  23. package/dist/regenerate/jobs.d.ts +4 -0
  24. package/dist/regenerate/jobs.js +24 -0
  25. package/dist/regenerate/ledger-view.d.ts +11 -3
  26. package/dist/regenerate/ledger-view.js +28 -10
  27. package/dist/regenerate/ledger.d.ts +9 -3
  28. package/dist/regenerate/ledger.js +4 -3
  29. package/dist/regenerate/loop.js +3 -4
  30. package/dist/regenerate/regenerate.js +20 -8
  31. package/dist/regenerate/stop-report.d.ts +1 -1
  32. package/dist/regenerate/stop-report.js +1 -15
  33. package/dist/resume-line.d.ts +2 -2
  34. package/dist/resume-line.js +3 -3
  35. package/docs/authoring-plain.md +2 -2
  36. package/docs/chats.md +3 -1
  37. package/docs/hosting.md +2 -2
  38. package/docs/regenerating.md +8 -6
  39. package/docs/using.md +7 -7
  40. package/genome/skills/brood/SKILL.md +1 -13
  41. package/genome/skills/spawn/SKILL.md +11 -16
  42. package/package.json +1 -1
@@ -15,7 +15,8 @@ import { runTiming } from "./test-scripts.js";
15
15
  * this agent: a session of its own - on the `debugger` role's model, else a stronger one than the coding agent's, the
16
16
  * judge model or the harness model - that reads the code, the tests, the build files and the
17
17
  * failing run - and the web - and says what the cause is and what change fixes it. It changes nothing; the coding
18
- * agent is given what it found, and goes on.
18
+ * agent is given what it found, and goes on - unless what it found is no change the coding agent may make: the testing
19
+ * script itself is wrong, or this machine lacks what it runs. Then the work stops, and the person is told.
19
20
  */
20
21
  /** How many tool calls the debugger has before it must hand over what it found. */
21
22
  const DEBUGGER_CALLS = 40;
@@ -41,6 +42,16 @@ function debugPrompt(input) {
41
42
  "",
42
43
  "You change nothing - you only read. When you know, call submit_diagnosis: the root cause, with the evidence for it,",
43
44
  "and the fix - which files, and exactly what to change in them.",
45
+ "",
46
+ "The coding agent changes the code and its unit tests, nothing else: never the testing script, never this machine. The",
47
+ "script decides, and the code fits what it needs - a folder, a test runner, a tool in the build's dependencies. Set",
48
+ "at_fault when no code and no tests could pass it, and the work stops for the person to fix it:",
49
+ "- script: the testing script itself is wrong - it overwrites the tests' exit code, checks the wrong folder, calls a",
50
+ " tool wrongly. Quote its lines in root_cause, and say in fix what to change in it.",
51
+ "- machine: this machine lacks what the script runs - an interpreter (`python`), a toolchain, a system library - not",
52
+ " a package the build can declare, which is the coding agent's. Say in root_cause what is missing, and the output",
53
+ " that shows it.",
54
+ "Leave it out when the code or its tests are what is wrong.",
44
55
  ...(timedOut
45
56
  ? [
46
57
  "",
@@ -81,11 +92,19 @@ export async function debugFailure(input) {
81
92
  too_slow: Type.Optional(Type.Boolean({
82
93
  description: "Only for a run the time limit stopped: true when nothing hangs - it was making progress up to the limit and needs more time than a test run may take",
83
94
  })),
95
+ at_fault: Type.Optional(Type.Union([Type.Literal("script"), Type.Literal("machine")], {
96
+ description: "Only when no code and no tests could pass the script: script - the testing script itself is wrong; machine - this machine lacks what it runs. Left out, the code or its tests are what is wrong",
97
+ })),
84
98
  }),
85
99
  async execute(_id, params) {
86
100
  const given = params;
87
101
  // Too slow is a verdict on a run the limit stopped, and on no other.
88
- found = { rootCause: given.root_cause, fix: given.fix, ...(given.too_slow === true && input.run?.timedOut === true ? { tooSlow: true } : {}) };
102
+ found = {
103
+ rootCause: given.root_cause,
104
+ fix: given.fix,
105
+ ...(given.too_slow === true && input.run?.timedOut === true ? { tooSlow: true } : {}),
106
+ ...(given.at_fault === "script" || given.at_fault === "machine" ? { atFault: given.at_fault } : {}),
107
+ };
89
108
  return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
90
109
  },
91
110
  });
@@ -119,6 +119,31 @@ export declare function timeLimitError(run: {
119
119
  kind: "unit" | "conformance";
120
120
  reason: string;
121
121
  }): EnvironmentError;
122
+ /**
123
+ * A testing script that fails whatever the code and its tests are - judged so by the debugger or the conformance judge,
124
+ * from the script's own lines and the run: `script`, the script itself is wrong - it overwrites the tests' exit code, runs
125
+ * the wrong folder, calls a tool wrongly; `machine`, this machine lacks what it runs - an interpreter, a toolchain - and
126
+ * the harness's own check (`missingToolchain`) did not see it in the output. No agent may change a testing script or
127
+ * install a toolchain, so no round would pass it: the pulse stops, rolled back, no retry, and the person is told.
128
+ */
129
+ export declare function scriptFaultError(at: "script" | "machine", details: {
130
+ moduleName: string;
131
+ script: string;
132
+ kind: "unit" | "conformance";
133
+ reason: string;
134
+ fix?: string;
135
+ }): EnvironmentError;
136
+ /**
137
+ * What the conformance tests' run is given is what is wrong - a credential missing or rejected, an account or a quota a
138
+ * service refuses - judged so by the conformance judge `times` times in a row, the tests checked in between. Only the
139
+ * person can fix it: the pulse stops, rolled back, no retry.
140
+ */
141
+ export declare function runEnvironmentError(details: {
142
+ moduleName: string;
143
+ script: string;
144
+ reason: string;
145
+ times: number;
146
+ }): EnvironmentError;
122
147
  /** The harness's run of a testing script, when it could not find its toolchain: the error that stops the pulse. */
123
148
  export declare function missingToolchainError(run: {
124
149
  exitCode: number;
@@ -241,6 +241,52 @@ export function timeLimitError(run, details) {
241
241
  source: "agent",
242
242
  });
243
243
  }
244
+ /**
245
+ * A testing script that fails whatever the code and its tests are - judged so by the debugger or the conformance judge,
246
+ * from the script's own lines and the run: `script`, the script itself is wrong - it overwrites the tests' exit code, runs
247
+ * the wrong folder, calls a tool wrongly; `machine`, this machine lacks what it runs - an interpreter, a toolchain - and
248
+ * the harness's own check (`missingToolchain`) did not see it in the output. No agent may change a testing script or
249
+ * install a toolchain, so no round would pass it: the pulse stops, rolled back, no retry, and the person is told.
250
+ */
251
+ export function scriptFaultError(at, details) {
252
+ const what = `${details.kind} testing script`;
253
+ const name = `\`${basename(details.script)}\``;
254
+ const reason = details.reason.replace(/\s+/g, " ").trim().slice(0, 600);
255
+ const fix = details.fix?.replace(/\s+/g, " ").trim().slice(0, 600);
256
+ return at === "script"
257
+ ? new EnvironmentError({
258
+ moduleName: details.moduleName,
259
+ missing: `a ${what} that can pass - ${name} fails whatever the code and its tests are`,
260
+ neededBy: `every pulse of ${details.moduleName}: the ${what} decides whether its work passes`,
261
+ shownBy: reason,
262
+ fix: `change the ${what}, \`${details.script}\`${fix !== undefined && fix !== "" ? ` - ${fix}` : ""}. turri's agents may not change the testing scripts.`,
263
+ source: "agent",
264
+ })
265
+ : new EnvironmentError({
266
+ moduleName: details.moduleName,
267
+ missing: `what the ${what}, ${name}, runs on this machine`,
268
+ neededBy: `the ${what}, ${name}`,
269
+ shownBy: reason,
270
+ fix: `${fix !== undefined && fix !== "" ? `${fix} - ` : ""}install it on this machine, where the testing scripts run, so the script finds it on its PATH.`,
271
+ source: "agent",
272
+ });
273
+ }
274
+ /**
275
+ * What the conformance tests' run is given is what is wrong - a credential missing or rejected, an account or a quota a
276
+ * service refuses - judged so by the conformance judge `times` times in a row, the tests checked in between. Only the
277
+ * person can fix it: the pulse stops, rolled back, no retry.
278
+ */
279
+ export function runEnvironmentError(details) {
280
+ return new EnvironmentError({
281
+ moduleName: details.moduleName,
282
+ missing: "what the conformance tests' run needs - a valid credential, an account, a service it can reach",
283
+ neededBy: `the conformance testing script, \`${basename(details.script)}\``,
284
+ shownBy: `judged so ${details.times} times in a row: ${details.reason.replace(/\s+/g, " ").trim().slice(0, 600)}`,
285
+ fix: "provide what the run is missing where the conformance testing script reads it. The testing scripts run on this machine " +
286
+ "in your own environment: a credential they need is read from there - your shell's variables, or a .env the script loads.",
287
+ source: "agent",
288
+ });
289
+ }
244
290
  /** The harness's run of a testing script, when it could not find its toolchain: the error that stops the pulse. */
245
291
  export function missingToolchainError(run, details) {
246
292
  const missing = missingToolchain(run);
@@ -39,8 +39,8 @@ export declare function resetKeysForTests(): void;
39
39
  export declare function breath(now: number): number;
40
40
  export declare function spinner(now: number): string;
41
41
  /**
42
- * What finished, a line a module - its name, then its pulses oldest first - fitted to `width` from the left: the
43
- * newest that fit, the rest counted as earlier. The side panel lists every one.
42
+ * What finished, a line a module - the module that finished last on top, its name, then its pulses newest first -
43
+ * fitted to `width`: the newest that fit, the rest counted as earlier. The side panel lists every one.
44
44
  */
45
45
  export declare function finishedLines(finished: Job[], width: number | undefined, marks?: MarkPaint): string[];
46
46
  /** A pulse that ran to its end and whose build did not pass - stopped at a gate - is not a success. */
@@ -67,12 +67,12 @@ function finishedItem(job, marks) {
67
67
  return `${finishedMark(job, marks)} ${job.pulse} ${job.kind}`;
68
68
  }
69
69
  /**
70
- * What finished, a line a module - its name, then its pulses oldest first - fitted to `width` from the left: the
71
- * newest that fit, the rest counted as earlier. The side panel lists every one.
70
+ * What finished, a line a module - the module that finished last on top, its name, then its pulses newest first -
71
+ * fitted to `width`: the newest that fit, the rest counted as earlier. The side panel lists every one.
72
72
  */
73
73
  export function finishedLines(finished, width, marks = PLAIN_MARKS) {
74
74
  const byModule = new Map();
75
- for (const job of [...finished].sort((a, b) => a.sequence - b.sequence)) {
75
+ for (const job of [...finished].sort((a, b) => (b.finishedAt ?? 0) - (a.finishedAt ?? 0) || b.sequence - a.sequence)) {
76
76
  const key = job.module ?? job.kind;
77
77
  byModule.set(key, [...(byModule.get(key) ?? []), job]);
78
78
  }
@@ -80,11 +80,11 @@ export function finishedLines(finished, width, marks = PLAIN_MARKS) {
80
80
  return [...byModule].map(([name, jobs]) => {
81
81
  const head = ` ${name}${" ".repeat(pad - visibleWidth(name))} `;
82
82
  const items = jobs.map((job) => finishedItem(job, marks));
83
- const lineOf = (from) => `${head}${from > 0 ? `… ${from} earlier · ` : ""}${items.slice(from).join(" · ")}`;
84
- let from = 0;
85
- while (width !== undefined && from < items.length - 1 && visibleWidth(lineOf(from)) > width)
86
- from += 1;
87
- return lineOf(from);
83
+ const lineOf = (shown) => `${head}${items.slice(0, shown).join(" · ")}${shown < items.length ? ` · … ${items.length - shown} earlier` : ""}`;
84
+ let shown = items.length;
85
+ while (width !== undefined && shown > 1 && visibleWidth(lineOf(shown)) > width)
86
+ shown -= 1;
87
+ return lineOf(shown);
88
88
  });
89
89
  }
90
90
  /** A pulse that ran to its end and whose build did not pass - stopped at a gate - is not a success. */
@@ -163,8 +163,9 @@ export function wrapKeepingIndent(line, width) {
163
163
  export function jobPanelLines(jobs, options = {}) {
164
164
  const now = options.now ?? Date.now();
165
165
  const expanded = options.expanded === true;
166
- const inOrder = [...jobs].sort((a, b) => a.sequence - b.sequence);
167
- const running = inOrder.filter((job) => job.status === "running");
166
+ // Newest first, as every list of pulses is.
167
+ const inOrder = [...jobs].sort((a, b) => b.sequence - a.sequence);
168
+ const running = inOrder.filter((job) => job.status === "running").sort((a, b) => b.startedAt - a.startedAt || b.sequence - a.sequence);
168
169
  // A module whose work is being planned: the planner is an agent, and takes a while - shown at once, as work going on.
169
170
  const planning = inOrder.filter((job) => job.status === "queued" && job.planning !== undefined);
170
171
  // Nothing running or being planned: no panel - what waits and what finished are in the side panel.
@@ -172,7 +173,7 @@ export function jobPanelLines(jobs, options = {}) {
172
173
  return [];
173
174
  const finished = inOrder.filter((job) => (job.status === "succeeded" || job.status === "failed" || job.status === "stopped") && job.replacedBy === undefined);
174
175
  const lines = [];
175
- // What finished, on top - a line a module, oldest first - and a rule under it: what runs now is below, by the input.
176
+ // What finished, on top - a line a module, newest first - and a rule under it: what runs now is below, by the input.
176
177
  if (finished.length > 0) {
177
178
  lines.push(...finishedLines(finished, options.width, options.marks ?? PLAIN_MARKS));
178
179
  lines.push(` ${"─".repeat(Math.max(10, (options.width ?? 80) - 4))}`);
@@ -46,9 +46,9 @@ export type TreeRow = {
46
46
  file: string;
47
47
  first: boolean;
48
48
  };
49
- /** The rows, top to bottom: each section's header, and its jobs when it is open - finished ones newest first. */
50
49
  /** The pulse list put on one pulse: its section open, the pulse opened - its stages, its agents, its log - and the cursor on it. */
51
50
  export declare function selectJob(jobs: Job[], state: JobsTreeState, id: string): boolean;
51
+ /** The rows, top to bottom: each section's header, and its jobs when it is open - newest first in every section. */
52
52
  export declare function jobsTreeRows(jobs: Job[], state: JobsTreeState): TreeRow[];
53
53
  /** The rows as lines, the cursor's marked, and where each row starts - for scrolling to it. */
54
54
  export declare function jobsTreeLines(jobs: Job[], state: JobsTreeState, width: number, style: TranscriptStyle): {
@@ -45,7 +45,6 @@ function isOpen(state, job) {
45
45
  state.openJobs.delete(job.id);
46
46
  return false;
47
47
  }
48
- /** The rows, top to bottom: each section's header, and its jobs when it is open - finished ones newest first. */
49
48
  /** The pulse list put on one pulse: its section open, the pulse opened - its stages, its agents, its log - and the cursor on it. */
50
49
  export function selectJob(jobs, state, id) {
51
50
  const job = jobs.find((entry) => entry.id === id);
@@ -59,13 +58,13 @@ export function selectJob(jobs, state, id) {
59
58
  state.cursor = at;
60
59
  return at !== -1;
61
60
  }
61
+ /** The rows, top to bottom: each section's header, and its jobs when it is open - newest first in every section. */
62
62
  export function jobsTreeRows(jobs, state) {
63
63
  // A job queued again as another is that other one: it is not listed apart.
64
- const bySequence = jobs.filter((job) => job.replacedBy === undefined).sort((a, b) => a.sequence - b.sequence);
65
- const of = (section) => {
66
- const inSection = bySequence.filter((job) => sectionOf(job) === section);
67
- return section === "finished" || section === "earlier" ? inSection.sort((a, b) => (b.finishedAt ?? b.sequence) - (a.finishedAt ?? a.sequence) || b.sequence - a.sequence) : inSection;
68
- };
64
+ const listed = jobs.filter((job) => job.replacedBy === undefined);
65
+ // Every section newest first: running by when it started, queued by when it was queued, finished by when it ended.
66
+ const when = (job, section) => section === "running" ? job.startedAt : section === "queued" ? job.queuedAt : (job.finishedAt ?? job.queuedAt);
67
+ const of = (section) => listed.filter((job) => sectionOf(job) === section).sort((a, b) => when(b, section) - when(a, section) || b.sequence - a.sequence);
69
68
  // Forget what is open for jobs that moved on - they close as they leave.
70
69
  for (const job of jobs)
71
70
  isOpen(state, job);
@@ -116,6 +116,8 @@ export interface StartJobInput {
116
116
  title?: string;
117
117
  /** For a render spec diff job: the diff, to show in the panel and the transcript. */
118
118
  diff?: string;
119
+ /** Where the diff's lines are in the .plain files, when they could be placed. */
120
+ diffWhere?: import("./diff-view.ts").DiffWhere;
119
121
  /** "provider/model-id" its agents run on, to show where the job is shown. */
120
122
  model?: string;
121
123
  /** For a conformance job: the render it is the conformance of. */
@@ -146,6 +148,8 @@ export interface JobRegistry {
146
148
  notify?(): void;
147
149
  /** Resolves when the job settles. Used by callers that asked to wait. */
148
150
  wait(id: string): Promise<Job>;
151
+ /** A job's ***plain diff, moved - its pulse steered: shown and recorded in the ledger as it is now. */
152
+ setDiff(id: string, diff: string, where?: import("./diff-view.ts").DiffWhere): void;
149
153
  }
150
154
  /** What a stopped job says when the person stopped it. */
151
155
  export declare const STOPPED_ON_REQUEST = "Stopped on request.";
@@ -45,6 +45,8 @@ export function createJobRegistry(options) {
45
45
  * settles - and whatever queues behind it has to wait that long too.
46
46
  */
47
47
  const done = new Set();
48
+ /** Where each job's events are recorded: its id, project and module. */
49
+ const places = new Map();
48
50
  let sequence = 0;
49
51
  return {
50
52
  setMaxRunning(count) {
@@ -76,6 +78,7 @@ export function createJobRegistry(options) {
76
78
  ...(input.pulse !== undefined ? { pulse: input.pulse } : {}),
77
79
  ...(input.title !== undefined ? { title: input.title } : {}),
78
80
  diff: input.diff,
81
+ ...(input.diffWhere !== undefined ? { diffWhere: input.diffWhere } : {}),
79
82
  model: input.model,
80
83
  ...(stack !== undefined ? { stack } : {}),
81
84
  ...(chat !== undefined ? { chat } : {}),
@@ -107,6 +110,10 @@ export function createJobRegistry(options) {
107
110
  waitingOn: blockers,
108
111
  ...(input.follows !== undefined ? { follows: input.follows } : {}),
109
112
  }, place);
113
+ // Its ***plain diff, whole: what an earlier session's pulse regenerated is read back from the ledger.
114
+ if (input.diff !== undefined && input.diff.trim() !== "")
115
+ record({ kind: "job.diff", diff: input.diff, ...(input.diffWhere !== undefined ? { where: input.diffWhere } : {}) }, place);
116
+ places.set(id, place);
110
117
  const changed = () => {
111
118
  // Like onFinish: a display that throws must not take the job down.
112
119
  try {
@@ -376,6 +383,23 @@ export function createJobRegistry(options) {
376
383
  get(id) {
377
384
  return jobs.get(id);
378
385
  },
386
+ setDiff(id, diff, where) {
387
+ const job = jobs.get(id);
388
+ if (job === undefined)
389
+ return;
390
+ job.diff = diff;
391
+ if (where !== undefined)
392
+ job.diffWhere = where;
393
+ else
394
+ delete job.diffWhere;
395
+ record({ kind: "job.diff", diff, ...(where !== undefined ? { where } : {}) }, places.get(id) ?? { job: id });
396
+ try {
397
+ options?.onChange?.(job);
398
+ }
399
+ catch {
400
+ /* a display that throws must not take the job down */
401
+ }
402
+ },
379
403
  stop(id, reason) {
380
404
  const job = jobs.get(id);
381
405
  const controller = controllers.get(id);
@@ -21,6 +21,9 @@ export interface PulseView {
21
21
  };
22
22
  started?: Of<"job.started">;
23
23
  finished?: Of<"job.finished">;
24
+ /** Its ***plain diff as it last was, and how many times steering moved it. */
25
+ diff?: Of<"job.diff">;
26
+ steered: number;
24
27
  stages: string[];
25
28
  retried: Array<Of<"job.retried">>;
26
29
  rounds: Map<string, {
@@ -44,11 +47,16 @@ export interface PulseView {
44
47
  }
45
48
  /** A pulse's events, gathered: undefined when the ledger has none of it. `entries` are its run's, oldest first. */
46
49
  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;
50
+ /**
51
+ * A pulse from the ledger, for the chat and `turri ledger <pulse>`: what happened in it, as facts - and, last, the
52
+ * ***plain diff it regenerated, unless `diff` is false.
53
+ */
54
+ export declare function describePulseView(view: PulseView, options?: {
55
+ diff?: boolean;
56
+ }): string;
49
57
  /** A pulse the ledger of any of these projects knows, described - or undefined. */
50
58
  export declare function describePastPulse(projects: string[], id: string): string | undefined;
51
- /** A run of the project's, and its pulses: the most recent first. */
59
+ /** A run of the project's, and its pulses: runs and pulses the most recent first. */
52
60
  export interface RunView {
53
61
  run: string;
54
62
  started?: Of<"run.started">;
@@ -36,6 +36,7 @@ export function pulseView(id, entries) {
36
36
  const view = {
37
37
  id: fullId,
38
38
  run: own[0].run,
39
+ steered: 0,
39
40
  stages: [],
40
41
  retried: [],
41
42
  rounds: new Map(),
@@ -65,6 +66,11 @@ export function pulseView(id, entries) {
65
66
  view.finished = entry;
66
67
  view.usage = entry.usage;
67
68
  break;
69
+ case "job.diff":
70
+ if (view.diff !== undefined)
71
+ view.steered += 1;
72
+ view.diff = entry;
73
+ break;
68
74
  case "job.stage":
69
75
  view.stages.push(entry.label);
70
76
  break;
@@ -135,8 +141,11 @@ function outcomeOf(view) {
135
141
  const stopped = finished.stoppedBy !== undefined ? ` by ${finished.stoppedBy === "turri" ? "turri" : "the person"}` : "";
136
142
  return `${finished.status}${stopped}${landed}, after ${formatElapsed(finished.ms)}`;
137
143
  }
138
- /** A pulse from the ledger, for the chat and `turri ledger <pulse>`: what happened in it, as facts. */
139
- export function describePulseView(view) {
144
+ /**
145
+ * A pulse from the ledger, for the chat and `turri ledger <pulse>`: what happened in it, as facts - and, last, the
146
+ * ***plain diff it regenerated, unless `diff` is false.
147
+ */
148
+ export function describePulseView(view, options = {}) {
140
149
  const lines = [`${view.id} · ${nameOf(view)}${view.queued?.title !== undefined ? ` - ${view.queued.title}` : ""}`];
141
150
  lines.push(` run ${view.run}${view.runMode !== undefined ? ` (${view.runMode})` : ""}`);
142
151
  lines.push(` ${outcomeOf(view)}${view.usage !== undefined && describeUsage(view.usage) !== "" ? ` · ${describeUsage(view.usage)}` : ""}${view.queued?.model !== undefined ? ` · ${view.queued.model}` : ""}`);
@@ -144,7 +153,7 @@ export function describePulseView(view) {
144
153
  lines.push(` ended with: ${view.finished.error}`);
145
154
  if (view.environment !== undefined)
146
155
  lines.push(` the environment lacks ${view.environment.missing} (found by ${view.environment.source === "harness" ? "turri" : "an agent"})`);
147
- for (const retry of view.retried)
156
+ for (const retry of [...view.retried].reverse())
148
157
  lines.push(` tried again (${retry.attempt} of ${retry.of}) after: ${retry.failure}`);
149
158
  if (view.rounds.size > 0)
150
159
  lines.push(` rounds: ${[...view.rounds].map(([gate, { round, of }]) => `${gate} ${round} of ${of}`).join("; ")}`);
@@ -187,7 +196,7 @@ export function describePulseView(view) {
187
196
  lines.push(` turri refused calls: ${[...view.refused].map(([rule, times]) => `${rule} ${times}x`).join(", ")}`);
188
197
  if (view.stopped.length > 0)
189
198
  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) {
199
+ for (const event of [...view.history].reverse()) {
191
200
  if (event.kind === "history.committed")
192
201
  lines.push(` committed: ${event.subject}`);
193
202
  else if (event.kind === "history.failed")
@@ -197,11 +206,15 @@ export function describePulseView(view) {
197
206
  else
198
207
  lines.push(` rolled back: ${event.why}`);
199
208
  }
209
+ if (view.steered > 0)
210
+ lines.push(` steered ${view.steered} time${view.steered === 1 ? "" : "s"}: its ***plain diff moved with ***plain`);
200
211
  if (view.stages.length > 0)
201
- lines.push(" last stages:", ...view.stages.slice(-8).map((stage) => ` ${stage}`));
212
+ lines.push(" last stages, newest first:", ...view.stages.slice(-8).reverse().map((stage) => ` ${stage}`));
202
213
  const log = view.finished?.logFile ?? view.started?.logFile;
203
214
  if (log !== undefined)
204
215
  lines.push(` log: ${log}`);
216
+ if (options.diff !== false && view.diff !== undefined)
217
+ lines.push(` its ***plain diff${view.steered > 0 ? ", as last steered" : ""}:`, ...view.diff.diff.trimEnd().split("\n").map((line) => ` ${line}`));
205
218
  return lines.join("\n");
206
219
  }
207
220
  /** A pulse the ledger of any of these projects knows, described - or undefined. */
@@ -224,7 +237,8 @@ export function runViews(project) {
224
237
  const ids = [...new Set(own.filter((entry) => entry.kind === "job.queued").map((entry) => entry.job))];
225
238
  const started = own.find((entry) => entry.kind === "run.started");
226
239
  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) };
240
+ // Its pulses the most recent first, as its runs are.
241
+ return { run, ...(started !== undefined ? { started } : {}), ...(ended !== undefined ? { ended } : {}), pulses: ids.map((id) => pulseView(id, own)).filter(Boolean).reverse() };
228
242
  })
229
243
  .reverse();
230
244
  }
@@ -274,6 +288,8 @@ export function asJob(view) {
274
288
  ...(view.queued?.pulse !== undefined ? { pulse: view.queued.pulse } : {}),
275
289
  ...(view.queued?.title !== undefined ? { title: view.queued.title } : {}),
276
290
  ...(view.queued?.model !== undefined ? { model: view.queued.model } : {}),
291
+ // Its ***plain diff, shown a file at a time as this session's pulses' are.
292
+ ...(view.diff !== undefined ? { diff: view.diff.diff, ...(view.diff.where !== undefined ? { diffWhere: view.diff.where } : {}) } : {}),
277
293
  status,
278
294
  sequence: 0,
279
295
  queuedAt,
@@ -288,7 +304,7 @@ export function asJob(view) {
288
304
  ...(logFile !== undefined ? { logFile } : {}),
289
305
  ...(finished?.error !== undefined ? { error: finished.error } : {}),
290
306
  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(/^ /, "")) },
307
+ earlier: { run: view.run, ...(view.runMode !== undefined ? { runMode: view.runMode } : {}), lines: describePulseView(view, { diff: false }).split("\n").slice(1).map((line) => line.replace(/^ /, "")) },
292
308
  };
293
309
  }
294
310
  /** The envelope's fields, said apart from an event's own. */
@@ -298,7 +314,9 @@ export function eventLine(entry) {
298
314
  const fields = Object.entries(entry)
299
315
  .filter(([key]) => !ENVELOPE.has(key))
300
316
  .map(([key, value]) => {
301
- const text = typeof value === "string" ? JSON.stringify(value.length > 160 ? `${value.slice(0, 159)}…` : value) : JSON.stringify(value);
317
+ // A long value - a ***plain diff, where its lines are - cut to the start of it: the line is a glance.
318
+ const whole = typeof value === "string" ? value : JSON.stringify(value);
319
+ const text = typeof value === "string" ? JSON.stringify(value.length > 160 ? `${value.slice(0, 159)}…` : value) : whole.length > 160 ? `${whole.slice(0, 159)}…` : whole;
302
320
  return `${key}=${text}`;
303
321
  });
304
322
  const where = [entry.job, entry.module, entry.agent?.role ?? entry.agent?.label].filter((part) => part !== undefined);
@@ -325,11 +343,11 @@ export function describeEvents(projects, query = {}) {
325
343
  const pulses = new Set(found.map((entry) => entry.job).filter((job) => job !== undefined)).size;
326
344
  const runs = new Set(found.map((entry) => entry.run)).size;
327
345
  const limit = Math.max(1, query.limit ?? 20);
328
- const recent = found.slice(-limit);
346
+ const recent = found.slice(-limit).reverse();
329
347
  return [
330
348
  `${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
349
  "",
332
- recent.length === found.length ? "Each, oldest first:" : `The last ${recent.length}, oldest first:`,
350
+ recent.length === found.length ? "Each, newest first:" : `The last ${recent.length}, newest first:`,
333
351
  ...recent.map(eventLine),
334
352
  ].join("\n");
335
353
  }
@@ -1,3 +1,4 @@
1
+ import type { DiffWhere } from "./diff-view.ts";
1
2
  import { type UsageReport } from "./activity.ts";
2
3
  /**
3
4
  * The ledger: what turri does, as typed facts, one JSON line each, written as it happens - to
@@ -8,9 +9,10 @@ import { type UsageReport } from "./activity.ts";
8
9
  * was queued, started, went through its stages and ended, and how; what each agent's model calls and
9
10
  * turns cost; each tool call and test run, how long it took and how it ended; the gates' rounds and
10
11
  * 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.
12
+ * from an earlier session did is looked up here (`pulses`, `diagnose`, `turri ledger`) - and what it
13
+ * regenerated, its ***plain diff, kept whole as the one body the ledger has. Never stored: prompts,
14
+ * replies, thinking, tool arguments and results, file contents - and what strings an event carries are
15
+ * masked as the logs are.
14
16
  *
15
17
  * A host hears each event as it is recorded (`onLedgerEvent`), with the project it is about, rather than reading the
16
18
  * file again - written or not, so a project that keeps no ledger is heard all the same.
@@ -64,6 +66,10 @@ export type LedgerEvent = {
64
66
  kind: "job.started";
65
67
  waitedMs: number;
66
68
  logFile?: string;
69
+ } | {
70
+ kind: "job.diff";
71
+ diff: string;
72
+ where?: DiffWhere;
67
73
  } | {
68
74
  kind: "job.stage";
69
75
  label: string;
@@ -17,9 +17,10 @@ import { ensureInScratch } from "./paths.js";
17
17
  * was queued, started, went through its stages and ended, and how; what each agent's model calls and
18
18
  * turns cost; each tool call and test run, how long it took and how it ended; the gates' rounds and
19
19
  * verdicts; what turri refused or stopped; what its healers did; the history's commits. What a pulse
20
- * from an earlier session did is looked up here (`pulses`, `diagnose`, `turri ledger`). Never stored:
21
- * prompts, replies, thinking, tool arguments and results, file contents - and what strings an event
22
- * carries are masked as the logs are.
20
+ * from an earlier session did is looked up here (`pulses`, `diagnose`, `turri ledger`) - and what it
21
+ * regenerated, its ***plain diff, kept whole as the one body the ledger has. Never stored: prompts,
22
+ * replies, thinking, tool arguments and results, file contents - and what strings an event carries are
23
+ * masked as the logs are.
23
24
  *
24
25
  * A host hears each event as it is recorded (`onLedgerEvent`), with the project it is about, rather than reading the
25
26
  * file again - written or not, so a project that keeps no ledger is heard all the same.
@@ -435,8 +435,7 @@ export function createWorkLoop(options) {
435
435
  const job = options.registry.get(running.jobId);
436
436
  if (job !== undefined) {
437
437
  const shown = changeDiffInFiles(agentChanges(changesBetween(serializeTree(running.from), serializeTree(steering.agent)), "code"), relative(projectDirOf(tip.plainFile), tip.plainFile) || basename(tip.plainFile), (diff) => whereInFiles(diff, state.chain));
438
- job.diff = shown.diff;
439
- job.diffWhere = shown.where;
438
+ options.registry.setDiff(job.id, shown.diff, shown.where);
440
439
  }
441
440
  reportActivity(` ⇢ steered pulse ${running.jobId} (${steering.steers} of ${MAX_STEERS}): ${why}`);
442
441
  return true;
@@ -521,6 +520,8 @@ export function createWorkLoop(options) {
521
520
  pulse: (state.pulses ?? 0) + 1,
522
521
  title: planned.title,
523
522
  diff: shown.diff,
523
+ // Its lines where they are written: the module's .plain file, those it imports, and those of the modules below it.
524
+ diffWhere: shown.where,
524
525
  ...(state.model !== undefined ? { model: state.model } : {}),
525
526
  lanes: [...moduleLanes(tip.plainFile)],
526
527
  rerun: `turri regenerate ${relative(options.cwd, tip.plainFile) || basename(tip.plainFile)}`,
@@ -617,8 +618,6 @@ export function createWorkLoop(options) {
617
618
  };
618
619
  },
619
620
  });
620
- // Its lines where they are written: the module's .plain file, those it imports, and those of the modules below it.
621
- job.diffWhere = shown.where;
622
621
  state.pulses = (state.pulses ?? 0) + 1;
623
622
  state.running = { jobId: job.id, number: state.pulses, planned, from, after: planned.step.tree, ...(steering !== undefined ? { steering } : {}) };
624
623
  void options.registry.wait(job.id).then((settled) => settle(state, settled), () => settle(state, undefined));
@@ -21,7 +21,7 @@ import { createWebTools, WEB_TOOLS } from "./web.js";
21
21
  import { createCanItFailTool, createMyDiffTool, selfCheckNote } from "./self-check.js";
22
22
  import { createServiceTools } from "./services.js";
23
23
  import { createUnitGate } from "./unit-gate.js";
24
- import { timeLimitError, toolchainError } from "./environment-error.js";
24
+ import { scriptFaultError, timeLimitError, toolchainError } from "./environment-error.js";
25
25
  import { workspaceBlock } from "./workspace.js";
26
26
  import { ATTEMPTS, describeStop } from "./stop-report.js";
27
27
  import { summarizeDiff } from "./job-panel.js";
@@ -341,8 +341,11 @@ export async function renderSpecDiff(options) {
341
341
  let unchanged = false;
342
342
  /** What the unit testing script needs and does not find, last run. */
343
343
  let missing;
344
- /** The unit testing script needs more time than a test run may take - the debugger's finding: the pulse stops on it. */
345
- let slow;
344
+ /**
345
+ * What no round could change, as the debugger found it - the unit testing script needs more time than a test run may
346
+ * take, the script itself is wrong, this machine lacks what it runs: the pulse stops on it.
347
+ */
348
+ let blocked;
346
349
  /** The last round failed on the script's static checks alone: the unit tests passed. */
347
350
  let staticOnly = false;
348
351
  const testRemovals = [];
@@ -472,7 +475,16 @@ export async function renderSpecDiff(options) {
472
475
  // Nothing hangs: the suite needs more time than a test run may take, and no agent can give it that.
473
476
  if (found?.tooSlow === true) {
474
477
  stage(`the unit testing script needs more than the ${Math.round(verdict.run.timeoutMs / 1000)}s a test run may take - stopping`);
475
- slow = timeLimitError(verdict.run, { moduleName, script: verdict.script, kind: "unit", reason: found.rootCause });
478
+ blocked = { error: timeLimitError(verdict.run, { moduleName, script: verdict.script, kind: "unit", reason: found.rootCause }), why: "the unit testing script needs more time than a test run may take" };
479
+ return false;
480
+ }
481
+ // Not the code's: no round would pass the script, the coding agent may change neither it nor this machine.
482
+ if (found?.atFault !== undefined) {
483
+ stage(found.atFault === "script"
484
+ ? "the debugger finds the unit testing script itself is wrong - no change to the code would pass it - stopping"
485
+ : "the debugger finds this machine lacks what the unit testing script runs - stopping");
486
+ const error = scriptFaultError(found.atFault, { moduleName, script: verdict.script, kind: "unit", reason: found.rootCause, fix: found.fix });
487
+ blocked = { error, why: found.atFault === "script" ? "the unit testing script itself is wrong" : "this machine lacks what the unit testing script runs" };
476
488
  return false;
477
489
  }
478
490
  if (found !== undefined) {
@@ -582,8 +594,8 @@ export async function renderSpecDiff(options) {
582
594
  ? "the unit testing script could not run"
583
595
  : unchanged
584
596
  ? "regenerating again, the coding agent built nothing anew"
585
- : slow !== undefined
586
- ? "the unit testing script needs more time than a test run may take"
597
+ : blocked !== undefined
598
+ ? blocked.why
587
599
  : staticOnly
588
600
  ? "the spec change did not pass its static checks"
589
601
  : "the spec change did not pass its unit tests");
@@ -591,8 +603,8 @@ export async function renderSpecDiff(options) {
591
603
  if (missing !== undefined) {
592
604
  throw toolchainError(missing, { moduleName, script: scripts.unit, kind: "unit" });
593
605
  }
594
- if (slow !== undefined)
595
- throw slow;
606
+ if (blocked !== undefined)
607
+ throw blocked.error;
596
608
  return { ...base, passed: false, stoppedAt: "unit tests", ...(staticOnly && !unitReviewStopped ? { staticChecks: true } : {}) };
597
609
  }
598
610
  stage("unit tests pass - landing the change on them");
@@ -19,7 +19,7 @@ export interface StopFacts {
19
19
  at: StopPoint;
20
20
  /** The unit test runs, the last of which is the failing one. */
21
21
  runs?: TestRun[];
22
- conformance?: Pick<RenderConformanceTestsResult, "runs" | "rounds" | "specConflict" | "stuck" | "environment" | "escalation" | "review" | "unlabelled">;
22
+ conformance?: Pick<RenderConformanceTestsResult, "runs" | "rounds" | "specConflict" | "stuck" | "escalation" | "review" | "unlabelled">;
23
23
  /** Repairs made to earlier functionalities' code before it gave up. */
24
24
  repairs?: number;
25
25
  /** How many times the harness judged it before it stopped - the unit tests' attempts. */