@turri-dev/cli 0.1.3-next.17f0f7f → 0.1.3-next.2babf06
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 +93 -32
- package/dist/loop-cli.js +7 -74
- package/dist/pi-launch.js +13 -4
- package/dist/picture-row.js +21 -0
- package/dist/regenerate/panel-dock.js +57 -0
- package/dist/regenerate/side-panel.js +14 -4
- package/dist/regenerate/test-scripts.js +23 -8
- 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 +7 -0
- package/docs/headless.md +12 -4
- package/docs/installation.md +1 -1
- package/docs/regenerating.md +6 -4
- package/docs/using.md +13 -4
- 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
|
@@ -5,7 +5,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
5
5
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { conversationCommand, conversationSettled, conversationWriting } from "./regenerate/conversation-writes.js";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { allocateImageId, Box,
|
|
8
|
+
import { allocateImageId, Box, getCapabilities, getPngDimensions, isKeyRelease, isKeyRepeat, matchesKey, renderImage, Text, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
9
9
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { Type } from "typebox";
|
|
11
11
|
import { CODING_MODEL_ENV } from "./chat-models.js";
|
|
@@ -31,6 +31,8 @@ import { whyFailed } from "./regenerate/diagnose-job.js";
|
|
|
31
31
|
import { createWorkLoop, describeModules, isUpToDate, previewJobs } from "./regenerate/loop.js";
|
|
32
32
|
import { brandBlue, plainPaint } from "./regenerate/plain-palette.js";
|
|
33
33
|
import { createJobPanel, didNotPass, keyPressed, PANEL_KEY, spinner } from "./regenerate/job-panel.js";
|
|
34
|
+
import { clickToFocus, dockedWidth, narrowChat } from "./regenerate/panel-dock.js";
|
|
35
|
+
import { overPictureRow, PICTURE_AT } from "./picture-row.js";
|
|
34
36
|
import { skillAboutLine, skillProgressLine } from "./skill-progress.js";
|
|
35
37
|
import { createSidePanel } from "./regenerate/side-panel.js";
|
|
36
38
|
import { createSpecMapSource, defaultMapFiles } from "./regenerate/spec-map.js";
|
|
@@ -52,6 +54,7 @@ import { registerStacks } from "./stacks.js";
|
|
|
52
54
|
import { catalogueLine, discoverSkills, leftOutNote } from "./skills.js";
|
|
53
55
|
import { supersededByTurri } from "./chat-skills.js";
|
|
54
56
|
import { TURRI_IDENTITY } from "./identity.js";
|
|
57
|
+
import { turriIdentity } from "./environment.js";
|
|
55
58
|
import { conformanceChooser } from "./regenerate/choose-conformance.js";
|
|
56
59
|
import { answerSideQuestion, askWithAsides, BTW_HOW, runningSkill, skillRunStarted, skillSessionText, transcriptOf } from "./btw.js";
|
|
57
60
|
import { createJobRegistry, defaultParallelJobs, describeJob, formatDuration, pulseName, shouldWait, STOPPED_ON_REQUEST, } from "./regenerate/jobs.js";
|
|
@@ -1467,9 +1470,17 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1467
1470
|
return { content: [{ type: "text", text }], details: {} };
|
|
1468
1471
|
},
|
|
1469
1472
|
});
|
|
1470
|
-
// The side panel: the regeneration - and the pulses behind p - over the whole screen
|
|
1471
|
-
// key
|
|
1473
|
+
// The side panel: the regeneration - and the pulses behind p - over the whole screen, or beside the chat (s, from one to
|
|
1474
|
+
// the other). Its key (PANEL_KEY) opens it; the key again closes it and goes back to the chat, as esc does over the
|
|
1475
|
+
// screen - beside the chat, esc gives the chat its keys back and the panel stays, and the key gives them to the panel.
|
|
1476
|
+
// While the panel has the keys, every key is its own.
|
|
1472
1477
|
let panel;
|
|
1478
|
+
// Beside the chat or over it: as the person last left it, for the conversation.
|
|
1479
|
+
let panelDocked = false;
|
|
1480
|
+
// The terminal the panel is drawn on, for its width: set as it opens.
|
|
1481
|
+
let panelTerminal;
|
|
1482
|
+
/** The columns the panel takes beside the chat - 0 over the whole screen, or when the terminal is too narrow to share. */
|
|
1483
|
+
const panelBeside = () => (panelDocked && panelTerminal !== undefined ? dockedWidth(panelTerminal.columns) : 0);
|
|
1473
1484
|
// What is open in the pulse list and the map, and where their cursors are: the conversation's, as left each time it opens.
|
|
1474
1485
|
const jobsTree = createJobsTreeState();
|
|
1475
1486
|
const mapState = createSpecMapState();
|
|
@@ -1497,13 +1508,51 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1497
1508
|
needsLooking.delete(jobs);
|
|
1498
1509
|
if (panel !== undefined) {
|
|
1499
1510
|
panel.handle?.setHidden(false);
|
|
1511
|
+
panel.keys = true;
|
|
1500
1512
|
panel.handle?.focus();
|
|
1501
1513
|
return;
|
|
1502
1514
|
}
|
|
1503
1515
|
// Not awaited: the panel lives on while the jobs it shows run.
|
|
1504
1516
|
void ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
1505
1517
|
const close = () => done(undefined);
|
|
1518
|
+
panelTerminal = tui.terminal;
|
|
1506
1519
|
let component;
|
|
1520
|
+
// Beside the chat: the chat drawn narrower, on the left, and a click on either side giving it the keys - undone
|
|
1521
|
+
// when the panel goes over the screen, or closes.
|
|
1522
|
+
let undock;
|
|
1523
|
+
const layOut = () => {
|
|
1524
|
+
if (panelDocked && undock === undefined) {
|
|
1525
|
+
const widen = narrowChat(tui, panelBeside);
|
|
1526
|
+
const unwatch = clickToFocus(tui, () => tui.terminal.columns - panelBeside(), (side) => {
|
|
1527
|
+
if (panelBeside() === 0)
|
|
1528
|
+
return;
|
|
1529
|
+
if (side === "chat")
|
|
1530
|
+
toChat();
|
|
1531
|
+
else
|
|
1532
|
+
toPanel();
|
|
1533
|
+
});
|
|
1534
|
+
undock = () => {
|
|
1535
|
+
unwatch();
|
|
1536
|
+
widen();
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
else if (!panelDocked && undock !== undefined) {
|
|
1540
|
+
undock();
|
|
1541
|
+
undock = undefined;
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
// Beside the chat, back to it: the chat has the keys, the panel stays.
|
|
1545
|
+
const toChat = () => {
|
|
1546
|
+
entry.keys = false;
|
|
1547
|
+
entry.handle?.unfocus();
|
|
1548
|
+
tui.requestRender();
|
|
1549
|
+
};
|
|
1550
|
+
const toPanel = () => {
|
|
1551
|
+
entry.keys = true;
|
|
1552
|
+
entry.handle?.focus();
|
|
1553
|
+
tui.requestRender();
|
|
1554
|
+
};
|
|
1555
|
+
const back = () => (panelBeside() > 0 ? toChat() : close());
|
|
1507
1556
|
// Opened, it reads the files again at once: they may have changed while it was closed.
|
|
1508
1557
|
specMap.refresh();
|
|
1509
1558
|
const view = createSidePanel({
|
|
@@ -1513,6 +1562,14 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1513
1562
|
mapState,
|
|
1514
1563
|
rows: () => tui.terminal.rows,
|
|
1515
1564
|
key: PANEL_KEY,
|
|
1565
|
+
dock: {
|
|
1566
|
+
docked: () => panelBeside() > 0,
|
|
1567
|
+
toggle: () => {
|
|
1568
|
+
panelDocked = panelBeside() === 0;
|
|
1569
|
+
layOut();
|
|
1570
|
+
tui.requestRender();
|
|
1571
|
+
},
|
|
1572
|
+
},
|
|
1516
1573
|
style: {
|
|
1517
1574
|
heading: (text) => theme.bold(theme.fg("accent", text)),
|
|
1518
1575
|
muted: (text) => theme.fg("muted", text),
|
|
@@ -1527,22 +1584,25 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1527
1584
|
// What conformance tests show does what its spec says: brand blue.
|
|
1528
1585
|
conforms: brandBlue(theme),
|
|
1529
1586
|
},
|
|
1530
|
-
back
|
|
1587
|
+
back,
|
|
1531
1588
|
close,
|
|
1532
1589
|
// A module that needs the person: the chat takes it up with them, in their words - what went wrong, and what now.
|
|
1533
1590
|
discuss: (page) => {
|
|
1534
|
-
|
|
1591
|
+
back();
|
|
1535
1592
|
const why = page.codebase?.stuck?.why;
|
|
1536
1593
|
pi.sendUserMessage(`regen says ${page.module} needs me: ${page.status}. What went wrong, and what do we do about it?${why !== undefined ? `\n\n(turri's reason: ${why})` : ""}`, { deliverAs: "followUp" });
|
|
1537
1594
|
},
|
|
1538
1595
|
// Something took the keys' focus while the panel showed - a question in the chat - and waits there.
|
|
1539
1596
|
waiting: () => {
|
|
1597
|
+
if (!entry.keys)
|
|
1598
|
+
return false;
|
|
1540
1599
|
const focused = tui.getFocusedComponent?.() ?? null;
|
|
1541
1600
|
return focused !== null && focused !== component;
|
|
1542
1601
|
},
|
|
1543
1602
|
});
|
|
1544
|
-
const entry = { view, close };
|
|
1603
|
+
const entry = { view, close, keys: true };
|
|
1545
1604
|
panel = entry;
|
|
1605
|
+
layOut();
|
|
1546
1606
|
// Every key goes to the panel while it shows, whatever has the focus: a question the chat model asks in the
|
|
1547
1607
|
// background takes the focus, but no key reaches it - an enter never answers it unseen. It waits in the chat
|
|
1548
1608
|
// until the panel closes, and has the keys then. Mouse events are the screen's, before this sees them.
|
|
@@ -1564,6 +1624,11 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1564
1624
|
}
|
|
1565
1625
|
if (!state)
|
|
1566
1626
|
return undefined;
|
|
1627
|
+
// Beside the chat, with the chat's keys: they are the chat's - PANEL_KEY among them, for the shortcut that gives
|
|
1628
|
+
// them to the panel. A click on the panel gives it the focus, and the keys with it.
|
|
1629
|
+
if (!entry.keys && entry.handle?.isFocused() !== true)
|
|
1630
|
+
return undefined;
|
|
1631
|
+
entry.keys = true;
|
|
1567
1632
|
// A terminal on the Kitty keyboard protocol reports a key's release, and listeners are given it: the release of
|
|
1568
1633
|
// the ctrl+h that opened the panel would close it again at once, and every arrow would move twice. Only
|
|
1569
1634
|
// presses count - and ctrl+h held down, repeating, does not open and close it over and over.
|
|
@@ -1590,7 +1655,8 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1590
1655
|
}, WATCH_REDRAW_MS);
|
|
1591
1656
|
timer.unref?.();
|
|
1592
1657
|
component = {
|
|
1593
|
-
|
|
1658
|
+
// Beside the chat, a rule down its left edge between them.
|
|
1659
|
+
render: (width) => (panelBeside() > 0 ? view.render(width - 2).map((line) => `${theme.fg("borderMuted", "│")} ${line}`) : view.render(width)),
|
|
1594
1660
|
// Reached only if the listener above is gone: the same, so the panel never passes a key on.
|
|
1595
1661
|
handleInput: (data) => {
|
|
1596
1662
|
if (isKeyRepeat(data) && matchesKey(data, PANEL_KEY))
|
|
@@ -1606,6 +1672,7 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1606
1672
|
dispose: () => {
|
|
1607
1673
|
unlisten();
|
|
1608
1674
|
clearInterval(timer);
|
|
1675
|
+
undock?.();
|
|
1609
1676
|
if (panel === entry)
|
|
1610
1677
|
panel = undefined;
|
|
1611
1678
|
},
|
|
@@ -1613,8 +1680,15 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1613
1680
|
return component;
|
|
1614
1681
|
}, {
|
|
1615
1682
|
overlay: true,
|
|
1616
|
-
// The whole screen
|
|
1617
|
-
overlayOptions: {
|
|
1683
|
+
// The whole screen, or the columns on the right beside the chat - read at each frame, so it follows the terminal.
|
|
1684
|
+
overlayOptions: {
|
|
1685
|
+
get width() {
|
|
1686
|
+
return panelBeside() > 0 ? panelBeside() : "100%";
|
|
1687
|
+
},
|
|
1688
|
+
maxHeight: "100%",
|
|
1689
|
+
anchor: "top-right",
|
|
1690
|
+
margin: 0,
|
|
1691
|
+
},
|
|
1618
1692
|
onHandle: (handle) => {
|
|
1619
1693
|
if (panel !== undefined)
|
|
1620
1694
|
panel.handle = handle;
|
|
@@ -1640,7 +1714,7 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1640
1714
|
panelOpeners.set(jobs, (ctx) => openPanel(ctx));
|
|
1641
1715
|
questionPeek = {
|
|
1642
1716
|
open: (ctx) => openPanel(ctx),
|
|
1643
|
-
shown: () => panel !== undefined,
|
|
1717
|
+
shown: () => panel !== undefined && panel.keys,
|
|
1644
1718
|
};
|
|
1645
1719
|
pi.registerShortcut(PANEL_KEY, {
|
|
1646
1720
|
description: "Peek at regen and the pulses - a question waiting in the chat waits for you - or go back to it",
|
|
@@ -2158,7 +2232,8 @@ function registerBranding(pi, ruleCount, skills) {
|
|
|
2158
2232
|
if (data === undefined)
|
|
2159
2233
|
return undefined;
|
|
2160
2234
|
const lines = [
|
|
2161
|
-
`${theme.fg("accent", "turri")} ${theme.fg("muted", `v${data.version}`)}
|
|
2235
|
+
`${theme.fg("accent", "turri")} ${theme.fg("muted", `v${data.version}`)}` +
|
|
2236
|
+
(data.commit !== undefined ? theme.fg("dim", ` from a clone, ${data.commit}${data.changed ? " with changes" : ""}`) : ""),
|
|
2162
2237
|
theme.fg("text", "regenerative software - the code is regenerated from the spec"),
|
|
2163
2238
|
theme.fg("muted", `${data.rules} rules · ${data.skills} skill${data.skills === 1 ? "" : "s"}` +
|
|
2164
2239
|
(data.projectSkills !== undefined && data.projectSkills > 0 ? ` (${data.projectSkills} the project's)` : "") +
|
|
@@ -2180,8 +2255,11 @@ function registerBranding(pi, ruleCount, skills) {
|
|
|
2180
2255
|
if (ctx.hasUI)
|
|
2181
2256
|
animateJellyfish(ctx);
|
|
2182
2257
|
try {
|
|
2258
|
+
const identity = turriIdentity();
|
|
2183
2259
|
pi.appendEntry(BANNER_ENTRY, {
|
|
2184
|
-
version:
|
|
2260
|
+
version: identity.version ?? "0.0.0",
|
|
2261
|
+
commit: identity.git_commit?.slice(0, 7),
|
|
2262
|
+
changed: identity.git_dirty === true,
|
|
2185
2263
|
rules: ruleCount,
|
|
2186
2264
|
skills: skills.skills.length,
|
|
2187
2265
|
projectSkills: skills.skills.filter((skill) => skill.origin !== "turri").length,
|
|
@@ -2234,8 +2312,6 @@ function animateJellyfish(ctx) {
|
|
|
2234
2312
|
}, { placement: "belowEditor" });
|
|
2235
2313
|
}
|
|
2236
2314
|
const JELLYFISH_CLOCK_KEY = "turri-jellyfish";
|
|
2237
|
-
/** What the banner puts between a row's text and the picture after it: back to the jellyfish's column. */
|
|
2238
|
-
const PICTURE_AT = "\x1b[2G";
|
|
2239
2315
|
/** The columns the banner's picture takes from the left edge, while it shows one; 0 when it does not. */
|
|
2240
2316
|
let pictureRight = 0;
|
|
2241
2317
|
/**
|
|
@@ -2250,14 +2326,7 @@ function drawOverlaysBesidePicture(tui) {
|
|
|
2250
2326
|
return;
|
|
2251
2327
|
target.turriPicture = true;
|
|
2252
2328
|
const original = target.compositeLineAt.bind(tui);
|
|
2253
|
-
target.compositeLineAt = (base, overlay, col, width, total) =>
|
|
2254
|
-
const at = base.lastIndexOf(PICTURE_AT);
|
|
2255
|
-
const picture = at === -1 ? "" : base.slice(at + PICTURE_AT.length);
|
|
2256
|
-
if (!/^(\x1b\[\d+A)?(\x1b_G|\x1b\]1337;File=)/.test(picture))
|
|
2257
|
-
return original(base, overlay, col, width, total);
|
|
2258
|
-
const drawn = compositeTuiLine(base.slice(0, at), overlay, col, width, total);
|
|
2259
|
-
return col >= pictureRight ? `${drawn}${PICTURE_AT}${picture}` : drawn;
|
|
2260
|
-
};
|
|
2329
|
+
target.compositeLineAt = (base, overlay, col, width, total) => overPictureRow(base, overlay, col, width, total, pictureRight) ?? original(base, overlay, col, width, total);
|
|
2261
2330
|
}
|
|
2262
2331
|
/** Columns between the jellyfish and the lines beside it. */
|
|
2263
2332
|
const JELLYFISH_GAP = 3;
|
|
@@ -2289,7 +2358,8 @@ class Banner {
|
|
|
2289
2358
|
const art = this.art;
|
|
2290
2359
|
const placed = art?.kind === "image" ? this.placeImage(art, Math.max(JELLYFISH_ROWS, this.lines.length), width - 2 - JELLYFISH_GAP - BANNER_TEXT_MIN) : art?.kind === "text" && art.columns <= width - 2 - JELLYFISH_GAP - BANNER_TEXT_MIN ? art : undefined;
|
|
2291
2360
|
const left = placed !== undefined ? 1 + placed.columns + JELLYFISH_GAP : 1;
|
|
2292
|
-
|
|
2361
|
+
// Wrapped in the columns beside the picture, never cut short - the chat beside the docked panel is narrow.
|
|
2362
|
+
const text = this.lines.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width - 1 - left)));
|
|
2293
2363
|
const height = Math.max(text.length, placed?.rows ?? 0);
|
|
2294
2364
|
const above = Math.floor((height - text.length) / 2);
|
|
2295
2365
|
const drawn = placed?.kind === "text" ? placed.frames[jellyfishFrame % placed.frames.length] : undefined;
|
|
@@ -2361,12 +2431,3 @@ function jellyfishArt(appearance, ink) {
|
|
|
2361
2431
|
return undefined;
|
|
2362
2432
|
}
|
|
2363
2433
|
}
|
|
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.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { compositeTuiLine } from "@earendil-works/pi-tui";
|
|
2
|
+
/** What the banner puts between a row's text and the picture after it: back to the jellyfish's column. */
|
|
3
|
+
export const PICTURE_AT = "\x1b[2G";
|
|
4
|
+
/** The picture's escape sequence, as the banner writes it after PICTURE_AT: Kitty's, in chunks, or iTerm2's - climbing first on iTerm2. */
|
|
5
|
+
const PICTURE = /^(?:\x1b\[\d+A)?(?:(?:\x1b_G[^\x1b]*\x1b\\)+|\x1b\]1337;File=[^\x07]*\x07)/;
|
|
6
|
+
/**
|
|
7
|
+
* The banner's picture row with an overlay drawn over its text, the picture after it - left out while the overlay
|
|
8
|
+
* reaches over the picture, `pictureRight` columns from the left edge. Undefined for a row that holds no picture.
|
|
9
|
+
*
|
|
10
|
+
* What follows the picture - the padding a row narrower than the screen is given, the chat's beside the docked panel -
|
|
11
|
+
* is the row's text: written after the picture, from the picture's column, it would blank the row.
|
|
12
|
+
*/
|
|
13
|
+
export function overPictureRow(base, overlay, col, width, total, pictureRight) {
|
|
14
|
+
const at = base.lastIndexOf(PICTURE_AT);
|
|
15
|
+
const picture = at === -1 ? null : PICTURE.exec(base.slice(at + PICTURE_AT.length));
|
|
16
|
+
if (picture === null)
|
|
17
|
+
return undefined;
|
|
18
|
+
const after = base.slice(at + PICTURE_AT.length + picture[0].length);
|
|
19
|
+
const drawn = compositeTuiLine(`${base.slice(0, at)}${after}`, overlay, col, width, total);
|
|
20
|
+
return col >= pictureRight ? `${drawn}${PICTURE_AT}${picture[0]}` : drawn;
|
|
21
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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 - 0 when the terminal is too narrow to share. */
|
|
13
|
+
export function dockedWidth(columns) {
|
|
14
|
+
return columns < SHARED_FROM ? 0 : Math.max(48, Math.floor(columns * 0.45));
|
|
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
|
+
}
|
|
@@ -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 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();
|
|
@@ -609,19 +609,34 @@ function timedOutFailure(run, what) {
|
|
|
609
609
|
`the same ${limit} seconds, and a skipped or deleted test does not count as a fix.`,
|
|
610
610
|
].join("\n");
|
|
611
611
|
}
|
|
612
|
+
/** A command `timeout` could not start, or a shell could not find - on a line of its own. */
|
|
613
|
+
const COULD_NOT_RUN = /^(?:[^:]*: )?failed to run command [‘'`"]?([\w.+-]+)[’'`"]?: No such file or directory$/;
|
|
614
|
+
const NOT_FOUND = /^(?:[^:]*: )?(?:line \d+: )?([\w.+-]+): command not found$/;
|
|
612
615
|
/**
|
|
613
616
|
* 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`
|
|
617
|
+
* the run failed for another reason. A missing command is the cause only on evidence that it ended the run: exit 69,
|
|
618
|
+
* the testing scripts' own way to say a toolchain is missing, or 127, what a shell or `timeout` gives for a command it
|
|
619
|
+
* could not find - else a command not found on the last line the run printed, nothing having run after it. Earlier in
|
|
620
|
+
* the output it is a warning, not the failure: a shell profile's, an optional tool probed for, a fallback taken - and a
|
|
621
|
+
* run that failed for its own reasons is the agent's to fix, not a stop for the person.
|
|
616
622
|
*/
|
|
617
623
|
export function missingToolchain(run) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (run.exitCode === 69 || run.exitCode === 127)
|
|
624
|
+
if (run.exitCode === 0)
|
|
625
|
+
return undefined;
|
|
626
|
+
const lines = run.output.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
627
|
+
const named = (line) => COULD_NOT_RUN.exec(line)?.[1] ?? NOT_FOUND.exec(line)?.[1];
|
|
628
|
+
if (run.exitCode === 69 || run.exitCode === 127) {
|
|
629
|
+
// The command is the last one named: the one the run stopped on.
|
|
630
|
+
for (let at = lines.length - 1; at >= 0; at -= 1) {
|
|
631
|
+
const command = named(lines[at]);
|
|
632
|
+
if (command !== undefined)
|
|
633
|
+
return { command };
|
|
634
|
+
}
|
|
623
635
|
return {};
|
|
624
|
-
|
|
636
|
+
}
|
|
637
|
+
const last = lines.at(-1);
|
|
638
|
+
const command = last !== undefined ? named(last) : undefined;
|
|
639
|
+
return command !== undefined ? { command } : undefined;
|
|
625
640
|
}
|
|
626
641
|
/** Where `command` is on this machine's PATH, or undefined when it is not there. */
|
|
627
642
|
export function onThisMachine(command) {
|
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
|
|
|
@@ -69,6 +75,7 @@ trust `turri-oss/turri`, the workflow `publish.yml` and the environment `npm`. R
|
|
|
69
75
|
| `src/regenerate/confine.ts`, `src/regenerate/agent-shell.ts`, `src/regenerate/command-check.ts`, `src/regenerate/os-sandbox.ts`, `src/regenerate/audit.ts`, `src/regenerate/agent-env.ts`, `src/regenerate/shell.ts` | the file-tool fence and the walls around every agent command: the check, the OS sandbox, the audit, the module's environment, the host runner - see [sandbox](sandbox.md) |
|
|
70
76
|
| `src/regenerate/engine.ts` | choosing and resolving render models |
|
|
71
77
|
| `src/regenerate/job-panel.ts`, `src/regenerate/transcript-view.ts`, `src/regenerate/diff-view.ts` | the job panel, the side panel's colours, and a spec diff as they show it - highlighted and banded |
|
|
78
|
+
| `src/regenerate/side-panel.ts`, `src/regenerate/panel-dock.ts` | the side panel, and the chat narrowed beside it when it is docked |
|
|
72
79
|
| `src/regenerate/spec-map.ts`, `src/regenerate/spec-map-view.ts` | regen: each item's mark, from the records, the `.plain` files and the work loop's plan (`mapPlan`), and the map drawn |
|
|
73
80
|
| `src/regenerate/debug-log.ts` | each render agent's whole conversation, always streamed to `.turri/debug/` |
|
|
74
81
|
| `genome/` | what turri knows and can do: `genome/skills/`, the skill library, and `genome/rules/`, the \*\*\*plain authoring rules |
|
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/regenerating.md
CHANGED
|
@@ -218,7 +218,7 @@ and the `implement-unit-testing-script` and `implement-conformance-testing-scrip
|
|
|
218
218
|
- **The formatting is the agents'.** The harness formats nothing: the scripts check, and leave their inputs as they
|
|
219
219
|
found them. Every agent that writes code or tests is told to run, in its shell, the formatter its script checks with,
|
|
220
220
|
on the files it changes, before the final run.
|
|
221
|
-
- **A toolchain missing** where the checks run - exit 69, 127, `command not found` - stops the pulse as it does for the
|
|
221
|
+
- **A toolchain missing** where the checks run - exit 69, 127, a `command not found` the run ended on - stops the pulse as it does for the
|
|
222
222
|
tests: the person installs it, the agents install nothing.
|
|
223
223
|
|
|
224
224
|
### Checking its own work
|
|
@@ -507,8 +507,10 @@ your own environment with this machine's toolchains, turri's model providers' AP
|
|
|
507
507
|
and whatever they do is your doing. That is the only way the two scripts are ever run: the harness's own runs, and the
|
|
508
508
|
agents' `run_unit_tests` and `run_conformance_tests`, which run them exactly as the harness does - through the host
|
|
509
509
|
runner, in a process group of their own, so the time limit and a stopped job end the script and all it started. A
|
|
510
|
-
toolchain the script needs and this machine lacks - exit 69, exit 127, or a `command not found` /
|
|
511
|
-
|
|
510
|
+
toolchain the script needs and this machine lacks - exit 69, exit 127, or a run that ended on a `command not found` /
|
|
511
|
+
`failed to run command`, the last line it printed (`missingToolchain`) - is yours to install. Only on that evidence: a
|
|
512
|
+
`command not found` earlier in the output, the run going on after it and failing for its own reasons, is a warning -
|
|
513
|
+
a shell profile's, an optional tool probed for - and the failure is the agent's to fix. A missing toolchain is yours to install, never the agent's: the agent is told to report it
|
|
512
514
|
with `report_environment_error` rather than work around it, and the pulse stops at the harness's first run of either
|
|
513
515
|
script - unit or conformance - that cannot find it, rolled back, saying what to install (see
|
|
514
516
|
[when the environment lacks something](#when-the-environment-lacks-something)). The code and the tests an agent wrote run unfenced inside the script too: what an agent runs with its own
|
|
@@ -562,7 +564,7 @@ It is raised two ways:
|
|
|
562
564
|
is shown its output once, and a report made again after that stands. A report that stands ends its turn, the pulse
|
|
563
565
|
stops, and what it said goes to you as it is;
|
|
564
566
|
- **by the harness itself**: a testing script - the unit or the conformance one, in a code pulse, a conformance pulse or
|
|
565
|
-
the whole-suite run - that cannot find its toolchain (exit 69 or 127, `command not found`), or
|
|
567
|
+
the whole-suite run - that cannot find its toolchain (exit 69 or 127, a `command not found` the run ended on), or
|
|
566
568
|
a credential `agent-env:` names that is not there when the pulse starts - before any agent does. What the harness
|
|
567
569
|
found it works out at once - the diagnosis, not after a second failure - to say how to fix it on this machine.
|
|
568
570
|
|
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)).
|
|
@@ -128,7 +130,10 @@ come waits for a free slot, and says so.
|
|
|
128
130
|
says `ctrl+h shows this pulse`; **`/regen`** on regen, **`/pulses`** on the pulses; not `ctrl+j`, which a terminal without the Kitty
|
|
129
131
|
keyboard protocol sends as a line feed, the same as `shift+enter`, and which a dialog takes for
|
|
130
132
|
`enter`; both stay pi's new-line keys. `ctrl+h` is the byte old terminals sent for backspace; a
|
|
131
|
-
terminal set to send `^H` for its Backspace key would open the panel on it) takes the whole screen - the panel or the chat, one at a time
|
|
133
|
+
terminal set to send `^H` for its Backspace key would open the panel on it) takes the whole screen - the panel or the chat, one at a time -
|
|
134
|
+
or, `s`, sits beside the chat: the chat on the left, the panel in the right 45% of the screen, and `s` again
|
|
135
|
+
over the whole screen. It opens as it was last left, for the conversation; a terminal under 100 columns has
|
|
136
|
+
no room for both and shows the panel over the whole screen.
|
|
132
137
|
It opens on **regen**: the spec itself, in full, with how much of it the code has been regenerated from drawn
|
|
133
138
|
onto it.
|
|
134
139
|
|
|
@@ -212,7 +217,11 @@ come waits for a free slot, and says so.
|
|
|
212
217
|
closes when it moves to another section. What is open is kept while the panel is closed. What the agents
|
|
213
218
|
said and did is not shown - it is in each pulse's log (see [Agent logs](#agent-logs)).
|
|
214
219
|
|
|
215
|
-
`ctrl+h` again, or `esc`, closes it and goes back to the chat.
|
|
220
|
+
`ctrl+h` again, or `esc`, closes it and goes back to the chat. Beside the chat, a click decides which side has
|
|
221
|
+
the keys - the chat or the panel, the one clicked - and `esc` gives the chat the keys too, the panel staying and
|
|
222
|
+
following the pulses; `ctrl+h` gives the panel the keys again, and from the panel closes it. (pi's regular screen,
|
|
223
|
+
`--tui-mode regular`, reads no mouse: there it is the keys alone.)
|
|
224
|
+
While the panel has the keys, every key is the panel's,
|
|
216
225
|
whatever else happens: a question the chat model asks in the background (`ask_user`) waits in the chat,
|
|
217
226
|
unanswered - an `enter` in the panel never answers it - and the panel's top line says "the chat is waiting
|
|
218
227
|
for your answer". Back in the chat, the question is there, with the keys.
|