@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.
- 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/cli-options.js +12 -6
- package/dist/config-edit.js +1 -0
- package/dist/config.js +22 -2
- package/dist/cultivate-cli.js +16 -23
- package/dist/extension.js +128 -108
- package/dist/init-config.js +5 -0
- package/dist/jellyfish.js +130 -0
- package/dist/loop-cli.js +16 -8
- package/dist/loop-tools.js +60 -21
- package/dist/module-report.js +5 -4
- package/dist/pi.js +76 -19
- package/dist/picture-row.js +3 -2
- package/dist/regenerate/activity.js +21 -0
- package/dist/regenerate/jobs.js +10 -5
- package/dist/regenerate/list-arguments.js +121 -0
- package/dist/regenerate/loop.js +66 -43
- package/dist/regenerate/panel-dock.js +7 -5
- package/dist/regenerate/plain-palette.js +52 -37
- package/dist/regenerate/regenerate.js +4 -2
- package/dist/regenerate/self-check.js +13 -6
- package/dist/regenerate/side-panel.js +1 -1
- package/dist/regenerate/spec-map-view.js +9 -5
- package/dist/regenerate/test-scripts.js +79 -13
- package/dist/regenerate/unit-review.js +1 -1
- package/dist/regenerate-cli.js +11 -5
- package/dist/release-cli.js +3 -8
- package/dist/review/live.js +2 -1
- package/dist/run-report.js +26 -1
- package/dist/screen-padding.js +72 -0
- package/dist/stacks.js +3 -5
- package/dist/validate-cli.js +3 -8
- package/docs/README.md +5 -12
- package/docs/authoring-plain.md +37 -119
- package/docs/config.md +55 -178
- package/docs/deploy.md +21 -80
- package/docs/development.md +14 -41
- package/docs/headless.md +40 -129
- 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 -942
- package/docs/sandbox.md +38 -152
- package/docs/skills-and-gates.md +50 -173
- package/docs/theming.md +45 -46
- package/docs/using.md +68 -344
- 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/func-specs.md +1 -0
- package/genome/rules/integration-embedded-testing.md +1 -4
- package/genome/rules/integrations.md +1 -2
- package/genome/rules/project-model.md +14 -35
- package/genome/rules/stdlib.md +14 -49
- package/genome/rules/turri-regeneration.md +48 -0
- 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 +4 -7
- package/genome/skills/implement-unit-testing-script/SKILL.md +2 -3
- package/genome/skills/init-plain-project/SKILL.md +1 -1
- package/genome/skills/mature/SKILL.md +21 -69
- package/genome/skills/refactor-module/SKILL.md +24 -2
- 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
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";
|
|
@@ -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 {
|
|
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
|
|
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,
|
|
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}
|
|
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
|
-
|
|
679
|
-
|
|
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
|
-
?
|
|
682
|
-
:
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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"}.
|
|
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,
|
|
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:
|
|
1632
|
-
conforms:
|
|
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
|
-
//
|
|
1696
|
-
|
|
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,
|
|
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:
|
|
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,
|
|
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
|
|
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", "
|
|
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
|
-
|
|
2310
|
-
|
|
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, (
|
|
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:
|
|
2343
|
-
const JELLYFISH_ROWS =
|
|
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
|
|
2466
|
-
*
|
|
2467
|
-
*
|
|
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,
|
|
2497
|
+
function jellyfishArt(appearance, truecolor) {
|
|
2470
2498
|
const dir = join(PACKAGE_ROOT, "assets", "jellyfish");
|
|
2471
2499
|
try {
|
|
2472
2500
|
if (getCapabilities().images === null) {
|
|
2473
|
-
|
|
2474
|
-
|
|
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) {
|
package/dist/init-config.js
CHANGED
|
@@ -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
|
"",
|