@turri-dev/cli 0.1.4-next.fa9223b → 0.1.5-next.6df6879

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 (108) 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/cli-options.js +12 -6
  30. package/dist/config-edit.js +1 -0
  31. package/dist/config.js +22 -2
  32. package/dist/cultivate-cli.js +16 -23
  33. package/dist/extension.js +128 -108
  34. package/dist/init-config.js +5 -0
  35. package/dist/jellyfish.js +130 -0
  36. package/dist/loop-cli.js +16 -8
  37. package/dist/loop-tools.js +60 -21
  38. package/dist/module-report.js +5 -4
  39. package/dist/pi.js +76 -19
  40. package/dist/picture-row.js +3 -2
  41. package/dist/regenerate/activity.js +21 -0
  42. package/dist/regenerate/jobs.js +10 -5
  43. package/dist/regenerate/list-arguments.js +121 -0
  44. package/dist/regenerate/loop.js +66 -43
  45. package/dist/regenerate/panel-dock.js +7 -5
  46. package/dist/regenerate/plain-palette.js +52 -37
  47. package/dist/regenerate/regenerate.js +4 -2
  48. package/dist/regenerate/self-check.js +13 -6
  49. package/dist/regenerate/side-panel.js +1 -1
  50. package/dist/regenerate/spec-map-view.js +9 -5
  51. package/dist/regenerate/test-scripts.js +79 -13
  52. package/dist/regenerate/unit-review.js +1 -1
  53. package/dist/regenerate-cli.js +11 -5
  54. package/dist/release-cli.js +3 -8
  55. package/dist/review/live.js +2 -1
  56. package/dist/run-report.js +26 -1
  57. package/dist/screen-padding.js +72 -0
  58. package/dist/stacks.js +3 -5
  59. package/dist/validate-cli.js +3 -8
  60. package/docs/README.md +5 -12
  61. package/docs/authoring-plain.md +37 -119
  62. package/docs/config.md +55 -178
  63. package/docs/deploy.md +21 -80
  64. package/docs/development.md +14 -41
  65. package/docs/headless.md +40 -129
  66. package/docs/images/how-turri-works.svg +29 -31
  67. package/docs/images/turri-banner.png +0 -0
  68. package/docs/images/turri-screenshot.png +0 -0
  69. package/docs/installation.md +13 -40
  70. package/docs/parser.md +15 -43
  71. package/docs/regenerating.md +231 -942
  72. package/docs/sandbox.md +38 -152
  73. package/docs/skills-and-gates.md +50 -173
  74. package/docs/theming.md +45 -46
  75. package/docs/using.md +68 -344
  76. package/genome/rules/bullet-continuation.md +5 -13
  77. package/genome/rules/concise-specs-examples.md +2 -4
  78. package/genome/rules/concise-specs-integration-examples.md +2 -6
  79. package/genome/rules/concise-specs.md +1 -2
  80. package/genome/rules/func-specs.md +1 -0
  81. package/genome/rules/integration-embedded-testing.md +1 -4
  82. package/genome/rules/integrations.md +1 -2
  83. package/genome/rules/project-model.md +14 -35
  84. package/genome/rules/stdlib.md +14 -49
  85. package/genome/rules/turri-regeneration.md +48 -0
  86. package/genome/skills/add-implementation-requirement/SKILL.md +1 -2
  87. package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +3 -8
  88. package/genome/skills/brood/SKILL.md +37 -127
  89. package/genome/skills/brood/references/intent.md +4 -12
  90. package/genome/skills/brood/references/product.md +24 -76
  91. package/genome/skills/brood/references/stack.md +7 -29
  92. package/genome/skills/brood/references/testing.md +11 -42
  93. package/genome/skills/bud/SKILL.md +32 -109
  94. package/genome/skills/bud/references/intent.md +4 -11
  95. package/genome/skills/implement-conformance-testing-script/SKILL.md +4 -7
  96. package/genome/skills/implement-unit-testing-script/SKILL.md +2 -3
  97. package/genome/skills/init-plain-project/SKILL.md +1 -1
  98. package/genome/skills/mature/SKILL.md +21 -69
  99. package/genome/skills/refactor-module/SKILL.md +24 -2
  100. package/genome/skills/spawn/SKILL.md +28 -113
  101. package/package.json +1 -1
  102. package/assets/jellyfish/dark-12.png +0 -0
  103. package/assets/jellyfish/dark-13.png +0 -0
  104. package/assets/jellyfish/dark-14.png +0 -0
  105. package/assets/jellyfish/fallback.txt +0 -252
  106. package/assets/jellyfish/light-12.png +0 -0
  107. package/assets/jellyfish/light-13.png +0 -0
  108. package/assets/jellyfish/light-14.png +0 -0
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";
@@ -16,7 +16,7 @@ import { readScript, scriptAgainstMachine, stackOnMachine, toolchainsBlock } fro
16
16
  import { sandboxAvailable, sandboxSummary } from "./regenerate/os-sandbox.js";
17
17
  import { findTestScripts } from "./regenerate/test-scripts.js";
18
18
  import { createAskUserTool } from "./tools.js";
19
- import { asksFromScratch, driftTool, fromWorkingDir, haltTool, loopReply, MODEL_PARAM, regenerateTool, stopJob, WAIT_PARAM, WORKING_DIR_PARAM } from "./loop-tools.js";
19
+ import { asksFromScratch, asksWholeChain, driftTool, fromWorkingDir, haltTool, loopReply, MODEL_PARAM, regenerateTool, STACK_PARAM, stopJob, WAIT_PARAM, withCallStack, WORKING_DIR_PARAM } from "./loop-tools.js";
20
20
  import { createWebTools } from "./regenerate/web.js";
21
21
  import { describeGates, interruptedRun, runSkill } from "./genome.js";
22
22
  import { skillStatus } from "./skill.js";
@@ -29,10 +29,12 @@ 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";
37
+ import { padScreen, paddedColumns, paddedRows, SCREEN_PADDING } from "./screen-padding.js";
36
38
  import { skillAboutLine, skillProgressLine } from "./skill-progress.js";
37
39
  import { createSidePanel } from "./regenerate/side-panel.js";
38
40
  import { createSpecMapSource, defaultMapFiles } from "./regenerate/spec-map.js";
@@ -41,13 +43,13 @@ import { createJobsTreeState, selectJob } from "./regenerate/jobs-tree.js";
41
43
  import { describeDiagnosis, diagnoseFailure } from "./regenerate/diagnose.js";
42
44
  import { HISTORY_DIR, historyLog, historyShow } from "./regenerate/lineage.js";
43
45
  import { defaultBuildDir } from "./regenerate/paths.js";
44
- import { describeUsage } from "./regenerate/activity.js";
46
+ import { describeTurnIncident, describeUsage, onModelEvent } from "./regenerate/activity.js";
45
47
  import { describeSpecConflict } from "./regenerate/stop-report.js";
46
48
  import { inheritProviders, inheritThinkingLevel } from "./pi.js";
47
49
  import { beginCapture, captureConsole, describeCaptured, dropNoise } from "./quiet.js";
48
50
  import { loadRules, renderRulesBlock } from "./rules.js";
49
51
  import { registerTurriConfig } from "./config-command.js";
50
- import { describeStdlib, stdlibStackCount } from "./stdlib.js";
52
+ import { describeStdlib } from "./stdlib.js";
51
53
  import { peekKeyListener, runAsk } from "./ask.js";
52
54
  import { QuestionDialog } from "./question-dialog.js";
53
55
  import { registerStacks } from "./stacks.js";
@@ -96,7 +98,7 @@ aside) {
96
98
  ...(context !== undefined ? { context } : {}),
97
99
  ...(options !== undefined ? { options } : {}),
98
100
  ...(selected !== undefined ? { selected } : {}),
99
- rows: () => tui.terminal.rows,
101
+ rows: () => paddedRows(tui.terminal.rows),
100
102
  style: { accent: (text) => theme.fg("accent", text), muted: (text) => theme.fg("muted", text), bold: (text) => theme.bold(text) },
101
103
  done: (answer) => {
102
104
  if (questionShown === shown)
@@ -328,6 +330,19 @@ export default function (pi) {
328
330
  };
329
331
  pi.on("agent_end", endTurnCapture);
330
332
  pi.on("agent_settled", endTurnCapture);
333
+ // A pulse's provider failing and waited out, or a turn turri ended itself: said in the chat as it happens - the job
334
+ // panel's activity holds it too, but scrolls by.
335
+ let stopTurnIncidents;
336
+ pi.on("session_start", (_event, ctx) => {
337
+ stopTurnIncidents?.();
338
+ if (!ctx.hasUI)
339
+ return;
340
+ stopTurnIncidents = onModelEvent((event) => {
341
+ const line = describeTurnIncident(event);
342
+ if (line !== undefined)
343
+ ctx.ui.notify(event.job !== undefined ? `${event.job}: ${line}` : line, "warning");
344
+ });
345
+ });
331
346
  // What a turri that was killed left running - its commands' process groups - stopped as this one starts.
332
347
  const orphans = sweepOrphanedGroups();
333
348
  let orphansSaid = orphans === 0;
@@ -357,7 +372,7 @@ export default function (pi) {
357
372
  conversationModels = ctx.modelRegistry;
358
373
  });
359
374
  inheritProviders(() => conversationModels);
360
- registerBranding(pi, rules.length, discoveredSkills);
375
+ registerBranding(pi, discoveredSkills);
361
376
  registerInterviewLog(pi);
362
377
  registerJobTools(pi, jobs, cwd);
363
378
  registerCheckPlain(pi, cwd);
@@ -611,7 +626,7 @@ function registerRegenerate(pi, cwd, jobs) {
611
626
  // by the planner: one functional spec at a time, a few together or the whole module in one, as it judges. What
612
627
  // follows the module is guidance for that plan, in the person's words.
613
628
  pi.registerCommand("regenerate", {
614
- description: "Regenerate a module from its spec as it is now, in pulses the planner cuts - named functionalities though they did not change, or all of it from scratch: /regenerate [module | file.plain] [functionality ...] [from scratch] [how to cut the work]",
629
+ description: "Regenerate a module from its spec as it is now, in pulses the planner cuts - named functionalities though they did not change, or all of it from scratch, its whole chain too when asked: /regenerate [module | file.plain] [functionality ...] [from scratch [whole chain]] [how to cut the work]",
615
630
  handler: async (args, ctx) => {
616
631
  const command = `/regenerate ${args.trim()}`.trim();
617
632
  await echoCommand(pi, command);
@@ -653,13 +668,17 @@ function registerRegenerate(pi, cwd, jobs) {
653
668
  }
654
669
  // From scratch - started over, its code deleted - asked in the person's words: confirmed first, as it deletes.
655
670
  const scratch = asksFromScratch(guidance);
671
+ // The whole chain with it - "from scratch, the whole chain" - every module it requires built again from the bottom.
672
+ const chain = scratch && asksWholeChain(guidance);
656
673
  const module = basename(plainFile).replace(/\.plain$/, "");
657
674
  if (scratch && again.length > 0) {
658
675
  say("Name functionalities or ask for it from scratch, not both: from scratch regenerates every one of them.", "warning");
659
676
  return;
660
677
  }
661
678
  if (scratch && ctx.hasUI) {
662
- const agreed = await ctx.ui.confirm(`Regenerate ${module} from scratch?`, `Its code and conformance tests are deleted - committed first, so /lineage ${module} keeps them - and the whole module is regenerated from its spec.`);
679
+ const agreed = await ctx.ui.confirm(chain ? `Regenerate ${module} and its whole chain from scratch?` : `Regenerate ${module} from scratch?`, chain
680
+ ? `The code and conformance tests of ${module} and of every module it requires are deleted - committed first, so /lineage keeps them - and the whole chain is regenerated from its spec, from the bottom.`
681
+ : `Its code and conformance tests are deleted - committed first, so /lineage ${module} keeps them - and the whole module is regenerated from its spec.`);
663
682
  if (!agreed)
664
683
  return;
665
684
  }
@@ -669,17 +688,18 @@ function registerRegenerate(pi, cwd, jobs) {
669
688
  // Said at once, and kept in the chat: what follows takes a while - the planner is an agent - and the person sees it
670
689
  // began, not a chat that stays empty until the first pulse starts. The plan is said under it once it is made.
671
690
  say(scratch
672
- ? `Deleting ${module}'s code, then planning all of it again…`
691
+ ? `Deleting ${module}'s code${chain ? " and its whole chain's" : ""}, then planning all of it again…`
673
692
  : again.length > 0
674
693
  ? `Planning ${module}'s functionalit${again.length === 1 ? "y" : "ies"} ${again.join(", ")} again…`
675
694
  : `Planning ${module}'s pulses…`, "info");
676
695
  await nextFrame();
677
696
  try {
678
- const statuses = scratch
679
- ? await loop.fromScratch(plainFile, request)
697
+ // Built with the stack chosen now - pinned to its pulses - or refused, a project of stacks built with none.
698
+ const statuses = await withCallStack(plainFile, undefined, () => scratch
699
+ ? loop.fromScratch(plainFile, { ...request, chain })
680
700
  : again.length > 0
681
- ? await loop.regenerateAgain(plainFile, again, request)
682
- : await loop.bringUp(plainFile, request);
701
+ ? loop.regenerateAgain(plainFile, again, request)
702
+ : loop.bringUp(plainFile, request));
683
703
  // Nothing to do: say how to build it again anyway.
684
704
  const hint = statuses.length > 0 && statuses.every(isUpToDate)
685
705
  ? `\nTo regenerate parts of it anyway, name their functionalities - /regenerate ${basename(plainFile)} 2.1 - or start over with /regenerate ${basename(plainFile)} from scratch.`
@@ -798,21 +818,24 @@ function conformTool(options) {
798
818
  description: "Only when the user asks for them in pulses of their own grouping: the functional specs by id, a list a pulse - [[\"1\",\"2\",\"3\"],[\"4\",\"5\",\"6\"]]. Each is tested in a pulse of its own; they are added to those chosen, as frids are",
799
819
  })),
800
820
  one_by_one: Type.Optional(Type.Boolean({ description: "Only when the user asks for it: a pulse a functional spec. Omitted: every functional spec tested in one pulse, as far as the size guardrail lets" })),
821
+ stack: STACK_PARAM,
801
822
  model: MODEL_PARAM,
802
823
  wait: WAIT_PARAM,
803
824
  }),
804
825
  async execute(_id, given, _signal, _onUpdate, ctx) {
805
826
  const params = fromWorkingDir(cwd, given);
806
827
  const plainFile = resolve(params.base, params.plain_file);
807
- const model = options.model(ctx, plainFile, params.model);
808
- const loop = options.loop(ctx);
809
- const statuses = await loop.conformance(plainFile, {
810
- ...(params.frids !== undefined ? { frids: params.frids } : {}),
811
- ...(params.groups !== undefined ? { groups: params.groups } : {}),
812
- ...(model !== undefined ? { model } : {}),
813
- ...(params.one_by_one === true ? { separately: true } : {}),
828
+ return withCallStack(plainFile, params.stack, async () => {
829
+ const model = options.model(ctx, plainFile, params.model);
830
+ const loop = options.loop(ctx);
831
+ const statuses = await loop.conformance(plainFile, {
832
+ ...(params.frids !== undefined ? { frids: params.frids } : {}),
833
+ ...(params.groups !== undefined ? { groups: params.groups } : {}),
834
+ ...(model !== undefined ? { model } : {}),
835
+ ...(params.one_by_one === true ? { separately: true } : {}),
836
+ });
837
+ return loopReply(loop, statuses, options.wait(ctx, params.wait));
814
838
  });
815
- return loopReply(loop, statuses, options.wait(ctx, params.wait));
816
839
  },
817
840
  });
818
841
  }
@@ -830,34 +853,37 @@ function registerGlimpse(pi, cwd, jobs) {
830
853
  parameters: Type.Object({
831
854
  plain_file: Type.String({ description: "Path to the .plain file" }),
832
855
  working_dir: WORKING_DIR_PARAM,
856
+ stack: STACK_PARAM,
833
857
  }),
834
858
  async execute(_id, given, _signal, _onUpdate, ctx) {
835
859
  const params = fromWorkingDir(cwd, given);
836
860
  const plainFile = resolve(params.base, params.plain_file);
837
- const running = new Set(workLoopFor(jobs, cwd, ctx).status().filter((status) => status.running !== undefined).map((status) => status.module));
838
- const planner = chatModelOf(ctx);
839
- const model = modelFor(chosenModels, "spec_changes", projectModel(ctx, plainFile) ?? planner);
840
- const { statuses, previews, busy } = await previewJobs({
841
- plainFile,
842
- cwd,
843
- ...(model !== undefined ? { model } : {}),
844
- ...(planner !== undefined ? { plannerModel: planner } : {}),
845
- steering: (file) => steeringFor(file, cwd, steeringStore),
846
- busy: (module) => running.has(module),
847
- });
848
- const text = [
849
- describeModules(statuses),
850
- ...busy.map((module) => `\n${module} has a pulse running: its next pulse is planned from what that one lands - preview it once it has.`),
851
- ...previews.map((preview) => [
861
+ return withCallStack(plainFile, params.stack, async () => {
862
+ const running = new Set(workLoopFor(jobs, cwd, ctx).status().filter((status) => status.running !== undefined).map((status) => status.module));
863
+ const planner = chatModelOf(ctx);
864
+ const model = modelFor(chosenModels, "spec_changes", projectModel(ctx, plainFile) ?? planner);
865
+ const { statuses, previews, busy } = await previewJobs({
866
+ plainFile,
867
+ cwd,
868
+ ...(model !== undefined ? { model } : {}),
869
+ ...(planner !== undefined ? { plannerModel: planner } : {}),
870
+ steering: (file) => steeringFor(file, cwd, steeringStore),
871
+ busy: (module) => running.has(module),
872
+ });
873
+ const text = [
874
+ describeModules(statuses),
875
+ ...busy.map((module) => `\n${module} has a pulse running: its next pulse is planned from what that one lands - preview it once it has.`),
876
+ ...previews.map((preview) => [
877
+ "",
878
+ `<next pulse module="${preview.module}" track="${preview.track}" title="${preview.title}">`,
879
+ preview.task ?? "A conformance pulse's prompt is built from what the code it tests holds once it is built: it is not shown before.",
880
+ "</next pulse>",
881
+ ].join("\n")),
852
882
  "",
853
- `<next pulse module="${preview.module}" track="${preview.track}" title="${preview.title}">`,
854
- preview.task ?? "A conformance pulse's prompt is built from what the code it tests holds once it is built: it is not shown before.",
855
- "</next pulse>",
856
- ].join("\n")),
857
- "",
858
- "Nothing was started. Tell the user what the next pulse is and what it will be told - the parts they are likely to care about, not the whole prompt unless they ask.",
859
- ].join("\n");
860
- return { content: [{ type: "text", text }], details: {} };
883
+ "Nothing was started. Tell the user what the next pulse is and what it will be told - the parts they are likely to care about, not the whole prompt unless they ask.",
884
+ ].join("\n");
885
+ return { content: [{ type: "text", text }], details: {} };
886
+ });
861
887
  },
862
888
  });
863
889
  }
@@ -1015,7 +1041,7 @@ export function projectConfigBlock(root) {
1015
1041
  return [
1016
1042
  ...settings(readProjectConfig(dir), withStack(undefined, () => findTestScripts(dir)), dir === root ? `${projectConfigFile(dir)}, for the specs here:` : `${relative(root, dir)}/${projectConfigFile(dir)}, for the specs in ${relative(root, dir)}/ - built in ${relative(root, dir)}/plain_modules:`, where, stacks.length > 0 ? "project of stacks" : "project"),
1017
1043
  ...(stacks.length > 0
1018
- ? ["", `Stacks: ${stacks.join(", ")} - chosen now: ${chosen ?? "none, so the project's file alone"}. Everything is built with the chosen one; switch with the stack tool when the user asks.`]
1044
+ ? ["", `Stacks: ${stacks.join(", ")} - chosen now: ${chosen ?? "none, so the project's file alone"}. Each module is built with the stack of its part - a backend's spec with the backend stack: pass it as stack to regenerate, conform and glimpse, and it is pinned to that module's pulses, whatever is chosen after. Choose one for the whole session with the stack tool when the user asks.`]
1019
1045
  : []),
1020
1046
  // A stack: its file over the project's, for everything built while it is chosen.
1021
1047
  ...stacks.flatMap((stack) => [
@@ -1500,7 +1526,8 @@ function registerJobTools(pi, jobs, cwd) {
1500
1526
  });
1501
1527
  // The side panel: the regeneration - and the pulses behind p - over the whole screen, or beside the chat (s, from one to
1502
1528
  // the other). Its key (PANEL_KEY) opens it; the key again closes it and goes back to the chat, as esc does over the
1503
- // screen - beside the chat, esc gives the chat its keys back and the panel stays, and the key gives them to the panel.
1529
+ // screen - beside the chat, the key switches sides, giving the keys to the chat or the panel, as esc gives them to the
1530
+ // chat, the panel staying; ctrl+c closes it.
1504
1531
  // While the panel has the keys, every key is its own.
1505
1532
  let panel;
1506
1533
  // Beside the chat or over it: as the person last left it, for the conversation.
@@ -1508,7 +1535,7 @@ function registerJobTools(pi, jobs, cwd) {
1508
1535
  // The terminal the panel is drawn on, for its width: set as it opens.
1509
1536
  let panelTerminal;
1510
1537
  /** The columns the panel takes beside the chat - 0 over the whole screen, or when the terminal is too narrow to share. */
1511
- const panelBeside = () => (panelDocked && panelTerminal !== undefined ? dockedWidth(panelTerminal.columns) : 0);
1538
+ const panelBeside = () => (panelDocked && panelTerminal !== undefined ? dockedWidth(paddedColumns(panelTerminal.columns)) : 0);
1512
1539
  // What is open in the pulse list and the map, and where their cursors are: the conversation's, as left each time it opens.
1513
1540
  const jobsTree = createJobsTreeState();
1514
1541
  const mapState = createSpecMapState();
@@ -1566,7 +1593,7 @@ function registerJobTools(pi, jobs, cwd) {
1566
1593
  const layOut = () => {
1567
1594
  if (panelDocked && undock === undefined) {
1568
1595
  const widen = narrowChat(tui, panelBeside);
1569
- const unwatch = clickToFocus(tui, () => tui.terminal.columns - panelBeside(), (side) => {
1596
+ const unwatch = clickToFocus(tui, () => tui.terminal.columns - SCREEN_PADDING.right - panelBeside(), (side) => {
1570
1597
  if (panelBeside() === 0)
1571
1598
  return;
1572
1599
  if (side === "chat")
@@ -1607,7 +1634,7 @@ function registerJobTools(pi, jobs, cwd) {
1607
1634
  tree: jobsTree,
1608
1635
  map: () => specMap.read(),
1609
1636
  mapState,
1610
- rows: () => tui.terminal.rows,
1637
+ rows: () => paddedRows(tui.terminal.rows),
1611
1638
  key: PANEL_KEY,
1612
1639
  dock: {
1613
1640
  docked: () => panelBeside() > 0,
@@ -1628,8 +1655,8 @@ function registerJobTools(pi, jobs, cwd) {
1628
1655
  removed: (text) => theme.fg("toolDiffRemoved", text),
1629
1656
  // A spec diff's ***plain highlighted as plyn highlights it, and its added and removed lines filled.
1630
1657
  diff: plainPaint(theme),
1631
- // What conformance tests show does what its spec says: brand blue.
1632
- conforms: brandBlue(theme),
1658
+ // What conformance tests show does what its spec says: neon.
1659
+ conforms: brandNeon(theme),
1633
1660
  },
1634
1661
  back,
1635
1662
  close,
@@ -1671,17 +1698,6 @@ function registerJobTools(pi, jobs, cwd) {
1671
1698
  }
1672
1699
  if (!state)
1673
1700
  return undefined;
1674
- // Beside the chat, ⌥⌘← and ⌥⌘→ go to the side they point at, whichever has the keys - on a terminal that tells
1675
- // turri of ⌘ (the Kitty keyboard protocol). Its release goes nowhere.
1676
- if (panelBeside() > 0 && (matchesKey(data, SIDE_KEYS.chat) || matchesKey(data, SIDE_KEYS.panel))) {
1677
- if (!isKeyRelease(data)) {
1678
- if (matchesKey(data, SIDE_KEYS.chat))
1679
- toChat();
1680
- else
1681
- toPanel();
1682
- }
1683
- return { consume: true };
1684
- }
1685
1701
  // Beside the chat, with the chat's keys: they are the chat's - PANEL_KEY among them, for the shortcut that gives
1686
1702
  // them to the panel. A click on the panel gives it the focus, and the keys with it.
1687
1703
  if (!entry.keys && entry.handle?.isFocused() !== true)
@@ -1692,9 +1708,13 @@ function registerJobTools(pi, jobs, cwd) {
1692
1708
  // presses count - and ctrl+h held down, repeating, does not open and close it over and over.
1693
1709
  if (isKeyRelease(data) || (isKeyRepeat(data) && matchesKey(data, PANEL_KEY)))
1694
1710
  return { consume: true };
1695
- // ctrl+c closes it too: whatever else goes wrong, a key always leads back to the chat.
1696
- if (matchesKey(data, PANEL_KEY) || matchesKey(data, "ctrl+c"))
1711
+ // PANEL_KEY goes back: beside the chat, to the chat, the panel staying - the switch between the sides, as it is
1712
+ // from the chat to the panel - and over the screen, closing it. ctrl+c closes it either way: whatever else goes
1713
+ // wrong, a key always leads back to the chat.
1714
+ if (matchesKey(data, "ctrl+c"))
1697
1715
  close();
1716
+ else if (matchesKey(data, PANEL_KEY))
1717
+ back();
1698
1718
  else {
1699
1719
  try {
1700
1720
  view.handleInput(data);
@@ -1724,8 +1744,10 @@ function registerJobTools(pi, jobs, cwd) {
1724
1744
  handleInput: (data) => {
1725
1745
  if (isKeyRepeat(data) && matchesKey(data, PANEL_KEY))
1726
1746
  return;
1727
- if (matchesKey(data, PANEL_KEY))
1747
+ if (matchesKey(data, "ctrl+c"))
1728
1748
  close();
1749
+ else if (matchesKey(data, PANEL_KEY))
1750
+ back();
1729
1751
  else
1730
1752
  view.handleInput(data);
1731
1753
  tui.requestRender();
@@ -1743,14 +1765,15 @@ function registerJobTools(pi, jobs, cwd) {
1743
1765
  return component;
1744
1766
  }, {
1745
1767
  overlay: true,
1746
- // The whole screen, or the columns on the right beside the chat - read at each frame, so it follows the terminal.
1768
+ // The whole screen, or the columns on the right beside the chat - read at each frame, so it follows the terminal -
1769
+ // inside the screen's padding.
1747
1770
  overlayOptions: {
1748
1771
  get width() {
1749
1772
  return panelBeside() > 0 ? panelBeside() : "100%";
1750
1773
  },
1751
1774
  maxHeight: "100%",
1752
1775
  anchor: "top-right",
1753
- margin: 0,
1776
+ margin: { ...SCREEN_PADDING },
1754
1777
  },
1755
1778
  onHandle: (handle) => {
1756
1779
  if (panel !== undefined)
@@ -1860,8 +1883,6 @@ export const JOB_FINISHED_REACTION = [
1860
1883
  " to do, and that \"resume\" picks it up once it is in.",
1861
1884
  "- a credential `agent-env:` names that is not there: never make one up - tell the person where it is read from.",
1862
1885
  ].join("\n");
1863
- /** The keys that go to the chat and to the panel, side by side: ⌥⌘← and ⌥⌘→. */
1864
- const SIDE_KEYS = { chat: "alt+super+left", panel: "alt+super+right" };
1865
1886
  /** Opens the side panel, and goes back to it from the conversation. */
1866
1887
  /** The modules in this project that have a history, by name. */
1867
1888
  function modulesWithHistory(cwd) {
@@ -2284,7 +2305,7 @@ const BANNER_ENTRY = "turri_banner";
2284
2305
  export function chatRulesBlock(rules = loadRules(join(PACKAGE_ROOT, "genome", "rules"))) {
2285
2306
  return renderRulesBlock(rules);
2286
2307
  }
2287
- function registerBranding(pi, ruleCount, skills) {
2308
+ function registerBranding(pi, skills) {
2288
2309
  pi.registerCommand("stdlib", {
2289
2310
  description: "List the ***plain standard library: the stacks a spec can import instead of writing its implementation reqs.",
2290
2311
  handler: async (_args, ctx) => {
@@ -2297,40 +2318,31 @@ function registerBranding(pi, ruleCount, skills) {
2297
2318
  if (data === undefined)
2298
2319
  return undefined;
2299
2320
  const lines = [
2300
- `${theme.fg("accent", "turri")} ${theme.fg("muted", `v${data.version}`)}` +
2321
+ `${brandPink(theme)("turri")} ${theme.fg("muted", `v${data.version}`)}` +
2301
2322
  (data.commit !== undefined ? theme.fg("dim", ` from a clone, ${data.commit}${data.changed ? " with changes" : ""}`) : ""),
2302
- theme.fg("text", "regenerative software - the code is regenerated from the spec"),
2303
- theme.fg("muted", `${data.rules} rules · ${data.skills} skill${data.skills === 1 ? "" : "s"}` +
2304
- (data.projectSkills !== undefined && data.projectSkills > 0 ? ` (${data.projectSkills} the project's)` : "") +
2305
- (data.stdlib !== undefined && data.stdlib > 0 ? ` · ${data.stdlib} stdlib stacks (/stdlib)` : "") +
2306
- (data.pending > 0 ? theme.fg("warning", ` (${data.pending} failed to load)`) : "")),
2323
+ theme.fg("text", "the first coding agent with the power of regenerative software"),
2307
2324
  // Where the settings are changed, without leaving the chat.
2308
2325
  theme.fg("muted", "configure with /turri-config"),
2309
- theme.fg("dim", data.cwd),
2310
- // Where every command an agent runs will run - or that none can.
2311
- ...(data.sandbox
2312
- ? [theme.fg(data.sandbox.level === "ok" ? "dim" : data.sandbox.level === "limited" ? "warning" : "error", data.sandbox.text)]
2313
- : []),
2326
+ // Only what is wrong: skills that did not load, and agents' commands less fenced than they should be.
2327
+ ...(data.pending > 0 ? [theme.fg("warning", `${data.pending} skill${data.pending === 1 ? "" : "s"} failed to load`)] : []),
2328
+ ...(data.sandbox !== undefined && data.sandbox.level !== "ok" ? [theme.fg(data.sandbox.level === "limited" ? "warning" : "error", data.sandbox.text)] : []),
2314
2329
  ];
2315
2330
  // The jellyfish turri is named after, in ink for the theme's background: a picture where the
2316
2331
  // terminal shows images, drawn in text where it does not.
2317
- return new Banner(lines, (rule) => theme.fg("borderMuted", rule), jellyfishArt(theme.appearance, (art) => theme.fg("text", art)));
2332
+ return new Banner(lines, (rule) => theme.fg("borderMuted", rule), jellyfishArt(theme.appearance, (theme.getColorMode?.() ?? "truecolor") === "truecolor"));
2318
2333
  });
2319
2334
  pi.on("session_start", (_event, ctx) => {
2320
- if (ctx.hasUI)
2335
+ if (ctx.hasUI) {
2336
+ padTheScreen(ctx);
2321
2337
  animateJellyfish(ctx);
2338
+ }
2322
2339
  try {
2323
2340
  const identity = turriIdentity();
2324
2341
  pi.appendEntry(BANNER_ENTRY, {
2325
2342
  version: identity.version ?? "0.0.0",
2326
2343
  commit: identity.git_commit?.slice(0, 7),
2327
2344
  changed: identity.git_dirty === true,
2328
- rules: ruleCount,
2329
- skills: skills.skills.length,
2330
- projectSkills: skills.skills.filter((skill) => skill.origin !== "turri").length,
2331
2345
  pending: skills.pending.length,
2332
- stdlib: stdlibStackCount(),
2333
- cwd: ctx.cwd,
2334
2346
  sandbox: sandboxSummary(),
2335
2347
  });
2336
2348
  }
@@ -2339,10 +2351,8 @@ function registerBranding(pi, ruleCount, skills) {
2339
2351
  }
2340
2352
  });
2341
2353
  }
2342
- /** How many rows the banner's jellyfish takes: taller than the lines beside it. */
2343
- const JELLYFISH_ROWS = 13;
2344
- /** How long each frame of the jellyfish shows. */
2345
- const JELLYFISH_FRAME_MS = 250;
2354
+ /** How many rows the banner's jellyfish takes at most: as tall as the one drawn in text. */
2355
+ const JELLYFISH_ROWS = 10;
2346
2356
  /** Which frame of the jellyfish every banner shows, moved on by `animateJellyfish`. */
2347
2357
  let jellyfishFrame = 0;
2348
2358
  let jellyfishTimer;
@@ -2377,6 +2387,24 @@ function animateJellyfish(ctx) {
2377
2387
  }, { placement: "belowEditor" });
2378
2388
  }
2379
2389
  const JELLYFISH_CLOCK_KEY = "turri-jellyfish";
2390
+ /**
2391
+ * The whole screen inside its padding (`screen-padding.ts`). An extension is given pi's screen only through what it
2392
+ * draws, so an empty widget below the editor hands it over - at each frame, as pi starts a new screen when it changes
2393
+ * how it draws.
2394
+ */
2395
+ function padTheScreen(ctx) {
2396
+ ctx.ui.setWidget(SCREEN_PADDING_KEY, (tui) => {
2397
+ padScreen(tui);
2398
+ return {
2399
+ render: () => {
2400
+ padScreen(tui);
2401
+ return [];
2402
+ },
2403
+ invalidate: () => { },
2404
+ };
2405
+ }, { placement: "belowEditor" });
2406
+ }
2407
+ const SCREEN_PADDING_KEY = "turri-padding";
2380
2408
  /** The columns the banner's picture takes from the left edge, while it shows one; 0 when it does not. */
2381
2409
  let pictureRight = 0;
2382
2410
  /**
@@ -2435,7 +2463,7 @@ class Banner {
2435
2463
  return line !== undefined ? `${before}${line}` : placed?.kind === "text" ? ` ${jellyfish}` : "";
2436
2464
  });
2437
2465
  // The side panel is drawn beside the picture (drawOverlaysBesidePicture), which goes on showing.
2438
- pictureRight = placed?.kind === "image" ? 1 + placed.columns : 0;
2466
+ pictureRight = placed?.kind === "image" ? SCREEN_PADDING.left + 1 + placed.columns : 0;
2439
2467
  if (placed?.kind === "image") {
2440
2468
  const at = placed.onLastRow ? placed.rows - 1 : 0;
2441
2469
  rows[at] = `${rows[at]}${PICTURE_AT}${placed.sequence}`;
@@ -2462,24 +2490,16 @@ class Banner {
2462
2490
  }
2463
2491
  const jellyfishFrameCache = new Map();
2464
2492
  /**
2465
- * The banner's jellyfish for this terminal: where it shows images, the picture's frames spread evenly
2466
- * over the video, light ink for a dark background and dark ink for a light one; where it does not, the
2467
- * jellyfish drawn in text, in `ink`. None when they cannot be read.
2493
+ * The banner's jellyfish for this terminal: where it shows images, the picture's frames
2494
+ * (scripts/jellyfish-frames.mjs writes them), in the colours for the theme's background; where it does not, the
2495
+ * small one in half blocks. None when the pictures cannot be read.
2468
2496
  */
2469
- function jellyfishArt(appearance, ink) {
2497
+ function jellyfishArt(appearance, truecolor) {
2470
2498
  const dir = join(PACKAGE_ROOT, "assets", "jellyfish");
2471
2499
  try {
2472
2500
  if (getCapabilities().images === null) {
2473
- // Its frames one after another, each ended by a line of "---" (scripts/jellyfish-frames.mjs writes them).
2474
- const drawn = readFileSync(join(dir, "fallback.txt"), "utf8")
2475
- .split(/^---$/m)
2476
- .map((frame) => frame.replace(/^\n+|\n+$/g, "").split("\n"))
2477
- .filter((frame) => frame.some((line) => line.trim() !== ""));
2478
- if (drawn.length === 0)
2479
- return undefined;
2480
- const columns = Math.max(...drawn.flat().map((line) => visibleWidth(line)));
2481
- const rows = Math.max(...drawn.map((frame) => frame.length));
2482
- return { kind: "text", frames: drawn.map((frame) => frame.map((line) => ink(line.padEnd(columns)))), columns, rows };
2501
+ const frames = JELLYFISH_TEXT_FRAMES.map((pixels) => halfBlocks(pixels, appearance, truecolor));
2502
+ return { kind: "text", frames, columns: JELLYFISH_TEXT_FRAMES[0][0].length, rows: frames[0].length };
2483
2503
  }
2484
2504
  let frames = jellyfishFrameCache.get(appearance);
2485
2505
  if (frames === undefined) {
@@ -110,6 +110,7 @@ export function withModels(text, lines) {
110
110
  export const DEFAULTS = {
111
111
  conformanceTests: false,
112
112
  review: false,
113
+ mutationTesting: true,
113
114
  allowToStopJobs: true,
114
115
  releaseAfterPulse: false,
115
116
  testingScriptsRunOn: "in-place",
@@ -143,6 +144,10 @@ export function quickStartConfig(vendor = "other", bedrock) {
143
144
  "# Whether the code and its tests are reviewed once they pass - two reviews at most, the second weighed by the arbiter.",
144
145
  `review: ${DEFAULTS.review}`,
145
146
  "",
147
+ "# Whether the coding agent and the reviewer can put a bug in the code for one run of the unit tests (can_it_fail),",
148
+ "# to see that a test catches it - on a copy of the code, or in place and put back after.",
149
+ `mutation-testing: ${DEFAULTS.mutationTesting}`,
150
+ "",
146
151
  "# Whether turri stops a running pulse the spec has moved on from. false: it lands, the change regenerated on top.",
147
152
  `allow-to-stop-jobs: ${DEFAULTS.allowToStopJobs}`,
148
153
  "",