@turri-dev/cli 0.1.4-next.afc43a8 → 0.1.4-next.d6e59a8
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/.pi/themes/turri-dark.json +67 -66
- package/.pi/themes/turri-light.json +71 -73
- package/README.md +41 -127
- package/assets/jellyfish/dark-00.png +0 -0
- package/assets/jellyfish/dark-01.png +0 -0
- package/assets/jellyfish/dark-02.png +0 -0
- package/assets/jellyfish/dark-03.png +0 -0
- package/assets/jellyfish/dark-04.png +0 -0
- package/assets/jellyfish/dark-05.png +0 -0
- package/assets/jellyfish/dark-06.png +0 -0
- package/assets/jellyfish/dark-07.png +0 -0
- package/assets/jellyfish/dark-08.png +0 -0
- package/assets/jellyfish/dark-09.png +0 -0
- package/assets/jellyfish/dark-10.png +0 -0
- package/assets/jellyfish/dark-11.png +0 -0
- package/assets/jellyfish/light-00.png +0 -0
- package/assets/jellyfish/light-01.png +0 -0
- package/assets/jellyfish/light-02.png +0 -0
- package/assets/jellyfish/light-03.png +0 -0
- package/assets/jellyfish/light-04.png +0 -0
- package/assets/jellyfish/light-05.png +0 -0
- package/assets/jellyfish/light-06.png +0 -0
- package/assets/jellyfish/light-07.png +0 -0
- package/assets/jellyfish/light-08.png +0 -0
- package/assets/jellyfish/light-09.png +0 -0
- package/assets/jellyfish/light-10.png +0 -0
- package/assets/jellyfish/light-11.png +0 -0
- package/dist/brand.js +7 -7
- package/dist/cultivate-cli.js +16 -23
- package/dist/extension.js +33 -59
- package/dist/jellyfish.js +130 -0
- package/dist/loop-cli.js +8 -8
- package/dist/module-report.js +5 -4
- package/dist/regenerate/plain-palette.js +52 -37
- package/dist/regenerate/side-panel.js +1 -1
- package/dist/regenerate/spec-map-view.js +9 -5
- package/dist/release-cli.js +3 -8
- package/dist/validate-cli.js +3 -8
- package/docs/README.md +5 -12
- package/docs/authoring-plain.md +37 -121
- package/docs/config.md +50 -176
- package/docs/deploy.md +21 -80
- package/docs/development.md +13 -41
- package/docs/headless.md +30 -136
- package/docs/images/how-turri-works.svg +29 -31
- package/docs/images/turri-banner.png +0 -0
- package/docs/images/turri-screenshot.png +0 -0
- package/docs/installation.md +13 -40
- package/docs/parser.md +15 -43
- package/docs/regenerating.md +231 -964
- package/docs/sandbox.md +38 -152
- package/docs/skills-and-gates.md +50 -173
- package/docs/theming.md +45 -46
- package/docs/using.md +66 -356
- package/genome/rules/bullet-continuation.md +5 -13
- package/genome/rules/concise-specs-examples.md +2 -4
- package/genome/rules/concise-specs-integration-examples.md +2 -6
- package/genome/rules/concise-specs.md +1 -2
- package/genome/rules/integration-embedded-testing.md +1 -4
- package/genome/rules/integrations.md +1 -2
- package/genome/rules/project-model.md +14 -37
- package/genome/rules/stdlib.md +14 -49
- package/genome/rules/turri-regeneration.md +35 -23
- package/genome/skills/add-implementation-requirement/SKILL.md +1 -2
- package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +3 -8
- package/genome/skills/brood/SKILL.md +37 -127
- package/genome/skills/brood/references/intent.md +4 -12
- package/genome/skills/brood/references/product.md +24 -76
- package/genome/skills/brood/references/stack.md +7 -29
- package/genome/skills/brood/references/testing.md +11 -42
- package/genome/skills/bud/SKILL.md +32 -109
- package/genome/skills/bud/references/intent.md +4 -11
- package/genome/skills/implement-conformance-testing-script/SKILL.md +3 -6
- package/genome/skills/implement-unit-testing-script/SKILL.md +1 -2
- package/genome/skills/mature/SKILL.md +21 -69
- package/genome/skills/spawn/SKILL.md +28 -113
- package/package.json +1 -1
- package/assets/jellyfish/dark-12.png +0 -0
- package/assets/jellyfish/dark-13.png +0 -0
- package/assets/jellyfish/dark-14.png +0 -0
- package/assets/jellyfish/fallback.txt +0 -252
- package/assets/jellyfish/light-12.png +0 -0
- package/assets/jellyfish/light-13.png +0 -0
- package/assets/jellyfish/light-14.png +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/brand.js
CHANGED
|
@@ -2,8 +2,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
/**
|
|
5
|
-
* turri's colours for what its command line prints - the
|
|
6
|
-
*
|
|
5
|
+
* turri's colours for what its command line prints - the design system's, as its themes use them (.pi/themes): on a
|
|
6
|
+
* dark terminal neon and pink, on a light one blue whale and eggplant. Nothing is coloured when the output is not a terminal, or NO_COLOR is set;
|
|
7
7
|
* a terminal without 24-bit colour gets the nearest of its sixteen.
|
|
8
8
|
*/
|
|
9
9
|
/** Whether the terminal is light: COLORFGBG says so (a light background, 7 or 15), or pi's theme is a light one. */
|
|
@@ -35,15 +35,15 @@ function paint(stream = process.stdout) {
|
|
|
35
35
|
};
|
|
36
36
|
return {
|
|
37
37
|
/** turri's accent: what to look at - a heading, a choice's number, the command to run. */
|
|
38
|
-
accent: light ? ink("#
|
|
38
|
+
accent: light ? ink("#1e3442", 34) : ink("#5ce1e6", 96),
|
|
39
39
|
/** Done, and what was written. */
|
|
40
|
-
success: light ? ink("#
|
|
40
|
+
success: light ? ink("#732553", 35) : ink("#ff5c8d", 95),
|
|
41
41
|
/** What is wrong: what stops turri. */
|
|
42
|
-
error:
|
|
42
|
+
error: ink("#ff7a3d", 33),
|
|
43
43
|
/** What works, with limits. */
|
|
44
|
-
warning: ink("#
|
|
44
|
+
warning: ink("#ff7a3d", 33),
|
|
45
45
|
/** What goes with it, and can be skimmed. */
|
|
46
|
-
muted: ink("#
|
|
46
|
+
muted: light ? ink("#3e5a6b", 90) : ink("#85a3b2", 37),
|
|
47
47
|
bold: (text) => (on ? `\x1b[1m${text}\x1b[22m` : text),
|
|
48
48
|
};
|
|
49
49
|
}
|
package/dist/cultivate-cli.js
CHANGED
|
@@ -7,40 +7,33 @@ import { skillRunDir } from "./regenerate/paths.js";
|
|
|
7
7
|
import { loadSkill } from "./skill.js";
|
|
8
8
|
import { discoverSkills } from "./skills.js";
|
|
9
9
|
import { PACKAGE_ROOT } from "./environment.js";
|
|
10
|
-
|
|
11
|
-
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
12
|
-
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
13
|
-
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
14
|
-
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
15
|
-
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
16
|
-
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
17
|
-
};
|
|
10
|
+
import { c } from "./module-report.js";
|
|
18
11
|
const reporter = {
|
|
19
|
-
step: (name, attempt) => console.log(`\n${c.bold(c.
|
|
12
|
+
step: (name, attempt) => console.log(`\n${c.bold(c.accent(`> step ${name}`))} ${c.dim(`attempt ${attempt}`)}`),
|
|
20
13
|
artifact: (path, body) => {
|
|
21
14
|
console.log(c.dim(` ${path}:`));
|
|
22
15
|
for (const line of body.split("\n"))
|
|
23
16
|
console.log(c.dim(` | ${line}`));
|
|
24
17
|
},
|
|
25
18
|
gate: (name, judges) => console.log(judges === 0
|
|
26
|
-
? `${c.bold(c.
|
|
27
|
-
: `${c.bold(c.
|
|
19
|
+
? `${c.bold(c.warning(`> gate ${name}`))} ${c.dim("no artifact - failing without spending judges")}`
|
|
20
|
+
: `${c.bold(c.warning(`> gate ${name}`))} ${c.dim(`${judges} independent judges`)}`),
|
|
28
21
|
livePass: (step, scope, findings) => console.log(findings === 0
|
|
29
22
|
? c.dim(` review ${step}: ${scope} - clean`)
|
|
30
|
-
: ` ${c.
|
|
23
|
+
: ` ${c.warning(`review ${step}`)}: ${scope} ${c.dim(`- ${findings} to fix`)}`),
|
|
31
24
|
liveReview: (name, passes, findings) => console.log(findings === 0
|
|
32
|
-
? `${c.bold(c.
|
|
33
|
-
: `${c.bold(c.
|
|
25
|
+
? `${c.bold(c.warning(`> gate ${name}`))} ${c.dim(`reviewer read along in ${passes} passes, nothing outstanding`)}`
|
|
26
|
+
: `${c.bold(c.warning(`> gate ${name}`))} ${c.dim(`${findings} unresolved from the reviewer - failing without spending judges`)}`),
|
|
34
27
|
judge: (index, model, verdict, violations) => {
|
|
35
|
-
const mark = verdict === "pass" ? c.
|
|
28
|
+
const mark = verdict === "pass" ? c.success("pass") : c.error(verdict);
|
|
36
29
|
console.log(` judge ${index + 1} ${c.dim(`(${model})`)}: ${mark}${violations ? c.dim(` (${violations})`) : ""}`);
|
|
37
30
|
},
|
|
38
31
|
outcome: (name, decision, critique) => {
|
|
39
32
|
const colour = {
|
|
40
|
-
advance: c.
|
|
41
|
-
retry: c.
|
|
42
|
-
escalate: c.
|
|
43
|
-
abort: c.
|
|
33
|
+
advance: c.success,
|
|
34
|
+
retry: c.warning,
|
|
35
|
+
escalate: c.warning,
|
|
36
|
+
abort: c.error,
|
|
44
37
|
};
|
|
45
38
|
console.log(` ${c.bold(colour[decision](decision))} ${c.dim(`<- gate ${name}`)}`);
|
|
46
39
|
if (critique)
|
|
@@ -78,7 +71,7 @@ async function main() {
|
|
|
78
71
|
select: async (title, options, context) => {
|
|
79
72
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
80
73
|
const list = options.map((option, i) => ` ${c.bold(String(i + 1))}. ${option}`).join("\n");
|
|
81
|
-
const reply = (await rl.question(`\n${context !== undefined ? `${context}\n\n` : ""}${c.bold(c.
|
|
74
|
+
const reply = (await rl.question(`\n${context !== undefined ? `${context}\n\n` : ""}${c.bold(c.accent("? "))}${title}\n${list}\n> `)).trim();
|
|
82
75
|
rl.close();
|
|
83
76
|
const index = Number.parseInt(reply, 10);
|
|
84
77
|
if (Number.isInteger(index) && index >= 1 && index <= options.length)
|
|
@@ -87,13 +80,13 @@ async function main() {
|
|
|
87
80
|
},
|
|
88
81
|
input: async (title, context) => {
|
|
89
82
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
90
|
-
const reply = (await rl.question(`\n${context !== undefined ? `${context}\n\n` : ""}${c.bold(c.
|
|
83
|
+
const reply = (await rl.question(`\n${context !== undefined ? `${context}\n\n` : ""}${c.bold(c.accent("? "))}${title}\n> `)).trim();
|
|
91
84
|
rl.close();
|
|
92
85
|
return reply === "" ? undefined : reply;
|
|
93
86
|
},
|
|
94
87
|
}),
|
|
95
88
|
escalate: async ({ question, critique }) => {
|
|
96
|
-
console.log(`\n${c.bold(c.
|
|
89
|
+
console.log(`\n${c.bold(c.warning("! human review required"))}`);
|
|
97
90
|
console.log(c.dim(critique));
|
|
98
91
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
99
92
|
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
@@ -101,7 +94,7 @@ async function main() {
|
|
|
101
94
|
return answer === "y";
|
|
102
95
|
},
|
|
103
96
|
});
|
|
104
|
-
console.log(`\n${result.ok ? c.
|
|
97
|
+
console.log(`\n${result.ok ? c.success(c.bold("run complete")) : c.error(c.bold("run aborted at a gate"))} ${c.dim(`artifacts in ${relative(cwd, skillRunDir(cwd, skill.name))}`)}`);
|
|
105
98
|
process.exit(result.ok ? 0 : 1);
|
|
106
99
|
}
|
|
107
100
|
main().catch((err) => {
|
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, getCapabilities, getPngDimensions, isKeyRelease, isKeyRepeat, matchesKey, renderImage, Text,
|
|
8
|
+
import { allocateImageId, Box, getCapabilities, getPngDimensions, isKeyRelease, isKeyRepeat, matchesKey, renderImage, Text, 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";
|
|
@@ -29,7 +29,8 @@ import { describeChoices, describeResolution, modelFor, resolveModel } from "./r
|
|
|
29
29
|
import { checkPlainFile, checkPlainTargets, formatCheckResults } from "./plain/check.js";
|
|
30
30
|
import { whyFailed } from "./regenerate/diagnose-job.js";
|
|
31
31
|
import { createWorkLoop, describeModules, isUpToDate, previewJobs } from "./regenerate/loop.js";
|
|
32
|
-
import {
|
|
32
|
+
import { brandNeon, brandPink, plainPaint } from "./regenerate/plain-palette.js";
|
|
33
|
+
import { halfBlocks, JELLYFISH_FRAME_MS, JELLYFISH_TEXT_FRAMES } from "./jellyfish.js";
|
|
33
34
|
import { createJobPanel, didNotPass, keyPressed, PANEL_KEY, spinner } from "./regenerate/job-panel.js";
|
|
34
35
|
import { clickToFocus, dockedWidth, narrowChat, shade, shadeChat } from "./regenerate/panel-dock.js";
|
|
35
36
|
import { overPictureRow, PICTURE_AT } from "./picture-row.js";
|
|
@@ -47,7 +48,7 @@ import { inheritProviders, inheritThinkingLevel } from "./pi.js";
|
|
|
47
48
|
import { beginCapture, captureConsole, describeCaptured, dropNoise } from "./quiet.js";
|
|
48
49
|
import { loadRules, renderRulesBlock } from "./rules.js";
|
|
49
50
|
import { registerTurriConfig } from "./config-command.js";
|
|
50
|
-
import { describeStdlib
|
|
51
|
+
import { describeStdlib } from "./stdlib.js";
|
|
51
52
|
import { peekKeyListener, runAsk } from "./ask.js";
|
|
52
53
|
import { QuestionDialog } from "./question-dialog.js";
|
|
53
54
|
import { registerStacks } from "./stacks.js";
|
|
@@ -370,7 +371,7 @@ export default function (pi) {
|
|
|
370
371
|
conversationModels = ctx.modelRegistry;
|
|
371
372
|
});
|
|
372
373
|
inheritProviders(() => conversationModels);
|
|
373
|
-
registerBranding(pi,
|
|
374
|
+
registerBranding(pi, discoveredSkills);
|
|
374
375
|
registerInterviewLog(pi);
|
|
375
376
|
registerJobTools(pi, jobs, cwd);
|
|
376
377
|
registerCheckPlain(pi, cwd);
|
|
@@ -1517,7 +1518,8 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1517
1518
|
});
|
|
1518
1519
|
// The side panel: the regeneration - and the pulses behind p - over the whole screen, or beside the chat (s, from one to
|
|
1519
1520
|
// the other). Its key (PANEL_KEY) opens it; the key again closes it and goes back to the chat, as esc does over the
|
|
1520
|
-
// screen - beside the chat,
|
|
1521
|
+
// screen - beside the chat, the key switches sides, giving the keys to the chat or the panel, as esc gives them to the
|
|
1522
|
+
// chat, the panel staying; ctrl+c closes it.
|
|
1521
1523
|
// While the panel has the keys, every key is its own.
|
|
1522
1524
|
let panel;
|
|
1523
1525
|
// Beside the chat or over it: as the person last left it, for the conversation.
|
|
@@ -1645,8 +1647,8 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1645
1647
|
removed: (text) => theme.fg("toolDiffRemoved", text),
|
|
1646
1648
|
// A spec diff's ***plain highlighted as plyn highlights it, and its added and removed lines filled.
|
|
1647
1649
|
diff: plainPaint(theme),
|
|
1648
|
-
// What conformance tests show does what its spec says:
|
|
1649
|
-
conforms:
|
|
1650
|
+
// What conformance tests show does what its spec says: neon.
|
|
1651
|
+
conforms: brandNeon(theme),
|
|
1650
1652
|
},
|
|
1651
1653
|
back,
|
|
1652
1654
|
close,
|
|
@@ -1688,17 +1690,6 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1688
1690
|
}
|
|
1689
1691
|
if (!state)
|
|
1690
1692
|
return undefined;
|
|
1691
|
-
// Beside the chat, ⌥⌘← and ⌥⌘→ go to the side they point at, whichever has the keys - on a terminal that tells
|
|
1692
|
-
// turri of ⌘ (the Kitty keyboard protocol). Its release goes nowhere.
|
|
1693
|
-
if (panelBeside() > 0 && (matchesKey(data, SIDE_KEYS.chat) || matchesKey(data, SIDE_KEYS.panel))) {
|
|
1694
|
-
if (!isKeyRelease(data)) {
|
|
1695
|
-
if (matchesKey(data, SIDE_KEYS.chat))
|
|
1696
|
-
toChat();
|
|
1697
|
-
else
|
|
1698
|
-
toPanel();
|
|
1699
|
-
}
|
|
1700
|
-
return { consume: true };
|
|
1701
|
-
}
|
|
1702
1693
|
// Beside the chat, with the chat's keys: they are the chat's - PANEL_KEY among them, for the shortcut that gives
|
|
1703
1694
|
// them to the panel. A click on the panel gives it the focus, and the keys with it.
|
|
1704
1695
|
if (!entry.keys && entry.handle?.isFocused() !== true)
|
|
@@ -1709,9 +1700,13 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1709
1700
|
// presses count - and ctrl+h held down, repeating, does not open and close it over and over.
|
|
1710
1701
|
if (isKeyRelease(data) || (isKeyRepeat(data) && matchesKey(data, PANEL_KEY)))
|
|
1711
1702
|
return { consume: true };
|
|
1712
|
-
//
|
|
1713
|
-
|
|
1703
|
+
// PANEL_KEY goes back: beside the chat, to the chat, the panel staying - the switch between the sides, as it is
|
|
1704
|
+
// from the chat to the panel - and over the screen, closing it. ctrl+c closes it either way: whatever else goes
|
|
1705
|
+
// wrong, a key always leads back to the chat.
|
|
1706
|
+
if (matchesKey(data, "ctrl+c"))
|
|
1714
1707
|
close();
|
|
1708
|
+
else if (matchesKey(data, PANEL_KEY))
|
|
1709
|
+
back();
|
|
1715
1710
|
else {
|
|
1716
1711
|
try {
|
|
1717
1712
|
view.handleInput(data);
|
|
@@ -1741,8 +1736,10 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1741
1736
|
handleInput: (data) => {
|
|
1742
1737
|
if (isKeyRepeat(data) && matchesKey(data, PANEL_KEY))
|
|
1743
1738
|
return;
|
|
1744
|
-
if (matchesKey(data,
|
|
1739
|
+
if (matchesKey(data, "ctrl+c"))
|
|
1745
1740
|
close();
|
|
1741
|
+
else if (matchesKey(data, PANEL_KEY))
|
|
1742
|
+
back();
|
|
1746
1743
|
else
|
|
1747
1744
|
view.handleInput(data);
|
|
1748
1745
|
tui.requestRender();
|
|
@@ -1877,8 +1874,6 @@ export const JOB_FINISHED_REACTION = [
|
|
|
1877
1874
|
" to do, and that \"resume\" picks it up once it is in.",
|
|
1878
1875
|
"- a credential `agent-env:` names that is not there: never make one up - tell the person where it is read from.",
|
|
1879
1876
|
].join("\n");
|
|
1880
|
-
/** The keys that go to the chat and to the panel, side by side: ⌥⌘← and ⌥⌘→. */
|
|
1881
|
-
const SIDE_KEYS = { chat: "alt+super+left", panel: "alt+super+right" };
|
|
1882
1877
|
/** Opens the side panel, and goes back to it from the conversation. */
|
|
1883
1878
|
/** The modules in this project that have a history, by name. */
|
|
1884
1879
|
function modulesWithHistory(cwd) {
|
|
@@ -2301,7 +2296,7 @@ const BANNER_ENTRY = "turri_banner";
|
|
|
2301
2296
|
export function chatRulesBlock(rules = loadRules(join(PACKAGE_ROOT, "genome", "rules"))) {
|
|
2302
2297
|
return renderRulesBlock(rules);
|
|
2303
2298
|
}
|
|
2304
|
-
function registerBranding(pi,
|
|
2299
|
+
function registerBranding(pi, skills) {
|
|
2305
2300
|
pi.registerCommand("stdlib", {
|
|
2306
2301
|
description: "List the ***plain standard library: the stacks a spec can import instead of writing its implementation reqs.",
|
|
2307
2302
|
handler: async (_args, ctx) => {
|
|
@@ -2314,24 +2309,18 @@ function registerBranding(pi, ruleCount, skills) {
|
|
|
2314
2309
|
if (data === undefined)
|
|
2315
2310
|
return undefined;
|
|
2316
2311
|
const lines = [
|
|
2317
|
-
`${theme
|
|
2312
|
+
`${brandPink(theme)("turri")} ${theme.fg("muted", `v${data.version}`)}` +
|
|
2318
2313
|
(data.commit !== undefined ? theme.fg("dim", ` from a clone, ${data.commit}${data.changed ? " with changes" : ""}`) : ""),
|
|
2319
|
-
theme.fg("text", "
|
|
2320
|
-
theme.fg("muted", `${data.rules} rules · ${data.skills} skill${data.skills === 1 ? "" : "s"}` +
|
|
2321
|
-
(data.projectSkills !== undefined && data.projectSkills > 0 ? ` (${data.projectSkills} the project's)` : "") +
|
|
2322
|
-
(data.stdlib !== undefined && data.stdlib > 0 ? ` · ${data.stdlib} stdlib stacks (/stdlib)` : "") +
|
|
2323
|
-
(data.pending > 0 ? theme.fg("warning", ` (${data.pending} failed to load)`) : "")),
|
|
2314
|
+
theme.fg("text", "the first coding agent with the power of regenerative software"),
|
|
2324
2315
|
// Where the settings are changed, without leaving the chat.
|
|
2325
2316
|
theme.fg("muted", "configure with /turri-config"),
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
...(data.sandbox
|
|
2329
|
-
? [theme.fg(data.sandbox.level === "ok" ? "dim" : data.sandbox.level === "limited" ? "warning" : "error", data.sandbox.text)]
|
|
2330
|
-
: []),
|
|
2317
|
+
// Only what is wrong: skills that did not load, and agents' commands less fenced than they should be.
|
|
2318
|
+
...(data.pending > 0 ? [theme.fg("warning", `${data.pending} skill${data.pending === 1 ? "" : "s"} failed to load`)] : []),
|
|
2319
|
+
...(data.sandbox !== undefined && data.sandbox.level !== "ok" ? [theme.fg(data.sandbox.level === "limited" ? "warning" : "error", data.sandbox.text)] : []),
|
|
2331
2320
|
];
|
|
2332
2321
|
// The jellyfish turri is named after, in ink for the theme's background: a picture where the
|
|
2333
2322
|
// terminal shows images, drawn in text where it does not.
|
|
2334
|
-
return new Banner(lines, (rule) => theme.fg("borderMuted", rule), jellyfishArt(theme.appearance, (
|
|
2323
|
+
return new Banner(lines, (rule) => theme.fg("borderMuted", rule), jellyfishArt(theme.appearance, (theme.getColorMode?.() ?? "truecolor") === "truecolor"));
|
|
2335
2324
|
});
|
|
2336
2325
|
pi.on("session_start", (_event, ctx) => {
|
|
2337
2326
|
if (ctx.hasUI)
|
|
@@ -2342,12 +2331,7 @@ function registerBranding(pi, ruleCount, skills) {
|
|
|
2342
2331
|
version: identity.version ?? "0.0.0",
|
|
2343
2332
|
commit: identity.git_commit?.slice(0, 7),
|
|
2344
2333
|
changed: identity.git_dirty === true,
|
|
2345
|
-
rules: ruleCount,
|
|
2346
|
-
skills: skills.skills.length,
|
|
2347
|
-
projectSkills: skills.skills.filter((skill) => skill.origin !== "turri").length,
|
|
2348
2334
|
pending: skills.pending.length,
|
|
2349
|
-
stdlib: stdlibStackCount(),
|
|
2350
|
-
cwd: ctx.cwd,
|
|
2351
2335
|
sandbox: sandboxSummary(),
|
|
2352
2336
|
});
|
|
2353
2337
|
}
|
|
@@ -2356,10 +2340,8 @@ function registerBranding(pi, ruleCount, skills) {
|
|
|
2356
2340
|
}
|
|
2357
2341
|
});
|
|
2358
2342
|
}
|
|
2359
|
-
/** How many rows the banner's jellyfish takes:
|
|
2360
|
-
const JELLYFISH_ROWS =
|
|
2361
|
-
/** How long each frame of the jellyfish shows. */
|
|
2362
|
-
const JELLYFISH_FRAME_MS = 250;
|
|
2343
|
+
/** How many rows the banner's jellyfish takes at most: as tall as the one drawn in text. */
|
|
2344
|
+
const JELLYFISH_ROWS = 10;
|
|
2363
2345
|
/** Which frame of the jellyfish every banner shows, moved on by `animateJellyfish`. */
|
|
2364
2346
|
let jellyfishFrame = 0;
|
|
2365
2347
|
let jellyfishTimer;
|
|
@@ -2479,24 +2461,16 @@ class Banner {
|
|
|
2479
2461
|
}
|
|
2480
2462
|
const jellyfishFrameCache = new Map();
|
|
2481
2463
|
/**
|
|
2482
|
-
* The banner's jellyfish for this terminal: where it shows images, the picture's frames
|
|
2483
|
-
*
|
|
2484
|
-
*
|
|
2464
|
+
* The banner's jellyfish for this terminal: where it shows images, the picture's frames
|
|
2465
|
+
* (scripts/jellyfish-frames.mjs writes them), in the colours for the theme's background; where it does not, the
|
|
2466
|
+
* small one in half blocks. None when the pictures cannot be read.
|
|
2485
2467
|
*/
|
|
2486
|
-
function jellyfishArt(appearance,
|
|
2468
|
+
function jellyfishArt(appearance, truecolor) {
|
|
2487
2469
|
const dir = join(PACKAGE_ROOT, "assets", "jellyfish");
|
|
2488
2470
|
try {
|
|
2489
2471
|
if (getCapabilities().images === null) {
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
.split(/^---$/m)
|
|
2493
|
-
.map((frame) => frame.replace(/^\n+|\n+$/g, "").split("\n"))
|
|
2494
|
-
.filter((frame) => frame.some((line) => line.trim() !== ""));
|
|
2495
|
-
if (drawn.length === 0)
|
|
2496
|
-
return undefined;
|
|
2497
|
-
const columns = Math.max(...drawn.flat().map((line) => visibleWidth(line)));
|
|
2498
|
-
const rows = Math.max(...drawn.map((frame) => frame.length));
|
|
2499
|
-
return { kind: "text", frames: drawn.map((frame) => frame.map((line) => ink(line.padEnd(columns)))), columns, rows };
|
|
2472
|
+
const frames = JELLYFISH_TEXT_FRAMES.map((pixels) => halfBlocks(pixels, appearance, truecolor));
|
|
2473
|
+
return { kind: "text", frames, columns: JELLYFISH_TEXT_FRAMES[0][0].length, rows: frames[0].length };
|
|
2500
2474
|
}
|
|
2501
2475
|
let frames = jellyfishFrameCache.get(appearance);
|
|
2502
2476
|
if (frames === undefined) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { sgr } from "./regenerate/plain-palette.js";
|
|
2
|
+
/** The pixels' colours on a dark background and a light one - the tentacles darker on light, where neon does not show. */
|
|
3
|
+
export const JELLYFISH_COLOURS = {
|
|
4
|
+
dark: { p: "#ff5c8d", h: "#ff7fa6", w: "#f4f7f9", o: "#ff7a3d", e: "#732553", N: "#8eebee", n: "#5ce1e6", l: "#85a3b2", g: "#3e5a6b" },
|
|
5
|
+
light: { p: "#ff5c8d", h: "#ff7fa6", w: "#f4f7f9", o: "#ff7a3d", e: "#732553", N: "#3e5a6b", n: "#1e3442", l: "#85a3b2", g: "#85a3b2" },
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* How shut the bell is, frame by frame - a quarter second each: shut quickly, held a moment, opened slowly, at rest
|
|
9
|
+
* until the next stroke.
|
|
10
|
+
*/
|
|
11
|
+
const SHUT = [0.1, 0.55, 0.9, 1, 0.85, 0.7, 0.56, 0.44, 0.33, 0.23, 0.14, 0.06];
|
|
12
|
+
const LARGE = {
|
|
13
|
+
width: 24,
|
|
14
|
+
height: 26,
|
|
15
|
+
open: [6, 12, 16, 18, 20, 22, 22, 24, 24],
|
|
16
|
+
shut: [6, 10, 14, 16, 18, 18, 18, 20, 20],
|
|
17
|
+
glint: [[-3, 2], [-4, 2], [-5, 3], [-6, 4], [-6, 5]],
|
|
18
|
+
shine: [-2, 2],
|
|
19
|
+
gut: [".oo..oo.", "oooooooo", "oooooooo", ".oo..oo."],
|
|
20
|
+
gutRow: 4,
|
|
21
|
+
strands: [
|
|
22
|
+
{ at: -8, length: 10, sway: 1.1, ink: "N" },
|
|
23
|
+
{ at: -4.5, length: 13, sway: 1.3, ink: "N" },
|
|
24
|
+
{ at: -1, length: 14, sway: 0.8, ink: "n" },
|
|
25
|
+
{ at: 2, length: 13, sway: 0.8, ink: "n" },
|
|
26
|
+
{ at: 5.5, length: 13, sway: 1.3, ink: "N" },
|
|
27
|
+
{ at: 9, length: 10, sway: 1.1, ink: "N" },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
const SMALL = {
|
|
31
|
+
width: 18,
|
|
32
|
+
height: 20,
|
|
33
|
+
open: [4, 10, 12, 14, 16, 18, 18],
|
|
34
|
+
shut: [4, 8, 10, 12, 14, 14, 14],
|
|
35
|
+
glint: [[-2, 1], [-3, 2], [-4, 3]],
|
|
36
|
+
shine: [-1, 1],
|
|
37
|
+
gut: ["oo..oo", "oooooo", "oo..oo"],
|
|
38
|
+
gutRow: 3,
|
|
39
|
+
strands: [
|
|
40
|
+
{ at: -6, length: 8, sway: 0.9, ink: "N" },
|
|
41
|
+
{ at: -3, length: 10, sway: 1, ink: "N" },
|
|
42
|
+
{ at: -0.5, length: 11, sway: 0.7, ink: "n" },
|
|
43
|
+
{ at: 1.5, length: 10, sway: 0.7, ink: "n" },
|
|
44
|
+
{ at: 4, length: 10, sway: 1, ink: "N" },
|
|
45
|
+
{ at: 7, length: 8, sway: 0.9, ink: "N" },
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
/** One frame of a design: its height in rows of its width in pixels, "." where there is none. */
|
|
49
|
+
function frame(design, t) {
|
|
50
|
+
const grid = Array.from({ length: design.height }, () => Array(design.width).fill("."));
|
|
51
|
+
const centre = design.width / 2;
|
|
52
|
+
const set = (y, x, pixel, over = true) => {
|
|
53
|
+
if (y < 0 || y >= design.height || x < 0 || x >= design.width)
|
|
54
|
+
return;
|
|
55
|
+
if (over || grid[y][x] === ".")
|
|
56
|
+
grid[y][x] = pixel;
|
|
57
|
+
};
|
|
58
|
+
const shut = SHUT[t % SHUT.length];
|
|
59
|
+
// Shut, it has risen a pixel.
|
|
60
|
+
const top = shut > 0.6 ? 0 : 1;
|
|
61
|
+
const widths = design.open.map((open, row) => 2 * Math.round((open - (open - design.shut[row]) * shut) / 2));
|
|
62
|
+
widths.forEach((width, row) => {
|
|
63
|
+
for (let x = centre - width / 2; x < centre + width / 2; x++)
|
|
64
|
+
set(top + row, x, "p");
|
|
65
|
+
});
|
|
66
|
+
const rimWidth = widths.at(-1);
|
|
67
|
+
const rim = top + widths.length;
|
|
68
|
+
// The underside along the bottom row, the rim scalloped under it.
|
|
69
|
+
for (let x = centre - rimWidth / 2 + 2; x < centre + rimWidth / 2 - 2; x++)
|
|
70
|
+
set(rim - 1, x, "e");
|
|
71
|
+
for (let x = centre - rimWidth / 2; x < centre + rimWidth / 2; x += 2)
|
|
72
|
+
set(rim, x + 1, "p");
|
|
73
|
+
// A glint up on the left, curving with the dome, and a pixel of shine.
|
|
74
|
+
for (const [dx, dy] of design.glint)
|
|
75
|
+
set(top + dy, centre + dx, "h");
|
|
76
|
+
set(top + design.shine[1], centre + design.shine[0], "w");
|
|
77
|
+
const gutLeft = centre - Math.floor(design.gut[0].length / 2);
|
|
78
|
+
design.gut.forEach((line, dy) => [...line].forEach((pixel, dx) => pixel === "o" && set(top + design.gutRow + dy, gutLeft + dx, "o")));
|
|
79
|
+
// The tentacles: rooted under the bell, drawn in as it shuts, swaying - the further down, the later.
|
|
80
|
+
const phase = (2 * Math.PI * t) / SHUT.length;
|
|
81
|
+
design.strands.forEach((strand, i) => {
|
|
82
|
+
const root = centre - 0.5 + strand.at * (rimWidth / design.open.at(-1));
|
|
83
|
+
for (let y = 0; y < strand.length; y++) {
|
|
84
|
+
const x = Math.round(root + strand.sway * Math.sin(phase - y * 0.55 + i * 0.8) * Math.min(1, y / 3));
|
|
85
|
+
const left = strand.length - y;
|
|
86
|
+
// The tips come apart, every other pixel, fading - into code.
|
|
87
|
+
if (left <= 3 && y % 2 === 1)
|
|
88
|
+
continue;
|
|
89
|
+
set(rim + 1 + y, x, left > 5 ? strand.ink : left > 2 ? "l" : "g", false);
|
|
90
|
+
// The oral arms are frilled.
|
|
91
|
+
if (strand.ink === "n" && left > 5 && (y + t) % 3 === 0)
|
|
92
|
+
set(rim + 1 + y, x + (strand.at < 0 ? -1 : 1), "n", false);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return grid.map((row) => row.join(""));
|
|
96
|
+
}
|
|
97
|
+
export const JELLYFISH_WIDTH = LARGE.width;
|
|
98
|
+
export const JELLYFISH_HEIGHT = LARGE.height;
|
|
99
|
+
/** The large jellyfish's frames, a stroke: what the pictures are rendered from. */
|
|
100
|
+
export const JELLYFISH_FRAMES = SHUT.map((_, t) => frame(LARGE, t));
|
|
101
|
+
/** The small jellyfish's frames, a stroke: what a terminal without images draws, in half blocks. */
|
|
102
|
+
export const JELLYFISH_TEXT_FRAMES = SHUT.map((_, t) => frame(SMALL, t));
|
|
103
|
+
/** How long each frame shows. */
|
|
104
|
+
export const JELLYFISH_FRAME_MS = 250;
|
|
105
|
+
/**
|
|
106
|
+
* A frame drawn in half blocks for a terminal that shows no images: two pixels a cell, the upper one the text's colour
|
|
107
|
+
* and the lower one the background's, in as many colours as the terminal has: a column a pixel, a row every two.
|
|
108
|
+
*/
|
|
109
|
+
export function halfBlocks(pixels, appearance, truecolor) {
|
|
110
|
+
const colours = JELLYFISH_COLOURS[appearance];
|
|
111
|
+
const colour = (pixel) => (pixel === undefined || pixel === "." ? undefined : colours[pixel]);
|
|
112
|
+
const rows = [];
|
|
113
|
+
for (let y = 0; y < pixels.length; y += 2) {
|
|
114
|
+
let row = "";
|
|
115
|
+
for (let x = 0; x < (pixels[0]?.length ?? 0); x++) {
|
|
116
|
+
const upper = colour(pixels[y]?.[x]);
|
|
117
|
+
const lower = colour(pixels[y + 1]?.[x]);
|
|
118
|
+
if (upper === undefined && lower === undefined)
|
|
119
|
+
row += " ";
|
|
120
|
+
else if (upper === undefined)
|
|
121
|
+
row += `\x1b[${sgr(lower, false, truecolor)}m▄\x1b[39m`;
|
|
122
|
+
else if (lower === undefined)
|
|
123
|
+
row += `\x1b[${sgr(upper, false, truecolor)}m▀\x1b[39m`;
|
|
124
|
+
else
|
|
125
|
+
row += `\x1b[${sgr(upper, false, truecolor)};${sgr(lower, true, truecolor)}m▀\x1b[39;49m`;
|
|
126
|
+
}
|
|
127
|
+
rows.push(row);
|
|
128
|
+
}
|
|
129
|
+
return rows;
|
|
130
|
+
}
|
package/dist/loop-cli.js
CHANGED
|
@@ -80,17 +80,17 @@ async function runLoopReporting(options) {
|
|
|
80
80
|
return;
|
|
81
81
|
seen.set(job.id, line);
|
|
82
82
|
if (job.status === "running")
|
|
83
|
-
console.log(`${c.
|
|
83
|
+
console.log(`${c.accent(`▶ ${job.id}`)} ${pulseName(job)}${job.stage !== undefined ? c.dim(` - ${job.stage}`) : ""}`);
|
|
84
84
|
},
|
|
85
85
|
onFinish: (job) => {
|
|
86
|
-
const mark = job.status === "succeeded" ? c.
|
|
86
|
+
const mark = job.status === "succeeded" ? c.success("✓") : job.status === "stopped" ? c.dim("■") : c.error("✗");
|
|
87
87
|
console.log(`${mark} ${job.id} ${pulseName(job)} - ${job.status}`);
|
|
88
88
|
if (job.status !== "succeeded")
|
|
89
89
|
console.log(c.dim((job.announce ?? job.report ?? job.error ?? "").trim()));
|
|
90
90
|
// Its report is not printed when it succeeds, and nobody is asked here: the spec gaps its agents met are said.
|
|
91
91
|
else
|
|
92
92
|
for (const gap of job.details?.specGaps ?? [])
|
|
93
|
-
console.log(c.
|
|
93
|
+
console.log(c.warning(` spec gap: ${gap.question}`), c.dim(`Meanwhile: ${gap.assumption}`));
|
|
94
94
|
report?.write();
|
|
95
95
|
},
|
|
96
96
|
});
|
|
@@ -98,7 +98,7 @@ async function runLoopReporting(options) {
|
|
|
98
98
|
onModelEvent((event) => {
|
|
99
99
|
const line = describeTurnIncident(event);
|
|
100
100
|
if (line !== undefined)
|
|
101
|
-
console.log(c.
|
|
101
|
+
console.log(c.warning(` ${event.job !== undefined ? `${event.job}: ` : ""}${line}`));
|
|
102
102
|
});
|
|
103
103
|
const planner = modelSpec(projectModels(project).harness);
|
|
104
104
|
const loop = createWorkLoop({
|
|
@@ -131,7 +131,7 @@ async function runLoopReporting(options) {
|
|
|
131
131
|
// No terminal and no --start-over: no one to ask, so the run stops here rather than build nothing and say it is done.
|
|
132
132
|
if (options.startOver !== true && process.stdin.isTTY !== true) {
|
|
133
133
|
const message = `${why}\nThere is no terminal to ask whether to start over: run it again with --start-over to start over without asking.`;
|
|
134
|
-
console.error(c.
|
|
134
|
+
console.error(c.error(message));
|
|
135
135
|
report?.end("incomplete", message);
|
|
136
136
|
process.exit(1);
|
|
137
137
|
}
|
|
@@ -162,10 +162,10 @@ async function runLoopReporting(options) {
|
|
|
162
162
|
// waiting for a start over, nothing the planner left out.
|
|
163
163
|
const unfinished = moduleStates(loop.drift(plainFile)).filter((module) => module.state !== "complete");
|
|
164
164
|
const verdict = failed.length > 0
|
|
165
|
-
? c.
|
|
165
|
+
? c.error(c.bold(`${failed.length} job${failed.length === 1 ? "" : "s"} failed`))
|
|
166
166
|
: unfinished.length > 0
|
|
167
|
-
? c.
|
|
168
|
-
: c.
|
|
167
|
+
? c.error(c.bold(`not complete: ${unfinished.map((module) => `${module.module}${module.why !== undefined ? ` - ${module.why}` : ""}`).join("; ")}`))
|
|
168
|
+
: c.success(c.bold("all jobs done"));
|
|
169
169
|
console.log(`\n${verdict}`);
|
|
170
170
|
for (const job of [...ended].reverse())
|
|
171
171
|
console.log(` ${job.id} ${pulseName(job)}: ${job.status}`);
|
package/dist/module-report.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { brand } from "./brand.js";
|
|
1
2
|
/** Colours for what the command line prints. */
|
|
2
3
|
export const c = {
|
|
3
4
|
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
4
5
|
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
success: brand.success,
|
|
7
|
+
error: brand.error,
|
|
8
|
+
warning: brand.warning,
|
|
9
|
+
accent: brand.accent,
|
|
9
10
|
};
|