@turri-dev/cli 0.1.3-next.17f0f7f → 0.1.3-next.26f08c5
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/dist/cli-options.js +82 -0
- package/dist/conform-cli.js +2 -1
- package/dist/environment.js +19 -0
- package/dist/extension.js +7 -11
- package/dist/loop-cli.js +7 -74
- package/dist/pi-launch.js +13 -4
- package/dist/regenerate-cli.js +2 -1
- package/dist/run-report.js +2 -22
- package/dist/setup.js +23 -15
- package/dist/spec-diff-cli.js +2 -1
- package/docs/development.md +6 -0
- package/docs/headless.md +12 -4
- package/docs/installation.md +1 -1
- package/docs/using.md +4 -2
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { conformanceOption, overrideConfig, reviewOption, splitModelSpec } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* The command line of turri's commands: their words, and the options that stand for the config's own keys - kept apart
|
|
4
|
+
* from the commands that use them, so a light one (doctor) reads them as a regenerate does without loading it.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* A command's arguments: the positional ones, `--flag`s and `--name value` options (`--name=value` too) -
|
|
8
|
+
* `--coding-model` is `--model`. An option it does not take is refused, rather than taking the word after it, and so is an
|
|
9
|
+
* option with no value - the end of the words, or another option where its value should be.
|
|
10
|
+
*/
|
|
11
|
+
export function parseArgs(argv, flags, usage, valued = []) {
|
|
12
|
+
const positional = [];
|
|
13
|
+
const options = {};
|
|
14
|
+
const takes = new Set(["--model", ...valued]);
|
|
15
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
const [name, inline] = arg.startsWith("--") && arg.includes("=") ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)] : [arg, undefined];
|
|
18
|
+
const option = name === "--coding-model" ? "--model" : name;
|
|
19
|
+
if (arg === "-h" || arg === "--help") {
|
|
20
|
+
console.log(usage);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
else if (flags.includes(option) && inline === undefined)
|
|
24
|
+
options[option.slice(2)] = "yes";
|
|
25
|
+
else if (takes.has(option)) {
|
|
26
|
+
// Its value is the next word - never another option, which would be taken as the value and lost.
|
|
27
|
+
const value = inline ?? argv[i + 1];
|
|
28
|
+
if (value === undefined || (inline === undefined && value.startsWith("--"))) {
|
|
29
|
+
console.error(`${usage}\n\n${name} needs a value${value !== undefined ? `, not the option ${value}` : ""} - ${name}=<value> gives one that starts with --.`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (inline === undefined)
|
|
33
|
+
i += 1;
|
|
34
|
+
options[option.slice(2)] = value;
|
|
35
|
+
}
|
|
36
|
+
else if (arg.startsWith("-")) {
|
|
37
|
+
console.error(`${usage}\n\nNo such option: ${name}.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
else
|
|
41
|
+
positional.push(arg);
|
|
42
|
+
}
|
|
43
|
+
return { positional, options };
|
|
44
|
+
}
|
|
45
|
+
/** An option's value, as parseArgs left it: refused when it was given with none. */
|
|
46
|
+
export function optionValue(options, name) {
|
|
47
|
+
const value = options[name];
|
|
48
|
+
if (name in options && (value === undefined || value.trim() === ""))
|
|
49
|
+
failOption(`--${name} needs a value`);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The options that stand for the config's own keys - `--harness-model`, `--coding-model` (`--model`), `--judge-model`
|
|
54
|
+
* and, as asked, `--review` and `--conformance-tests` - read as if config.turri.yaml had them, in their place in the
|
|
55
|
+
* order the config resolves them in (overrideConfig): a run, or doctor, with them is the same as one with them in the
|
|
56
|
+
* file. Exits on a value it cannot read.
|
|
57
|
+
*/
|
|
58
|
+
export function configKeyOptions(options, take = {}) {
|
|
59
|
+
const tests = take.conformance === true ? optionValue(options, "conformance-tests") : undefined;
|
|
60
|
+
const setting = tests !== undefined ? conformanceOption(tests) : undefined;
|
|
61
|
+
if (tests !== undefined && setting === undefined)
|
|
62
|
+
failOption(`--conformance-tests takes all, chosen, module or false, not ${tests}`);
|
|
63
|
+
const reviewed = take.review === true ? optionValue(options, "review") : undefined;
|
|
64
|
+
const review = reviewed !== undefined ? reviewOption(reviewed) : undefined;
|
|
65
|
+
if (reviewed !== undefined && review === undefined)
|
|
66
|
+
failOption(`--review takes true or false, not ${reviewed}`);
|
|
67
|
+
const model = (name, key) => {
|
|
68
|
+
const spec = optionValue(options, name);
|
|
69
|
+
return spec !== undefined ? { [key]: splitModelSpec(spec) } : {};
|
|
70
|
+
};
|
|
71
|
+
overrideConfig({
|
|
72
|
+
...model("harness-model", "harnessModel"),
|
|
73
|
+
...model("model", "codingModel"),
|
|
74
|
+
...model("judge-model", "judgeModel"),
|
|
75
|
+
...(setting !== undefined ? { conformanceTests: setting } : {}),
|
|
76
|
+
...(review !== undefined ? { review } : {}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function failOption(message) {
|
|
80
|
+
console.error(message);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
package/dist/conform-cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseArgs } from "./cli-options.js";
|
|
2
|
+
import { headlessOptions, runLoop } from "./loop-cli.js";
|
|
2
3
|
const USAGE = `usage: turri conform <file.plain> [frid ...] [options]
|
|
3
4
|
|
|
4
5
|
Writes and runs the black-box conformance tests of a module's functional specs - those named, or
|
package/dist/environment.js
CHANGED
|
@@ -68,3 +68,22 @@ export function turriOnPath() {
|
|
|
68
68
|
const found = which.status === 0 ? which.stdout.trim().split("\n")[0] : undefined;
|
|
69
69
|
return found === "" ? undefined : found;
|
|
70
70
|
}
|
|
71
|
+
/** turri's version, and the commit when it runs from a clone. */
|
|
72
|
+
export function turriIdentity() {
|
|
73
|
+
let version = null;
|
|
74
|
+
try {
|
|
75
|
+
version = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")).version ?? null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* no package.json: left out */
|
|
79
|
+
}
|
|
80
|
+
if (!existsSync(join(PACKAGE_ROOT, ".git")))
|
|
81
|
+
return { version, git_commit: null, git_dirty: null };
|
|
82
|
+
const commit = spawnSync("git", ["-C", PACKAGE_ROOT, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
83
|
+
const status = spawnSync("git", ["-C", PACKAGE_ROOT, "status", "--porcelain", "--untracked-files=no"], { encoding: "utf8" });
|
|
84
|
+
return {
|
|
85
|
+
version,
|
|
86
|
+
git_commit: commit.status === 0 ? commit.stdout.trim() : null,
|
|
87
|
+
git_dirty: status.status === 0 ? status.stdout.trim() !== "" : null,
|
|
88
|
+
};
|
|
89
|
+
}
|
package/dist/extension.js
CHANGED
|
@@ -52,6 +52,7 @@ import { registerStacks } from "./stacks.js";
|
|
|
52
52
|
import { catalogueLine, discoverSkills, leftOutNote } from "./skills.js";
|
|
53
53
|
import { supersededByTurri } from "./chat-skills.js";
|
|
54
54
|
import { TURRI_IDENTITY } from "./identity.js";
|
|
55
|
+
import { turriIdentity } from "./environment.js";
|
|
55
56
|
import { conformanceChooser } from "./regenerate/choose-conformance.js";
|
|
56
57
|
import { answerSideQuestion, askWithAsides, BTW_HOW, runningSkill, skillRunStarted, skillSessionText, transcriptOf } from "./btw.js";
|
|
57
58
|
import { createJobRegistry, defaultParallelJobs, describeJob, formatDuration, pulseName, shouldWait, STOPPED_ON_REQUEST, } from "./regenerate/jobs.js";
|
|
@@ -2158,7 +2159,8 @@ function registerBranding(pi, ruleCount, skills) {
|
|
|
2158
2159
|
if (data === undefined)
|
|
2159
2160
|
return undefined;
|
|
2160
2161
|
const lines = [
|
|
2161
|
-
`${theme.fg("accent", "turri")} ${theme.fg("muted", `v${data.version}`)}
|
|
2162
|
+
`${theme.fg("accent", "turri")} ${theme.fg("muted", `v${data.version}`)}` +
|
|
2163
|
+
(data.commit !== undefined ? theme.fg("dim", ` from a clone, ${data.commit}${data.changed ? " with changes" : ""}`) : ""),
|
|
2162
2164
|
theme.fg("text", "regenerative software - the code is regenerated from the spec"),
|
|
2163
2165
|
theme.fg("muted", `${data.rules} rules · ${data.skills} skill${data.skills === 1 ? "" : "s"}` +
|
|
2164
2166
|
(data.projectSkills !== undefined && data.projectSkills > 0 ? ` (${data.projectSkills} the project's)` : "") +
|
|
@@ -2180,8 +2182,11 @@ function registerBranding(pi, ruleCount, skills) {
|
|
|
2180
2182
|
if (ctx.hasUI)
|
|
2181
2183
|
animateJellyfish(ctx);
|
|
2182
2184
|
try {
|
|
2185
|
+
const identity = turriIdentity();
|
|
2183
2186
|
pi.appendEntry(BANNER_ENTRY, {
|
|
2184
|
-
version:
|
|
2187
|
+
version: identity.version ?? "0.0.0",
|
|
2188
|
+
commit: identity.git_commit?.slice(0, 7),
|
|
2189
|
+
changed: identity.git_dirty === true,
|
|
2185
2190
|
rules: ruleCount,
|
|
2186
2191
|
skills: skills.skills.length,
|
|
2187
2192
|
projectSkills: skills.skills.filter((skill) => skill.origin !== "turri").length,
|
|
@@ -2361,12 +2366,3 @@ function jellyfishArt(appearance, ink) {
|
|
|
2361
2366
|
return undefined;
|
|
2362
2367
|
}
|
|
2363
2368
|
}
|
|
2364
|
-
function packageVersion() {
|
|
2365
|
-
try {
|
|
2366
|
-
const raw = readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
2367
|
-
return JSON.parse(raw).version ?? "0.0.0";
|
|
2368
|
-
}
|
|
2369
|
-
catch {
|
|
2370
|
-
return "0.0.0";
|
|
2371
|
-
}
|
|
2372
|
-
}
|
package/dist/loop-cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sweepOrphanedGroups } from "./regenerate/shell.js";
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { resolve, sep } from "node:path";
|
|
4
|
-
import { configForSpec,
|
|
4
|
+
import { configForSpec, inheritModel, modelOnly, modelSpec, projectModels, readProjectConfig, roleModel, ROLE_THINKING, splitModelSpec, } from "./config.js";
|
|
5
5
|
import { PACKAGE_ROOT } from "./environment.js";
|
|
6
6
|
import { c } from "./module-report.js";
|
|
7
7
|
import { tryItCommand } from "./regenerate/delivery.js";
|
|
@@ -13,6 +13,7 @@ import { MODULES_DIR, projectDirOf } from "./regenerate/paths.js";
|
|
|
13
13
|
import { exportModule } from "./regenerate/release.js";
|
|
14
14
|
import { listFiles } from "./regenerate/testing.js";
|
|
15
15
|
import { startRunReport } from "./run-report.js";
|
|
16
|
+
import { configKeyOptions, optionValue } from "./cli-options.js";
|
|
16
17
|
/**
|
|
17
18
|
* What every command that builds - `turri regenerate`, `conform`, `spec-diff` - does around its own
|
|
18
19
|
* request of the work loop, as the chat does it: the project's models, a job that fails run once more and, failing again,
|
|
@@ -235,83 +236,15 @@ function reportModels(project, model) {
|
|
|
235
236
|
}
|
|
236
237
|
/**
|
|
237
238
|
* The options every command that regenerates takes for a headless run: `--report` and `--build-dir`, and the config's
|
|
238
|
-
* own keys - `--harness-model`, `--coding-model` (`--model`), `--judge-model`, `--review` and, where
|
|
239
|
-
* true, `--conformance-tests` - read as if config.turri.yaml had them
|
|
240
|
-
* Exits on a value it cannot read.
|
|
239
|
+
* own keys (configKeyOptions) - `--harness-model`, `--coding-model` (`--model`), `--judge-model`, `--review` and, where
|
|
240
|
+
* `conformance` is true, `--conformance-tests` - read as if config.turri.yaml had them. Exits on a value it cannot read.
|
|
241
241
|
*/
|
|
242
242
|
export function headlessOptions(options, conformance) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
failOption(`--${name} needs a value`);
|
|
247
|
-
return value;
|
|
248
|
-
};
|
|
249
|
-
const tests = conformance ? given("conformance-tests") : undefined;
|
|
250
|
-
const setting = tests !== undefined ? conformanceOption(tests) : undefined;
|
|
251
|
-
if (tests !== undefined && setting === undefined)
|
|
252
|
-
failOption(`--conformance-tests takes all, chosen, module or false, not ${tests}`);
|
|
253
|
-
const reviewed = given("review");
|
|
254
|
-
const review = reviewed !== undefined ? reviewOption(reviewed) : undefined;
|
|
255
|
-
if (reviewed !== undefined && review === undefined)
|
|
256
|
-
failOption(`--review takes true or false, not ${reviewed}`);
|
|
257
|
-
const model = (name) => {
|
|
258
|
-
const spec = given(name);
|
|
259
|
-
return spec !== undefined ? { [name === "model" ? "codingModel" : name === "harness-model" ? "harnessModel" : "judgeModel"]: splitModelSpec(spec) } : {};
|
|
260
|
-
};
|
|
261
|
-
overrideConfig({
|
|
262
|
-
...model("harness-model"),
|
|
263
|
-
...model("model"),
|
|
264
|
-
...model("judge-model"),
|
|
265
|
-
...(setting !== undefined ? { conformanceTests: setting } : {}),
|
|
266
|
-
...(review !== undefined ? { review } : {}),
|
|
267
|
-
});
|
|
268
|
-
const report = given("report");
|
|
269
|
-
const buildDir = given("build-dir");
|
|
243
|
+
configKeyOptions(options, { conformance, review: true });
|
|
244
|
+
const report = optionValue(options, "report");
|
|
245
|
+
const buildDir = optionValue(options, "build-dir");
|
|
270
246
|
return { ...(report !== undefined ? { report } : {}), ...(buildDir !== undefined ? { buildDir } : {}) };
|
|
271
247
|
}
|
|
272
|
-
function failOption(message) {
|
|
273
|
-
console.error(message);
|
|
274
|
-
process.exit(1);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* A command's arguments: the positional ones, `--flag`s and `--name value` options (`--name=value` too) -
|
|
278
|
-
* `--coding-model` is `--model`. An option it does not take is refused, rather than taking the word after it, and so is an
|
|
279
|
-
* option with no value - the end of the words, or another option where its value should be.
|
|
280
|
-
*/
|
|
281
|
-
export function parseArgs(argv, flags, usage, valued = []) {
|
|
282
|
-
const positional = [];
|
|
283
|
-
const options = {};
|
|
284
|
-
const takes = new Set(["--model", ...valued]);
|
|
285
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
286
|
-
const arg = argv[i];
|
|
287
|
-
const [name, inline] = arg.startsWith("--") && arg.includes("=") ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)] : [arg, undefined];
|
|
288
|
-
const option = name === "--coding-model" ? "--model" : name;
|
|
289
|
-
if (arg === "-h" || arg === "--help") {
|
|
290
|
-
console.log(usage);
|
|
291
|
-
process.exit(0);
|
|
292
|
-
}
|
|
293
|
-
else if (flags.includes(option) && inline === undefined)
|
|
294
|
-
options[option.slice(2)] = "yes";
|
|
295
|
-
else if (takes.has(option)) {
|
|
296
|
-
// Its value is the next word - never another option, which would be taken as the value and lost.
|
|
297
|
-
const value = inline ?? argv[i + 1];
|
|
298
|
-
if (value === undefined || (inline === undefined && value.startsWith("--"))) {
|
|
299
|
-
console.error(`${usage}\n\n${name} needs a value${value !== undefined ? `, not the option ${value}` : ""} - ${name}=<value> gives one that starts with --.`);
|
|
300
|
-
process.exit(1);
|
|
301
|
-
}
|
|
302
|
-
if (inline === undefined)
|
|
303
|
-
i += 1;
|
|
304
|
-
options[option.slice(2)] = value;
|
|
305
|
-
}
|
|
306
|
-
else if (arg.startsWith("-")) {
|
|
307
|
-
console.error(`${usage}\n\nNo such option: ${name}.`);
|
|
308
|
-
process.exit(1);
|
|
309
|
-
}
|
|
310
|
-
else
|
|
311
|
-
positional.push(arg);
|
|
312
|
-
}
|
|
313
|
-
return { positional, options };
|
|
314
|
-
}
|
|
315
248
|
/** Yes or no at the terminal - or undefined when there is none, and so no one to ask. */
|
|
316
249
|
async function askAtTerminal(title, body) {
|
|
317
250
|
if (process.stdin.isTTY !== true)
|
package/dist/pi-launch.js
CHANGED
|
@@ -9,19 +9,28 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
11
|
import { main, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { ProcessTerminal } from "@earendil-works/pi-tui";
|
|
12
13
|
import "./terminal-images.js";
|
|
13
14
|
/**
|
|
14
15
|
* pi, started the way its own CLI starts it, for `turri`.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* takes
|
|
19
|
-
* machine, so it is answered for this run instead. `--verbose` still shows it.
|
|
17
|
+
* What differs is pi's own branding, because turri shows its own: its startup header - its
|
|
18
|
+
* version, key hints and the list of every loaded skill, extension and theme - its update notice
|
|
19
|
+
* and its tab title. pi only takes the header from the `quietStartup` setting, and a setting would
|
|
20
|
+
* quiet every `pi` on the machine, so it is answered for this run instead. `--verbose` still shows it.
|
|
20
21
|
*/
|
|
21
22
|
const args = process.argv.slice(2);
|
|
22
23
|
if (!args.includes("--verbose")) {
|
|
23
24
|
SettingsManager.prototype.getQuietStartup = () => true;
|
|
24
25
|
}
|
|
26
|
+
// pi's "Update Available" notice is left out: the pi turri runs is its pinned dependency, which `pi update`
|
|
27
|
+
// does not touch - it moves when turri does.
|
|
28
|
+
process.env.PI_SKIP_VERSION_CHECK = "1";
|
|
29
|
+
// The terminal's tab title: pi writes `π - <folder>` (its APP_TITLE, a constant), turri a jellyfish and its name.
|
|
30
|
+
const setTitle = ProcessTerminal.prototype.setTitle;
|
|
31
|
+
ProcessTerminal.prototype.setTitle = function (title) {
|
|
32
|
+
setTitle.call(this, title.replace(/^π(?= |$)/, "🪼 turri"));
|
|
33
|
+
};
|
|
25
34
|
// pi's own keys stand: a new line is shift+enter or ctrl+j. turri's side panel is on ctrl+h (job-panel.ts PANEL_KEY).
|
|
26
35
|
// What pi's CLI does before main: the process title, pi's environment markers, the HTTP
|
|
27
36
|
// dispatcher. Not exported, so loaded from the file - beside pi's entry point, wherever npm put it.
|
package/dist/regenerate-cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CONFIG_FILE, readProjectConfig } from "./config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { parseArgs } from "./cli-options.js";
|
|
3
|
+
import { headlessOptions, runLoop } from "./loop-cli.js";
|
|
3
4
|
const USAGE = `usage: turri regenerate [file.plain] [frid ...] [options]
|
|
4
5
|
|
|
5
6
|
Regenerates the module's code from its spec as it is now, and everything it requires, bottom first, as
|
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/dist/setup.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
import { globalExtensionFile, MIN_NODE, nodeVersionOk, PACKAGE_ROOT, turriOnPath } from "./environment.js";
|
|
7
7
|
import { sandboxAvailable } from "./regenerate/os-sandbox.js";
|
|
8
8
|
import { findTestScripts } from "./regenerate/test-scripts.js";
|
|
9
|
+
import { configKeyOptions, parseArgs } from "./cli-options.js";
|
|
9
10
|
import { hasProjectConfig, readProjectConfig, stackOf } from "./config.js";
|
|
10
11
|
import { checkModels, modelsInUse, piModelAccess } from "./model-check.js";
|
|
11
12
|
import { brand } from "./brand.js";
|
|
@@ -151,7 +152,7 @@ function checkGlobal() {
|
|
|
151
152
|
export async function checkProjectModels(project, access) {
|
|
152
153
|
const used = modelsInUse(readProjectConfig(project, stackOf(project)));
|
|
153
154
|
if (used.length === 0) {
|
|
154
|
-
return [{ level: "missing", name: "Models", detail: "config.turri.yaml names none", fix: "turri init-config writes the two models" }];
|
|
155
|
+
return [{ level: "missing", name: "Models", detail: "config.turri.yaml names none", fix: "turri init-config writes the two models - or name them here: --harness-model, --coding-model" }];
|
|
155
156
|
}
|
|
156
157
|
let verdicts;
|
|
157
158
|
try {
|
|
@@ -167,16 +168,17 @@ export async function checkProjectModels(project, access) {
|
|
|
167
168
|
: { level: "missing", name, detail: verdict.spec, more: [verdict.problem], fix: verdict.fix };
|
|
168
169
|
});
|
|
169
170
|
}
|
|
171
|
+
/** `modelsGiven`: models named on the command line - checked wherever doctor runs, a project or not. */
|
|
170
172
|
async function allChecks(options = {}) {
|
|
171
173
|
const checks = [checkNode(), checkShell(), checkGit(), checkSandbox(), await checkProvider(), checkPath(), ...checkGlobal()];
|
|
172
174
|
// In a project: what it needs of this machine, and whether its models answer.
|
|
173
175
|
const project = process.cwd();
|
|
174
|
-
|
|
176
|
+
const inProject = hasProjectConfig(project) || existsSync(join(project, "plain_modules"));
|
|
177
|
+
if (inProject)
|
|
175
178
|
checks.push(...checkProject(project));
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
179
|
+
if (options.models !== false && (hasProjectConfig(project) || options.modelsGiven === true)) {
|
|
180
|
+
console.log(brand.muted("Asking each model the config names for a word..."));
|
|
181
|
+
checks.push(...(await checkProjectModels(project)));
|
|
180
182
|
}
|
|
181
183
|
return checks;
|
|
182
184
|
}
|
|
@@ -235,21 +237,27 @@ function removeGlobalExtension() {
|
|
|
235
237
|
rmSync(file, { force: true });
|
|
236
238
|
console.log(` removed ${file}`);
|
|
237
239
|
}
|
|
240
|
+
const USAGE = [
|
|
241
|
+
"usage: turri doctor [--harness-model <m>] [--coding-model <m>] [--judge-model <m>] [--no-models] [--yes]",
|
|
242
|
+
"",
|
|
243
|
+
"Check everything turri needs - in a project, what its modules need of this machine too, and that",
|
|
244
|
+
"each model its config names answers with this machine's credentials (one short request a model;",
|
|
245
|
+
"--no-models skips it). The model options are read as the config's keys, as turri regenerate reads them:",
|
|
246
|
+
"a headless run's models can be checked before it starts, with no config.turri.yaml.",
|
|
247
|
+
"It changes nothing but this, asked first: a plain pi that an older turri made load turri too is put back to",
|
|
248
|
+
"plain pi. --yes answers yes.",
|
|
249
|
+
].join("\n");
|
|
238
250
|
export async function main(args) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
"each model its config names answers with this machine's credentials (one short request a model;\n--no-models skips it).\n" +
|
|
242
|
-
"It changes nothing but this, asked first: a plain pi that an older turri made load turri too is put back to\n" +
|
|
243
|
-
"plain pi. --yes answers yes.");
|
|
244
|
-
return 0;
|
|
245
|
-
}
|
|
251
|
+
const { options } = parseArgs(args, ["--no-models", "--yes", "-y"], USAGE, ["--harness-model", "--judge-model"]);
|
|
252
|
+
configKeyOptions(options);
|
|
246
253
|
console.log(brand.bold(brand.accent("Checking what turri needs.")));
|
|
247
254
|
if (!nodeVersionOk())
|
|
248
255
|
return report([checkNode()]);
|
|
249
|
-
const
|
|
256
|
+
const modelsGiven = ["harness-model", "model", "judge-model"].some((name) => options[name] !== undefined);
|
|
257
|
+
const code = report(await allChecks({ models: options["no-models"] === undefined, modelsGiven }));
|
|
250
258
|
// turri and pi are kept apart: a plain pi does not start turri. An older turri's file that made it do so goes, asked.
|
|
251
259
|
const global = globalExtensionFile();
|
|
252
|
-
if (global !== undefined && (await confirm(`\nA plain \`pi\` loads turri too, through ${global}. Remove it, so pi stays pi?`, true,
|
|
260
|
+
if (global !== undefined && (await confirm(`\nA plain \`pi\` loads turri too, through ${global}. Remove it, so pi stays pi?`, true, options.yes !== undefined || args.includes("-y")))) {
|
|
253
261
|
removeGlobalExtension();
|
|
254
262
|
}
|
|
255
263
|
return code;
|
package/dist/spec-diff-cli.js
CHANGED
|
@@ -2,7 +2,8 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { checkPlainFile } from "./plain/check.js";
|
|
4
4
|
import { applySpecDiff, revertSpecDiff } from "./regenerate/regenerate.js";
|
|
5
|
-
import {
|
|
5
|
+
import { parseArgs } from "./cli-options.js";
|
|
6
|
+
import { headlessOptions, runLoop } from "./loop-cli.js";
|
|
6
7
|
const USAGE = `usage: turri spec-diff <file.plain> <diff-file> [options]
|
|
7
8
|
|
|
8
9
|
Applies a change to the specs, given as a git diff of the .plain files, and builds it: the headless
|
package/docs/development.md
CHANGED
|
@@ -18,6 +18,12 @@ which tests reach which mutant (`vitest.stryker.config.ts`, the suite without th
|
|
|
18
18
|
mutant against those tests. `scripts/mutation.mjs` sets two things in Stryker's vitest runner the suite needs - forked
|
|
19
19
|
workers, and a test-name filter that matches vitest 5's names - again after every `npm install`.
|
|
20
20
|
|
|
21
|
+
The `mutation` workflow (`.github/workflows/mutation.yml`) runs it on GitHub, started by hand: Actions, mutation, Run
|
|
22
|
+
workflow. It takes the parts to run, space-separated (empty for every part), each a job of its own side by side, on
|
|
23
|
+
four cores (`TURRI_MUTATION_CONCURRENCY`; six here). Each part's `mutation.html` and `mutation.json` are uploaded as an
|
|
24
|
+
artifact, `mutation-<part>`, and summed up on the run's page. Stryker's incremental file is cached from run to run, so
|
|
25
|
+
a run tests again only the mutants whose code or tests changed; tick `fresh` to test every one again.
|
|
26
|
+
|
|
21
27
|
Working on turri itself, `npm link` points the global `turri` at this clone, so edits
|
|
22
28
|
take effect immediately. `npm run chat` starts pi with the extension loaded from `src/`.
|
|
23
29
|
|
package/docs/headless.md
CHANGED
|
@@ -14,7 +14,8 @@ Everything marked `✗` stops the run, so fix those first. Two more things matte
|
|
|
14
14
|
a module's implementation reqs name that this machine lacks.
|
|
15
15
|
- **The models.** In a project, `turri doctor` sends each model `config.turri.yaml` names one short request, as turri's
|
|
16
16
|
agents will - so a key the provider refuses, a model pi does not know or an account without access to it shows now,
|
|
17
|
-
not an hour into the run.
|
|
17
|
+
not an hour into the run. Models the run is given on the command line are given to doctor the same way -
|
|
18
|
+
`--harness-model`, `--coding-model`, `--judge-model`, read as the config's keys - and are checked with the config's.
|
|
18
19
|
- **Credentials that do not expire mid-run.** An API key in the environment
|
|
19
20
|
(`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, ...) or pi's saved login - whatever
|
|
20
21
|
`doctor` shows as the model provider.
|
|
@@ -260,9 +261,16 @@ turri regenerate app.plain --harness-model amazon-bedrock/eu.anthropic.claude-op
|
|
|
260
261
|
- **What it needs:** Node 22.19 or newer, `bash`, `git` (each module's history, and the spec diffs
|
|
261
262
|
the work is planned from), `/bin/sh` with `kill` and `sleep`, a writable `HOME` (pi keeps
|
|
262
263
|
`~/.pi/agent/auth.json` there, even with no login - or set `PI_CODING_AGENT_DIR`) and `TMPDIR`, and
|
|
263
|
-
the toolchains the project's testing scripts and implementation reqs name. `turri doctor
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
the toolchains the project's testing scripts and implementation reqs name. `turri doctor` in the project, given
|
|
265
|
+
the run's models, checks them all and that each model answers - at the start of the task's log, not when a pulse
|
|
266
|
+
fails:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
turri doctor --harness-model amazon-bedrock/eu.anthropic.claude-opus-5-5 \
|
|
270
|
+
--coding-model amazon-bedrock/eu.anthropic.claude-sonnet-5-5 --judge-model amazon-bedrock/eu.anthropic.claude-opus-5-5
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
It exits 1 on anything marked `✗`.
|
|
266
274
|
- **Without bubblewrap:** a container that cannot make user namespaces - no
|
|
267
275
|
`--security-opt seccomp=unconfined` - has no OS sandbox. turri runs anyway, the command check and the
|
|
268
276
|
audit holding alone under the agents' commands; `turri doctor` shows it as `!`, not `✗`. Installing
|
package/docs/installation.md
CHANGED
|
@@ -68,7 +68,7 @@ In a project - a folder with `config.turri.yaml` (or the old `config.yaml`) or `
|
|
|
68
68
|
| --- | --- |
|
|
69
69
|
| each module's toolchains | what its implementation reqs name - Python, Node, Java, Go, Rust, C/C++, Zig, ... - against this machine's PATH: the agents use this machine's toolchains and install none, so a missing one is yours to install |
|
|
70
70
|
| the testing scripts | what in them this machine does not have: a program named by its whole path that is not here (a macOS-only `/usr/libexec/java_home` on Linux), a toolchain they run that is not on the PATH |
|
|
71
|
-
| the models | each model `config.turri.yaml` names - `harness-model`, `coding-model`, `judge-model`, every role under `models:` - sent one short request through pi, with the credentials turri's agents will use, for any provider pi supports. One that answers is ready; for one that does not, it says what the provider said and what to do: the credentials pi reads for that provider (`AWS_PROFILE` and `AWS_REGION` for Amazon Bedrock, `OPENAI_API_KEY`, `/login`, ...), model access in the Bedrock console, the closest model ids pi knows for one it does not. `--no-models` skips it |
|
|
71
|
+
| the models | each model `config.turri.yaml` names - `harness-model`, `coding-model`, `judge-model`, every role under `models:` - sent one short request through pi, with the credentials turri's agents will use, for any provider pi supports. One that answers is ready; for one that does not, it says what the provider said and what to do: the credentials pi reads for that provider (`AWS_PROFILE` and `AWS_REGION` for Amazon Bedrock, `OPENAI_API_KEY`, `/login`, ...), model access in the Bedrock console, the closest model ids pi knows for one it does not. `--harness-model`, `--coding-model` (`--model`) and `--judge-model` name models as the config's keys, as `turri regenerate` reads them - checked in a project or not. `--no-models` skips it |
|
|
72
72
|
|
|
73
73
|
Removing that file is the one thing `doctor` changes, and it asks first; `turri doctor --yes` says yes. Run without a
|
|
74
74
|
terminal - piped, or in CI - it asks nothing and changes nothing. Run it again whenever something breaks;
|
package/docs/using.md
CHANGED
|
@@ -13,8 +13,10 @@ banner - the jellyfish it is named after, swimming until the conversation fills
|
|
|
13
13
|
terminal that shows images (Kitty, Ghostty, WezTerm, Warp, iTerm2), and drawn in text in any other, tmux
|
|
14
14
|
included - swimming a stroke a loop, its tentacles following its bell, frames `scripts/jellyfish-frames.mjs`
|
|
15
15
|
writes -
|
|
16
|
-
|
|
17
|
-
and resource list; `turri --verbose` shows
|
|
16
|
+
turri's version (and, run from a clone, the commit it is on, and whether the clone has changes), where commands
|
|
17
|
+
will run, the rules and skills loaded - rather than pi's startup header and resource list; `turri --verbose` shows
|
|
18
|
+
those too. pi's "Update Available" notice is left out - turri's pi is a pinned dependency `pi update` never
|
|
19
|
+
touches - and the terminal's tab reads `🪼 turri - <folder>`, not pi's `π`. Talk to it: ask it to start a
|
|
18
20
|
project, add a feature, render a module, fix a spec. It decides which skill or tool applies - but
|
|
19
21
|
not *how* a skill runs: `cultivate` hands the work to the gated runner, which owns the steps, the
|
|
20
22
|
gates and the judges (see [skills and gates](skills-and-gates.md)).
|