@turri-dev/cli 0.1.7 → 0.1.8-next.01c7342
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 -7
- package/dist/btw.d.ts +2 -0
- package/dist/btw.js +2 -2
- package/dist/extension.d.ts +11 -0
- package/dist/extension.js +43 -5
- package/dist/identity.d.ts +6 -1
- package/dist/identity.js +33 -7
- package/dist/loop-tools.js +3 -1
- package/dist/regenerate/cases.js +3 -0
- package/dist/regenerate/conformance-agents.js +2 -2
- package/dist/regenerate/conformance.js +3 -3
- package/dist/regenerate/environment-error.d.ts +6 -0
- package/dist/regenerate/environment-error.js +19 -1
- package/dist/regenerate/jobs.d.ts +9 -0
- package/dist/regenerate/jobs.js +11 -0
- package/dist/regenerate/lineage.d.ts +9 -1
- package/dist/regenerate/lineage.js +47 -12
- package/dist/regenerate/loop.d.ts +6 -0
- package/dist/regenerate/loop.js +26 -5
- package/dist/regenerate/records.d.ts +5 -3
- package/dist/regenerate/records.js +6 -4
- package/dist/regenerate/regenerate.js +3 -3
- package/dist/regenerate/regression.js +2 -2
- package/dist/regenerate/spec-map.js +8 -5
- package/dist/run-report.d.ts +1 -1
- package/dist/run-report.js +6 -0
- package/dist/self-invocation.d.ts +1 -1
- package/dist/self-invocation.js +3 -2
- package/dist/skill-progress.d.ts +1 -1
- package/dist/skill.d.ts +5 -0
- package/dist/skill.js +1 -0
- package/dist/skills.d.ts +14 -0
- package/dist/skills.js +19 -1
- package/dist/surface.d.ts +5 -0
- package/dist/surface.js +18 -0
- package/docs/authoring-plain.md +2 -2
- package/docs/config.md +1 -1
- package/docs/headless.md +4 -0
- package/docs/hosting.md +17 -0
- package/docs/regenerating.md +4 -4
- package/docs/skills-and-gates.md +3 -1
- package/docs/using.md +6 -4
- package/genome/rules/func-specs.md +1 -0
- package/genome/rules/turri-regeneration.md +3 -0
- package/genome/skills/add-functional-spec/SKILL.md +12 -3
- package/genome/skills/add-functional-specs/SKILL.md +12 -2
- package/genome/skills/brood/SKILL.md +2 -0
- package/genome/skills/bud/SKILL.md +2 -0
- package/genome/skills/spawn/SKILL.md +47 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
<p align="center"><img src="docs/images/turri-banner.gif" alt="turri, beside its mark - the first coding agent with the power of regenerative software: npm install -g @turri-dev/cli - and a pixel jellyfish regenerating, from a polyp on the sea floor"></p>
|
|
2
2
|
|
|
3
|
-
<h1 align="center">turri<br>the first coding agent with the power of<br>regenerative software<br> </h1>
|
|
4
|
-
|
|
5
3
|
<p align="center">
|
|
6
4
|
<a href="https://www.npmjs.com/package/@turri-dev/cli"><img src="https://img.shields.io/npm/v/@turri-dev/cli?label=npm" alt="npm version"></a>
|
|
7
5
|
<a href="LICENSE"><img src="https://img.shields.io/npm/l/@turri-dev/cli" alt="MIT licence"></a>
|
|
@@ -49,7 +47,7 @@ turri init-config --vendor anthropic
|
|
|
49
47
|
|
|
50
48
|
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).
|
|
51
49
|
|
|
52
|
-
Once `turri` is running,
|
|
50
|
+
Once `turri` is running, type: `/spawn a small API where people can register and sign in` - or `/brood` for an interview that settles each feature with you first.
|
|
53
51
|
|
|
54
52
|
No project of your own yet? Build one of [**the examples**](examples/README.md) with turri, step by step.
|
|
55
53
|
|
|
@@ -146,11 +144,11 @@ cd ~/my-project
|
|
|
146
144
|
turri
|
|
147
145
|
```
|
|
148
146
|
|
|
149
|
-
Then
|
|
147
|
+
Then start the interview:
|
|
150
148
|
|
|
151
|
-
>
|
|
149
|
+
> /brood a small API where people can register and sign in
|
|
152
150
|
|
|
153
|
-
It
|
|
151
|
+
It asks you about the problem, the stack and the product, writes \*\*\*plain, and builds each functionality as it is written. While it works:
|
|
154
152
|
|
|
155
153
|
- the **panel** above the input shows every build - its stage, what its agents are doing, its model and cost - and the queue behind it;
|
|
156
154
|
- 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;
|
|
@@ -188,7 +186,7 @@ Start at the [documentation index](docs/README.md), or go straight to a page:
|
|
|
188
186
|
| | |
|
|
189
187
|
| --- | --- |
|
|
190
188
|
| [Installation](docs/installation.md) | doctor, what turri needs of this machine, **turri** and `pi`, environment variables |
|
|
191
|
-
| [Examples](examples/README.md) | projects to build with turri step by step:
|
|
189
|
+
| [Examples](examples/README.md) | projects to build with turri step by step: guestbook - a backend and a frontend in minutes - a CLI, and a backend service with conformance tests |
|
|
192
190
|
| [Using **turri**](docs/using.md) | the conversation, skills, tools, pulses and regen, choosing models, the command line |
|
|
193
191
|
| [The project config](docs/config.md) | `config.turri.yaml` and each stack's `config.turri.<stack>.yaml`: every key, models and their inheritance, conformance, the testing scripts, stacks and choosing one, the old `config.yaml` |
|
|
194
192
|
| [Deploy](docs/deploy.md) | trying what is built: the last commit in `dist/<module>`, started on your machine, nothing installed without asking |
|
package/dist/btw.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface SideQuestion {
|
|
|
53
53
|
projectDir: string;
|
|
54
54
|
/** turri's own folder: its source, skills, rules and docs. */
|
|
55
55
|
harnessDir: string;
|
|
56
|
+
/** A host's surface, in its own words (TurriHost.surface): where the person watches the work instead of the terminal. */
|
|
57
|
+
surface?: string;
|
|
56
58
|
}
|
|
57
59
|
export declare function sideQuestionPrompt(side: SideQuestion): string;
|
|
58
60
|
/** Answers a side question with an agent of its own, read only, on `model`. */
|
package/dist/btw.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { confinedTools } from "./regenerate/confine.js";
|
|
15
|
-
import {
|
|
15
|
+
import { conversationIdentity } from "./identity.js";
|
|
16
16
|
import { ask, BEGIN, createExecutor } from "./pi.js";
|
|
17
17
|
/** How many tool calls the side agent may make before it is told to answer. */
|
|
18
18
|
export const SIDE_CALLS = 20;
|
|
@@ -85,7 +85,7 @@ export function skillSessionText(runDir, maxChars = SKILL_SESSION_CHARS) {
|
|
|
85
85
|
}
|
|
86
86
|
export function sideQuestionPrompt(side) {
|
|
87
87
|
return [
|
|
88
|
-
|
|
88
|
+
conversationIdentity(side.surface),
|
|
89
89
|
"",
|
|
90
90
|
"The person is in the middle of something with you - a skill, a pulse - and has asked you a question on the side",
|
|
91
91
|
"with /btw. Answer it as turri: in the first person, to them, in whole sentences - briefly and plainly, from what is",
|
package/dist/extension.d.ts
CHANGED
|
@@ -41,6 +41,17 @@ export interface TurriHost {
|
|
|
41
41
|
onPlanChange?(): void;
|
|
42
42
|
/** The dialogs ask_user puts its questions in - the conversation's and a skill's interview - instead of the terminal's. */
|
|
43
43
|
dialogs?: AskUserIO;
|
|
44
|
+
/**
|
|
45
|
+
* The host's own UI, in its own words: the views it has in place of the terminal's, what its marks and buttons mean,
|
|
46
|
+
* where questions appear. Given, turri tells the model none of the terminal's - its keys, the side panel, regen,
|
|
47
|
+
* /pulses - and gives it this instead, as a `<surface>` block in turri's identity. Absent, the terminal is the surface.
|
|
48
|
+
*/
|
|
49
|
+
surface?: string;
|
|
50
|
+
/**
|
|
51
|
+
* The last word on the whole system prompt, as turri has built it for this turn - after its own before_agent_start
|
|
52
|
+
* work: what it returns is what is sent. For a host that needs to change anything else.
|
|
53
|
+
*/
|
|
54
|
+
systemPrompt?(prompt: string): string;
|
|
44
55
|
}
|
|
45
56
|
/** turri's conversation as an extension of pi's, for a host that runs it itself: see TurriHost. */
|
|
46
57
|
export declare function createTurriExtension(host?: TurriHost): (pi: ExtensionAPI) => void;
|
package/dist/extension.js
CHANGED
|
@@ -53,13 +53,14 @@ import { describeStdlib } from "./stdlib.js";
|
|
|
53
53
|
import { peekKeyListener, runAsk } from "./ask.js";
|
|
54
54
|
import { QuestionDialog } from "./question-dialog.js";
|
|
55
55
|
import { registerStacks } from "./stacks.js";
|
|
56
|
-
import { catalogueLine, discoverSkills, leftOutNote } from "./skills.js";
|
|
56
|
+
import { catalogueLine, createCommandGate, discoverSkills, leftOutNote, startedByCommandOnly } from "./skills.js";
|
|
57
57
|
import { supersededByTurri } from "./chat-skills.js";
|
|
58
|
-
import {
|
|
58
|
+
import { conversationIdentity } from "./identity.js";
|
|
59
|
+
import { setSurface, surfaceOf } from "./surface.js";
|
|
59
60
|
import { turriIdentity } from "./environment.js";
|
|
60
61
|
import { conformanceChooser } from "./regenerate/choose-conformance.js";
|
|
61
62
|
import { answerSideQuestion, askWithAsides, BTW_HOW, runningSkill, skillRunStarted, skillSessionText, transcriptOf } from "./btw.js";
|
|
62
|
-
import { createJobRegistry, defaultParallelJobs, describeJob, formatDuration, pulseName, shouldWait, STOPPED_ON_REQUEST, } from "./regenerate/jobs.js";
|
|
63
|
+
import { createJobRegistry, defaultParallelJobs, describeJob, firstRegeneration, formatDuration, formatElapsed, pulseName, shouldWait, STOPPED_ON_REQUEST, } from "./regenerate/jobs.js";
|
|
63
64
|
/**
|
|
64
65
|
* Makes gated skills reachable from a conversation.
|
|
65
66
|
*
|
|
@@ -180,6 +181,14 @@ export function chatAskUserTool(dialogs) {
|
|
|
180
181
|
},
|
|
181
182
|
};
|
|
182
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* The conversation's system prompt for a turn: pi's, turri's identity - with the host's surface in place of the
|
|
186
|
+
* terminal's, when it has one - and turri's blocks; then the host's last word on it.
|
|
187
|
+
*/
|
|
188
|
+
function conversationSystemPrompt(base, host, blocks) {
|
|
189
|
+
const prompt = [base, conversationIdentity(host.surface), ...blocks].filter((part) => part !== "").join("\n\n");
|
|
190
|
+
return host.systemPrompt !== undefined ? host.systemPrompt(prompt) : prompt;
|
|
191
|
+
}
|
|
183
192
|
/** The host each conversation's jobs report to, for the work loop made for them later. */
|
|
184
193
|
const hosts = new WeakMap();
|
|
185
194
|
/** turri's conversation as an extension of pi's, for a host that runs it itself: see TurriHost. */
|
|
@@ -260,6 +269,8 @@ function turriExtension(pi, host) {
|
|
|
260
269
|
},
|
|
261
270
|
});
|
|
262
271
|
hosts.set(jobs, host);
|
|
272
|
+
// What the model is told of where the person watches the work, deep in the tools too: the host's surface, or the terminal.
|
|
273
|
+
setSurface(jobs, host.surface);
|
|
263
274
|
host.onJobs?.(jobs);
|
|
264
275
|
// The ***plain rules go into the system prompt in full, every turn. The block is
|
|
265
276
|
// identical each time, so it sits inside the cached prefix rather than invalidating it.
|
|
@@ -298,7 +309,7 @@ function turriExtension(pi, host) {
|
|
|
298
309
|
}
|
|
299
310
|
}
|
|
300
311
|
return {
|
|
301
|
-
systemPrompt:
|
|
312
|
+
systemPrompt: conversationSystemPrompt(event.systemPrompt, host, [TOOLS_NOT_CLI, projectConfigBlock(cwd), machineBlock(cwd, machine.now()), rulesBlock()]),
|
|
302
313
|
};
|
|
303
314
|
});
|
|
304
315
|
// The conversation's agent does not run turri again through its shell, unless the person
|
|
@@ -314,7 +325,7 @@ function turriExtension(pi, host) {
|
|
|
314
325
|
if (event.toolName === "bash" && typeof command === "string") {
|
|
315
326
|
const what = selfInvocation(command);
|
|
316
327
|
if (what !== undefined && !askedForCommandLine(lastUserMessage))
|
|
317
|
-
return { block: true, reason: refusal(what) };
|
|
328
|
+
return { block: true, reason: refusal(what, surfaceOf(jobs)) };
|
|
318
329
|
}
|
|
319
330
|
markConversationWrite(event.toolCallId, event.toolName, event.input);
|
|
320
331
|
return undefined;
|
|
@@ -416,6 +427,19 @@ function turriExtension(pi, host) {
|
|
|
416
427
|
return;
|
|
417
428
|
const catalogue = skills.map(catalogueLine).join("\n");
|
|
418
429
|
const pendingNote = leftOutNote(discoveredSkills);
|
|
430
|
+
// The skills the person starts themselves - brood, spawn, bud: each has its command, and cultivate starts it only
|
|
431
|
+
// once its command is typed. The command puts what was typed in the chat, as the person's message, for the model to
|
|
432
|
+
// run it with what follows as its context.
|
|
433
|
+
const gate = createCommandGate();
|
|
434
|
+
for (const skill of skills.filter((s) => s.commandOnly)) {
|
|
435
|
+
pi.registerCommand(skill.name, {
|
|
436
|
+
description: `Start ${skill.name}${skill.steps.length > 1 ? ` - ${skill.steps.map((step) => step.name).join(" → ")}` : ""}: /${skill.name} [what you want]`,
|
|
437
|
+
handler: async (args) => {
|
|
438
|
+
gate.typed(skill.name);
|
|
439
|
+
pi.sendUserMessage(`/${skill.name}${args.trim() !== "" ? ` ${args.trim()}` : ""}`, { deliverAs: "followUp" });
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
}
|
|
419
443
|
pi.registerTool({
|
|
420
444
|
name: "cultivate",
|
|
421
445
|
label: "Cultivate",
|
|
@@ -426,6 +450,7 @@ function turriExtension(pi, host) {
|
|
|
426
450
|
promptSnippet: "cultivate: run a gated skill - its steps and review gates enforced by turri",
|
|
427
451
|
promptGuidelines: [
|
|
428
452
|
"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.",
|
|
453
|
+
"A skill marked as started only by the person runs only from its command: a message that is its command - /spawn a small server - is the person starting it, so call cultivate for it at once, with what follows the command as context. Never call cultivate for one otherwise; tell them to type its command.",
|
|
429
454
|
// pi lists .agents/skills among its own skills too: read and followed by hand, a skill's gates would be skipped.
|
|
430
455
|
"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.",
|
|
431
456
|
"cultivate returns the reviewed artifact and a trace of every gate decision. Report the artifact to the user; do not rewrite it.",
|
|
@@ -458,6 +483,8 @@ function turriExtension(pi, host) {
|
|
|
458
483
|
details: {},
|
|
459
484
|
};
|
|
460
485
|
}
|
|
486
|
+
if (!gate.mayStart(skill))
|
|
487
|
+
return { content: [{ type: "text", text: startedByCommandOnly(skill.name) }], details: { notStarted: true } };
|
|
461
488
|
// A new run clears the run folder. An interrupted one is not thrown away by accident:
|
|
462
489
|
// whether to pick it up or start over is the user's call.
|
|
463
490
|
const runDir = skillRunDir(cwd, skill.name);
|
|
@@ -475,6 +502,8 @@ function turriExtension(pi, host) {
|
|
|
475
502
|
details: { interrupted },
|
|
476
503
|
};
|
|
477
504
|
}
|
|
505
|
+
// Its command is used up: started again later, it is typed again.
|
|
506
|
+
gate.started(skill.name);
|
|
478
507
|
const trace = [`skill: ${skill.name}`, ...describeGates(skill).map((l) => ` ${l}`)];
|
|
479
508
|
// While it runs, what its agent is doing leads: pi shows a running tool's first lines, and the trace's first
|
|
480
509
|
// lines are the gates, which say nothing about now. The result keeps the trace alone.
|
|
@@ -560,6 +589,8 @@ function turriExtension(pi, host) {
|
|
|
560
589
|
const filesBefore = projectFiles(cwd);
|
|
561
590
|
// A side question asked while it runs is told what it has done so far.
|
|
562
591
|
const ended = skillRunStarted({ skill: skill.name, dir: skill.dir, step: () => live.step, trace: () => [...trace], runDir });
|
|
592
|
+
// From here the time to first regeneration is counted, for a skill that writes spec diffs: its first pulse started.
|
|
593
|
+
const began = Date.now();
|
|
563
594
|
const { result, output: captured } = await captureConsole(() => runSkill({
|
|
564
595
|
skillDir: skill.dir,
|
|
565
596
|
cwd,
|
|
@@ -620,11 +651,17 @@ function turriExtension(pi, host) {
|
|
|
620
651
|
? `${specsChanged.join(", ")} changed, and nothing is regenerated from that yet: call regenerate for it, and the code is brought up to the spec.`
|
|
621
652
|
: "";
|
|
622
653
|
const gated = skill.steps.some((step) => step.gate !== undefined);
|
|
654
|
+
const first = skill.allowedTools.includes("write_spec_diff") ? firstRegeneration(jobs.list(), began) : {};
|
|
655
|
+
const ttfr = first.started === undefined
|
|
656
|
+
? ""
|
|
657
|
+
: `Time to first regeneration: ${formatElapsed(first.started)} - its first pulse started that long after ${skill.name} began` +
|
|
658
|
+
(first.landed !== undefined ? `, and landed after ${formatElapsed(first.landed)}.` : ".");
|
|
623
659
|
const text = result.ok
|
|
624
660
|
? [
|
|
625
661
|
gated ? `${skill.name}: passed all gates.` : `${skill.name}: finished. It has no review gates, so nothing checked its work: read what it changed before you report it.`,
|
|
626
662
|
changed,
|
|
627
663
|
...(unrendered !== "" ? [unrendered] : []),
|
|
664
|
+
...(ttfr !== "" ? [ttfr] : []),
|
|
628
665
|
"",
|
|
629
666
|
"<artifact>",
|
|
630
667
|
result.output,
|
|
@@ -2083,6 +2120,7 @@ async function askOnTheSide(pi, ctx, cwd, jobs, question) {
|
|
|
2083
2120
|
pulses: jobs.list().map((job) => `${job.id} ${job.status}: ${pulseName(job)}`),
|
|
2084
2121
|
projectDir: cwd,
|
|
2085
2122
|
harnessDir: PACKAGE_ROOT,
|
|
2123
|
+
...(surfaceOf(jobs) !== undefined ? { surface: surfaceOf(jobs) } : {}),
|
|
2086
2124
|
}, chatModelOf(ctx));
|
|
2087
2125
|
try {
|
|
2088
2126
|
pi.appendEntry(SIDE_ENTRY, { question, answer });
|
package/dist/identity.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Who the conversation's agent is: turri, and what it does - first, so it speaks as turri, in the first person. Where
|
|
3
|
+
* the person watches the work is the terminal's regen; given a host's surface, that is said in its place.
|
|
4
|
+
*/
|
|
5
|
+
export declare function conversationIdentity(surface?: string): string;
|
|
6
|
+
/** turri as the terminal runs it. */
|
|
2
7
|
export declare const TURRI_IDENTITY: string;
|
package/dist/identity.js
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/** In the terminal: regen, where the person watches the work - the side panel, its key, and the pulses behind it. */
|
|
2
|
+
const REGEN = [
|
|
3
|
+
"- regen is the side panel (ctrl+h): the spec with each functionality and item marked built, regenerating,",
|
|
4
|
+
" planned or waiting - and behind it the pulses (/pulses), every pulse regenerating, planned and finished. Say regen,",
|
|
5
|
+
" lowercase. It was the regeneration, the job queue, or the job panel.",
|
|
6
|
+
];
|
|
7
|
+
/** In a host: where the person watches the work is the host's to say - its surface, below. */
|
|
8
|
+
const HOST_SURFACE = [
|
|
9
|
+
"- What the person sees of your work - where they watch it, and what they press - is the app's you run in, and",
|
|
10
|
+
" <surface> below says it in its own words. Say only what it says, and speak of no key, panel or view it does not",
|
|
11
|
+
" name.",
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Who the conversation's agent is: turri, and what it does - first, so it speaks as turri, in the first person. Where
|
|
15
|
+
* the person watches the work is the terminal's regen; given a host's surface, that is said in its place.
|
|
16
|
+
*/
|
|
17
|
+
export function conversationIdentity(surface) {
|
|
18
|
+
const text = surface?.trim() ?? "";
|
|
19
|
+
const hosted = text !== "";
|
|
20
|
+
return [
|
|
21
|
+
...IDENTITY_BEFORE,
|
|
22
|
+
...(hosted ? HOST_SURFACE : REGEN),
|
|
23
|
+
...IDENTITY_AFTER,
|
|
24
|
+
...(hosted ? ["", "<surface>", text, "</surface>"] : []),
|
|
25
|
+
"</turri>",
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
28
|
+
const IDENTITY_BEFORE = [
|
|
3
29
|
"<turri>",
|
|
4
30
|
"You are turri - always lowercase - a coding agent with the power of regenerative software. The software is its",
|
|
5
31
|
"***plain; the code is regenerated from it. You help the person write ***plain, and you regenerate the code from it a",
|
|
@@ -25,11 +51,11 @@ export const TURRI_IDENTITY = [
|
|
|
25
51
|
"- A pulse is one unit of work regenerating the code - a spec diff regenerated in a codebase, or its conformance",
|
|
26
52
|
" tests. It was called a job, and still is in places: the job_id a tool takes is a pulse's id, and parallel-jobs and",
|
|
27
53
|
" allow-to-stop-jobs in config.turri.yaml are about pulses. \"Job\" in what you read is a pulse; say pulse.",
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
" lowercase. It was the regeneration, the job queue, or the job panel.",
|
|
54
|
+
];
|
|
55
|
+
const IDENTITY_AFTER = [
|
|
31
56
|
"- mutate, in a skill's activity, is write_spec_diff: a spec change written and regenerated in one step.",
|
|
32
57
|
"- Regenerate is the word for what you do to the code: never grow, grew or grown - older text, a tool's description",
|
|
33
58
|
" or a report may still say it, and means regenerate.",
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
];
|
|
60
|
+
/** turri as the terminal runs it. */
|
|
61
|
+
export const TURRI_IDENTITY = conversationIdentity();
|
package/dist/loop-tools.js
CHANGED
|
@@ -9,6 +9,7 @@ import { pulseName } from "./regenerate/jobs.js";
|
|
|
9
9
|
import { stopDeployed } from "./regenerate/release.js";
|
|
10
10
|
import { projectDirOf } from "./regenerate/paths.js";
|
|
11
11
|
import { findTestScripts } from "./regenerate/test-scripts.js";
|
|
12
|
+
import { surfaceOf } from "./surface.js";
|
|
12
13
|
/** How the queue is to be planned, for the work this call queues - see regenerate/steer.ts. */
|
|
13
14
|
export const GUIDANCE_PARAM = Type.Optional(Type.String({
|
|
14
15
|
description: "How to plan the queue for this work, in a sentence or two - which functional specs go together, what first - as " +
|
|
@@ -114,8 +115,9 @@ export function stopJob(jobs, named, reason, loop) {
|
|
|
114
115
|
if (module !== undefined)
|
|
115
116
|
return module;
|
|
116
117
|
const matches = jobs.list().filter((job) => job.id.startsWith(named.trim()));
|
|
118
|
+
// /pulses is the terminal's: in a host, the pulses tool lists them.
|
|
117
119
|
if (matches.length === 0)
|
|
118
|
-
return `No pulse ${named}. /pulses shows them
|
|
120
|
+
return `No pulse ${named}. ${surfaceOf(jobs) !== undefined ? "The pulses tool lists them." : "/pulses shows them."}`;
|
|
119
121
|
if (matches.length > 1)
|
|
120
122
|
return `'${named}' matches ${matches.map((job) => job.id).join(", ")}. Give more of the id.`;
|
|
121
123
|
const target = matches[0];
|
package/dist/regenerate/cases.js
CHANGED
|
@@ -64,6 +64,9 @@ export function conformanceCasesBlock(cases) {
|
|
|
64
64
|
if (cases.patch.length > 0) {
|
|
65
65
|
lines.push(`- Changed - its text, its acceptance tests, or a concept it uses: ${named(cases.patch)}.`, " Its conformance tests exist: rewrite them to what it says now. The tests of everything else stay as they are.");
|
|
66
66
|
}
|
|
67
|
+
if (cases.again.length > 0) {
|
|
68
|
+
lines.push(`- Regenerated again - its code was built anew, and its conformance tests were written against the code it replaced: ${named(cases.again)}.`, " Its text did not change, but the code did. Run its tests against the new code. What they check that the", " specification says stays checked: rewrite such a test only as the new code needs it reached. What they check", " that the specification leaves open - a path, a field, a message, a format the old code chose and the", " specification never names - is the new code's choice now: rewrite those checks to what the new code does,", " or take them out. No test may hold the new code to what only the old code did. The tests of everything else", " stay as they are.");
|
|
69
|
+
}
|
|
67
70
|
if (cases.delete.length > 0)
|
|
68
71
|
lines.push(`- Taken out - remove its conformance tests: ${named(cases.delete)}.`);
|
|
69
72
|
if (cases.mechanical) {
|
|
@@ -316,9 +316,9 @@ export function createAskExplorerTool(options) {
|
|
|
316
316
|
return defineTool({
|
|
317
317
|
name: "ask_explorer",
|
|
318
318
|
label: "Ask the explorer",
|
|
319
|
-
description: "Ask the explorer - who read
|
|
319
|
+
description: "Ask the explorer - who read the code, and wrote the account of it you were given - something the account " +
|
|
320
320
|
"does not say: how a functionality is reached, what a field is called, what the software needs around it. It reads " +
|
|
321
|
-
`
|
|
321
|
+
`the code to answer - it runs nothing, so what only running the software shows is yours to try. ${options.questions ?? EXPLORER_QUESTIONS} questions a run: ask what reading the code yourself would take long to find.`,
|
|
322
322
|
promptSnippet: "ask_explorer: a question about how the software is reached, answered by the explorer from the code",
|
|
323
323
|
parameters: Type.Object({ question: Type.String({ description: "What you need to know, exactly" }) }),
|
|
324
324
|
async execute(_id, params) {
|
|
@@ -5,7 +5,7 @@ import { PLAIN_SOURCE_FILE_EXTENSION, plainFileParser } from "../plain/parser.js
|
|
|
5
5
|
import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, getFrids, getInheritedFunctionalSpecs, getSpecificationsForFrid, TEST_REQUIREMENTS, } from "../plain/spec.js";
|
|
6
6
|
import { reportActivity, reportLogNote, reportSpec, reportStage, unitOf } from "./activity.js";
|
|
7
7
|
import { confinedTools } from "./confine.js";
|
|
8
|
-
import { abandonWork, diffSinceSnapshot,
|
|
8
|
+
import { abandonWork, diffSinceSnapshot, landWork, moduleDirOf, startWork, workDiff, workSnapshot } from "./lineage.js";
|
|
9
9
|
import { createWorkLedger, describeAccounts, SUBMIT_WORK_NOTE, unanswered, unansweredPrompt } from "./submit-work.js";
|
|
10
10
|
import { CONFORMANCE_COMPACTION } from "./compaction.js";
|
|
11
11
|
import { NOTES_TURN, notesForSession, passedOutcome, readNotes, reflectOnReview, withNotes } from "./notes.js";
|
|
@@ -797,9 +797,9 @@ export async function renderConformanceTests(options) {
|
|
|
797
797
|
// The tests, and the code when it changed: one commit if they share a module folder.
|
|
798
798
|
const subject = `conformance ${moduleName} ${options.diff !== undefined ? "for a spec change" : `fs ${frid}`}${changed ? " (code changed)" : ""}`;
|
|
799
799
|
stage(`committing the conformance tests${changed ? " and the code changes" : ""}`);
|
|
800
|
-
|
|
800
|
+
landWork(conformanceDir, subject);
|
|
801
801
|
if (moduleDirOf(buildDir) !== moduleDirOf(conformanceDir))
|
|
802
|
-
|
|
802
|
+
landWork(buildDir, subject);
|
|
803
803
|
}
|
|
804
804
|
else if (start !== undefined) {
|
|
805
805
|
const why = notLandedWhy(options.diff !== undefined ? "for the spec change" : `of functionality ${frid}`, unlabelled);
|
|
@@ -128,6 +128,12 @@ export declare function missingToolchainError(run: {
|
|
|
128
128
|
script: string;
|
|
129
129
|
kind: "unit" | "conformance";
|
|
130
130
|
}): EnvironmentError | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Work that passed could not be committed into the module's history: the next pulse, putting the module back to its last
|
|
133
|
+
* commit, would undo it while turri took it as built - so the pulse does not land, and is rolled back like any other that
|
|
134
|
+
* did not, and the person is told why. `said` is what git said.
|
|
135
|
+
*/
|
|
136
|
+
export declare function historyError(moduleDir: string, said: string): EnvironmentError;
|
|
131
137
|
/**
|
|
132
138
|
* Before a pulse in `moduleDir` starts any agent: a credential `agent-env:` names that is not there stops it - every
|
|
133
139
|
* command that needs it would fail, and no agent may make one up. Nothing when all are there, or the folder is no
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { basename, dirname, isAbsolute, sep } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, sep } from "node:path";
|
|
4
4
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { passedCredentials } from "./agent-env.js";
|
|
@@ -246,6 +246,24 @@ export function missingToolchainError(run, details) {
|
|
|
246
246
|
const missing = missingToolchain(run);
|
|
247
247
|
return missing !== undefined ? toolchainError(missing, details) : undefined;
|
|
248
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Work that passed could not be committed into the module's history: the next pulse, putting the module back to its last
|
|
251
|
+
* commit, would undo it while turri took it as built - so the pulse does not land, and is rolled back like any other that
|
|
252
|
+
* did not, and the person is told why. `said` is what git said.
|
|
253
|
+
*/
|
|
254
|
+
export function historyError(moduleDir, said) {
|
|
255
|
+
const lock = /index\.lock/.test(said);
|
|
256
|
+
return new EnvironmentError({
|
|
257
|
+
moduleName: moduleNameOf(moduleDir),
|
|
258
|
+
missing: `a commit into the module's history, \`${join(moduleDir, ".plaingit")}\``,
|
|
259
|
+
neededBy: "the work that passed - without the commit, the next pulse would undo it",
|
|
260
|
+
shownBy: `git said: ${said.replace(/\s+/g, " ").trim().slice(0, 600)}`,
|
|
261
|
+
fix: lock
|
|
262
|
+
? `if no git is still running on it, remove \`${join(moduleDir, ".plaingit", "index.lock")}\`, which a git stopped mid-commit left behind.`
|
|
263
|
+
: "fix what git said - a full disk, files it cannot read - so a commit can be made there.",
|
|
264
|
+
source: "harness",
|
|
265
|
+
});
|
|
266
|
+
}
|
|
249
267
|
/** The module a folder is part of, by name: `plain_modules/<module>/...`. */
|
|
250
268
|
function moduleNameOf(moduleDir) {
|
|
251
269
|
return basename(moduleDir);
|
|
@@ -163,6 +163,15 @@ export declare function createJobRegistry(options?: {
|
|
|
163
163
|
export declare function formatDuration(job: Job): string;
|
|
164
164
|
/** "42s", "3m 12s", "1h 5m": how long something has run. */
|
|
165
165
|
export declare function formatElapsed(ms: number): string;
|
|
166
|
+
/**
|
|
167
|
+
* How soon after `since` the first code pulse queued since then started - the time to first regeneration - and how soon
|
|
168
|
+
* the first of them landed, when one has: from then on there is something to try. In milliseconds; either is undefined
|
|
169
|
+
* until it has happened.
|
|
170
|
+
*/
|
|
171
|
+
export declare function firstRegeneration(jobs: Job[], since: number): {
|
|
172
|
+
started?: number;
|
|
173
|
+
landed?: number;
|
|
174
|
+
};
|
|
166
175
|
/**
|
|
167
176
|
* What a pulse is called wherever it is shown in a line: its module, kind and number - `shorty · regenerate pulse 3`,
|
|
168
177
|
* `shorty · conformance pulse 6` - never what it regenerates, which is the agents' words and long. A job of no module,
|
package/dist/regenerate/jobs.js
CHANGED
|
@@ -377,6 +377,17 @@ export function formatElapsed(ms) {
|
|
|
377
377
|
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
378
378
|
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
379
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* How soon after `since` the first code pulse queued since then started - the time to first regeneration - and how soon
|
|
382
|
+
* the first of them landed, when one has: from then on there is something to try. In milliseconds; either is undefined
|
|
383
|
+
* until it has happened.
|
|
384
|
+
*/
|
|
385
|
+
export function firstRegeneration(jobs, since) {
|
|
386
|
+
const pulses = jobs.filter((job) => job.kind === "regenerate" && job.queuedAt >= since && job.status !== "queued");
|
|
387
|
+
const started = Math.min(...pulses.map((job) => job.startedAt));
|
|
388
|
+
const landed = Math.min(...pulses.filter((job) => job.status === "succeeded" && job.details?.landed === true && job.finishedAt !== undefined).map((job) => job.finishedAt));
|
|
389
|
+
return { ...(Number.isFinite(started) ? { started: started - since } : {}), ...(Number.isFinite(landed) ? { landed: landed - since } : {}) };
|
|
390
|
+
}
|
|
380
391
|
/**
|
|
381
392
|
* What a pulse is called wherever it is shown in a line: its module, kind and number - `shorty · regenerate pulse 3`,
|
|
382
393
|
* `shorty · conformance pulse 6` - never what it regenerates, which is the agents' words and long. A job of no module,
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* A commit is made after every step that changes the module - a render, a regeneration, a
|
|
11
11
|
* conformance run, a regression repair, a spec change, a start from the module below - with
|
|
12
12
|
* a message saying what it was. History is a record of the work, never a gate on it: a git
|
|
13
|
-
* failure is swallowed, not raised
|
|
13
|
+
* failure is swallowed, not raised - except landing work (`landWork`), whose commit the next
|
|
14
|
+
* start would otherwise undo.
|
|
14
15
|
*/
|
|
15
16
|
export declare const HISTORY_DIR = ".plaingit";
|
|
16
17
|
/** The module folder a build or tests directory belongs to. */
|
|
@@ -20,6 +21,13 @@ export declare function moduleDirOf(dir: string): string;
|
|
|
20
21
|
* directory - belongs to. Nothing changed, nothing is committed. Returns the commit, if any.
|
|
21
22
|
*/
|
|
22
23
|
export declare function recordHistory(dir: string, subject: string, body?: string): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Commit work that landed - the code or tests a pulse passed, with the record of what they are built from. Unlike any
|
|
26
|
+
* other step's, this commit is not only a record: the next unit of work puts the module back to its last commit, and
|
|
27
|
+
* would undo what landed with it. So a commit that cannot be made is raised, as the environment's, with what git said -
|
|
28
|
+
* the work left in place, uncommitted. Nothing changed, nothing is committed. Returns the commit, if any.
|
|
29
|
+
*/
|
|
30
|
+
export declare function landWork(dir: string, subject: string, body?: string): string | undefined;
|
|
23
31
|
/**
|
|
24
32
|
* The notes the harness writes once a pulse has ended, committed - or the next pulse, putting the module back to its last
|
|
25
33
|
* commit, would take them away. Into the pulse's own commit when it landed one - `landedSince` (ms), when it started - so
|
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { reportActivity } from "./activity.js";
|
|
5
|
+
import { historyError } from "./environment-error.js";
|
|
5
6
|
import { projectRootOf } from "./paths.js";
|
|
6
7
|
/**
|
|
7
8
|
* Version history for each module's generated code and tests.
|
|
@@ -15,7 +16,8 @@ import { projectRootOf } from "./paths.js";
|
|
|
15
16
|
* A commit is made after every step that changes the module - a render, a regeneration, a
|
|
16
17
|
* conformance run, a regression repair, a spec change, a start from the module below - with
|
|
17
18
|
* a message saying what it was. History is a record of the work, never a gate on it: a git
|
|
18
|
-
* failure is swallowed, not raised
|
|
19
|
+
* failure is swallowed, not raised - except landing work (`landWork`), whose commit the next
|
|
20
|
+
* start would otherwise undo.
|
|
19
21
|
*/
|
|
20
22
|
export const HISTORY_DIR = ".plaingit";
|
|
21
23
|
const AUTHOR = ["-c", "user.name=turri", "-c", "user.email=turri@localhost", "-c", "commit.gpgsign=false"];
|
|
@@ -39,7 +41,10 @@ const EXCLUDED = [
|
|
|
39
41
|
".DS_Store",
|
|
40
42
|
];
|
|
41
43
|
function git(moduleDir, args, input) {
|
|
42
|
-
|
|
44
|
+
const gitDir = join(moduleDir, HISTORY_DIR);
|
|
45
|
+
// The history is turri's, not the person's: hooks their git config names - gitleaks, husky - would refuse some of its
|
|
46
|
+
// commits, and the next start would put back what landed. Its own hooks folder holds only git's samples.
|
|
47
|
+
return spawnSync("git", [...AUTHOR, "-c", `core.hooksPath=${join(gitDir, "hooks")}`, "--git-dir", gitDir, "--work-tree", moduleDir, ...args], {
|
|
43
48
|
cwd: moduleDir,
|
|
44
49
|
encoding: "utf8",
|
|
45
50
|
input,
|
|
@@ -79,21 +84,51 @@ function ensureHistory(moduleDir) {
|
|
|
79
84
|
*/
|
|
80
85
|
export function recordHistory(dir, subject, body) {
|
|
81
86
|
try {
|
|
82
|
-
|
|
83
|
-
if (!existsSync(moduleDir) || !ensureHistory(moduleDir))
|
|
84
|
-
return undefined;
|
|
85
|
-
git(moduleDir, ["add", "-A"]);
|
|
86
|
-
if (git(moduleDir, ["diff", "--cached", "--quiet"]).status === 0)
|
|
87
|
-
return undefined;
|
|
88
|
-
const message = body ? `${subject}\n\n${body.trim()}\n` : subject;
|
|
89
|
-
if (git(moduleDir, ["commit", "-q", "-F", "-"], message).status !== 0)
|
|
90
|
-
return undefined;
|
|
91
|
-
return head(dir);
|
|
87
|
+
return commitAll(dir, subject, body).commit;
|
|
92
88
|
}
|
|
93
89
|
catch {
|
|
94
90
|
return undefined;
|
|
95
91
|
}
|
|
96
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Commit work that landed - the code or tests a pulse passed, with the record of what they are built from. Unlike any
|
|
95
|
+
* other step's, this commit is not only a record: the next unit of work puts the module back to its last commit, and
|
|
96
|
+
* would undo what landed with it. So a commit that cannot be made is raised, as the environment's, with what git said -
|
|
97
|
+
* the work left in place, uncommitted. Nothing changed, nothing is committed. Returns the commit, if any.
|
|
98
|
+
*/
|
|
99
|
+
export function landWork(dir, subject, body) {
|
|
100
|
+
const moduleDir = moduleDirOf(dir);
|
|
101
|
+
let made;
|
|
102
|
+
try {
|
|
103
|
+
made = commitAll(dir, subject, body);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
made = { failed: error instanceof Error ? error.message : String(error) };
|
|
107
|
+
}
|
|
108
|
+
if (made.failed !== undefined)
|
|
109
|
+
throw historyError(moduleDir, made.failed);
|
|
110
|
+
return made.commit;
|
|
111
|
+
}
|
|
112
|
+
/** Commit everything the step changed in the module `dir` belongs to: the commit, nothing when nothing changed, or why git failed. */
|
|
113
|
+
function commitAll(dir, subject, body) {
|
|
114
|
+
const moduleDir = moduleDirOf(dir);
|
|
115
|
+
if (!existsSync(moduleDir))
|
|
116
|
+
return {};
|
|
117
|
+
if (!ensureHistory(moduleDir))
|
|
118
|
+
return { failed: `git could not make the module's history in ${join(moduleDir, HISTORY_DIR)}` };
|
|
119
|
+
const failed = (result) => (result.error?.message ?? `${result.stderr}${result.stdout}`.trim()) || `git exited ${result.status}`;
|
|
120
|
+
const added = git(moduleDir, ["add", "-A"]);
|
|
121
|
+
if (added.status !== 0)
|
|
122
|
+
return { failed: failed(added) };
|
|
123
|
+
if (git(moduleDir, ["diff", "--cached", "--quiet"]).status === 0)
|
|
124
|
+
return {};
|
|
125
|
+
const message = body ? `${subject}\n\n${body.trim()}\n` : subject;
|
|
126
|
+
const committed = git(moduleDir, ["commit", "-q", "-F", "-"], message);
|
|
127
|
+
if (committed.status !== 0)
|
|
128
|
+
return { failed: failed(committed) };
|
|
129
|
+
const commit = head(dir);
|
|
130
|
+
return commit !== undefined ? { commit } : {};
|
|
131
|
+
}
|
|
97
132
|
/**
|
|
98
133
|
* The notes the harness writes once a pulse has ended, committed - or the next pulse, putting the module back to its last
|
|
99
134
|
* commit, would take them away. Into the pulse's own commit when it landed one - `landedSince` (ms), when it started - so
|
|
@@ -385,6 +385,12 @@ export declare function isUpToDate(status: ModuleStatus): boolean;
|
|
|
385
385
|
export declare function describeModules(statuses: ModuleStatus[], options?: {
|
|
386
386
|
for?: "chat" | "command line";
|
|
387
387
|
}): string;
|
|
388
|
+
/**
|
|
389
|
+
* A module's first pulse when it builds one functional spec - the project set up and the smallest thing that runs: it lands on its
|
|
390
|
+
* unit tests alone, with no review. A reviewer, and an arbiter after it, on a scaffold is time spent before anything can
|
|
391
|
+
* be tried.
|
|
392
|
+
*/
|
|
393
|
+
export declare function skipsReview(cases: JobCases): boolean;
|
|
388
394
|
/** How a code pulse went, for its line in the notes: the rounds it took, its test runs, what the debugger found. */
|
|
389
395
|
export declare function codeStory(result: {
|
|
390
396
|
attempts?: number;
|