@turri-dev/cli 0.1.3-next.79e2ad0 → 0.1.3-next.b675516
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 +1 -1
- package/dist/config.js +3 -2
- package/dist/environment.js +19 -0
- package/dist/extension.js +134 -52
- package/dist/identity.js +1 -1
- package/dist/loop-cli.js +1 -1
- package/dist/loop-tools.js +1 -1
- package/dist/pi-launch.js +13 -4
- package/dist/picture-row.js +21 -0
- package/dist/regenerate/confine.js +1 -0
- package/dist/regenerate/conformance-agents.js +220 -11
- package/dist/regenerate/conformance.js +87 -33
- package/dist/regenerate/debugger.js +27 -3
- package/dist/regenerate/environment-error.js +63 -9
- package/dist/regenerate/panel-dock.js +86 -0
- package/dist/regenerate/prompt.js +3 -1
- package/dist/regenerate/regenerate.js +24 -6
- package/dist/regenerate/side-panel.js +14 -4
- package/dist/regenerate/stop-report.js +13 -0
- package/dist/regenerate/test-scripts.js +45 -9
- package/dist/regenerate/unit-review.js +11 -0
- package/dist/regenerate-cli.js +1 -1
- package/dist/run-report.js +2 -22
- package/docs/config.md +3 -2
- package/docs/development.md +7 -0
- package/docs/headless.md +2 -2
- package/docs/regenerating.md +66 -15
- package/docs/using.md +16 -5
- package/package.json +3 -3
|
@@ -4,6 +4,7 @@ import { Type } from "typebox";
|
|
|
4
4
|
import { ask, BEGIN, createExecutor } from "../pi.js";
|
|
5
5
|
import { confinedTools } from "./confine.js";
|
|
6
6
|
import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
7
|
+
import { runTiming } from "./test-scripts.js";
|
|
7
8
|
/**
|
|
8
9
|
* A fresh look at a failure the coding agent keeps hitting.
|
|
9
10
|
*
|
|
@@ -24,9 +25,12 @@ export function diagnosisText(diagnosis) {
|
|
|
24
25
|
}
|
|
25
26
|
function debugPrompt(input) {
|
|
26
27
|
const script = input.script !== undefined && existsSync(input.script) ? readFileSync(input.script, "utf8") : undefined;
|
|
28
|
+
const timedOut = input.run?.timedOut === true;
|
|
27
29
|
return [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
timedOut
|
|
31
|
+
? `The unit testing script of the codebase in ${input.buildDir} did not finish: the time limit stopped it. You look at it with fresh eyes.`
|
|
32
|
+
: `The unit tests of the codebase in ${input.buildDir} have failed the same way ${input.times} times in a row. The coding agent`,
|
|
33
|
+
...(timedOut ? [] : ["keeps changing things and the failure does not move: it has not seen the cause. You look at it with fresh eyes."]),
|
|
30
34
|
"",
|
|
31
35
|
"Find the root cause - the one thing that makes this failure happen - and the change that fixes it. Read the failing",
|
|
32
36
|
"run below, the tests it names, the code they exercise, the build files (pyproject.toml, package.json, go.mod, pom.xml,",
|
|
@@ -37,6 +41,21 @@ function debugPrompt(input) {
|
|
|
37
41
|
"",
|
|
38
42
|
"You change nothing - you only read. When you know, call submit_diagnosis: the root cause, with the evidence for it,",
|
|
39
43
|
"and the fix - which files, and exactly what to change in them.",
|
|
44
|
+
...(timedOut
|
|
45
|
+
? [
|
|
46
|
+
"",
|
|
47
|
+
"<timed out>",
|
|
48
|
+
"The run's output ends where the time limit stopped it: the test running then, or the step after the last one it",
|
|
49
|
+
"printed, is what did not finish. Find what kept it:",
|
|
50
|
+
"- something hangs - a test or the code waits on input, keeps a server in the foreground, never exits, loops, blocks",
|
|
51
|
+
" on a lock or a network call that never answers: the root cause and the fix, as for any failure;",
|
|
52
|
+
"- nothing hangs - the output shows the run making steady progress right up to the limit (installing, building, test",
|
|
53
|
+
" after test passing) and it needs more time than a test run may take: set too_slow, and say in root_cause from the",
|
|
54
|
+
" output how far it got and what took the time. No agent can give it more time: the work stops, and the person is",
|
|
55
|
+
" told to raise the limit. Judge from the output and the files, never from the time alone.",
|
|
56
|
+
"</timed out>",
|
|
57
|
+
]
|
|
58
|
+
: []),
|
|
40
59
|
"",
|
|
41
60
|
"<what the change is meant to do>",
|
|
42
61
|
input.task.trim(),
|
|
@@ -44,6 +63,7 @@ function debugPrompt(input) {
|
|
|
44
63
|
...(script !== undefined ? ["", `<testing script path="${input.script}">`, script.trimEnd(), "</testing script>"] : []),
|
|
45
64
|
"",
|
|
46
65
|
"<failing run>",
|
|
66
|
+
...(input.run !== undefined ? [runTiming(input.run), ""] : []),
|
|
47
67
|
input.failure.trimEnd().slice(-8000),
|
|
48
68
|
"</failing run>",
|
|
49
69
|
].join("\n");
|
|
@@ -58,10 +78,14 @@ export async function debugFailure(input) {
|
|
|
58
78
|
parameters: Type.Object({
|
|
59
79
|
root_cause: Type.String({ description: "The one thing that makes the failure happen, and the evidence for it: the lines of the run, the code, the docs" }),
|
|
60
80
|
fix: Type.String({ description: "The change that fixes it: which files, and exactly what to change in them" }),
|
|
81
|
+
too_slow: Type.Optional(Type.Boolean({
|
|
82
|
+
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
|
+
})),
|
|
61
84
|
}),
|
|
62
85
|
async execute(_id, params) {
|
|
63
86
|
const given = params;
|
|
64
|
-
|
|
87
|
+
// 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 } : {}) };
|
|
65
89
|
return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
|
|
66
90
|
},
|
|
67
91
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { basename, dirname, isAbsolute, sep } from "node:path";
|
|
3
4
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { Type } from "typebox";
|
|
@@ -35,7 +36,7 @@ export class EnvironmentError extends Error {
|
|
|
35
36
|
sandbox;
|
|
36
37
|
constructor(details) {
|
|
37
38
|
super(details.sandbox !== undefined
|
|
38
|
-
? `${details.moduleName} stopped: turri's sandbox kept its agents from ${details.sandbox.paths[0]}, which is on this machine.`
|
|
39
|
+
? `${details.moduleName} stopped: turri's sandbox kept its agents from ${details.sandbox.access === "write" ? "writing" : "reading"} ${details.sandbox.paths[0]}, which is on this machine.`
|
|
39
40
|
: `${details.moduleName} stopped: the environment lacks ${details.missing}.`);
|
|
40
41
|
this.name = "EnvironmentError";
|
|
41
42
|
this.moduleName = details.moduleName;
|
|
@@ -50,6 +51,10 @@ export class EnvironmentError extends Error {
|
|
|
50
51
|
this.sandbox = details.sandbox;
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
/** A file's name that says it is a lock: `lockfile`, `x.lock`, `x.lck`, `.lock-x`, `x.lock.y`. */
|
|
55
|
+
const LOCK_FILE = /(?:^|[._-])(?:lock|lck|lockfile)(?:$|[._-])/i;
|
|
56
|
+
/** What says the sandbox refused a write, not a read: writing, creating, a lock not taken - not a name with "lock" in it. */
|
|
57
|
+
const WRITE_REFUSED = /\b(?:writ(?:e|es|ing|able)|creat(?:e|es|ing)|acquir(?:e|ing)|(?:take|obtain|get) (?:a |the )?lock|locking|read-only file system)\b/i;
|
|
53
58
|
/** A folder named for a version - `24.16.0`, `v22.3.0`, `3.12.4` - the root of a version manager's install. */
|
|
54
59
|
const VERSION = /^v?\d+\.\d+/;
|
|
55
60
|
/** The parts of an install below its root, where the root ends: `…/24.16.0/lib/node_modules/…` ends before `lib`. */
|
|
@@ -74,27 +79,59 @@ export function sandboxGapOf(text, options = {}) {
|
|
|
74
79
|
const system = (options.platform ?? process.platform) === "darwin" ? MACOS_MACHINE : LINUX_MACHINE;
|
|
75
80
|
const within = (path, root) => path === root || path.startsWith(`${root}/`);
|
|
76
81
|
const named = [...text.matchAll(/(?:^|[\s'"`(=:])(\/[^\s'"`()<>,;]+)/g)].map((match) => match[1].replace(/[.:]+$/, ""));
|
|
77
|
-
const
|
|
78
|
-
|
|
82
|
+
const isLock = (path) => LOCK_FILE.test(basename(path));
|
|
83
|
+
// A lock file the tool could not create is not there: its folder is.
|
|
84
|
+
const found = [...new Set(named)].filter((path) => path.split("/").length > 3 &&
|
|
85
|
+
(exists(path) || (isLock(path) && exists(dirname(path)))) &&
|
|
86
|
+
!system.some((root) => within(path, root)) &&
|
|
87
|
+
(options.project === undefined || !within(path, options.project)));
|
|
88
|
+
if (found.length === 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
// The lock first: it is what the build could not take.
|
|
91
|
+
const paths = [...found.filter(isLock), ...found.filter((path) => !isLock(path))];
|
|
92
|
+
const lock = isLock(paths[0]);
|
|
93
|
+
return { paths, folder: toolchainFolder(paths[0]), access: lock || WRITE_REFUSED.test(text) ? "write" : "read", ...(lock ? { lock } : {}) };
|
|
79
94
|
}
|
|
80
95
|
/**
|
|
81
96
|
* What the person is told, in the chat, at the end of `turri regenerate`, and in the job's log. `rerun`: the command that runs
|
|
82
97
|
* the work again, as the pulse knows it - the spec the person ran; without it, the module's own spec by its name.
|
|
83
98
|
*/
|
|
84
99
|
export function describeEnvironmentError(error, rerun) {
|
|
85
|
-
|
|
100
|
+
const again = `Then re-run: \`${rerun ?? `turri regenerate ${error.moduleName}.plain`}\`, or say "resume" in the chat.`;
|
|
101
|
+
const needed = `Needed by: ${error.neededBy}${error.shownBy !== undefined ? ` (${error.shownBy})` : ""}.`;
|
|
102
|
+
const gap = error.sandbox;
|
|
103
|
+
if (gap?.access === "write") {
|
|
104
|
+
const what = gap.lock === true ? `the lock file \`${gap.paths[0]}\`, which the build takes as it runs` : `\`${gap.paths[0]}\``;
|
|
105
|
+
const home = homedir();
|
|
106
|
+
const inHome = gap.paths[0] === home || gap.paths[0].startsWith(`${home}/`);
|
|
86
107
|
return [
|
|
87
|
-
`**${error.moduleName} stopped: turri's sandbox kept its agents from
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
`**${error.moduleName} stopped: turri's sandbox kept its agents from writing ${what} - it is on this machine; nothing is missing, and nothing needs installing.**`,
|
|
109
|
+
needed,
|
|
110
|
+
"An agent's commands write only their module's folders, their own home and temporary folder, and the package caches turri " +
|
|
111
|
+
"points the build tools at - no setting lets them write anywhere else, and `toolchains:` lets them read a folder, not write it.",
|
|
112
|
+
inHome
|
|
113
|
+
? "To fix: it is in your home, not the agents' own - the tool found your home without `$HOME`, as a JVM does for `user.home`, " +
|
|
114
|
+
"or a setting of yours points it there. Have it use `$HOME` instead - for a JVM tool, `-Duser.home=$HOME` where the build " +
|
|
115
|
+
"starts its JVM - and the agents' own home takes it."
|
|
116
|
+
: `To fix: it is in a toolchain's own install, \`${gap.folder}\`, which the build writes into as it runs - and the agents may ` +
|
|
117
|
+
"not change a toolchain. Have the build keep it elsewhere if the tool has a setting for it, or install a copy of the " +
|
|
118
|
+
"toolchain that keeps its writes in its cache.",
|
|
119
|
+
again,
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
if (gap !== undefined) {
|
|
123
|
+
return [
|
|
124
|
+
`**${error.moduleName} stopped: turri's sandbox kept its agents from reading \`${gap.paths[0]}\` - it is on this machine; nothing is missing.**`,
|
|
125
|
+
needed,
|
|
126
|
+
`To fix: let the agents read that toolchain - add its folder to \`toolchains:\` in config.turri.yaml, keeping what is there: \`toolchains: [${gap.folder}]\`. Nothing needs installing.`,
|
|
127
|
+
again,
|
|
91
128
|
].join("\n");
|
|
92
129
|
}
|
|
93
130
|
return [
|
|
94
131
|
`**${error.moduleName} stopped: the environment lacks ${error.missing}.**`,
|
|
95
132
|
`Needed by: ${error.neededBy}${error.shownBy !== undefined ? ` (${error.shownBy})` : ""}.`,
|
|
96
133
|
`To fix: ${error.fix ?? "provide it on this machine, then run it again."}`,
|
|
97
|
-
|
|
134
|
+
again,
|
|
98
135
|
].join("\n");
|
|
99
136
|
}
|
|
100
137
|
/** The name agents know the tool by - their wall text names it. */
|
|
@@ -187,6 +224,23 @@ export function toolchainError(missing, details) {
|
|
|
187
224
|
source: "harness",
|
|
188
225
|
});
|
|
189
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* A testing script the time limit stopped while it was still making progress - judged so by the judge or the debugger,
|
|
229
|
+
* from where its output stops: nothing hangs, the suite needs more time than a test run may take. No agent can give it
|
|
230
|
+
* that, so the pulse stops and the person is told how to.
|
|
231
|
+
*/
|
|
232
|
+
export function timeLimitError(run, details) {
|
|
233
|
+
const limit = Math.round(run.timeoutMs / 1000);
|
|
234
|
+
return new EnvironmentError({
|
|
235
|
+
moduleName: details.moduleName,
|
|
236
|
+
missing: `more time for a test run than its ${limit}s limit`,
|
|
237
|
+
neededBy: `the ${details.kind} testing script, \`${basename(details.script)}\``,
|
|
238
|
+
shownBy: `it ran ${Math.round((run.elapsedMs ?? run.timeoutMs) / 1000)}s and was stopped at the limit, still making progress - ${details.reason.replace(/\s+/g, " ").trim().slice(0, 400)}`,
|
|
239
|
+
fix: `give a test run more time - set TURRI_COMMAND_TIMEOUT, in seconds, above ${limit} (e.g. TURRI_COMMAND_TIMEOUT=${limit * 3}) in the ` +
|
|
240
|
+
"environment turri runs in - or make the suite faster: what it installs or builds on every run, and the tests that take longest.",
|
|
241
|
+
source: "agent",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
190
244
|
/** The harness's run of a testing script, when it could not find its toolchain: the error that stops the pulse. */
|
|
191
245
|
export function missingToolchainError(run, details) {
|
|
192
246
|
const missing = missingToolchain(run);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { HStack } from "@earendil-works/pi-tui";
|
|
2
|
+
/**
|
|
3
|
+
* The side panel beside the chat rather than over it: the chat drawn on the left in what the panel leaves it, the panel
|
|
4
|
+
* laid over the columns on the right.
|
|
5
|
+
*
|
|
6
|
+
* pi draws its screen one of two ways, and the chat is narrowed in each as it is drawn: full screen (pi's default), a
|
|
7
|
+
* layout - its root put in a row beside a filler as wide as the panel; the regular screen, the lines its render gives -
|
|
8
|
+
* asked for in fewer columns.
|
|
9
|
+
*/
|
|
10
|
+
/** Narrower than this, the chat and the panel have no room side by side: the panel takes the whole screen. */
|
|
11
|
+
const SHARED_FROM = 100;
|
|
12
|
+
/** The columns the panel takes beside the chat: half the screen, the rule between them in the middle - 0 when the terminal is too narrow to share. */
|
|
13
|
+
export function dockedWidth(columns) {
|
|
14
|
+
return columns < SHARED_FROM ? 0 : Math.floor(columns / 2);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The chat narrowed by `panelWidth()` columns, read at each frame - 0 gives it the whole width. Returns what gives it the
|
|
18
|
+
* whole width back for good.
|
|
19
|
+
*/
|
|
20
|
+
export function narrowChat(tui, panelWidth) {
|
|
21
|
+
const screen = tui;
|
|
22
|
+
if (typeof screen.setLayoutRoot === "function" && screen.layoutRoot !== undefined) {
|
|
23
|
+
const root = screen.layoutRoot;
|
|
24
|
+
// A line as wide as the panel: the row gives the filler what it measures, the chat the rest.
|
|
25
|
+
const filler = { render: () => [" ".repeat(panelWidth())], invalidate() { } };
|
|
26
|
+
screen.setLayoutRoot(new HStack([{ component: root, basis: 0, grow: 1 }, { component: filler }]));
|
|
27
|
+
return () => screen.setLayoutRoot(root);
|
|
28
|
+
}
|
|
29
|
+
// Read from the prototype: the screen's own render, not one laid over it.
|
|
30
|
+
const render = Object.getPrototypeOf(tui).render;
|
|
31
|
+
screen.render = function (width) {
|
|
32
|
+
return render.call(this, Math.max(1, width - panelWidth()));
|
|
33
|
+
};
|
|
34
|
+
return () => {
|
|
35
|
+
screen.render = render;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A press of a mouse button, as pi's full screen takes it - `side` told whether it was in the chat, left of `chatWidth()`
|
|
40
|
+
* columns, or in the panel - before the press goes on as it would have. Motion and the wheel are not presses. pi's
|
|
41
|
+
* regular screen reads no mouse: nothing is done there. Returns what takes the watch off.
|
|
42
|
+
*/
|
|
43
|
+
export function clickToFocus(tui, chatWidth, side) {
|
|
44
|
+
const screen = tui;
|
|
45
|
+
const handle = screen.handleMouseEvent;
|
|
46
|
+
if (typeof handle !== "function")
|
|
47
|
+
return () => { };
|
|
48
|
+
screen.handleMouseEvent = function (raw) {
|
|
49
|
+
// 32: motion; 64: the wheel.
|
|
50
|
+
if (raw.release !== true && (raw.button & (32 | 64)) === 0)
|
|
51
|
+
side(raw.x < chatWidth() ? "chat" : "panel");
|
|
52
|
+
return handle.call(this, raw);
|
|
53
|
+
};
|
|
54
|
+
return () => {
|
|
55
|
+
screen.handleMouseEvent = handle;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A line drawn faint - the side that does not have the keys, in the shade. Faint (SGR 2) holds through a line's colours;
|
|
60
|
+
* only a reset - SGR 0, or 22 - ends it, so it is set again after each one. A line holding an image is left as it is.
|
|
61
|
+
*/
|
|
62
|
+
export function shade(line) {
|
|
63
|
+
// Kitty's image escape, or iTerm2's.
|
|
64
|
+
if (line.includes("\x1b_G") || line.includes("\x1b]1337;File="))
|
|
65
|
+
return line;
|
|
66
|
+
const again = line.replace(/\x1b\[([\d;]*)m/g, (sgr, params) => (params.split(";").some((param) => param === "" || param === "0" || param === "22") ? `${sgr}${FAINT}` : sgr));
|
|
67
|
+
return `${FAINT}${again}${NOT_FAINT}`;
|
|
68
|
+
}
|
|
69
|
+
const FAINT = "\x1b[2m";
|
|
70
|
+
const NOT_FAINT = "\x1b[22m";
|
|
71
|
+
/**
|
|
72
|
+
* The chat in the shade while `shaded()`: each of its rows drawn faint as the screen lays the overlays over it - the
|
|
73
|
+
* docked panel the last of them, which covers its own columns. Returns what takes the shade off.
|
|
74
|
+
*/
|
|
75
|
+
export function shadeChat(tui, shaded) {
|
|
76
|
+
const screen = tui;
|
|
77
|
+
const composite = screen.compositeLineAt;
|
|
78
|
+
if (typeof composite !== "function")
|
|
79
|
+
return () => { };
|
|
80
|
+
screen.compositeLineAt = function (base, overlay, col, width, total) {
|
|
81
|
+
return composite.call(this, shaded() ? shade(base) : base, overlay, col, width, total);
|
|
82
|
+
};
|
|
83
|
+
return () => {
|
|
84
|
+
screen.compositeLineAt = composite;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -278,8 +278,10 @@ function itemText(serialized) {
|
|
|
278
278
|
return item;
|
|
279
279
|
const { markdown, [ACCEPTANCE_TESTS]: tests } = (item ?? {});
|
|
280
280
|
const lines = [markdown ?? JSON.stringify(item)];
|
|
281
|
+
// Each test is an item of the tree, its text in its markdown - as spec-steps.ts `itemText` reads it.
|
|
282
|
+
const testText = (test) => (typeof test === "string" ? test : (test?.markdown ?? JSON.stringify(test)));
|
|
281
283
|
if (Array.isArray(tests) && tests.length > 0)
|
|
282
|
-
lines.push(" <acceptance tests>", ...tests.map((test) => ` ${
|
|
284
|
+
lines.push(" <acceptance tests>", ...tests.map((test) => ` ${testText(test).replace(/\n/g, "\n ")}`), " </acceptance tests>");
|
|
283
285
|
return lines.join("\n");
|
|
284
286
|
}
|
|
285
287
|
const indent = (text, by) => text.split("\n").map((line) => `${by}${line}`).join("\n");
|
|
@@ -20,7 +20,7 @@ import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
|
20
20
|
import { createCanItFailTool, createMyDiffTool, selfCheckNote } from "./self-check.js";
|
|
21
21
|
import { createServiceTools } from "./services.js";
|
|
22
22
|
import { createUnitGate } from "./unit-gate.js";
|
|
23
|
-
import { toolchainError } from "./environment-error.js";
|
|
23
|
+
import { timeLimitError, toolchainError } from "./environment-error.js";
|
|
24
24
|
import { workspaceBlock } from "./workspace.js";
|
|
25
25
|
import { ATTEMPTS, describeStop } from "./stop-report.js";
|
|
26
26
|
import { summarizeDiff } from "./job-panel.js";
|
|
@@ -333,6 +333,8 @@ export async function renderSpecDiff(options) {
|
|
|
333
333
|
let draftAsks = 0;
|
|
334
334
|
/** What the unit testing script needs and does not find, last run. */
|
|
335
335
|
let missing;
|
|
336
|
+
/** The unit testing script needs more time than a test run may take - the debugger's finding: the pulse stops on it. */
|
|
337
|
+
let slow;
|
|
336
338
|
/** The last round failed on the script's static checks alone: the unit tests passed. */
|
|
337
339
|
let staticOnly = false;
|
|
338
340
|
const testRemovals = [];
|
|
@@ -423,24 +425,35 @@ export async function renderSpecDiff(options) {
|
|
|
423
425
|
seenFailures.set(key, seen);
|
|
424
426
|
let prompt = verdict.prompt;
|
|
425
427
|
const circling = loop.stuck;
|
|
426
|
-
|
|
428
|
+
// A run the time limit stopped is looked at at once: whether something hangs, or the suite only needs more time.
|
|
429
|
+
const timedOut = verdict.run.timedOut === true;
|
|
430
|
+
if (circling || timedOut || (key !== undefined && seen >= STUCK_ROUNDS)) {
|
|
427
431
|
if (key !== undefined)
|
|
428
432
|
seenFailures.delete(key);
|
|
429
433
|
const times = circling ? Math.max(seen, loop.failing) : seen;
|
|
430
434
|
loop.reset();
|
|
431
435
|
stage(circling
|
|
432
436
|
? `the coding agent is going round in circles - ${times} failing runs - a debugger looks at it with fresh eyes`
|
|
433
|
-
:
|
|
437
|
+
: timedOut && seen < STUCK_ROUNDS
|
|
438
|
+
? `the unit testing script did not finish in ${Math.round(verdict.run.timeoutMs / 1000)}s - a debugger works out whether something hangs`
|
|
439
|
+
: `the same failure ${seen} times - a debugger looks at it with fresh eyes`);
|
|
434
440
|
const found = await debugFailure({
|
|
435
441
|
buildDir,
|
|
436
442
|
failure: verdict.run.output,
|
|
437
443
|
times,
|
|
438
444
|
task: steeredChange() ?? codingText ?? changeText ?? options.diff ?? "",
|
|
439
445
|
script: verdict.script,
|
|
446
|
+
run: verdict.run,
|
|
440
447
|
label: `${moduleName} spec change`,
|
|
441
448
|
...(debuggerModel !== undefined ? { model: debuggerModel } : {}),
|
|
442
449
|
onDelta: options.onDelta,
|
|
443
450
|
}).catch(() => undefined);
|
|
451
|
+
// Nothing hangs: the suite needs more time than a test run may take, and no agent can give it that.
|
|
452
|
+
if (found?.tooSlow === true) {
|
|
453
|
+
stage(`the unit testing script needs more than the ${Math.round(verdict.run.timeoutMs / 1000)}s a test run may take - stopping`);
|
|
454
|
+
slow = timeLimitError(verdict.run, { moduleName, script: verdict.script, kind: "unit", reason: found.rootCause });
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
444
457
|
if (found !== undefined) {
|
|
445
458
|
const cause = found.rootCause.split("\n")[0].slice(0, 240);
|
|
446
459
|
reportActivity(` the debugger: ${cause.slice(0, 160)}`);
|
|
@@ -486,6 +499,7 @@ export async function renderSpecDiff(options) {
|
|
|
486
499
|
...(options.cases !== undefined ? { cases: options.cases } : {}),
|
|
487
500
|
label: `${moduleName} spec change`,
|
|
488
501
|
...(scripts.runOn === "copy" ? { unitScript: scripts.unit } : {}),
|
|
502
|
+
testingScript: { path: relative(root, scripts.unit), content: readFileSync(scripts.unit, "utf8") },
|
|
489
503
|
model: options.judgeModel ?? modelSpec(inheritModel(configForSpec(tip.plainFile).judgeModel, modelOnly(options.model))),
|
|
490
504
|
send: async (next) => {
|
|
491
505
|
response = await ask(executor, next, options.onDelta);
|
|
@@ -543,13 +557,17 @@ export async function renderSpecDiff(options) {
|
|
|
543
557
|
stage(unitReviewStopped ? "the unit tests pass but still leave the change untested - undoing the change" : "unit tests NOT passing - undoing the change");
|
|
544
558
|
abandonWork(unit, moduleName, missing !== undefined
|
|
545
559
|
? "the unit testing script could not run"
|
|
546
|
-
:
|
|
547
|
-
? "the
|
|
548
|
-
:
|
|
560
|
+
: slow !== undefined
|
|
561
|
+
? "the unit testing script needs more time than a test run may take"
|
|
562
|
+
: staticOnly
|
|
563
|
+
? "the spec change did not pass its static checks"
|
|
564
|
+
: "the spec change did not pass its unit tests");
|
|
549
565
|
// The environment lacks what the testing script needs: the pulse stops, rolled back, and the person is told.
|
|
550
566
|
if (missing !== undefined) {
|
|
551
567
|
throw toolchainError(missing, { moduleName, script: scripts.unit, kind: "unit" });
|
|
552
568
|
}
|
|
569
|
+
if (slow !== undefined)
|
|
570
|
+
throw slow;
|
|
553
571
|
return { ...base, passed: false, stoppedAt: "unit tests", ...(staticOnly && !unitReviewStopped ? { staticChecks: true } : {}) };
|
|
554
572
|
}
|
|
555
573
|
stage("unit tests pass - landing the change on them");
|
|
@@ -12,6 +12,12 @@ export function createSidePanel(options) {
|
|
|
12
12
|
let drawnTree = { rows: [], rowStarts: [], lineCount: 0 };
|
|
13
13
|
let drawnMap = { rows: [], rowStarts: [], lineCount: 0 };
|
|
14
14
|
let pages = [];
|
|
15
|
+
/** The keys back to the chat - and `s`, from over the screen to beside the chat and back. */
|
|
16
|
+
const backKeys = () => {
|
|
17
|
+
if (options.dock === undefined)
|
|
18
|
+
return [`esc or ${options.key} back to the chat`];
|
|
19
|
+
return options.dock.docked() ? ["s full screen", "esc or ⌥⌘← to the chat", `${options.key} close`] : ["s beside the chat", `esc or ${options.key} back to the chat`];
|
|
20
|
+
};
|
|
15
21
|
const waitingNote = () =>
|
|
16
22
|
// A question asked while it showed waits in the chat, unanswered: the person is told it is there.
|
|
17
23
|
options.waiting?.() === true ? ` ${style.warning?.("your question is waiting") ?? style.accent("your question is waiting")} - ${options.key} or esc to answer it, as you left it` : "";
|
|
@@ -59,7 +65,7 @@ export function createSidePanel(options) {
|
|
|
59
65
|
/** The rule under the top. */
|
|
60
66
|
const rule = (width) => style.muted("─".repeat(Math.max(1, width)));
|
|
61
67
|
const renderPulses = (width, rows) => {
|
|
62
|
-
const keys = ["↑↓ move", "enter open/close", "←→", "o/c all", "pgup pgdn scroll", ...(options.map !== undefined ? ["p regen"] : []),
|
|
68
|
+
const keys = ["↑↓ move", "enter open/close", "←→", "o/c all", "pgup pgdn scroll", ...(options.map !== undefined ? ["p regen"] : []), ...backKeys()];
|
|
63
69
|
const top = topBlock([`${style.heading("pulses")}${waitingNote()}`], keys, width);
|
|
64
70
|
// Under the top: the rule, and a line of space - as the map has.
|
|
65
71
|
const bodyHeight = Math.max(3, rows - top.length - 2);
|
|
@@ -77,7 +83,7 @@ export function createSidePanel(options) {
|
|
|
77
83
|
pages = options.map();
|
|
78
84
|
const now = Date.now();
|
|
79
85
|
if (pages.length === 0) {
|
|
80
|
-
const top = topBlock([`${style.heading("regen")}${waitingNote()}`], ["p pulses",
|
|
86
|
+
const top = topBlock([`${style.heading("regen")}${waitingNote()}`], ["p pulses", ...backKeys()], width);
|
|
81
87
|
lastBodyHeight = Math.max(3, rows - top.length - 1);
|
|
82
88
|
drawnMap = { rows: [], rowStarts: [], lineCount: 0 };
|
|
83
89
|
const body = [style.muted(" No .plain file to map: name one to regenerate, or set module: in config.turri.yaml."), ...new Array(Math.max(0, lastBodyHeight - 1)).fill("")];
|
|
@@ -87,7 +93,7 @@ export function createSidePanel(options) {
|
|
|
87
93
|
const tabbed = pages.length > 1;
|
|
88
94
|
const page = shownPage(pages, map);
|
|
89
95
|
const discuss = page.stuck && options.discuss !== undefined ? [`${DISCUSS_KEY} talk it through in the chat`] : [];
|
|
90
|
-
const keys = [...discuss, "↑↓ move", "enter open/close", ...(tabbed ? ["←→ module"] : []), "o/c all", "pgup pgdn", "p pulses",
|
|
96
|
+
const keys = [...discuss, "↑↓ move", "enter open/close", ...(tabbed ? ["←→ module"] : []), "o/c all", "pgup pgdn", "p pulses", ...backKeys()];
|
|
91
97
|
// The legend's box in the upper right corner, over what is under it; too narrow a panel has none.
|
|
92
98
|
const box = width >= 60 ? legendBox(legendLines(page, style)) : [];
|
|
93
99
|
const room = box.length > 0 ? width - visibleWidth(box[0]) - 1 : width;
|
|
@@ -210,7 +216,7 @@ export function createSidePanel(options) {
|
|
|
210
216
|
}
|
|
211
217
|
catch (error) {
|
|
212
218
|
// A frame that throws is never drawn - nor is anything after it: the screen would freeze. What failed is shown instead.
|
|
213
|
-
const top = topBlock([`${style.heading(map.view === "map" ? "regen" : "pulses")}${waitingNote()}`], [...(options.map !== undefined ? ["p the other view"] : []),
|
|
219
|
+
const top = topBlock([`${style.heading(map.view === "map" ? "regen" : "pulses")}${waitingNote()}`], [...(options.map !== undefined ? ["p the other view"] : []), ...backKeys()], width);
|
|
214
220
|
const lines = (error instanceof Error ? (error.stack ?? error.message) : String(error)).split("\n").slice(0, Math.max(1, rows - top.length - 1));
|
|
215
221
|
return [
|
|
216
222
|
...top,
|
|
@@ -225,6 +231,10 @@ export function createSidePanel(options) {
|
|
|
225
231
|
options.back();
|
|
226
232
|
return;
|
|
227
233
|
}
|
|
234
|
+
if (options.dock !== undefined && data === "s") {
|
|
235
|
+
options.dock.toggle();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
228
238
|
if (options.map !== undefined && data === "p") {
|
|
229
239
|
if (map.view === "map")
|
|
230
240
|
toPulses();
|
|
@@ -89,6 +89,19 @@ export function describeStop(facts) {
|
|
|
89
89
|
`though they were sent back for it again and again. Still missing:\n${facts.conformance.unlabelled.map((line) => ` - ${line}`).join("\n")}`;
|
|
90
90
|
todo = ["If the test reqs leave open where the tests go or how they are built, say so exactly - the folder, the files, the build."];
|
|
91
91
|
}
|
|
92
|
+
else if (facts.at === "conformance" && facts.conformance?.escalation !== undefined) {
|
|
93
|
+
const { error, reason, evidence, changedNothing } = facts.conformance.escalation;
|
|
94
|
+
why =
|
|
95
|
+
`its conformance tests kept failing with ${error}. The conformance agent's fix of the tests did not make it go away, so ` +
|
|
96
|
+
`a judge looked again and found it the code's; the coding agent ${changedNothing === true ? "changed nothing for it" : "fixed the code, and the run still failed the same way"}. ` +
|
|
97
|
+
`Why the judge found it the code's:\n${reason.trim().split("\n").map((line) => ` ${line}`).join("\n")}` +
|
|
98
|
+
(evidence.length > 0 ? `\n On the evidence:\n${evidence.map((item) => ` - ${item.source.trim()}: ${item.shows.trim().replace(/\s+/g, " ")}`).join("\n")}` : "");
|
|
99
|
+
todo = [
|
|
100
|
+
"If the code has to produce something the spec leaves open - a build output, an entry point, a module the conformance testing " +
|
|
101
|
+
"script runs or checks for - say exactly what in the implementation reqs: its path and how it is built.",
|
|
102
|
+
"If the conformance testing script expects something the spec does not ask for, change the script or the test reqs so the two agree.",
|
|
103
|
+
];
|
|
104
|
+
}
|
|
92
105
|
else if (facts.at === "conformance" && facts.conformance?.stuck !== undefined) {
|
|
93
106
|
const { error, times, rounds, changedNothing } = facts.conformance.stuck;
|
|
94
107
|
why =
|
|
@@ -335,7 +335,15 @@ async function runScript(cwd, script, args, logDir, options = {}) {
|
|
|
335
335
|
const noted = withScriptLineNote(text, script);
|
|
336
336
|
// Its failures, counts and end are what the agent is shown; the whole is kept, a read_output away.
|
|
337
337
|
const log = keepTestOutput(logDir, basename(script), noted);
|
|
338
|
-
return {
|
|
338
|
+
return {
|
|
339
|
+
command: shown(script),
|
|
340
|
+
exitCode: timedOut === true ? 124 : exitCode,
|
|
341
|
+
output: shownTestOutput(noted, log),
|
|
342
|
+
timedOut: timedOut === true,
|
|
343
|
+
timeoutMs,
|
|
344
|
+
elapsedMs: Date.now() - started,
|
|
345
|
+
...(log !== undefined ? { log } : {}),
|
|
346
|
+
};
|
|
339
347
|
}
|
|
340
348
|
/** What bash says when a line of the script itself cannot run: a command or file that is not there, or not runnable. */
|
|
341
349
|
const OWN_LINE_FAILURE = /: (No such file or directory|command not found|Permission denied)$/;
|
|
@@ -589,6 +597,19 @@ function loopNotice(loop) {
|
|
|
589
597
|
"the failure with fresh eyes, and you will be given what it finds.",
|
|
590
598
|
].join("\n");
|
|
591
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* How long a run took against its time limit, in a line: for whoever judges it or works out its cause - so a run that
|
|
602
|
+
* ended on its own is never taken for one the limit stopped, nor the other way.
|
|
603
|
+
*/
|
|
604
|
+
export function runTiming(run) {
|
|
605
|
+
const seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
|
|
606
|
+
const limit = seconds(run.timeoutMs);
|
|
607
|
+
if (run.timedOut === true) {
|
|
608
|
+
return `The time limit stopped this run: it had run ${seconds(run.elapsedMs ?? run.timeoutMs)} of the ${limit} a test run may take, and had not finished.`;
|
|
609
|
+
}
|
|
610
|
+
const took = run.elapsedMs !== undefined ? `after ${seconds(run.elapsedMs)} of the ${limit} a test run may take` : `well within the ${limit} a test run may take`;
|
|
611
|
+
return `This run ended on its own - exit ${run.exitCode}, ${took}. The time limit did not stop it and played no part in how it ended.`;
|
|
612
|
+
}
|
|
592
613
|
/** A run the time limit stopped: failed, and what most often keeps a test run from ending. */
|
|
593
614
|
function timedOutFailure(run, what) {
|
|
594
615
|
const limit = Math.round(run.timeoutMs / 1000);
|
|
@@ -609,19 +630,34 @@ function timedOutFailure(run, what) {
|
|
|
609
630
|
`the same ${limit} seconds, and a skipped or deleted test does not count as a fix.`,
|
|
610
631
|
].join("\n");
|
|
611
632
|
}
|
|
633
|
+
/** A command `timeout` could not start, or a shell could not find - on a line of its own. */
|
|
634
|
+
const COULD_NOT_RUN = /^(?:[^:]*: )?failed to run command [‘'`"]?([\w.+-]+)[’'`"]?: No such file or directory$/;
|
|
635
|
+
const NOT_FOUND = /^(?:[^:]*: )?(?:line \d+: )?([\w.+-]+): command not found$/;
|
|
612
636
|
/**
|
|
613
637
|
* What a failed run says is not installed where it ran - the command, when its output names it - or undefined when
|
|
614
|
-
* the run failed for another reason.
|
|
615
|
-
* or a shell or `timeout`
|
|
638
|
+
* the run failed for another reason. A missing command is the cause only on evidence that it ended the run: exit 69,
|
|
639
|
+
* the testing scripts' own way to say a toolchain is missing, or 127, what a shell or `timeout` gives for a command it
|
|
640
|
+
* could not find - else a command not found on the last line the run printed, nothing having run after it. Earlier in
|
|
641
|
+
* the output it is a warning, not the failure: a shell profile's, an optional tool probed for, a fallback taken - and a
|
|
642
|
+
* run that failed for its own reasons is the agent's to fix, not a stop for the person.
|
|
616
643
|
*/
|
|
617
644
|
export function missingToolchain(run) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (run.exitCode === 69 || run.exitCode === 127)
|
|
645
|
+
if (run.exitCode === 0)
|
|
646
|
+
return undefined;
|
|
647
|
+
const lines = run.output.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
648
|
+
const named = (line) => COULD_NOT_RUN.exec(line)?.[1] ?? NOT_FOUND.exec(line)?.[1];
|
|
649
|
+
if (run.exitCode === 69 || run.exitCode === 127) {
|
|
650
|
+
// The command is the last one named: the one the run stopped on.
|
|
651
|
+
for (let at = lines.length - 1; at >= 0; at -= 1) {
|
|
652
|
+
const command = named(lines[at]);
|
|
653
|
+
if (command !== undefined)
|
|
654
|
+
return { command };
|
|
655
|
+
}
|
|
623
656
|
return {};
|
|
624
|
-
|
|
657
|
+
}
|
|
658
|
+
const last = lines.at(-1);
|
|
659
|
+
const command = last !== undefined ? named(last) : undefined;
|
|
660
|
+
return command !== undefined ? { command } : undefined;
|
|
625
661
|
}
|
|
626
662
|
/** Where `command` is on this machine's PATH, or undefined when it is not there. */
|
|
627
663
|
export function onThisMachine(command) {
|
|
@@ -63,6 +63,17 @@ export function unitReviewTask(options) {
|
|
|
63
63
|
options.spec.trim(),
|
|
64
64
|
"</specification>",
|
|
65
65
|
"",
|
|
66
|
+
...(options.testingScript !== undefined
|
|
67
|
+
? [
|
|
68
|
+
"The harness runs the unit tests with this script, and it decides whether they pass. Read it for which tests it",
|
|
69
|
+
"runs and how: a test it never reaches - a file it does not pick up, a folder it does not look in - is not run, and",
|
|
70
|
+
"checks nothing.",
|
|
71
|
+
`<unit testing script path="${options.testingScript.path}">`,
|
|
72
|
+
options.testingScript.content.trimEnd(),
|
|
73
|
+
"</unit testing script>",
|
|
74
|
+
"",
|
|
75
|
+
]
|
|
76
|
+
: []),
|
|
66
77
|
...(options.cases !== undefined ? [...codeReviewCasesBlock(options.cases), ""] : []),
|
|
67
78
|
// A job that touches no functional spec is held only to that: the checks of behaviour are not its.
|
|
68
79
|
...(mechanical ? [] : ["Check that the code does what the specification states for this work:"]),
|
package/dist/regenerate-cli.js
CHANGED
|
@@ -19,7 +19,7 @@ its conformance tests too when they are on.
|
|
|
19
19
|
--start-over start over, without asking, a codebase regenerated without a module it now requires -
|
|
20
20
|
with no terminal to ask and without it, the run stops there
|
|
21
21
|
--from-scratch delete the module's code and conformance tests (its lineage keeps them) and regenerate all of it
|
|
22
|
-
--harness-model <p/id> config.turri.yaml's harness-model for this run: the planner
|
|
22
|
+
--harness-model <p/id> config.turri.yaml's harness-model for this run: the planner and the conformance chooser
|
|
23
23
|
--coding-model <p/id> its coding-model, --model too: the agents that build; p/id:level for how hard a model thinks
|
|
24
24
|
--judge-model <p/id> its judge-model: the judge and the test review
|
|
25
25
|
--review <true|false> its review: whether the code and its tests are reviewed once they pass
|
package/dist/run-report.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import { turriIdentity } from "./environment.js";
|
|
5
4
|
import { onModelEvent } from "./regenerate/activity.js";
|
|
6
5
|
/**
|
|
7
6
|
* A headless run's report: one JSON file, `turri regenerate --report <file>`, saying what the run did - how it ended,
|
|
@@ -57,25 +56,6 @@ function sumTotals(a, b) {
|
|
|
57
56
|
function rounded(totals) {
|
|
58
57
|
return { ...totals, cost_usd: Math.round(totals.cost_usd * 1e6) / 1e6, model_seconds: Math.round(totals.model_seconds * 10) / 10 };
|
|
59
58
|
}
|
|
60
|
-
/** turri's version, and the commit when it runs from a clone. */
|
|
61
|
-
function turriIdentity() {
|
|
62
|
-
let version = null;
|
|
63
|
-
try {
|
|
64
|
-
version = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")).version ?? null;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
/* no package.json: left out */
|
|
68
|
-
}
|
|
69
|
-
if (!existsSync(join(PACKAGE_ROOT, ".git")))
|
|
70
|
-
return { version, git_commit: null, git_dirty: null };
|
|
71
|
-
const commit = spawnSync("git", ["-C", PACKAGE_ROOT, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
72
|
-
const status = spawnSync("git", ["-C", PACKAGE_ROOT, "status", "--porcelain", "--untracked-files=no"], { encoding: "utf8" });
|
|
73
|
-
return {
|
|
74
|
-
version,
|
|
75
|
-
git_commit: commit.status === 0 ? commit.stdout.trim() : null,
|
|
76
|
-
git_dirty: status.status === 0 ? status.stdout.trim() !== "" : null,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
59
|
const pulseKind = (job) => (job.kind === "regenerate" ? "code" : job.kind);
|
|
80
60
|
const firstLines = (text) => {
|
|
81
61
|
const trimmed = text?.trim();
|
package/docs/config.md
CHANGED
|
@@ -122,7 +122,7 @@ models: # each agent role's model, thinking and tur
|
|
|
122
122
|
| key | what it sets | left out |
|
|
123
123
|
| --- | --- | --- |
|
|
124
124
|
| `module` | the `.plain` file `turri regenerate` and the chat's `regenerate` build when none is named, from the project root | a file has to be named |
|
|
125
|
-
| `harness-model` | the model `turri` starts on - the one you talk to -
|
|
125
|
+
| `harness-model` | the model `turri` starts on - the one you talk to - and, in the conversation, the planner that cuts the work into jobs (on whatever model the chat is on when it plans); under `turri regenerate`, the planner too | pi's configured model |
|
|
126
126
|
| `coding-model` | every render: functionalities, spec diffs, conformance tests, repairs, planning spec diffs, the explorer | `harness-model` |
|
|
127
127
|
| `judge-model` | the judge and the test review of a conformance run | `coding-model` |
|
|
128
128
|
| `conformance-tests` | `all` (or `true`): conformance tests for every functional spec of every module with test reqs, written once each module's code is built, before anything is built on it. `chosen`: for the functional specs chosen - every one with acceptance tests, and those the chooser picks as each code pulse ends, from what that pulse regenerated. `module`: the same, but the chooser is asked once, when the whole module is regenerated, and plans the conformance pulses too. Under both, the choice is made inside the pulse, before anything is built on the module, and the pulse's report says what was chosen and why; those the person asks for with `conform` are written too; once chosen, a functional spec keeps them, and they are rewritten when it changes - see [who chooses, and when](regenerating.md#conformance-tests-in-the-work-loop). `false`: none written, and none of the existing ones run - until the conversation asks with `conform` | `false` |
|
|
@@ -158,7 +158,8 @@ wrong type, as if it were not there. `model:`, from before there were separate m
|
|
|
158
158
|
| --- | --- |
|
|
159
159
|
| `default` | every role not named below - but the conversation's side |
|
|
160
160
|
| `chat` | `turri`'s model - the same as `harness-model`, which it wins over |
|
|
161
|
-
| `chooser`, `planner
|
|
161
|
+
| `chooser`, `planner` | the conversation's side: the conformance chooser of `turri regenerate`, the planner's agents. Unnamed, the chat's model |
|
|
162
|
+
| `diagnosis` | the diagnosis of a job that failed for good: why it did not land, and what to do. Unnamed, the coding model the job ran on, thinking medium |
|
|
162
163
|
| `release` | `turri release`'s agent and release-after-pulse's: it follows steps - a script, a start command - so it runs on the coding model, thinking low |
|
|
163
164
|
| `coding` | the coding agent, the code fixer of a conformance run and the repair agent - the same as `coding-model`, which it wins over. `--model` on the command line, or a model chosen in the conversation (`engine`), wins over both |
|
|
164
165
|
| `conformance` | the conformance agent |
|