@turri-dev/cli 0.1.25-next.d0c7575 → 0.1.25
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 +5 -5
- package/dist/extension.d.ts +0 -4
- package/dist/extension.js +13 -55
- package/dist/genome.d.ts +0 -3
- package/dist/genome.js +4 -7
- package/dist/graph.js +5 -2
- package/dist/new-project.d.ts +1 -3
- package/dist/new-project.js +3 -13
- package/dist/plain/parser.d.ts +1 -0
- package/dist/plain/parser.js +1 -1
- package/dist/plain-files.d.ts +21 -0
- package/dist/plain-files.js +156 -0
- package/dist/regenerate/diff-view.d.ts +4 -1
- package/dist/regenerate/diff-view.js +6 -3
- package/dist/regenerate/stop-report.js +1 -1
- package/dist/regenerate/test-scripts.d.ts +2 -0
- package/dist/regenerate/test-scripts.js +1 -1
- package/dist/skill-progress.d.ts +4 -4
- package/dist/skill.d.ts +1 -1
- package/dist/skills.d.ts +4 -4
- package/dist/skills.js +5 -5
- package/docs/authoring-plain.md +7 -23
- package/docs/chats.md +1 -1
- package/docs/config.md +3 -3
- package/docs/development.md +0 -1
- package/docs/headless.md +2 -2
- package/docs/hosting.md +1 -1
- package/docs/regenerating.md +3 -3
- package/docs/skills-and-gates.md +17 -15
- package/docs/using.md +5 -13
- package/genome/rules/concise-specs-examples.md +1 -1
- package/genome/rules/concise-specs-integration-examples.md +1 -1
- package/genome/rules/func-specs.md +10 -2
- package/genome/rules/integration-embedded.md +2 -2
- package/genome/rules/integration-standalone.md +2 -2
- package/genome/skills/break-down-func-spec/SKILL.md +8 -8
- package/genome/skills/consolidate-concepts/SKILL.md +1 -1
- package/genome/skills/resolve-section-ownership/SKILL.md +2 -2
- package/genome/skills/resolve-spec-conflict/SKILL.md +1 -1
- package/package.json +1 -1
- package/dist/mature-tools.d.ts +0 -53
- package/dist/mature-tools.js +0 -193
- package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +0 -206
- package/genome/skills/brood/SKILL.md +0 -129
- package/genome/skills/brood/references/intent.md +0 -27
- package/genome/skills/brood/references/product.md +0 -49
- package/genome/skills/brood/references/stack.md +0 -39
- package/genome/skills/brood/references/testing.md +0 -25
- package/genome/skills/bud/SKILL.md +0 -104
- package/genome/skills/bud/references/intent.md +0 -27
- package/genome/skills/mature/SKILL.md +0 -78
- package/genome/skills/spawn/SKILL.md +0 -85
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ turri init-config --vendor anthropic
|
|
|
50
50
|
|
|
51
51
|
It then tells you which API key to set - or to type `/login` once **turri** is running instead. **turri** loads the `config.turri.yaml` in the folder you start it from. Below the two models, the file sets every other setting to its default - change any of them in the file, or from the chat with `/turri-config`. Any other model works too: [models](#models).
|
|
52
52
|
|
|
53
|
-
Once `turri` is running,
|
|
53
|
+
Once `turri` is running, say what you want - `a small API where people can register and sign in` - and **turri** starts the project and regenerates it at once.
|
|
54
54
|
|
|
55
55
|
No project of your own yet? Build one of [**the examples**](examples/README.md) with turri, step by step.
|
|
56
56
|
|
|
@@ -147,17 +147,17 @@ cd ~/my-project
|
|
|
147
147
|
turri
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
Then
|
|
150
|
+
Then say what you want:
|
|
151
151
|
|
|
152
|
-
>
|
|
152
|
+
> A small API where people can register and sign in, in Python.
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
In a new project **turri** starts it itself, without asking how: it chooses the stack - the one you name, else a standard library stack this machine can build - writes the module and its unit testing script (and the conformance tests' when you ask for them), regenerates the smallest thing that runs at once, then the main thing you described, and says what it chose and what it would write next. Every change after is yours to ask for: **turri** writes it as \*\*\*plain and regenerates it. While it works:
|
|
155
155
|
|
|
156
156
|
- the **panel** above the input shows every build - its stage, what its agents are doing, its model and cost - and the queue behind it;
|
|
157
157
|
- ask to try it at any time - "let me try it" - and **`release`** puts what is built so far in `dist/<module>` and **`activate`** starts it, installing what it needs - nothing to approve;
|
|
158
158
|
- the **side panel** (`ctrl+h`) takes the screen and opens on **regen** - \*\*\*plain, every item marked planned, working, regenerated, validating, conform or needs you - and `p` turns it to the **pulses** (`/pulses`) - every pulse, running, queued and finished, and what each regenerates; what its agents did is in each pulse's log in `.turri/jobs/`, kept with `--debug` (`debug: true`);
|
|
159
159
|
- **`/halt`** halts a pulse, **`/lineage`** shows every generation of a module, and the **`drift`** tool says what is left to regenerate;
|
|
160
|
-
- **`/btw <question>`** asks turri something on the side - answered at once, even
|
|
160
|
+
- **`/btw <question>`** asks turri something on the side - answered at once, even while a question waits for you (typed as the answer, under "Something else…"), and left out of the conversation;
|
|
161
161
|
- **`/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;
|
|
162
162
|
- **`/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.
|
|
163
163
|
|
package/dist/extension.d.ts
CHANGED
|
@@ -92,10 +92,6 @@ export declare function moduleNamedFirst(cwd: string, words: string[]): {
|
|
|
92
92
|
* is set - else half this machine's cores, between one and four.
|
|
93
93
|
*/
|
|
94
94
|
export declare function parallelJobs(cwd: string): number;
|
|
95
|
-
/** The project's own files and a digest of each - not plain_modules, dependencies, or turri's and git's folders. */
|
|
96
|
-
export declare function projectFiles(root: string): Map<string, string>;
|
|
97
|
-
/** The files added, changed or removed between two digests of the project. */
|
|
98
|
-
export declare function changedFiles(before: Map<string, string>, after: Map<string, string>): string[];
|
|
99
95
|
/**
|
|
100
96
|
* This machine, for the chat's agent, read afresh every turn: which wall is up under the agents' commands, whether the
|
|
101
97
|
* machine has the toolchains the project's modules name, and what in its testing scripts it lacks - for the person to
|
package/dist/extension.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Whether the terminal shows images, before anything draws: the banner's jellyfish is a picture or text.
|
|
2
2
|
import "./terminal-images.js";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
3
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
5
4
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
5
|
import { conversationCommand, conversationSettled, conversationWriting } from "./regenerate/conversation-writes.js";
|
|
@@ -29,6 +28,7 @@ import { askedForCommandLine, refusal, selfInvocation, TOOLS_NOT_CLI } from "./s
|
|
|
29
28
|
import { NEW_PROJECT } from "./new-project.js";
|
|
30
29
|
import { describeChoices, describeResolution, modelFor, resolveModel } from "./regenerate/engine.js";
|
|
31
30
|
import { checkPlainFile, checkPlainTargets, formatCheckResults } from "./plain/check.js";
|
|
31
|
+
import { changedFiles, projectFiles } from "./plain-files.js";
|
|
32
32
|
import { whyFailed } from "./regenerate/diagnose-job.js";
|
|
33
33
|
import { createWorkLoop, describeModules, isUpToDate, previewJobs } from "./regenerate/loop.js";
|
|
34
34
|
import { brandNeon, brandPink, plainPaint } from "./regenerate/plain-palette.js";
|
|
@@ -618,7 +618,7 @@ function turriExtension(api, session, link) {
|
|
|
618
618
|
registerBranding(pi, discoveredSkills);
|
|
619
619
|
registerInterviewLog(pi);
|
|
620
620
|
registerJobTools(pi, jobs, cwd);
|
|
621
|
-
// The skills the person starts themselves -
|
|
621
|
+
// The skills the person starts themselves - a project's, marked command-only: each has its command, and cultivate starts it only
|
|
622
622
|
// once its command is typed, or picked among the options the conversation offers with ask_user.
|
|
623
623
|
const gate = createCommandGate();
|
|
624
624
|
// A skill picked is started at once, with what the person asked for: they never type its command after picking it.
|
|
@@ -667,7 +667,7 @@ function turriExtension(api, session, link) {
|
|
|
667
667
|
promptSnippet: "cultivate: run a gated skill - its steps and review gates enforced by turri",
|
|
668
668
|
promptGuidelines: [
|
|
669
669
|
"When a request matches a skill listed in cultivate's description, call cultivate instead of doing the work yourself. Doing it by hand skips the review gates.",
|
|
670
|
-
"A skill marked as started only by the person runs only from its command: a message that is its command -
|
|
670
|
+
"A skill marked as started only by the person runs only from its command: a message that is its command - /<name> what they want - is the person starting it, and so is an option starting with it that they picked in your ask_user. Call cultivate for it at once, with what they said as context. Never call cultivate for one otherwise.",
|
|
671
671
|
// pi lists .agents/skills among its own skills too: read and followed by hand, a skill's gates would be skipped.
|
|
672
672
|
"A skill cultivate lists may also be among your own skills, from the project's .agents/skills: run it with cultivate all the same, never by reading its SKILL.md and following it.",
|
|
673
673
|
"cultivate returns the reviewed artifact and a trace of every gate decision. Report the artifact to the user; do not rewrite it.",
|
|
@@ -817,8 +817,9 @@ function turriExtension(api, session, link) {
|
|
|
817
817
|
push(` ${line}`);
|
|
818
818
|
},
|
|
819
819
|
};
|
|
820
|
-
// What the run changes in the project - its specs, resources, scripts - is said with its result: a skill
|
|
821
|
-
// writes nothing is not taken for one that did its work.
|
|
820
|
+
// What the run changes in the project's ***plain - its specs, resources, scripts - is said with its result: a skill
|
|
821
|
+
// that writes nothing is not taken for one that did its work. Only those: a release or a running app changes other
|
|
822
|
+
// files while it runs, and they are not its doing.
|
|
822
823
|
const filesBefore = projectFiles(cwd);
|
|
823
824
|
// A side question asked while it runs is told what it has done so far.
|
|
824
825
|
const ended = skillRunStarted({ skill: skill.name, dir: skill.dir, step: () => live.step, trace: () => [...trace], runDir });
|
|
@@ -847,8 +848,6 @@ function turriExtension(api, session, link) {
|
|
|
847
848
|
// pulses sees them without knowing a skill started them.
|
|
848
849
|
// Asked per job as it is queued, so a model chosen mid-interview applies from then on.
|
|
849
850
|
renderQueue: workLoopFor(jobs, cwd, ctx),
|
|
850
|
-
// A skill that waits for its builds and reads what they recorded - `mature` - sees the same jobs.
|
|
851
|
-
jobs,
|
|
852
851
|
// The person answers the skill's agent while it runs, so it can take "render these with ..." too.
|
|
853
852
|
// validate too: a skill validates what it wrote with the same check the conversation has.
|
|
854
853
|
sharedTools: [setRenderModelTool(pi, ctx), checkPlainTool(cwd), ...createWebTools()],
|
|
@@ -900,10 +899,14 @@ function turriExtension(api, session, link) {
|
|
|
900
899
|
const noise = describeCaptured(captured);
|
|
901
900
|
if (noise !== null)
|
|
902
901
|
push(noise);
|
|
903
|
-
const changes = changedFiles(filesBefore, projectFiles(cwd));
|
|
902
|
+
const changes = changedFiles(filesBefore, projectFiles(cwd, filesBefore.keys()));
|
|
903
|
+
// A skill that only reads and reports - an analysis - changes nothing, and its result is its verdict.
|
|
904
|
+
const writes = ["write", "edit", "bash", "change_plain"].some((tool) => skill.allowedTools.includes(tool));
|
|
904
905
|
const changed = changes.length > 0
|
|
905
906
|
? `It changed ${changes.length} file${changes.length === 1 ? "" : "s"} in the project: ${changes.join(", ")}.`
|
|
906
|
-
:
|
|
907
|
+
: writes
|
|
908
|
+
? "It changed no file in the project - whatever it said it wrote is not there. Say so to the user; do not report the work as done."
|
|
909
|
+
: "It only reads: what it found is the artifact below.";
|
|
907
910
|
// A skill that edits the specs with the file tools builds nothing: the change is the conversation's to render.
|
|
908
911
|
const specsChanged = changes.filter((path) => path.endsWith(".plain"));
|
|
909
912
|
const unrendered = specsChanged.length > 0 && !skill.allowedTools.includes("change_plain")
|
|
@@ -989,7 +992,7 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
989
992
|
say(`${relative(cwd, plainFile) || plainFile} does not parse, so nothing can be regenerated from it yet: ${check.error}`, "error");
|
|
990
993
|
return;
|
|
991
994
|
}
|
|
992
|
-
// From scratch - started over, its code deleted - asked in the person's words:
|
|
995
|
+
// From scratch - started over, its code deleted - asked in the person's words: started at once, as asked.
|
|
993
996
|
const scratch = asksFromScratch(guidance);
|
|
994
997
|
// The whole chain with it - "from scratch, the whole chain" - every module it requires built again from the bottom.
|
|
995
998
|
const chain = scratch && asksWholeChain(guidance);
|
|
@@ -998,13 +1001,6 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
998
1001
|
say("Name functionalities or ask for it from scratch, not both: from scratch regenerates every one of them.", "warning");
|
|
999
1002
|
return;
|
|
1000
1003
|
}
|
|
1001
|
-
if (scratch && ctx.hasUI) {
|
|
1002
|
-
const agreed = await ctx.ui.confirm(chain ? `Regenerate ${module} and its whole chain from scratch?` : `Regenerate ${module} from scratch?`, chain
|
|
1003
|
-
? `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 ***plain, from the bottom.`
|
|
1004
|
-
: `Its code and conformance tests are deleted - committed first, so /lineage ${module} keeps them - and the whole module is regenerated from its ***plain.`);
|
|
1005
|
-
if (!agreed)
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
1004
|
const loop = workLoopFor(jobs, cwd, ctx);
|
|
1009
1005
|
const model = jobModel(ctx, again.length > 0 ? "functionalities" : "spec_changes", undefined, plainFile);
|
|
1010
1006
|
const request = { ...(model !== undefined ? { model } : {}), ...(guidance !== "" ? { guidance } : {}) };
|
|
@@ -1242,44 +1238,6 @@ function configOf(plainFile) {
|
|
|
1242
1238
|
export function parallelJobs(cwd) {
|
|
1243
1239
|
return readProjectConfig(cwd).parallelJobs ?? defaultParallelJobs();
|
|
1244
1240
|
}
|
|
1245
|
-
/** The project's own files and a digest of each - not plain_modules, dependencies, or turri's and git's folders. */
|
|
1246
|
-
export function projectFiles(root) {
|
|
1247
|
-
const files = new Map();
|
|
1248
|
-
const walk = (dir, depth) => {
|
|
1249
|
-
if (depth > 8)
|
|
1250
|
-
return;
|
|
1251
|
-
let entries;
|
|
1252
|
-
try {
|
|
1253
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
1254
|
-
}
|
|
1255
|
-
catch {
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
for (const entry of entries) {
|
|
1259
|
-
if (entry.name.startsWith(".") || entry.name === "plain_modules" || entry.name === "node_modules" || entry.name === "__pycache__")
|
|
1260
|
-
continue;
|
|
1261
|
-
const path = join(dir, entry.name);
|
|
1262
|
-
if (entry.isDirectory())
|
|
1263
|
-
walk(path, depth + 1);
|
|
1264
|
-
else if (entry.isFile()) {
|
|
1265
|
-
try {
|
|
1266
|
-
files.set(relative(root, path), createHash("sha1").update(readFileSync(path)).digest("hex"));
|
|
1267
|
-
}
|
|
1268
|
-
catch {
|
|
1269
|
-
// A file that cannot be read is left out.
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
};
|
|
1274
|
-
walk(root, 0);
|
|
1275
|
-
return files;
|
|
1276
|
-
}
|
|
1277
|
-
/** The files added, changed or removed between two digests of the project. */
|
|
1278
|
-
export function changedFiles(before, after) {
|
|
1279
|
-
const changed = [...after].filter(([path, digest]) => before.get(path) !== digest).map(([path]) => path);
|
|
1280
|
-
const removed = [...before.keys()].filter((path) => !after.has(path)).map((path) => `${path} (removed)`);
|
|
1281
|
-
return [...changed, ...removed].sort();
|
|
1282
|
-
}
|
|
1283
1241
|
/**
|
|
1284
1242
|
* This machine, for the chat's agent, read afresh every turn: which wall is up under the agents' commands, whether the
|
|
1285
1243
|
* machine has the toolchains the project's modules name, and what in its testing scripts it lacks - for the person to
|
package/dist/genome.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { type Reporter } from "./graph.ts";
|
|
|
2
2
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type Skill } from "./skill.ts";
|
|
4
4
|
import { type AskUser } from "./tools.ts";
|
|
5
|
-
import type { JobRegistry } from "./regenerate/jobs.ts";
|
|
6
5
|
import type { WorkLoop } from "./regenerate/loop.ts";
|
|
7
6
|
export interface EscalationRequest {
|
|
8
7
|
step: string;
|
|
@@ -56,8 +55,6 @@ export interface RunSkillOptions {
|
|
|
56
55
|
* builds stand - and regenerate and halt for a skill that names them in its allowed-tools.
|
|
57
56
|
*/
|
|
58
57
|
loopTools?: ToolDefinition[];
|
|
59
|
-
/** The jobs the render queue runs, for a skill that waits for its builds and reads what they recorded. */
|
|
60
|
-
jobs?: JobRegistry;
|
|
61
58
|
/**
|
|
62
59
|
* Stops the run: its agents end their turns and it throws JobStoppedError, its manifest left as it was - an
|
|
63
60
|
* interrupted run, for `resume` to pick up. What it started on the work loop goes on.
|
package/dist/genome.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { Command, MemorySaver } from "@langchain/langgraph";
|
|
4
4
|
import { compileSkill } from "./graph.js";
|
|
5
5
|
import { createExecutor } from "./pi.js";
|
|
6
|
+
import { plainWriteTools } from "./plain-files.js";
|
|
6
7
|
import { toolchainsBlock } from "./stack.js";
|
|
7
8
|
import { clearOfStop, describeToolCall, describeToolResult, withStopSignal } from "./regenerate/activity.js";
|
|
8
9
|
import { createLiveReviewer } from "./review/live.js";
|
|
@@ -11,7 +12,6 @@ import { createAskUserTool, createEndQuestionsTool, createReviewTools, createWri
|
|
|
11
12
|
import { DECLINED, DISMISSED } from "./ask.js";
|
|
12
13
|
import { createQuestioning, levelOf, questioningQuestion } from "./questioning.js";
|
|
13
14
|
import { createSpecDiffTools } from "./spec-tools.js";
|
|
14
|
-
import { createMatureTools, MATURE_TOOLS } from "./mature-tools.js";
|
|
15
15
|
const CHANGE_PLAIN = "change_plain";
|
|
16
16
|
/** The conversation's tools a skill acts on the work loop with - never pi built-ins. */
|
|
17
17
|
const LOOP_TOOLS = ["regenerate", "drift", "halt", "conform"];
|
|
@@ -28,7 +28,6 @@ export async function runSkill(opts) {
|
|
|
28
28
|
return withStopSignal(opts.signal, () => runStoppable({
|
|
29
29
|
...opts,
|
|
30
30
|
...(opts.renderQueue !== undefined ? { renderQueue: clearOfStop(opts.renderQueue) } : {}),
|
|
31
|
-
...(opts.jobs !== undefined ? { jobs: clearOfStop(opts.jobs) } : {}),
|
|
32
31
|
...(opts.loopTools !== undefined ? { loopTools: opts.loopTools.map(clearOfStop) } : {}),
|
|
33
32
|
}));
|
|
34
33
|
}
|
|
@@ -128,14 +127,12 @@ async function runStoppable(opts) {
|
|
|
128
127
|
}
|
|
129
128
|
if (diffs !== undefined)
|
|
130
129
|
customTools.push(...diffs.tools);
|
|
131
|
-
// A skill that builds what the spec holds, waits for it, and reads back where the builds had to guess.
|
|
132
|
-
if (opts.renderQueue !== undefined && opts.jobs !== undefined && skill.allowedTools.some((tool) => MATURE_TOOLS.includes(tool))) {
|
|
133
|
-
customTools.push(...createMatureTools({ jobs: opts.jobs, cwd: opts.cwd }));
|
|
134
|
-
}
|
|
135
130
|
customTools.push(...(opts.sharedTools ?? []));
|
|
131
|
+
// Its write and edit reach the project's ***plain only - not the code, a release, or what an app it started writes.
|
|
132
|
+
customTools.push(...plainWriteTools(opts.cwd).filter((tool) => skill.allowedTools.includes(tool.name)));
|
|
136
133
|
const names = customTools.map((tool) => tool.name);
|
|
137
134
|
// Tools this harness provides are never pi built-ins, even when it did not provide them.
|
|
138
|
-
const builtins = skill.allowedTools.filter((t) => !names.includes(t) && t !== CHANGE_PLAIN && !
|
|
135
|
+
const builtins = skill.allowedTools.filter((t) => !names.includes(t) && t !== CHANGE_PLAIN && !LOOP_TOOLS.includes(t));
|
|
139
136
|
const executor = await createExecutor({
|
|
140
137
|
cwd: opts.cwd,
|
|
141
138
|
customTools,
|
package/dist/graph.js
CHANGED
|
@@ -87,8 +87,8 @@ function stepPrompt(skill, step, critique, context) {
|
|
|
87
87
|
`This skill's own files are in ${skill.dir}: a path its steps name, such as references/<file>.md, is there. ` +
|
|
88
88
|
`A skill it says to follow is beside it, in ${dirname(skill.dir)}/<skill name>/SKILL.md - read it and follow it.`,
|
|
89
89
|
"",
|
|
90
|
-
// The artifact exists for the gate's judges to read. Without a gate
|
|
91
|
-
// and asking for
|
|
90
|
+
// The artifact exists for the gate's judges to read. Without a gate its reply is what the step gives,
|
|
91
|
+
// and asking for a file is a tool call spent on what the reply already says.
|
|
92
92
|
step.gate
|
|
93
93
|
? `When you are done, call write_artifact with path "${artifactPath(step)}" holding the record the review will be checked against.`
|
|
94
94
|
: "",
|
|
@@ -133,6 +133,9 @@ export function compileSkill(opts) {
|
|
|
133
133
|
catch {
|
|
134
134
|
// Left empty on purpose: the gate treats a missing artifact as a failure.
|
|
135
135
|
}
|
|
136
|
+
// A step with no gate is asked for no artifact: what it found is its reply - an analysis's verdict, all it gives.
|
|
137
|
+
if (artifact === "" && !step.gate)
|
|
138
|
+
artifact = reply.trim();
|
|
136
139
|
if (artifact)
|
|
137
140
|
report.artifact(artifactPath(step), artifact);
|
|
138
141
|
return {
|
package/dist/new-project.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Starting a project from the conversation: the person says what they want built - "create a small server in Java
|
|
3
|
-
* Spring Boot" - and the chat
|
|
4
|
-
* ***plain itself, here, with no skill. An option picked that starts with a skill's command starts that skill, as if
|
|
5
|
-
* typed (`chosenCommand` in skills.ts).
|
|
3
|
+
* Spring Boot" - and the chat writes the ***plain itself, here, with no skill, regenerating from the first functional spec.
|
|
6
4
|
*/
|
|
7
5
|
export declare const NEW_PROJECT: string;
|
package/dist/new-project.js
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Starting a project from the conversation: the person says what they want built - "create a small server in Java
|
|
3
|
-
* Spring Boot" - and the chat
|
|
4
|
-
* ***plain itself, here, with no skill. An option picked that starts with a skill's command starts that skill, as if
|
|
5
|
-
* typed (`chosenCommand` in skills.ts).
|
|
3
|
+
* Spring Boot" - and the chat writes the ***plain itself, here, with no skill, regenerating from the first functional spec.
|
|
6
4
|
*/
|
|
7
5
|
export const NEW_PROJECT = [
|
|
8
6
|
"<new-project>",
|
|
9
|
-
"When the person asks for new software / project ",
|
|
10
|
-
"
|
|
11
|
-
"ask_user: one question, these three options, word for word, so they know the skills are there:",
|
|
12
|
-
`- "${"/brood - an interview: the stack, the testing and every feature settled with you, each answer regenerated"}"`,
|
|
13
|
-
`- "${"/spawn - something running in minutes: the quick start, asking the stack in one question unless your message names it"}"`,
|
|
14
|
-
`- "${"Just do it - no skill: I write it here and start regenerating"}"`,
|
|
15
|
-
"Picked /brood or /spawn: the answer starts it - call cultivate for it at once, with what they said as its context,",
|
|
16
|
-
"never asking them to type its command. Do not ask when their",
|
|
17
|
-
"message says already - they typed the command, or said to just build it, with no skill, without questions: then",
|
|
18
|
-
"start it yourself. Started yourself, how soon the first pulse starts, and something they can try, come first:",
|
|
7
|
+
"When the person asks for new software / project and there is no ***plain module for it yet, or they ask for a new",
|
|
8
|
+
"project, start it yourself, without asking how. How soon the first pulse starts, and something they can try, come first:",
|
|
19
9
|
"1. The stack: the one they named; otherwise one of the standard library's that this machine can build - common,",
|
|
20
10
|
" simple - said in a line. A stack named in a language the machine lacks is written all the same: tell them what to",
|
|
21
11
|
" install, and that turri doctor checks it. Read the stack's template, its language's and its kind's common first.",
|
package/dist/plain/parser.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface PlainFileParseResult {
|
|
|
21
21
|
}
|
|
22
22
|
export declare function getFilenameFromModuleName(moduleName: string): string;
|
|
23
23
|
export declare function getModuleNameFromFilename(filename: string): string;
|
|
24
|
+
export declare function hasScheme(target: string): boolean;
|
|
24
25
|
/** The links written in an item - what each says and where it points - as the parser reads them, checked or not. */
|
|
25
26
|
export declare function linksIn(section: AnyNode): Array<{
|
|
26
27
|
text: string;
|
package/dist/plain/parser.js
CHANGED
|
@@ -27,7 +27,7 @@ export function getFilenameFromModuleName(moduleName) {
|
|
|
27
27
|
export function getModuleNameFromFilename(filename) {
|
|
28
28
|
return filename.split(PLAIN_SOURCE_FILE_EXTENSION).join("");
|
|
29
29
|
}
|
|
30
|
-
function hasScheme(target) {
|
|
30
|
+
export function hasScheme(target) {
|
|
31
31
|
return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(target);
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* The project's ***plain and what it is built with, and a digest of each: its .plain files, the resources they link,
|
|
4
|
+
* its testing scripts and its config.turri*.yaml. Nothing else is a skill's to change - not the built code, a release,
|
|
5
|
+
* what a running app writes. `also`: paths that were among them before, digested while they are there, so one a skill
|
|
6
|
+
* stopped linking or naming is still seen if it changed it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function projectFiles(root: string, also?: Iterable<string>): Map<string, string>;
|
|
9
|
+
/** The files added, changed or removed between two digests of the project. */
|
|
10
|
+
export declare function changedFiles(before: Map<string, string>, after: Map<string, string>): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Why a skill's agent may not write a path, or nothing when it may. It writes the project's ***plain: a file of it
|
|
13
|
+
* (`projectFiles`), or a new one - a resource it is about to link, a testing script - anywhere in the project but where
|
|
14
|
+
* what is built, released or installed lives. Never an existing file that is not the ***plain's: the code, a release,
|
|
15
|
+
* what an app it started writes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function plainWriteRefusal(root: string, path: string): string | undefined;
|
|
18
|
+
type AnyTool = ToolDefinition<any, any, any>;
|
|
19
|
+
/** pi's write and edit for a skill's agent: each path checked by `plainWriteRefusal` - as given, and as the tool resolved it. */
|
|
20
|
+
export declare function plainWriteTools(root: string): AnyTool[];
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { createEditToolDefinition, createWriteToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { readProjectConfig, stacksOf } from "./config.js";
|
|
6
|
+
import { parseMarkdown } from "./plain/markdown.js";
|
|
7
|
+
import { hasScheme, linksIn } from "./plain/parser.js";
|
|
8
|
+
import { insideAny } from "./regenerate/confine.js";
|
|
9
|
+
import { credentialRefusal, isCredentialFile } from "./regenerate/credentials.js";
|
|
10
|
+
import { DIST_DIR, MODULES_DIR } from "./regenerate/paths.js";
|
|
11
|
+
import { PREPARE_NAME, SCRIPTS_DIR, scriptKind } from "./regenerate/test-scripts.js";
|
|
12
|
+
/** A config.turri.yaml, a stack's config.turri.<stack>.yaml - what the project's testing scripts are named in. */
|
|
13
|
+
const TURRI_CONFIG_NAME = /^config\.turri(\.[^/]+)?\.yaml$/;
|
|
14
|
+
/** Folders that hold what is built, installed or turri's own - never the project's ***plain. */
|
|
15
|
+
function outsidePlain(path) {
|
|
16
|
+
const parts = path.split(sep);
|
|
17
|
+
return parts[0] === DIST_DIR || parts.some((part) => part.startsWith(".") || part === MODULES_DIR || part === "node_modules" || part === "__pycache__");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The project's ***plain and what it is built with, and a digest of each: its .plain files, the resources they link,
|
|
21
|
+
* its testing scripts and its config.turri*.yaml. Nothing else is a skill's to change - not the built code, a release,
|
|
22
|
+
* what a running app writes. `also`: paths that were among them before, digested while they are there, so one a skill
|
|
23
|
+
* stopped linking or naming is still seen if it changed it.
|
|
24
|
+
*/
|
|
25
|
+
export function projectFiles(root, also = []) {
|
|
26
|
+
const all = [];
|
|
27
|
+
const walk = (dir, depth) => {
|
|
28
|
+
if (depth > 8)
|
|
29
|
+
return;
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (entry.name.startsWith(".") || entry.name === MODULES_DIR || entry.name === "node_modules" || entry.name === "__pycache__")
|
|
39
|
+
continue;
|
|
40
|
+
const path = join(dir, entry.name);
|
|
41
|
+
if (entry.isDirectory())
|
|
42
|
+
walk(path, depth + 1);
|
|
43
|
+
else if (entry.isFile())
|
|
44
|
+
all.push(relative(root, path));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
walk(root, 0);
|
|
48
|
+
const wanted = new Set(also);
|
|
49
|
+
const named = (path) => {
|
|
50
|
+
if (path !== undefined && path.trim() !== "")
|
|
51
|
+
wanted.add(relative(root, resolve(root, path.trim())));
|
|
52
|
+
};
|
|
53
|
+
for (const path of all) {
|
|
54
|
+
const name = basename(path);
|
|
55
|
+
if (path.split(sep)[0] === SCRIPTS_DIR || scriptKind(name) !== undefined || PREPARE_NAME.test(name) || TURRI_CONFIG_NAME.test(name))
|
|
56
|
+
wanted.add(path);
|
|
57
|
+
if (!path.endsWith(".plain"))
|
|
58
|
+
continue;
|
|
59
|
+
wanted.add(path);
|
|
60
|
+
// The resources it links, as the parser reads its links: from the project's folder.
|
|
61
|
+
try {
|
|
62
|
+
for (const { target } of linksIn(parseMarkdown(readFileSync(join(root, path), "utf8")))) {
|
|
63
|
+
if (target !== "" && !hasScheme(target) && !isAbsolute(target))
|
|
64
|
+
named(target);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// A file that cannot be read links nothing.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Scripts config.turri*.yaml names, whatever they are called.
|
|
72
|
+
for (const stack of [undefined, ...stacksOf(root)]) {
|
|
73
|
+
const config = readProjectConfig(root, stack);
|
|
74
|
+
named(config.unittestsScript);
|
|
75
|
+
named(config.conformanceTestsScript);
|
|
76
|
+
named(config.prepareEnvironmentScript);
|
|
77
|
+
}
|
|
78
|
+
const files = new Map();
|
|
79
|
+
for (const path of wanted) {
|
|
80
|
+
if (path.startsWith("..") || isAbsolute(path) || path.split(sep).includes(MODULES_DIR))
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
files.set(path, createHash("sha1").update(readFileSync(join(root, path))).digest("hex"));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// A file that cannot be read, or is not there, is left out.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
91
|
+
/** The files added, changed or removed between two digests of the project. */
|
|
92
|
+
export function changedFiles(before, after) {
|
|
93
|
+
const changed = [...after].filter(([path, digest]) => before.get(path) !== digest).map(([path]) => path);
|
|
94
|
+
const removed = [...before.keys()].filter((path) => !after.has(path)).map((path) => `${path} (removed)`);
|
|
95
|
+
return [...changed, ...removed].sort();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Why a skill's agent may not write a path, or nothing when it may. It writes the project's ***plain: a file of it
|
|
99
|
+
* (`projectFiles`), or a new one - a resource it is about to link, a testing script - anywhere in the project but where
|
|
100
|
+
* what is built, released or installed lives. Never an existing file that is not the ***plain's: the code, a release,
|
|
101
|
+
* what an app it started writes.
|
|
102
|
+
*/
|
|
103
|
+
export function plainWriteRefusal(root, path) {
|
|
104
|
+
const absolute = resolve(root, path);
|
|
105
|
+
if (!insideAny(absolute, [root]))
|
|
106
|
+
return `${absolute} is outside the project. A skill writes the project's ***plain, and nothing else.`;
|
|
107
|
+
if (isCredentialFile(absolute))
|
|
108
|
+
return credentialRefusal(absolute);
|
|
109
|
+
const inProject = relative(resolve(root), absolute);
|
|
110
|
+
if (outsidePlain(inProject)) {
|
|
111
|
+
return (`${inProject} is not the project's ***plain - ${MODULES_DIR}/, ${DIST_DIR}/ and the hidden folders hold what is ` +
|
|
112
|
+
"built, released and installed, and turri's own. A skill writes .plain files, the resources they link and the testing scripts.");
|
|
113
|
+
}
|
|
114
|
+
if (existsSync(absolute) && !projectFiles(root).has(inProject)) {
|
|
115
|
+
return (`${inProject} is not the project's ***plain: no .plain file links it, and it is not a testing script or a ` +
|
|
116
|
+
"config.turri*.yaml. A skill changes only those - link a resource from the ***plain before changing it.");
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
/** pi's write and edit for a skill's agent: each path checked by `plainWriteRefusal` - as given, and as the tool resolved it. */
|
|
121
|
+
export function plainWriteTools(root) {
|
|
122
|
+
const check = (path) => {
|
|
123
|
+
if (path === undefined)
|
|
124
|
+
return;
|
|
125
|
+
const refusal = plainWriteRefusal(root, path.trim().replace(/^@/, ""));
|
|
126
|
+
if (refusal !== undefined)
|
|
127
|
+
throw new Error(refusal);
|
|
128
|
+
};
|
|
129
|
+
const guard = (def) => ({
|
|
130
|
+
...def,
|
|
131
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
132
|
+
check(params.path);
|
|
133
|
+
return def.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
return [
|
|
137
|
+
guard(createWriteToolDefinition(root, {
|
|
138
|
+
operations: {
|
|
139
|
+
writeFile: async (path, content) => (check(path), writeFileSync(path, content)),
|
|
140
|
+
// A folder that is there already is not written: only where it would be made is checked.
|
|
141
|
+
mkdir: async (dir) => {
|
|
142
|
+
if (!existsSync(dir))
|
|
143
|
+
check(dir);
|
|
144
|
+
mkdirSync(dir, { recursive: true });
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
})),
|
|
148
|
+
guard(createEditToolDefinition(root, {
|
|
149
|
+
operations: {
|
|
150
|
+
readFile: async (path) => (check(path), readFileSync(path)),
|
|
151
|
+
writeFile: async (path, content) => (check(path), writeFileSync(path, content)),
|
|
152
|
+
access: async (path) => check(path),
|
|
153
|
+
},
|
|
154
|
+
})),
|
|
155
|
+
];
|
|
156
|
+
}
|
|
@@ -21,12 +21,15 @@ export interface DiffPaint {
|
|
|
21
21
|
export declare const PLAIN_PAINT: DiffPaint;
|
|
22
22
|
/**
|
|
23
23
|
* Where the lines of a diff are in the .plain files: by a line's place in the diff, the file it is in and its
|
|
24
|
-
* line there. Only lines that are in a file - added and unchanged ones; a removed line is in none now.
|
|
24
|
+
* line there. Only lines that are in a file - added and unchanged ones; a removed line is in none now. `written`
|
|
25
|
+
* is the line as the file has it - its links as written, not as the diff reads them - so a host can find it
|
|
26
|
+
* again in the file's text once it is edited; absent in a ledger written before it.
|
|
25
27
|
*/
|
|
26
28
|
export type DiffWhere = Record<number, {
|
|
27
29
|
file: string;
|
|
28
30
|
line: number;
|
|
29
31
|
section?: string;
|
|
32
|
+
written?: string;
|
|
30
33
|
}>;
|
|
31
34
|
/**
|
|
32
35
|
* Place a spec diff's lines in the .plain files they come from - `files`, the module's and every one it
|
|
@@ -25,7 +25,10 @@ function asRead(line) {
|
|
|
25
25
|
export function locateDiffLines(diff, files) {
|
|
26
26
|
const where = {};
|
|
27
27
|
// Each line as the parser renders it: a link `[text](path)` is the span `` `[resource]text` `` in the spec as read.
|
|
28
|
-
const texts = files.map((file) =>
|
|
28
|
+
const texts = files.map((file) => {
|
|
29
|
+
const written = file.text.split("\n").map((line) => line.trimEnd());
|
|
30
|
+
return { name: file.name, written, lines: written.map(asRead) };
|
|
31
|
+
});
|
|
29
32
|
// The section each line of a file is in: the nearest `***...***` header above it, or on it.
|
|
30
33
|
const sections = texts.map((file) => {
|
|
31
34
|
let current;
|
|
@@ -36,7 +39,7 @@ export function locateDiffLines(diff, files) {
|
|
|
36
39
|
const take = (file, line, index) => {
|
|
37
40
|
used.add(`${file}:${line}`);
|
|
38
41
|
const section = sections[file][line];
|
|
39
|
-
where[index] = { file: texts[file].name, line: line + 1, ...(section !== undefined ? { section } : {}) };
|
|
42
|
+
where[index] = { file: texts[file].name, line: line + 1, ...(section !== undefined ? { section } : {}), written: texts[file].written[line] };
|
|
40
43
|
last = { file, line };
|
|
41
44
|
};
|
|
42
45
|
diff
|
|
@@ -54,7 +57,7 @@ export function locateDiffLines(diff, files) {
|
|
|
54
57
|
if (file !== -1) {
|
|
55
58
|
const at = texts[file].lines.indexOf(removed);
|
|
56
59
|
const section = sections[file][at];
|
|
57
|
-
where[index] = { file: texts[file].name, line: at + 1, ...(section !== undefined ? { section } : {}) };
|
|
60
|
+
where[index] = { file: texts[file].name, line: at + 1, ...(section !== undefined ? { section } : {}), written: texts[file].written[at] };
|
|
58
61
|
}
|
|
59
62
|
return;
|
|
60
63
|
}
|
|
@@ -42,7 +42,7 @@ export function describeStop(facts) {
|
|
|
42
42
|
why = `after ${tries} attempt${tries === 1 ? "" : "s"} it still could not be built to pass its own unit tests.`;
|
|
43
43
|
todo = [
|
|
44
44
|
"The functional spec is most likely too big for one functionality, or leaves a choice open that keeps coming " +
|
|
45
|
-
"out wrong. Split it into smaller functional specs (`
|
|
45
|
+
"out wrong. Split it into smaller functional specs (`break-down-func-spec`), " +
|
|
46
46
|
"or say exactly what it should do.",
|
|
47
47
|
"If it needs something the stack does not name - a library, a tool, a service - add it to the implementation reqs.",
|
|
48
48
|
"If its unit tests have to be run in a particular way, say how in the implementation reqs.",
|
|
@@ -49,6 +49,8 @@ export interface TestScripts {
|
|
|
49
49
|
*/
|
|
50
50
|
export declare function withoutConformance(scripts: TestScripts): TestScripts;
|
|
51
51
|
export declare function scriptKind(name: string): ScriptKind | undefined;
|
|
52
|
+
/** The name the prepare script has: `prepare_environment_<lang>.sh`. */
|
|
53
|
+
export declare const PREPARE_NAME: RegExp;
|
|
52
54
|
export declare const SCRIPTS_DIR = "test_scripts";
|
|
53
55
|
/**
|
|
54
56
|
* The project a build belongs to: the folder that holds its plain_modules. A build folder
|
|
@@ -30,7 +30,7 @@ export function scriptKind(name) {
|
|
|
30
30
|
return Object.keys(NAMES).find((kind) => NAMES[kind].test(basename(name)));
|
|
31
31
|
}
|
|
32
32
|
/** The name the prepare script has: `prepare_environment_<lang>.sh`. */
|
|
33
|
-
const PREPARE_NAME = /^prepare_environment[^/]*\.sh$/i;
|
|
33
|
+
export const PREPARE_NAME = /^prepare_environment[^/]*\.sh$/i;
|
|
34
34
|
function isFile(path) {
|
|
35
35
|
try {
|
|
36
36
|
return statSync(path).isFile();
|
package/dist/skill-progress.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A skill's run as one line above the input: every phase by name, in order - the ones done ticked, the one it is in
|
|
3
|
-
* marked, the rest to come - and where that is, phase 2 of 4.
|
|
4
|
-
* questions and
|
|
3
|
+
* marked, the rest to come - and where that is, phase 2 of 4. A skill of many phases takes many turns of
|
|
4
|
+
* questions and ***plain changes; without it, nothing on the screen says how far through it is.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* onboard ✓ intent ━━━━ ⠋ stack ──── ○ product phase 2 of 3
|
|
7
7
|
* stack - the language, frameworks, storage and architecture
|
|
8
8
|
*
|
|
9
|
-
* The second line says what the phase it is in does: a phase's name alone - `
|
|
9
|
+
* The second line says what the phase it is in does: a phase's name alone - `stack` - says little to someone
|
|
10
10
|
* who has not read the skill.
|
|
11
11
|
*
|
|
12
12
|
* A skill of one phase, or none, has nothing to be partway through: its line is the spinner and its name alone.
|
package/dist/skill.d.ts
CHANGED
|
@@ -86,7 +86,7 @@ export interface Skill {
|
|
|
86
86
|
*/
|
|
87
87
|
diffPerAnswer: string[];
|
|
88
88
|
/**
|
|
89
|
-
* Started only by the person, with its slash command - `/
|
|
89
|
+
* Started only by the person, with its slash command - `/release-notes` - never by the model on its own
|
|
90
90
|
* (`metadata.command-only: true`): cultivate runs it only when the person has just typed its command.
|
|
91
91
|
*/
|
|
92
92
|
commandOnly: boolean;
|