@turri-dev/cli 0.1.8-next.01c7342 → 0.1.8-next.b03162b
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/dist/cli.js +2 -1
- package/dist/config-edit.d.ts +1 -1
- package/dist/config-edit.js +2 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +5 -1
- package/dist/extension.js +69 -8
- package/dist/host.d.ts +2 -0
- package/dist/host.js +3 -0
- package/dist/init-config.d.ts +1 -0
- package/dist/init-config.js +5 -0
- package/dist/ledger-cli.d.ts +1 -0
- package/dist/ledger-cli.js +50 -0
- package/dist/loop-cli.js +4 -0
- package/dist/pi.js +17 -0
- package/dist/regenerate/activity.d.ts +9 -0
- package/dist/regenerate/activity.js +11 -0
- package/dist/regenerate/agent-shell.js +5 -0
- package/dist/regenerate/chunk-plan.d.ts +0 -6
- package/dist/regenerate/chunk-plan.js +16 -4
- package/dist/regenerate/conformance.js +3 -0
- package/dist/regenerate/diagnose.js +4 -2
- 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 +38 -3
- package/dist/regenerate/ledger-view.d.ts +73 -0
- package/dist/regenerate/ledger-view.js +286 -0
- package/dist/regenerate/ledger.d.ts +274 -0
- package/dist/regenerate/ledger.js +349 -0
- package/dist/regenerate/lineage.js +18 -1
- package/dist/regenerate/loop.js +10 -1
- package/dist/regenerate/regenerate.js +4 -2
- package/dist/regenerate/regression.js +2 -0
- package/dist/regenerate/side-panel.d.ts +2 -0
- package/dist/regenerate/side-panel.js +4 -3
- package/dist/regenerate/test-scripts.js +12 -1
- package/dist/regenerate/unit-review.js +3 -0
- package/docs/config.md +3 -1
- package/docs/development.md +1 -0
- package/docs/hosting.md +28 -0
- package/docs/installation.md +1 -0
- package/docs/regenerating.md +1 -1
- package/docs/using.md +27 -1
- package/package.json +1 -1
package/dist/regenerate/loop.js
CHANGED
|
@@ -6,13 +6,14 @@ import { ACCEPTANCE_TESTS, FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, T
|
|
|
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
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;
|
|
@@ -12,6 +12,7 @@ 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
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";
|
|
@@ -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;
|
|
@@ -7,6 +7,7 @@ import { confinedTools } from "./confine.js";
|
|
|
7
7
|
import { projectRootOf } from "./paths.js";
|
|
8
8
|
import { buildRegressionFixPrompt, sectionModules } from "./prompt.js";
|
|
9
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";
|
|
@@ -161,6 +162,7 @@ export async function ensureRegressionPasses(input) {
|
|
|
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,
|
|
@@ -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,16 @@ 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
|
+
...(run.exitCode !== 0 ? { signature: signatureHash(failureSignature(run)) } : {}),
|
|
382
|
+
});
|
|
383
|
+
return run;
|
|
373
384
|
}
|
|
374
385
|
/** What bash says when a line of the script itself cannot run: a command or file that is not there, or not runnable. */
|
|
375
386
|
const OWN_LINE_FAILURE = /: (No such file or directory|command not found|Permission denied)$/;
|
|
@@ -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/docs/config.md
CHANGED
|
@@ -47,6 +47,7 @@ Each change says when it takes effect. None needs turri started again - what tur
|
|
|
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` |
|
|
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,7 @@ 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 |
|
|
94
96
|
| `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
97
|
| `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
98
|
| `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) |
|
|
@@ -229,7 +231,7 @@ How a stack's config is made:
|
|
|
229
231
|
- **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
232
|
- **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
233
|
- **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 `
|
|
234
|
+
- **The project's alone.** `module:`, `harness-model:`, `parallel-jobs:`, `rules-off:`, `compact-at:` and `ledger-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
235
|
|
|
234
236
|
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
237
|
|
package/docs/development.md
CHANGED
|
@@ -57,6 +57,7 @@ The workflow signs in to npm with npm's trusted publishing (OIDC), not a token:
|
|
|
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
59
|
| `src/regenerate/debug-log.ts` | each render agent's whole conversation, always streamed to `.turri/debug/` |
|
|
60
|
+
| `src/regenerate/ledger.ts`, `src/regenerate/ledger-view.ts`, `src/ledger-cli.ts` | the ledger: what turri did, as typed facts, in `.turri/ledger/` - `record()`, read back for `pulses`, `diagnose` and `turri ledger` - see [the ledger](using.md#the-ledger) |
|
|
60
61
|
| `genome/` | what turri knows and can do: `genome/skills/`, the skill library, and `genome/rules/`, the \*\*\*plain authoring rules |
|
|
61
62
|
|
|
62
63
|
## Known edges
|
package/docs/hosting.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
| `highlightLines`, `roleOf` | a spec's lines coloured by turri's own \*\*\*plain grammar. |
|
|
17
17
|
| `functionalityCompletion`, `createWorkLoop`, `createJobRegistry`, `promptOf`, `describeModules` | how far each functional spec is built, and - on a loop of the host's own with `dryRun` - the work a spec change would plan, as `turri glimpse` does. |
|
|
18
18
|
| `onModelEvent`, `describeTurnIncident` | a provider failing and waited out, a turn turri ended itself. |
|
|
19
|
+
| `onLedgerEvent`, `readLedger`, `ledgerDir`, `runViews`, `pulseView`, `describePulseView`, `describeRuns`, `ledgerProjects`, `earlierPulses`, `asJob` | what turri did, as facts - the [ledger](using.md#the-ledger): each event heard as it is recorded, and every run and pulse read back, earlier sessions' too - see [the ledger in a host](#the-ledger-in-a-host). |
|
|
19
20
|
| `sweepOrphanedGroups` | stop what a turri killed outright left running, before anything starts. |
|
|
20
21
|
|
|
21
22
|
It is turri's own code, as the terminal runs it: a 0.x release may change any of it.
|
|
@@ -64,6 +65,33 @@ the chat, and the toolbar's buttons send them too.
|
|
|
64
65
|
- The Run button releases what is built and opens it.`;
|
|
65
66
|
```
|
|
66
67
|
|
|
68
|
+
## The ledger in a host
|
|
69
|
+
|
|
70
|
+
The conversation's extension starts the ledger itself, in the folder the host made the session in - so a host that runs it there, the project's, needs nothing to have one: its pulses are recorded as `host` runs, and the conversation's `pulses` and `diagnose` already tell the model of earlier sessions' pulses. What the host can do with it is show it.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { onLedgerEvent, pulseView, readLedger, runViews } from "@turri-dev/cli/host";
|
|
74
|
+
|
|
75
|
+
// Each event as it is recorded - written or not, so a project with ledger-keep: 0 is heard too.
|
|
76
|
+
const stop = onLedgerEvent((entry, project) => {
|
|
77
|
+
if (entry.kind === "healer.failed") notify(`the ${entry.healer} failed: ${entry.error}`);
|
|
78
|
+
if (entry.kind === "provider.error" && !entry.transient) notify(`the model provider: ${entry.error}`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The project's runs, the most recent first, each with its pulses - how each ended, how long, what it cost.
|
|
82
|
+
const runs = runViews(projectDir);
|
|
83
|
+
|
|
84
|
+
// One pulse, from this session or an earlier one: its rounds, test runs, verdicts, healers, refusals, history.
|
|
85
|
+
const view = pulseView("646d", readLedger(projectDir, { job: "646d" }));
|
|
86
|
+
|
|
87
|
+
// Earlier sessions' pulses as jobs, to list beside this session's - none this session's registry knows.
|
|
88
|
+
const { jobs, more } = earlierPulses(ledgerProjects(projectDir), new Set(registry.list().map((job) => job.id)));
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- **Live.** `onLedgerEvent(listener)` is told every event as turri records it, with the project it is about, until what it returns is called; a listener that throws misses that event and nothing else. It is the pulse's own detail - its stages, test runs, verdicts, refusals - where `onJobChange` is the job as a whole.
|
|
92
|
+
- **Read back.** `readLedger(project, { job, run, kinds, since })` is the project's events, oldest first, the gzipped runs too; `runViews` and `pulseView` gather them into runs and pulses, and `describePulseView` and `describeRuns` say them in lines, as `turri ledger` prints them. `ledgerProjects(dir)` finds the ledgers of a folder and the projects under it. `earlierPulses(projects, known, runs = 3)` gives the last runs before this one as `Job`s - the shape a host already draws for this session's - each marked `earlier: { run, runMode, lines }`, `lines` what the ledger says of it; `more` is how many runs are beyond them. They are read only: never started, halted or waited for. turri's own side panel lists them so, as **earlier sessions**.
|
|
93
|
+
- **What it is for.** Looking up, never deciding: what is built and what is left come from the records and the work loop, as before. Its events and their fields are `LedgerEvent`'s - a kind is added, never changed, and every event carries `v: 1`.
|
|
94
|
+
|
|
67
95
|
## Running it
|
|
68
96
|
|
|
69
97
|
The host entry is compiled JavaScript, so any Node from 22.19 runs it - Electron's own included; nothing strips types. It finds turri's own folders - `genome/`, `stdlib/`, `.pi/` - beside `dist/` in the package, wherever npm installed it.
|
package/docs/installation.md
CHANGED
|
@@ -93,3 +93,4 @@ For Gemini, `GEMINI_API_KEY` is what makes pi *offer* the google provider, but w
|
|
|
93
93
|
| --- | --- |
|
|
94
94
|
| `PLAIN_TEMPLATE_DIRS` | colon-separated template directories, e.g. the standard library |
|
|
95
95
|
| `TURRI_COMMAND_TIMEOUT` | seconds a command may run before it is stopped - an agent's own, its tests (default `120`) |
|
|
96
|
+
| `TURRI_LEDGER` | `off`: write no [ledger](using.md#the-ledger) at all, whatever `ledger-keep:` says - the test suite sets it |
|
package/docs/regenerating.md
CHANGED
|
@@ -462,7 +462,7 @@ Two records per module, each committed with what it describes in the module's `.
|
|
|
462
462
|
| `code/.turri.json` | the code record: the spec the code is built from - its whole requires chain's: the definitions and implementation reqs of the modules below, each tagged with its module, their functional specs among the ones it inherits, and its own - without the test reqs and the acceptance tests, which are for conformance only |
|
|
463
463
|
| `tests/.turri.json` | the tests record: the functional specs the conformance tests are written for - of the code record's, with their acceptance tests - the definitions and implementation reqs they were written against, and the test reqs they follow |
|
|
464
464
|
|
|
465
|
-
Nothing about how a build went: whether tests passed and which files a job touched are in the history. And nothing else is stored: what is left to build is worked out from the records whenever a job ends or the spec changes - the code work from the code record to the spec now (`src/regenerate/drift.ts`), the conformance work from the tests record to what the tests are to be written for. Only the running job is kept; what the panel shows as planned is worked out again. The tests record never goes ahead of the code's: a conformance test exercises functionality, and only what is in the code can be tested.
|
|
465
|
+
Nothing about how a build went: whether tests passed and which files a job touched are in the history, and how each pulse went is in the [ledger](using.md#the-ledger), which nothing reads to decide what to build. And nothing else is stored: what is left to build is worked out from the records whenever a job ends or the spec changes - the code work from the code record to the spec now (`src/regenerate/drift.ts`), the conformance work from the tests record to what the tests are to be written for. Only the running job is kept; what the panel shows as planned is worked out again. The tests record never goes ahead of the code's: a conformance test exercises functionality, and only what is in the code can be tested.
|
|
466
466
|
|
|
467
467
|
What goes into them:
|
|
468
468
|
|
package/docs/using.md
CHANGED
|
@@ -80,7 +80,7 @@ A render takes minutes, so in the TUI every render tool starts the work as a pul
|
|
|
80
80
|
|
|
81
81
|
One module is shown at a time, with a tab for each module of the chain on the line under the top line, in the chain's order - the module it is built on to the left: `d365_version_api [d365_http_client] d365_utils 12 ›`. A tab regenerating spins, one stuck says `!`; what does not fit is counted at either end. `←` `→` go from one tab to the next. The module shown is the one in focus - the one regenerating now, else the one asked for, else the first - until you pick another tab; it goes back to following once another module is in focus, or the one in focus starts regenerating. Under the module's own text is what it imports, under **imported**, a folded block a file, `▸ from stdlib/backend/python-fastapi · 7 reqs · all built`, closed until opened. The modules it requires are tabs of their own: a chain of many modules is never drawn nested, one in the next. A map longer than the screen has a minimap down its right edge, a cell a slice in the colour of its most pressing mark, the slice shown solid. The map is read from the files - the code record, the `.plain` files, the tests record - so it is right the moment turri opens a project, before anything runs; the work loop's plan adds the pulses on top. It maps the files asked to build this conversation, else `module:` in `config.turri.yaml`, else the `.plain` files of the folder that no other one there imports or requires - the folder's files looked at again only once one of them, or its config, changes. `↑` `↓` move row by row, `enter` or `space` open and close an item or an import block - a functional spec opens to its sub-bullets and its `***acceptance tests***`, nested under it as the spec has them - `←` `→` go to the next module's tab, `o` and `c` open or close everything shown, `pgup` `pgdn` and the wheel scroll, `g`/`G` go to the top and the bottom.
|
|
82
82
|
|
|
83
|
-
`p` goes to the **pulses** - and back - onto the pulse that regenerates the item under the cursor, and back onto the first item the pulse under the cursor regenerates. The pulses are every pulse as three sections - running, queued, finished - each opened and closed on its own (running and queued start open). A pulse is one line: its module, kind and number (`hubspot · regenerate pulse 1`, `hubspot · conformance pulse 2`) - so it is named wherever a pulse is named in a line (the job panel, the `pulses` tool, `/halt`, the command line), never by what it regenerates, which is in it, opened - its time (none while it is queued: it has not started) and its cost - a finished one's mark green when it passed, red when it failed or did not pass, amber when it was stopped. Opened, a running one shows what it is doing; a queued one, what it waits for; a finished one, its agents and what each spent, and whether it did not pass. Under it is a row for each file its spec diff changes - the file of the module being rendered first and open, the rest by name and closed - each opening onto that file's lines. A file with nothing added or taken out is not listed. `↑` `↓` move, `enter` or `space` open and close what is under the cursor, `→` `←` open and close (`←` on a closed pulse goes to its section, on a closed file to its pulse), `o` and `c` open or close every pulse of the section - or, on a file, every file of its pulse - `pgup` `pgdn` and the wheel scroll, `g`/`G` go to the top and the bottom. The cursor stays on its row when the pulse under it moves on; a pulse opened closes when it moves to another section. What is open is kept while the panel is closed. What the agents said and did is not shown - it is in each pulse's log (see [Agent logs](#agent-logs)).
|
|
83
|
+
`p` goes to the **pulses** - and back - onto the pulse that regenerates the item under the cursor, and back onto the first item the pulse under the cursor regenerates. The pulses are every pulse as three sections - running, queued, finished - each opened and closed on its own (running and queued start open) - and a fourth, **earlier sessions**, closed, when the [ledger](#the-ledger) has pulses of sessions before this one: its last three runs', each pulse read only - opened, what the ledger says of it (how it ended, its rounds and test runs, the healers, what turri refused and committed, its last stages and its log) - and a line for how many more runs it has. A pulse is one line: its module, kind and number (`hubspot · regenerate pulse 1`, `hubspot · conformance pulse 2`) - so it is named wherever a pulse is named in a line (the job panel, the `pulses` tool, `/halt`, the command line), never by what it regenerates, which is in it, opened - its time (none while it is queued: it has not started) and its cost - a finished one's mark green when it passed, red when it failed or did not pass, amber when it was stopped. Opened, a running one shows what it is doing; a queued one, what it waits for; a finished one, its agents and what each spent, and whether it did not pass. Under it is a row for each file its spec diff changes - the file of the module being rendered first and open, the rest by name and closed - each opening onto that file's lines. A file with nothing added or taken out is not listed. `↑` `↓` move, `enter` or `space` open and close what is under the cursor, `→` `←` open and close (`←` on a closed pulse goes to its section, on a closed file to its pulse), `o` and `c` open or close every pulse of the section - or, on a file, every file of its pulse - `pgup` `pgdn` and the wheel scroll, `g`/`G` go to the top and the bottom. The cursor stays on its row when the pulse under it moves on; a pulse opened closes when it moves to another section. What is open is kept while the panel is closed. What the agents said and did is not shown - it is in each pulse's log (see [Agent logs](#agent-logs)).
|
|
84
84
|
|
|
85
85
|
`ctrl+h` again, or `esc`, closes it and goes back to the chat. Beside the chat, one side has the keys and the other is drawn faint: `ctrl+h` switches sides - from the chat to the panel, from the panel to the chat, the panel staying and following the pulses - `esc` gives the chat the keys too, and a click gives them to the side clicked. `ctrl+c` closes the panel; so do `s`, over the whole screen, then `esc`. (pi's regular screen, `--tui-mode regular`, reads no mouse: there it is the keys alone.) While the panel has the keys, every key is the panel's, whatever else happens: a question the chat model asks in the background (`ask_user`) waits in the chat, unanswered - an `enter` in the panel never answers it - and the panel's top line says "the chat is waiting for your answer". Back in the chat, the question is there, with the keys.
|
|
86
86
|
- `turri` runs pi in fullscreen mode, so the conversation stays where you scroll it while the pulses redraw (PgUp/PgDn, the wheel); `--tui-mode regular` goes back to the terminal's own scrollback.
|
|
@@ -170,6 +170,31 @@ Beside them, each pulse has a log of its own in `.turri/jobs/` - `<time>-<pulse
|
|
|
170
170
|
|
|
171
171
|
No agent can read the logs: they are outside `plain_modules` - or, for a `--build-dir` outside it, beside that folder rather than in it. Nothing prunes them either - delete the folder when you are done with it. A log that cannot be written is given up on without failing the render.
|
|
172
172
|
|
|
173
|
+
## The ledger
|
|
174
|
+
|
|
175
|
+
The logs keep everything a pulse's agents said, for reading one pulse closely. The ledger keeps what turri did, as facts, across pulses, sessions and runs - to answer *why did last night's run fail*, *how often does the debugger fail*, *what did this module cost*, long after the session that ran it is gone (`src/regenerate/ledger.ts`).
|
|
176
|
+
|
|
177
|
+
It is in `.turri/ledger/` in the project, a JSON line an event, one file a run - a conversation, a headless run, a host's session: `<time>-<run>-p<pid>.jsonl`. Each line has the time with its date, its order in the run, the run, and - where they apply - the pulse, the module and the agent. What it records:
|
|
178
|
+
|
|
179
|
+
| | |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| runs | `run.started` (how it was started, turri's version and commit, the machine), `run.ended` |
|
|
182
|
+
| pulses | `job.queued`, `job.started`, every `job.stage`, `job.retried`, `job.finished` - its status, whether it landed, how long it ran, what it cost, how it ended |
|
|
183
|
+
| the work loop | `loop.halted`, `loop.stopped` (a pulse the spec moved on from), `planner.failed`, `planner.fellBack` |
|
|
184
|
+
| the model | `model.call` (time and tokens), `model.turn`, `model.compaction`, `provider.retry`, `provider.stall`, `provider.error`, `turn.stopped` (a turn turri ended itself, and why) |
|
|
185
|
+
| tools and tests | `tool.call` (the tool, how long, whether it failed - never its arguments or result), `script.run` (the testing script, its exit, how long, whether the time limit stopped it, the failure's fingerprint) |
|
|
186
|
+
| the gates | `gate.round`, `review.verdict`, `arbiter.ruling`, `judge.verdict` |
|
|
187
|
+
| what turri refused | `guard.refused` (a repeated call, a call past its budget, a tool the turn does not have), `command.refused`, `audit.putBack` |
|
|
188
|
+
| the healers | `healer.started`, `healer.ended` with what it found, `healer.failed` and why - the debugger, the diagnosis of a failed pulse |
|
|
189
|
+
| the history | `history.committed`, `history.failed`, `history.reset`, `history.rolledBack` |
|
|
190
|
+
| the environment | `environment.error` |
|
|
191
|
+
|
|
192
|
+
Never in it: prompts, replies, thinking, tool arguments and results, file contents. What an event does carry - a stage, an error's first line - is masked as the logs are.
|
|
193
|
+
|
|
194
|
+
`turri ledger` prints the last runs, a line a pulse; `turri ledger <pulse>` one pulse - its rounds, its test runs and how many failed the same way, the verdicts, the healers, what turri refused, what its history recorded, its log; `--json` the events themselves, for `jq`. In the chat, `pulses` lists the last three earlier runs under this session's pulses, and `pulses` and `diagnose` tell one of them by its id; the side panel's pulses list has them as **earlier sessions** (see [the side panel](#in-a-conversation)).
|
|
195
|
+
|
|
196
|
+
It is for looking up, never for deciding: what to build comes from the records alone (see [what the code is built from](regenerating.md#what-the-code-is-built-from)), so deleting `.turri/ledger/` loses nothing but what can be looked up. When a run first writes to the project, the files of runs that have ended are gzipped and the oldest pruned past `ledger-keep:` megabytes - 100 unset, `0` for none at all; a run still going is never touched. `.turri` ignores itself, so the ledger is never committed, and nothing of it is sent anywhere. A ledger that cannot be written is said once and given up on, never failing the work.
|
|
197
|
+
|
|
173
198
|
## From the command line
|
|
174
199
|
|
|
175
200
|
The same work, without a conversation. Run it from the directory that holds the spec, so its linked resources resolve the way the parser expects. For a long run - a whole spec overnight - see [running unattended](headless.md).
|
|
@@ -185,6 +210,7 @@ turri regenerate <file.plain> --from-scratch-chain # the same for every module
|
|
|
185
210
|
turri spec-diff <file.plain> <diff> # apply a spec diff, and regenerate it
|
|
186
211
|
turri conform <file.plain> 1 # black-box tests for functionality 1
|
|
187
212
|
turri lineage <module> # what each regeneration changed in a module, and when
|
|
213
|
+
turri ledger [pulse] # what turri did here: the last runs and their pulses, or one pulse
|
|
188
214
|
turri cultivate brood # run a skill directly - by name, the project's too, or by its folder
|
|
189
215
|
turri release <file.plain> # release what is regenerated to dist/<module>, and activate it here
|
|
190
216
|
```
|