@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.
Files changed (46) hide show
  1. package/dist/cli.js +2 -1
  2. package/dist/config-edit.d.ts +1 -1
  3. package/dist/config-edit.js +2 -0
  4. package/dist/config.d.ts +6 -0
  5. package/dist/config.js +5 -1
  6. package/dist/extension.js +69 -8
  7. package/dist/host.d.ts +2 -0
  8. package/dist/host.js +3 -0
  9. package/dist/init-config.d.ts +1 -0
  10. package/dist/init-config.js +5 -0
  11. package/dist/ledger-cli.d.ts +1 -0
  12. package/dist/ledger-cli.js +50 -0
  13. package/dist/loop-cli.js +4 -0
  14. package/dist/pi.js +17 -0
  15. package/dist/regenerate/activity.d.ts +9 -0
  16. package/dist/regenerate/activity.js +11 -0
  17. package/dist/regenerate/agent-shell.js +5 -0
  18. package/dist/regenerate/chunk-plan.d.ts +0 -6
  19. package/dist/regenerate/chunk-plan.js +16 -4
  20. package/dist/regenerate/conformance.js +3 -0
  21. package/dist/regenerate/diagnose.js +4 -2
  22. package/dist/regenerate/job-log.d.ts +3 -1
  23. package/dist/regenerate/job-log.js +8 -5
  24. package/dist/regenerate/jobs-tree.d.ts +7 -3
  25. package/dist/regenerate/jobs-tree.js +21 -6
  26. package/dist/regenerate/jobs.d.ts +9 -0
  27. package/dist/regenerate/jobs.js +38 -3
  28. package/dist/regenerate/ledger-view.d.ts +73 -0
  29. package/dist/regenerate/ledger-view.js +286 -0
  30. package/dist/regenerate/ledger.d.ts +274 -0
  31. package/dist/regenerate/ledger.js +349 -0
  32. package/dist/regenerate/lineage.js +18 -1
  33. package/dist/regenerate/loop.js +10 -1
  34. package/dist/regenerate/regenerate.js +4 -2
  35. package/dist/regenerate/regression.js +2 -0
  36. package/dist/regenerate/side-panel.d.ts +2 -0
  37. package/dist/regenerate/side-panel.js +4 -3
  38. package/dist/regenerate/test-scripts.js +12 -1
  39. package/dist/regenerate/unit-review.js +3 -0
  40. package/docs/config.md +3 -1
  41. package/docs/development.md +1 -0
  42. package/docs/hosting.md +28 -0
  43. package/docs/installation.md +1 -0
  44. package/docs/regenerating.md +1 -1
  45. package/docs/using.md +27 -1
  46. package/package.json +1 -1
@@ -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
- }).catch(() => undefined);
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
- const tree_ = jobsTreeLines(options.jobs(), tree, width, style);
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.status === "running" ? "running" : job.status === "queued" ? "queued" : "finished";
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
- return {
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 `compact-at:` 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.
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
 
@@ -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.
@@ -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 |
@@ -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
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turri-dev/cli",
3
- "version": "0.1.8-next.01c7342",
3
+ "version": "0.1.8-next.b03162b",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {