@turri-dev/cli 0.1.16-next.b861614 → 0.1.16-next.ce2cd46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/themes/turri-dark.json +1 -1
- package/dist/extension.js +2 -1
- package/dist/init-config.d.ts +1 -1
- package/dist/init-config.js +1 -1
- package/dist/loop-tools.js +1 -1
- package/dist/regenerate/cases.js +2 -2
- package/dist/regenerate/change-view.js +5 -3
- package/dist/regenerate/conform.js +0 -2
- package/dist/regenerate/delivery.d.ts +1 -5
- package/dist/regenerate/delivery.js +2 -9
- package/dist/regenerate/loop.js +1 -2
- package/dist/regenerate/regenerate.js +27 -9
- package/dist/regenerate/release.d.ts +3 -7
- package/dist/regenerate/release.js +59 -15
- package/dist/regenerate/testing.d.ts +7 -0
- package/dist/regenerate/testing.js +14 -0
- package/dist/regenerate-cli.js +1 -1
- package/dist/release-agent.d.ts +1 -1
- package/dist/release-agent.js +3 -3
- package/dist/release-tools.js +2 -1
- package/dist/skill-progress.d.ts +1 -1
- package/docs/authoring-plain.md +2 -2
- package/docs/config.md +1 -1
- package/docs/deploy.md +4 -6
- package/docs/hosting.md +1 -1
- package/docs/regenerating.md +3 -3
- package/docs/using.md +2 -2
- package/genome/rules/func-specs.md +30 -0
- package/genome/rules/impl-reqs.md +1 -1
- package/genome/rules/integration-embedded-testing.md +1 -1
- package/genome/rules/integration-embedded.md +1 -1
- package/genome/rules/integration-standalone.md +1 -1
- package/genome/rules/linked-resources.md +4 -0
- package/genome/rules/project-model.md +1 -1
- package/genome/rules/test-reqs.md +1 -1
- package/genome/rules/turri-regeneration.md +1 -1
- package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +5 -5
- package/genome/skills/break-down-func-spec/SKILL.md +1 -1
- package/genome/skills/brood/references/product.md +2 -2
- package/genome/skills/bud/SKILL.md +4 -4
- package/genome/skills/debug-specs/SKILL.md +6 -6
- package/genome/skills/mature/SKILL.md +1 -1
- package/genome/skills/resolve-section-ownership/SKILL.md +4 -4
- package/genome/skills/resolve-spec-conflict/SKILL.md +3 -3
- package/genome/skills/spawn/SKILL.md +2 -2
- package/package.json +1 -1
- package/genome/skills/add-acceptance-test/SKILL.md +0 -100
- package/genome/skills/add-concept/SKILL.md +0 -71
- package/genome/skills/add-functional-spec/SKILL.md +0 -93
- package/genome/skills/add-functional-specs/SKILL.md +0 -128
- package/genome/skills/add-implementation-requirement/SKILL.md +0 -85
- package/genome/skills/add-resource/SKILL.md +0 -105
- package/genome/skills/add-template/SKILL.md +0 -64
- package/genome/skills/add-test-requirement/SKILL.md +0 -73
package/dist/extension.js
CHANGED
|
@@ -1995,7 +1995,8 @@ export const JOB_FINISHED_REACTION = [
|
|
|
1995
1995
|
"someone), do that, as it says. Otherwise say in a line or two what finished and what it means for the person - the",
|
|
1996
1996
|
"report is theirs to read in full; do not repeat it - and, when it failed, what they can do. Start no other work.",
|
|
1997
1997
|
"A report that says \"Released after the pulse\" was released by turri itself, as the pulse's last stage: do not call",
|
|
1998
|
-
"release for it
|
|
1998
|
+
"release for it. Say in a line what was released, or why nothing was - then, when it was released, do what you were",
|
|
1999
|
+
"told to do once something is released (opening it where the person can watch it, saying what to test), if anything.",
|
|
1999
2000
|
"A pulse that stopped on its environment - \"stopped: turri's sandbox kept its agents from…\" or \"stopped: the",
|
|
2000
2001
|
"environment lacks…\" - stops the same way again until that is fixed: never regenerate it before. Heal it, then regenerate the",
|
|
2001
2002
|
"module to resume:",
|
package/dist/init-config.d.ts
CHANGED
|
@@ -112,7 +112,7 @@ export declare const DEFAULTS: {
|
|
|
112
112
|
readonly review: false;
|
|
113
113
|
readonly mutationTesting: true;
|
|
114
114
|
readonly allowToStopJobs: true;
|
|
115
|
-
readonly releaseAfterPulse:
|
|
115
|
+
readonly releaseAfterPulse: true;
|
|
116
116
|
readonly testingScriptsRunOn: "in-place";
|
|
117
117
|
readonly turnLimit: 120;
|
|
118
118
|
readonly compactAt: 100000;
|
package/dist/init-config.js
CHANGED
package/dist/loop-tools.js
CHANGED
|
@@ -165,7 +165,7 @@ export function regenerateTool(options) {
|
|
|
165
165
|
"editor. It takes no diff: what is left to regenerate - from what each module's code was regenerated from to its spec now - is " +
|
|
166
166
|
"worked out and planned into pulses, and a pulse running that regenerates something the spec no longer wants is stopped and " +
|
|
167
167
|
"rolled back. Nothing to do where the code is regenerated from the spec as it is. With functionalities named, those are " +
|
|
168
|
-
"regenerated though their spec did not change -
|
|
168
|
+
"regenerated though their spec did not change - their code taken out and built anew, better made, never left as it was - " +
|
|
169
169
|
"planned into pulses as any change is, the ones next to each other together; their conformance tests stay as they are.",
|
|
170
170
|
promptSnippet: "regenerate a module's code from its spec as it is now - after any change to the spec",
|
|
171
171
|
promptGuidelines: [
|
package/dist/regenerate/cases.js
CHANGED
|
@@ -19,7 +19,7 @@ export function codeCasesBlock(cases) {
|
|
|
19
19
|
lines.push(`- Changed - its text, or a concept it uses: ${named(cases.patch)}.`, " Its code and unit tests exist: change them to match what it says now. Do not build it again from scratch.");
|
|
20
20
|
}
|
|
21
21
|
if (cases.again.length > 0) {
|
|
22
|
-
lines.push(`- Regenerated again - its code and unit tests are built, and are to be regenerated again: ${named(cases.again)}.`, "
|
|
22
|
+
lines.push(`- Regenerated again - its code and unit tests are built, and are to be regenerated again: ${named(cases.again)}.`, " Take its implementation out and build it anew from the specification - made better, and doing exactly what", " the specification says now. Its text being unchanged, or the old code already doing what it says, is no reason", " to keep it: the person asked for it to be built again, and work that leaves it as it was has not done that.", " Take out what the new code replaces: no second implementation beside the old one, nothing of the old left", " unused. What the rest of the code uses from it keeps working. Write its unit tests anew too, to test what the", " specification says.");
|
|
23
23
|
}
|
|
24
24
|
if (cases.delete.length > 0) {
|
|
25
25
|
lines.push(`- Taken out - remove its code, and the unit tests only it had, where nothing that remains needs them: ${named(cases.delete)}.`);
|
|
@@ -45,7 +45,7 @@ export function codeReviewCasesBlock(cases) {
|
|
|
45
45
|
...(cases.patch.length > 0 ? [`- Changed: ${named(cases.patch)} - the code and its tests changed to match it now, not left as they were.`] : []),
|
|
46
46
|
...(cases.again.length > 0
|
|
47
47
|
? [
|
|
48
|
-
`- Regenerated again: ${named(cases.again)} - its implementation
|
|
48
|
+
`- Regenerated again: ${named(cases.again)} - its implementation built anew, not kept or patched beside the old one: nothing of what it`,
|
|
49
49
|
" replaced left behind or duplicated, it does what the specification says, what the rest of the code uses from it",
|
|
50
50
|
" still works, and its unit tests test the specification.",
|
|
51
51
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS } from "../plain/spec.js";
|
|
1
|
+
import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_REQUIREMENTS } from "../plain/spec.js";
|
|
2
2
|
import { resourceDiff } from "./spec-steps.js";
|
|
3
3
|
/**
|
|
4
4
|
* A job's change as its agent is given it, for the panels: the same items `describeSpecChange` writes into
|
|
@@ -10,7 +10,9 @@ import { resourceDiff } from "./spec-steps.js";
|
|
|
10
10
|
* things - the lines around a change, the items no job takes yet - and leave out what an import adds.
|
|
11
11
|
*/
|
|
12
12
|
/** The sections in the order the agent is given them, then any other. */
|
|
13
|
-
const ORDER = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS];
|
|
13
|
+
const ORDER = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, FUNCTIONAL_REQUIREMENTS];
|
|
14
|
+
/** A section's header as a .plain file writes it: a required module's functional specs are under its own `***functional specs***`. */
|
|
15
|
+
const heading = (section) => `***${section === REQUIRED_FUNCTIONAL_SPECS ? FUNCTIONAL_REQUIREMENTS : section}***`;
|
|
14
16
|
/** An item's lines as a .plain file writes them: its markdown, then its acceptance tests indented under it. */
|
|
15
17
|
function itemLines(serialized) {
|
|
16
18
|
if (serialized === undefined)
|
|
@@ -92,7 +94,7 @@ function laidOut(changes, file, asGiven = false) {
|
|
|
92
94
|
}
|
|
93
95
|
if (body.length === 0)
|
|
94
96
|
continue;
|
|
95
|
-
lines.push("@@ @@", `
|
|
97
|
+
lines.push("@@ @@", ` ${heading(section)}`, ...body);
|
|
96
98
|
}
|
|
97
99
|
return { diff: `${lines.join("\n")}\n${resourceDiff(changes, file)}`, starts };
|
|
98
100
|
}
|
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export declare const TRY_IT_LINE = "Want to try it? Just ask me - I'll set it up from what's built so far.";
|
|
3
|
-
/** The same for a command-line run, which has no one to ask: the command that does it. */
|
|
1
|
+
/** A command-line run has no one to ask: the command that releases what it built. */
|
|
4
2
|
export declare function tryItCommand(plainFile: string): string;
|
|
5
|
-
/** A job's report, ending with how to try what it built when it landed. */
|
|
6
|
-
export declare function withTryItLine(report: string, landed: boolean): string;
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { relative } from "node:path";
|
|
2
2
|
/**
|
|
3
|
-
* How a
|
|
4
|
-
* from what is built.
|
|
3
|
+
* How a command-line run that landed code ends: the command that releases it to try.
|
|
5
4
|
*/
|
|
6
5
|
function shortPath(buildDir) {
|
|
7
6
|
const fromHere = relative(process.cwd(), buildDir);
|
|
8
7
|
return fromHere === "" || fromHere.startsWith("..") ? buildDir : fromHere;
|
|
9
8
|
}
|
|
10
|
-
/**
|
|
11
|
-
export const TRY_IT_LINE = "Want to try it? Just ask me - I'll set it up from what's built so far.";
|
|
12
|
-
/** The same for a command-line run, which has no one to ask: the command that does it. */
|
|
9
|
+
/** A command-line run has no one to ask: the command that releases what it built. */
|
|
13
10
|
export function tryItCommand(plainFile) {
|
|
14
11
|
return `To try it: turri release ${shortPath(plainFile)}`;
|
|
15
12
|
}
|
|
16
|
-
/** A job's report, ending with how to try what it built when it landed. */
|
|
17
|
-
export function withTryItLine(report, landed) {
|
|
18
|
-
return landed ? `${report}\n\n${TRY_IT_LINE}` : report;
|
|
19
|
-
}
|
package/dist/regenerate/loop.js
CHANGED
|
@@ -18,7 +18,6 @@ import { budgetFor, choosingWhen, configForSpec, conformanceSwitchedOn, stackFor
|
|
|
18
18
|
import { describeSpecChange } from "./prompt.js";
|
|
19
19
|
import { resolveTemplateDirs } from "./paths.js";
|
|
20
20
|
import { builtTips, copyRequiredCode, importingModules, isImportModule, newlyRequired, resolveRequiresChain, startFromScratch } from "./requires.js";
|
|
21
|
-
import { withTryItLine } from "./delivery.js";
|
|
22
21
|
import { describeSpecDiff, renderSpecDiff, specFilesOf } from "./regenerate.js";
|
|
23
22
|
import { locateDiffLines } from "./diff-view.js";
|
|
24
23
|
import { agentChanges, changeDiffInFiles } from "./change-view.js";
|
|
@@ -1644,7 +1643,7 @@ const defaultRunner = async (job, onDelta) => {
|
|
|
1644
1643
|
onDelta,
|
|
1645
1644
|
});
|
|
1646
1645
|
// Landed code can be tried.
|
|
1647
|
-
return { landed: result.passed, report:
|
|
1646
|
+
return { landed: result.passed, report: describeSpecDiff(result), story: codeStory(result),
|
|
1648
1647
|
...(result.specGaps !== undefined ? { specGaps: result.specGaps } : {}),
|
|
1649
1648
|
};
|
|
1650
1649
|
};
|
|
@@ -36,7 +36,7 @@ import { inheritedRecord, readRecord, withResourceDigests, writeCodeRecord } fro
|
|
|
36
36
|
import { changesBetween, sameSpec, serializeTree } from "./spec-steps.js";
|
|
37
37
|
import { untilUnitTestsReviewed } from "./unit-review.js";
|
|
38
38
|
import { configForSpec, inheritModel, modelOnly, modelSpec } from "../config.js";
|
|
39
|
-
import { findTestRemovals, inventoryTests, listFiles, removalChallengePrompt, draftPrompt, DRAFT_ASKS, } from "./testing.js";
|
|
39
|
+
import { findTestRemovals, inventoryTests, listFiles, removalChallengePrompt, AGAIN_ASKS, againPrompt, draftPrompt, DRAFT_ASKS, } from "./testing.js";
|
|
40
40
|
/**
|
|
41
41
|
* Render a change to the specs, given as a git diff of the .plain files.
|
|
42
42
|
*
|
|
@@ -335,6 +335,10 @@ export async function renderSpecDiff(options) {
|
|
|
335
335
|
let attempts = 0;
|
|
336
336
|
/** How many times the agent, having written nothing, was asked for a draft. */
|
|
337
337
|
let draftAsks = 0;
|
|
338
|
+
/** The functional specs this job regenerates again: their code is to be built anew, never left as it was. */
|
|
339
|
+
const again = options.cases?.again ?? [];
|
|
340
|
+
/** Regenerating again, the agent wrote nothing however it was asked. */
|
|
341
|
+
let unchanged = false;
|
|
338
342
|
/** What the unit testing script needs and does not find, last run. */
|
|
339
343
|
let missing;
|
|
340
344
|
/** The unit testing script needs more time than a test run may take - the debugger's finding: the pulse stops on it. */
|
|
@@ -398,6 +402,19 @@ export async function renderSpecDiff(options) {
|
|
|
398
402
|
response = await ask(executor, misplacedTestsPrompt(misplaced, buildDir), options.onDelta);
|
|
399
403
|
continue;
|
|
400
404
|
}
|
|
405
|
+
// Regenerating functional specs again, nothing written is never the answer: sent back to build them anew, and
|
|
406
|
+
// still nothing, the work does not land.
|
|
407
|
+
if (again.length > 0 && wroteNothing(buildDir, digestsBefore)) {
|
|
408
|
+
if (draftAsks >= AGAIN_ASKS) {
|
|
409
|
+
unchanged = true;
|
|
410
|
+
stage("the coding agent regenerated nothing again - undoing the change");
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
draftAsks += 1;
|
|
414
|
+
stage("the coding agent has changed nothing yet - sending it back to build what it regenerates again anew");
|
|
415
|
+
response = await ask(executor, againPrompt(again), options.onDelta);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
401
418
|
// Nothing written - a turn spent planning - leaves nothing to test or review: ask for a best-guess draft, or why none.
|
|
402
419
|
if (draftAsks < DRAFT_ASKS && wroteNothing(buildDir, digestsBefore)) {
|
|
403
420
|
draftAsks += 1;
|
|
@@ -559,14 +576,17 @@ export async function renderSpecDiff(options) {
|
|
|
559
576
|
landWork(buildDir, `spec diff: ${summarizeDiff(options.diff)}`, options.diff);
|
|
560
577
|
};
|
|
561
578
|
if (!unitPassed) {
|
|
562
|
-
|
|
579
|
+
if (!unchanged)
|
|
580
|
+
stage(unitReviewStopped ? "the unit tests pass but still leave the change untested - undoing the change" : "unit tests NOT passing - undoing the change");
|
|
563
581
|
abandonWork(unit, moduleName, missing !== undefined
|
|
564
582
|
? "the unit testing script could not run"
|
|
565
|
-
:
|
|
566
|
-
? "
|
|
567
|
-
:
|
|
568
|
-
? "the
|
|
569
|
-
:
|
|
583
|
+
: unchanged
|
|
584
|
+
? "regenerating again, the coding agent built nothing anew"
|
|
585
|
+
: slow !== undefined
|
|
586
|
+
? "the unit testing script needs more time than a test run may take"
|
|
587
|
+
: staticOnly
|
|
588
|
+
? "the spec change did not pass its static checks"
|
|
589
|
+
: "the spec change did not pass its unit tests");
|
|
570
590
|
// The environment lacks what the testing script needs: the pulse stops, rolled back, and the person is told.
|
|
571
591
|
if (missing !== undefined) {
|
|
572
592
|
throw toolchainError(missing, { moduleName, script: scripts.unit, kind: "unit" });
|
|
@@ -614,9 +634,7 @@ export function describeSpecDiff(result) {
|
|
|
614
634
|
...(result.steered !== undefined ? [`Steered while it ran - the spec changed inside what it was regenerating, and its agent was told: ${result.steered.join("; ")}.`] : []),
|
|
615
635
|
...specGapsText(result.specGaps),
|
|
616
636
|
"",
|
|
617
|
-
"<agent report>",
|
|
618
637
|
result.response,
|
|
619
|
-
"</agent report>",
|
|
620
638
|
]
|
|
621
639
|
.filter((line) => line !== "")
|
|
622
640
|
.join("\n");
|
|
@@ -105,13 +105,8 @@ export interface StartResult {
|
|
|
105
105
|
process?: DeployedProcess;
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* Start
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* With a URL, it is ready once something answers there - any answer - and not when it exits first or
|
|
112
|
-
* nothing answers in time. A port something else holds already is said, with what holds it, and
|
|
113
|
-
* nothing is started: an app moved to a port of the harness's choosing would not be the one the
|
|
114
|
-
* person was told about. With no URL, it only has to still be running a few seconds on.
|
|
108
|
+
* Start what keeps running, in `dir` - the deployed folder `root`, or one inside it. A folder that is not there starts
|
|
109
|
+
* nothing: said, with what the deployed folder holds, so the agent can name the right one.
|
|
115
110
|
*/
|
|
116
111
|
export declare function startDeployed(input: {
|
|
117
112
|
name: string;
|
|
@@ -120,4 +115,5 @@ export declare function startDeployed(input: {
|
|
|
120
115
|
moduleName: string;
|
|
121
116
|
logDir: string;
|
|
122
117
|
url?: string;
|
|
118
|
+
root?: string;
|
|
123
119
|
}): Promise<StartResult>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { connect } from "node:net";
|
|
4
4
|
import { dirname, join, relative } from "node:path";
|
|
5
5
|
import { HISTORY_DIR } from "./lineage.js";
|
|
@@ -206,7 +206,39 @@ const ALIVE_AFTER_MS = 4000;
|
|
|
206
206
|
* nothing is started: an app moved to a port of the harness's choosing would not be the one the
|
|
207
207
|
* person was told about. With no URL, it only has to still be running a few seconds on.
|
|
208
208
|
*/
|
|
209
|
+
/** A folder's entries as a listing says them - folders with a slash - or that it is empty. */
|
|
210
|
+
function listing(dir) {
|
|
211
|
+
try {
|
|
212
|
+
const entries = readdirSync(dir, { withFileTypes: true }).map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name)).sort();
|
|
213
|
+
if (entries.length === 0)
|
|
214
|
+
return "nothing";
|
|
215
|
+
return entries.length > 40 ? `${entries.slice(0, 40).join(", ")} and ${entries.length - 40} more` : entries.join(", ");
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return "nothing turri can read";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Start what keeps running, in `dir` - the deployed folder `root`, or one inside it. A folder that is not there starts
|
|
223
|
+
* nothing: said, with what the deployed folder holds, so the agent can name the right one.
|
|
224
|
+
*/
|
|
209
225
|
export async function startDeployed(input) {
|
|
226
|
+
let isFolder = false;
|
|
227
|
+
try {
|
|
228
|
+
isFolder = statSync(input.dir).isDirectory();
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
/* not there */
|
|
232
|
+
}
|
|
233
|
+
if (!isFolder) {
|
|
234
|
+
const why = existsSync(input.dir) ? "is not a folder" : "does not exist";
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
text: input.root !== undefined
|
|
238
|
+
? `${input.name} was not started: ${shortPath(input.dir)} ${why}. \`dir\` is relative to the deployed folder, ${shortPath(input.root)} - a folder inside it, e.g. frontend, or none for the deployed folder itself; never the project's path to it. ${shortPath(input.root)} holds: ${listing(input.root)}.`
|
|
239
|
+
: `${input.name} was not started: ${shortPath(input.dir)} ${why}.`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
210
242
|
await stopDeployed({ name: input.name });
|
|
211
243
|
let url;
|
|
212
244
|
if (input.url !== undefined) {
|
|
@@ -229,35 +261,47 @@ export async function startDeployed(input) {
|
|
|
229
261
|
const log = join(input.logDir, `${input.moduleName}-${input.name.replace(/[^\w.-]+/g, "-")}.log`);
|
|
230
262
|
const fd = openSync(log, "w");
|
|
231
263
|
const child = spawn("bash", ["-c", input.command], { cwd: input.dir, detached: true, stdio: ["ignore", fd, fd], env: process.env });
|
|
264
|
+
// Listened to at once: a child's 'error' with no listener is thrown, and takes the whole process down with it.
|
|
265
|
+
let info;
|
|
266
|
+
let spawnError;
|
|
267
|
+
const spawned = new Promise((done) => {
|
|
268
|
+
child.once("spawn", () => done());
|
|
269
|
+
child.once("error", () => done());
|
|
270
|
+
});
|
|
271
|
+
child.on("error", (error) => {
|
|
272
|
+
spawnError ??= error;
|
|
273
|
+
if (info !== undefined)
|
|
274
|
+
info.exited ??= { code: null, signal: null };
|
|
275
|
+
});
|
|
232
276
|
closeSync(fd);
|
|
233
|
-
if (child.pid === undefined)
|
|
234
|
-
|
|
235
|
-
|
|
277
|
+
if (child.pid === undefined) {
|
|
278
|
+
await spawned;
|
|
279
|
+
return { ok: false, text: `${input.name} could not be started in ${shortPath(input.dir)}: ${spawnError?.message ?? "no process was made"}.` };
|
|
280
|
+
}
|
|
281
|
+
const started = { name: input.name, moduleName: input.moduleName, command: input.command, dir: input.dir, log, pid: child.pid, startedAt: Date.now(), ...(input.url !== undefined ? { url: input.url } : {}) };
|
|
282
|
+
info = started;
|
|
236
283
|
child.on("exit", (code, signal) => {
|
|
237
|
-
|
|
238
|
-
});
|
|
239
|
-
child.on("error", () => {
|
|
240
|
-
info.exited ??= { code: null, signal: null };
|
|
284
|
+
started.exited = { code, signal };
|
|
241
285
|
});
|
|
242
|
-
processes.set(input.name, { info, child });
|
|
286
|
+
processes.set(input.name, { info: started, child });
|
|
243
287
|
stopOnExit();
|
|
244
288
|
const failed = (why) => {
|
|
245
289
|
const tail = logTail(log);
|
|
246
|
-
return { ok: false, text: `${input.name} ${why}.${tail !== "" ? `\nThe end of its output (${shortPath(log)}):\n${tail}` : ""}`, process:
|
|
290
|
+
return { ok: false, text: `${input.name} ${why}.${tail !== "" ? `\nThe end of its output (${shortPath(log)}):\n${tail}` : ""}`, process: started };
|
|
247
291
|
};
|
|
248
|
-
const ended = () => `exited${
|
|
292
|
+
const ended = () => `exited${started.exited?.code !== null && started.exited?.code !== undefined ? ` with ${started.exited.code}` : ""} before it was ready`;
|
|
249
293
|
if (url === undefined) {
|
|
250
294
|
await sleep(ALIVE_AFTER_MS);
|
|
251
|
-
if (
|
|
295
|
+
if (started.exited !== undefined)
|
|
252
296
|
return failed(ended());
|
|
253
|
-
return { ok: true, text: `${input.name} is running (pid ${
|
|
297
|
+
return { ok: true, text: `${input.name} is running (pid ${started.pid}); its output is in ${shortPath(log)}.`, process: started };
|
|
254
298
|
}
|
|
255
299
|
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
256
300
|
while (Date.now() < deadline) {
|
|
257
|
-
if (
|
|
301
|
+
if (started.exited !== undefined)
|
|
258
302
|
return failed(ended());
|
|
259
303
|
if (await answers(url.href))
|
|
260
|
-
return { ok: true, text: `${input.name} is up at ${url.href} (pid ${
|
|
304
|
+
return { ok: true, text: `${input.name} is up at ${url.href} (pid ${started.pid}); its output is in ${shortPath(log)}.`, process: started };
|
|
261
305
|
await sleep(1000);
|
|
262
306
|
}
|
|
263
307
|
return failed(`is running, but nothing answered at ${url.href} in ${Math.round(READY_TIMEOUT_MS / 1000)}s`);
|
|
@@ -89,6 +89,13 @@ export declare function removalChallengePrompt(removals: TestRemoval[], subject:
|
|
|
89
89
|
export declare const DRAFT_ASKS = 1;
|
|
90
90
|
/** For a coding agent that has written nothing yet: write a best-guess draft now, or say why no code is needed. */
|
|
91
91
|
export declare function draftPrompt(): string;
|
|
92
|
+
/** How many times a coding agent regenerating functional specs again, having written nothing, is sent back to build them anew. */
|
|
93
|
+
export declare const AGAIN_ASKS = 2;
|
|
94
|
+
/** For a coding agent regenerating functional specs again that has written nothing: no change is no answer. */
|
|
95
|
+
export declare function againPrompt(again: Array<{
|
|
96
|
+
frid: string;
|
|
97
|
+
module?: string;
|
|
98
|
+
}>): string;
|
|
92
99
|
export type UnitVerdict = {
|
|
93
100
|
ok: true;
|
|
94
101
|
command: string;
|
|
@@ -313,6 +313,20 @@ export function draftPrompt() {
|
|
|
313
313
|
"with update_notes. Then run the unit tests.",
|
|
314
314
|
].join("\n");
|
|
315
315
|
}
|
|
316
|
+
/** How many times a coding agent regenerating functional specs again, having written nothing, is sent back to build them anew. */
|
|
317
|
+
export const AGAIN_ASKS = 2;
|
|
318
|
+
/** For a coding agent regenerating functional specs again that has written nothing: no change is no answer. */
|
|
319
|
+
export function againPrompt(again) {
|
|
320
|
+
const named = again.map((spec) => `fs ${spec.frid}${spec.module !== undefined ? ` of module \`${spec.module}\`` : ""}`).join(", ");
|
|
321
|
+
return [
|
|
322
|
+
"You have not written anything yet: the code folder is as it was when you started.",
|
|
323
|
+
"",
|
|
324
|
+
`${named} ${again.length === 1 ? "is" : "are"} to be regenerated again: the person asked for ${again.length === 1 ? "its" : "their"} code to be built anew. That the`,
|
|
325
|
+
"text did not change, or that the old code already does what it says, is no reason to leave it: take its",
|
|
326
|
+
"implementation and its unit tests out and build them anew from the specification, then run the unit tests.",
|
|
327
|
+
"Work that changes nothing does not land.",
|
|
328
|
+
].join("\n");
|
|
329
|
+
}
|
|
316
330
|
/**
|
|
317
331
|
* The harness's own run of the unit tests, which is what "the tests pass" rests on.
|
|
318
332
|
*
|
package/dist/regenerate-cli.js
CHANGED
|
@@ -8,7 +8,7 @@ the chat does: what is left to regenerate - from what each module's code was reg
|
|
|
8
8
|
spec now - is planned into jobs, and they run one after the other until the code is regenerated from
|
|
9
9
|
the spec, each followed by the conformance tests of what it regenerated when they are on. Then it says
|
|
10
10
|
how each went. With functional specs named, those are regenerated though their spec did not change - as regenerate
|
|
11
|
-
does in the chat with functionalities named: their code
|
|
11
|
+
does in the chat with functionalities named: their code taken out and built anew, doing what the spec says.
|
|
12
12
|
|
|
13
13
|
The project's config.turri.yaml, in the current directory, gives the defaults: module (the file to
|
|
14
14
|
build when none is given), coding-model (the agents that build), harness-model (the planner), and
|
package/dist/release-agent.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface ReleaseAgentInput {
|
|
|
15
15
|
inModule?: string;
|
|
16
16
|
/** What the person said of how to release it. */
|
|
17
17
|
guidance?: string;
|
|
18
|
-
/** Released as a pulse's last stage: what the pulse landed. The
|
|
18
|
+
/** Released as a pulse's last stage: what the pulse landed. The setting has said to release it; the guidance says how. */
|
|
19
19
|
afterPulse?: {
|
|
20
20
|
moduleName: string;
|
|
21
21
|
subject: string;
|
package/dist/release-agent.js
CHANGED
|
@@ -84,7 +84,7 @@ export async function runReleaseAgent(input) {
|
|
|
84
84
|
].join("\n"),
|
|
85
85
|
task: [
|
|
86
86
|
input.afterPulse !== undefined
|
|
87
|
-
? `A pulse of module ${input.afterPulse.moduleName} just landed: ${input.afterPulse.subject}.
|
|
87
|
+
? `A pulse of module ${input.afterPulse.moduleName} just landed: ${input.afterPulse.subject}. Release it now, before the module's next pulse starts: the project releases after every pulse.`
|
|
88
88
|
: `Release ${shortPath(plainFile)} for the person to try, with the release tool, then do what it says.`,
|
|
89
89
|
"",
|
|
90
90
|
"First ask yourself: is there guidance for how this project releases it - what the person said below, the project's",
|
|
@@ -94,8 +94,8 @@ export async function runReleaseAgent(input) {
|
|
|
94
94
|
...(input.afterPulse !== undefined
|
|
95
95
|
? [
|
|
96
96
|
"",
|
|
97
|
-
"
|
|
98
|
-
"When release says it is already released, leave it at that.",
|
|
97
|
+
"Release it whether or not the guidance speaks of releasing after a build - the project's setting has said to. What",
|
|
98
|
+
"the guidance says is how. When release says it is already released, leave it at that.",
|
|
99
99
|
]
|
|
100
100
|
: []),
|
|
101
101
|
...(input.inModule !== undefined ? ["", `Release the codebase of module ${input.inModule}: pass in_module.`] : []),
|
package/dist/release-tools.js
CHANGED
|
@@ -126,7 +126,7 @@ export function createDeployTools(options) {
|
|
|
126
126
|
name: Type.String({ description: "A short name for it: backend, frontend, app" }),
|
|
127
127
|
command: Type.String({ description: "The command that starts it, as run from its folder" }),
|
|
128
128
|
url: Type.Optional(Type.String({ description: "Where it serves, e.g. http://localhost:5173 - it is ready once this answers" })),
|
|
129
|
-
dir: Type.Optional(Type.String({ description: "
|
|
129
|
+
dir: Type.Optional(Type.String({ description: "The folder to run it in, relative to the deployed folder - e.g. frontend - never the project's path to the deployed folder (dist/<module>). Default: the deployed folder itself." })),
|
|
130
130
|
}),
|
|
131
131
|
async execute(_id, params) {
|
|
132
132
|
const current = needsDeploy();
|
|
@@ -134,6 +134,7 @@ export function createDeployTools(options) {
|
|
|
134
134
|
name: params.name,
|
|
135
135
|
command: params.command,
|
|
136
136
|
dir: inside(current, params.dir),
|
|
137
|
+
root: current.hostDir ?? current.dir,
|
|
137
138
|
moduleName: current.moduleName,
|
|
138
139
|
logDir: deployLogDir(projectDirOf(current.plainFile)),
|
|
139
140
|
...(params.url !== undefined ? { url: params.url } : {}),
|
package/dist/skill-progress.d.ts
CHANGED
package/docs/authoring-plain.md
CHANGED
|
@@ -100,7 +100,7 @@ A requirement beyond the stack - storage, an outside service - is added on top o
|
|
|
100
100
|
| --- | --- |
|
|
101
101
|
| `module-structure.md` | the file skeleton, frontmatter fields, section order, module kinds, section ownership |
|
|
102
102
|
| `definitions.md` | concept syntax, predefined concepts, define before use, no circular definitions, granularity |
|
|
103
|
-
| `func-specs.md` | voice, the complexity limit, chronological order, no conflicts, language agnosticism, testability, acceptance tests |
|
|
103
|
+
| `func-specs.md` | voice, the complexity limit, chronological order, no conflicts, how a functional spec is added - checked for size and conflicts before it is written in - and where it goes, language agnosticism, testability, acceptance tests |
|
|
104
104
|
| `impl-reqs.md` | HOW not WHAT, unit tests live here, what belongs and what does not |
|
|
105
105
|
| `test-reqs.md` | conformance tests only, what belongs, test types |
|
|
106
106
|
| `concise-specs.md` (+ two example banks) | keeping \*\*\*plain short enough for a person to review: no obvious lines, plain words, no uncheckable adjectives, no hedging |
|
|
@@ -147,7 +147,7 @@ Every skill in `genome/skills/` can be run from a conversation; most are also us
|
|
|
147
147
|
| --- | --- |
|
|
148
148
|
| start a project | `brood`, `spawn` - or ask, and choose `Just do it` for no skill |
|
|
149
149
|
| make rough \*\*\*plain build | `mature` |
|
|
150
|
-
| add to \*\*\*plain | `bud
|
|
150
|
+
| add to \*\*\*plain | `bud` - or ask in the conversation: it writes a concept, a functional spec, a req, an acceptance test or a linked resource itself, by the rules in its prompt, as a \*\*\*plain diff |
|
|
151
151
|
| check what is written | `analyze-if-func-spec-too-complex`, `analyze-2-func-specs`, `analyze-func-specs`, `resolve-section-ownership` |
|
|
152
152
|
| fix what is written | `break-down-func-spec`, `resolve-spec-conflict`, `debug-specs` |
|
|
153
153
|
| restructure modules | `refactor-module`, `consolidate-concepts` |
|
package/docs/config.md
CHANGED
|
@@ -89,7 +89,7 @@ models: # each agent role's model, thinking and tur
|
|
|
89
89
|
| `mutation-testing` | `true` or `false`: whether the coding agent and the reviewer have `can_it_fail` - one change put in the code for one run of the unit testing script, to see that a test catches it; on a copy of the code, or in place and put back after the run (see [regenerating the code](regenerating.md#checking-its-own-work)). `turri regenerate --mutation-testing false` for one run | `true` |
|
|
90
90
|
| `rules-off` | a list of rule names - `integrations`, `concise-specs`, the files of `genome/rules/` without `.md` - left out of the conversation's prompt and every \*\*\*plain-writing agent's: a rule this project's own skills or AGENTS.md replace, such as one that says to probe a live API before any \*\*\*plain is written. `turri init-config` writes it with `integration-embedded`, `integration-embedded-testing` and `integration-standalone` - take them out when the project writes an integration against a REST API. The project's alone | none |
|
|
91
91
|
| `allow-to-stop-jobs` | `true` or `false`: whether turri stops a running job \*\*\*plain has moved on from - a functional spec it regenerates changed, or a file one links was edited - rolls it back and regenerates \*\*\*plain as it now is. `false`: the job runs to its end and lands, and what changed since is regenerated on top of it by the next job - see [regenerating the code](regenerating.md). Your own `halt` stops a job either way | `true` |
|
|
92
|
-
| `release-after-pulse` | `true` or `false`: whether every pulse that lands code releases it, in the chat, as its last stage - before the module's next pulse starts, whatever the chat is doing: an agent on the coding model
|
|
92
|
+
| `release-after-pulse` | `true` or `false`: whether every pulse that lands code releases it, in the chat, as its last stage - before the module's next pulse starts, whatever the chat is doing: an agent on the coding model releases it, following the project's release guidance as the chat finds it (what was passed to `release` this conversation, AGENTS.md, the project's skills) for how, or to `dist/<module>` without any - see [deploy](deploy.md#after-every-pulse). One release a pulse that lands; a release that fails is said in the pulse's report, and what it landed stays. `turri init-config` writes `true` | `false` |
|
|
93
93
|
| `unittests-script` | the unit testing script, by path from the project root - it runs the code's static checks too, exiting 80 when only they fail (see [the static checks](regenerating.md#the-static-checks)) | looked for - see below |
|
|
94
94
|
| `conformance-tests-script` | the conformance testing script, by path from the project root | looked for - see below |
|
|
95
95
|
| `prepare-environment-script` | optional: run as `<script> <build>` before every run of the conformance testing script, which runs only once it passes - see [the testing scripts](regenerating.md#the-testing-scripts). turri never writes one | the one `prepare_environment*.sh` at the root or in `test_scripts/`, else none |
|
package/docs/deploy.md
CHANGED
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Ask to try it - "let me try it", "run the app" - at any time, even while a regeneration is running. The chat calls `deploy`: it puts the module's code, as it was last built, in `dist/<module>` in the project, and starts it on your machine. An app with a backend and a frontend is started whole, and you are told where to open your browser. For a command-line tool or a library you get a few commands to paste, run from `dist/<module>`, and what they print.
|
|
4
4
|
|
|
5
|
-
Every pulse that lands code - a regeneration, a \*\*\*plain diff, a conformance run, a functionality regenerated - ends with "Want to try it? Just ask me - I'll set it up from what's built so far." When a regeneration's conformance tests follow as a pulse of their own, the conformance pulse says it, once they pass too.
|
|
6
|
-
|
|
7
5
|
## When the project says how it is released
|
|
8
6
|
|
|
9
7
|
Every release starts the same way: the module's last commit - what last landed whole - goes in `dist/<module>`, as below. Not everything is tried from there, though. An integration that a host application loads goes on into the host's own tree, with the host's own scripts, and then the host is started. So the model asks itself whether there is guidance for how this project is released - in what you said, the project's AGENTS.md, a skill, anything it was given - and passes it to `release` whole:
|
|
10
8
|
|
|
11
|
-
- **There is guidance.** The code goes on from `dist/<module>` as the guidance says, on your machine, from the host codebase - the git repository the project is in, or else where **turri** runs: the scripts it names, in their order, and any change it says the host needs. The code is taken from `dist`, never from `plain_modules`, where a pulse may be writing; where the host's tooling reads `plain_modules` itself, the model points it at `dist`, or does by hand from `dist` what it does.
|
|
9
|
+
- **There is guidance.** The code goes on from `dist/<module>` as the guidance says, on your machine, from the host codebase - the git repository the project is in, or else where **turri** runs: the scripts it names, in their order, and any change it says the host needs. The code is taken from `dist`, never from `plain_modules`, where a pulse may be writing; where the host's tooling reads `plain_modules` itself, the model points it at `dist`, or does by hand from `dist` what it does. Whether to release after every pulse is `release-after-pulse`'s to say - see [after every pulse](#after-every-pulse). The rules below hold as they do for `dist`: the model runs the project's scripts and installs what they need itself, and what keeps running - a start script, a server - is started with `activate`. Nothing built is changed.
|
|
12
10
|
- **There is none.** The default: it is started from `dist/<module>`, as below, as best it can be.
|
|
13
11
|
|
|
14
12
|
Guidance is whatever says it, in any words - a line in AGENTS.md such as *integrations are released with `scripts/deploy_integrations.sh`, then the app is started with `scripts/start.sh`*, or the same said in the chat.
|
|
@@ -27,7 +25,7 @@ It runs on your machine, outside the [sandbox](sandbox.md) - with your toolchain
|
|
|
27
25
|
|
|
28
26
|
1. **Checks the toolchains** it needs (`node`, `python3`, `go`, ...). One is missing: it installs it with the machine's package manager (`brew` on macOS, `apt` or `dnf` on Linux).
|
|
29
27
|
2. **Installs what it needs itself.** Dependencies into a virtual environment or `node_modules`, a build that downloads packages, an image pull, a guided release's project scripts: it runs them in its own shell, as any agent with a shell would, and fixes what fails. Nothing is put to you - there is no dialog to answer.
|
|
30
|
-
3. **Starts what serves** with `activate`: in the background, in its own process group, its output in `.turri/deploy/<module>-<name>.log`. It waits until something answers at the server's URL - up to a minute - so the URL you are given has answered once; one that exits first comes back with the end of its output. A port something else holds is said, with what holds it, and nothing is started there: the app is not moved to a port you were not told about.
|
|
28
|
+
3. **Starts what serves** with `activate`: in the background, in its own process group, its output in `.turri/deploy/<module>-<name>.log`. It waits until something answers at the server's URL - up to a minute - so the URL you are given has answered once; one that exits first comes back with the end of its output. A port something else holds is said, with what holds it, and nothing is started there: the app is not moved to a port you were not told about. It runs in the deployed folder, or a folder inside it the model names relative to it (`frontend`); a folder that is not there starts nothing, and the model is told so with what the deployed folder holds, to name the right one.
|
|
31
29
|
|
|
32
30
|
What it started is listed by the `pulses` tool and by **`/halt`**, and stopped by **`/halt activated <name>`**, by `halt`, by the next deploy of the module, and when the chat exits.
|
|
33
31
|
|
|
@@ -35,9 +33,9 @@ What it started is listed by the `pulses` tool and by **`/halt`**, and stopped b
|
|
|
35
33
|
|
|
36
34
|
With `release-after-pulse: true` in `config.turri.yaml`, turri releases what each pulse lands itself, as the pulse's last stage (`releasing what this pulse landed`): the pulse ends once it is released, so the module's next pulse starts on what is out, and the chat - waiting on your answer to a question, say - has no part in it. One release a pulse that lands, in order.
|
|
37
35
|
|
|
38
|
-
The release is done by an agent on the coding model, thinking low - it follows steps, and a small model does that well - the same one `turri release` runs, and it finds the guidance as the chat would: what was passed to `release` earlier in the conversation, the project's AGENTS.md and its skills.
|
|
36
|
+
The release is done by an agent on the coding model, thinking low - it follows steps, and a small model does that well - the same one `turri release` runs, and it finds the guidance as the chat would: what was passed to `release` earlier in the conversation, the project's AGENTS.md and its skills. The setting is the word to release; the guidance says only how - without any, the code goes to `dist/<module>` and is started from there. Its last words go in the pulse's report - "Released after the pulse: ..." - and the chat, seeing them, does not release it again: it says in a line what was released, then does what it was told to do once something is released - a host such as a desktop app tells it, in its `surface`, to open it where the person can watch and say what to test. A release that fails is said there too; what the pulse landed stays.
|
|
39
37
|
|
|
40
|
-
Off
|
|
38
|
+
Off without the key: releasing after a pulse is then the chat's, when its guidance says so. `turri init-config` writes `release-after-pulse: true`.
|
|
41
39
|
|
|
42
40
|
## On the command line
|
|
43
41
|
|
package/docs/hosting.md
CHANGED
|
@@ -94,7 +94,7 @@ await session.bindExtensions({ uiContext, mode: "tui" });
|
|
|
94
94
|
|
|
95
95
|
- **The jobs.** The pulses the conversation starts - its tools, `/regenerate`, a \*\*\*plain change - run in its own job registry and work loop, the project's one. The host is handed both as they are made, and told of each job as it changes, so it shows the very jobs the conversation runs. Its buttons are best the conversation's commands - `session.prompt("/regenerate app.plain")` - so there is never a second loop on the same project.
|
|
96
96
|
- **The questions.** `dialogs` is `ask_user`'s two dialogs: `select(title, options, context, at)` and `input(title, context)`, each resolving with the answer, or undefined when the person dismissed it. Given, the conversation's questions and a skill's interview are put in them; without it, in the terminal's own dialog.
|
|
97
|
-
- **The surface.** What turri tells the model of where the person watches the work is the terminal's by default: regen, the side panel on `ctrl+h`, `/pulses`. In a host none of that is there, and the model would send the person to keys and panels that do not exist. `surface` is the host's own description of its UI - the views it has in place of the terminal's, what its marks and buttons mean, where questions appear - in its own words. Given, turri leaves every terminal instruction out of what the model is told and puts the surface in turri's identity, as a `<surface>` block, where the identity speaks of regen. It reaches the side questions and the tools' own words too: a pulse not found says the `pulses` tool lists them, not `/pulses`. Text only the person sees in the terminal - the job panel, the side panel - is the terminal's and stays as it is. Without `surface`, the prompt is the terminal's, byte for byte.
|
|
97
|
+
- **The surface.** What turri tells the model of where the person watches the work is the terminal's by default: regen, the side panel on `ctrl+h`, `/pulses`. In a host none of that is there, and the model would send the person to keys and panels that do not exist. `surface` is the host's own description of its UI - the views it has in place of the terminal's, what its marks and buttons mean, where questions appear - in its own words. Given, turri leaves every terminal instruction out of what the model is told and puts the surface in turri's identity, as a `<surface>` block, where the identity speaks of regen. It reaches the side questions and the tools' own words too: a pulse not found says the `pulses` tool lists them, not `/pulses`. Text only the person sees in the terminal - the job panel, the side panel - is the terminal's and stays as it is. Without `surface`, the prompt is the terminal's, byte for byte. What the conversation is to do once something is released belongs there too - open it in the host's preview, say what to test: after a pulse released under `release-after-pulse`, the conversation does what it was told to do once something is released.
|
|
98
98
|
- **The system prompt.** `systemPrompt(prompt)` gets the whole system prompt for the turn - [pi](https://pi.dev)'s, turri's identity and blocks, the rules - after turri's own `before_agent_start` work, and returns what is sent. It is for what `surface` does not cover; to describe the UI, use `surface`, rather than words appended after turri's that contradict them.
|
|
99
99
|
- **The mode.** `mode: "tui"` runs the work in the background and keeps the conversation free, as the terminal does; `rpc`, `json` and `print` wait for each job. The extension's other UI calls - its notices, its working line, a confirm - come to the host's `uiContext`, as [pi](https://pi.dev)'s [SDK](https://github.com/earendil-works/pi) describes it.
|
|
100
100
|
|