@turri-dev/cli 0.1.4 → 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/README.md +1 -1
- package/dist/config.js +17 -0
- package/dist/extension.js +44 -37
- package/dist/loop-tools.js +52 -19
- package/dist/regenerate/jobs.js +10 -5
- package/dist/regenerate/loop.js +57 -38
- package/dist/regenerate/test-scripts.js +25 -3
- package/dist/stacks.js +3 -5
- package/docs/config.md +4 -2
- package/docs/regenerating.md +2 -2
- package/docs/using.md +2 -2
- package/genome/skills/brood/SKILL.md +1 -1
- package/genome/skills/brood/references/testing.md +1 -1
- package/genome/skills/implement-conformance-testing-script/SKILL.md +1 -1
- package/genome/skills/implement-unit-testing-script/SKILL.md +1 -1
- package/genome/skills/init-plain-project/SKILL.md +1 -1
- package/genome/skills/spawn/SKILL.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,7 +160,7 @@ It runs the interview skill, asks you about the problem, the stack and the produ
|
|
|
160
160
|
- **`/halt`** halts a pulse, **`/lineage`** shows every generation of a module, and the **`drift`** tool says what is left to regenerate;
|
|
161
161
|
- **`/btw <question>`** asks turri something on the side - answered at once, even in the middle of a skill's interview (typed as the answer, under "Something else…"), and left out of the conversation;
|
|
162
162
|
- **`/turri-config`** changes any setting of the project's `config.turri.yaml` - the models, review, conformance tests, how many pulses at once - and says when it takes effect: now, at your next message, or at the next pulse. No restart: `/turri-config review false`, or `/turri-config` alone to pick from the list;
|
|
163
|
-
- **`/stack`** chooses the stack to build with, in a project whose backend and frontend each have their own testing scripts in `config.turri.<stack>.yaml` - `/stack backend`, `/stack none` - for the session (`--stack <name>` on the command line).
|
|
163
|
+
- **`/stack`** chooses the stack to build with, in a project whose backend and frontend each have their own testing scripts in `config.turri.<stack>.yaml` - `/stack backend`, `/stack none` - for the session (`--stack <name>` on the command line). The chat's model builds each part with its own stack by itself, pinned to that module's pulses.
|
|
164
164
|
|
|
165
165
|
Other things to ask for:
|
|
166
166
|
|
package/dist/config.js
CHANGED
|
@@ -157,6 +157,23 @@ export function chooseStack(stack) {
|
|
|
157
157
|
export function withStack(stack, work) {
|
|
158
158
|
return pinnedStack.run({ stack }, work);
|
|
159
159
|
}
|
|
160
|
+
/** The words that choose no stack: the project's config alone. */
|
|
161
|
+
export const NO_STACK = ["none", "off", "base", "-"];
|
|
162
|
+
/**
|
|
163
|
+
* The stack `name` names for the project at `root` - undefined for none - refused when the project has no file for
|
|
164
|
+
* it, with the stacks it has.
|
|
165
|
+
*/
|
|
166
|
+
export function stackNamed(root, name) {
|
|
167
|
+
const wanted = name.trim();
|
|
168
|
+
if (wanted === "" || NO_STACK.includes(wanted.toLowerCase()))
|
|
169
|
+
return undefined;
|
|
170
|
+
const stacks = stacksOf(root);
|
|
171
|
+
if (stacks.includes(wanted))
|
|
172
|
+
return wanted;
|
|
173
|
+
throw new Error(stacks.length > 0
|
|
174
|
+
? `There is no stack ${wanted} in ${root}. Its stacks: ${stacks.join(", ")} - or none.`
|
|
175
|
+
: `${root} has no stacks: a stack is a ${stackConfigFile("<stack>")} beside config.turri.yaml.`);
|
|
176
|
+
}
|
|
160
177
|
/** The stack a project folder is built with now: the one chosen, when the folder has a file for it. */
|
|
161
178
|
export function stackOf(root) {
|
|
162
179
|
const stack = chosenStack();
|
package/dist/extension.js
CHANGED
|
@@ -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, asksWholeChain, 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";
|
|
@@ -694,11 +694,12 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
694
694
|
: `Planning ${module}'s pulses…`, "info");
|
|
695
695
|
await nextFrame();
|
|
696
696
|
try {
|
|
697
|
-
|
|
698
|
-
|
|
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 })
|
|
699
700
|
: again.length > 0
|
|
700
|
-
?
|
|
701
|
-
:
|
|
701
|
+
? loop.regenerateAgain(plainFile, again, request)
|
|
702
|
+
: loop.bringUp(plainFile, request));
|
|
702
703
|
// Nothing to do: say how to build it again anyway.
|
|
703
704
|
const hint = statuses.length > 0 && statuses.every(isUpToDate)
|
|
704
705
|
? `\nTo regenerate parts of it anyway, name their functionalities - /regenerate ${basename(plainFile)} 2.1 - or start over with /regenerate ${basename(plainFile)} from scratch.`
|
|
@@ -817,21 +818,24 @@ function conformTool(options) {
|
|
|
817
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",
|
|
818
819
|
})),
|
|
819
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,
|
|
820
822
|
model: MODEL_PARAM,
|
|
821
823
|
wait: WAIT_PARAM,
|
|
822
824
|
}),
|
|
823
825
|
async execute(_id, given, _signal, _onUpdate, ctx) {
|
|
824
826
|
const params = fromWorkingDir(cwd, given);
|
|
825
827
|
const plainFile = resolve(params.base, params.plain_file);
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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));
|
|
833
838
|
});
|
|
834
|
-
return loopReply(loop, statuses, options.wait(ctx, params.wait));
|
|
835
839
|
},
|
|
836
840
|
});
|
|
837
841
|
}
|
|
@@ -849,34 +853,37 @@ function registerGlimpse(pi, cwd, jobs) {
|
|
|
849
853
|
parameters: Type.Object({
|
|
850
854
|
plain_file: Type.String({ description: "Path to the .plain file" }),
|
|
851
855
|
working_dir: WORKING_DIR_PARAM,
|
|
856
|
+
stack: STACK_PARAM,
|
|
852
857
|
}),
|
|
853
858
|
async execute(_id, given, _signal, _onUpdate, ctx) {
|
|
854
859
|
const params = fromWorkingDir(cwd, given);
|
|
855
860
|
const plainFile = resolve(params.base, params.plain_file);
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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")),
|
|
871
882
|
"",
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
"",
|
|
877
|
-
"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.",
|
|
878
|
-
].join("\n");
|
|
879
|
-
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
|
+
});
|
|
880
887
|
},
|
|
881
888
|
});
|
|
882
889
|
}
|
|
@@ -1034,7 +1041,7 @@ export function projectConfigBlock(root) {
|
|
|
1034
1041
|
return [
|
|
1035
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"),
|
|
1036
1043
|
...(stacks.length > 0
|
|
1037
|
-
? ["", `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.`]
|
|
1038
1045
|
: []),
|
|
1039
1046
|
// A stack: its file over the project's, for everything built while it is chosen.
|
|
1040
1047
|
...stacks.flatMap((stack) => [
|
package/dist/loop-tools.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { relative, resolve } from "node:path";
|
|
2
|
+
import { basename, relative, resolve } from "node:path";
|
|
3
3
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
-
import { CONFIG_FILE, readProjectConfig } from "./config.js";
|
|
5
|
+
import { chosenStack, CONFIG_FILE, readProjectConfig, stackConfigFile, stackNamed, stackOf, stacksOf, withStack } from "./config.js";
|
|
6
6
|
import { checkPlainFile } from "./plain/check.js";
|
|
7
7
|
import { describeDrift, describeModules } from "./regenerate/loop.js";
|
|
8
8
|
import { pulseName } from "./regenerate/jobs.js";
|
|
9
9
|
import { stopDeployed } from "./regenerate/release.js";
|
|
10
|
+
import { projectDirOf } from "./regenerate/paths.js";
|
|
11
|
+
import { findTestScripts } from "./regenerate/test-scripts.js";
|
|
10
12
|
/** How the queue is to be planned, for the work this call queues - see regenerate/steer.ts. */
|
|
11
13
|
export const GUIDANCE_PARAM = Type.Optional(Type.String({
|
|
12
14
|
description: "How to plan the queue for this work, in a sentence or two - which functional specs go together, what first - as " +
|
|
@@ -23,6 +25,33 @@ export const MODEL_PARAM = Type.Optional(Type.String({
|
|
|
23
25
|
"Omitted: the model chosen for this kind of work with engine, else the conversation's. " +
|
|
24
26
|
"To change it for every later render, use engine instead.",
|
|
25
27
|
}));
|
|
28
|
+
export const STACK_PARAM = Type.Optional(Type.String({
|
|
29
|
+
description: "In a project of stacks: the stack that builds this module - its config.turri.<stack>.yaml over config.turri.yaml, " +
|
|
30
|
+
"its testing scripts, toolchains and models - for every pulse of it, whatever stack is chosen later. \"none\": " +
|
|
31
|
+
"config.turri.yaml alone. Omitted: the stack chosen now.",
|
|
32
|
+
}));
|
|
33
|
+
/**
|
|
34
|
+
* `work` with the stack a call names - or, none named, the one chosen now - chosen for it: what it asks for is built with
|
|
35
|
+
* that stack. A project of stacks that would be built with none, when config.turri.yaml alone names no unit testing
|
|
36
|
+
* script, is refused before anything is planned: its pulses would all stop for want of one.
|
|
37
|
+
*/
|
|
38
|
+
export function withCallStack(plainFile, named, work) {
|
|
39
|
+
const root = projectDirOf(plainFile);
|
|
40
|
+
const stack = named !== undefined ? stackNamed(root, named) : chosenStack();
|
|
41
|
+
return withStack(stack, () => {
|
|
42
|
+
const stacks = stacksOf(root);
|
|
43
|
+
if (named === undefined && stackOf(root) === undefined && stacks.length > 0 && findTestScripts(root).unit === undefined) {
|
|
44
|
+
const scripts = stacks.map((each) => {
|
|
45
|
+
const unit = withStack(each, () => findTestScripts(root).unit);
|
|
46
|
+
return `${each}${unit !== undefined ? ` (${stackConfigFile(each)}: ${relative(root, unit)})` : ""}`;
|
|
47
|
+
});
|
|
48
|
+
throw new Error(`Nothing was started: ${basename(plainFile)} would be built with no stack - ${CONFIG_FILE} alone - and that names no unit testing ` +
|
|
49
|
+
`script, so every pulse would stop. The project's stacks: ${scripts.join(", ")}. Pass the stack that builds ` +
|
|
50
|
+
`${basename(plainFile)} as stack - or ask the user which, when the spec does not make it plain.`);
|
|
51
|
+
}
|
|
52
|
+
return work();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
26
55
|
export const WAIT_PARAM = Type.Optional(Type.Boolean({
|
|
27
56
|
description: "Block until the work finishes instead of returning a pulse id. Defaults to false in the " +
|
|
28
57
|
"interactive TUI and true elsewhere, where a pulse would not outlive the turn.",
|
|
@@ -135,6 +164,7 @@ export function regenerateTool(options) {
|
|
|
135
164
|
"A change to a file the spec links - an API surface, a mapping document - is a change to the spec: regenerate sees it by itself, and regenerates every functional spec that uses what links it.",
|
|
136
165
|
"Never stop pulses and start them again yourself to follow a spec change: regenerate does it.",
|
|
137
166
|
"Set from_scratch only when the user asks for the module to be regenerated from scratch - started over, its code deleted - in this conversation, or agrees to it when a module's status says it has to start over: never to fix a pulse that did not land.",
|
|
167
|
+
"In a project of stacks - a backend and a frontend, each with its config.turri.<stack>.yaml - pass stack: the one that builds this module's part, the backend's spec with the backend stack. It is pinned to the module's pulses: regenerate a backend and a frontend side by side, each with its own, without switching the stack chosen. What it says back names the stack each module is built with.",
|
|
138
168
|
'from_scratch "module" takes out the code where the module is built; the modules below keep theirs, and the module starts from a copy of the code below it. "chain" takes out every codebase of its requires chain, the base\'s too, and builds all of it again from the bottom: use it after modules were split, merged or moved (refactor-module), or when the user asks for the whole chain.',
|
|
139
169
|
],
|
|
140
170
|
parameters: Type.Object({
|
|
@@ -148,28 +178,31 @@ export function regenerateTool(options) {
|
|
|
148
178
|
description: "Only when the user asks for it: the module's code and conformance tests deleted - committed first, so its lineage keeps them - and the whole module regenerated from its spec. " +
|
|
149
179
|
'"chain": every module of its requires chain too, built again from the bottom',
|
|
150
180
|
})),
|
|
181
|
+
stack: STACK_PARAM,
|
|
151
182
|
model: MODEL_PARAM,
|
|
152
183
|
wait: WAIT_PARAM,
|
|
153
184
|
}),
|
|
154
185
|
async execute(_id, given, _signal, _onUpdate, ctx) {
|
|
155
186
|
const plainFile = plainFileOf(options.cwd, given, "regenerate");
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
return withCallStack(plainFile, given.stack, async () => {
|
|
188
|
+
const check = checkPlainFile(plainFile);
|
|
189
|
+
if (!check.ok)
|
|
190
|
+
throw new Error(`${relative(options.cwd, plainFile) || plainFile} does not parse, so nothing can be regenerated from it yet: ${check.error}`);
|
|
191
|
+
const again = given.functionalities ?? [];
|
|
192
|
+
if (again.length > 0 && given.from_scratch !== undefined)
|
|
193
|
+
throw new Error("Name functionalities or set from_scratch, not both: from scratch regenerates every one of them.");
|
|
194
|
+
// Named functionalities are work on functionalities, as engine chooses a model for it; the rest, spec changes.
|
|
195
|
+
const work = again.length > 0 ? "functionalities" : "spec_changes";
|
|
196
|
+
const model = options.model !== undefined ? options.model(ctx, plainFile, given.model, work) : given.model;
|
|
197
|
+
const loop = options.loop(ctx);
|
|
198
|
+
const request = { ...(model !== undefined ? { model } : {}), ...(given.guidance !== undefined ? { guidance: given.guidance } : {}) };
|
|
199
|
+
const statuses = given.from_scratch !== undefined
|
|
200
|
+
? await loop.fromScratch(plainFile, { ...request, chain: given.from_scratch === "chain" })
|
|
201
|
+
: again.length > 0
|
|
202
|
+
? await loop.regenerateAgain(plainFile, again, request)
|
|
203
|
+
: await loop.bringUp(plainFile, request);
|
|
204
|
+
return loopReply(loop, statuses, options.wait !== undefined ? options.wait(ctx, given.wait) : given.wait === true);
|
|
205
|
+
});
|
|
173
206
|
},
|
|
174
207
|
});
|
|
175
208
|
}
|
package/dist/regenerate/jobs.js
CHANGED
|
@@ -52,6 +52,9 @@ export function createJobRegistry(options) {
|
|
|
52
52
|
},
|
|
53
53
|
start(input) {
|
|
54
54
|
const id = randomUUID().slice(0, 8);
|
|
55
|
+
// The stack chosen as it is queued - or pinned for the work that queues it - not as it starts: a job that waits
|
|
56
|
+
// for a slot is built with the stack it was asked for, whatever is chosen meanwhile.
|
|
57
|
+
const stack = chosenStack();
|
|
55
58
|
const claimed = [...new Set(input.lanes ?? [])];
|
|
56
59
|
const blockers = [
|
|
57
60
|
...new Set(claimed
|
|
@@ -70,6 +73,7 @@ export function createJobRegistry(options) {
|
|
|
70
73
|
...(input.title !== undefined ? { title: input.title } : {}),
|
|
71
74
|
diff: input.diff,
|
|
72
75
|
model: input.model,
|
|
76
|
+
...(stack !== undefined ? { stack } : {}),
|
|
73
77
|
...(input.follows !== undefined ? { follows: input.follows } : {}),
|
|
74
78
|
status: blockers.length > 0 || (limit !== undefined && active >= limit) ? "queued" : "running",
|
|
75
79
|
sequence: (sequence += 1),
|
|
@@ -273,9 +277,9 @@ export function createJobRegistry(options) {
|
|
|
273
277
|
log = startJobLog(job, claimed);
|
|
274
278
|
if (log !== undefined)
|
|
275
279
|
job.logFile = log.file;
|
|
276
|
-
// What the job's agents start runs until it ends, and no longer (withPulse) - with the stack
|
|
277
|
-
//
|
|
278
|
-
return withStack(
|
|
280
|
+
// What the job's agents start runs until it ends, and no longer (withPulse) - with the stack it was queued
|
|
281
|
+
// with, whatever is chosen while it waits or runs.
|
|
282
|
+
return withStack(stack, () => (guarded ? runGuarded() : runOnce()));
|
|
279
283
|
})
|
|
280
284
|
.then((outcome) => {
|
|
281
285
|
// Asked to stop, it is stopped - whether its work threw as it unwound or returned what it had.
|
|
@@ -385,10 +389,11 @@ export function pulseName(job) {
|
|
|
385
389
|
}
|
|
386
390
|
/** What the agent is told when it checks on a job. */
|
|
387
391
|
export function describeJob(job) {
|
|
392
|
+
const name = `${pulseName(job)}${job.stack !== undefined ? ` (stack ${job.stack})` : ""}`;
|
|
388
393
|
if (job.status === "queued") {
|
|
389
|
-
return `${
|
|
394
|
+
return `${name}: queued behind ${job.waitingOn.map((id) => `pulse ${id}`).join(" and ")}. It starts when that finishes.`;
|
|
390
395
|
}
|
|
391
|
-
const header = `${
|
|
396
|
+
const header = `${name}: ${job.status}, ${formatDuration(job)}`;
|
|
392
397
|
if (job.status === "running") {
|
|
393
398
|
return [
|
|
394
399
|
header,
|
package/dist/regenerate/loop.js
CHANGED
|
@@ -13,7 +13,7 @@ import { adoptDigests, chainTree, codeTree, conformanceTarget, REGENERATE_AGAIN,
|
|
|
13
13
|
import { describeSpecDiffConformance, renderSpecDiffConformance } from "./conform.js";
|
|
14
14
|
import { conformanceDirFor } from "./conformance.js";
|
|
15
15
|
import { modulesRoot } from "./paths.js";
|
|
16
|
-
import { budgetFor, choosingWhen, configForSpec, conformanceSwitchedOn } from "../config.js";
|
|
16
|
+
import { budgetFor, chosenStack, choosingWhen, configForSpec, conformanceSwitchedOn, withStack } from "../config.js";
|
|
17
17
|
import { describeSpecChange } from "./prompt.js";
|
|
18
18
|
import { resolveTemplateDirs } from "./paths.js";
|
|
19
19
|
import { builtTips, copyRequiredCode, importingModules, isImportModule, newlyRequired, resolveRequiresChain, startFromScratch } from "./requires.js";
|
|
@@ -189,6 +189,8 @@ export function createWorkLoop(options) {
|
|
|
189
189
|
// One tick at a time: each reads the records and the spec, and decides.
|
|
190
190
|
let ticking = Promise.resolve();
|
|
191
191
|
const tipEntry = (state) => tipOf(state.chain);
|
|
192
|
+
/** `work` with the codebase's stack chosen - the one it was asked for with - when it has one pinned. */
|
|
193
|
+
const inStack = (state, work) => (state.stack !== undefined ? withStack(state.stack.name, work) : work());
|
|
192
194
|
const keyOf = (spec) => serializeTree(codeTree(spec.tree));
|
|
193
195
|
/** What a codebase is built from: its record, or - nothing built - what it starts from, the code of the module it requires. */
|
|
194
196
|
const builtOf = (state) => {
|
|
@@ -811,7 +813,7 @@ export function createWorkLoop(options) {
|
|
|
811
813
|
// A codebase left behind - built on since - is not looked at, unless a job of it is still running; what it had
|
|
812
814
|
// planned, or waited for, is no longer shown.
|
|
813
815
|
if (now.has(state) || state.running !== undefined)
|
|
814
|
-
await look(state);
|
|
816
|
+
await inStack(state, () => look(state));
|
|
815
817
|
else {
|
|
816
818
|
state.why = "left behind: the code is built on further up the chain, where changes to it are made";
|
|
817
819
|
state.waitsFor = undefined;
|
|
@@ -963,7 +965,8 @@ export function createWorkLoop(options) {
|
|
|
963
965
|
};
|
|
964
966
|
const statusOf = (state) => describe(state).status;
|
|
965
967
|
/** A codebase's status, and beside each job it lists as planned, the planned job it is - none for one being planned or scheduled. */
|
|
966
|
-
const describe = (state) =>
|
|
968
|
+
const describe = (state) => inStack(state, () => describeIn(state));
|
|
969
|
+
const describeIn = (state) => {
|
|
967
970
|
// What runs next: the code's jobs, then the conformance tests' - the module's, once its code is built - the running job first.
|
|
968
971
|
const running = state.running?.planned;
|
|
969
972
|
const own = (jobs) => (jobs ?? []).filter((job) => job !== running);
|
|
@@ -1011,6 +1014,7 @@ export function createWorkLoop(options) {
|
|
|
1011
1014
|
const status = {
|
|
1012
1015
|
module: tip.moduleName,
|
|
1013
1016
|
plainFile: tip.plainFile,
|
|
1017
|
+
...(state.stack?.name !== undefined ? { stack: state.stack.name } : {}),
|
|
1014
1018
|
...(state.running !== undefined
|
|
1015
1019
|
? {
|
|
1016
1020
|
running: {
|
|
@@ -1027,8 +1031,11 @@ export function createWorkLoop(options) {
|
|
|
1027
1031
|
};
|
|
1028
1032
|
return { status, jobs: (state.running !== undefined ? listed.slice(1) : listed).map((item) => item.job), scheduled: (state.running !== undefined ? listed.slice(1) : listed).map((item) => (item.entry.waitsFor !== undefined ? { title: item.entry.title, specs: item.entry.cases.add.map((touched) => touched.text) } : undefined)) };
|
|
1029
1033
|
};
|
|
1030
|
-
/**
|
|
1031
|
-
|
|
1034
|
+
/**
|
|
1035
|
+
* Look after a codebase - its chain up to it - from now on; says its state. `pin`: the stack it is built with from now
|
|
1036
|
+
* on - kept, when `keep` and it has one already.
|
|
1037
|
+
*/
|
|
1038
|
+
const track = (chain, askedFile, request, pin) => {
|
|
1032
1039
|
const tip = tipOf(chain);
|
|
1033
1040
|
let state = modules.get(tip.buildDir);
|
|
1034
1041
|
if (state === undefined) {
|
|
@@ -1043,7 +1050,10 @@ export function createWorkLoop(options) {
|
|
|
1043
1050
|
}
|
|
1044
1051
|
state.chain = chain;
|
|
1045
1052
|
state.askedFile = askedFile;
|
|
1046
|
-
|
|
1053
|
+
if (pin !== undefined && (pin.keep !== true || state.stack === undefined))
|
|
1054
|
+
state.stack = { name: pin.name };
|
|
1055
|
+
// Its model as its stack has it: a stack's file can name a coding model of its own.
|
|
1056
|
+
const model = request.model ?? inStack(state, () => options.modelFor?.(tip.plainFile));
|
|
1047
1057
|
if (model !== undefined)
|
|
1048
1058
|
state.model = model;
|
|
1049
1059
|
if (request.guidance !== undefined && !state.guidance.includes(request.guidance))
|
|
@@ -1056,7 +1066,10 @@ export function createWorkLoop(options) {
|
|
|
1056
1066
|
const files = codebasesOf(resolve(options.cwd, plainFile));
|
|
1057
1067
|
for (const file of files)
|
|
1058
1068
|
asked.set(file, { ...asked.get(file), ...request });
|
|
1059
|
-
|
|
1069
|
+
// Asked for, it is built with the stack chosen now - pinned, whatever is chosen later; a spec change keeps the one
|
|
1070
|
+
// it was asked for with.
|
|
1071
|
+
const pin = { name: chosenStack(), keep: specChange === true };
|
|
1072
|
+
const states = withParseCache(() => files.flatMap((file) => chainsFor(file).flatMap((chain) => toBuild(chain).map((entry) => track(chain.slice(0, chain.indexOf(entry) + 1), file, request, pin)))));
|
|
1060
1073
|
// Asked again, a halted codebase is tried again - one the person halted too, unless only its spec changed.
|
|
1061
1074
|
for (const state of states) {
|
|
1062
1075
|
state.halted = undefined;
|
|
@@ -1110,32 +1123,34 @@ export function createWorkLoop(options) {
|
|
|
1110
1123
|
const tip = tipOf(codebase);
|
|
1111
1124
|
// The codebase as the loop knows it, or - never asked for - as its records have it.
|
|
1112
1125
|
const state = modules.get(tip.buildDir) ?? { chain: codebase, askedFile: file, guidance: [] };
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1126
|
+
return inStack(state, () => {
|
|
1127
|
+
const report = { module: tip.moduleName, plainFile: tip.plainFile, code: undefined, conformance: undefined, scheduled: [] };
|
|
1128
|
+
let spec;
|
|
1129
|
+
try {
|
|
1130
|
+
spec = chainSpec(codebase);
|
|
1131
|
+
}
|
|
1132
|
+
catch (error) {
|
|
1133
|
+
return { ...report, why: `its spec does not parse: ${error instanceof Error ? error.message : String(error)}` };
|
|
1134
|
+
}
|
|
1135
|
+
const from = builtOf(state);
|
|
1136
|
+
const fresh = tip.requires === null && !started(tip);
|
|
1137
|
+
const code = codeWork(from, spec.tree);
|
|
1138
|
+
report.code = code.length === 0 ? null : casesOf(from, code, { fresh, module: tip.moduleName });
|
|
1139
|
+
if (conformanceWanted(state, spec)) {
|
|
1140
|
+
const tested = testedOf(state);
|
|
1141
|
+
const tests = workOf("conformance", tested, state, spec);
|
|
1142
|
+
report.conformance = tests.length === 0 ? null : casesOf(tested, tests, { module: tip.moduleName });
|
|
1143
|
+
}
|
|
1144
|
+
report.scheduled = (state.scheduled ?? []).map((group) => ({ frids: group.specs.map((touched) => touched.frid), unbuilt: group.unbuilt }));
|
|
1145
|
+
if (state.running !== undefined)
|
|
1146
|
+
report.running = { jobId: state.running.jobId, title: state.running.planned.title };
|
|
1147
|
+
const halted = state.held?.why ?? state.halted?.why ?? state.conformanceHalted?.why;
|
|
1148
|
+
if (halted !== undefined)
|
|
1149
|
+
report.why = halted;
|
|
1150
|
+
if (state.startOver !== undefined)
|
|
1151
|
+
report.startOver = state.startOver;
|
|
1152
|
+
return report;
|
|
1153
|
+
});
|
|
1139
1154
|
})));
|
|
1140
1155
|
},
|
|
1141
1156
|
async conformance(plainFile, request = {}) {
|
|
@@ -1176,7 +1191,7 @@ export function createWorkLoop(options) {
|
|
|
1176
1191
|
}
|
|
1177
1192
|
}
|
|
1178
1193
|
}
|
|
1179
|
-
const tracked = withParseCache(() => (wentBack ? [own] : chainsFor(file)).flatMap((chain) => toBuild(chain).map((entry) => track(chain.slice(0, chain.indexOf(entry) + 1), file, request))));
|
|
1194
|
+
const tracked = withParseCache(() => (wentBack ? [own] : chainsFor(file)).flatMap((chain) => toBuild(chain).map((entry) => track(chain.slice(0, chain.indexOf(entry) + 1), file, request, { name: chosenStack() }))));
|
|
1180
1195
|
// The module asked for, in its own codebase - and those below it: never the codebases built on it.
|
|
1181
1196
|
const asking = tracked.find((state) => tipEntry(state).buildDir === self.buildDir);
|
|
1182
1197
|
const states = asking !== undefined ? tracked.filter((state) => own.some((entry) => entry.buildDir === tipEntry(state).buildDir)) : tracked;
|
|
@@ -1194,7 +1209,7 @@ export function createWorkLoop(options) {
|
|
|
1194
1209
|
// On for the whole module already - asked for before, or by the config: asked again, nothing changes, and it is said.
|
|
1195
1210
|
const already = new Set(states.filter((state) => {
|
|
1196
1211
|
const file = tipEntry(state).plainFile;
|
|
1197
|
-
return (state.conformance !== undefined && state.conformance.chosen === undefined) || (configConformance(file) && !configChosen(file));
|
|
1212
|
+
return (state.conformance !== undefined && state.conformance.chosen === undefined) || inStack(state, () => configConformance(file) && !configChosen(file));
|
|
1198
1213
|
}));
|
|
1199
1214
|
// Only the module asked for: the modules below it keep their conformance tests as their config and choices have them.
|
|
1200
1215
|
for (const state of tip !== undefined ? [tip] : []) {
|
|
@@ -1402,7 +1417,7 @@ export function createWorkLoop(options) {
|
|
|
1402
1417
|
plainFile: tip.plainFile,
|
|
1403
1418
|
buildDir: tip.buildDir,
|
|
1404
1419
|
pulses,
|
|
1405
|
-
conformance: state.conformance !== undefined || configConformance(tip.plainFile),
|
|
1420
|
+
conformance: state.conformance !== undefined || inStack(state, () => configConformance(tip.plainFile)),
|
|
1406
1421
|
...(state.planning !== undefined ? { planning: state.planning } : {}),
|
|
1407
1422
|
...(running === undefined && state.waitsFor !== undefined ? { waitsFor: state.waitsFor } : {}),
|
|
1408
1423
|
...(running === undefined && state.why !== undefined ? { why: state.why } : {}),
|
|
@@ -1413,7 +1428,11 @@ export function createWorkLoop(options) {
|
|
|
1413
1428
|
},
|
|
1414
1429
|
async drain() {
|
|
1415
1430
|
for (;;) {
|
|
1416
|
-
|
|
1431
|
+
// A pulse that settles meanwhile ticks again: its next pulse is started by that tick, so it is waited for too.
|
|
1432
|
+
const looking = ticking;
|
|
1433
|
+
await looking;
|
|
1434
|
+
if (looking !== ticking)
|
|
1435
|
+
continue;
|
|
1417
1436
|
const running = order.map((key) => modules.get(key).running?.jobId).filter((id) => id !== undefined);
|
|
1418
1437
|
if (running.length === 0)
|
|
1419
1438
|
return;
|
|
@@ -1530,7 +1549,7 @@ export function describeModules(statuses, options = {}) {
|
|
|
1530
1549
|
// Nothing to do there: said in a sentence, not as a status.
|
|
1531
1550
|
if (isUpToDate(status))
|
|
1532
1551
|
return `${status.module} is up to date: its code is built from the spec as it is now in ${basename(status.plainFile)}, so there is nothing to regenerate.`;
|
|
1533
|
-
const lines = [`${status.module} (${basename(status.plainFile)}):`];
|
|
1552
|
+
const lines = [`${status.module} (${basename(status.plainFile)}${status.stack !== undefined ? `, stack ${status.stack}` : ""}):`];
|
|
1534
1553
|
if (status.running !== undefined) {
|
|
1535
1554
|
lines.push(` running: pulse ${status.running.jobId} - ${status.running.title}`);
|
|
1536
1555
|
// Steered, not stopped or planned again: what changed went to its agent.
|
|
@@ -2,7 +2,7 @@ import { fixMethod } from "./prompt.js";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
|
|
5
|
-
import { CONFIG_FILE, CONFIG_KEYS, projectConfigFile, readProjectConfig, stackConfigFileIn, stackOf } from "../config.js";
|
|
5
|
+
import { CONFIG_FILE, CONFIG_KEYS, projectConfigFile, readProjectConfig, stackConfigFile, stackConfigFileIn, stackOf, stacksOf } from "../config.js";
|
|
6
6
|
import { reportActivity, reportLogNote, reportNote } from "./activity.js";
|
|
7
7
|
import { ensureInModulesScratch, modulesScratchDir } from "./paths.js";
|
|
8
8
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
@@ -85,8 +85,14 @@ export function findTestScripts(root, plainFile, buildDir) {
|
|
|
85
85
|
const stack = stackOf(root);
|
|
86
86
|
const config = readProjectConfig(root, stack);
|
|
87
87
|
const file = stack !== undefined ? `${stackConfigFileIn(root, stack)} (over ${projectConfigFile(root)})` : projectConfigFile(root);
|
|
88
|
-
//
|
|
89
|
-
const
|
|
88
|
+
// A project of stacks with none chosen: its scripts are named in the stacks' files.
|
|
89
|
+
const stacks = stack === undefined ? stacksOf(root) : [];
|
|
90
|
+
// Where a script is named: the stack's file, once one is chosen - in a project of stacks, the one of the stack that builds it.
|
|
91
|
+
const nameIn = stack !== undefined
|
|
92
|
+
? stackConfigFileIn(root, stack)
|
|
93
|
+
: stacks.length > 0
|
|
94
|
+
? "the config.turri.<stack>.yaml of the stack that builds it"
|
|
95
|
+
: `${CONFIG_FILE} - or, for a project of several stacks, the stack's config.turri.<stack>.yaml`;
|
|
90
96
|
if (config.testingScriptsRunOn !== undefined)
|
|
91
97
|
found.runOn = config.testingScriptsRunOn;
|
|
92
98
|
for (const kind of Object.keys(SCRIPT_KEYS)) {
|
|
@@ -139,6 +145,22 @@ export function findTestScripts(root, plainFile, buildDir) {
|
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
}
|
|
148
|
+
// A project of stacks, none chosen: the scripts are likely in a stack's file - the note says which, and how to choose it.
|
|
149
|
+
if (stacks.length > 0) {
|
|
150
|
+
for (const kind of Object.keys(SCRIPT_KEYS)) {
|
|
151
|
+
if (found[kind] !== undefined)
|
|
152
|
+
continue;
|
|
153
|
+
const named = stacks.flatMap((each) => {
|
|
154
|
+
const script = kind === "unit" ? readProjectConfig(root, each).unittestsScript : readProjectConfig(root, each).conformanceTestsScript;
|
|
155
|
+
return script !== undefined ? [`${stackConfigFile(each)} names ${script.trim()}`] : [];
|
|
156
|
+
});
|
|
157
|
+
notes[kind] = [
|
|
158
|
+
...(notes[kind] !== undefined ? [notes[kind]] : []),
|
|
159
|
+
`No stack is chosen, so ${CONFIG_FILE} alone is read - this project's stacks are ${stacks.join(", ")}${named.length > 0 ? `, and ${named.join("; ")}` : ""}. ` +
|
|
160
|
+
"Choose the stack that builds this module: stack on regenerate, or /stack, in the chat; --stack <name> on the command line.",
|
|
161
|
+
].join(" ");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
142
164
|
if (Object.keys(notes).length > 0)
|
|
143
165
|
found.notes = notes;
|
|
144
166
|
return found;
|
package/dist/stacks.js
CHANGED
|
@@ -7,9 +7,7 @@ import { readdirSync } from "node:fs";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Type } from "typebox";
|
|
10
|
-
import { chooseStack, chosenStack, hasProjectConfig, stackConfigFile, stacksOf } from "./config.js";
|
|
11
|
-
/** The words that choose no stack: the project's config alone. */
|
|
12
|
-
const NONE = ["none", "off", "base", "-"];
|
|
10
|
+
import { chooseStack, chosenStack, hasProjectConfig, NO_STACK, stackConfigFile, stacksOf } from "./config.js";
|
|
13
11
|
/** The stacks of the projects here: the folder's own, and those of the projects one folder down (specs kept in plain/). */
|
|
14
12
|
export function knownStacks(root) {
|
|
15
13
|
const below = (() => {
|
|
@@ -29,7 +27,7 @@ export function knownStacks(root) {
|
|
|
29
27
|
export function switchStack(root, name) {
|
|
30
28
|
const stacks = knownStacks(root);
|
|
31
29
|
const wanted = name?.trim() ?? "";
|
|
32
|
-
if (wanted === "" ||
|
|
30
|
+
if (wanted === "" || NO_STACK.includes(wanted.toLowerCase())) {
|
|
33
31
|
chooseStack(undefined);
|
|
34
32
|
return "No stack chosen: everything is built with the project's config.turri.yaml alone, from now on.";
|
|
35
33
|
}
|
|
@@ -79,7 +77,7 @@ export function registerStacks(pi, cwd) {
|
|
|
79
77
|
"For the session; a pulse already running keeps the stack it started with. Without a name, says the stacks and the one chosen.",
|
|
80
78
|
promptSnippet: "choose the stack - its testing scripts and settings - that everything is built with",
|
|
81
79
|
promptGuidelines: [
|
|
82
|
-
"Switch the stack only when the user asks for it,
|
|
80
|
+
"Switch the stack only when the user asks for it, and say which you chose. To build a part of the project another stack builds - a backend's, a frontend's - pass that stack as stack to regenerate, conform or glimpse instead: it is pinned to that module's pulses, and the session's choice stays as it is.",
|
|
83
81
|
],
|
|
84
82
|
parameters: Type.Object({
|
|
85
83
|
name: Type.Optional(Type.String({ description: "The stack to choose, or none. Omitted: the stacks there are, and the one chosen" })),
|
package/docs/config.md
CHANGED
|
@@ -218,7 +218,8 @@ coding-model:
|
|
|
218
218
|
|
|
219
219
|
A stack is chosen, never worked out from the spec:
|
|
220
220
|
|
|
221
|
-
- **In the chat
|
|
221
|
+
- **In the chat, for a module**, by the chat's model: `regenerate`, `conform` and `glimpse` take a `stack`, and it builds each module with the stack of its part - `backend.plain` with `backend` - leaving the session's choice as it is. A backend and a frontend can be regenerated side by side this way, each with its own.
|
|
222
|
+
- **In the chat, for the session**, with `/stack` - a list of the project's stacks and none - or `/stack backend`, `/stack none`. The chat's model has a `stack` tool too, and switches the stack when you ask it to. `/stack` alone says which is chosen now; a module asked for without a stack of its own is built with it.
|
|
222
223
|
- **On the command line**, with `--stack <name>` on any command - `turri regenerate`, `conform`, `spec-diff`, `glimpse`, `release`, `cultivate` - and on `turri --stack backend` for the chat; or with `TURRI_STACK` in the environment.
|
|
223
224
|
|
|
224
225
|
The choice lasts for the session only: nothing remembers it, and turri started again has none chosen. A stack the project has no file for is refused, with the stacks it has. With none chosen, `config.turri.yaml` alone is used.
|
|
@@ -226,7 +227,8 @@ The choice lasts for the session only: nothing remembers it, and turri started a
|
|
|
226
227
|
How a stack's config is made:
|
|
227
228
|
|
|
228
229
|
- **Laid over `config.turri.yaml`, key by key.** What the stack's file gives wins; what it leaves out is the project's. A model it names takes what it leaves out from the project's same model - above, with the stack frontend chosen, everything renders on the project's coding model, thinking `high`.
|
|
229
|
-
- **
|
|
230
|
+
- **Pinned to a module when it is asked for.** A module is built with the stack chosen - or named in the call - when it is asked for, from its first pulse to its last: the pulses planned after it, its test runs, the scripts found, its plans and its conformance tests, whatever stack is chosen meanwhile. A pulse waiting for a free slot keeps the stack it was queued with. A change to its spec keeps its stack; asked for again, it takes the one chosen then. What `regenerate` says back names each module's stack, and so does a pulse's report.
|
|
231
|
+
- **Refused with none, when that cannot build.** In a project of stacks, a module asked for with no stack - none named, none chosen - is refused before anything is planned when `config.turri.yaml` alone names no unit testing script: every pulse would stop for want of one. The refusal names the stacks and the script each names. `stack: none` builds with `config.turri.yaml` alone all the same.
|
|
230
232
|
- **The project's alone.** `module:`, `harness-model:`, `parallel-jobs:`, `rules-off:` and `compact-at:` are read only from `config.turri.yaml` - there is one conversation, one module built when none is named, one machine. `agent-env:` and `toolchains:` in a stack's file add to the project's.
|
|
231
233
|
|
|
232
234
|
The chat is told each stack's config and the one chosen. The skills that write testing scripts put each stack's in its own file once a project has two stacks, and say which stack to choose before building each part.
|
package/docs/regenerating.md
CHANGED
|
@@ -45,7 +45,7 @@ The harness's scratch - each ignores itself, and is never part of a module's his
|
|
|
45
45
|
|
|
46
46
|
### Parts on different stacks
|
|
47
47
|
|
|
48
|
-
One project can hold modules that share nothing but a common module - a Python backend and a React frontend side by side, each importing `base`, neither requiring the other. Each stack gets its testing scripts, models and conformance in its own `config.turri.<stack>.yaml`, laid over `config.turri.yaml`
|
|
48
|
+
One project can hold modules that share nothing but a common module - a Python backend and a React frontend side by side, each importing `base`, neither requiring the other. Each stack gets its testing scripts, models and conformance in its own `config.turri.<stack>.yaml`, laid over `config.turri.yaml` for the modules built with it - in the chat, `regenerate` with `stack: backend`, pinned to that module's pulses, or `/stack backend` for the session; `--stack backend` on the command line: see [the project config](config.md#stacks).
|
|
49
49
|
|
|
50
50
|
## One job
|
|
51
51
|
|
|
@@ -218,7 +218,7 @@ The `implement-unit-testing-script` and `implement-conformance-testing-script` s
|
|
|
218
218
|
2. the root itself: the one `run_unittests*.sh` and `run_conformance_tests*.sh` there;
|
|
219
219
|
3. `test_scripts/`: the one of each there.
|
|
220
220
|
|
|
221
|
-
The same `config.turri.yaml` holds the project's other defaults - `module`, `model`, `conformance-tests` - for [unattended runs](headless.md). A folder holding two of a kind is not guessed between: name the one to use in `config.turri.yaml
|
|
221
|
+
The same `config.turri.yaml` holds the project's other defaults - `module`, `model`, `conformance-tests` - for [unattended runs](headless.md). A folder holding two of a kind is not guessed between: name the one to use in `config.turri.yaml` - or, in a project of stacks, in each stack's own file, and build each module with its stack. A module of such a project built with none says so when it stops: which stacks there are, and which script each names. The scripts are not linked from the specs; a link to one is ignored.
|
|
222
222
|
|
|
223
223
|
They are required: a build with no unit testing script, or with conformance on and no conformance testing script, stops before it starts and says which skill adds it.
|
|
224
224
|
|
package/docs/using.md
CHANGED
|
@@ -85,7 +85,7 @@ A render takes minutes, so in the TUI every render tool starts the work as a pul
|
|
|
85
85
|
`ctrl+h` again, or `esc`, closes it and goes back to the chat. Beside the chat, one side has the keys and the other is drawn faint: `ctrl+h` switches sides - from the chat to the panel, from the panel to the chat, the panel staying and following the pulses - `esc` gives the chat the keys too, and a click gives them to the side clicked. `ctrl+c` closes the panel; so do `s`, over the whole screen, then `esc`. (pi's regular screen, `--tui-mode regular`, reads no mouse: there it is the keys alone.) While the panel has the keys, every key is the panel's, whatever else happens: a question the chat model asks in the background (`ask_user`) waits in the chat, unanswered - an `enter` in the panel never answers it - and the panel's top line says "the chat is waiting for your answer". Back in the chat, the question is there, with the keys.
|
|
86
86
|
- `turri` runs pi in fullscreen mode, so the conversation stays where you scroll it while the pulses redraw (PgUp/PgDn, the wheel); `--tui-mode regular` goes back to the terminal's own scrollback.
|
|
87
87
|
- **`/turri-config`** changes a setting of the project's `config.turri.yaml` - the models, review, conformance tests, any key - and says when it takes effect: now, at your next message, or at the next pulse; never only after a restart. See [changing it from the chat](config.md#changing-it-from-the-chat).
|
|
88
|
-
- **`/stack`** chooses the stack everything is built with from then on, in a project whose parts are on different stacks: a list of the project's stacks, or `/stack backend`, `/stack none`. The stack's `config.turri.<stack>.yaml` is laid over `config.turri.yaml` - its testing scripts, toolchains, models - for the session; a
|
|
88
|
+
- **`/stack`** chooses the stack everything is built with from then on, in a project whose parts are on different stacks: a list of the project's stacks, or `/stack backend`, `/stack none`. The stack's `config.turri.<stack>.yaml` is laid over `config.turri.yaml` - its testing scripts, toolchains, models - for the session; a module already asked for keeps the stack it was asked for with. The chat model switches it too when you ask - and, to build one part, passes that part's stack to `regenerate` instead, leaving the session's as it is. See [stacks](config.md#stacks).
|
|
89
89
|
- **`/regenerate [module | file.plain] [functionality ...] [from scratch [whole chain]] [how to cut the work]`** regenerates a module from its spec as it is now, as the `regenerate` tool does: what is left to regenerate is planned into pulses - one functional spec at a time, a few together or the whole module in one, as the planner judges - and the first starts. Words after the module are guidance for that plan (`/regenerate app models first`). A module's name typed with spaces for its underscores is that module - `/regenerate d365_function executor` is `d365_function_executor`. With no module named: the one `module:` in `config.turri.yaml` names, else the folder's top modules, to pick from. Asked for from scratch - `from scratch`, `start over`, `delete the module` - it asks first, then deletes the module's code and conformance tests in every codebase where they live, committed so `/lineage` keeps them, and regenerates the whole module from its spec, built though nothing in it changed: `/regenerate shorty from scratch`. The modules below it keep their code, and the module starts again from a copy of the code below it. Asked for its chain too - `/regenerate shorty from scratch, the whole chain` - the code of every module of its requires chain is deleted, the base's included, and the whole chain is regenerated from the bottom: what to do after modules were split, merged or moved, when the code below was laid out for modules that are no more. The chat's `regenerate` does the same with `from_scratch: "module"` or `"chain"`, when you ask for it; `turri regenerate --from-scratch` or `--from-scratch-chain` on the command line, not with `--dry-run`. Functionalities named after the module - `/regenerate shorty 3 5` - are regenerated though their spec did not change, on the model chosen for functionalities: as `turri regenerate shorty.plain 3 5` does, and the chat's `regenerate` with `functionalities`.
|
|
90
90
|
- **`/halt <id>`** halts a pulse - a queued one never starts - **`/halt <module>`** (or a planned row's id) halts a module's planned pulses until you ask for it again, a spec change not starting it, and **`/halt activated <name>`** halts a server `activate` started; it lists all three when no id is given.
|
|
91
91
|
- **What these commands show you, the chat knows.** What `/regenerate` and `/halt activated` tell you - a spec that does not parse, the pulses planned, a server halted - stays in the chat under the command, and since a slash command runs outside the conversation, it is put in the conversation as well: say "fix it" next and turri knows what you are looking at. A halted pulse reaches it as every pulse's end does.
|
|
@@ -102,7 +102,7 @@ In `print`, `json` and `rpc` modes the process ends when the answer is written,
|
|
|
102
102
|
|
|
103
103
|
The chat reads the project's `config.turri.yaml` afresh every turn and follows it: its agent is told what it sets, and the render tools default to it - `module` when you ask to build without naming one, `conformance-tests: false` to render without conformance tests (none written, none of the existing ones run) unless you ask for them, `coding-model` for what renders run on, and the testing scripts. `harness-model` is what `turri` starts on. What you ask for in the conversation wins. Every key, what happens without it, and how the models inherit: [the project config](config.md).
|
|
104
104
|
|
|
105
|
-
A project whose parts are on different stacks gives each stack its own `config.turri.<stack>.yaml` beside `config.turri.yaml` - `config.turri.backend.yaml`, `config.turri.frontend.yaml` - with its testing scripts, models and conformance over the project's. A stack is chosen, never worked out:
|
|
105
|
+
A project whose parts are on different stacks gives each stack its own `config.turri.<stack>.yaml` beside `config.turri.yaml` - `config.turri.backend.yaml`, `config.turri.frontend.yaml` - with its testing scripts, models and conformance over the project's. A stack is chosen, never worked out: ask for the backend and the chat's model regenerates it with `stack: backend`, the frontend with `stack: frontend` - each pinned to its module's pulses, so both can be built side by side - or choose one for the session with `/stack backend`. The chat is told each stack's and which is chosen, and the status bar shows it. See [stacks](config.md#stacks).
|
|
106
106
|
|
|
107
107
|
A project with only a `config.yaml` - turri's file before it had its own name, or another tool's - is read from it until turri first writes a setting, which copies it to `config.turri.yaml`; see [the old config.yaml](config.md#the-old-configyaml).
|
|
108
108
|
|
|
@@ -102,7 +102,7 @@ Create the module structure and frontmatter so there is a `.plain` file to build
|
|
|
102
102
|
|
|
103
103
|
Write no definitions, no functional specs, no `***test reqs***` and no `***acceptance tests***`.
|
|
104
104
|
|
|
105
|
-
End the phase with the unit tests: their framework, as a `:UnitTests:` requirement, and the unit testing script, which every build needs from the first one on. If turri already finds a script - named by `unittests-script:` in `config.turri.yaml` at the project root, or the one `run_unittests*.sh` in the root or in `test_scripts/` - keep it. Otherwise write `test_scripts/run_unittests_<language>.sh` with the `implement-unit-testing-script` skill, for the language and framework just settled. Do not link it from the specs. Without it, every build stops before it starts. When this module sits in a project beside another on a different stack - a frontend beside a backend, sharing only an imported module - its script is its stack's: name it in that stack's `config.turri.<stack>.yaml` beside `config.turri.yaml` (`unittests-script:`) - `config.turri.frontend.yaml` for the stack `frontend` - not in `config.turri.yaml`, which is the project's and holds what the stacks share. A stack is chosen, never worked out:
|
|
105
|
+
End the phase with the unit tests: their framework, as a `:UnitTests:` requirement, and the unit testing script, which every build needs from the first one on. If turri already finds a script - named by `unittests-script:` in `config.turri.yaml` at the project root, or the one `run_unittests*.sh` in the root or in `test_scripts/` - keep it. Otherwise write `test_scripts/run_unittests_<language>.sh` with the `implement-unit-testing-script` skill, for the language and framework just settled. Do not link it from the specs. Without it, every build stops before it starts. When this module sits in a project beside another on a different stack - a frontend beside a backend, sharing only an imported module - its script is its stack's: name it in that stack's `config.turri.<stack>.yaml` beside `config.turri.yaml` (`unittests-script:`) - `config.turri.frontend.yaml` for the stack `frontend` - not in `config.turri.yaml`, which is the project's and holds what the stacks share. A stack is chosen, never worked out: this module is built with its own - in the chat, `regenerate` with `stack: <stack>`, pinned to its pulses whatever stack is chosen after; `--stack <stack>` on the command line - so say which stack builds it.
|
|
106
106
|
|
|
107
107
|
The unit testing script runs the static checks too - the formatter, linter and type checker the stack names, which count as the unit tests do; `implement-unit-testing-script` says which for each language. There is no separate static checks script.
|
|
108
108
|
|
|
@@ -19,7 +19,7 @@ Write no `***acceptance tests***`, no functional specs and no `***implementation
|
|
|
19
19
|
3. **How the tests reach the software** - as a subprocess of the CLI, over HTTP against a server the tests start, through the public entry point it exports. Author it.
|
|
20
20
|
4. **Setup and outside services** - fixtures, test data, databases, third-party APIs: started for real, faked, or mocked at the boundary. Author each decision as its own requirement.
|
|
21
21
|
5. **How the whole suite is run** - the one command that discovers every test. Author it.
|
|
22
|
-
6. **The conformance testing script** - if turri already finds one (named by `conformance-tests-script:` in `config.turri.yaml`, or the one `run_conformance_tests*.sh` in the project root or in `test_scripts/`), check it runs the suite the way the test reqs now say, and keep it. Otherwise write `test_scripts/run_conformance_tests_<language>.sh` with `implement-conformance-testing-script`, to match the test reqs. Never link it from the specs. If `test_scripts/` would hold two of a kind, name the one to use in `config.turri.yaml`. In a project whose parts are on different stacks, name each stack's script in its own `config.turri.<stack>.yaml` (`conformance-tests-script:`), and
|
|
22
|
+
6. **The conformance testing script** - if turri already finds one (named by `conformance-tests-script:` in `config.turri.yaml`, or the one `run_conformance_tests*.sh` in the project root or in `test_scripts/`), check it runs the suite the way the test reqs now say, and keep it. Otherwise write `test_scripts/run_conformance_tests_<language>.sh` with `implement-conformance-testing-script`, to match the test reqs. Never link it from the specs. If `test_scripts/` would hold two of a kind, name the one to use in `config.turri.yaml`. In a project whose parts are on different stacks, name each stack's script in its own `config.turri.<stack>.yaml` (`conformance-tests-script:`), and build each part with its own stack - in the chat, `regenerate` with `stack: <stack>`, pinned to that part's pulses; `--stack <stack>` on the command line.
|
|
23
23
|
7. **Anything else** - anything about testing the user wants to add or change.
|
|
24
24
|
|
|
25
25
|
Run `validate` on every module you changed. The product phase's first build needs the test reqs and the conformance testing script both there: without them it stops before it starts.
|
|
@@ -208,7 +208,7 @@ The harness never runs `.ps1` scripts. These notes apply only to an optional cop
|
|
|
208
208
|
4. Pick the right row from the [Dependency isolation](#dependency-isolation) table — use the same flag/env var in steps 7, 7b and 8 — and the checks from [Static checks of the tests](#static-checks-of-the-tests).
|
|
209
209
|
5. Add the language-appropriate "no tests discovered" guard from [No tests discovered detection](#no-tests-discovered-detection), and the guard against conformance tests in the build folder from [Tests in the build folder](#tests-in-the-build-folder).
|
|
210
210
|
6. Save the new script as `test_scripts/run_conformance_tests_<lang>.sh` and `chmod +x` it. Write a `.ps1` copy only if the user wants to run it by hand on Windows.
|
|
211
|
-
7. **Make sure turri finds the script.** Do not link it from the specs - turri finds the conformance testing script in the project root (the folder that holds `plain_modules/`) and puts it, whole, into the prompts of the agents that write and review the conformance tests itself, whenever conformance tests are written for a build; a link to it from a spec is ignored. It looks, in order, at `config.turri.yaml` in the root (`conformance-tests-script: <path relative to the root>`) - with the chosen stack's `config.turri.<stack>.yaml` over it, then for the one `run_conformance_tests*.sh` in the root, then for the one in `test_scripts/`. Saving the script in `test_scripts/` (or the root) is enough when it is the only one there. If the project keeps it elsewhere, name it in `config.turri.yaml` as `conformance-tests-script:` - and when the project has only a `config.yaml`, copy it to `config.turri.yaml` first (and each `config.<name>.yaml` to `config.turri.<name>.yaml`) and edit the copy; `config.yaml` may be another tool's, and is never written. If the project has modules on different stacks - e.g. a Python backend and a React frontend side by side, sharing only an imported module - each part is on a stack of its own, and each stack names its own script in its own file beside `config.turri.yaml`: `conformance-tests-script:` in `config.turri.backend.yaml` for the stack `backend`, in `config.turri.frontend.yaml` for the stack `frontend`. A stack is chosen, never worked out:
|
|
211
|
+
7. **Make sure turri finds the script.** Do not link it from the specs - turri finds the conformance testing script in the project root (the folder that holds `plain_modules/`) and puts it, whole, into the prompts of the agents that write and review the conformance tests itself, whenever conformance tests are written for a build; a link to it from a spec is ignored. It looks, in order, at `config.turri.yaml` in the root (`conformance-tests-script: <path relative to the root>`) - with the chosen stack's `config.turri.<stack>.yaml` over it, then for the one `run_conformance_tests*.sh` in the root, then for the one in `test_scripts/`. Saving the script in `test_scripts/` (or the root) is enough when it is the only one there. If the project keeps it elsewhere, name it in `config.turri.yaml` as `conformance-tests-script:` - and when the project has only a `config.yaml`, copy it to `config.turri.yaml` first (and each `config.<name>.yaml` to `config.turri.<name>.yaml`) and edit the copy; `config.yaml` may be another tool's, and is never written. If the project has modules on different stacks - e.g. a Python backend and a React frontend side by side, sharing only an imported module - each part is on a stack of its own, and each stack names its own script in its own file beside `config.turri.yaml`: `conformance-tests-script:` in `config.turri.backend.yaml` for the stack `backend`, in `config.turri.frontend.yaml` for the stack `frontend`. A stack is chosen, never worked out: each part is built with its own - in the chat, `regenerate` with `stack: <stack>`, pinned to that part's pulses whatever stack is chosen after; `--stack <stack>` on the command line - so say which stack builds which part. turri never guesses between two. The script is required: with conformance testing on, a build without one stops before it starts. The `***test reqs***` still describe the conformance framework and command in words; do not paraphrase the script into them.
|
|
212
212
|
|
|
213
213
|
## Anti-Patterns
|
|
214
214
|
|
|
@@ -139,7 +139,7 @@ Notes:
|
|
|
139
139
|
3. Translate each of the eight steps above into the equivalent commands for the target language **and** shell. The toolchain check, dependency install, static checks and test invocation are the language-specific parts; the rest is mechanical translation between Bash and PowerShell syntax.
|
|
140
140
|
4. Pick the dependency-isolation mechanism from the [Dependency isolation](#dependency-isolation) table and use it consistently in steps 6, 7 and 8, and the checks from the [Static checks](#static-checks) table.
|
|
141
141
|
5. Save the new script as `test_scripts/run_unittests_<lang>.sh` (a `.ps1` copy next to it only if the user wants to run it by hand on Windows). For Bash, `chmod +x` it.
|
|
142
|
-
6. **Make sure turri finds the script.** Do not link it from the specs - turri finds the unit testing script in the project root (the folder that holds `plain_modules/`) and puts it, whole, into the prompt of the coding agent itself; a link to it from a spec is ignored. It looks, in order, at `config.turri.yaml` in the root (`unittests-script: <path relative to the root>`) - with the chosen stack's `config.turri.<stack>.yaml` over it, then for the one `run_unittests*.sh` in the root, then for the one in `test_scripts/`. Saving the script in `test_scripts/` (or the root) is enough when it is the only one there. If the project keeps it elsewhere, name it in `config.turri.yaml` as `unittests-script:` - and when the project has only a `config.yaml`, copy it to `config.turri.yaml` first (and each `config.<name>.yaml` to `config.turri.<name>.yaml`) and edit the copy; `config.yaml` may be another tool's, and is never written. If the project has modules on different stacks - e.g. a Python backend and a React frontend side by side, sharing only an imported module - each part is on a stack of its own, and each stack names its own script in its own file beside `config.turri.yaml`: `unittests-script:` in `config.turri.backend.yaml` for the stack `backend`, in `config.turri.frontend.yaml` for the stack `frontend`. A stack is chosen, never worked out:
|
|
142
|
+
6. **Make sure turri finds the script.** Do not link it from the specs - turri finds the unit testing script in the project root (the folder that holds `plain_modules/`) and puts it, whole, into the prompt of the coding agent itself; a link to it from a spec is ignored. It looks, in order, at `config.turri.yaml` in the root (`unittests-script: <path relative to the root>`) - with the chosen stack's `config.turri.<stack>.yaml` over it, then for the one `run_unittests*.sh` in the root, then for the one in `test_scripts/`. Saving the script in `test_scripts/` (or the root) is enough when it is the only one there. If the project keeps it elsewhere, name it in `config.turri.yaml` as `unittests-script:` - and when the project has only a `config.yaml`, copy it to `config.turri.yaml` first (and each `config.<name>.yaml` to `config.turri.<name>.yaml`) and edit the copy; `config.yaml` may be another tool's, and is never written. If the project has modules on different stacks - e.g. a Python backend and a React frontend side by side, sharing only an imported module - each part is on a stack of its own, and each stack names its own script in its own file beside `config.turri.yaml`: `unittests-script:` in `config.turri.backend.yaml` for the stack `backend`, in `config.turri.frontend.yaml` for the stack `frontend`. A stack is chosen, never worked out: each part is built with its own - in the chat, `regenerate` with `stack: <stack>`, pinned to that part's pulses whatever stack is chosen after; `--stack <stack>` on the command line - so say which stack builds which part. turri never guesses between two. The script is required: a build without one stops before it starts.
|
|
143
143
|
|
|
144
144
|
## Anti-Patterns
|
|
145
145
|
|
|
@@ -96,7 +96,7 @@ Call the corresponding implementation skills in order. Each skill writes a Bash
|
|
|
96
96
|
|
|
97
97
|
### 6. Check turri finds the scripts
|
|
98
98
|
|
|
99
|
-
turri finds the testing scripts in the project root, not through the specs: first in `config.turri.yaml` (`unittests-script:`, `conformance-tests-script:`, paths relative to the root), then the one `run_unittests*.sh` / `run_conformance_tests*.sh` in the root, then the one of each kind in `test_scripts/`. It puts them into the prompts of the agents that write and run the tests itself, so do not link them from `template/base.plain` (a link to one is ignored). Confirm every generated script is in `test_scripts/` and is the only one of its kind there - or, if the project wants it, that a `config.turri.yaml` at the root names the one to use and the path resolves. A project with parts on different stacks (e.g. a Python backend and a React frontend sharing an imported `base`) keeps one `config.turri.yaml` for what they share and gives each stack its own `config.turri.<stack>.yaml` beside it - `config.turri.backend.yaml`, `config.turri.frontend.yaml` - naming that stack's `unittests-script:` and `conformance-tests-script:`. A stack is chosen, never worked out:
|
|
99
|
+
turri finds the testing scripts in the project root, not through the specs: first in `config.turri.yaml` (`unittests-script:`, `conformance-tests-script:`, paths relative to the root), then the one `run_unittests*.sh` / `run_conformance_tests*.sh` in the root, then the one of each kind in `test_scripts/`. It puts them into the prompts of the agents that write and run the tests itself, so do not link them from `template/base.plain` (a link to one is ignored). Confirm every generated script is in `test_scripts/` and is the only one of its kind there - or, if the project wants it, that a `config.turri.yaml` at the root names the one to use and the path resolves. A project with parts on different stacks (e.g. a Python backend and a React frontend sharing an imported `base`) keeps one `config.turri.yaml` for what they share and gives each stack its own `config.turri.<stack>.yaml` beside it - `config.turri.backend.yaml`, `config.turri.frontend.yaml` - naming that stack's `unittests-script:` and `conformance-tests-script:`. A stack is chosen, never worked out: each part is built with its own - in the chat, `regenerate` with `stack: <stack>`, pinned to that part's pulses whatever stack is chosen after; `--stack <stack>` on the command line - so say which stack builds which part. When the project has only a `config.yaml`, copy it to `config.turri.yaml` first - and each `config.<name>.yaml` to `config.turri.<name>.yaml` - and write the copies; never write `config.yaml`.
|
|
100
100
|
|
|
101
101
|
Do **not** run `validate` here — keeping this skill light is the whole point. The user can run it when they want validation.
|
|
102
102
|
|
|
@@ -38,7 +38,7 @@ Read what is already there first: an existing `.plain` file, `config.turri.yaml`
|
|
|
38
38
|
|
|
39
39
|
- Templates go in `template/`; import paths omit the `template/` prefix. Resources go in `resources/`.
|
|
40
40
|
- Generated code lands in `plain_modules/<module>/code/`, conformance tests in `plain_modules/<module>/tests/` (both read-only).
|
|
41
|
-
- One module, unless the intent needs two stacks - a web frontend and an API backend. Then two modules side by side, neither requiring the other, sharing what they both mean through one imported module in `template/`. Each is on a stack of its own, whose testing scripts are named in `config.turri.<stack>.yaml` beside `config.turri.yaml` (`unittests-script:`, `conformance-tests-script:`) - `config.turri.backend.yaml`, `config.turri.frontend.yaml`. A stack is chosen, never worked out:
|
|
41
|
+
- One module, unless the intent needs two stacks - a web frontend and an API backend. Then two modules side by side, neither requiring the other, sharing what they both mean through one imported module in `template/`. Each is on a stack of its own, whose testing scripts are named in `config.turri.<stack>.yaml` beside `config.turri.yaml` (`unittests-script:`, `conformance-tests-script:`) - `config.turri.backend.yaml`, `config.turri.frontend.yaml`. A stack is chosen, never worked out: each part is built with its own - in the chat, `regenerate` with `stack: <stack>`, pinned to that part's pulses whatever stack is chosen after; `--stack <stack>` on the command line - so say which stack builds which part.
|
|
42
42
|
|
|
43
43
|
## Steps
|
|
44
44
|
|