@turri-dev/cli 0.1.3-next.26f08c5 → 0.1.3-next.6fab174
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/extension.js +103 -39
- package/dist/identity.js +1 -1
- package/dist/loop-tools.js +1 -1
- package/dist/picture-row.js +21 -0
- package/dist/regenerate/confine.js +1 -0
- package/dist/regenerate/environment-error.js +46 -9
- 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/docs/development.md +1 -0
- package/docs/regenerating.md +17 -5
- package/docs/using.md +10 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -212,7 +212,7 @@ It runs the interview skill, asks you about the problem, the stack and the produ
|
|
|
212
212
|
model and cost - and the queue behind it;
|
|
213
213
|
- ask to try it at any time - "let me try it" - and **`release`** puts what is built so far in
|
|
214
214
|
`dist/<module>` and **`activate`** starts it, installing what it needs - nothing to approve;
|
|
215
|
-
- the **side panel** (`ctrl+h`) takes the screen and opens on **regen**
|
|
215
|
+
- the **side panel** (`ctrl+h`) takes the screen and opens on **regen** - \*\*\*plain, every item marked planned, working, regenerated, validating, conform or needs you - and `p` turns it to the **pulses** (`/pulses`) - every pulse, running, queued and finished, and what each regenerates; what its agents did is in each pulse's log in `.turri/jobs/`;
|
|
216
216
|
- **`/halt`** halts a pulse, **`/lineage`** shows every generation of a
|
|
217
217
|
module, and the **`drift`** tool says what is left to regenerate;
|
|
218
218
|
- **`/btw <question>`** asks turri something on the side - answered at once, even in the middle of a skill's interview
|
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";
|
|
@@ -1468,9 +1470,17 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1468
1470
|
return { content: [{ type: "text", text }], details: {} };
|
|
1469
1471
|
},
|
|
1470
1472
|
});
|
|
1471
|
-
// The side panel: the regeneration - and the pulses behind p - over the whole screen
|
|
1472
|
-
// 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.
|
|
1473
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);
|
|
1474
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.
|
|
1475
1485
|
const jobsTree = createJobsTreeState();
|
|
1476
1486
|
const mapState = createSpecMapState();
|
|
@@ -1498,13 +1508,51 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1498
1508
|
needsLooking.delete(jobs);
|
|
1499
1509
|
if (panel !== undefined) {
|
|
1500
1510
|
panel.handle?.setHidden(false);
|
|
1511
|
+
panel.keys = true;
|
|
1501
1512
|
panel.handle?.focus();
|
|
1502
1513
|
return;
|
|
1503
1514
|
}
|
|
1504
1515
|
// Not awaited: the panel lives on while the jobs it shows run.
|
|
1505
1516
|
void ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
1506
1517
|
const close = () => done(undefined);
|
|
1518
|
+
panelTerminal = tui.terminal;
|
|
1507
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());
|
|
1508
1556
|
// Opened, it reads the files again at once: they may have changed while it was closed.
|
|
1509
1557
|
specMap.refresh();
|
|
1510
1558
|
const view = createSidePanel({
|
|
@@ -1514,6 +1562,14 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1514
1562
|
mapState,
|
|
1515
1563
|
rows: () => tui.terminal.rows,
|
|
1516
1564
|
key: PANEL_KEY,
|
|
1565
|
+
dock: {
|
|
1566
|
+
docked: () => panelBeside() > 0,
|
|
1567
|
+
toggle: () => {
|
|
1568
|
+
panelDocked = panelBeside() === 0;
|
|
1569
|
+
layOut();
|
|
1570
|
+
tui.requestRender();
|
|
1571
|
+
},
|
|
1572
|
+
},
|
|
1517
1573
|
style: {
|
|
1518
1574
|
heading: (text) => theme.bold(theme.fg("accent", text)),
|
|
1519
1575
|
muted: (text) => theme.fg("muted", text),
|
|
@@ -1528,22 +1584,25 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1528
1584
|
// What conformance tests show does what its spec says: brand blue.
|
|
1529
1585
|
conforms: brandBlue(theme),
|
|
1530
1586
|
},
|
|
1531
|
-
back
|
|
1587
|
+
back,
|
|
1532
1588
|
close,
|
|
1533
1589
|
// A module that needs the person: the chat takes it up with them, in their words - what went wrong, and what now.
|
|
1534
1590
|
discuss: (page) => {
|
|
1535
|
-
|
|
1591
|
+
back();
|
|
1536
1592
|
const why = page.codebase?.stuck?.why;
|
|
1537
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" });
|
|
1538
1594
|
},
|
|
1539
1595
|
// Something took the keys' focus while the panel showed - a question in the chat - and waits there.
|
|
1540
1596
|
waiting: () => {
|
|
1597
|
+
if (!entry.keys)
|
|
1598
|
+
return false;
|
|
1541
1599
|
const focused = tui.getFocusedComponent?.() ?? null;
|
|
1542
1600
|
return focused !== null && focused !== component;
|
|
1543
1601
|
},
|
|
1544
1602
|
});
|
|
1545
|
-
const entry = { view, close };
|
|
1603
|
+
const entry = { view, close, keys: true };
|
|
1546
1604
|
panel = entry;
|
|
1605
|
+
layOut();
|
|
1547
1606
|
// Every key goes to the panel while it shows, whatever has the focus: a question the chat model asks in the
|
|
1548
1607
|
// background takes the focus, but no key reaches it - an enter never answers it unseen. It waits in the chat
|
|
1549
1608
|
// until the panel closes, and has the keys then. Mouse events are the screen's, before this sees them.
|
|
@@ -1565,6 +1624,11 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1565
1624
|
}
|
|
1566
1625
|
if (!state)
|
|
1567
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;
|
|
1568
1632
|
// A terminal on the Kitty keyboard protocol reports a key's release, and listeners are given it: the release of
|
|
1569
1633
|
// the ctrl+h that opened the panel would close it again at once, and every arrow would move twice. Only
|
|
1570
1634
|
// presses count - and ctrl+h held down, repeating, does not open and close it over and over.
|
|
@@ -1591,7 +1655,8 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1591
1655
|
}, WATCH_REDRAW_MS);
|
|
1592
1656
|
timer.unref?.();
|
|
1593
1657
|
component = {
|
|
1594
|
-
|
|
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)),
|
|
1595
1660
|
// Reached only if the listener above is gone: the same, so the panel never passes a key on.
|
|
1596
1661
|
handleInput: (data) => {
|
|
1597
1662
|
if (isKeyRepeat(data) && matchesKey(data, PANEL_KEY))
|
|
@@ -1607,6 +1672,7 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1607
1672
|
dispose: () => {
|
|
1608
1673
|
unlisten();
|
|
1609
1674
|
clearInterval(timer);
|
|
1675
|
+
undock?.();
|
|
1610
1676
|
if (panel === entry)
|
|
1611
1677
|
panel = undefined;
|
|
1612
1678
|
},
|
|
@@ -1614,34 +1680,36 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1614
1680
|
return component;
|
|
1615
1681
|
}, {
|
|
1616
1682
|
overlay: true,
|
|
1617
|
-
// The whole screen
|
|
1618
|
-
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
|
+
},
|
|
1619
1692
|
onHandle: (handle) => {
|
|
1620
1693
|
if (panel !== undefined)
|
|
1621
1694
|
panel.handle = handle;
|
|
1622
1695
|
},
|
|
1623
1696
|
});
|
|
1624
1697
|
};
|
|
1625
|
-
//
|
|
1626
|
-
//
|
|
1627
|
-
// in each pulse's log.
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
return;
|
|
1637
|
-
openPanel(ctx, view);
|
|
1638
|
-
},
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1698
|
+
// /pulses opens the side panel on every pulse, regenerating, planned and finished, and what each regenerates; regen -
|
|
1699
|
+
// the spec, with how far the code has been regenerated into it - is PANEL_KEY's, no command of its own beside
|
|
1700
|
+
// /regenerate. What the agents did is in each pulse's log.
|
|
1701
|
+
pi.registerCommand("pulses", {
|
|
1702
|
+
description: `The pulses, in the side panel: every pulse, regenerating, planned and finished, and what each regenerates. ${PANEL_KEY} opens the panel too.`,
|
|
1703
|
+
handler: async (_args, ctx) => {
|
|
1704
|
+
if (!ctx.hasUI)
|
|
1705
|
+
return;
|
|
1706
|
+
openPanel(ctx, "pulses");
|
|
1707
|
+
},
|
|
1708
|
+
});
|
|
1641
1709
|
panelOpeners.set(jobs, (ctx) => openPanel(ctx));
|
|
1642
1710
|
questionPeek = {
|
|
1643
1711
|
open: (ctx) => openPanel(ctx),
|
|
1644
|
-
shown: () => panel !== undefined,
|
|
1712
|
+
shown: () => panel !== undefined && panel.keys,
|
|
1645
1713
|
};
|
|
1646
1714
|
pi.registerShortcut(PANEL_KEY, {
|
|
1647
1715
|
description: "Peek at regen and the pulses - a question waiting in the chat waits for you - or go back to it",
|
|
@@ -1712,8 +1780,12 @@ export const JOB_FINISHED_REACTION = [
|
|
|
1712
1780
|
"A pulse that stopped on its environment - \"stopped: turri's sandbox kept its agents from…\" or \"stopped: the",
|
|
1713
1781
|
"environment lacks…\" - stops the same way again until that is fixed: never regenerate it before. Heal it, then regenerate the",
|
|
1714
1782
|
"module to resume:",
|
|
1715
|
-
"- turri's sandbox kept the agents from a folder on this machine: nothing is missing. Add the folder its report
|
|
1716
|
-
" `toolchains:` in config.turri.yaml - a list, keeping what is there - say so in a line, and resume.",
|
|
1783
|
+
"- turri's sandbox kept the agents from reading a folder on this machine: nothing is missing. Add the folder its report",
|
|
1784
|
+
" names to `toolchains:` in config.turri.yaml - a list, keeping what is there - say so in a line, and resume.",
|
|
1785
|
+
"- turri's sandbox kept the agents from writing a file - a lock a build tool takes, say: no setting lets them, and",
|
|
1786
|
+
" `toolchains:` does not - never add it there, and never resume before the fix. Do what the report says if it is the",
|
|
1787
|
+
" project's to change - a tool told to use `$HOME` in the build's own files - say so, and resume; otherwise tell the",
|
|
1788
|
+
" person, in a line or two, what the report says and that \"resume\" picks it up once it is done.",
|
|
1717
1789
|
"- the machine lacks a toolchain or a tool - a JDK, Maven, Gradle, Go, uv, Node, a system library: first find out, in your",
|
|
1718
1790
|
" shell, whether it is installed somewhere off the PATH (`command -v`, Homebrew's opt folders, /Library/Java/",
|
|
1719
1791
|
" JavaVirtualMachines, a version manager the person uses - mise, asdf, sdkman, nvm). Then ask the person with ask_user",
|
|
@@ -2239,8 +2311,6 @@ function animateJellyfish(ctx) {
|
|
|
2239
2311
|
}, { placement: "belowEditor" });
|
|
2240
2312
|
}
|
|
2241
2313
|
const JELLYFISH_CLOCK_KEY = "turri-jellyfish";
|
|
2242
|
-
/** What the banner puts between a row's text and the picture after it: back to the jellyfish's column. */
|
|
2243
|
-
const PICTURE_AT = "\x1b[2G";
|
|
2244
2314
|
/** The columns the banner's picture takes from the left edge, while it shows one; 0 when it does not. */
|
|
2245
2315
|
let pictureRight = 0;
|
|
2246
2316
|
/**
|
|
@@ -2255,14 +2325,7 @@ function drawOverlaysBesidePicture(tui) {
|
|
|
2255
2325
|
return;
|
|
2256
2326
|
target.turriPicture = true;
|
|
2257
2327
|
const original = target.compositeLineAt.bind(tui);
|
|
2258
|
-
target.compositeLineAt = (base, overlay, col, width, total) =>
|
|
2259
|
-
const at = base.lastIndexOf(PICTURE_AT);
|
|
2260
|
-
const picture = at === -1 ? "" : base.slice(at + PICTURE_AT.length);
|
|
2261
|
-
if (!/^(\x1b\[\d+A)?(\x1b_G|\x1b\]1337;File=)/.test(picture))
|
|
2262
|
-
return original(base, overlay, col, width, total);
|
|
2263
|
-
const drawn = compositeTuiLine(base.slice(0, at), overlay, col, width, total);
|
|
2264
|
-
return col >= pictureRight ? `${drawn}${PICTURE_AT}${picture}` : drawn;
|
|
2265
|
-
};
|
|
2328
|
+
target.compositeLineAt = (base, overlay, col, width, total) => overPictureRow(base, overlay, col, width, total, pictureRight) ?? original(base, overlay, col, width, total);
|
|
2266
2329
|
}
|
|
2267
2330
|
/** Columns between the jellyfish and the lines beside it. */
|
|
2268
2331
|
const JELLYFISH_GAP = 3;
|
|
@@ -2294,7 +2357,8 @@ class Banner {
|
|
|
2294
2357
|
const art = this.art;
|
|
2295
2358
|
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;
|
|
2296
2359
|
const left = placed !== undefined ? 1 + placed.columns + JELLYFISH_GAP : 1;
|
|
2297
|
-
|
|
2360
|
+
// Wrapped in the columns beside the picture, never cut short - the chat beside the docked panel is narrow.
|
|
2361
|
+
const text = this.lines.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width - 1 - left)));
|
|
2298
2362
|
const height = Math.max(text.length, placed?.rows ?? 0);
|
|
2299
2363
|
const above = Math.floor((height - text.length) / 2);
|
|
2300
2364
|
const drawn = placed?.kind === "text" ? placed.frames[jellyfishFrame % placed.frames.length] : undefined;
|
package/dist/identity.js
CHANGED
|
@@ -25,7 +25,7 @@ export const TURRI_IDENTITY = [
|
|
|
25
25
|
"- A pulse is one unit of work regenerating the code - a spec diff regenerated in a codebase, or its conformance",
|
|
26
26
|
" tests. It was called a job, and still is in places: the job_id a tool takes is a pulse's id, and parallel-jobs and",
|
|
27
27
|
" allow-to-stop-jobs in config.turri.yaml are about pulses. \"Job\" in what you read is a pulse; say pulse.",
|
|
28
|
-
"- regen is the side panel (
|
|
28
|
+
"- regen is the side panel (ctrl+h): the spec with each functionality and item marked built, regenerating,",
|
|
29
29
|
" planned or waiting - and behind it the pulses (/pulses), every pulse regenerating, planned and finished. Say regen,",
|
|
30
30
|
" lowercase. It was the regeneration, the job queue, or the job panel.",
|
|
31
31
|
"- mutate, in a skill's activity, is write_spec_diff: a spec change written and regenerated in one step.",
|
package/dist/loop-tools.js
CHANGED
|
@@ -73,7 +73,7 @@ export async function loopReply(loop, statuses, wait) {
|
|
|
73
73
|
export function stopJob(jobs, named, reason) {
|
|
74
74
|
const matches = jobs.list().filter((job) => job.id.startsWith(named.trim()));
|
|
75
75
|
if (matches.length === 0)
|
|
76
|
-
return `No pulse ${named}. /
|
|
76
|
+
return `No pulse ${named}. /pulses shows them.`;
|
|
77
77
|
if (matches.length > 1)
|
|
78
78
|
return `'${named}' matches ${matches.map((job) => job.id).join(", ")}. Give more of the id.`;
|
|
79
79
|
const target = matches[0];
|
|
@@ -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
|
+
}
|
|
@@ -188,6 +188,7 @@ export function sandboxNote(fence) {
|
|
|
188
188
|
const report = fence.environmentTool !== undefined
|
|
189
189
|
? `report it as an environment error with ${fence.environmentTool}`
|
|
190
190
|
: "say so plainly - what is missing and what needs it - and end your turn";
|
|
191
|
+
lines.push("- A command refused a file outside your folder - `Operation not permitted`, `Permission denied`, a lock it could not", ` take - is these walls, not the code, and nothing you change gets past them: ${report} at once, naming the file`, " and the command that wanted it.");
|
|
191
192
|
lines.push(fence.write.length > 0
|
|
192
193
|
? "- The project's dependencies are yours: declare them in the build manifest, and let the build tools fetch them."
|
|
193
194
|
: "- You install nothing.", `${fence.write.length > 0 ? " Installing anything else is not" : " Nor is installing anything on the machine yours"}: no toolchains, system packages, drivers, global packages or \`sudo\`. If the`, ` machine lacks something the code needs, ${report}, name what is missing, and go no further with what depends on it.`, " The person decides.", "- Ports and processes: you may stop only what you started. If a port is taken by something else, use another port.", " `ps` may show nothing but what you started: check a server you started with `kill -0 $!`, its log, or a request to it.", "- Temporary files go in `$TMPDIR`: `/tmp` is outside your walls.");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { basename, dirname, isAbsolute, sep } from "node:path";
|
|
3
4
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { Type } from "typebox";
|
|
@@ -35,7 +36,7 @@ export class EnvironmentError extends Error {
|
|
|
35
36
|
sandbox;
|
|
36
37
|
constructor(details) {
|
|
37
38
|
super(details.sandbox !== undefined
|
|
38
|
-
? `${details.moduleName} stopped: turri's sandbox kept its agents from ${details.sandbox.paths[0]}, which is on this machine.`
|
|
39
|
+
? `${details.moduleName} stopped: turri's sandbox kept its agents from ${details.sandbox.access === "write" ? "writing" : "reading"} ${details.sandbox.paths[0]}, which is on this machine.`
|
|
39
40
|
: `${details.moduleName} stopped: the environment lacks ${details.missing}.`);
|
|
40
41
|
this.name = "EnvironmentError";
|
|
41
42
|
this.moduleName = details.moduleName;
|
|
@@ -50,6 +51,10 @@ export class EnvironmentError extends Error {
|
|
|
50
51
|
this.sandbox = details.sandbox;
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
/** A file's name that says it is a lock: `lockfile`, `x.lock`, `x.lck`, `.lock-x`, `x.lock.y`. */
|
|
55
|
+
const LOCK_FILE = /(?:^|[._-])(?:lock|lck|lockfile)(?:$|[._-])/i;
|
|
56
|
+
/** What says the sandbox refused a write, not a read: writing, creating, a lock not taken - not a name with "lock" in it. */
|
|
57
|
+
const WRITE_REFUSED = /\b(?:writ(?:e|es|ing|able)|creat(?:e|es|ing)|acquir(?:e|ing)|(?:take|obtain|get) (?:a |the )?lock|locking|read-only file system)\b/i;
|
|
53
58
|
/** A folder named for a version - `24.16.0`, `v22.3.0`, `3.12.4` - the root of a version manager's install. */
|
|
54
59
|
const VERSION = /^v?\d+\.\d+/;
|
|
55
60
|
/** The parts of an install below its root, where the root ends: `…/24.16.0/lib/node_modules/…` ends before `lib`. */
|
|
@@ -74,27 +79,59 @@ export function sandboxGapOf(text, options = {}) {
|
|
|
74
79
|
const system = (options.platform ?? process.platform) === "darwin" ? MACOS_MACHINE : LINUX_MACHINE;
|
|
75
80
|
const within = (path, root) => path === root || path.startsWith(`${root}/`);
|
|
76
81
|
const named = [...text.matchAll(/(?:^|[\s'"`(=:])(\/[^\s'"`()<>,;]+)/g)].map((match) => match[1].replace(/[.:]+$/, ""));
|
|
77
|
-
const
|
|
78
|
-
|
|
82
|
+
const isLock = (path) => LOCK_FILE.test(basename(path));
|
|
83
|
+
// A lock file the tool could not create is not there: its folder is.
|
|
84
|
+
const found = [...new Set(named)].filter((path) => path.split("/").length > 3 &&
|
|
85
|
+
(exists(path) || (isLock(path) && exists(dirname(path)))) &&
|
|
86
|
+
!system.some((root) => within(path, root)) &&
|
|
87
|
+
(options.project === undefined || !within(path, options.project)));
|
|
88
|
+
if (found.length === 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
// The lock first: it is what the build could not take.
|
|
91
|
+
const paths = [...found.filter(isLock), ...found.filter((path) => !isLock(path))];
|
|
92
|
+
const lock = isLock(paths[0]);
|
|
93
|
+
return { paths, folder: toolchainFolder(paths[0]), access: lock || WRITE_REFUSED.test(text) ? "write" : "read", ...(lock ? { lock } : {}) };
|
|
79
94
|
}
|
|
80
95
|
/**
|
|
81
96
|
* What the person is told, in the chat, at the end of `turri regenerate`, and in the job's log. `rerun`: the command that runs
|
|
82
97
|
* the work again, as the pulse knows it - the spec the person ran; without it, the module's own spec by its name.
|
|
83
98
|
*/
|
|
84
99
|
export function describeEnvironmentError(error, rerun) {
|
|
85
|
-
|
|
100
|
+
const again = `Then re-run: \`${rerun ?? `turri regenerate ${error.moduleName}.plain`}\`, or say "resume" in the chat.`;
|
|
101
|
+
const needed = `Needed by: ${error.neededBy}${error.shownBy !== undefined ? ` (${error.shownBy})` : ""}.`;
|
|
102
|
+
const gap = error.sandbox;
|
|
103
|
+
if (gap?.access === "write") {
|
|
104
|
+
const what = gap.lock === true ? `the lock file \`${gap.paths[0]}\`, which the build takes as it runs` : `\`${gap.paths[0]}\``;
|
|
105
|
+
const home = homedir();
|
|
106
|
+
const inHome = gap.paths[0] === home || gap.paths[0].startsWith(`${home}/`);
|
|
107
|
+
return [
|
|
108
|
+
`**${error.moduleName} stopped: turri's sandbox kept its agents from writing ${what} - it is on this machine; nothing is missing, and nothing needs installing.**`,
|
|
109
|
+
needed,
|
|
110
|
+
"An agent's commands write only their module's folders, their own home and temporary folder, and the package caches turri " +
|
|
111
|
+
"points the build tools at - no setting lets them write anywhere else, and `toolchains:` lets them read a folder, not write it.",
|
|
112
|
+
inHome
|
|
113
|
+
? "To fix: it is in your home, not the agents' own - the tool found your home without `$HOME`, as a JVM does for `user.home`, " +
|
|
114
|
+
"or a setting of yours points it there. Have it use `$HOME` instead - for a JVM tool, `-Duser.home=$HOME` where the build " +
|
|
115
|
+
"starts its JVM - and the agents' own home takes it."
|
|
116
|
+
: `To fix: it is in a toolchain's own install, \`${gap.folder}\`, which the build writes into as it runs - and the agents may ` +
|
|
117
|
+
"not change a toolchain. Have the build keep it elsewhere if the tool has a setting for it, or install a copy of the " +
|
|
118
|
+
"toolchain that keeps its writes in its cache.",
|
|
119
|
+
again,
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
if (gap !== undefined) {
|
|
86
123
|
return [
|
|
87
|
-
`**${error.moduleName} stopped: turri's sandbox kept its agents from reading \`${
|
|
88
|
-
|
|
89
|
-
`To fix: let the agents read that toolchain - add its folder to \`toolchains:\` in config.yaml, keeping what is there: \`toolchains: [${
|
|
90
|
-
|
|
124
|
+
`**${error.moduleName} stopped: turri's sandbox kept its agents from reading \`${gap.paths[0]}\` - it is on this machine; nothing is missing.**`,
|
|
125
|
+
needed,
|
|
126
|
+
`To fix: let the agents read that toolchain - add its folder to \`toolchains:\` in config.turri.yaml, keeping what is there: \`toolchains: [${gap.folder}]\`. Nothing needs installing.`,
|
|
127
|
+
again,
|
|
91
128
|
].join("\n");
|
|
92
129
|
}
|
|
93
130
|
return [
|
|
94
131
|
`**${error.moduleName} stopped: the environment lacks ${error.missing}.**`,
|
|
95
132
|
`Needed by: ${error.neededBy}${error.shownBy !== undefined ? ` (${error.shownBy})` : ""}.`,
|
|
96
133
|
`To fix: ${error.fix ?? "provide it on this machine, then run it again."}`,
|
|
97
|
-
|
|
134
|
+
again,
|
|
98
135
|
].join("\n");
|
|
99
136
|
}
|
|
100
137
|
/** The name agents know the tool by - their wall text names it. */
|
|
@@ -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/docs/development.md
CHANGED
|
@@ -75,6 +75,7 @@ trust `turri-oss/turri`, the workflow `publish.yml` and the environment `npm`. R
|
|
|
75
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) |
|
|
76
76
|
| `src/regenerate/engine.ts` | choosing and resolving render models |
|
|
77
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 |
|
|
78
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 |
|
|
79
80
|
| `src/regenerate/debug-log.ts` | each render agent's whole conversation, always streamed to `.turri/debug/` |
|
|
80
81
|
| `genome/` | what turri knows and can do: `genome/skills/`, the skill library, and `genome/rules/`, the \*\*\*plain authoring rules |
|
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
|
|
|
@@ -575,10 +577,20 @@ not an install:
|
|
|
575
577
|
> **lunch stopped: turri's sandbox kept its agents from reading `…/mise/installs/node/24.16.0/lib/node_modules/npm/bin/npm-cli.js` - it is on this machine; nothing is missing.**
|
|
576
578
|
> To fix: let the agents read that toolchain - add its folder to `toolchains:` in config.turri.yaml, keeping what is there: `toolchains: [/Users/…/mise/installs/node/24.16.0]`. Nothing needs installing.
|
|
577
579
|
|
|
580
|
+
A write the sandbox refused is told apart from a read - a report naming a lock file (`*.lock`, `*.lck`, `lockfile`, even
|
|
581
|
+
one never made), or saying a write, a create or a lock not taken. No setting lets the agents write outside their
|
|
582
|
+
module's folders, their own home and temporary folder and the package caches, and `toolchains:` only lets them read; so
|
|
583
|
+
the stop names the lock and what fixes it instead. In your home - a tool that found it without `$HOME`, as a JVM does for
|
|
584
|
+
`user.home` - it is having the tool use `$HOME` (`-Duser.home=$HOME` where the build starts its JVM); in a toolchain's own
|
|
585
|
+
install, it is a setting of the tool's that keeps the write elsewhere, or a copy of the toolchain that keeps its writes in
|
|
586
|
+
its cache. The agents are told such a refusal is the sandbox's, to report at once - not to work around it for half an
|
|
587
|
+
hour.
|
|
588
|
+
|
|
578
589
|
**The conversation heals it.** Told of the stop, it never regenerates the module again before the cause is fixed - a pulse
|
|
579
590
|
stopped on its environment stops the same way again - and fixes what it can:
|
|
580
591
|
|
|
581
|
-
- **hidden by the sandbox**: it adds the folder to `toolchains:` itself, says so in a line, and resumes
|
|
592
|
+
- **hidden by the sandbox**: it adds the folder to `toolchains:` itself, says so in a line, and resumes - a refused
|
|
593
|
+
write, which no setting fixes, it fixes in the project's own files when it can, and otherwise tells you what to do;
|
|
582
594
|
- **a toolchain or a tool missing** - a JDK, Maven, Go, uv: it looks first for one installed off the PATH - Homebrew's opt
|
|
583
595
|
folders, `/Library/Java/JavaVirtualMachines`, mise, asdf, sdkman, nvm - then asks you, with the exact command for this
|
|
584
596
|
machine (`brew install openjdk@21`, `mise use -g java@21`), and on your yes runs it in its shell, checks the command is
|
package/docs/using.md
CHANGED
|
@@ -127,10 +127,13 @@ come waits for a free slot, and says so.
|
|
|
127
127
|
as GitHub names a hunk's function: each section is named once.
|
|
128
128
|
- **The side panel** (`ctrl+h`, as it was left - but on the pulse that last failed, stopped or did not pass, when one has
|
|
129
129
|
since it was last opened: the live panel that showed it is gone with the last pulse, and its announcement in the chat
|
|
130
|
-
says `ctrl+h shows this pulse`; **`/
|
|
130
|
+
says `ctrl+h shows this pulse`; **`/pulses`** on the pulses; not `ctrl+j`, which a terminal without the Kitty
|
|
131
131
|
keyboard protocol sends as a line feed, the same as `shift+enter`, and which a dialog takes for
|
|
132
132
|
`enter`; both stay pi's new-line keys. `ctrl+h` is the byte old terminals sent for backspace; a
|
|
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
|
|
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.
|
|
134
137
|
It opens on **regen**: the spec itself, in full, with how much of it the code has been regenerated from drawn
|
|
135
138
|
onto it.
|
|
136
139
|
|
|
@@ -214,7 +217,11 @@ come waits for a free slot, and says so.
|
|
|
214
217
|
closes when it moves to another section. What is open is kept while the panel is closed. What the agents
|
|
215
218
|
said and did is not shown - it is in each pulse's log (see [Agent logs](#agent-logs)).
|
|
216
219
|
|
|
217
|
-
`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,
|
|
218
225
|
whatever else happens: a question the chat model asks in the background (`ask_user`) waits in the chat,
|
|
219
226
|
unanswered - an `enter` in the panel never answers it - and the panel's top line says "the chat is waiting
|
|
220
227
|
for your answer". Back in the chat, the question is there, with the keys.
|