@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.
Files changed (84) hide show
  1. package/.pi/themes/turri-dark.json +67 -66
  2. package/.pi/themes/turri-light.json +71 -73
  3. package/README.md +41 -127
  4. package/assets/jellyfish/dark-00.png +0 -0
  5. package/assets/jellyfish/dark-01.png +0 -0
  6. package/assets/jellyfish/dark-02.png +0 -0
  7. package/assets/jellyfish/dark-03.png +0 -0
  8. package/assets/jellyfish/dark-04.png +0 -0
  9. package/assets/jellyfish/dark-05.png +0 -0
  10. package/assets/jellyfish/dark-06.png +0 -0
  11. package/assets/jellyfish/dark-07.png +0 -0
  12. package/assets/jellyfish/dark-08.png +0 -0
  13. package/assets/jellyfish/dark-09.png +0 -0
  14. package/assets/jellyfish/dark-10.png +0 -0
  15. package/assets/jellyfish/dark-11.png +0 -0
  16. package/assets/jellyfish/light-00.png +0 -0
  17. package/assets/jellyfish/light-01.png +0 -0
  18. package/assets/jellyfish/light-02.png +0 -0
  19. package/assets/jellyfish/light-03.png +0 -0
  20. package/assets/jellyfish/light-04.png +0 -0
  21. package/assets/jellyfish/light-05.png +0 -0
  22. package/assets/jellyfish/light-06.png +0 -0
  23. package/assets/jellyfish/light-07.png +0 -0
  24. package/assets/jellyfish/light-08.png +0 -0
  25. package/assets/jellyfish/light-09.png +0 -0
  26. package/assets/jellyfish/light-10.png +0 -0
  27. package/assets/jellyfish/light-11.png +0 -0
  28. package/dist/brand.js +7 -7
  29. package/dist/cultivate-cli.js +16 -23
  30. package/dist/extension.js +33 -59
  31. package/dist/jellyfish.js +130 -0
  32. package/dist/loop-cli.js +8 -8
  33. package/dist/module-report.js +5 -4
  34. package/dist/regenerate/plain-palette.js +52 -37
  35. package/dist/regenerate/side-panel.js +1 -1
  36. package/dist/regenerate/spec-map-view.js +9 -5
  37. package/dist/release-cli.js +3 -8
  38. package/dist/validate-cli.js +3 -8
  39. package/docs/README.md +5 -12
  40. package/docs/authoring-plain.md +37 -121
  41. package/docs/config.md +50 -176
  42. package/docs/deploy.md +21 -80
  43. package/docs/development.md +13 -41
  44. package/docs/headless.md +30 -136
  45. package/docs/images/how-turri-works.svg +29 -31
  46. package/docs/images/turri-banner.png +0 -0
  47. package/docs/images/turri-screenshot.png +0 -0
  48. package/docs/installation.md +13 -40
  49. package/docs/parser.md +15 -43
  50. package/docs/regenerating.md +231 -964
  51. package/docs/sandbox.md +38 -152
  52. package/docs/skills-and-gates.md +50 -173
  53. package/docs/theming.md +45 -46
  54. package/docs/using.md +66 -356
  55. package/genome/rules/bullet-continuation.md +5 -13
  56. package/genome/rules/concise-specs-examples.md +2 -4
  57. package/genome/rules/concise-specs-integration-examples.md +2 -6
  58. package/genome/rules/concise-specs.md +1 -2
  59. package/genome/rules/integration-embedded-testing.md +1 -4
  60. package/genome/rules/integrations.md +1 -2
  61. package/genome/rules/project-model.md +14 -37
  62. package/genome/rules/stdlib.md +14 -49
  63. package/genome/rules/turri-regeneration.md +35 -23
  64. package/genome/skills/add-implementation-requirement/SKILL.md +1 -2
  65. package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +3 -8
  66. package/genome/skills/brood/SKILL.md +37 -127
  67. package/genome/skills/brood/references/intent.md +4 -12
  68. package/genome/skills/brood/references/product.md +24 -76
  69. package/genome/skills/brood/references/stack.md +7 -29
  70. package/genome/skills/brood/references/testing.md +11 -42
  71. package/genome/skills/bud/SKILL.md +32 -109
  72. package/genome/skills/bud/references/intent.md +4 -11
  73. package/genome/skills/implement-conformance-testing-script/SKILL.md +3 -6
  74. package/genome/skills/implement-unit-testing-script/SKILL.md +1 -2
  75. package/genome/skills/mature/SKILL.md +21 -69
  76. package/genome/skills/spawn/SKILL.md +28 -113
  77. package/package.json +1 -1
  78. package/assets/jellyfish/dark-12.png +0 -0
  79. package/assets/jellyfish/dark-13.png +0 -0
  80. package/assets/jellyfish/dark-14.png +0 -0
  81. package/assets/jellyfish/fallback.txt +0 -252
  82. package/assets/jellyfish/light-12.png +0 -0
  83. package/assets/jellyfish/light-13.png +0 -0
  84. 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 ones its themes use (.pi/themes): on a dark terminal its lime
6
- * accent and green, on a light one its blue. Nothing is coloured when the output is not a terminal, or NO_COLOR is set;
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("#0a1fd4", 34) : ink("#e0ff6e", 93),
38
+ accent: light ? ink("#1e3442", 34) : ink("#5ce1e6", 96),
39
39
  /** Done, and what was written. */
40
- success: light ? ink("#223936", 32) : ink("#79fc96", 92),
40
+ success: light ? ink("#732553", 35) : ink("#ff5c8d", 95),
41
41
  /** What is wrong: what stops turri. */
42
- error: light ? ink("#d00505", 31) : ink("#dd5353", 91),
42
+ error: ink("#ff7a3d", 33),
43
43
  /** What works, with limits. */
44
- warning: ink("#dc6803", 33),
44
+ warning: ink("#ff7a3d", 33),
45
45
  /** What goes with it, and can be skimmed. */
46
- muted: ink("#808080", 90),
46
+ muted: light ? ink("#3e5a6b", 90) : ink("#85a3b2", 37),
47
47
  bold: (text) => (on ? `\x1b[1m${text}\x1b[22m` : text),
48
48
  };
49
49
  }
@@ -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
- const c = {
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.cyan(`> step ${name}`))} ${c.dim(`attempt ${attempt}`)}`),
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.yellow(`> gate ${name}`))} ${c.dim("no artifact - failing without spending judges")}`
27
- : `${c.bold(c.yellow(`> gate ${name}`))} ${c.dim(`${judges} independent judges`)}`),
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.yellow(`review ${step}`)}: ${scope} ${c.dim(`- ${findings} to fix`)}`),
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.yellow(`> gate ${name}`))} ${c.dim(`reviewer read along in ${passes} passes, nothing outstanding`)}`
33
- : `${c.bold(c.yellow(`> gate ${name}`))} ${c.dim(`${findings} unresolved from the reviewer - failing without spending judges`)}`),
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.green("pass") : c.red(verdict);
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.green,
41
- retry: c.yellow,
42
- escalate: c.yellow,
43
- abort: c.red,
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.cyan("? "))}${title}\n${list}\n> `)).trim();
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.cyan("? "))}${title}\n> `)).trim();
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.yellow("! human review required"))}`);
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.green(c.bold("run complete")) : c.red(c.bold("run aborted at a gate"))} ${c.dim(`artifacts in ${relative(cwd, skillRunDir(cwd, skill.name))}`)}`);
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, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
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 { brandBlue, plainPaint } from "./regenerate/plain-palette.js";
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, stdlibStackCount } from "./stdlib.js";
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, rules.length, discoveredSkills);
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, esc gives the chat its keys back and the panel stays, and the key gives them to the panel.
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: brand blue.
1649
- conforms: brandBlue(theme),
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
- // ctrl+c closes it too: whatever else goes wrong, a key always leads back to the chat.
1713
- if (matchesKey(data, PANEL_KEY) || matchesKey(data, "ctrl+c"))
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, PANEL_KEY))
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, ruleCount, skills) {
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.fg("accent", "turri")} ${theme.fg("muted", `v${data.version}`)}` +
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", "regenerative software - the code is regenerated from the spec"),
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
- theme.fg("dim", data.cwd),
2327
- // Where every command an agent runs will run - or that none can.
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, (art) => theme.fg("text", art)));
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: taller than the lines beside it. */
2360
- const JELLYFISH_ROWS = 13;
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 spread evenly
2483
- * over the video, light ink for a dark background and dark ink for a light one; where it does not, the
2484
- * jellyfish drawn in text, in `ink`. None when they cannot be read.
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, ink) {
2468
+ function jellyfishArt(appearance, truecolor) {
2487
2469
  const dir = join(PACKAGE_ROOT, "assets", "jellyfish");
2488
2470
  try {
2489
2471
  if (getCapabilities().images === null) {
2490
- // Its frames one after another, each ended by a line of "---" (scripts/jellyfish-frames.mjs writes them).
2491
- const drawn = readFileSync(join(dir, "fallback.txt"), "utf8")
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.cyan(`▶ ${job.id}`)} ${pulseName(job)}${job.stage !== undefined ? c.dim(` - ${job.stage}`) : ""}`);
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.green("✓") : job.status === "stopped" ? c.dim("■") : c.red("✗");
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.yellow(` spec gap: ${gap.question}`), c.dim(`Meanwhile: ${gap.assumption}`));
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.yellow(` ${event.job !== undefined ? `${event.job}: ` : ""}${line}`));
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.red(message));
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.red(c.bold(`${failed.length} job${failed.length === 1 ? "" : "s"} failed`))
165
+ ? c.error(c.bold(`${failed.length} job${failed.length === 1 ? "" : "s"} failed`))
166
166
  : unfinished.length > 0
167
- ? c.red(c.bold(`not complete: ${unfinished.map((module) => `${module.module}${module.why !== undefined ? ` - ${module.why}` : ""}`).join("; ")}`))
168
- : c.green(c.bold("all jobs done"));
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}`);
@@ -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
- green: (s) => `\x1b[32m${s}\x1b[0m`,
6
- red: (s) => `\x1b[31m${s}\x1b[0m`,
7
- yellow: (s) => `\x1b[33m${s}\x1b[0m`,
8
- cyan: (s) => `\x1b[36m${s}\x1b[0m`,
6
+ success: brand.success,
7
+ error: brand.error,
8
+ warning: brand.warning,
9
+ accent: brand.accent,
9
10
  };