@turri-dev/cli 0.1.7 → 0.1.8-next.b03162b
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 +5 -7
- package/dist/btw.d.ts +2 -0
- package/dist/btw.js +2 -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.d.ts +11 -0
- package/dist/extension.js +112 -13
- package/dist/host.d.ts +2 -0
- package/dist/host.js +3 -0
- package/dist/identity.d.ts +6 -1
- package/dist/identity.js +33 -7
- 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/loop-cli.js +4 -0
- package/dist/loop-tools.js +3 -1
- 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/cases.js +3 -0
- package/dist/regenerate/chunk-plan.d.ts +0 -6
- package/dist/regenerate/chunk-plan.js +16 -4
- package/dist/regenerate/conformance-agents.js +2 -2
- package/dist/regenerate/conformance.js +6 -3
- package/dist/regenerate/diagnose.js +4 -2
- package/dist/regenerate/environment-error.d.ts +6 -0
- package/dist/regenerate/environment-error.js +19 -1
- 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 +18 -0
- package/dist/regenerate/jobs.js +49 -3
- package/dist/regenerate/ledger-view.d.ts +73 -0
- package/dist/regenerate/ledger-view.js +286 -0
- package/dist/regenerate/ledger.d.ts +274 -0
- package/dist/regenerate/ledger.js +349 -0
- package/dist/regenerate/lineage.d.ts +9 -1
- package/dist/regenerate/lineage.js +64 -12
- package/dist/regenerate/loop.d.ts +6 -0
- package/dist/regenerate/loop.js +36 -6
- package/dist/regenerate/records.d.ts +5 -3
- package/dist/regenerate/records.js +6 -4
- package/dist/regenerate/regenerate.js +7 -5
- package/dist/regenerate/regression.js +4 -2
- package/dist/regenerate/side-panel.d.ts +2 -0
- package/dist/regenerate/side-panel.js +4 -3
- package/dist/regenerate/spec-map.js +8 -5
- package/dist/regenerate/test-scripts.js +12 -1
- package/dist/regenerate/unit-review.js +3 -0
- package/dist/run-report.d.ts +1 -1
- package/dist/run-report.js +6 -0
- package/dist/self-invocation.d.ts +1 -1
- package/dist/self-invocation.js +3 -2
- package/dist/skill-progress.d.ts +1 -1
- package/dist/skill.d.ts +5 -0
- package/dist/skill.js +1 -0
- package/dist/skills.d.ts +14 -0
- package/dist/skills.js +19 -1
- package/dist/surface.d.ts +5 -0
- package/dist/surface.js +18 -0
- package/docs/authoring-plain.md +2 -2
- package/docs/config.md +4 -2
- package/docs/development.md +1 -0
- package/docs/headless.md +4 -0
- package/docs/hosting.md +45 -0
- package/docs/installation.md +1 -0
- package/docs/regenerating.md +5 -5
- package/docs/skills-and-gates.md +3 -1
- package/docs/using.md +33 -5
- package/genome/rules/func-specs.md +1 -0
- package/genome/rules/turri-regeneration.md +3 -0
- package/genome/skills/add-functional-spec/SKILL.md +12 -3
- package/genome/skills/add-functional-specs/SKILL.md +12 -2
- package/genome/skills/brood/SKILL.md +2 -0
- package/genome/skills/bud/SKILL.md +2 -0
- package/genome/skills/spawn/SKILL.md +47 -35
- package/package.json +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { readLedger } from "./regenerate/ledger.js";
|
|
3
|
+
import { describePastPulse, describeRuns, ledgerProjects, runViews } from "./regenerate/ledger-view.js";
|
|
4
|
+
const USAGE = `usage: turri ledger [pulse] [--runs <n>] [--json]
|
|
5
|
+
|
|
6
|
+
What turri did here, from the project's ledger (.turri/ledger): the last runs, a line a pulse - how each ended, how
|
|
7
|
+
long it took and what it cost. With a pulse's id, or the start of it, that pulse: its rounds, test runs, verdicts,
|
|
8
|
+
healers, what turri refused and what its history recorded. --json prints the events themselves, a JSON line each.
|
|
9
|
+
|
|
10
|
+
--runs <n> how many runs to show, the most recent first (default 5)
|
|
11
|
+
--json the events, as the ledger has them - the pulse's alone with a pulse`;
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
14
|
+
console.log(USAGE);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
const runsAt = args.indexOf("--runs");
|
|
18
|
+
const runs = runsAt >= 0 ? Number(args[runsAt + 1]) : 5;
|
|
19
|
+
if (runsAt >= 0 && (!Number.isInteger(runs) || runs < 1)) {
|
|
20
|
+
console.error(`--runs takes a number of runs, at least 1\n\n${USAGE}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const [pulse] = args.filter((arg, at) => !arg.startsWith("-") && (runsAt < 0 || at !== runsAt + 1));
|
|
24
|
+
const projects = ledgerProjects(process.cwd());
|
|
25
|
+
if (projects.length === 0) {
|
|
26
|
+
console.log("No ledger here: turri has not run in this folder, or in a project under it, since the ledger began.");
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
if (args.includes("--json")) {
|
|
30
|
+
for (const project of projects)
|
|
31
|
+
for (const entry of readLedger(project, pulse !== undefined ? { job: pulse } : {}))
|
|
32
|
+
console.log(JSON.stringify(entry));
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
if (pulse !== undefined) {
|
|
36
|
+
const described = describePastPulse(projects, pulse);
|
|
37
|
+
if (described === undefined) {
|
|
38
|
+
console.error(`The ledger has no pulse ${pulse}.`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
console.log(described);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
for (const project of projects) {
|
|
45
|
+
if (projects.length > 1)
|
|
46
|
+
console.log(`── ${resolve(project)}\n`);
|
|
47
|
+
console.log(describeRuns(runViews(project).slice(0, runs)));
|
|
48
|
+
if (projects.length > 1)
|
|
49
|
+
console.log("");
|
|
50
|
+
}
|
package/dist/loop-cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { configForSpec, inheritModel, modelOnly, modelSpec, projectModels, readP
|
|
|
5
5
|
import { PACKAGE_ROOT } from "./environment.js";
|
|
6
6
|
import { c } from "./module-report.js";
|
|
7
7
|
import { describeTurnIncident, onModelEvent } from "./regenerate/activity.js";
|
|
8
|
+
import { ledgerWanted, startLedger } from "./regenerate/ledger.js";
|
|
8
9
|
import { tryItCommand } from "./regenerate/delivery.js";
|
|
9
10
|
import { describeDiagnosis, diagnoseFailure } from "./regenerate/diagnose.js";
|
|
10
11
|
import { createJobRegistry, pulseName } from "./regenerate/jobs.js";
|
|
@@ -32,6 +33,9 @@ export async function runLoop(options) {
|
|
|
32
33
|
// The models as the config has them - with a command's own options read as its keys (headlessOptions), as the chat reads them.
|
|
33
34
|
const model = modelSpec(projectModels(project).coding);
|
|
34
35
|
const projectDir = projectDirOf(plainFile);
|
|
36
|
+
// What the run does, as facts, in the project's .turri/ledger.
|
|
37
|
+
if (ledgerWanted() && options.dryRun !== true)
|
|
38
|
+
startLedger({ mode: options.command?.[0] ?? "headless", project: projectDir, warn: (message) => console.error(message) });
|
|
35
39
|
const buildDir = options.buildDir !== undefined && options.dryRun !== true ? resolve(options.buildDir) : undefined;
|
|
36
40
|
if (buildDir !== undefined)
|
|
37
41
|
refuseBuildDir(buildDir, projectDir);
|
package/dist/loop-tools.js
CHANGED
|
@@ -9,6 +9,7 @@ import { pulseName } from "./regenerate/jobs.js";
|
|
|
9
9
|
import { stopDeployed } from "./regenerate/release.js";
|
|
10
10
|
import { projectDirOf } from "./regenerate/paths.js";
|
|
11
11
|
import { findTestScripts } from "./regenerate/test-scripts.js";
|
|
12
|
+
import { surfaceOf } from "./surface.js";
|
|
12
13
|
/** How the queue is to be planned, for the work this call queues - see regenerate/steer.ts. */
|
|
13
14
|
export const GUIDANCE_PARAM = Type.Optional(Type.String({
|
|
14
15
|
description: "How to plan the queue for this work, in a sentence or two - which functional specs go together, what first - as " +
|
|
@@ -114,8 +115,9 @@ export function stopJob(jobs, named, reason, loop) {
|
|
|
114
115
|
if (module !== undefined)
|
|
115
116
|
return module;
|
|
116
117
|
const matches = jobs.list().filter((job) => job.id.startsWith(named.trim()));
|
|
118
|
+
// /pulses is the terminal's: in a host, the pulses tool lists them.
|
|
117
119
|
if (matches.length === 0)
|
|
118
|
-
return `No pulse ${named}. /pulses shows them
|
|
120
|
+
return `No pulse ${named}. ${surfaceOf(jobs) !== undefined ? "The pulses tool lists them." : "/pulses shows them."}`;
|
|
119
121
|
if (matches.length > 1)
|
|
120
122
|
return `'${named}' matches ${matches.map((job) => job.id).join(", ")}. Give more of the id.`;
|
|
121
123
|
const target = matches[0];
|
package/dist/pi.js
CHANGED
|
@@ -9,6 +9,7 @@ import { checkAgainstScript, ENVIRONMENT_ERROR_TOOL, EnvironmentError, environme
|
|
|
9
9
|
import { MODULES_DIR, projectRootOf, sessionsDir } from "./regenerate/paths.js";
|
|
10
10
|
import { currentStopSignal, describeToolCall, describeToolResult, JobStoppedError, shownName, reportActivity, reportNow, reportAgent, reportTranscript, reportModelEvent, reportUsage, } from "./regenerate/activity.js";
|
|
11
11
|
import { debugLogOf, startDebugLog } from "./regenerate/debug-log.js";
|
|
12
|
+
import { firstLine, record } from "./regenerate/ledger.js";
|
|
12
13
|
let thinkingSource;
|
|
13
14
|
/**
|
|
14
15
|
* Where sub-agents take their thinking level from: the conversation that started them.
|
|
@@ -280,6 +281,8 @@ async function askOnce(session, text, onDelta) {
|
|
|
280
281
|
// Calls to one tool failing one after another - arguments it refuses, or errors - with nothing going through.
|
|
281
282
|
let failing = 0;
|
|
282
283
|
let failingTool;
|
|
284
|
+
/** When each tool call under way started, by its id: how long it took is recorded as it ends. */
|
|
285
|
+
const toolStarted = new Map();
|
|
283
286
|
harnessStops.delete(session);
|
|
284
287
|
reportNow("waiting for the model");
|
|
285
288
|
// Every entry says which agent it is from, and what it runs on - for reading one agent at a time.
|
|
@@ -348,6 +351,7 @@ async function askOnce(session, text, onDelta) {
|
|
|
348
351
|
}
|
|
349
352
|
}
|
|
350
353
|
if (event.type === "tool_execution_start") {
|
|
354
|
+
toolStarted.set(event.toolCallId, Date.now());
|
|
351
355
|
const args = JSON.stringify(event.args ?? {});
|
|
352
356
|
transcript({ kind: "tool", text: `${shownName(event.toolName)} ${clipText(args, MAX_TOOL_ARGS)}`, full: `${shownName(event.toolName)} ${args}` });
|
|
353
357
|
const took = Date.now() - stepStarted;
|
|
@@ -369,6 +373,9 @@ async function askOnce(session, text, onDelta) {
|
|
|
369
373
|
stopTurn(session, `${failing} ${shownName(event.toolName)} calls failed in a row`);
|
|
370
374
|
}
|
|
371
375
|
const output = resultText(event.result);
|
|
376
|
+
const started = toolStarted.get(event.toolCallId);
|
|
377
|
+
toolStarted.delete(event.toolCallId);
|
|
378
|
+
record({ kind: "tool.call", tool: event.toolName, ms: started !== undefined ? Date.now() - started : 0, ok: !event.isError, resultChars: output.length }, { agent: modelEventOf(session) });
|
|
372
379
|
transcript({ kind: "result", text: clipText(output, MAX_TOOL_RESULT), error: event.isError, full: output });
|
|
373
380
|
const line = describeToolResult(event.toolName, event.result, event.isError);
|
|
374
381
|
if (line !== undefined)
|
|
@@ -483,6 +490,7 @@ async function askOnce(session, text, onDelta) {
|
|
|
483
490
|
// A turn turri ended is ended, whatever the provider was doing then: what comes back of the abort is not the provider failing.
|
|
484
491
|
if (providerError !== undefined && stoppedFor === undefined) {
|
|
485
492
|
debug?.note(`turn ended with a provider error after ${seconds(Date.now() - turnStarted)}: ${providerError}`);
|
|
493
|
+
record({ kind: "provider.error", error: firstLine(providerError, 300), transient: TRANSIENT_PROVIDER_ERROR.test(providerError) }, { agent: modelEventOf(session) });
|
|
486
494
|
throw new ProviderError(`${session.model?.id ?? "model"}: ${providerError}`);
|
|
487
495
|
}
|
|
488
496
|
debug?.note(`turn finished after ${seconds(Date.now() - turnStarted)}`);
|
|
@@ -659,6 +667,11 @@ export function guardRepeats(tools, session, budget) {
|
|
|
659
667
|
// Calls refused in the restricted turn under way, and which turn's set that was.
|
|
660
668
|
let refused = 0;
|
|
661
669
|
let refusedIn;
|
|
670
|
+
/** The agent a refusal is of, for the ledger. */
|
|
671
|
+
const guardPlace = () => {
|
|
672
|
+
const current = session();
|
|
673
|
+
return current !== undefined ? { agent: modelEventOf(current) } : {};
|
|
674
|
+
};
|
|
662
675
|
return tools.map((tool) => ({
|
|
663
676
|
...tool,
|
|
664
677
|
execute: async (...args) => {
|
|
@@ -669,6 +682,7 @@ export function guardRepeats(tools, session, budget) {
|
|
|
669
682
|
if (refusedIn !== only)
|
|
670
683
|
(refused = 0), (refusedIn = only);
|
|
671
684
|
refused += 1;
|
|
685
|
+
record({ kind: "guard.refused", rule: "restricted", tool: tool.name, times: refused }, guardPlace());
|
|
672
686
|
if (refused > REFUSED_BEFORE_STOP) {
|
|
673
687
|
reportActivity(` stopped the agent's turn: ${refused} calls to tools this turn does not have`);
|
|
674
688
|
stopTurn(current, `${refused} calls to tools this turn does not have`);
|
|
@@ -679,6 +693,7 @@ export function guardRepeats(tools, session, budget) {
|
|
|
679
693
|
}
|
|
680
694
|
calls += 1;
|
|
681
695
|
if (budget !== undefined && !budget.finish.includes(tool.name) && calls > budget.calls) {
|
|
696
|
+
record({ kind: "guard.refused", rule: "budget", tool: tool.name, times: calls - budget.calls }, guardPlace());
|
|
682
697
|
if (calls > budget.calls * 2) {
|
|
683
698
|
reportActivity(` stopped the agent's turn: ${calls} tool calls, past its budget of ${budget.calls}`);
|
|
684
699
|
stopTurn(session(), `${calls} tool calls, past its budget of ${budget.calls}`);
|
|
@@ -707,6 +722,8 @@ export function guardRepeats(tools, session, budget) {
|
|
|
707
722
|
// Anything else may have changed what a look would find.
|
|
708
723
|
looks.clear();
|
|
709
724
|
}
|
|
725
|
+
if (repeats > REPEATS_ALLOWED)
|
|
726
|
+
record({ kind: "guard.refused", rule: "repeat", tool: tool.name, times: repeats }, guardPlace());
|
|
710
727
|
if (repeats > REPEATS_BEFORE_STOP) {
|
|
711
728
|
reportActivity(` stopped the agent's turn: it made the same ${tool.name} call ${repeats} times with nothing changed`);
|
|
712
729
|
stopTurn(session(), `the same ${tool.name} call ${repeats} times with nothing changed`);
|
|
@@ -36,6 +36,9 @@ export interface ActivitySink {
|
|
|
36
36
|
kind: string;
|
|
37
37
|
summary: string;
|
|
38
38
|
};
|
|
39
|
+
/** The project the job works in, and its module: where what it does is recorded (ledger.ts). */
|
|
40
|
+
project?: string;
|
|
41
|
+
module?: string;
|
|
39
42
|
/** The spec was found to contradict itself - for the person to decide. */
|
|
40
43
|
specConflict?(conflict: SpecConflict): void;
|
|
41
44
|
/** The functional spec being built now, or nothing when none is. */
|
|
@@ -89,6 +92,12 @@ export declare function currentJob(): {
|
|
|
89
92
|
kind: string;
|
|
90
93
|
summary: string;
|
|
91
94
|
} | undefined;
|
|
95
|
+
/** Where this code runs: the job it runs for, its project and its module - each when it runs for one. */
|
|
96
|
+
export declare function currentPlace(): {
|
|
97
|
+
job?: string;
|
|
98
|
+
project?: string;
|
|
99
|
+
module?: string;
|
|
100
|
+
};
|
|
92
101
|
/**
|
|
93
102
|
* The conversation an agent is having, as it happens: what it was asked, what it wrote and
|
|
94
103
|
* thought, what it called and what came back. Text and thinking arrive as deltas and are
|
|
@@ -22,6 +22,17 @@ export function currentStopSignal() {
|
|
|
22
22
|
export function currentJob() {
|
|
23
23
|
return store.getStore()?.job;
|
|
24
24
|
}
|
|
25
|
+
/** Where this code runs: the job it runs for, its project and its module - each when it runs for one. */
|
|
26
|
+
export function currentPlace() {
|
|
27
|
+
const sink = store.getStore();
|
|
28
|
+
if (sink === undefined)
|
|
29
|
+
return {};
|
|
30
|
+
return {
|
|
31
|
+
...(sink.job !== undefined ? { job: sink.job.id } : {}),
|
|
32
|
+
...(sink.project !== undefined ? { project: sink.project } : {}),
|
|
33
|
+
...(sink.module !== undefined ? { module: sink.module } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
25
36
|
export function reportTranscript(entry) {
|
|
26
37
|
store.getStore()?.transcript(entry);
|
|
27
38
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dirname, join, resolve, sep } from "node:path";
|
|
2
2
|
import { readProjectConfig, stackOf } from "../config.js";
|
|
3
3
|
import { reportNote } from "./activity.js";
|
|
4
|
+
import { record } from "./ledger.js";
|
|
4
5
|
import { agentEnvironment, DARWIN_TEMP_SUFFIX, packageCacheDir, TOOLCHAIN_VARIABLES } from "./agent-env.js";
|
|
5
6
|
import { afterCommand, auditMessage, beforeCommand } from "./audit.js";
|
|
6
7
|
import { checkCommand, machineReadable } from "./command-check.js";
|
|
@@ -67,6 +68,9 @@ export function agentShell(role, options = {}) {
|
|
|
67
68
|
throw error;
|
|
68
69
|
}
|
|
69
70
|
const result = afterCommand(before);
|
|
71
|
+
if (result.putBack.length + result.left.length + result.lost.length + result.conversation.length > 0) {
|
|
72
|
+
record({ kind: "audit.putBack", putBack: result.putBack.length, left: result.left.length, lost: result.lost.length, conversation: result.conversation.length });
|
|
73
|
+
}
|
|
70
74
|
if (result.putBack.length > 0)
|
|
71
75
|
reportNote(` an agent's command changed ${result.putBack.join(", ")} - put back`);
|
|
72
76
|
if (result.left.length > 0)
|
|
@@ -90,6 +94,7 @@ export function agentShell(role, options = {}) {
|
|
|
90
94
|
async run(command, runOptions = {}) {
|
|
91
95
|
const checked = checkCommand(command, commandFence);
|
|
92
96
|
if (!checked.ok) {
|
|
97
|
+
record({ kind: "command.refused", rule: checked.rule });
|
|
93
98
|
reportNote(` refused an agent's command (${checked.rule}): ${command.length > 120 ? `${command.slice(0, 120)}...` : command}`);
|
|
94
99
|
const output = `Not run: ${checked.reason}`;
|
|
95
100
|
runOptions.onData?.(Buffer.from(output));
|
package/dist/regenerate/cases.js
CHANGED
|
@@ -64,6 +64,9 @@ export function conformanceCasesBlock(cases) {
|
|
|
64
64
|
if (cases.patch.length > 0) {
|
|
65
65
|
lines.push(`- Changed - its text, its acceptance tests, or a concept it uses: ${named(cases.patch)}.`, " Its conformance tests exist: rewrite them to what it says now. The tests of everything else stay as they are.");
|
|
66
66
|
}
|
|
67
|
+
if (cases.again.length > 0) {
|
|
68
|
+
lines.push(`- Regenerated again - its code was built anew, and its conformance tests were written against the code it replaced: ${named(cases.again)}.`, " Its text did not change, but the code did. Run its tests against the new code. What they check that the", " specification says stays checked: rewrite such a test only as the new code needs it reached. What they check", " that the specification leaves open - a path, a field, a message, a format the old code chose and the", " specification never names - is the new code's choice now: rewrite those checks to what the new code does,", " or take them out. No test may hold the new code to what only the old code did. The tests of everything else", " stay as they are.");
|
|
69
|
+
}
|
|
67
70
|
if (cases.delete.length > 0)
|
|
68
71
|
lines.push(`- Taken out - remove its conformance tests: ${named(cases.delete)}.`);
|
|
69
72
|
if (cases.mechanical) {
|
|
@@ -147,12 +147,6 @@ export interface ChunkAgents {
|
|
|
147
147
|
export declare function groupProblems(specs: NewSpec[], answer: GroupAnswer, maxSpecs?: number): string[];
|
|
148
148
|
/** What is wrong with a placement: every new req once, each where there is such a job. */
|
|
149
149
|
export declare function placeProblems(input: PlaceInput, answer: PlaceAnswer): string[];
|
|
150
|
-
/**
|
|
151
|
-
* Cut the queue's work - from `input.base` to `input.current` - into jobs, as the three steps say. Returns the draft
|
|
152
|
-
* the work loop runs: checked (checkDraft), or, when nothing holds, the whole work as one job. `notes` is given what
|
|
153
|
-
* 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
|
|
154
|
-
* group split for its size - for the conversation to say, rather than guess.
|
|
155
|
-
*/
|
|
156
150
|
export declare function planChunks(input: PlanInput, agents?: ChunkAgents, notes?: string[]): Promise<DraftJob[]>;
|
|
157
151
|
export declare function groupPrompt(specs: NewSpec[], steering?: Steering[], maxSpecs?: number): string;
|
|
158
152
|
export declare function placePrompt(input: PlaceInput): string;
|
|
@@ -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
|
}
|
|
@@ -316,9 +316,9 @@ export function createAskExplorerTool(options) {
|
|
|
316
316
|
return defineTool({
|
|
317
317
|
name: "ask_explorer",
|
|
318
318
|
label: "Ask the explorer",
|
|
319
|
-
description: "Ask the explorer - who read
|
|
319
|
+
description: "Ask the explorer - who read the code, and wrote the account of it you were given - something the account " +
|
|
320
320
|
"does not say: how a functionality is reached, what a field is called, what the software needs around it. It reads " +
|
|
321
|
-
`
|
|
321
|
+
`the code to answer - it runs nothing, so what only running the software shows is yours to try. ${options.questions ?? EXPLORER_QUESTIONS} questions a run: ask what reading the code yourself would take long to find.`,
|
|
322
322
|
promptSnippet: "ask_explorer: a question about how the software is reached, answered by the explorer from the code",
|
|
323
323
|
parameters: Type.Object({ question: Type.String({ description: "What you need to know, exactly" }) }),
|
|
324
324
|
async execute(_id, params) {
|
|
@@ -5,7 +5,8 @@ import { PLAIN_SOURCE_FILE_EXTENSION, plainFileParser } from "../plain/parser.js
|
|
|
5
5
|
import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, getFrids, getInheritedFunctionalSpecs, getSpecificationsForFrid, TEST_REQUIREMENTS, } from "../plain/spec.js";
|
|
6
6
|
import { reportActivity, reportLogNote, reportSpec, reportStage, unitOf } from "./activity.js";
|
|
7
7
|
import { confinedTools } from "./confine.js";
|
|
8
|
-
import { abandonWork, diffSinceSnapshot,
|
|
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) {
|
|
@@ -797,9 +800,9 @@ export async function renderConformanceTests(options) {
|
|
|
797
800
|
// The tests, and the code when it changed: one commit if they share a module folder.
|
|
798
801
|
const subject = `conformance ${moduleName} ${options.diff !== undefined ? "for a spec change" : `fs ${frid}`}${changed ? " (code changed)" : ""}`;
|
|
799
802
|
stage(`committing the conformance tests${changed ? " and the code changes" : ""}`);
|
|
800
|
-
|
|
803
|
+
landWork(conformanceDir, subject);
|
|
801
804
|
if (moduleDirOf(buildDir) !== moduleDirOf(conformanceDir))
|
|
802
|
-
|
|
805
|
+
landWork(buildDir, subject);
|
|
803
806
|
}
|
|
804
807
|
else if (start !== undefined) {
|
|
805
808
|
const why = notLandedWhy(options.diff !== undefined ? "for the spec change" : `of functionality ${frid}`, unlabelled);
|
|
@@ -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();
|
|
@@ -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
|
|
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;
|
|
@@ -163,6 +172,15 @@ export declare function createJobRegistry(options?: {
|
|
|
163
172
|
export declare function formatDuration(job: Job): string;
|
|
164
173
|
/** "42s", "3m 12s", "1h 5m": how long something has run. */
|
|
165
174
|
export declare function formatElapsed(ms: number): string;
|
|
175
|
+
/**
|
|
176
|
+
* How soon after `since` the first code pulse queued since then started - the time to first regeneration - and how soon
|
|
177
|
+
* the first of them landed, when one has: from then on there is something to try. In milliseconds; either is undefined
|
|
178
|
+
* until it has happened.
|
|
179
|
+
*/
|
|
180
|
+
export declare function firstRegeneration(jobs: Job[], since: number): {
|
|
181
|
+
started?: number;
|
|
182
|
+
landed?: number;
|
|
183
|
+
};
|
|
166
184
|
/**
|
|
167
185
|
* What a pulse is called wherever it is shown in a line: its module, kind and number - `shorty · regenerate pulse 3`,
|
|
168
186
|
* `shorty · conformance pulse 6` - never what it regenerates, which is the agents' words and long. A job of no module,
|