@turri-dev/cli 0.1.7 → 0.1.8-next.91b957c

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 (96) hide show
  1. package/README.md +6 -8
  2. package/dist/btw.d.ts +2 -0
  3. package/dist/btw.js +2 -2
  4. package/dist/cli.js +6 -2
  5. package/dist/config-edit.d.ts +1 -1
  6. package/dist/config-edit.js +4 -0
  7. package/dist/config.d.ts +27 -0
  8. package/dist/config.js +28 -1
  9. package/dist/extension.d.ts +11 -0
  10. package/dist/extension.js +118 -14
  11. package/dist/genome.js +2 -0
  12. package/dist/host.d.ts +3 -1
  13. package/dist/host.js +4 -1
  14. package/dist/identity.d.ts +6 -1
  15. package/dist/identity.js +33 -7
  16. package/dist/init-config.d.ts +3 -0
  17. package/dist/init-config.js +15 -0
  18. package/dist/ledger-cli.d.ts +1 -0
  19. package/dist/ledger-cli.js +50 -0
  20. package/dist/ledger-tools.d.ts +10 -0
  21. package/dist/ledger-tools.js +64 -0
  22. package/dist/loop-cli.js +4 -0
  23. package/dist/loop-tools.js +3 -1
  24. package/dist/pi.d.ts +2 -0
  25. package/dist/pi.js +24 -4
  26. package/dist/regenerate/activity.d.ts +9 -0
  27. package/dist/regenerate/activity.js +11 -0
  28. package/dist/regenerate/agent-env.d.ts +7 -0
  29. package/dist/regenerate/agent-env.js +126 -5
  30. package/dist/regenerate/agent-shell.js +5 -0
  31. package/dist/regenerate/cases.js +3 -0
  32. package/dist/regenerate/chunk-plan.d.ts +5 -6
  33. package/dist/regenerate/chunk-plan.js +43 -10
  34. package/dist/regenerate/conformance-agents.js +2 -2
  35. package/dist/regenerate/conformance.js +6 -3
  36. package/dist/regenerate/debug-log.d.ts +3 -1
  37. package/dist/regenerate/debug-log.js +10 -2
  38. package/dist/regenerate/diagnose.js +12 -2
  39. package/dist/regenerate/environment-error.d.ts +6 -0
  40. package/dist/regenerate/environment-error.js +19 -1
  41. package/dist/regenerate/job-log.d.ts +3 -1
  42. package/dist/regenerate/job-log.js +8 -5
  43. package/dist/regenerate/jobs-tree.d.ts +7 -3
  44. package/dist/regenerate/jobs-tree.js +21 -6
  45. package/dist/regenerate/jobs.d.ts +18 -0
  46. package/dist/regenerate/jobs.js +62 -5
  47. package/dist/regenerate/ledger-view.d.ts +92 -0
  48. package/dist/regenerate/ledger-view.js +345 -0
  49. package/dist/regenerate/ledger.d.ts +275 -0
  50. package/dist/regenerate/ledger.js +349 -0
  51. package/dist/regenerate/lineage.d.ts +9 -1
  52. package/dist/regenerate/lineage.js +64 -12
  53. package/dist/regenerate/loop.d.ts +6 -0
  54. package/dist/regenerate/loop.js +36 -6
  55. package/dist/regenerate/pulse-files.d.ts +14 -0
  56. package/dist/regenerate/pulse-files.js +63 -0
  57. package/dist/regenerate/records.d.ts +5 -3
  58. package/dist/regenerate/records.js +6 -4
  59. package/dist/regenerate/regenerate.js +7 -5
  60. package/dist/regenerate/regression.js +4 -2
  61. package/dist/regenerate/services.js +3 -0
  62. package/dist/regenerate/side-panel.d.ts +2 -0
  63. package/dist/regenerate/side-panel.js +4 -3
  64. package/dist/regenerate/spec-map.js +8 -5
  65. package/dist/regenerate/test-scripts.js +18 -5
  66. package/dist/regenerate/testing.js +3 -0
  67. package/dist/regenerate/unit-review.js +3 -0
  68. package/dist/run-report.d.ts +1 -1
  69. package/dist/run-report.js +15 -5
  70. package/dist/self-invocation.d.ts +1 -1
  71. package/dist/self-invocation.js +3 -2
  72. package/dist/skill-progress.d.ts +1 -1
  73. package/dist/skill.d.ts +5 -0
  74. package/dist/skill.js +1 -0
  75. package/dist/skills.d.ts +14 -0
  76. package/dist/skills.js +19 -1
  77. package/dist/surface.d.ts +5 -0
  78. package/dist/surface.js +18 -0
  79. package/docs/authoring-plain.md +2 -2
  80. package/docs/config.md +24 -3
  81. package/docs/development.md +3 -1
  82. package/docs/headless.md +9 -4
  83. package/docs/hosting.md +46 -0
  84. package/docs/installation.md +1 -0
  85. package/docs/regenerating.md +11 -11
  86. package/docs/sandbox.md +5 -5
  87. package/docs/skills-and-gates.md +3 -1
  88. package/docs/using.md +39 -8
  89. package/genome/rules/func-specs.md +1 -0
  90. package/genome/rules/turri-regeneration.md +3 -0
  91. package/genome/skills/add-functional-spec/SKILL.md +12 -3
  92. package/genome/skills/add-functional-specs/SKILL.md +12 -2
  93. package/genome/skills/brood/SKILL.md +2 -0
  94. package/genome/skills/bud/SKILL.md +2 -0
  95. package/genome/skills/spawn/SKILL.md +47 -35
  96. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  <p align="center"><img src="docs/images/turri-banner.gif" alt="turri, beside its mark - the first coding agent with the power of regenerative software: npm install -g @turri-dev/cli - and a pixel jellyfish regenerating, from a polyp on the sea floor"></p>
2
2
 
3
- <h1 align="center">turri<br>the first coding agent with the power of<br>regenerative software<br>&nbsp;</h1>
4
-
5
3
  <p align="center">
6
4
  <a href="https://www.npmjs.com/package/@turri-dev/cli"><img src="https://img.shields.io/npm/v/@turri-dev/cli?label=npm" alt="npm version"></a>
7
5
  <a href="LICENSE"><img src="https://img.shields.io/npm/l/@turri-dev/cli" alt="MIT licence"></a>
@@ -49,7 +47,7 @@ turri init-config --vendor anthropic
49
47
 
50
48
  It then tells you which API key to set - or to type `/login` once **turri** is running instead. **turri** loads the `config.turri.yaml` in the folder you start it from. Below the two models, the file sets every other setting to its default - change any of them in the file, or from the chat with `/turri-config`. Any other model works too: [models](#models).
51
49
 
52
- Once `turri` is running, say: *Let's start a new project: a small API where people can register and sign in.*
50
+ Once `turri` is running, type: `/spawn a small API where people can register and sign in` - or `/brood` for an interview that settles each feature with you first.
53
51
 
54
52
  No project of your own yet? Build one of [**the examples**](examples/README.md) with turri, step by step.
55
53
 
@@ -146,15 +144,15 @@ cd ~/my-project
146
144
  turri
147
145
  ```
148
146
 
149
- Then talk to it:
147
+ Then start the interview:
150
148
 
151
- > Let's start a new project: a small API where people can register and sign in.
149
+ > /brood a small API where people can register and sign in
152
150
 
153
- It runs the interview skill, asks you about the problem, the stack and the product, writes \*\*\*plain, and builds each functionality as it is written. While it works:
151
+ It asks you about the problem, the stack and the product, writes \*\*\*plain, and builds each functionality as it is written. While it works:
154
152
 
155
153
  - the **panel** above the input shows every build - its stage, what its agents are doing, its model and cost - and the queue behind it;
156
154
  - ask to try it at any time - "let me try it" - and **`release`** puts what is built so far in `dist/<module>` and **`activate`** starts it, installing what it needs - nothing to approve;
157
- - the **side panel** (`ctrl+h`) takes the screen and opens on **regen** - \*\*\*plain, every item marked planned, working, regenerated, validating, conform or needs you - and `p` turns it to the **pulses** (`/pulses`) - every pulse, running, queued and finished, and what each regenerates; what its agents did is in each pulse's log in `.turri/jobs/`;
155
+ - the **side panel** (`ctrl+h`) takes the screen and opens on **regen** - \*\*\*plain, every item marked planned, working, regenerated, validating, conform or needs you - and `p` turns it to the **pulses** (`/pulses`) - every pulse, running, queued and finished, and what each regenerates; what its agents did is in each pulse's log in `.turri/jobs/`, kept with `--debug` (`debug: true`);
158
156
  - **`/halt`** halts a pulse, **`/lineage`** shows every generation of a module, and the **`drift`** tool says what is left to regenerate;
159
157
  - **`/btw <question>`** asks turri something on the side - answered at once, even in the middle of a skill's interview (typed as the answer, under "Something else…"), and left out of the conversation;
160
158
  - **`/turri-config`** changes any setting of the project's `config.turri.yaml` - the models, review, conformance tests, how many pulses at once - and says when it takes effect: now, at your next message, or at the next pulse. No restart: `/turri-config review false`, or `/turri-config` alone to pick from the list;
@@ -188,7 +186,7 @@ Start at the [documentation index](docs/README.md), or go straight to a page:
188
186
  | | |
189
187
  | --- | --- |
190
188
  | [Installation](docs/installation.md) | doctor, what turri needs of this machine, **turri** and `pi`, environment variables |
191
- | [Examples](examples/README.md) | projects to build with turri step by step: cognio from an empty folder, guestbook - a backend and a frontend in minutes - a CLI, a backend service with conformance tests, a REST API integration |
189
+ | [Examples](examples/README.md) | projects to build with turri step by step: guestbook - a backend and a frontend in minutes - a CLI, and a backend service with conformance tests |
192
190
  | [Using **turri**](docs/using.md) | the conversation, skills, tools, pulses and regen, choosing models, the command line |
193
191
  | [The project config](docs/config.md) | `config.turri.yaml` and each stack's `config.turri.<stack>.yaml`: every key, models and their inheritance, conformance, the testing scripts, stacks and choosing one, the old `config.yaml` |
194
192
  | [Deploy](docs/deploy.md) | trying what is built: the last commit in `dist/<module>`, started on your machine, nothing installed without asking |
package/dist/btw.d.ts CHANGED
@@ -53,6 +53,8 @@ export interface SideQuestion {
53
53
  projectDir: string;
54
54
  /** turri's own folder: its source, skills, rules and docs. */
55
55
  harnessDir: string;
56
+ /** A host's surface, in its own words (TurriHost.surface): where the person watches the work instead of the terminal. */
57
+ surface?: string;
56
58
  }
57
59
  export declare function sideQuestionPrompt(side: SideQuestion): string;
58
60
  /** Answers a side question with an agent of its own, read only, on `model`. */
package/dist/btw.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import { readFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { confinedTools } from "./regenerate/confine.js";
15
- import { TURRI_IDENTITY } from "./identity.js";
15
+ import { conversationIdentity } from "./identity.js";
16
16
  import { ask, BEGIN, createExecutor } from "./pi.js";
17
17
  /** How many tool calls the side agent may make before it is told to answer. */
18
18
  export const SIDE_CALLS = 20;
@@ -85,7 +85,7 @@ export function skillSessionText(runDir, maxChars = SKILL_SESSION_CHARS) {
85
85
  }
86
86
  export function sideQuestionPrompt(side) {
87
87
  return [
88
- TURRI_IDENTITY,
88
+ conversationIdentity(side.surface),
89
89
  "",
90
90
  "The person is in the middle of something with you - a skill, a pulse - and has asked you a question on the side",
91
91
  "with /btw. Answer it as turri: in the first person, to them, in whole sentences - briefly and plainly, from what is",
package/dist/cli.js CHANGED
@@ -81,6 +81,7 @@ const COMMANDS = {
81
81
  "spec-diff": { module: "./spec-diff-cli.ts", blurb: "apply a change to the specs, given as a git diff, and regenerate it" },
82
82
  validate: { module: "./validate-cli.ts", blurb: "validate a ***plain spec, regenerating nothing" },
83
83
  lineage: { module: "./lineage-cli.ts", blurb: "a module's lineage: what was regenerated into it, and when" },
84
+ ledger: { module: "./ledger-cli.ts", blurb: "what turri did here: the last runs and their pulses, or one pulse, from the ledger" },
84
85
  release: { module: "./release-cli.ts", blurb: "release what is regenerated to dist/<module> and activate it on this machine" },
85
86
  cultivate: { module: "./cultivate-cli.ts", blurb: "run a gated skill" },
86
87
  };
@@ -96,6 +97,7 @@ const USAGE = [
96
97
  "",
97
98
  ` ${"options".padEnd(12)} --harness-model <m> the model you chat with --coding-model <m> the model renders run on`,
98
99
  ` ${"".padEnd(12)} --stack <name> build with config.turri.<name>.yaml over config.turri.yaml - here and in every command`,
100
+ ` ${"".padEnd(12)} --debug keep what turri writes for debugging it - agents' logs and sessions - as debug: true does`,
99
101
  ` ${"".padEnd(12)} --use-theme <t> another theme than turri-dark - and every option of pi's own`,
100
102
  "",
101
103
  ...Object.entries(COMMANDS).map(([name, { blurb }]) => ` ${name.padEnd(12)} ${blurb}`),
@@ -109,7 +111,7 @@ const USAGE = [
109
111
  ].join("\n");
110
112
  dropNoise();
111
113
  // --stack <name>, for any command and for the conversation: the stack everything is built with (stacks.ts), taken out
112
- // before the rest is read - a command never sees it.
114
+ // before the rest is read - a command never sees it. --debug too: TURRI_DEBUG for this run, and the pi it starts.
113
115
  const argv = await withoutStack(process.argv.slice(2));
114
116
  const [command, ...rest] = argv;
115
117
  async function withoutStack(args) {
@@ -121,6 +123,8 @@ async function withoutStack(args) {
121
123
  stack = args[++at] ?? "";
122
124
  else if (arg.startsWith("--stack="))
123
125
  stack = arg.slice("--stack=".length);
126
+ else if (arg === "--debug")
127
+ process.env.TURRI_DEBUG = "1";
124
128
  else
125
129
  kept.push(arg);
126
130
  }
@@ -173,7 +177,7 @@ process.argv = [
173
177
  // A headless run is stopped by a signal - SIGTERM, SIGHUP, Ctrl-C - whatever else listens for it: it exits, and what
174
178
  // its agents left running goes with it (shell.ts kills every process group on exit). Left to the libraries' own
175
179
  // handlers, one ran on for minutes after it was told to stop, building in the same codebase as the run after it.
176
- if (!["validate", "check", "drift", "lineage"].includes(command)) {
180
+ if (!["validate", "check", "drift", "lineage", "ledger"].includes(command)) {
177
181
  for (const [signal, code] of [["SIGTERM", 143], ["SIGHUP", 129], ["SIGINT", 130]]) {
178
182
  process.on(signal, () => {
179
183
  console.error(`\nstopped (${signal})`);
@@ -5,7 +5,7 @@
5
5
  * kept; `models:` and `agent-env:`, which nest, are written again whole, their own comments lost.
6
6
  */
7
7
  /** When a change takes effect. Nothing turri reads needs a restart: what is fixed at its start is changed live. */
8
- export type Effect = "now" | "next-message" | "next-pulse" | "next-plan" | "next-test-run" | "next-landing" | "next-regenerate";
8
+ export type Effect = "now" | "next-message" | "next-pulse" | "next-plan" | "next-test-run" | "next-landing" | "next-regenerate" | "next-start";
9
9
  export type SettingKind = "boolean" | "choice" | "number" | "model" | "list" | "path" | "budget";
10
10
  export interface Setting {
11
11
  /** As written in the file - a nested one by its path: `models.coding`, `models.coding.turn-limit`, `agent-env.files`. */
@@ -11,6 +11,7 @@ export const EFFECT_TEXT = {
11
11
  "next-test-run": "in effect from the next test run - the running pulse's too",
12
12
  "next-landing": "in effect from the next pulse that lands - the running one too",
13
13
  "next-regenerate": "in effect for the next regeneration that names no .plain file",
14
+ "next-start": "in effect the next time turri starts here",
14
15
  };
15
16
  const SECONDS = ["0", "60", "120", "180", "300"];
16
17
  /** A role's line: which agents it is, for the list. */
@@ -75,6 +76,9 @@ export function settings(rules = []) {
75
76
  { key: "agent-env.files", about: "dotenv files whose variables the agents' commands get", kind: "list", unset: "none", effect: "next-pulse", projectOnly: true },
76
77
  { key: "agent-env.vars", about: "variables from turri's environment the agents' commands get, by name", kind: "list", unset: "none", effect: "next-pulse", projectOnly: true },
77
78
  { key: "toolchains", about: "folders of this machine an agent's commands may read beside the usual ones", kind: "list", unset: "none", effect: "next-pulse", projectOnly: true },
79
+ { key: "debug", about: "whether turri keeps what it writes for debugging it - agents' logs and sessions, pulse logs, outputs", kind: "boolean", unset: "false", effect: "next-pulse", projectOnly: true },
80
+ { key: "cache-keep", about: "megabytes of package caches the project keeps; 0 keeps none", kind: "number", options: ["0", "1024", "4096", "16384"], min: 0, unset: "4096", effect: "next-start", projectOnly: true },
81
+ { key: "ledger-keep", about: "megabytes of the ledger - what turri did, as facts - the project keeps; 0 keeps none", kind: "number", options: ["0", "50", "100", "500"], min: 0, unset: "100", effect: "next-start", projectOnly: true },
78
82
  ...roles,
79
83
  ];
80
84
  }
package/dist/config.d.ts CHANGED
@@ -260,6 +260,23 @@ export interface ProjectConfig {
260
260
  * toolchain installed somewhere unusual. A module's own are added to the project's.
261
261
  */
262
262
  toolchains?: string[];
263
+ /**
264
+ * Megabytes of the ledger - what turri did, as facts, in .turri/ledger - the project keeps: past it, the oldest runs'
265
+ * are pruned when a run first writes. 0 keeps none, and writes none. The project's alone. Unset: 100.
266
+ */
267
+ ledgerKeep?: number;
268
+ /**
269
+ * Whether turri keeps what it writes for a person debugging it: every agent's whole log and saved session, each
270
+ * pulse's log, the testing scripts' and services' outputs, the agents' temporary files, and the dependencies installed
271
+ * in the modules a chain has left behind. Off, only what turri reads to work is kept. The project's alone. Unset: false.
272
+ */
273
+ debug?: boolean;
274
+ /**
275
+ * Megabytes of package caches - plain_modules/.turri/cache, and the caches in the agents' home folders - the project
276
+ * keeps: past it, the least recently written are removed when a run first starts an agent's command. 0 keeps none.
277
+ * The project's alone. Unset: 4096.
278
+ */
279
+ cacheKeep?: number;
263
280
  /** Each agent role's own model, thinking and turn limit; `default` for every role not named. */
264
281
  models?: Partial<Record<AgentRole | "default", RoleChoice>>;
265
282
  /** Seconds each turn of an agent a render starts has, for every role without its own. Unset: 120. 0: none. */
@@ -288,6 +305,9 @@ export declare const CONFIG_KEYS: {
288
305
  readonly maxLinesPerPulse: "max-lines-per-pulse";
289
306
  readonly agentEnv: "agent-env";
290
307
  readonly toolchains: "toolchains";
308
+ readonly ledgerKeep: "ledger-keep";
309
+ readonly debug: "debug";
310
+ readonly cacheKeep: "cache-keep";
291
311
  readonly models: "models";
292
312
  readonly turnLimit: "turn-limit";
293
313
  };
@@ -310,6 +330,13 @@ export declare function readProjectConfig(root: string, stack?: string): Project
310
330
  * edited. Never set in the conversation.
311
331
  */
312
332
  type RunOverrides = Partial<Pick<ProjectConfig, "harnessModel" | "codingModel" | "judgeModel" | "conformanceTests" | "review" | "mutationTesting">>;
333
+ /** Megabytes of package caches a project keeps when `cache-keep:` does not say. */
334
+ export declare const DEFAULT_CACHE_KEEP_MB = 4096;
335
+ /**
336
+ * Whether turri keeps what it writes for a person debugging it, in this project: TURRI_DEBUG - `--debug` sets it, and
337
+ * `0` or `false` turns it off - else the project's `debug:`. Off by default.
338
+ */
339
+ export declare function debugMode(project: string): boolean;
313
340
  /** Read a command's options as the config's keys for the rest of this process. */
314
341
  export declare function overrideConfig(overrides: RunOverrides): void;
315
342
  /** `--conformance-tests <value>`: the config's own values - all (or true), chosen, module, false. Undefined for anything else. */
package/dist/config.js CHANGED
@@ -379,6 +379,9 @@ export const CONFIG_KEYS = {
379
379
  maxLinesPerPulse: "max-lines-per-pulse",
380
380
  agentEnv: "agent-env",
381
381
  toolchains: "toolchains",
382
+ ledgerKeep: "ledger-keep",
383
+ debug: "debug",
384
+ cacheKeep: "cache-keep",
382
385
  models: "models",
383
386
  turnLimit: "turn-limit",
384
387
  };
@@ -431,7 +434,7 @@ function readStackedConfig(root, stack) {
431
434
  // The project's alone: one conversation, one module built when none is named.
432
435
  const merged = { ...project };
433
436
  for (const key of Object.keys(CONFIG_KEYS)) {
434
- if (key === "module" || key === "harnessModel" || key === "parallelJobs" || key === "rulesOff" || key === "compactAt" || key === "toolchains" || key === "agentEnv" || own[key] === undefined)
437
+ if (key === "module" || key === "harnessModel" || key === "parallelJobs" || key === "rulesOff" || key === "compactAt" || key === "ledgerKeep" || key === "debug" || key === "cacheKeep" || key === "toolchains" || key === "agentEnv" || own[key] === undefined)
435
438
  continue;
436
439
  merged[key] = own[key];
437
440
  }
@@ -443,6 +446,23 @@ function readStackedConfig(root, stack) {
443
446
  return withoutUndefined(merged);
444
447
  }
445
448
  let runOverrides = {};
449
+ /** Megabytes of package caches a project keeps when `cache-keep:` does not say. */
450
+ export const DEFAULT_CACHE_KEEP_MB = 4096;
451
+ /**
452
+ * Whether turri keeps what it writes for a person debugging it, in this project: TURRI_DEBUG - `--debug` sets it, and
453
+ * `0` or `false` turns it off - else the project's `debug:`. Off by default.
454
+ */
455
+ export function debugMode(project) {
456
+ const said = process.env.TURRI_DEBUG?.trim().toLowerCase();
457
+ if (said !== undefined && said !== "")
458
+ return said !== "0" && said !== "false";
459
+ try {
460
+ return readProjectConfig(project).debug === true;
461
+ }
462
+ catch {
463
+ return false;
464
+ }
465
+ }
446
466
  /** Read a command's options as the config's keys for the rest of this process. */
447
467
  export function overrideConfig(overrides) {
448
468
  runOverrides = { ...overrides };
@@ -542,6 +562,13 @@ function readConfigFile(path) {
542
562
  : undefined,
543
563
  maxSpecsPerPulse: positiveInteger(raw[CONFIG_KEYS.maxSpecsPerPulse]),
544
564
  maxLinesPerPulse: positiveInteger(raw[CONFIG_KEYS.maxLinesPerPulse]),
565
+ ledgerKeep: typeof raw[CONFIG_KEYS.ledgerKeep] === "number" && Number.isInteger(raw[CONFIG_KEYS.ledgerKeep]) && raw[CONFIG_KEYS.ledgerKeep] >= 0
566
+ ? raw[CONFIG_KEYS.ledgerKeep]
567
+ : undefined,
568
+ debug: typeof raw[CONFIG_KEYS.debug] === "boolean" ? raw[CONFIG_KEYS.debug] : undefined,
569
+ cacheKeep: typeof raw[CONFIG_KEYS.cacheKeep] === "number" && Number.isInteger(raw[CONFIG_KEYS.cacheKeep]) && raw[CONFIG_KEYS.cacheKeep] >= 0
570
+ ? raw[CONFIG_KEYS.cacheKeep]
571
+ : undefined,
545
572
  models: modelsSetting(raw[CONFIG_KEYS.models]),
546
573
  turnLimit: turnLimitSetting(raw[CONFIG_KEYS.turnLimit]),
547
574
  };
@@ -41,6 +41,17 @@ export interface TurriHost {
41
41
  onPlanChange?(): void;
42
42
  /** The dialogs ask_user puts its questions in - the conversation's and a skill's interview - instead of the terminal's. */
43
43
  dialogs?: AskUserIO;
44
+ /**
45
+ * The host's own UI, in its own words: the views it has in place of the terminal's, what its marks and buttons mean,
46
+ * where questions appear. Given, turri tells the model none of the terminal's - its keys, the side panel, regen,
47
+ * /pulses - and gives it this instead, as a `<surface>` block in turri's identity. Absent, the terminal is the surface.
48
+ */
49
+ surface?: string;
50
+ /**
51
+ * The last word on the whole system prompt, as turri has built it for this turn - after its own before_agent_start
52
+ * work: what it returns is what is sent. For a host that needs to change anything else.
53
+ */
54
+ systemPrompt?(prompt: string): string;
44
55
  }
45
56
  /** turri's conversation as an extension of pi's, for a host that runs it itself: see TurriHost. */
46
57
  export declare function createTurriExtension(host?: TurriHost): (pi: ExtensionAPI) => void;
package/dist/extension.js CHANGED
@@ -44,6 +44,9 @@ import { describeDiagnosis, diagnoseFailure } from "./regenerate/diagnose.js";
44
44
  import { HISTORY_DIR, historyLog, historyShow } from "./regenerate/lineage.js";
45
45
  import { defaultBuildDir } from "./regenerate/paths.js";
46
46
  import { describeTurnIncident, describeUsage, onModelEvent } from "./regenerate/activity.js";
47
+ import { ledgerWanted, startLedger } from "./regenerate/ledger.js";
48
+ import { describePastPulse, describeRuns, earlierPulses, ledgerProjects } from "./regenerate/ledger-view.js";
49
+ import { ledgerTool } from "./ledger-tools.js";
47
50
  import { describeSpecConflict } from "./regenerate/stop-report.js";
48
51
  import { inheritProviders, inheritThinkingLevel } from "./pi.js";
49
52
  import { beginCapture, captureConsole, describeCaptured, dropNoise } from "./quiet.js";
@@ -53,13 +56,14 @@ import { describeStdlib } from "./stdlib.js";
53
56
  import { peekKeyListener, runAsk } from "./ask.js";
54
57
  import { QuestionDialog } from "./question-dialog.js";
55
58
  import { registerStacks } from "./stacks.js";
56
- import { catalogueLine, discoverSkills, leftOutNote } from "./skills.js";
59
+ import { catalogueLine, createCommandGate, discoverSkills, leftOutNote, startedByCommandOnly } from "./skills.js";
57
60
  import { supersededByTurri } from "./chat-skills.js";
58
- import { TURRI_IDENTITY } from "./identity.js";
61
+ import { conversationIdentity } from "./identity.js";
62
+ import { setSurface, surfaceOf } from "./surface.js";
59
63
  import { turriIdentity } from "./environment.js";
60
64
  import { conformanceChooser } from "./regenerate/choose-conformance.js";
61
65
  import { answerSideQuestion, askWithAsides, BTW_HOW, runningSkill, skillRunStarted, skillSessionText, transcriptOf } from "./btw.js";
62
- import { createJobRegistry, defaultParallelJobs, describeJob, formatDuration, pulseName, shouldWait, STOPPED_ON_REQUEST, } from "./regenerate/jobs.js";
66
+ import { createJobRegistry, defaultParallelJobs, describeJob, firstRegeneration, formatDuration, formatElapsed, pulseName, shouldWait, STOPPED_ON_REQUEST, } from "./regenerate/jobs.js";
63
67
  /**
64
68
  * Makes gated skills reachable from a conversation.
65
69
  *
@@ -180,6 +184,14 @@ export function chatAskUserTool(dialogs) {
180
184
  },
181
185
  };
182
186
  }
187
+ /**
188
+ * The conversation's system prompt for a turn: pi's, turri's identity - with the host's surface in place of the
189
+ * terminal's, when it has one - and turri's blocks; then the host's last word on it.
190
+ */
191
+ function conversationSystemPrompt(base, host, blocks) {
192
+ const prompt = [base, conversationIdentity(host.surface), ...blocks].filter((part) => part !== "").join("\n\n");
193
+ return host.systemPrompt !== undefined ? host.systemPrompt(prompt) : prompt;
194
+ }
183
195
  /** The host each conversation's jobs report to, for the work loop made for them later. */
184
196
  const hosts = new WeakMap();
185
197
  /** turri's conversation as an extension of pi's, for a host that runs it itself: see TurriHost. */
@@ -260,6 +272,8 @@ function turriExtension(pi, host) {
260
272
  },
261
273
  });
262
274
  hosts.set(jobs, host);
275
+ // What the model is told of where the person watches the work, deep in the tools too: the host's surface, or the terminal.
276
+ setSurface(jobs, host.surface);
263
277
  host.onJobs?.(jobs);
264
278
  // The ***plain rules go into the system prompt in full, every turn. The block is
265
279
  // identical each time, so it sits inside the cached prefix rather than invalidating it.
@@ -298,7 +312,7 @@ function turriExtension(pi, host) {
298
312
  }
299
313
  }
300
314
  return {
301
- systemPrompt: [event.systemPrompt, TURRI_IDENTITY, TOOLS_NOT_CLI, projectConfigBlock(cwd), machineBlock(cwd, machine.now()), rulesBlock()].filter((part) => part !== "").join("\n\n"),
315
+ systemPrompt: conversationSystemPrompt(event.systemPrompt, host, [TOOLS_NOT_CLI, projectConfigBlock(cwd), machineBlock(cwd, machine.now()), rulesBlock()]),
302
316
  };
303
317
  });
304
318
  // The conversation's agent does not run turri again through its shell, unless the person
@@ -314,7 +328,7 @@ function turriExtension(pi, host) {
314
328
  if (event.toolName === "bash" && typeof command === "string") {
315
329
  const what = selfInvocation(command);
316
330
  if (what !== undefined && !askedForCommandLine(lastUserMessage))
317
- return { block: true, reason: refusal(what) };
331
+ return { block: true, reason: refusal(what, surfaceOf(jobs)) };
318
332
  }
319
333
  markConversationWrite(event.toolCallId, event.toolName, event.input);
320
334
  return undefined;
@@ -368,6 +382,24 @@ function turriExtension(pi, host) {
368
382
  ctx.ui.notify(event.job !== undefined ? `${event.job}: ${line}` : line, "warning");
369
383
  });
370
384
  });
385
+ // What this session does, as facts, in the project's .turri/ledger - a pulse's own in its project. Said once, in the
386
+ // chat, when it cannot be written.
387
+ const ledgerTrouble = [];
388
+ let tellLedgerTrouble;
389
+ if (ledgerWanted()) {
390
+ startLedger({
391
+ mode: Object.keys(host).length > 0 ? "host" : "chat",
392
+ project: cwd,
393
+ warn: (message) => (tellLedgerTrouble !== undefined ? tellLedgerTrouble(message) : ledgerTrouble.push(message)),
394
+ });
395
+ }
396
+ pi.on("session_start", (_event, ctx) => {
397
+ if (!ctx.hasUI)
398
+ return;
399
+ tellLedgerTrouble = (message) => ctx.ui.notify(message, "warning");
400
+ for (const message of ledgerTrouble.splice(0))
401
+ tellLedgerTrouble(message);
402
+ });
371
403
  // What a turri that was killed left running - its commands' process groups - stopped as this one starts.
372
404
  const orphans = sweepOrphanedGroups();
373
405
  let orphansSaid = orphans === 0;
@@ -416,6 +448,19 @@ function turriExtension(pi, host) {
416
448
  return;
417
449
  const catalogue = skills.map(catalogueLine).join("\n");
418
450
  const pendingNote = leftOutNote(discoveredSkills);
451
+ // The skills the person starts themselves - brood, spawn, bud: each has its command, and cultivate starts it only
452
+ // once its command is typed. The command puts what was typed in the chat, as the person's message, for the model to
453
+ // run it with what follows as its context.
454
+ const gate = createCommandGate();
455
+ for (const skill of skills.filter((s) => s.commandOnly)) {
456
+ pi.registerCommand(skill.name, {
457
+ description: `Start ${skill.name}${skill.steps.length > 1 ? ` - ${skill.steps.map((step) => step.name).join(" → ")}` : ""}: /${skill.name} [what you want]`,
458
+ handler: async (args) => {
459
+ gate.typed(skill.name);
460
+ pi.sendUserMessage(`/${skill.name}${args.trim() !== "" ? ` ${args.trim()}` : ""}`, { deliverAs: "followUp" });
461
+ },
462
+ });
463
+ }
419
464
  pi.registerTool({
420
465
  name: "cultivate",
421
466
  label: "Cultivate",
@@ -426,6 +471,7 @@ function turriExtension(pi, host) {
426
471
  promptSnippet: "cultivate: run a gated skill - its steps and review gates enforced by turri",
427
472
  promptGuidelines: [
428
473
  "When a request matches a skill listed in cultivate's description, call cultivate instead of doing the work yourself. Doing it by hand skips the review gates.",
474
+ "A skill marked as started only by the person runs only from its command: a message that is its command - /spawn a small server - is the person starting it, so call cultivate for it at once, with what follows the command as context. Never call cultivate for one otherwise; tell them to type its command.",
429
475
  // pi lists .agents/skills among its own skills too: read and followed by hand, a skill's gates would be skipped.
430
476
  "A skill cultivate lists may also be among your own skills, from the project's .agents/skills: run it with cultivate all the same, never by reading its SKILL.md and following it.",
431
477
  "cultivate returns the reviewed artifact and a trace of every gate decision. Report the artifact to the user; do not rewrite it.",
@@ -458,6 +504,8 @@ function turriExtension(pi, host) {
458
504
  details: {},
459
505
  };
460
506
  }
507
+ if (!gate.mayStart(skill))
508
+ return { content: [{ type: "text", text: startedByCommandOnly(skill.name) }], details: { notStarted: true } };
461
509
  // A new run clears the run folder. An interrupted one is not thrown away by accident:
462
510
  // whether to pick it up or start over is the user's call.
463
511
  const runDir = skillRunDir(cwd, skill.name);
@@ -475,6 +523,8 @@ function turriExtension(pi, host) {
475
523
  details: { interrupted },
476
524
  };
477
525
  }
526
+ // Its command is used up: started again later, it is typed again.
527
+ gate.started(skill.name);
478
528
  const trace = [`skill: ${skill.name}`, ...describeGates(skill).map((l) => ` ${l}`)];
479
529
  // While it runs, what its agent is doing leads: pi shows a running tool's first lines, and the trace's first
480
530
  // lines are the gates, which say nothing about now. The result keeps the trace alone.
@@ -560,6 +610,8 @@ function turriExtension(pi, host) {
560
610
  const filesBefore = projectFiles(cwd);
561
611
  // A side question asked while it runs is told what it has done so far.
562
612
  const ended = skillRunStarted({ skill: skill.name, dir: skill.dir, step: () => live.step, trace: () => [...trace], runDir });
613
+ // From here the time to first regeneration is counted, for a skill that writes spec diffs: its first pulse started.
614
+ const began = Date.now();
563
615
  const { result, output: captured } = await captureConsole(() => runSkill({
564
616
  skillDir: skill.dir,
565
617
  cwd,
@@ -620,11 +672,17 @@ function turriExtension(pi, host) {
620
672
  ? `${specsChanged.join(", ")} changed, and nothing is regenerated from that yet: call regenerate for it, and the code is brought up to the spec.`
621
673
  : "";
622
674
  const gated = skill.steps.some((step) => step.gate !== undefined);
675
+ const first = skill.allowedTools.includes("write_spec_diff") ? firstRegeneration(jobs.list(), began) : {};
676
+ const ttfr = first.started === undefined
677
+ ? ""
678
+ : `Time to first regeneration: ${formatElapsed(first.started)} - its first pulse started that long after ${skill.name} began` +
679
+ (first.landed !== undefined ? `, and landed after ${formatElapsed(first.landed)}.` : ".");
623
680
  const text = result.ok
624
681
  ? [
625
682
  gated ? `${skill.name}: passed all gates.` : `${skill.name}: finished. It has no review gates, so nothing checked its work: read what it changed before you report it.`,
626
683
  changed,
627
684
  ...(unrendered !== "" ? [unrendered] : []),
685
+ ...(ttfr !== "" ? [ttfr] : []),
628
686
  "",
629
687
  "<artifact>",
630
688
  result.output,
@@ -1411,28 +1469,40 @@ async function deliver(jobs, input) {
1411
1469
  const panelOpeners = new WeakMap();
1412
1470
  /** By registry: the last pulse that failed, stopped or did not pass, not yet looked at - ctrl+h opens on it. */
1413
1471
  const needsLooking = new WeakMap();
1472
+ /** What heads a pulse told from the ledger: it is not this session's, and the facts are all there is of it. */
1473
+ const pastPulseNote = "A pulse of an earlier session, as the ledger recorded it - facts, not its transcript. Its whole log is the file it names, " +
1474
+ "for the read tool.";
1414
1475
  function registerJobTools(pi, jobs, cwd) {
1415
1476
  pi.registerTool({
1416
1477
  name: "diagnose",
1417
1478
  label: "Diagnose a pulse",
1418
1479
  description: "Why a pulse did not land, from its whole transcript: how it ended and its diagnosis, the stages that went wrong, the " +
1419
1480
  "last failing test run with its output, the last review that sent the work back, and the tool calls that failed - " +
1420
- "and where the whole log is.",
1481
+ "and where the whole log is. A pulse of an earlier session is told from the ledger - what it recorded of it, as facts.",
1421
1482
  promptSnippet: "diagnose: why a pulse did not land, from its transcript",
1422
1483
  promptGuidelines: ["When a pulse failed or did not land, call diagnose before you explain it or change the spec: go by what happened, not by the report alone."],
1423
1484
  parameters: Type.Object({ job_id: Type.String({ description: "The pulse's id" }) }),
1424
1485
  async execute(_id, params) {
1425
1486
  const job = jobs.get(params.job_id);
1426
- if (job === undefined)
1427
- return { content: [{ type: "text", text: `No pulse ${params.job_id} has run in this session.` }], details: { status: "unknown" } };
1428
- return { content: [{ type: "text", text: whyFailed(job) }], details: { status: job.status } };
1487
+ if (job === undefined) {
1488
+ const past = describePastPulse(ledgerProjects(cwd), params.job_id);
1489
+ if (past !== undefined)
1490
+ return { content: [{ type: "text", text: `${pastPulseNote}\n\n${past}` }], details: { status: "earlier" } };
1491
+ return { content: [{ type: "text", text: `No pulse ${params.job_id} has run in this session, and the ledger has none by that id.` }], details: { status: "unknown" } };
1492
+ }
1493
+ // What the ledger recorded of it besides: its rounds, test runs, healers and commits, as facts.
1494
+ const facts = describePastPulse(ledgerProjects(cwd), job.id);
1495
+ const text = facts !== undefined ? `${whyFailed(job)}\n\nWhat the ledger recorded of it:\n${facts}` : whyFailed(job);
1496
+ return { content: [{ type: "text", text }], details: { status: job.status } };
1429
1497
  },
1430
1498
  });
1499
+ pi.registerTool(ledgerTool({ cwd }));
1431
1500
  pi.registerTool({
1432
1501
  name: "pulses",
1433
1502
  label: "Pulses",
1434
1503
  description: "What is regenerating: with no pulse, every pulse of this session - running, planned and finished - and what deploy started; " +
1435
- "with a pulse's id, that pulse - whether it is still running, its latest output, and its report once it has finished.",
1504
+ "with a pulse's id, that pulse - whether it is still running, its latest output, and its report once it has finished. " +
1505
+ "A pulse of an earlier session is told from the ledger.",
1436
1506
  promptSnippet: "pulses: every pulse, or one - what is regenerating and how it is going",
1437
1507
  parameters: Type.Object({
1438
1508
  job_id: Type.Optional(Type.String({ description: "The id returned when the pulse started. Omitted: every pulse" })),
@@ -1440,7 +1510,7 @@ function registerJobTools(pi, jobs, cwd) {
1440
1510
  }),
1441
1511
  async execute(_id, params) {
1442
1512
  if (params.job_id === undefined) {
1443
- return { content: [{ type: "text", text: listJobs(jobs) }], details: { status: "all" } };
1513
+ return { content: [{ type: "text", text: listJobs(jobs, cwd) }], details: { status: "all" } };
1444
1514
  }
1445
1515
  const existing = jobs.get(params.job_id);
1446
1516
  const planned = shownJobs(jobs).find((job) => job.id === params.job_id && jobs.get(job.id) === undefined);
@@ -1451,6 +1521,9 @@ function registerJobTools(pi, jobs, cwd) {
1451
1521
  };
1452
1522
  }
1453
1523
  if (existing === undefined) {
1524
+ const past = describePastPulse(ledgerProjects(cwd), params.job_id);
1525
+ if (past !== undefined)
1526
+ return { content: [{ type: "text", text: `${pastPulseNote}\n\n${past}` }], details: { status: "earlier" } };
1454
1527
  const known = shownJobs(jobs).map((job) => job.id);
1455
1528
  return {
1456
1529
  content: [
@@ -1564,6 +1637,21 @@ function registerJobTools(pi, jobs, cwd) {
1564
1637
  const panelBeside = () => (panelDocked && panelTerminal !== undefined ? dockedWidth(paddedColumns(panelTerminal.columns)) : 0);
1565
1638
  // What is open in the pulse list and the map, and where their cursors are: the conversation's, as left each time it opens.
1566
1639
  const jobsTree = createJobsTreeState();
1640
+ // Earlier sessions' pulses, from the ledger: read as the panel first shows them - they do not change while this one runs.
1641
+ let earlier;
1642
+ const earlierJobs = () => {
1643
+ if (earlier !== undefined)
1644
+ return earlier;
1645
+ try {
1646
+ const found = earlierPulses(ledgerProjects(cwd), new Set(jobs.list().map((job) => job.id)));
1647
+ earlier = found.jobs;
1648
+ jobsTree.earlierMore = found.more;
1649
+ }
1650
+ catch {
1651
+ earlier = [];
1652
+ }
1653
+ return earlier;
1654
+ };
1567
1655
  const mapState = createSpecMapState();
1568
1656
  // The map from the files - the .plain files asked for, else the project's - with the work loop's plan laid over them.
1569
1657
  const specMap = createSpecMapSource({
@@ -1657,6 +1745,7 @@ function registerJobTools(pi, jobs, cwd) {
1657
1745
  specMap.refresh();
1658
1746
  const view = createSidePanel({
1659
1747
  jobs: () => shownJobs(jobs),
1748
+ earlier: earlierJobs,
1660
1749
  tree: jobsTree,
1661
1750
  map: () => specMap.read(),
1662
1751
  mapState,
@@ -1930,13 +2019,27 @@ function deployedLabel(name, what) {
1930
2019
  function shownJobs(jobs) {
1931
2020
  return [...(workLoops.get(jobs)?.loop.planned() ?? []), ...jobs.list()];
1932
2021
  }
1933
- function listJobs(jobs) {
2022
+ function listJobs(jobs, cwd) {
1934
2023
  const all = [...shownJobs(jobs)].reverse().filter((job) => job.replacedBy === undefined);
1935
2024
  const deployed = runningDeployed().map((item) => `deploy ${item.name} running ${item.moduleName}: ${item.url ?? item.command} (pid ${item.pid}, output in ${relative(process.cwd(), item.log) || item.log})`);
1936
2025
  const listed = deployed.length > 0 ? `\n\nActivated from dist - /halt activated <name> halts one:\n${deployed.join("\n")}` : "";
2026
+ const before = earlierRuns(jobs, cwd);
1937
2027
  if (all.length === 0)
1938
- return `No pulses have been started.${listed}`;
1939
- return `${jobLines(all)}${listed}`;
2028
+ return `No pulses have been started in this session.${listed}${before}`;
2029
+ return `${jobLines(all)}${listed}${before}`;
2030
+ }
2031
+ /** Earlier sessions' last runs and their pulses, from the ledger - nothing when it has none. */
2032
+ function earlierRuns(jobs, cwd) {
2033
+ try {
2034
+ const found = earlierPulses(ledgerProjects(cwd), new Set(jobs.list().map((job) => job.id)));
2035
+ if (found.runs.length === 0)
2036
+ return "";
2037
+ const more = found.more > 0 ? `\n\n${found.more} earlier run${found.more === 1 ? "" : "s"} beyond these are in the ledger.` : "";
2038
+ return `\n\nEarlier sessions, from the ledger - pulses or diagnose with a pulse's id tells one:\n${describeRuns(found.runs)}${more}`;
2039
+ }
2040
+ catch {
2041
+ return "";
2042
+ }
1940
2043
  }
1941
2044
  function jobLines(all) {
1942
2045
  return all
@@ -2083,6 +2186,7 @@ async function askOnTheSide(pi, ctx, cwd, jobs, question) {
2083
2186
  pulses: jobs.list().map((job) => `${job.id} ${job.status}: ${pulseName(job)}`),
2084
2187
  projectDir: cwd,
2085
2188
  harnessDir: PACKAGE_ROOT,
2189
+ ...(surfaceOf(jobs) !== undefined ? { surface: surfaceOf(jobs) } : {}),
2086
2190
  }, chatModelOf(ctx));
2087
2191
  try {
2088
2192
  pi.appendEntry(SIDE_ENTRY, { question, answer });
package/dist/genome.js CHANGED
@@ -136,6 +136,8 @@ export async function runSkill(opts) {
136
136
  rules: true,
137
137
  system: toolchainsBlock(),
138
138
  resumeFrom: resumed?.sessionFile,
139
+ // Its run picks it back up (resume) and the side questions read it (btw): saved whatever the mode.
140
+ saveSession: true,
139
141
  });
140
142
  const manifest = resumed ?? {
141
143
  skill: skill.name,
package/dist/host.d.ts CHANGED
@@ -8,7 +8,7 @@ export { createTurriExtension, type TurriHost } from "./extension.ts";
8
8
  export type { AskUserIO } from "./ask.ts";
9
9
  /** pi itself, as turri's extension is built against it: a host makes its session with this one, not another copy. */
10
10
  export * as pi from "@earendil-works/pi-coding-agent";
11
- export { configForSpec, hasProjectConfig, modelSpec, projectModels, readProjectConfig, stacksOf, type ProjectConfig } from "./config.ts";
11
+ export { configForSpec, debugMode, hasProjectConfig, modelSpec, projectModels, readProjectConfig, stacksOf, type ProjectConfig } from "./config.ts";
12
12
  export { MODULES_DIR, projectDirOf, resolveTemplateDirs } from "./regenerate/paths.ts";
13
13
  export { BEDROCK_DEFAULT, BEDROCK_REGIONS, bedrockModels, initConfig, VENDORS, type BedrockChoice, type Vendor } from "./init-config.ts";
14
14
  export { checkPlainFile, type CheckResult } from "./plain/check.ts";
@@ -19,3 +19,5 @@ export { createWorkLoop, describeModules, functionalityCompletion, promptOf, typ
19
19
  export { createJobRegistry, defaultParallelJobs, type Job, type JobRegistry } from "./regenerate/jobs.ts";
20
20
  export { describeTurnIncident, onModelEvent, type TranscriptEntry } from "./regenerate/activity.ts";
21
21
  export { sweepOrphanedGroups } from "./regenerate/shell.ts";
22
+ export { ledgerDir, onLedgerEvent, readLedger, type AgentRef, type LedgerEntry, type LedgerEvent, type LedgerKind, type LedgerQuery } from "./regenerate/ledger.ts";
23
+ export { asJob, describePulseView, describeRuns, earlierPulses, ledgerProjects, pulseView, runViews, type PulseView, type RunView } from "./regenerate/ledger-view.ts";
package/dist/host.js CHANGED
@@ -9,7 +9,7 @@ export { createTurriExtension } from "./extension.js";
9
9
  /** pi itself, as turri's extension is built against it: a host makes its session with this one, not another copy. */
10
10
  export * as pi from "@earendil-works/pi-coding-agent";
11
11
  // The project and its settings.
12
- export { configForSpec, hasProjectConfig, modelSpec, projectModels, readProjectConfig, stacksOf } from "./config.js";
12
+ export { configForSpec, debugMode, hasProjectConfig, modelSpec, projectModels, readProjectConfig, stacksOf } from "./config.js";
13
13
  export { MODULES_DIR, projectDirOf, resolveTemplateDirs } from "./regenerate/paths.js";
14
14
  // A project's first config.turri.yaml, as `turri init-config` writes it - the vendor whose models it takes chosen by the host.
15
15
  export { BEDROCK_DEFAULT, BEDROCK_REGIONS, bedrockModels, initConfig, VENDORS } from "./init-config.js";
@@ -23,3 +23,6 @@ export { createWorkLoop, describeModules, functionalityCompletion, promptOf } fr
23
23
  export { createJobRegistry, defaultParallelJobs } from "./regenerate/jobs.js";
24
24
  export { describeTurnIncident, onModelEvent } from "./regenerate/activity.js";
25
25
  export { sweepOrphanedGroups } from "./regenerate/shell.js";
26
+ // What turri did, as facts: the ledger, heard live and read back - this session's pulses and every earlier one's.
27
+ export { ledgerDir, onLedgerEvent, readLedger } from "./regenerate/ledger.js";
28
+ export { asJob, describePulseView, describeRuns, earlierPulses, ledgerProjects, pulseView, runViews } from "./regenerate/ledger-view.js";
@@ -1,2 +1,7 @@
1
- /** Who the conversation's agent is: turri, and what it does - first, so it speaks as turri, in the first person. */
1
+ /**
2
+ * Who the conversation's agent is: turri, and what it does - first, so it speaks as turri, in the first person. Where
3
+ * the person watches the work is the terminal's regen; given a host's surface, that is said in its place.
4
+ */
5
+ export declare function conversationIdentity(surface?: string): string;
6
+ /** turri as the terminal runs it. */
2
7
  export declare const TURRI_IDENTITY: string;