@turri-dev/cli 0.1.7 → 0.1.8-next.e553476
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/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/jobs.d.ts +9 -0
- package/dist/regenerate/jobs.js +11 -0
- package/dist/regenerate/loop.d.ts +6 -0
- package/dist/regenerate/loop.js +24 -3
- package/dist/regenerate/records.d.ts +5 -3
- package/dist/regenerate/records.js +6 -4
- 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 +3 -3
- 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
|
@@ -49,7 +49,7 @@ turri init-config --vendor anthropic
|
|
|
49
49
|
|
|
50
50
|
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
51
|
|
|
52
|
-
Once `turri` is running,
|
|
52
|
+
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
53
|
|
|
54
54
|
No project of your own yet? Build one of [**the examples**](examples/README.md) with turri, step by step.
|
|
55
55
|
|
|
@@ -146,11 +146,11 @@ cd ~/my-project
|
|
|
146
146
|
turri
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
Then
|
|
149
|
+
Then start the interview:
|
|
150
150
|
|
|
151
|
-
>
|
|
151
|
+
> /brood a small API where people can register and sign in
|
|
152
152
|
|
|
153
|
-
It
|
|
153
|
+
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
154
|
|
|
155
155
|
- 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
156
|
- 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 +188,7 @@ Start at the [documentation index](docs/README.md), or go straight to a page:
|
|
|
188
188
|
| | |
|
|
189
189
|
| --- | --- |
|
|
190
190
|
| [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:
|
|
191
|
+
| [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
192
|
| [Using **turri**](docs/using.md) | the conversation, skills, tools, pulses and regen, choosing models, the command line |
|
|
193
193
|
| [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
194
|
| [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) {
|
|
@@ -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,
|
|
@@ -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;
|
package/dist/regenerate/loop.js
CHANGED
|
@@ -9,7 +9,7 @@ import { commitNotes, recordHistory, resetToLastCommit } from "./lineage.js";
|
|
|
9
9
|
import { NOTES_FILE, recordPulse } from "./notes.js";
|
|
10
10
|
import { createJobRegistry, formatDuration } from "./jobs.js";
|
|
11
11
|
import { moduleLanes } from "./lanes.js";
|
|
12
|
-
import { adoptDigests, chainTree, codeTree, conformanceTarget, REGENERATE_AGAIN, inheritedRecord, notBuilt, readRecord, RECORD_FILE, withResourceDigests, writeCodeRecord, writeTestsRecord, } from "./records.js";
|
|
12
|
+
import { adoptDigests, chainTree, codeTree, conformanceTarget, marksRegenerateAgain, REGENERATE_AGAIN, inheritedRecord, notBuilt, readRecord, RECORD_FILE, withResourceDigests, writeCodeRecord, writeTestsRecord, } from "./records.js";
|
|
13
13
|
import { describeSpecDiffConformance, renderSpecDiffConformance } from "./conform.js";
|
|
14
14
|
import { conformanceDirFor } from "./conformance.js";
|
|
15
15
|
import { modulesRoot } from "./paths.js";
|
|
@@ -153,7 +153,8 @@ export function functionalityCompletion(plainFile) {
|
|
|
153
153
|
codebase.forEach((entry, index) => {
|
|
154
154
|
const count = counts[index];
|
|
155
155
|
const completed = entry.frids.filter((frid) => !touched.has(`${entry.moduleName}:${frid}`));
|
|
156
|
-
|
|
156
|
+
// Tests marked to regenerate again were written against code since replaced: not conformance tested until rewritten.
|
|
157
|
+
const tested = (readRecord(conformanceDirFor(entry.moduleName, modulesRoot(projectDirOf(entry.plainFile))))?.[FUNCTIONAL_REQUIREMENTS] ?? []).filter((item) => item.frid !== undefined && !marksRegenerateAgain(item) && item.markdown.trim() === specText(entry.moduleName, item.frid));
|
|
157
158
|
count.completed = completed.length;
|
|
158
159
|
count.tested = completed.filter((frid) => tested.some((item) => item.frid === frid));
|
|
159
160
|
count.conformanceTested = count.tested.length;
|
|
@@ -521,7 +522,7 @@ export function createWorkLoop(options) {
|
|
|
521
522
|
run: async (onDelta) => {
|
|
522
523
|
// The guardrail: a conformance job tests only code that is built - the code record, read as the job starts, has every functional spec it tests as it tests it.
|
|
523
524
|
if (conformance) {
|
|
524
|
-
const testing = [...planned.cases.add, ...planned.cases.patch].map((touched) => touched.frid);
|
|
525
|
+
const testing = [...planned.cases.add, ...planned.cases.patch, ...planned.cases.again].map((touched) => touched.frid);
|
|
525
526
|
const missing = notBuilt(readRecord(tip.buildDir), planned.step.tree, testing);
|
|
526
527
|
if (missing.length > 0) {
|
|
527
528
|
const record = relative(options.cwd, join(tip.buildDir, RECORD_FILE));
|
|
@@ -1109,6 +1110,17 @@ export function createWorkLoop(options) {
|
|
|
1109
1110
|
found.add(frid);
|
|
1110
1111
|
const marked = items.map((item, index) => (frids.includes(named(item, index)) ? { ...item, [REGENERATE_AGAIN]: true } : item));
|
|
1111
1112
|
writeCodeRecord(tip.buildDir, { ...record, [section]: marked });
|
|
1113
|
+
// Its conformance tests were written against the code being replaced: marked with it, they are written again -
|
|
1114
|
+
// checked against the new code, and rewritten where they relied on what the old code chose - once it lands.
|
|
1115
|
+
if (section === FUNCTIONAL_REQUIREMENTS) {
|
|
1116
|
+
const testsDir = conformanceDirFor(tip.moduleName, modulesRoot(projectDirOf(tip.plainFile)));
|
|
1117
|
+
const tests = readRecord(testsDir);
|
|
1118
|
+
const tested = tests?.[FUNCTIONAL_REQUIREMENTS] ?? [];
|
|
1119
|
+
if (tested.some((item) => item.frid !== undefined && taken.includes(item.frid))) {
|
|
1120
|
+
const again = tested.map((item) => (item.frid !== undefined && taken.includes(item.frid) ? { ...item, [REGENERATE_AGAIN]: true } : item));
|
|
1121
|
+
writeTestsRecord(testsDir, { ...tests, [FUNCTIONAL_REQUIREMENTS]: again });
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1112
1124
|
recordHistory(tip.buildDir, `${module} functional spec${taken.length === 1 ? "" : "s"} ${taken.join(", ")} to regenerate again`);
|
|
1113
1125
|
}
|
|
1114
1126
|
const missing = frids.filter((frid) => !found.has(frid));
|
|
@@ -1600,6 +1612,14 @@ const defaultConformanceRunner = async (job, onDelta) => {
|
|
|
1600
1612
|
}
|
|
1601
1613
|
return { landed: result.passed, report: describeSpecDiffConformance(result), ...(result.specGaps !== undefined ? { specGaps: result.specGaps } : {}) };
|
|
1602
1614
|
};
|
|
1615
|
+
/**
|
|
1616
|
+
* 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
|
|
1617
|
+
* unit tests alone, with no review. A reviewer, and an arbiter after it, on a scaffold is time spent before anything can
|
|
1618
|
+
* be tried.
|
|
1619
|
+
*/
|
|
1620
|
+
export function skipsReview(cases) {
|
|
1621
|
+
return cases.fresh && cases.add.length <= 1 && cases.patch.length === 0 && cases.again.length === 0 && cases.delete.length === 0;
|
|
1622
|
+
}
|
|
1603
1623
|
/** A job as a spec diff render, landed on its unit tests: conformance is a track of its own. */
|
|
1604
1624
|
const defaultRunner = async (job, onDelta) => {
|
|
1605
1625
|
const result = await renderSpecDiff({
|
|
@@ -1609,6 +1629,7 @@ const defaultRunner = async (job, onDelta) => {
|
|
|
1609
1629
|
applied: "already applied",
|
|
1610
1630
|
spec: job.spec,
|
|
1611
1631
|
cases: job.planned.cases,
|
|
1632
|
+
...(skipsReview(job.planned.cases) ? { unitReview: false } : {}),
|
|
1612
1633
|
...(job.model !== undefined ? { model: job.model } : {}),
|
|
1613
1634
|
...(job.steering !== undefined ? { steering: job.steering } : {}),
|
|
1614
1635
|
onDelta,
|
|
@@ -63,9 +63,11 @@ export type Chosen = (frid: string) => boolean;
|
|
|
63
63
|
* definitions and implementation reqs, with the test reqs as the spec has them now.
|
|
64
64
|
*
|
|
65
65
|
* Conformance follows the code, never goes ahead of it: a functional spec whose code is not caught up
|
|
66
|
-
* with the spec - its text in the code record is not the spec's,
|
|
67
|
-
* all,
|
|
68
|
-
* taken out only once the spec and the code have both dropped it.
|
|
66
|
+
* with the spec - its text in the code record is not the spec's, the code record does not have it at
|
|
67
|
+
* all, or has it marked to regenerate again - stays as the tests record has it, until the code lands. Its tests are
|
|
68
|
+
* taken out only once the spec and the code have both dropped it. Tests marked to regenerate again with their code
|
|
69
|
+
* (`regenerateAgain`) differ from the target once the code lands: written against the code it replaced, they are
|
|
70
|
+
* conformance work again, though the spec's text never changed.
|
|
69
71
|
*/
|
|
70
72
|
export declare function conformanceTarget(code: MarshalledPlainSource | null, tests: MarshalledPlainSource | null, spec: MarshalledPlainSource, chosen?: Chosen): MarshalledPlainSource;
|
|
71
73
|
/**
|
|
@@ -155,9 +155,11 @@ export function inheritedRecord(source, sourceModule) {
|
|
|
155
155
|
* definitions and implementation reqs, with the test reqs as the spec has them now.
|
|
156
156
|
*
|
|
157
157
|
* Conformance follows the code, never goes ahead of it: a functional spec whose code is not caught up
|
|
158
|
-
* with the spec - its text in the code record is not the spec's,
|
|
159
|
-
* all,
|
|
160
|
-
* taken out only once the spec and the code have both dropped it.
|
|
158
|
+
* with the spec - its text in the code record is not the spec's, the code record does not have it at
|
|
159
|
+
* all, or has it marked to regenerate again - stays as the tests record has it, until the code lands. Its tests are
|
|
160
|
+
* taken out only once the spec and the code have both dropped it. Tests marked to regenerate again with their code
|
|
161
|
+
* (`regenerateAgain`) differ from the target once the code lands: written against the code it replaced, they are
|
|
162
|
+
* conformance work again, though the spec's text never changed.
|
|
161
163
|
*/
|
|
162
164
|
export function conformanceTarget(code, tests, spec, chosen = () => true) {
|
|
163
165
|
const built = itemsOf(code, FUNCTIONAL_REQUIREMENTS);
|
|
@@ -168,7 +170,7 @@ export function conformanceTarget(code, tests, spec, chosen = () => true) {
|
|
|
168
170
|
for (let index = 0; index < Math.max(built.length, now.length); index += 1) {
|
|
169
171
|
const frid = String(index + 1);
|
|
170
172
|
const current = now[index];
|
|
171
|
-
if (current !== undefined && built[index]?.markdown.trim() === current.markdown.trim()) {
|
|
173
|
+
if (current !== undefined && built[index]?.markdown.trim() === current.markdown.trim() && !marksRegenerateAgain(built[index])) {
|
|
172
174
|
if (chosen(frid))
|
|
173
175
|
specs.push({ ...current, frid });
|
|
174
176
|
continue;
|
|
@@ -8,7 +8,7 @@ import { conformanceDirFor } from "./conformance.js";
|
|
|
8
8
|
import { chainSpec } from "./loop.js";
|
|
9
9
|
import { modulesRoot, projectDirOf } from "./paths.js";
|
|
10
10
|
import { HISTORY_DIR } from "./lineage.js";
|
|
11
|
-
import { codeTree, readRecord, RECORD_FILE } from "./records.js";
|
|
11
|
+
import { codeTree, marksRegenerateAgain, readRecord, RECORD_FILE } from "./records.js";
|
|
12
12
|
import { specFilesOf } from "./regenerate.js";
|
|
13
13
|
import { builtTips, resolveRequiresChain } from "./requires.js";
|
|
14
14
|
import { changesBetween, layoutOf, serializeTree } from "./spec-steps.js";
|
|
@@ -115,17 +115,20 @@ export function pageItems(input) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
// Conformance tests: of the functional specs, those the tests record has as they read now, as they read before, or taken out.
|
|
118
|
-
|
|
118
|
+
// Tests marked to regenerate again were written against code since replaced: written for an earlier build, not now.
|
|
119
|
+
const testedItems = itemsOf(input.tested ?? null, FUNCTIONAL_REQUIREMENTS);
|
|
120
|
+
const tested = new Set(testedItems.filter((item) => !marksRegenerateAgain(item)).map((item) => item.markdown.trim()));
|
|
121
|
+
const replaced = new Set(testedItems.filter((item) => marksRegenerateAgain(item)).map((item) => item.markdown.trim()));
|
|
119
122
|
for (const item of items) {
|
|
120
|
-
if (item.section !== FUNCTIONAL_REQUIREMENTS ||
|
|
123
|
+
if (item.section !== FUNCTIONAL_REQUIREMENTS || testedItems.length === 0)
|
|
121
124
|
continue;
|
|
122
125
|
if (item.state === "removed") {
|
|
123
|
-
if (tested.has(item.text.trim()))
|
|
126
|
+
if (tested.has(item.text.trim()) || replaced.has(item.text.trim()))
|
|
124
127
|
item.tests = "removed";
|
|
125
128
|
}
|
|
126
129
|
else if (tested.has(item.text.trim()))
|
|
127
130
|
item.tests = "now";
|
|
128
|
-
else if (item.was !== undefined && tested.has(item.was.trim()))
|
|
131
|
+
else if (replaced.has(item.text.trim()) || (item.was !== undefined && (tested.has(item.was.trim()) || replaced.has(item.was.trim()))))
|
|
129
132
|
item.tests = "older";
|
|
130
133
|
}
|
|
131
134
|
return keyed(items);
|
package/dist/run-report.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Job } from "./regenerate/jobs.ts";
|
|
2
2
|
/**
|
|
3
3
|
* A headless run's report: one JSON file, `turri regenerate --report <file>`, saying what the run did - how it ended,
|
|
4
4
|
* what it spent, which pulses landed, how many functional specs are completed - for a benchmark or CI to read rather
|
package/dist/run-report.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { turriIdentity } from "./environment.js";
|
|
4
4
|
import { onModelEvent } from "./regenerate/activity.js";
|
|
5
|
+
import { firstRegeneration } from "./regenerate/jobs.js";
|
|
5
6
|
/**
|
|
6
7
|
* A headless run's report: one JSON file, `turri regenerate --report <file>`, saying what the run did - how it ended,
|
|
7
8
|
* what it spent, which pulses landed, how many functional specs are completed - for a benchmark or CI to read rather
|
|
@@ -177,7 +178,12 @@ export function startRunReport(options) {
|
|
|
177
178
|
};
|
|
178
179
|
});
|
|
179
180
|
const ran = list.filter((pulse) => pulse.status !== "queued");
|
|
181
|
+
const first = firstRegeneration(jobs, startedAt);
|
|
182
|
+
const seconds = (ms) => (ms !== undefined ? Math.round(ms / 100) / 10 : null);
|
|
180
183
|
return {
|
|
184
|
+
// The time to first regeneration - the first code pulse started - and to the first one landed, from the run's start.
|
|
185
|
+
first_started_seconds: seconds(first.started),
|
|
186
|
+
first_landed_seconds: seconds(first.landed),
|
|
181
187
|
counts: {
|
|
182
188
|
total: ran.length,
|
|
183
189
|
landed: ran.filter((pulse) => pulse.landed).length,
|
|
@@ -20,6 +20,6 @@ export declare function selfInvocation(command: string): string | undefined;
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function askedForCommandLine(message: string): boolean;
|
|
22
22
|
/** What the agent is told when the call is refused. */
|
|
23
|
-
export declare function refusal(what: string): string;
|
|
23
|
+
export declare function refusal(what: string, surface?: string): string;
|
|
24
24
|
/** The instruction that goes into the conversation's system prompt. */
|
|
25
25
|
export declare const TOOLS_NOT_CLI: string;
|
package/dist/self-invocation.js
CHANGED
|
@@ -88,12 +88,13 @@ export function askedForCommandLine(message) {
|
|
|
88
88
|
/(^|\s)pi\s+-p\b/.test(text));
|
|
89
89
|
}
|
|
90
90
|
/** What the agent is told when the call is refused. */
|
|
91
|
-
export function refusal(what) {
|
|
91
|
+
export function refusal(what, surface) {
|
|
92
92
|
const instead = what === "pi"
|
|
93
93
|
? "Do not start another agent: do the work here, with this conversation's tools."
|
|
94
94
|
: `Use ${TOOL_FOR[what] ?? "the matching turri tool"} instead.`;
|
|
95
95
|
return (`Not run: that starts turri${what === "pi" ? "'s agent" : ` ${what}`} again from the shell - a separate, headless ` +
|
|
96
|
-
|
|
96
|
+
// In a host there is no panel: the person watches the pulses where its surface says.
|
|
97
|
+
(surface !== undefined ? "run outside this conversation and its pulses, where the person can neither watch it nor halt it. " : "run outside this conversation, its jobs, the panel and /halt. ") +
|
|
97
98
|
`${instead} Run turri from the command line only when the user asks for that.`);
|
|
98
99
|
}
|
|
99
100
|
/** The instruction that goes into the conversation's system prompt. */
|
package/dist/skill-progress.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* brood ✓ intent ━━━━ ⠋ stack ──── ○ testing ──── ○ product phase 2 of 4
|
|
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 - `skeleton` - 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
|
@@ -85,6 +85,11 @@ export interface Skill {
|
|
|
85
85
|
* last answer is written with `write_spec_diff`, and changes the agent decides itself need no answer before them.
|
|
86
86
|
*/
|
|
87
87
|
diffPerAnswer: string[];
|
|
88
|
+
/**
|
|
89
|
+
* Started only by the person, with its slash command - `/brood` - never by the model on its own
|
|
90
|
+
* (`metadata.command-only: true`): cultivate runs it only when the person has just typed its command.
|
|
91
|
+
*/
|
|
92
|
+
commandOnly: boolean;
|
|
88
93
|
}
|
|
89
94
|
export declare function loadSkill(skillDir: string): Skill;
|
|
90
95
|
export declare function readRubric(skill: Skill, review: {
|
package/dist/skill.js
CHANGED
package/dist/skills.d.ts
CHANGED
|
@@ -28,3 +28,17 @@ export declare function discoverSkills(packageRoot: string, projectDir: string |
|
|
|
28
28
|
export declare function catalogueLine(skill: FoundSkill): string;
|
|
29
29
|
/** What is said under the list about the skills it leaves out. */
|
|
30
30
|
export declare function leftOutNote(catalogue: SkillCatalogue): string;
|
|
31
|
+
/**
|
|
32
|
+
* Which skills cultivate may start now. A skill marked command-only (`/brood`) waits for the person to type its command:
|
|
33
|
+
* the command lets it start once, and the run starting uses that up - the model cannot start it again later on its own.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createCommandGate(): {
|
|
36
|
+
/** The person typed the skill's command. */
|
|
37
|
+
typed(name: string): void;
|
|
38
|
+
/** Whether cultivate may start it now. */
|
|
39
|
+
mayStart(skill: Pick<FoundSkill, "name" | "commandOnly">): boolean;
|
|
40
|
+
/** Its run started: the command is used up. */
|
|
41
|
+
started(name: string): void;
|
|
42
|
+
};
|
|
43
|
+
/** What cultivate says of a command-only skill the person did not start. */
|
|
44
|
+
export declare function startedByCommandOnly(name: string): string;
|