@turri-dev/cli 0.1.8-next.e553476 → 0.1.8

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 (66) hide show
  1. package/README.md +1 -3
  2. package/dist/cli.js +6 -2
  3. package/dist/config-edit.d.ts +1 -1
  4. package/dist/config-edit.js +4 -0
  5. package/dist/config.d.ts +27 -0
  6. package/dist/config.js +28 -1
  7. package/dist/extension.js +75 -9
  8. package/dist/genome.js +2 -0
  9. package/dist/host.d.ts +3 -1
  10. package/dist/host.js +4 -1
  11. package/dist/init-config.d.ts +3 -0
  12. package/dist/init-config.js +15 -0
  13. package/dist/ledger-cli.d.ts +1 -0
  14. package/dist/ledger-cli.js +50 -0
  15. package/dist/ledger-tools.d.ts +10 -0
  16. package/dist/ledger-tools.js +64 -0
  17. package/dist/loop-cli.js +4 -0
  18. package/dist/pi.d.ts +2 -0
  19. package/dist/pi.js +24 -4
  20. package/dist/regenerate/activity.d.ts +9 -0
  21. package/dist/regenerate/activity.js +11 -0
  22. package/dist/regenerate/agent-env.d.ts +7 -0
  23. package/dist/regenerate/agent-env.js +126 -5
  24. package/dist/regenerate/agent-shell.js +5 -0
  25. package/dist/regenerate/chunk-plan.d.ts +5 -6
  26. package/dist/regenerate/chunk-plan.js +43 -10
  27. package/dist/regenerate/conformance-agents.js +2 -2
  28. package/dist/regenerate/conformance.js +6 -3
  29. package/dist/regenerate/debug-log.d.ts +3 -1
  30. package/dist/regenerate/debug-log.js +10 -2
  31. package/dist/regenerate/diagnose.js +12 -2
  32. package/dist/regenerate/environment-error.d.ts +6 -0
  33. package/dist/regenerate/environment-error.js +19 -1
  34. package/dist/regenerate/job-log.d.ts +3 -1
  35. package/dist/regenerate/job-log.js +8 -5
  36. package/dist/regenerate/jobs-tree.d.ts +7 -3
  37. package/dist/regenerate/jobs-tree.js +21 -6
  38. package/dist/regenerate/jobs.d.ts +9 -0
  39. package/dist/regenerate/jobs.js +51 -5
  40. package/dist/regenerate/ledger-view.d.ts +92 -0
  41. package/dist/regenerate/ledger-view.js +345 -0
  42. package/dist/regenerate/ledger.d.ts +275 -0
  43. package/dist/regenerate/ledger.js +349 -0
  44. package/dist/regenerate/lineage.d.ts +9 -1
  45. package/dist/regenerate/lineage.js +64 -12
  46. package/dist/regenerate/loop.js +12 -3
  47. package/dist/regenerate/pulse-files.d.ts +14 -0
  48. package/dist/regenerate/pulse-files.js +63 -0
  49. package/dist/regenerate/regenerate.js +7 -5
  50. package/dist/regenerate/regression.js +4 -2
  51. package/dist/regenerate/services.js +3 -0
  52. package/dist/regenerate/side-panel.d.ts +2 -0
  53. package/dist/regenerate/side-panel.js +4 -3
  54. package/dist/regenerate/test-scripts.js +18 -5
  55. package/dist/regenerate/testing.js +3 -0
  56. package/dist/regenerate/unit-review.js +3 -0
  57. package/dist/run-report.js +9 -5
  58. package/docs/config.md +23 -2
  59. package/docs/development.md +3 -1
  60. package/docs/headless.md +5 -4
  61. package/docs/hosting.md +29 -0
  62. package/docs/installation.md +1 -0
  63. package/docs/regenerating.md +7 -7
  64. package/docs/sandbox.md +5 -5
  65. package/docs/using.md +36 -5
  66. package/package.json +1 -1
@@ -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, recordHistory, startWork } from "./lineage.js";
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
- recordHistory(tip.buildDir, `spec restructured: ${summarizeDiff(options.diff)}`, options.diff);
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
- }).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;
@@ -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
- recordHistory(buildDir, `spec diff: ${summarizeDiff(options.diff)}`, options.diff);
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, markWork, recordHistory } from "./lineage.js";
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
- recordHistory(input.buildDir, `regression ${input.moduleName} (pass after ${repairs.length} repair(s))`);
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
- 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,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`);
@@ -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
- logs: {
294
- jobs_dir: fromProject(join(projectRoot, ".turri", "jobs")),
295
- debug_dir: fromProject(join(projectRoot, ".turri", "debug")),
296
- sessions_dir: fromProject(join(projectRoot, ".turri", "sessions")),
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 `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.
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
 
@@ -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, always streamed to `.turri/debug/` |
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
package/docs/headless.md CHANGED
@@ -84,8 +84,9 @@ The command exits `0` only when every module is complete: its code regenerated f
84
84
  | `plain_modules/<module>/code/` | the code and unit tests |
85
85
  | `plain_modules/<module>/tests/` | the conformance tests |
86
86
  | `plain_modules/<module>/code/.turri.json` | the marshalled plain tree the code is built from |
87
- | `.turri/jobs/` | each pulse's log: its agents, its test runs, how it ended |
88
- | `.turri/debug/` | each agent's whole conversation |
87
+ | `.turri/ledger/` | what each pulse did, how it ended and what it cost - `turri ledger` |
88
+ | `.turri/jobs/` | each pulse's log: its agents, its test runs, how it ended - with `--debug` (or `debug: true`) only |
89
+ | `.turri/debug/` | each agent's whole conversation - with `--debug` only |
89
90
 
90
91
  And what happened, step by step, in the module's history:
91
92
 
@@ -142,7 +143,7 @@ A module that `requires` others is built in its own codebase on top of theirs: t
142
143
  "status": "succeeded", "landed": true, "failed_for_good": false,
143
144
  "started_at": "…", "duration_seconds": 512.3, "usage": { },
144
145
  "agents": [{ "label": "app 1 coding agent", "model": "…", "turns": 14, "compactions": 0, "input_tokens": 0, "…": 0 }],
145
- "log_file": ".turri/jobs/….log", "error": null
146
+ "log_file": ".turri/jobs/….log", "error": null // log_file null outside debug mode
146
147
  }]
147
148
  },
148
149
  "functionalities": {
@@ -164,7 +165,7 @@ A module that `requires` others is built in its own codebase on top of theirs: t
164
165
  { "kind": "stopped", "pulse_id": "5c1e0b2f", "role": "conformance explorer", "model": "…", "reason": "10 submit_exploration calls failed in a row" }
165
166
  ],
166
167
  "build_dir_export": { "exported": true, "dir": "/work/out", "module": "app", "commit": "8036b3b", "files": 23 },
167
- "logs": { "jobs_dir": ".turri/jobs", "debug_dir": ".turri/debug", "sessions_dir": ".turri/sessions" }
168
+ "logs": { "jobs_dir": ".turri/jobs", "debug_dir": ".turri/debug", "sessions_dir": ".turri/sessions" } // null outside debug mode
168
169
  }
169
170
  ```
170
171
 
package/docs/hosting.md CHANGED
@@ -11,11 +11,13 @@
11
11
  | `createTurriExtension(host)` | turri's conversation as an extension of pi's: every tool, slash command, rule and skill the terminal has. The host is told of its jobs and asks its questions - see below. |
12
12
  | `pi` | pi itself, as turri's extension is built against it. Make the session with this one - `pi.createAgentSession`, `pi.DefaultResourceLoader` - not another copy. |
13
13
  | `readProjectConfig`, `projectModels`, `modelSpec`, `configForSpec`, `hasProjectConfig`, `stacksOf` | the project's settings, as [config.turri.yaml](config.md) has them. |
14
+ | `debugMode` | whether turri keeps, in a project, what it writes only for debugging it - `debug:`, or `TURRI_DEBUG` over it (see [debug mode](config.md#debug-mode)). |
14
15
  | `initConfig(dir, { vendor, bedrock, force })`, `VENDORS`, `BEDROCK_REGIONS`, `BEDROCK_DEFAULT`, `bedrockModels` | a project's first config.turri.yaml, as `turri init-config` writes it: the host asks which vendor's models - `VENDORS` has each one's label and its two models, `BEDROCK_REGIONS` where Bedrock serves each maker's - and `initConfig` writes it, saying what it did. One already there keeps everything but its two models. |
15
16
  | `checkPlainFile`, `plainFileParser`, `resolveTemplateDirs`, `iterateFunctionalSpecs`, `FUNCTIONAL_REQUIREMENTS` | a spec checked and parsed, its functional specs one by one - as `turri validate` reads it. |
16
17
  | `highlightLines`, `roleOf` | a spec's lines coloured by turri's own \*\*\*plain grammar. |
17
18
  | `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
19
  | `onModelEvent`, `describeTurnIncident` | a provider failing and waited out, a turn turri ended itself. |
20
+ | `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
21
  | `sweepOrphanedGroups` | stop what a turri killed outright left running, before anything starts. |
20
22
 
21
23
  It is turri's own code, as the terminal runs it: a 0.x release may change any of it.
@@ -64,6 +66,33 @@ the chat, and the toolbar's buttons send them too.
64
66
  - The Run button releases what is built and opens it.`;
65
67
  ```
66
68
 
69
+ ## The ledger in a host
70
+
71
+ 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.
72
+
73
+ ```js
74
+ import { onLedgerEvent, pulseView, readLedger, runViews } from "@turri-dev/cli/host";
75
+
76
+ // Each event as it is recorded - written or not, so a project with ledger-keep: 0 is heard too.
77
+ const stop = onLedgerEvent((entry, project) => {
78
+ if (entry.kind === "healer.failed") notify(`the ${entry.healer} failed: ${entry.error}`);
79
+ if (entry.kind === "provider.error" && !entry.transient) notify(`the model provider: ${entry.error}`);
80
+ });
81
+
82
+ // The project's runs, the most recent first, each with its pulses - how each ended, how long, what it cost.
83
+ const runs = runViews(projectDir);
84
+
85
+ // One pulse, from this session or an earlier one: its rounds, test runs, verdicts, healers, refusals, history.
86
+ const view = pulseView("646d", readLedger(projectDir, { job: "646d" }));
87
+
88
+ // Earlier sessions' pulses as jobs, to list beside this session's - none this session's registry knows.
89
+ const { jobs, more } = earlierPulses(ledgerProjects(projectDir), new Set(registry.list().map((job) => job.id)));
90
+ ```
91
+
92
+ - **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.
93
+ - **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**.
94
+ - **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`.
95
+
67
96
  ## Running it
68
97
 
69
98
  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 |
@@ -33,12 +33,12 @@ The harness's scratch - each ignores itself, and is never part of a module's his
33
33
  | | |
34
34
  | --- | --- |
35
35
  | `plain_modules/.turri/` | what turri keeps beside the code - see [sandbox](sandbox.md#what-turri-keeps-beside-the-code) |
36
- | `plain_modules/.turri/cache/` | the package caches, so a dependency is fetched once |
36
+ | `plain_modules/.turri/cache/` | the package caches, so a dependency is fetched once - held to `cache-keep:` |
37
37
  | `plain_modules/.turri/scripts/` | the copies each testing script run happens in, one folder a run |
38
- | `plain_modules/.turri/logs/` | the whole output of each run or command an agent was shown cut short, by time and what ran - read by agents only through `read_output` |
39
- | `.turri/jobs/` | each job's log: its diff, its stages, its agents and what they cost, and which walls were up under its agents' commands |
40
- | `.turri/debug/` | each agent's log: its prompt, every tool call and every answer |
41
- | `.turri/sessions/` | the agents' saved sessions |
38
+ | `plain_modules/.turri/logs/` | the whole output of each run or command an agent was shown cut short, by time and what ran - read by agents only through `read_output`; removed as the pulse ends, outside [debug mode](config.md#debug-mode) |
39
+ | `.turri/jobs/` | each job's log: its diff, its stages, its agents and what they cost, and which walls were up under its agents' commands - kept in debug mode only |
40
+ | `.turri/debug/` | each agent's log: its prompt, every tool call and every answer - kept in debug mode only |
41
+ | `.turri/sessions/` | the agents' saved sessions - in debug mode, and a gated skill's run's |
42
42
  | `.turri/runs/<skill>/` | a gated skill's run: what an interrupted one resumes from, and its gates' evidence |
43
43
  | `.turri/deploy/` | the output of what `deploy` started |
44
44
  | `dist/<module>/` | what `deploy` puts out: the module's code as its last commit has it, replaced whole each time - see [deploy](deploy.md) |
@@ -411,7 +411,7 @@ The agents that work through a job keep **one session for it**: the coding agent
411
411
 
412
412
  An agent kept through a job re-reads everything it has done on every call. So between turns, once its conversation - what it has done, past its instructions - passes `compact-at` tokens (100,000 unless [the project config](config.md) says otherwise), the harness compacts it, with instructions for its role: the coding agent and the fixer keep what is done, what still fails and why, the decisions and every review problem's fate; a reviewer keeps every problem it raised and where each stands, and what it has already checked. pi's own compaction, when the context window is nearly full, stays on beneath it.
413
413
 
414
- What each agent of a job did - its turns, its compactions, its tokens and cost - is at the end of the job's log in `.turri/jobs/`, and in `pulses`.
414
+ What each agent of a job did - its turns, its compactions, its tokens and cost - is at the end of the job's log in `.turri/jobs/` (kept in [debug mode](config.md#debug-mode)), and in `pulses`.
415
415
 
416
416
  ## Working notes
417
417
 
@@ -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/sandbox.md CHANGED
@@ -107,12 +107,12 @@ It lives in `plain_modules/.turri`, which ignores itself in git:
107
107
 
108
108
  | | |
109
109
  | --- | --- |
110
- | `cache/` | package caches - pip, npm, cargo, Maven, Gradle, Go - so a dependency downloads once per project |
111
- | `home/<module>` | each module's home folder for its agents' commands, kept between pulses |
112
- | `tmp/<module>` | each module's `TMPDIR` |
110
+ | `cache/` | package caches - pip, npm, cargo, Maven, Gradle, Go, node-gyp's headers, the browsers Playwright, Puppeteer and Cypress download - so a dependency downloads once per project; held to `cache-keep:` megabytes, the least recently written going first |
111
+ | `home/<module>` | each module's home folder for its agents' commands, kept between pulses; the caches its tools keep there count towards `cache-keep:` |
112
+ | `tmp/<module>` | each module's `TMPDIR`, emptied as each of its pulses ends outside [debug mode](config.md#debug-mode) |
113
113
  | `scripts/` | the throwaway copies test runs happen in, emptied after each run |
114
- | `logs/` | whole outputs cut short for an agent, read through `read_output` |
115
- | `services/` | what the services agents started printed |
114
+ | `logs/` | whole outputs cut short for an agent, read through `read_output` - removed as the pulse ends, outside debug mode |
115
+ | `services/` | what the services agents started printed - removed as the pulse ends, outside debug mode |
116
116
 
117
117
  ## What the machine lacks
118
118
 
package/docs/using.md CHANGED
@@ -36,7 +36,8 @@ While a skill runs, a line above the input shows its phases, every one by name -
36
36
  | `engine` | show or choose the model the code is regenerated on - see [below](#choosing-a-model) |
37
37
  | `pulses` | the pulses - running, planned and finished - or one of them |
38
38
  | `halt` | halt a pulse, a module's planned pulses, or what `activate` started |
39
- | `diagnose` | why a pulse did not land, from its transcript: how it ended, the stages that went wrong, the last failing test run, the last review, the tool calls that failed |
39
+ | `diagnose` | why a pulse did not land, from its transcript: how it ended, the stages that went wrong, the last failing test run, the last review, the tool calls that failed - and what the [ledger](#the-ledger) recorded of it |
40
+ | `ledger` | what turri did, from the [ledger](#the-ledger), this session's and every earlier one's: a pulse as facts, by its id - its rounds, test runs, verdicts, healers, refusals and commits; events of some kinds counted and listed, for a module or since a time - `healer.failed`, or `healer` for the group; or the last runs, a line a pulse |
40
41
  | `lineage` | a module's lineage: what each regeneration and conformance run changed in it, and what it was regenerated from |
41
42
  | `cultivate` | run a gated skill, its steps and review gates enforced |
42
43
  | `release`, `activate` | try what is regenerated: release it to `dist/<module>` and activate it on this machine, installing what it needs - see [deploy](deploy.md) |
@@ -80,7 +81,7 @@ A render takes minutes, so in the TUI every render tool starts the work as a pul
80
81
 
81
82
  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
83
 
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)).
84
+ `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
85
 
85
86
  `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
87
  - `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.
@@ -151,7 +152,9 @@ A provider failure - no credits, bad key, rate limit - is raised with the provid
151
152
 
152
153
  Each `plain_modules/<module>` is a git repository whose git directory is `.plaingit` rather than `.git`, so it stays plain files to your own repository. A commit is made after every step that changes the module - a render, a functionality regenerated, a conformance run, a regression repair, a spec change. `/lineage [module]` and `lineage` show it.
153
154
 
154
- The last commit is also the last known good state. Every render, functionality regenerated, spec change and conformance run starts by putting the codebase it works in back to it, so if turri was stopped or restarted mid-pulse, whatever that pulse left half-written is undone first - and one that does not pass is undone the same way when it ends, commits and all. Only work that passed is committed. Installed dependencies and caches are not in the history and stay. With git directly:
155
+ The last commit is also the last known good state. Every render, functionality regenerated, spec change and conformance run starts by putting the codebase it works in back to it, so if turri was stopped or restarted mid-pulse, whatever that pulse left half-written is undone first - and one that does not pass is undone the same way when it ends, commits and all. Only work that passed is committed. Installed dependencies and caches are not in the history and stay.
156
+
157
+ The history is turri's own: the hooks your git config names (`core.hooksPath` - gitleaks, husky) do not run on its commits. When work that passed cannot be committed all the same - a `.plaingit/index.lock` a git stopped mid-commit left, a full disk - the pulse does not land: the next start would undo it while turri took it as built. It is rolled back, and the stop says what git said and how to fix it. Any other step's commit failing leaves only a gap in the history. With git directly:
155
158
 
156
159
  ```bash
157
160
  git --git-dir=plain_modules/<module>/.plaingit --work-tree=plain_modules/<module> log
@@ -159,14 +162,41 @@ git --git-dir=plain_modules/<module>/.plaingit --work-tree=plain_modules/<module
159
162
 
160
163
  ## Agent logs
161
164
 
162
- When an agent does something you cannot explain, its logs are where to look: the chat shows what its pulse is doing, not what its agents said. Every agent a render starts - the coding agent, the conformance agent, the repair agent, the spec-steps planner - also streams its whole conversation to a file of its own, always.
165
+ When an agent does something you cannot explain, its logs are where to look: the chat shows what its pulse is doing, not what its agents said. Every agent a render starts - the coding agent, the conformance agent, the repair agent, the spec-steps planner - also streams its whole conversation to a file of its own.
163
166
 
164
167
  The logs are in `.turri/debug/` in the project, beside `plain_modules`, one per agent, named by when it started and what it is: `2026-09-26T13-45-02-123Z-app-3-coding-agent.log`. Each starts with the pulse it runs for, its model and thinking level, its folder, its saved pi session, its tools and its system prompt, and then has, in order and stamped with the time: every prompt it is sent, whole; its thinking and replies as they stream; every tool call with its arguments, numbered
165
168
  - `→ #3 bash` - and its whole result under the same number, `← #3 bash done`, since calls made together run at once and finish in any order; a tool call a response was cut off in, marked as never run; each response's stop reason, tokens and cost; the provider's retries and errors; and how each turn ended. Every line is on disk as it happens, so `tail -f` follows an agent live and a run that crashed leaves its log up to that moment. The pulse's own log names each agent's log as it starts.
166
169
 
167
170
  Beside them, each pulse has a log of its own in `.turri/jobs/` - `<time>-<pulse id>-<kind>.log`: its whole transcript, written as it happens. The harness's stages, the functional spec or the diff it builds, and every one of its agents in turn - each prompt, its thinking and replies, every tool call and its result, whole where the screen clips them - and how the pulse ended. A finished pulse, opened in the side panel, names it. It is the one to read for a pulse as a whole; an agent's own log has its system prompt, tokens and retries besides.
168
171
 
169
- 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
+ They are kept only in [debug mode](config.md#debug-mode) - `debug: true` in `config.turri.yaml`, or `TURRI_DEBUG=1`, or `turri --debug` for one run. Otherwise a pulse's logs are written while it runs - the diagnosis of a pulse that fails twice reads them, and quotes what it needs, never sending you to a file that is about to go - and removed as it ends, and an agent outside a pulse writes none; the [ledger](#the-ledger) still says what each pulse did, how it ended and what it cost. To read why an agent did something, turn debug mode on and run it again.
173
+
174
+ The coding and conformance agents cannot read the logs: they are outside `plain_modules` - or, for a `--build-dir` outside it, beside that folder rather than in it. The diagnosis of a failed pulse reads them. In debug mode nothing prunes them - delete the folder when you are done with it. A log that cannot be written is given up on without failing the render.
175
+
176
+ ## The ledger
177
+
178
+ 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`).
179
+
180
+ 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:
181
+
182
+ | | |
183
+ | --- | --- |
184
+ | runs | `run.started` (how it was started, turri's version and commit, the machine), `run.ended` |
185
+ | 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 |
186
+ | the work loop | `loop.halted`, `loop.stopped` (a pulse the spec moved on from), `planner.failed`, `planner.fellBack` |
187
+ | 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) |
188
+ | 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, and `mutated` for a `can_it_fail` run on code broken on purpose) |
189
+ | the gates | `gate.round`, `review.verdict`, `arbiter.ruling`, `judge.verdict` |
190
+ | what turri refused | `guard.refused` (a repeated call, a call past its budget, a tool the turn does not have), `command.refused`, `audit.putBack` |
191
+ | the healers | `healer.started`, `healer.ended` with what it found, `healer.failed` and why - the debugger, the diagnosis of a failed pulse |
192
+ | the history | `history.committed`, `history.failed`, `history.reset`, `history.rolledBack` |
193
+ | the environment | `environment.error` |
194
+
195
+ 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.
196
+
197
+ `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, `ledger` is the way in - a pulse of this session or an earlier one, the events of some kinds counted (how often the debugger failed this week: `kinds: [healer.failed], since: 7d`), the last runs - and `pulses` lists the last three earlier runs under this session's pulses, `pulses` and `diagnose` tell one of them by its id, and `diagnose` adds the ledger's facts to a pulse of this session; the side panel's pulses list has them as **earlier sessions** (see [the side panel](#in-a-conversation)).
198
+
199
+ 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.
170
200
 
171
201
  ## From the command line
172
202
 
@@ -183,6 +213,7 @@ turri regenerate <file.plain> --from-scratch-chain # the same for every module
183
213
  turri spec-diff <file.plain> <diff> # apply a spec diff, and regenerate it
184
214
  turri conform <file.plain> 1 # black-box tests for functionality 1
185
215
  turri lineage <module> # what each regeneration changed in a module, and when
216
+ turri ledger [pulse] # what turri did here: the last runs and their pulses, or one pulse
186
217
  turri cultivate brood # run a skill directly - by name, the project's too, or by its folder
187
218
  turri release <file.plain> # release what is regenerated to dist/<module>, and activate it here
188
219
  ```