@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.
- package/README.md +0 -2
- package/dist/cli.js +2 -1
- package/dist/config-edit.d.ts +1 -1
- package/dist/config-edit.js +2 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +5 -1
- package/dist/extension.js +75 -9
- package/dist/host.d.ts +2 -0
- package/dist/host.js +3 -0
- package/dist/init-config.d.ts +1 -0
- package/dist/init-config.js +5 -0
- package/dist/ledger-cli.d.ts +1 -0
- package/dist/ledger-cli.js +50 -0
- package/dist/ledger-tools.d.ts +10 -0
- package/dist/ledger-tools.js +64 -0
- package/dist/loop-cli.js +4 -0
- package/dist/pi.js +17 -0
- package/dist/regenerate/activity.d.ts +9 -0
- package/dist/regenerate/activity.js +11 -0
- package/dist/regenerate/agent-shell.js +5 -0
- package/dist/regenerate/chunk-plan.d.ts +0 -6
- package/dist/regenerate/chunk-plan.js +16 -4
- package/dist/regenerate/conformance.js +3 -0
- package/dist/regenerate/diagnose.js +4 -2
- package/dist/regenerate/job-log.d.ts +3 -1
- package/dist/regenerate/job-log.js +8 -5
- package/dist/regenerate/jobs-tree.d.ts +7 -3
- package/dist/regenerate/jobs-tree.js +21 -6
- package/dist/regenerate/jobs.d.ts +9 -0
- package/dist/regenerate/jobs.js +38 -3
- package/dist/regenerate/ledger-view.d.ts +92 -0
- package/dist/regenerate/ledger-view.js +345 -0
- package/dist/regenerate/ledger.d.ts +275 -0
- package/dist/regenerate/ledger.js +349 -0
- package/dist/regenerate/lineage.js +18 -1
- package/dist/regenerate/loop.js +10 -1
- package/dist/regenerate/regenerate.js +4 -2
- package/dist/regenerate/regression.js +2 -0
- package/dist/regenerate/side-panel.d.ts +2 -0
- package/dist/regenerate/side-panel.js +4 -3
- package/dist/regenerate/test-scripts.js +18 -5
- package/dist/regenerate/unit-review.js +3 -0
- package/docs/config.md +3 -1
- package/docs/development.md +1 -0
- package/docs/hosting.md +28 -0
- package/docs/installation.md +1 -0
- package/docs/regenerating.md +1 -1
- package/docs/using.md +29 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { DEFAULT_CONCEPTS, extractConceptsFromDefinition, extractConceptsFromSpe
|
|
|
4
4
|
import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_REQUIREMENTS } from "../plain/spec.js";
|
|
5
5
|
import { ask, BEGIN, createExecutor } from "../pi.js";
|
|
6
6
|
import { reportActivity, reportStage } from "./activity.js";
|
|
7
|
+
import { firstLine, record } from "./ledger.js";
|
|
7
8
|
import { steeringBlock } from "./steer.js";
|
|
8
9
|
import { marksRegenerateAgain } from "./records.js";
|
|
9
10
|
import { validateConcepts } from "../plain/concepts.js";
|
|
@@ -245,6 +246,11 @@ function titleOf(change) {
|
|
|
245
246
|
* the plan did not do as asked - the person's wishes an agent did not follow and why, an answer that did not hold, a
|
|
246
247
|
* group split for its size - for the conversation to say, rather than guess.
|
|
247
248
|
*/
|
|
249
|
+
/** A planner agent that failed: the plan falls back, and the ledger says why. */
|
|
250
|
+
function plannerFailed(step, error) {
|
|
251
|
+
record({ kind: "planner.failed", step, error: firstLine(error instanceof Error ? error.message : String(error), 300) });
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
248
254
|
export async function planChunks(input, agents, notes) {
|
|
249
255
|
if (input.track !== "conformance")
|
|
250
256
|
return cut(input, agents, notes);
|
|
@@ -370,7 +376,7 @@ async function cut(input, agents, notes) {
|
|
|
370
376
|
...list.map((group) => ({ title: group.title, specs: group.specs.map((number) => bySpec.get(number)), definitions: [], reqs: [] })),
|
|
371
377
|
];
|
|
372
378
|
const conformance = input.track === "conformance";
|
|
373
|
-
const grouped = conformance ? undefined : ((await agents?.group(numbered, (answer) => groupProblems(numbered, answer, maxSpecs), input.steering, maxSpecs).catch(() =>
|
|
379
|
+
const grouped = conformance ? undefined : ((await agents?.group(numbered, (answer) => groupProblems(numbered, answer, maxSpecs), input.steering, maxSpecs).catch((error) => plannerFailed("group", error))) ?? undefined);
|
|
374
380
|
const groups = conformance
|
|
375
381
|
? conformanceGroups(numbered, input.separately === true, maxLines, (list) => {
|
|
376
382
|
const trial = chunksFor(list);
|
|
@@ -386,10 +392,14 @@ async function cut(input, agents, notes) {
|
|
|
386
392
|
if (grouped?.notes !== undefined && grouped.notes.trim() !== "")
|
|
387
393
|
say(`the grouping: ${grouped.notes.trim()}`);
|
|
388
394
|
if (!conformance && agents !== undefined) {
|
|
389
|
-
if (grouped === undefined)
|
|
395
|
+
if (grouped === undefined) {
|
|
396
|
+
record({ kind: "planner.fellBack", step: "group", why: "no grouping was given" });
|
|
390
397
|
say("the grouping was not given - a functional spec a pulse");
|
|
391
|
-
|
|
398
|
+
}
|
|
399
|
+
else if (groupProblems(numbered, grouped, maxSpecs).length > 0) {
|
|
400
|
+
record({ kind: "planner.fellBack", step: "group", why: firstLine(groupProblems(numbered, grouped, maxSpecs).join(" "), 300) });
|
|
392
401
|
say(`the grouping did not hold (${groupProblems(numbered, grouped, maxSpecs).join(" ")}) - a functional spec a pulse`);
|
|
402
|
+
}
|
|
393
403
|
}
|
|
394
404
|
let chunks = chunksFor(groups);
|
|
395
405
|
// 2. The definitions each group uses.
|
|
@@ -407,10 +417,12 @@ async function cut(input, agents, notes) {
|
|
|
407
417
|
implementationReqs: implementationReqs.map((change, index) => ({ number: index + 1, text: textOf(change) })),
|
|
408
418
|
testReqs: testReqs.map((change, index) => ({ number: index + 1, text: textOf(change) })),
|
|
409
419
|
};
|
|
410
|
-
const answer = (await agents?.place(placeInput, (given) => placeProblems(placeInput, given)).catch(() =>
|
|
420
|
+
const answer = (await agents?.place(placeInput, (given) => placeProblems(placeInput, given)).catch((error) => plannerFailed("place", error))) ?? undefined;
|
|
411
421
|
if (answer?.notes !== undefined && answer.notes.trim() !== "")
|
|
412
422
|
say(`the placement: ${answer.notes.trim()}`);
|
|
413
423
|
placement = answer !== undefined && placeProblems(placeInput, answer).length === 0 ? answer : undefined;
|
|
424
|
+
if (placement === undefined && agents !== undefined)
|
|
425
|
+
record({ kind: "planner.fellBack", step: "place", why: answer === undefined ? "no placement was given" : firstLine(placeProblems(placeInput, answer).join(" "), 300) });
|
|
414
426
|
if (placement === undefined && agents !== undefined)
|
|
415
427
|
say("the placement did not hold - every implementation req with the first pulse, every test req with the first conformance pulse");
|
|
416
428
|
}
|
|
@@ -6,6 +6,7 @@ import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_
|
|
|
6
6
|
import { reportActivity, reportLogNote, reportSpec, reportStage, unitOf } from "./activity.js";
|
|
7
7
|
import { confinedTools } from "./confine.js";
|
|
8
8
|
import { abandonWork, diffSinceSnapshot, landWork, moduleDirOf, startWork, workDiff, workSnapshot } from "./lineage.js";
|
|
9
|
+
import { firstLine, record } from "./ledger.js";
|
|
9
10
|
import { createWorkLedger, describeAccounts, SUBMIT_WORK_NOTE, unanswered, unansweredPrompt } from "./submit-work.js";
|
|
10
11
|
import { CONFORMANCE_COMPACTION } from "./compaction.js";
|
|
11
12
|
import { NOTES_TURN, notesForSession, passedOutcome, readNotes, reflectOnReview, withNotes } from "./notes.js";
|
|
@@ -417,6 +418,7 @@ export async function renderConformanceTests(options) {
|
|
|
417
418
|
// every functionality's tests - and the code's unit testing script.
|
|
418
419
|
stage(`running the conformance testing script on the whole suite (attempt ${rounds + 1} of ${maxRounds})`);
|
|
419
420
|
rounds += 1;
|
|
421
|
+
record({ kind: "gate.round", gate: "conformance", round: rounds, of: maxRounds });
|
|
420
422
|
let recheck = await runConformanceScript(buildDir, [conformanceDir], scripts);
|
|
421
423
|
runs.push({ command: recheck.command, exitCode: recheck.exitCode });
|
|
422
424
|
options.onRun?.(recheck.command, recheck.exitCode);
|
|
@@ -537,6 +539,7 @@ export async function renderConformanceTests(options) {
|
|
|
537
539
|
onDelta: options.onDelta,
|
|
538
540
|
});
|
|
539
541
|
const reason = verdict.reason.split("\n")[0].slice(0, 160);
|
|
542
|
+
record({ kind: "judge.verdict", culprit: verdict.culprit, round: rounds, reason: firstLine(verdict.reason, 300) });
|
|
540
543
|
// Stopped by the time limit while it was still getting on: no agent can give it more time. The pulse stops now -
|
|
541
544
|
// rolled back, no retry - and the person is told how to raise the limit.
|
|
542
545
|
if (verdict.culprit === "environment" && recheck.timedOut === true) {
|
|
@@ -49,8 +49,10 @@ export async function diagnoseFailure(options) {
|
|
|
49
49
|
if (diagnosis === undefined)
|
|
50
50
|
await ask(session, "You have not called submit_diagnosis. Call it now, with what you know.");
|
|
51
51
|
}
|
|
52
|
-
catch {
|
|
53
|
-
// Without a diagnosis the person still gets both failures, as they were.
|
|
52
|
+
catch (error) {
|
|
53
|
+
// Without a diagnosis the person still gets both failures, as they were - and the ledger why there is none.
|
|
54
|
+
if (diagnosis === undefined)
|
|
55
|
+
throw error;
|
|
54
56
|
}
|
|
55
57
|
finally {
|
|
56
58
|
session.dispose();
|
|
@@ -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
|
|
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
|
|
6
|
-
export function
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
package/dist/regenerate/jobs.js
CHANGED
|
@@ -3,7 +3,8 @@ 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";
|
|
6
|
+
import { projectOfLanes, startJobLog } from "./job-log.js";
|
|
7
|
+
import { firstLine, healing, record } from "./ledger.js";
|
|
7
8
|
import { chosenStack, withStack } from "../config.js";
|
|
8
9
|
const DEFAULT_TAIL_CHARS = 4000;
|
|
9
10
|
const ACTIVITY_LINES = 10;
|
|
@@ -88,6 +89,20 @@ export function createJobRegistry(options) {
|
|
|
88
89
|
};
|
|
89
90
|
jobs.set(job.id, job);
|
|
90
91
|
let transcriptChars = 0;
|
|
92
|
+
// Where what it does is recorded: its project, from the modules it writes.
|
|
93
|
+
const project = projectOfLanes(claimed);
|
|
94
|
+
const place = { job: id, ...(project !== undefined ? { project } : {}), ...(input.module !== undefined ? { module: input.module } : {}) };
|
|
95
|
+
record({
|
|
96
|
+
kind: "job.queued",
|
|
97
|
+
jobKind: input.kind,
|
|
98
|
+
summary: firstLine(input.summary, 300),
|
|
99
|
+
...(input.pulse !== undefined ? { pulse: input.pulse } : {}),
|
|
100
|
+
...(input.title !== undefined ? { title: input.title } : {}),
|
|
101
|
+
...(input.model !== undefined ? { model: input.model } : {}),
|
|
102
|
+
...(stack !== undefined ? { stack } : {}),
|
|
103
|
+
waitingOn: blockers,
|
|
104
|
+
...(input.follows !== undefined ? { follows: input.follows } : {}),
|
|
105
|
+
}, place);
|
|
91
106
|
const changed = () => {
|
|
92
107
|
// Like onFinish: a display that throws must not take the job down.
|
|
93
108
|
try {
|
|
@@ -105,6 +120,8 @@ export function createJobRegistry(options) {
|
|
|
105
120
|
const sink = {
|
|
106
121
|
signal: controller.signal,
|
|
107
122
|
job: { id, kind: input.kind, summary: input.summary },
|
|
123
|
+
...(project !== undefined ? { project } : {}),
|
|
124
|
+
...(input.module !== undefined ? { module: input.module } : {}),
|
|
108
125
|
specConflict(conflict) {
|
|
109
126
|
job.specConflict = conflict;
|
|
110
127
|
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 +129,7 @@ export function createJobRegistry(options) {
|
|
|
112
129
|
},
|
|
113
130
|
stage(label) {
|
|
114
131
|
job.stage = label;
|
|
132
|
+
record({ kind: "job.stage", label: firstLine(label, 300) }, place);
|
|
115
133
|
this.transcript({ kind: "stage", text: label });
|
|
116
134
|
changed();
|
|
117
135
|
},
|
|
@@ -213,22 +231,24 @@ export function createJobRegistry(options) {
|
|
|
213
231
|
// and the person is told. What the harness found itself is worked out at once; what an agent said goes as it is.
|
|
214
232
|
if (error instanceof EnvironmentError) {
|
|
215
233
|
job.failedForGood = true;
|
|
234
|
+
record({ kind: "environment.error", missing: firstLine(error.missing, 300), source: error.source, sandbox: error.sandbox !== undefined }, place);
|
|
216
235
|
if (error.source === "harness" && options?.diagnose !== undefined) {
|
|
217
236
|
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)
|
|
237
|
+
job.diagnosis = await withActivity(sink, () => healing("diagnosis", () => options.diagnose(job, [error.message, describeEnvironmentError(error, input.rerun)], log?.file), (found) => found));
|
|
219
238
|
}
|
|
220
239
|
job.environment = describeEnvironmentError(error, input.rerun);
|
|
221
240
|
throw new Error([job.environment, ...(job.diagnosis !== undefined ? ["", job.diagnosis] : [])].join("\n"));
|
|
222
241
|
}
|
|
223
242
|
failures.push(error !== undefined ? (error instanceof Error ? error.message : String(error)) : outcome.report);
|
|
224
243
|
if (attempt < retries) {
|
|
244
|
+
record({ kind: "job.retried", attempt: attempt + 1, of: retries, failure: firstLine(failures.at(-1), 300) }, place);
|
|
225
245
|
sink.stage(`the pulse failed - trying it once more (${attempt + 1} of ${retries})`);
|
|
226
246
|
continue;
|
|
227
247
|
}
|
|
228
248
|
job.failedForGood = true;
|
|
229
249
|
if (options?.diagnose !== undefined) {
|
|
230
250
|
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)
|
|
251
|
+
job.diagnosis = await withActivity(sink, () => healing("diagnosis", () => options.diagnose(job, failures, log?.file), (found) => found));
|
|
232
252
|
}
|
|
233
253
|
throw new Error([
|
|
234
254
|
`Failed ${failures.length} time${failures.length === 1 ? "" : "s"}, so it and every pulse queued behind it have stopped.`,
|
|
@@ -277,6 +297,7 @@ export function createJobRegistry(options) {
|
|
|
277
297
|
log = startJobLog(job, claimed);
|
|
278
298
|
if (log !== undefined)
|
|
279
299
|
job.logFile = log.file;
|
|
300
|
+
record({ kind: "job.started", waitedMs: job.startedAt - job.queuedAt, ...(log !== undefined ? { logFile: log.file } : {}) }, place);
|
|
280
301
|
// What the job's agents start runs until it ends, and no longer (withPulse) - with the stack it was queued
|
|
281
302
|
// with, whatever is chosen while it waits or runs.
|
|
282
303
|
return withStack(stack, () => (guarded ? runGuarded() : runOnce()));
|
|
@@ -302,6 +323,20 @@ export function createJobRegistry(options) {
|
|
|
302
323
|
giveSlot();
|
|
303
324
|
}
|
|
304
325
|
finished.finishedAt = Date.now();
|
|
326
|
+
const outcome = finished.details;
|
|
327
|
+
const stopReason = stopReasons.get(id);
|
|
328
|
+
record({
|
|
329
|
+
kind: "job.finished",
|
|
330
|
+
status: finished.status,
|
|
331
|
+
...(typeof outcome?.landed === "boolean" ? { landed: outcome.landed } : {}),
|
|
332
|
+
...(typeof outcome?.passed === "boolean" ? { passed: outcome.passed } : {}),
|
|
333
|
+
ms: finished.finishedAt - finished.startedAt,
|
|
334
|
+
usage: finished.usage,
|
|
335
|
+
...(finished.failedForGood === true ? { failedForGood: true } : {}),
|
|
336
|
+
...(finished.status === "stopped" ? { stoppedBy: stopReason !== undefined ? "turri" : "person" } : {}),
|
|
337
|
+
...(finished.error !== undefined ? { error: firstLine(finished.error, 300) } : {}),
|
|
338
|
+
...(finished.logFile !== undefined ? { logFile: finished.logFile } : {}),
|
|
339
|
+
}, place);
|
|
305
340
|
log?.end(finished.status === "succeeded" ? "finished" : finished.status === "stopped" ? "stopped" : "finished - failed", finished.announce ?? finished.report ?? finished.error, describeAgents(finished.agents));
|
|
306
341
|
// A failing listener must not take the job down with it; the result is
|
|
307
342
|
// already recorded and can still be collected by asking.
|
|
@@ -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 {};
|