@turri-dev/cli 0.1.3-next.79e2ad0 → 0.1.3-next.b675516
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/config.js +3 -2
- package/dist/environment.js +19 -0
- package/dist/extension.js +134 -52
- package/dist/identity.js +1 -1
- package/dist/loop-cli.js +1 -1
- package/dist/loop-tools.js +1 -1
- package/dist/pi-launch.js +13 -4
- package/dist/picture-row.js +21 -0
- package/dist/regenerate/confine.js +1 -0
- package/dist/regenerate/conformance-agents.js +220 -11
- package/dist/regenerate/conformance.js +87 -33
- package/dist/regenerate/debugger.js +27 -3
- package/dist/regenerate/environment-error.js +63 -9
- package/dist/regenerate/panel-dock.js +86 -0
- package/dist/regenerate/prompt.js +3 -1
- package/dist/regenerate/regenerate.js +24 -6
- package/dist/regenerate/side-panel.js +14 -4
- package/dist/regenerate/stop-report.js +13 -0
- package/dist/regenerate/test-scripts.js +45 -9
- package/dist/regenerate/unit-review.js +11 -0
- package/dist/regenerate-cli.js +1 -1
- package/dist/run-report.js +2 -22
- package/docs/config.md +3 -2
- package/docs/development.md +7 -0
- package/docs/headless.md +2 -2
- package/docs/regenerating.md +66 -15
- package/docs/using.md +16 -5
- package/package.json +3 -3
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/config.js
CHANGED
|
@@ -189,8 +189,9 @@ export const ROLE_THINKING = {
|
|
|
189
189
|
diagnosis: "medium",
|
|
190
190
|
};
|
|
191
191
|
/** The roles on the conversation's side: without their own entry they take the chat's model, not `default`. */
|
|
192
|
-
// The release is not: it follows steps - a script, a start command - and runs on the coding model, thinking low.
|
|
193
|
-
|
|
192
|
+
// The release is not: it follows steps - a script, a start command - and runs on the coding model, thinking low. Nor
|
|
193
|
+
// is the diagnosis: a post-mortem of a job that failed, read by the person - it runs on the coding model, thinking medium.
|
|
194
|
+
const CHAT_SIDE = ["chat", "chooser", "planner"];
|
|
194
195
|
function isThinkingLevel(value) {
|
|
195
196
|
return typeof value === "string" && THINKING_LEVELS.includes(value);
|
|
196
197
|
}
|
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,11 +5,11 @@ 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";
|
|
12
|
-
import { AGENT_ROLES, CONFIG_FILE, configForSpec, hasProjectConfig, LEGACY_CONFIG_FILE, projectConfigFile, stackConfigFileIn, stackOf, stacksOf, withStack, modelSpec, projectModels, readProjectConfig, roleModel, splitModelSpec } from "./config.js";
|
|
12
|
+
import { AGENT_ROLES, CONFIG_FILE, configForSpec, hasProjectConfig, LEGACY_CONFIG_FILE, projectConfigFile, stackConfigFileIn, stackOf, stacksOf, withStack, modelOnly, modelSpec, projectModels, readProjectConfig, roleModel, splitModelSpec } from "./config.js";
|
|
13
13
|
import { skillRunDir } from "./regenerate/paths.js";
|
|
14
14
|
import { sweepOrphanedGroups } from "./regenerate/shell.js";
|
|
15
15
|
import { readScript, scriptAgainstMachine, stackOnMachine, toolchainsBlock } from "./stack.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, shade, shadeChat } 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";
|
|
@@ -183,7 +186,8 @@ export default function (pi) {
|
|
|
183
186
|
projectDir: cwd,
|
|
184
187
|
harnessDir: PACKAGE_ROOT,
|
|
185
188
|
jobLog,
|
|
186
|
-
model:
|
|
189
|
+
// The coding model the job ran on - its model alone: the diagnosis thinks as its role has it.
|
|
190
|
+
model: modelSpec(modelOnly(job.model ?? modelSpec(projectModels(readProjectConfig(cwd)).coding))),
|
|
187
191
|
});
|
|
188
192
|
return diagnosis !== undefined ? describeDiagnosis(diagnosis) : undefined;
|
|
189
193
|
},
|
|
@@ -1467,9 +1471,17 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1467
1471
|
return { content: [{ type: "text", text }], details: {} };
|
|
1468
1472
|
},
|
|
1469
1473
|
});
|
|
1470
|
-
// The side panel: the regeneration - and the pulses behind p - over the whole screen
|
|
1471
|
-
// key
|
|
1474
|
+
// The side panel: the regeneration - and the pulses behind p - over the whole screen, or beside the chat (s, from one to
|
|
1475
|
+
// the other). Its key (PANEL_KEY) opens it; the key again closes it and goes back to the chat, as esc does over the
|
|
1476
|
+
// screen - beside the chat, esc gives the chat its keys back and the panel stays, and the key gives them to the panel.
|
|
1477
|
+
// While the panel has the keys, every key is its own.
|
|
1472
1478
|
let panel;
|
|
1479
|
+
// Beside the chat or over it: as the person last left it, for the conversation.
|
|
1480
|
+
let panelDocked = false;
|
|
1481
|
+
// The terminal the panel is drawn on, for its width: set as it opens.
|
|
1482
|
+
let panelTerminal;
|
|
1483
|
+
/** The columns the panel takes beside the chat - 0 over the whole screen, or when the terminal is too narrow to share. */
|
|
1484
|
+
const panelBeside = () => (panelDocked && panelTerminal !== undefined ? dockedWidth(panelTerminal.columns) : 0);
|
|
1473
1485
|
// 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
1486
|
const jobsTree = createJobsTreeState();
|
|
1475
1487
|
const mapState = createSpecMapState();
|
|
@@ -1497,13 +1509,54 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1497
1509
|
needsLooking.delete(jobs);
|
|
1498
1510
|
if (panel !== undefined) {
|
|
1499
1511
|
panel.handle?.setHidden(false);
|
|
1512
|
+
panel.keys = true;
|
|
1500
1513
|
panel.handle?.focus();
|
|
1501
1514
|
return;
|
|
1502
1515
|
}
|
|
1503
1516
|
// Not awaited: the panel lives on while the jobs it shows run.
|
|
1504
1517
|
void ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
1505
1518
|
const close = () => done(undefined);
|
|
1519
|
+
panelTerminal = tui.terminal;
|
|
1506
1520
|
let component;
|
|
1521
|
+
// Beside the chat: the chat drawn narrower, on the left, and a click on either side giving it the keys - undone
|
|
1522
|
+
// when the panel goes over the screen, or closes.
|
|
1523
|
+
let undock;
|
|
1524
|
+
const layOut = () => {
|
|
1525
|
+
if (panelDocked && undock === undefined) {
|
|
1526
|
+
const widen = narrowChat(tui, panelBeside);
|
|
1527
|
+
const unwatch = clickToFocus(tui, () => tui.terminal.columns - panelBeside(), (side) => {
|
|
1528
|
+
if (panelBeside() === 0)
|
|
1529
|
+
return;
|
|
1530
|
+
if (side === "chat")
|
|
1531
|
+
toChat();
|
|
1532
|
+
else
|
|
1533
|
+
toPanel();
|
|
1534
|
+
});
|
|
1535
|
+
// The side without the keys in the shade: the chat while the panel has them.
|
|
1536
|
+
const unshade = shadeChat(tui, () => panelBeside() > 0 && entry.keys);
|
|
1537
|
+
undock = () => {
|
|
1538
|
+
unshade();
|
|
1539
|
+
unwatch();
|
|
1540
|
+
widen();
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
else if (!panelDocked && undock !== undefined) {
|
|
1544
|
+
undock();
|
|
1545
|
+
undock = undefined;
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
// Beside the chat, back to it: the chat has the keys, the panel stays.
|
|
1549
|
+
const toChat = () => {
|
|
1550
|
+
entry.keys = false;
|
|
1551
|
+
entry.handle?.unfocus();
|
|
1552
|
+
tui.requestRender();
|
|
1553
|
+
};
|
|
1554
|
+
const toPanel = () => {
|
|
1555
|
+
entry.keys = true;
|
|
1556
|
+
entry.handle?.focus();
|
|
1557
|
+
tui.requestRender();
|
|
1558
|
+
};
|
|
1559
|
+
const back = () => (panelBeside() > 0 ? toChat() : close());
|
|
1507
1560
|
// Opened, it reads the files again at once: they may have changed while it was closed.
|
|
1508
1561
|
specMap.refresh();
|
|
1509
1562
|
const view = createSidePanel({
|
|
@@ -1513,6 +1566,14 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1513
1566
|
mapState,
|
|
1514
1567
|
rows: () => tui.terminal.rows,
|
|
1515
1568
|
key: PANEL_KEY,
|
|
1569
|
+
dock: {
|
|
1570
|
+
docked: () => panelBeside() > 0,
|
|
1571
|
+
toggle: () => {
|
|
1572
|
+
panelDocked = panelBeside() === 0;
|
|
1573
|
+
layOut();
|
|
1574
|
+
tui.requestRender();
|
|
1575
|
+
},
|
|
1576
|
+
},
|
|
1516
1577
|
style: {
|
|
1517
1578
|
heading: (text) => theme.bold(theme.fg("accent", text)),
|
|
1518
1579
|
muted: (text) => theme.fg("muted", text),
|
|
@@ -1527,22 +1588,25 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1527
1588
|
// What conformance tests show does what its spec says: brand blue.
|
|
1528
1589
|
conforms: brandBlue(theme),
|
|
1529
1590
|
},
|
|
1530
|
-
back
|
|
1591
|
+
back,
|
|
1531
1592
|
close,
|
|
1532
1593
|
// A module that needs the person: the chat takes it up with them, in their words - what went wrong, and what now.
|
|
1533
1594
|
discuss: (page) => {
|
|
1534
|
-
|
|
1595
|
+
back();
|
|
1535
1596
|
const why = page.codebase?.stuck?.why;
|
|
1536
1597
|
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
1598
|
},
|
|
1538
1599
|
// Something took the keys' focus while the panel showed - a question in the chat - and waits there.
|
|
1539
1600
|
waiting: () => {
|
|
1601
|
+
if (!entry.keys)
|
|
1602
|
+
return false;
|
|
1540
1603
|
const focused = tui.getFocusedComponent?.() ?? null;
|
|
1541
1604
|
return focused !== null && focused !== component;
|
|
1542
1605
|
},
|
|
1543
1606
|
});
|
|
1544
|
-
const entry = { view, close };
|
|
1607
|
+
const entry = { view, close, keys: true };
|
|
1545
1608
|
panel = entry;
|
|
1609
|
+
layOut();
|
|
1546
1610
|
// Every key goes to the panel while it shows, whatever has the focus: a question the chat model asks in the
|
|
1547
1611
|
// background takes the focus, but no key reaches it - an enter never answers it unseen. It waits in the chat
|
|
1548
1612
|
// until the panel closes, and has the keys then. Mouse events are the screen's, before this sees them.
|
|
@@ -1564,6 +1628,22 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1564
1628
|
}
|
|
1565
1629
|
if (!state)
|
|
1566
1630
|
return undefined;
|
|
1631
|
+
// Beside the chat, ⌥⌘← and ⌥⌘→ go to the side they point at, whichever has the keys - on a terminal that tells
|
|
1632
|
+
// turri of ⌘ (the Kitty keyboard protocol). Its release goes nowhere.
|
|
1633
|
+
if (panelBeside() > 0 && (matchesKey(data, SIDE_KEYS.chat) || matchesKey(data, SIDE_KEYS.panel))) {
|
|
1634
|
+
if (!isKeyRelease(data)) {
|
|
1635
|
+
if (matchesKey(data, SIDE_KEYS.chat))
|
|
1636
|
+
toChat();
|
|
1637
|
+
else
|
|
1638
|
+
toPanel();
|
|
1639
|
+
}
|
|
1640
|
+
return { consume: true };
|
|
1641
|
+
}
|
|
1642
|
+
// Beside the chat, with the chat's keys: they are the chat's - PANEL_KEY among them, for the shortcut that gives
|
|
1643
|
+
// them to the panel. A click on the panel gives it the focus, and the keys with it.
|
|
1644
|
+
if (!entry.keys && entry.handle?.isFocused() !== true)
|
|
1645
|
+
return undefined;
|
|
1646
|
+
entry.keys = true;
|
|
1567
1647
|
// A terminal on the Kitty keyboard protocol reports a key's release, and listeners are given it: the release of
|
|
1568
1648
|
// the ctrl+h that opened the panel would close it again at once, and every arrow would move twice. Only
|
|
1569
1649
|
// presses count - and ctrl+h held down, repeating, does not open and close it over and over.
|
|
@@ -1590,7 +1670,13 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1590
1670
|
}, WATCH_REDRAW_MS);
|
|
1591
1671
|
timer.unref?.();
|
|
1592
1672
|
component = {
|
|
1593
|
-
|
|
1673
|
+
// Beside the chat, a rule down its left edge between them - and the panel in the shade while the chat has the keys.
|
|
1674
|
+
render: (width) => {
|
|
1675
|
+
if (panelBeside() === 0)
|
|
1676
|
+
return view.render(width);
|
|
1677
|
+
const lines = view.render(width - 2).map((line) => `${theme.fg("borderMuted", "│")} ${line}`);
|
|
1678
|
+
return entry.keys ? lines : lines.map(shade);
|
|
1679
|
+
},
|
|
1594
1680
|
// Reached only if the listener above is gone: the same, so the panel never passes a key on.
|
|
1595
1681
|
handleInput: (data) => {
|
|
1596
1682
|
if (isKeyRepeat(data) && matchesKey(data, PANEL_KEY))
|
|
@@ -1606,6 +1692,7 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1606
1692
|
dispose: () => {
|
|
1607
1693
|
unlisten();
|
|
1608
1694
|
clearInterval(timer);
|
|
1695
|
+
undock?.();
|
|
1609
1696
|
if (panel === entry)
|
|
1610
1697
|
panel = undefined;
|
|
1611
1698
|
},
|
|
@@ -1613,34 +1700,36 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1613
1700
|
return component;
|
|
1614
1701
|
}, {
|
|
1615
1702
|
overlay: true,
|
|
1616
|
-
// The whole screen
|
|
1617
|
-
overlayOptions: {
|
|
1703
|
+
// The whole screen, or the columns on the right beside the chat - read at each frame, so it follows the terminal.
|
|
1704
|
+
overlayOptions: {
|
|
1705
|
+
get width() {
|
|
1706
|
+
return panelBeside() > 0 ? panelBeside() : "100%";
|
|
1707
|
+
},
|
|
1708
|
+
maxHeight: "100%",
|
|
1709
|
+
anchor: "top-right",
|
|
1710
|
+
margin: 0,
|
|
1711
|
+
},
|
|
1618
1712
|
onHandle: (handle) => {
|
|
1619
1713
|
if (panel !== undefined)
|
|
1620
1714
|
panel.handle = handle;
|
|
1621
1715
|
},
|
|
1622
1716
|
});
|
|
1623
1717
|
};
|
|
1624
|
-
//
|
|
1625
|
-
//
|
|
1626
|
-
// in each pulse's log.
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
return;
|
|
1636
|
-
openPanel(ctx, view);
|
|
1637
|
-
},
|
|
1638
|
-
});
|
|
1639
|
-
}
|
|
1718
|
+
// /pulses opens the side panel on every pulse, regenerating, planned and finished, and what each regenerates; regen -
|
|
1719
|
+
// the spec, with how far the code has been regenerated into it - is PANEL_KEY's, no command of its own beside
|
|
1720
|
+
// /regenerate. What the agents did is in each pulse's log.
|
|
1721
|
+
pi.registerCommand("pulses", {
|
|
1722
|
+
description: `The pulses, in the side panel: every pulse, regenerating, planned and finished, and what each regenerates. ${PANEL_KEY} opens the panel too.`,
|
|
1723
|
+
handler: async (_args, ctx) => {
|
|
1724
|
+
if (!ctx.hasUI)
|
|
1725
|
+
return;
|
|
1726
|
+
openPanel(ctx, "pulses");
|
|
1727
|
+
},
|
|
1728
|
+
});
|
|
1640
1729
|
panelOpeners.set(jobs, (ctx) => openPanel(ctx));
|
|
1641
1730
|
questionPeek = {
|
|
1642
1731
|
open: (ctx) => openPanel(ctx),
|
|
1643
|
-
shown: () => panel !== undefined,
|
|
1732
|
+
shown: () => panel !== undefined && panel.keys,
|
|
1644
1733
|
};
|
|
1645
1734
|
pi.registerShortcut(PANEL_KEY, {
|
|
1646
1735
|
description: "Peek at regen and the pulses - a question waiting in the chat waits for you - or go back to it",
|
|
@@ -1711,8 +1800,12 @@ export const JOB_FINISHED_REACTION = [
|
|
|
1711
1800
|
"A pulse that stopped on its environment - \"stopped: turri's sandbox kept its agents from…\" or \"stopped: the",
|
|
1712
1801
|
"environment lacks…\" - stops the same way again until that is fixed: never regenerate it before. Heal it, then regenerate the",
|
|
1713
1802
|
"module to resume:",
|
|
1714
|
-
"- turri's sandbox kept the agents from a folder on this machine: nothing is missing. Add the folder its report
|
|
1715
|
-
" `toolchains:` in config.turri.yaml - a list, keeping what is there - say so in a line, and resume.",
|
|
1803
|
+
"- turri's sandbox kept the agents from reading a folder on this machine: nothing is missing. Add the folder its report",
|
|
1804
|
+
" names to `toolchains:` in config.turri.yaml - a list, keeping what is there - say so in a line, and resume.",
|
|
1805
|
+
"- turri's sandbox kept the agents from writing a file - a lock a build tool takes, say: no setting lets them, and",
|
|
1806
|
+
" `toolchains:` does not - never add it there, and never resume before the fix. Do what the report says if it is the",
|
|
1807
|
+
" project's to change - a tool told to use `$HOME` in the build's own files - say so, and resume; otherwise tell the",
|
|
1808
|
+
" person, in a line or two, what the report says and that \"resume\" picks it up once it is done.",
|
|
1716
1809
|
"- the machine lacks a toolchain or a tool - a JDK, Maven, Gradle, Go, uv, Node, a system library: first find out, in your",
|
|
1717
1810
|
" shell, whether it is installed somewhere off the PATH (`command -v`, Homebrew's opt folders, /Library/Java/",
|
|
1718
1811
|
" JavaVirtualMachines, a version manager the person uses - mise, asdf, sdkman, nvm). Then ask the person with ask_user",
|
|
@@ -1724,6 +1817,8 @@ export const JOB_FINISHED_REACTION = [
|
|
|
1724
1817
|
" to do, and that \"resume\" picks it up once it is in.",
|
|
1725
1818
|
"- a credential `agent-env:` names that is not there: never make one up - tell the person where it is read from.",
|
|
1726
1819
|
].join("\n");
|
|
1820
|
+
/** The keys that go to the chat and to the panel, side by side: ⌥⌘← and ⌥⌘→. */
|
|
1821
|
+
const SIDE_KEYS = { chat: "alt+super+left", panel: "alt+super+right" };
|
|
1727
1822
|
/** Opens the side panel, and goes back to it from the conversation. */
|
|
1728
1823
|
/** The modules in this project that have a history, by name. */
|
|
1729
1824
|
function modulesWithHistory(cwd) {
|
|
@@ -2158,7 +2253,8 @@ function registerBranding(pi, ruleCount, skills) {
|
|
|
2158
2253
|
if (data === undefined)
|
|
2159
2254
|
return undefined;
|
|
2160
2255
|
const lines = [
|
|
2161
|
-
`${theme.fg("accent", "turri")} ${theme.fg("muted", `v${data.version}`)}
|
|
2256
|
+
`${theme.fg("accent", "turri")} ${theme.fg("muted", `v${data.version}`)}` +
|
|
2257
|
+
(data.commit !== undefined ? theme.fg("dim", ` from a clone, ${data.commit}${data.changed ? " with changes" : ""}`) : ""),
|
|
2162
2258
|
theme.fg("text", "regenerative software - the code is regenerated from the spec"),
|
|
2163
2259
|
theme.fg("muted", `${data.rules} rules · ${data.skills} skill${data.skills === 1 ? "" : "s"}` +
|
|
2164
2260
|
(data.projectSkills !== undefined && data.projectSkills > 0 ? ` (${data.projectSkills} the project's)` : "") +
|
|
@@ -2180,8 +2276,11 @@ function registerBranding(pi, ruleCount, skills) {
|
|
|
2180
2276
|
if (ctx.hasUI)
|
|
2181
2277
|
animateJellyfish(ctx);
|
|
2182
2278
|
try {
|
|
2279
|
+
const identity = turriIdentity();
|
|
2183
2280
|
pi.appendEntry(BANNER_ENTRY, {
|
|
2184
|
-
version:
|
|
2281
|
+
version: identity.version ?? "0.0.0",
|
|
2282
|
+
commit: identity.git_commit?.slice(0, 7),
|
|
2283
|
+
changed: identity.git_dirty === true,
|
|
2185
2284
|
rules: ruleCount,
|
|
2186
2285
|
skills: skills.skills.length,
|
|
2187
2286
|
projectSkills: skills.skills.filter((skill) => skill.origin !== "turri").length,
|
|
@@ -2234,8 +2333,6 @@ function animateJellyfish(ctx) {
|
|
|
2234
2333
|
}, { placement: "belowEditor" });
|
|
2235
2334
|
}
|
|
2236
2335
|
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
2336
|
/** The columns the banner's picture takes from the left edge, while it shows one; 0 when it does not. */
|
|
2240
2337
|
let pictureRight = 0;
|
|
2241
2338
|
/**
|
|
@@ -2250,14 +2347,7 @@ function drawOverlaysBesidePicture(tui) {
|
|
|
2250
2347
|
return;
|
|
2251
2348
|
target.turriPicture = true;
|
|
2252
2349
|
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
|
-
};
|
|
2350
|
+
target.compositeLineAt = (base, overlay, col, width, total) => overPictureRow(base, overlay, col, width, total, pictureRight) ?? original(base, overlay, col, width, total);
|
|
2261
2351
|
}
|
|
2262
2352
|
/** Columns between the jellyfish and the lines beside it. */
|
|
2263
2353
|
const JELLYFISH_GAP = 3;
|
|
@@ -2289,7 +2379,8 @@ class Banner {
|
|
|
2289
2379
|
const art = this.art;
|
|
2290
2380
|
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
2381
|
const left = placed !== undefined ? 1 + placed.columns + JELLYFISH_GAP : 1;
|
|
2292
|
-
|
|
2382
|
+
// Wrapped in the columns beside the picture, never cut short - the chat beside the docked panel is narrow.
|
|
2383
|
+
const text = this.lines.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width - 1 - left)));
|
|
2293
2384
|
const height = Math.max(text.length, placed?.rows ?? 0);
|
|
2294
2385
|
const above = Math.floor((height - text.length) / 2);
|
|
2295
2386
|
const drawn = placed?.kind === "text" ? placed.frames[jellyfishFrame % placed.frames.length] : undefined;
|
|
@@ -2361,12 +2452,3 @@ function jellyfishArt(appearance, ink) {
|
|
|
2361
2452
|
return undefined;
|
|
2362
2453
|
}
|
|
2363
2454
|
}
|
|
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/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-cli.js
CHANGED
|
@@ -69,7 +69,7 @@ async function runLoopReporting(options) {
|
|
|
69
69
|
const registry = createJobRegistry({
|
|
70
70
|
retries: 1,
|
|
71
71
|
diagnose: async (job, failures, jobLog) => {
|
|
72
|
-
const diagnosis = await diagnoseFailure({ job, failures, projectDir, harnessDir: PACKAGE_ROOT, jobLog, model: modelSpec(
|
|
72
|
+
const diagnosis = await diagnoseFailure({ job, failures, projectDir, harnessDir: PACKAGE_ROOT, jobLog, model: modelSpec(modelOnly(job.model ?? model)) });
|
|
73
73
|
return diagnosis !== undefined ? describeDiagnosis(diagnosis) : undefined;
|
|
74
74
|
},
|
|
75
75
|
onChange: (job) => {
|
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];
|
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
|
+
}
|
|
@@ -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.");
|