@turri-dev/cli 0.1.8-next.e553476 → 0.1.9-next.73bfddf
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/dist/cli.js +6 -2
- package/dist/config-edit.d.ts +1 -1
- package/dist/config-edit.js +4 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +28 -1
- package/dist/extension.js +75 -9
- package/dist/genome.js +2 -0
- package/dist/host.d.ts +3 -1
- package/dist/host.js +4 -1
- package/dist/init-config.d.ts +3 -0
- package/dist/init-config.js +15 -0
- package/dist/ledger-cli.d.ts +1 -0
- package/dist/ledger-cli.js +50 -0
- package/dist/ledger-tools.d.ts +10 -0
- package/dist/ledger-tools.js +64 -0
- package/dist/loop-cli.js +4 -0
- package/dist/pi.d.ts +2 -0
- package/dist/pi.js +24 -4
- package/dist/regenerate/activity.d.ts +9 -0
- package/dist/regenerate/activity.js +11 -0
- package/dist/regenerate/agent-env.d.ts +7 -0
- package/dist/regenerate/agent-env.js +126 -5
- package/dist/regenerate/agent-shell.js +5 -0
- package/dist/regenerate/chunk-plan.d.ts +28 -9
- package/dist/regenerate/chunk-plan.js +141 -17
- package/dist/regenerate/conformance-agents.js +2 -2
- package/dist/regenerate/conformance.js +6 -3
- package/dist/regenerate/debug-log.d.ts +3 -1
- package/dist/regenerate/debug-log.js +10 -2
- package/dist/regenerate/diagnose.js +12 -2
- package/dist/regenerate/environment-error.d.ts +6 -0
- package/dist/regenerate/environment-error.js +19 -1
- package/dist/regenerate/job-log.d.ts +3 -1
- package/dist/regenerate/job-log.js +8 -5
- package/dist/regenerate/jobs-tree.d.ts +7 -3
- package/dist/regenerate/jobs-tree.js +21 -6
- package/dist/regenerate/jobs.d.ts +9 -0
- package/dist/regenerate/jobs.js +51 -5
- package/dist/regenerate/ledger-view.d.ts +92 -0
- package/dist/regenerate/ledger-view.js +345 -0
- package/dist/regenerate/ledger.d.ts +275 -0
- package/dist/regenerate/ledger.js +349 -0
- package/dist/regenerate/lineage.d.ts +9 -1
- package/dist/regenerate/lineage.js +64 -12
- package/dist/regenerate/loop.js +12 -3
- package/dist/regenerate/pulse-files.d.ts +14 -0
- package/dist/regenerate/pulse-files.js +63 -0
- package/dist/regenerate/regenerate.js +7 -5
- package/dist/regenerate/regression.js +4 -2
- package/dist/regenerate/services.js +3 -0
- package/dist/regenerate/side-panel.d.ts +2 -0
- package/dist/regenerate/side-panel.js +4 -3
- package/dist/regenerate/test-scripts.js +18 -5
- package/dist/regenerate/testing.js +3 -0
- package/dist/regenerate/unit-review.js +3 -0
- package/dist/run-report.js +9 -5
- package/docs/config.md +23 -2
- package/docs/development.md +3 -1
- package/docs/headless.md +5 -4
- package/docs/hosting.md +29 -0
- package/docs/installation.md +1 -0
- package/docs/regenerating.md +11 -9
- package/docs/sandbox.md +5 -5
- package/docs/using.md +36 -5
- package/package.json +1 -1
|
@@ -2,6 +2,8 @@ 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";
|
|
6
|
+
import { firstLine, record } from "./ledger.js";
|
|
5
7
|
import { projectRootOf } from "./paths.js";
|
|
6
8
|
/**
|
|
7
9
|
* Version history for each module's generated code and tests.
|
|
@@ -15,7 +17,8 @@ import { projectRootOf } from "./paths.js";
|
|
|
15
17
|
* A commit is made after every step that changes the module - a render, a regeneration, a
|
|
16
18
|
* conformance run, a regression repair, a spec change, a start from the module below - with
|
|
17
19
|
* 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
|
|
20
|
+
* failure is swallowed, not raised - except landing work (`landWork`), whose commit the next
|
|
21
|
+
* start would otherwise undo.
|
|
19
22
|
*/
|
|
20
23
|
export const HISTORY_DIR = ".plaingit";
|
|
21
24
|
const AUTHOR = ["-c", "user.name=turri", "-c", "user.email=turri@localhost", "-c", "commit.gpgsign=false"];
|
|
@@ -39,7 +42,10 @@ const EXCLUDED = [
|
|
|
39
42
|
".DS_Store",
|
|
40
43
|
];
|
|
41
44
|
function git(moduleDir, args, input) {
|
|
42
|
-
|
|
45
|
+
const gitDir = join(moduleDir, HISTORY_DIR);
|
|
46
|
+
// The history is turri's, not the person's: hooks their git config names - gitleaks, husky - would refuse some of its
|
|
47
|
+
// commits, and the next start would put back what landed. Its own hooks folder holds only git's samples.
|
|
48
|
+
return spawnSync("git", [...AUTHOR, "-c", `core.hooksPath=${join(gitDir, "hooks")}`, "--git-dir", gitDir, "--work-tree", moduleDir, ...args], {
|
|
43
49
|
cwd: moduleDir,
|
|
44
50
|
encoding: "utf8",
|
|
45
51
|
input,
|
|
@@ -79,21 +85,65 @@ function ensureHistory(moduleDir) {
|
|
|
79
85
|
*/
|
|
80
86
|
export function recordHistory(dir, subject, body) {
|
|
81
87
|
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);
|
|
88
|
+
return recorded(dir, subject, commitAll(dir, subject, body), false).commit;
|
|
92
89
|
}
|
|
93
90
|
catch {
|
|
94
91
|
return undefined;
|
|
95
92
|
}
|
|
96
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Commit work that landed - the code or tests a pulse passed, with the record of what they are built from. Unlike any
|
|
96
|
+
* other step's, this commit is not only a record: the next unit of work puts the module back to its last commit, and
|
|
97
|
+
* would undo what landed with it. So a commit that cannot be made is raised, as the environment's, with what git said -
|
|
98
|
+
* the work left in place, uncommitted. Nothing changed, nothing is committed. Returns the commit, if any.
|
|
99
|
+
*/
|
|
100
|
+
export function landWork(dir, subject, body) {
|
|
101
|
+
const moduleDir = moduleDirOf(dir);
|
|
102
|
+
let made;
|
|
103
|
+
try {
|
|
104
|
+
made = commitAll(dir, subject, body);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
made = { failed: error instanceof Error ? error.message : String(error) };
|
|
108
|
+
}
|
|
109
|
+
recorded(dir, subject, made, true);
|
|
110
|
+
if (made.failed !== undefined)
|
|
111
|
+
throw historyError(moduleDir, made.failed);
|
|
112
|
+
return made.commit;
|
|
113
|
+
}
|
|
114
|
+
/** A commit made or failed, in the ledger - nothing for a step that changed nothing. */
|
|
115
|
+
function recorded(dir, subject, made, landing) {
|
|
116
|
+
const place = placeOf(moduleDirOf(dir));
|
|
117
|
+
if (made.failed !== undefined)
|
|
118
|
+
record({ kind: "history.failed", subject: firstLine(subject, 200), said: firstLine(made.failed, 400), landing }, place);
|
|
119
|
+
else if (made.commit !== undefined)
|
|
120
|
+
record({ kind: "history.committed", subject: firstLine(subject, 200), commit: made.commit }, place);
|
|
121
|
+
return made;
|
|
122
|
+
}
|
|
123
|
+
/** Where a module's history is, for the ledger: its project and its name. */
|
|
124
|
+
function placeOf(moduleDir) {
|
|
125
|
+
return { project: projectRootOf(moduleDir), module: basename(moduleDir) };
|
|
126
|
+
}
|
|
127
|
+
/** Commit everything the step changed in the module `dir` belongs to: the commit, nothing when nothing changed, or why git failed. */
|
|
128
|
+
function commitAll(dir, subject, body) {
|
|
129
|
+
const moduleDir = moduleDirOf(dir);
|
|
130
|
+
if (!existsSync(moduleDir))
|
|
131
|
+
return {};
|
|
132
|
+
if (!ensureHistory(moduleDir))
|
|
133
|
+
return { failed: `git could not make the module's history in ${join(moduleDir, HISTORY_DIR)}` };
|
|
134
|
+
const failed = (result) => (result.error?.message ?? `${result.stderr}${result.stdout}`.trim()) || `git exited ${result.status}`;
|
|
135
|
+
const added = git(moduleDir, ["add", "-A"]);
|
|
136
|
+
if (added.status !== 0)
|
|
137
|
+
return { failed: failed(added) };
|
|
138
|
+
if (git(moduleDir, ["diff", "--cached", "--quiet"]).status === 0)
|
|
139
|
+
return {};
|
|
140
|
+
const message = body ? `${subject}\n\n${body.trim()}\n` : subject;
|
|
141
|
+
const committed = git(moduleDir, ["commit", "-q", "-F", "-"], message);
|
|
142
|
+
if (committed.status !== 0)
|
|
143
|
+
return { failed: failed(committed) };
|
|
144
|
+
const commit = head(dir);
|
|
145
|
+
return commit !== undefined ? { commit } : {};
|
|
146
|
+
}
|
|
97
147
|
/**
|
|
98
148
|
* The notes the harness writes once a pulse has ended, committed - or the next pulse, putting the module back to its last
|
|
99
149
|
* commit, would take them away. Into the pulse's own commit when it landed one - `landedSince` (ms), when it started - so
|
|
@@ -156,6 +206,7 @@ export function resetToLastCommit(dir) {
|
|
|
156
206
|
return [];
|
|
157
207
|
git(moduleDir, ["reset", "-q", "--hard", "HEAD"]);
|
|
158
208
|
git(moduleDir, ["clean", "-q", "-fd"]);
|
|
209
|
+
record({ kind: "history.reset", undone: changed.length, paths: changed.slice(0, 20) }, placeOf(moduleDir));
|
|
159
210
|
return changed;
|
|
160
211
|
}
|
|
161
212
|
catch {
|
|
@@ -235,6 +286,7 @@ export function abandonWork(start, moduleName, why) {
|
|
|
235
286
|
git(moduleDir, ["reset", "-q", "--hard", commit]);
|
|
236
287
|
git(moduleDir, ["clean", "-q", "-fd"]);
|
|
237
288
|
cleanTestsBuild(moduleDir);
|
|
289
|
+
record({ kind: "history.rolledBack", why: firstLine(why, 300) }, placeOf(moduleDir));
|
|
238
290
|
reportActivity(`${moduleName}: ${why} - ${basename(moduleDir)} is back where the work started`);
|
|
239
291
|
}
|
|
240
292
|
catch {
|
package/dist/regenerate/loop.js
CHANGED
|
@@ -5,14 +5,15 @@ import { STDLIB_ROOT } from "../plain/stdlib.js";
|
|
|
5
5
|
import { ACCEPTANCE_TESTS, FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_REQUIREMENTS } from "../plain/spec.js";
|
|
6
6
|
import { reportActivity, reportStage } from "./activity.js";
|
|
7
7
|
import { llmChunkAgents, MAX_LINES_PER_JOB, MAX_SPECS_PER_JOB, planChunks, stepsOf } from "./chunk-plan.js";
|
|
8
|
-
import { commitNotes, recordHistory, resetToLastCommit } from "./lineage.js";
|
|
8
|
+
import { commitNotes, landWork, recordHistory, resetToLastCommit } from "./lineage.js";
|
|
9
|
+
import { firstLine, record } from "./ledger.js";
|
|
9
10
|
import { NOTES_FILE, recordPulse } from "./notes.js";
|
|
10
11
|
import { createJobRegistry, formatDuration } from "./jobs.js";
|
|
11
12
|
import { moduleLanes } from "./lanes.js";
|
|
12
13
|
import { adoptDigests, chainTree, codeTree, conformanceTarget, marksRegenerateAgain, REGENERATE_AGAIN, inheritedRecord, notBuilt, readRecord, RECORD_FILE, withResourceDigests, writeCodeRecord, writeTestsRecord, } from "./records.js";
|
|
13
14
|
import { describeSpecDiffConformance, renderSpecDiffConformance } from "./conform.js";
|
|
14
15
|
import { conformanceDirFor } from "./conformance.js";
|
|
15
|
-
import { modulesRoot } from "./paths.js";
|
|
16
|
+
import { modulesRoot, projectRootOf } from "./paths.js";
|
|
16
17
|
import { budgetFor, choosingWhen, configForSpec, conformanceSwitchedOn, stackForModule, stackPinned, withStack } from "../config.js";
|
|
17
18
|
import { describeSpecChange } from "./prompt.js";
|
|
18
19
|
import { resolveTemplateDirs } from "./paths.js";
|
|
@@ -191,6 +192,11 @@ export function createWorkLoop(options) {
|
|
|
191
192
|
// One tick at a time: each reads the records and the spec, and decides.
|
|
192
193
|
let ticking = Promise.resolve();
|
|
193
194
|
const tipEntry = (state) => tipOf(state.chain);
|
|
195
|
+
/** Where what the loop decides for a module is recorded: its tip's project, the module and the pulse it is about. */
|
|
196
|
+
const loopPlace = (state, job) => {
|
|
197
|
+
const tip = tipEntry(state);
|
|
198
|
+
return { project: projectRootOf(tip.buildDir), module: tip.moduleName, job };
|
|
199
|
+
};
|
|
194
200
|
/** `work` with the codebase's stack chosen - the one it was asked for with - when it has one pinned. */
|
|
195
201
|
const inStack = (state, work) => (state.stack !== undefined ? withStack(state.stack.name, work) : work());
|
|
196
202
|
const keyOf = (spec) => serializeTree(codeTree(spec.tree));
|
|
@@ -681,6 +687,7 @@ export function createWorkLoop(options) {
|
|
|
681
687
|
/* a spec that does not parse halts it all the same */
|
|
682
688
|
}
|
|
683
689
|
state.halted = { why: `${why} - nothing more is built on it until its conformance tests pass`, specKey: key, pulse: pulseOf(running), ...(settled?.status === "stopped" ? {} : causeOf(settled)) };
|
|
690
|
+
record({ kind: "loop.halted", why: firstLine(state.halted.why, 300), track: "conformance" }, loopPlace(state, running.jobId));
|
|
684
691
|
}
|
|
685
692
|
}
|
|
686
693
|
else if (running !== undefined && !landed) {
|
|
@@ -700,6 +707,7 @@ export function createWorkLoop(options) {
|
|
|
700
707
|
}
|
|
701
708
|
const why = settled?.status === "stopped" ? `pulse ${running.jobId} was stopped` : `pulse ${running.jobId} ("${running.planned.title}") did not land`;
|
|
702
709
|
state.halted = { why: `${why} - it runs again once the spec changes, or the module is asked for again`, specKey: key, pulse: pulseOf(running), ...(settled?.status === "stopped" ? {} : causeOf(settled)) };
|
|
710
|
+
record({ kind: "loop.halted", why: firstLine(state.halted.why, 300), track: "code" }, loopPlace(state, running.jobId));
|
|
703
711
|
}
|
|
704
712
|
}
|
|
705
713
|
else {
|
|
@@ -866,6 +874,7 @@ export function createWorkLoop(options) {
|
|
|
866
874
|
else {
|
|
867
875
|
state.running.stopping = true;
|
|
868
876
|
state.why = `pulse ${state.running.jobId} was stopped: ${why}, which it was regenerating from`;
|
|
877
|
+
record({ kind: "loop.stopped", why: firstLine(why, 300) }, loopPlace(state, state.running.jobId));
|
|
869
878
|
reportActivity(` ${tip.moduleName}: ${state.why}`);
|
|
870
879
|
options.registry.stop(state.running.jobId, `Stopped by turri, not by you: ${why}, which it was regenerating from. What it had done is rolled back, and the spec as it is now is regenerated instead. To have a running pulse finish first and the change regenerated on top of it, set allow-to-stop-jobs: false in config.turri.yaml.`);
|
|
871
880
|
return;
|
|
@@ -1608,7 +1617,7 @@ const defaultConformanceRunner = async (job, onDelta) => {
|
|
|
1608
1617
|
});
|
|
1609
1618
|
if (result.passed) {
|
|
1610
1619
|
writeTestsRecord(result.conformanceDir, job.planned.step.tree);
|
|
1611
|
-
|
|
1620
|
+
landWork(job.buildDir, `conformance tests: ${job.planned.title}`);
|
|
1612
1621
|
}
|
|
1613
1622
|
return { landed: result.passed, report: describeSpecDiffConformance(result), ...(result.specGaps !== undefined ? { specGaps: result.specGaps } : {}) };
|
|
1614
1623
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A file the running pulse wrote for a person to read - removed as it ends, unless in debug mode. Outside a pulse, nothing. */
|
|
2
|
+
export declare function pulseWrote(file: string, job?: string | undefined): void;
|
|
3
|
+
/**
|
|
4
|
+
* A pulse has ended. Unless the project is in debug mode, what it wrote for a person goes - its files, and its module's
|
|
5
|
+
* temporary folder - and so do the dependencies installed in the modules below its own in its chain. `lanes` are the
|
|
6
|
+
* pulse's (`plain_modules/<module>` of its whole chain), still held: no other pulse works in them. Returns whether
|
|
7
|
+
* everything was kept.
|
|
8
|
+
*/
|
|
9
|
+
export declare function pulseEnded(options: {
|
|
10
|
+
job: string;
|
|
11
|
+
project: string;
|
|
12
|
+
module?: string;
|
|
13
|
+
lanes: string[];
|
|
14
|
+
}): boolean;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, rmSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { debugMode } from "../config.js";
|
|
4
|
+
import { currentJob } from "./activity.js";
|
|
5
|
+
import { modulesScratchDir } from "./paths.js";
|
|
6
|
+
/**
|
|
7
|
+
* What a pulse writes only for a person debugging turri, and when it goes.
|
|
8
|
+
*
|
|
9
|
+
* A pulse writes its own log, every one of its agents' whole logs, the testing scripts' and services' full outputs and
|
|
10
|
+
* its agents' temporary files. Nothing of turri reads them once the pulse is over: while it runs, its agents read their
|
|
11
|
+
* outputs (`read_output`, `service_logs`) and the diagnosis of a failure reads the logs - so they are written as before,
|
|
12
|
+
* and removed as it ends. In debug mode (`debug: true`, TURRI_DEBUG, `--debug`) they are kept, as they always were.
|
|
13
|
+
*
|
|
14
|
+
* So are the dependencies installed in the modules below a pulse's own in its chain: the chain is built in its tip, and
|
|
15
|
+
* what was installed in the codebases it left behind is only installed again - from the package cache - if one of them
|
|
16
|
+
* is ever built on again. The testing scripts install what they run with themselves.
|
|
17
|
+
*/
|
|
18
|
+
/** The files each pulse wrote for a person to read, by its id. */
|
|
19
|
+
const written = new Map();
|
|
20
|
+
/** A file the running pulse wrote for a person to read - removed as it ends, unless in debug mode. Outside a pulse, nothing. */
|
|
21
|
+
export function pulseWrote(file, job = currentJob()?.id) {
|
|
22
|
+
if (job === undefined)
|
|
23
|
+
return;
|
|
24
|
+
const files = written.get(job) ?? new Set();
|
|
25
|
+
files.add(file);
|
|
26
|
+
written.set(job, files);
|
|
27
|
+
}
|
|
28
|
+
/** Where a codebase's dependencies are installed: what a module its chain has left behind keeps no longer. */
|
|
29
|
+
const INSTALLED = ["node_modules", ".venv", "venv"];
|
|
30
|
+
/**
|
|
31
|
+
* A pulse has ended. Unless the project is in debug mode, what it wrote for a person goes - its files, and its module's
|
|
32
|
+
* temporary folder - and so do the dependencies installed in the modules below its own in its chain. `lanes` are the
|
|
33
|
+
* pulse's (`plain_modules/<module>` of its whole chain), still held: no other pulse works in them. Returns whether
|
|
34
|
+
* everything was kept.
|
|
35
|
+
*/
|
|
36
|
+
export function pulseEnded(options) {
|
|
37
|
+
const files = written.get(options.job) ?? new Set();
|
|
38
|
+
written.delete(options.job);
|
|
39
|
+
if (debugMode(options.project))
|
|
40
|
+
return true;
|
|
41
|
+
const gone = (path) => {
|
|
42
|
+
try {
|
|
43
|
+
rmSync(path, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* what cannot be removed stays: never a reason for the pulse to fail */
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
for (const file of files)
|
|
50
|
+
gone(file);
|
|
51
|
+
for (const lane of options.lanes) {
|
|
52
|
+
// A codebase of the chain: its folder holds `code` - a lane may also be a spec file, or a build folder.
|
|
53
|
+
if (!existsSync(join(lane, "code")))
|
|
54
|
+
continue;
|
|
55
|
+
const name = basename(lane);
|
|
56
|
+
if (name === options.module)
|
|
57
|
+
gone(join(modulesScratchDir(lane), "tmp", name));
|
|
58
|
+
else
|
|
59
|
+
for (const installed of INSTALLED)
|
|
60
|
+
gone(join(lane, "code", installed));
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
@@ -11,7 +11,8 @@ import { debugFailure, diagnosisText } from "./debugger.js";
|
|
|
11
11
|
import { codebaseMap, diffFiles, digestFiles, wroteNothing } from "./codebase.js";
|
|
12
12
|
import { confinedTools } from "./confine.js";
|
|
13
13
|
import { buildSpecDiffPrompt, describeSpecChange, reviewedSpec, sectionModules } from "./prompt.js";
|
|
14
|
-
import { abandonWork,
|
|
14
|
+
import { abandonWork, landWork, startWork } from "./lineage.js";
|
|
15
|
+
import { healing, record, signatureHash } from "./ledger.js";
|
|
15
16
|
import { createWorkLedger, SUBMIT_WORK_NOTE } from "./submit-work.js";
|
|
16
17
|
import { CODER_COMPACTION } from "./compaction.js";
|
|
17
18
|
import { gapMark, gapsSince, NOTES_TURN, notesForSession, passedOutcome, recordNote, withNotes } from "./notes.js";
|
|
@@ -193,7 +194,7 @@ export async function renderSpecDiff(options) {
|
|
|
193
194
|
if (builtBefore !== null && sameSpec(builtBefore, tree) && !dryRun) {
|
|
194
195
|
stage("the specs were only restructured - nothing to render; recording the spec as it is now laid out");
|
|
195
196
|
writeCodeRecord(tip.buildDir, tree);
|
|
196
|
-
|
|
197
|
+
landWork(tip.buildDir, `spec restructured: ${summarizeDiff(options.diff)}`, options.diff);
|
|
197
198
|
return {
|
|
198
199
|
moduleName,
|
|
199
200
|
buildDir: tip.buildDir,
|
|
@@ -440,7 +441,7 @@ export async function renderSpecDiff(options) {
|
|
|
440
441
|
: timedOut && seen < STUCK_ROUNDS
|
|
441
442
|
? `the unit testing script did not finish in ${Math.round(verdict.run.timeoutMs / 1000)}s - a debugger works out whether something hangs`
|
|
442
443
|
: `the same failure ${seen} times - a debugger looks at it with fresh eyes`);
|
|
443
|
-
const found = await debugFailure({
|
|
444
|
+
const found = await healing("debugger", () => debugFailure({
|
|
444
445
|
buildDir,
|
|
445
446
|
failure: verdict.run.output,
|
|
446
447
|
times,
|
|
@@ -450,7 +451,7 @@ export async function renderSpecDiff(options) {
|
|
|
450
451
|
label: `${moduleName} spec change`,
|
|
451
452
|
...(debuggerModel !== undefined ? { model: debuggerModel } : {}),
|
|
452
453
|
onDelta: options.onDelta,
|
|
453
|
-
})
|
|
454
|
+
}), (finding) => finding?.rootCause);
|
|
454
455
|
// Nothing hangs: the suite needs more time than a test run may take, and no agent can give it that.
|
|
455
456
|
if (found?.tooSlow === true) {
|
|
456
457
|
stage(`the unit testing script needs more than the ${Math.round(verdict.run.timeoutMs / 1000)}s a test run may take - stopping`);
|
|
@@ -477,6 +478,7 @@ export async function renderSpecDiff(options) {
|
|
|
477
478
|
].join("\n");
|
|
478
479
|
}
|
|
479
480
|
}
|
|
481
|
+
record({ kind: "gate.round", gate: "unit tests", round: followUp + 1, of: MAX_FOLLOW_UPS, ...(key !== undefined ? { signature: signatureHash(key) } : {}) });
|
|
480
482
|
stage(`${verdict.staticOnly ? "static checks" : "unit tests"} not passing - fixing (round ${followUp + 1} of ${MAX_FOLLOW_UPS})`);
|
|
481
483
|
response = await ask(executor, prompt, options.onDelta);
|
|
482
484
|
continue;
|
|
@@ -554,7 +556,7 @@ export async function renderSpecDiff(options) {
|
|
|
554
556
|
stage("recording what the code is built from, and committing it");
|
|
555
557
|
// Steered, it is built from the spec as it was steered to - the record the loop moved with it.
|
|
556
558
|
writeCodeRecord(buildDir, steering?.target ?? tree);
|
|
557
|
-
|
|
559
|
+
landWork(buildDir, `spec diff: ${summarizeDiff(options.diff)}`, options.diff);
|
|
558
560
|
};
|
|
559
561
|
if (!unitPassed) {
|
|
560
562
|
stage(unitReviewStopped ? "the unit tests pass but still leave the change untested - undoing the change" : "unit tests NOT passing - undoing the change");
|
|
@@ -6,7 +6,8 @@ import { reportActivity, reportStage, unitOf } from "./activity.js";
|
|
|
6
6
|
import { confinedTools } from "./confine.js";
|
|
7
7
|
import { projectRootOf } from "./paths.js";
|
|
8
8
|
import { buildRegressionFixPrompt, sectionModules } from "./prompt.js";
|
|
9
|
-
import { abandonWork,
|
|
9
|
+
import { abandonWork, landWork, markWork } from "./lineage.js";
|
|
10
|
+
import { record } from "./ledger.js";
|
|
10
11
|
import { missingToolchainError } from "./environment-error.js";
|
|
11
12
|
import { workspaceBlock } from "./workspace.js";
|
|
12
13
|
import { CODER_COMPACTION } from "./compaction.js";
|
|
@@ -154,13 +155,14 @@ export async function ensureRegressionPasses(input) {
|
|
|
154
155
|
if (passing) {
|
|
155
156
|
reportStage(`${unitOf(input.moduleName)}: the whole conformance suite passes, and the unit tests with it${repairs.length > 0 ? ` after ${repairs.length} repair${repairs.length === 1 ? "" : "s"}` : ""}`);
|
|
156
157
|
if (repairs.length > 0) {
|
|
157
|
-
|
|
158
|
+
landWork(input.buildDir, `regression ${input.moduleName} (pass after ${repairs.length} repair(s))`);
|
|
158
159
|
}
|
|
159
160
|
return { passed: true, runs, repairs, command };
|
|
160
161
|
}
|
|
161
162
|
if (round === maxRounds)
|
|
162
163
|
break;
|
|
163
164
|
input.onEvent?.({ type: "repair", frids: input.frids });
|
|
165
|
+
record({ kind: "gate.round", gate: "regression", round: round + 1, of: maxRounds });
|
|
164
166
|
reportStage(`${unitOf(input.moduleName)}: an earlier functionality broke - repairing (round ${round + 1} of ${maxRounds})`);
|
|
165
167
|
repairs.push(await repairRegression({
|
|
166
168
|
testsDirs: dirs,
|
|
@@ -8,6 +8,7 @@ import { agentShell } from "./agent-shell.js";
|
|
|
8
8
|
import { maskSecrets } from "./masking.js";
|
|
9
9
|
import { modulesScratchDir } from "./paths.js";
|
|
10
10
|
import { isRunning, startInBackground, stopGroup } from "./shell.js";
|
|
11
|
+
import { pulseWrote } from "./pulse-files.js";
|
|
11
12
|
/**
|
|
12
13
|
* What an agent starts and keeps running while it works - a server, a worker - to try the software as someone would,
|
|
13
14
|
* from outside: with curl, or what the stack's own client is. Whatever the stack: the agent gives the command that
|
|
@@ -83,6 +84,8 @@ export function createServiceTools(options) {
|
|
|
83
84
|
return say(`Not started: ${checked.reason}`);
|
|
84
85
|
mkdirSync(logsDir, { recursive: true });
|
|
85
86
|
const log = join(logsDir, `${name.replace(/[^\w.-]+/g, "_")}-${randomUUID().slice(0, 8)}.log`);
|
|
87
|
+
// Read through service_logs while the pulse runs, and by no one after it (pulse-files.ts).
|
|
88
|
+
pulseWrote(log);
|
|
86
89
|
const limit = Math.min(Math.max(seconds ?? DEFAULT_READY_SECONDS, 1), MAX_READY_SECONDS);
|
|
87
90
|
// Started and waited on under the audit: what it writes while it comes up, outside its role, is put back.
|
|
88
91
|
const { value: outcome, note } = await fenced.audited(async () => {
|
|
@@ -17,6 +17,8 @@ import type { TranscriptStyle } from "./transcript-view.ts";
|
|
|
17
17
|
export interface SidePanelOptions {
|
|
18
18
|
/** Every job, oldest first. */
|
|
19
19
|
jobs: () => Job[];
|
|
20
|
+
/** Earlier sessions' pulses, from the ledger: listed under this session's, in a section of their own. */
|
|
21
|
+
earlier?: () => Job[];
|
|
20
22
|
/** The regeneration's pages. Not given: the pulse list alone. */
|
|
21
23
|
map?: () => MarkedPage[];
|
|
22
24
|
/** Rows the panel may use. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
-
import { createJobsTreeState, jobsTreeLines, jobsTreeRows, keepCursorInView, scrollTree, treeKey } from "./jobs-tree.js";
|
|
2
|
+
import { createJobsTreeState, jobsTreeLines, jobsTreeRows, keepCursorInView, scrollTree, sectionOf, treeKey } from "./jobs-tree.js";
|
|
3
3
|
import { columnHeadings, createSpecMapState, DISCUSS_KEY, legendLines, minimap, shownPage, specMapHeader, specMapLines, switchTab, tabBar } from "./spec-map-view.js";
|
|
4
4
|
export function createSidePanel(options) {
|
|
5
5
|
const style = options.style;
|
|
@@ -71,7 +71,8 @@ export function createSidePanel(options) {
|
|
|
71
71
|
const bodyHeight = Math.max(3, rows - top.length - 2);
|
|
72
72
|
lastBodyHeight = bodyHeight;
|
|
73
73
|
// The tree scrolls to keep the cursor in sight, and only that far.
|
|
74
|
-
|
|
74
|
+
// This session's pulses, and under them the earlier sessions' the ledger has.
|
|
75
|
+
const tree_ = jobsTreeLines([...options.jobs(), ...(options.earlier?.() ?? [])], tree, width, style);
|
|
75
76
|
drawnTree = { rows: tree_.rows, rowStarts: tree_.rowStarts, lineCount: tree_.lines.length };
|
|
76
77
|
keepCursorInView(tree, tree_.rowStarts, tree_.lines.length, bodyHeight);
|
|
77
78
|
const visible = tree_.lines.slice(tree.top, tree.top + bodyHeight);
|
|
@@ -132,7 +133,7 @@ export function createSidePanel(options) {
|
|
|
132
133
|
const job = jobs.find((entry) => entry.id === id);
|
|
133
134
|
if (job === undefined)
|
|
134
135
|
return;
|
|
135
|
-
const section = job
|
|
136
|
+
const section = sectionOf(job);
|
|
136
137
|
tree.open[section] = true;
|
|
137
138
|
const at = jobsTreeRows(jobs, tree).findIndex((entry) => entry.kind === "job" && entry.job.id === id);
|
|
138
139
|
if (at !== -1)
|
|
@@ -4,6 +4,7 @@ import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync
|
|
|
4
4
|
import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { CONFIG_FILE, CONFIG_KEYS, projectConfigFile, readProjectConfig, stackConfigFile, stackConfigFileIn, stackOf, stacksOf } from "../config.js";
|
|
6
6
|
import { reportActivity, reportLogNote, reportNote } from "./activity.js";
|
|
7
|
+
import { record, signatureHash } from "./ledger.js";
|
|
7
8
|
import { ensureInModulesScratch, modulesScratchDir } from "./paths.js";
|
|
8
9
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { Type } from "typebox";
|
|
@@ -361,7 +362,7 @@ async function runScript(cwd, script, args, logDir, options = {}) {
|
|
|
361
362
|
const noted = withScriptLineNote(text, script);
|
|
362
363
|
// Its failures, counts and end are what the agent is shown; the whole is kept, a read_output away.
|
|
363
364
|
const log = keepTestOutput(logDir, basename(script), noted);
|
|
364
|
-
|
|
365
|
+
const run = {
|
|
365
366
|
command: shown(script),
|
|
366
367
|
exitCode: timedOut === true ? 124 : exitCode,
|
|
367
368
|
output: shownTestOutput(noted, log),
|
|
@@ -370,6 +371,18 @@ async function runScript(cwd, script, args, logDir, options = {}) {
|
|
|
370
371
|
elapsedMs: Date.now() - started,
|
|
371
372
|
...(log !== undefined ? { log } : {}),
|
|
372
373
|
};
|
|
374
|
+
record({
|
|
375
|
+
kind: "script.run",
|
|
376
|
+
script: basename(script),
|
|
377
|
+
exit: run.exitCode,
|
|
378
|
+
ms: run.elapsedMs,
|
|
379
|
+
timedOut: run.timedOut === true,
|
|
380
|
+
inPlace: options.named === undefined,
|
|
381
|
+
// On code changed for the run - can_it_fail's bug: failing is what it is for.
|
|
382
|
+
...(options.mutated === true ? { mutated: true } : {}),
|
|
383
|
+
...(run.exitCode !== 0 ? { signature: signatureHash(failureSignature(run)) } : {}),
|
|
384
|
+
});
|
|
385
|
+
return run;
|
|
373
386
|
}
|
|
374
387
|
/** What bash says when a line of the script itself cannot run: a command or file that is not there, or not runnable. */
|
|
375
388
|
const OWN_LINE_FAILURE = /: (No such file or directory|command not found|Permission denied)$/;
|
|
@@ -406,10 +419,10 @@ function quote(path) {
|
|
|
406
419
|
return `'${path.replace(/'/g, "'\\''")}'`;
|
|
407
420
|
}
|
|
408
421
|
/** A testing script run on a fresh copy of the code: the project's script copied fresh into the staging folder, beside it. */
|
|
409
|
-
async function runOnCopy(stagingRoot, script, args, logDir, limitMs) {
|
|
422
|
+
async function runOnCopy(stagingRoot, script, args, logDir, limitMs, mutated) {
|
|
410
423
|
const copy = join(stagingRoot, basename(script));
|
|
411
424
|
writeFileSync(copy, readFileSync(script));
|
|
412
|
-
return runScript(stagingRoot, script, args, logDir, { named: copy, ...(limitMs !== undefined ? { limitMs } : {}) });
|
|
425
|
+
return runScript(stagingRoot, script, args, logDir, { named: copy, ...(limitMs !== undefined ? { limitMs } : {}), ...(mutated === true ? { mutated } : {}) });
|
|
413
426
|
}
|
|
414
427
|
/**
|
|
415
428
|
* The harness's run of the unit testing script on the code as it is now - and run_unit_tests'.
|
|
@@ -475,7 +488,7 @@ limitMs) {
|
|
|
475
488
|
return run;
|
|
476
489
|
};
|
|
477
490
|
if (project.inPlace) {
|
|
478
|
-
const inPlace = () => runScript(project.root, script, [resolve(buildDir)], buildDir, limitMs !== undefined ? { limitMs } : {});
|
|
491
|
+
const inPlace = () => runScript(project.root, script, [resolve(buildDir)], buildDir, { ...(limitMs !== undefined ? { limitMs } : {}), ...(change !== undefined ? { mutated: true } : {}) });
|
|
479
492
|
const started = Date.now();
|
|
480
493
|
if (change === undefined)
|
|
481
494
|
return passed(await inPlace(), started);
|
|
@@ -490,7 +503,7 @@ limitMs) {
|
|
|
490
503
|
if (change !== undefined)
|
|
491
504
|
writeFileSync(join(staging.build, change.file), change.text);
|
|
492
505
|
const started = Date.now();
|
|
493
|
-
return passed(await runOnCopy(staging.root, script, [staging.build], buildDir, limitMs), started);
|
|
506
|
+
return passed(await runOnCopy(staging.root, script, [staging.build], buildDir, limitMs, change !== undefined), started);
|
|
494
507
|
}
|
|
495
508
|
finally {
|
|
496
509
|
rmSync(staging.root, { recursive: true, force: true });
|
|
@@ -9,6 +9,7 @@ import { modulesScratchDir } from "./paths.js";
|
|
|
9
9
|
import { RECORD_FILE } from "./records.js";
|
|
10
10
|
import { missingToolchain, runUnitScript, scriptFailure } from "./test-scripts.js";
|
|
11
11
|
import { COMMAND_TIMEOUT_MS } from "./shell.js";
|
|
12
|
+
import { pulseWrote } from "./pulse-files.js";
|
|
12
13
|
/** Up to this much of a run's output is shown: all of it when it fits, otherwise its end. */
|
|
13
14
|
const MAX_OUTPUT_CHARS = 60_000;
|
|
14
15
|
/** A test run has the same time limit as any command: see COMMAND_TIMEOUT_MS. */
|
|
@@ -92,6 +93,8 @@ export function keepOutput(dir, label, output) {
|
|
|
92
93
|
const name = label.replace(/[^\w.-]+/g, "_").slice(0, 60);
|
|
93
94
|
const file = join(logs, `${stamp}-${name}.log`);
|
|
94
95
|
writeFileSync(file, typeof output === "string" ? maskSecrets(output) : maskSecrets(output.toString("utf8")));
|
|
96
|
+
// Read through read_output while the pulse runs, and by no one after it (pulse-files.ts).
|
|
97
|
+
pulseWrote(file);
|
|
95
98
|
const all = readdirSync(logs).sort();
|
|
96
99
|
for (const old of all.slice(0, Math.max(0, all.length - MAX_LOGS)))
|
|
97
100
|
rmSync(join(logs, old), { force: true });
|
|
@@ -3,6 +3,7 @@ import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { ask, createExecutor } from "../pi.js";
|
|
5
5
|
import { reportActivity } from "./activity.js";
|
|
6
|
+
import { record } from "./ledger.js";
|
|
6
7
|
import { diffFiles, digestFiles } from "./codebase.js";
|
|
7
8
|
import { confinedTools } from "./confine.js";
|
|
8
9
|
import { REVIEWER_CALLS, reviewKind } from "./conformance-agents.js";
|
|
@@ -288,6 +289,7 @@ async function reviewLoop(options, reviewer) {
|
|
|
288
289
|
letGo = [];
|
|
289
290
|
options.stage(`reviewing the code and its unit tests (review ${reviews + 1}): does the code do what the spec says, and does each test fail when that breaks`);
|
|
290
291
|
const review = await reviewer.review(request);
|
|
292
|
+
record({ kind: "review.verdict", reviewer: "unit", review: reviews + 1, pass: review.pass, problems: review.pass ? 0 : review.problems.length });
|
|
291
293
|
if (review.pass) {
|
|
292
294
|
options.stage("the review has nothing left to improve in the code or its unit tests");
|
|
293
295
|
return waived.length > 0 ? { passed: true, waived, reviews: reviews + 1 } : { passed: true, reviews: reviews + 1 };
|
|
@@ -316,6 +318,7 @@ async function reviewLoop(options, reviewer) {
|
|
|
316
318
|
});
|
|
317
319
|
// No ruling: the review stands, as it did before there was an arbiter.
|
|
318
320
|
if (ruling !== undefined) {
|
|
321
|
+
record({ kind: "arbiter.ruling", upheld: ruling.upheld.length, dismissed: ruling.dismissed.length });
|
|
319
322
|
waived.push(...ruling.dismissed.map((entry) => entry.problem));
|
|
320
323
|
if (ruling.dismissed.length > 0)
|
|
321
324
|
recordNote(options.buildDir, "pitfalls", dismissedNote(ruling, "unit test"), `${options.label} unit tests`);
|
package/dist/run-report.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { debugMode } from "./config.js";
|
|
3
4
|
import { turriIdentity } from "./environment.js";
|
|
4
5
|
import { onModelEvent } from "./regenerate/activity.js";
|
|
5
6
|
import { firstRegeneration } from "./regenerate/jobs.js";
|
|
@@ -290,11 +291,14 @@ export function startRunReport(options) {
|
|
|
290
291
|
spec_gaps: specGapsOf(jobs),
|
|
291
292
|
turn_incidents: turnIncidentsOf(),
|
|
292
293
|
...(exitParts ?? {}),
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
// Kept only in debug mode: otherwise each pulse's go as it ends, and the sessions are never written.
|
|
295
|
+
logs: debugMode(projectRoot)
|
|
296
|
+
? {
|
|
297
|
+
jobs_dir: fromProject(join(projectRoot, ".turri", "jobs")),
|
|
298
|
+
debug_dir: fromProject(join(projectRoot, ".turri", "debug")),
|
|
299
|
+
sessions_dir: fromProject(join(projectRoot, ".turri", "sessions")),
|
|
300
|
+
}
|
|
301
|
+
: null,
|
|
298
302
|
};
|
|
299
303
|
};
|
|
300
304
|
const write = (exitCode) => {
|
package/docs/config.md
CHANGED
|
@@ -42,11 +42,12 @@ Each change says when it takes effect. None needs turri started again - what tur
|
|
|
42
42
|
| --- | --- |
|
|
43
43
|
| now | `harness-model` and `models.chat` - the conversation switches to it; `parallel-jobs` - a pulse waiting for a slot starts if there is one now; `allow-to-stop-jobs` |
|
|
44
44
|
| your next message | `rules-off` - for the conversation; a skill's agents from the next skill run |
|
|
45
|
-
| the next pulse - one running keeps what it started with | `coding-model`, `judge-model`, every other `models.<role>`, `review`, `mutation-testing`, `turn-limit`, `compact-at`, `agent-env`, `toolchains` |
|
|
45
|
+
| the next pulse - one running keeps what it started with | `coding-model`, `judge-model`, every other `models.<role>`, `review`, `mutation-testing`, `turn-limit`, `compact-at`, `agent-env`, `toolchains`, `debug` |
|
|
46
46
|
| the next time the work is planned | `conformance-tests`, `conformance-budget`, `max-specs-per-pulse`, `max-lines-per-pulse` |
|
|
47
47
|
| the next test run - the running pulse's too | the testing scripts, `testing-scripts-run-on` |
|
|
48
48
|
| the next pulse that lands - the running one too | `release-after-pulse` |
|
|
49
49
|
| the next regeneration that names no `.plain` file | `module` |
|
|
50
|
+
| the next time turri starts here | `ledger-keep`, `cache-keep` |
|
|
50
51
|
|
|
51
52
|
A coding model chosen this session - `--coding-model`, or `engine` - gives way to one set here: the last word wins. It says, too, what wins over the change: `models.chat` over `harness-model` (and so on), and a stack's `config.turri.<stack>.yaml`, which is not changed here, while that stack is chosen. In a project still read from `config.yaml`, the first change copies it to `config.turri.yaml` first - see [the old config.yaml](#the-old-configyaml).
|
|
52
53
|
|
|
@@ -91,6 +92,9 @@ models: # each agent role's model, thinking and tur
|
|
|
91
92
|
| `conformance-tests-script` | the conformance testing script, by path from the project root | looked for - see below |
|
|
92
93
|
| `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 |
|
|
93
94
|
| `compact-at` | how many tokens of work - past its instructions - an agent kept through a job holds before the harness compacts it between turns; at least 1000; the project's alone | 100,000 - see [agent sessions](regenerating.md#agent-sessions) |
|
|
95
|
+
| `ledger-keep` | how many megabytes of the ledger - what turri did, as facts, in `.turri/ledger` - the project keeps: past it, the oldest runs' are pruned when a run first writes; `0` keeps none, and writes none; the project's alone. See [the ledger](using.md#the-ledger) | 100 |
|
|
96
|
+
| `debug` | `true` or `false`: whether turri keeps what it writes only for a person debugging it - every agent's whole log and saved session, each pulse's log, the whole outputs of its test runs and services, its agents' temporary files, and the dependencies installed in the modules a chain has left behind. `false`: only what turri reads to work is kept. `TURRI_DEBUG=1` - or `turri --debug` - turns it on for one run, and `TURRI_DEBUG=0` off; the project's alone. See [debug mode](#debug-mode) | `false` |
|
|
97
|
+
| `cache-keep` | how many megabytes of package caches the project keeps - `plain_modules/.turri/cache`, and the caches in the agents' home folders: past it, the least recently written - by the newest file in each - are removed when a run starts its first agent command, each downloaded again when it is wanted; `0` keeps none; the project's alone | 4096 |
|
|
94
98
|
| `max-specs-per-pulse` | the most functional specs the planner groups into one pulse - what builds one feature goes together, up to this many; a stack's file wins | 3 |
|
|
95
99
|
| `max-lines-per-pulse` | the most lines of spec in one pulse - its functional specs with the definitions they bring; a group past it is split again, a functional spec a pulse; a stack's file wins | 120 |
|
|
96
100
|
| `parallel-jobs` | how many jobs the chat runs at once - modules that share nothing build side by side up to it; read when the chat starts, from the folder it starts in | half this machine's cores, between one and four - see [how many jobs at once](#how-many-jobs-at-once) |
|
|
@@ -178,6 +182,23 @@ The optional prepare script - `prepare-environment-script:`, else the one `prepa
|
|
|
178
182
|
|
|
179
183
|
A folder holding two of a kind is not guessed between: name the one to use. A script named inside `plain_modules/` is refused - agents write there, so it could be changed by the work it judges. The scripts are never linked from the specs. What they are and how they run is in [regenerating the code](regenerating.md#the-testing-scripts).
|
|
180
184
|
|
|
185
|
+
## Debug mode
|
|
186
|
+
|
|
187
|
+
turri writes much more than it needs to work, for a person working out why it did what it did - each agent's whole conversation, each pulse's whole transcript, every test run's whole output. Most of it nothing of turri ever reads again. So by default it is not kept:
|
|
188
|
+
|
|
189
|
+
| | outside debug mode | in debug mode |
|
|
190
|
+
| --- | --- | --- |
|
|
191
|
+
| each pulse's log (`.turri/jobs/`) and its agents' (`.turri/debug/`) | written while the pulse runs - the diagnosis of a failure reads them - and removed as it ends | kept |
|
|
192
|
+
| the logs of agents outside a pulse - the release agent, a side question | not written | kept |
|
|
193
|
+
| the agents' saved sessions (`.turri/sessions/`) | not written - each is kept in memory while it works; a gated skill's alone is saved, as its run resumes from it | kept |
|
|
194
|
+
| whole outputs of test runs and services (`plain_modules/.turri/logs/`, `services/`) | read by the pulse's agents while it runs, removed as it ends | kept, the last 100 outputs |
|
|
195
|
+
| a module's `TMPDIR` (`plain_modules/.turri/tmp/<module>`) | emptied as each of its pulses ends | kept |
|
|
196
|
+
| dependencies installed in the modules below a pulse's own in its chain (`node_modules`, `.venv`) | removed as the pulse ends: the chain is built in its tip, and a module built on again installs them again, from the cache | kept |
|
|
197
|
+
|
|
198
|
+
Kept either way: the ledger - what each pulse did, how it ended, what it cost (see [the ledger](using.md#the-ledger)) - the code, its tests and their history, a gated skill's run, what a conformance explorer found, what `deploy` started, and the package caches, held to `cache-keep:`. The testing scripts install what they run with themselves, so nothing removed is ever missed by a test run.
|
|
199
|
+
|
|
200
|
+
Turn it on in `config.turri.yaml` (`debug: true`, or `/turri-config debug true`), or for one run with `TURRI_DEBUG=1` or `turri --debug`.
|
|
201
|
+
|
|
181
202
|
## Credentials for the agents
|
|
182
203
|
|
|
183
204
|
An agent's commands do not get the environment turri was started in: it holds your providers' API keys, cloud credentials and tokens. They get only the path, the locale, the terminal and the user, the variables that locate your toolchains (`JAVA_HOME`, `NVM_DIR`, `RUSTUP_HOME`, ... - the ones in your home found even when your shell sets none), and turri's own: a home folder of the module's own (`plain_modules/.turri/home/<module>`, kept between pulses, with a git identity of its own), a `TMPDIR`, the package caches in `plain_modules/.turri/cache`, and git stopped at the module folder, so a project that is itself a repository is never found from the code.
|
|
@@ -229,7 +250,7 @@ How a stack's config is made:
|
|
|
229
250
|
- **Laid over `config.turri.yaml`, key by key.** What the stack's file gives wins; what it leaves out is the project's. A model it names takes what it leaves out from the project's same model - above, with the stack frontend chosen, everything renders on the project's coding model, thinking `high`.
|
|
230
251
|
- **Pinned to a module when it is asked for.** A module is built with the stack it was asked for with, from its first pulse to its last: the pulses planned after it, its test runs, the scripts found, its plans and its conformance tests, whatever stack is chosen meanwhile. A pulse waiting for a free slot keeps the stack it was queued with. A change to its spec keeps its stack - unless it was built with none and has a stack of its own now; asked for again, its stack is worked out again. What `regenerate` says back names each module's stack, and so does a pulse's report.
|
|
231
252
|
- **Refused with none, when that cannot build.** In a project of stacks, a module asked for with no stack - none named, none named after it, none chosen - is refused before anything is planned when `config.turri.yaml` alone names no unit testing script: every pulse would stop for want of one. The refusal names the stacks and the script each names. A spec change `write_spec_diff` writes is never refused: it is written, and said not to be built yet - passed the stack, everything written is built. `stack: none` builds with `config.turri.yaml` alone all the same.
|
|
232
|
-
- **The project's alone.** `module:`, `harness-model:`, `parallel-jobs:`, `rules-off:` and `
|
|
253
|
+
- **The project's alone.** `module:`, `harness-model:`, `parallel-jobs:`, `rules-off:`, `compact-at:`, `ledger-keep:`, `debug:` and `cache-keep:` are read only from `config.turri.yaml` - there is one conversation, one module built when none is named, one machine. `agent-env:` and `toolchains:` in a stack's file add to the project's.
|
|
233
254
|
|
|
234
255
|
The chat is told each stack's config and the one chosen. The skills that write testing scripts put each stack's in its own file once a project has two stacks, each stack named after the module it builds.
|
|
235
256
|
|
package/docs/development.md
CHANGED
|
@@ -56,7 +56,9 @@ The workflow signs in to npm with npm's trusted publishing (OIDC), not a token:
|
|
|
56
56
|
| `src/regenerate/side-panel.ts`, `src/regenerate/panel-dock.ts` | the side panel, and the chat narrowed beside it when it is docked |
|
|
57
57
|
| `src/screen-padding.ts` | the blank row above and below the whole screen and the two columns either side, in both of pi's ways of drawing it |
|
|
58
58
|
| `src/regenerate/spec-map.ts`, `src/regenerate/spec-map-view.ts` | regen: each item's mark, from the records, the `.plain` files and the work loop's plan (`mapPlan`), and the map drawn |
|
|
59
|
-
| `src/regenerate/debug-log.ts` | each render agent's whole conversation,
|
|
59
|
+
| `src/regenerate/debug-log.ts` | each render agent's whole conversation, streamed to `.turri/debug/` - while its pulse runs, and kept in debug mode |
|
|
60
|
+
| `src/regenerate/pulse-files.ts` | what a pulse writes only for a person - its logs, outputs, temporary files, its chain's installed dependencies - removed as it ends outside debug mode |
|
|
61
|
+
| `src/regenerate/ledger.ts`, `src/regenerate/ledger-view.ts`, `src/ledger-tools.ts`, `src/ledger-cli.ts` | the ledger: what turri did, as typed facts, in `.turri/ledger/` - `record()`, read back for the `ledger` tool, `pulses`, `diagnose` and `turri ledger` - see [the ledger](using.md#the-ledger) |
|
|
60
62
|
| `genome/` | what turri knows and can do: `genome/skills/`, the skill library, and `genome/rules/`, the \*\*\*plain authoring rules |
|
|
61
63
|
|
|
62
64
|
## Known edges
|