@turri-dev/cli 0.1.26 → 0.1.27-next.763d091

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/host.d.ts CHANGED
@@ -25,7 +25,7 @@ export { conceptBeingTyped, findConcepts } from "./plain/concepts.ts";
25
25
  export { plainGraph, type PlainConceptNode, type PlainGraph, type PlainItemNode, type PlainLink, type PlainModuleNode, type PlainSection } from "./plain/graph.ts";
26
26
  export { isStack, STDLIB_KINDS, stdlibTemplates, type StdlibKind, type StdlibTemplate } from "./stdlib.ts";
27
27
  export { createWorkLoop, describeModules, functionalityCompletion, promptOf, type DriftReport, type ModuleStatus, type WorkLoop } from "./regenerate/loop.ts";
28
- export { createJobRegistry, defaultParallelJobs, type Job, type JobRegistry } from "./regenerate/jobs.ts";
28
+ export { createJobRegistry, defaultParallelJobs, type Job, type JobRegistry, type PulseCases, type PulseSpec } from "./regenerate/jobs.ts";
29
29
  export { describeTurnIncident, onModelEvent, type TranscriptEntry } from "./regenerate/activity.ts";
30
30
  export { sweepOrphanedGroups } from "./regenerate/shell.ts";
31
31
  export { ledgerDir, onLedgerEvent, readLedger, type AgentRef, type LedgerEntry, type LedgerEvent, type LedgerKind, type LedgerQuery } from "./regenerate/ledger.ts";
@@ -65,7 +65,7 @@ export function conformanceCasesBlock(cases) {
65
65
  lines.push(`- Changed - its text, its acceptance tests, or a concept it uses: ${named(cases.patch)}.`, " Its conformance tests exist: rewrite them to what it says now. The tests of everything else stay as they are.");
66
66
  }
67
67
  if (cases.again.length > 0) {
68
- lines.push(`- Regenerated again - its code was built anew, and its conformance tests were written against the code it replaced: ${named(cases.again)}.`, " Its text did not change, but the code did. Run its tests against the new code. What they check that the", " specification says stays checked: rewrite such a test only as the new code needs it reached. What they check", " that the specification leaves open - a path, a field, a message, a format the old code chose and the", " specification never names - is the new code's choice now: rewrite those checks to what the new code does,", " or take them out. No test may hold the new code to what only the old code did. The tests of everything else", " stay as they are.");
68
+ lines.push(`- Regenerated again - its code was built anew, and its conformance tests were written against the code it replaced: ${named(cases.again)}.`, " Its text did not change, but the code did: write its tests again for the new code - that they still pass is", " no sign they fit it. What they check that the specification says stays checked, as the new code is reached.", " What they check that the specification leaves open - a path, a field, a message, a format the old code chose", " and the specification never names - is the new code's choice now: rewrite those checks to what the new code", " does, or take them out. No test may hold the new code to what only the old code did. A run that leaves every", " test file labelled with it as it was is sent back. The tests of everything else stay as they are.");
69
69
  }
70
70
  if (cases.delete.length > 0)
71
71
  lines.push(`- Taken out - remove its conformance tests: ${named(cases.delete)}.`);
@@ -109,6 +109,11 @@ export interface RenderConformanceTestsResult {
109
109
  };
110
110
  /** The tests were sent back to be written and labelled, and never were: what was missing the last time. */
111
111
  unlabelled?: string[];
112
+ /**
113
+ * Of the functional specs the job regenerates again, those whose tests this run wrote - a test file labelled with it
114
+ * added or changed. Only these have tests for the new code; the rest are still marked to regenerate again.
115
+ */
116
+ rewritten?: string[];
112
117
  /** The tests were optimised as far as REVIEW_ROUNDS reviews could take them: what the last one still suggested. */
113
118
  reviewWaived?: ReviewProblem[];
114
119
  /** The suite passed, but the tests never passed review: what the review last found. */
@@ -126,6 +131,8 @@ export interface RenderConformanceTestsResult {
126
131
  * and this refuses to start without one.
127
132
  */
128
133
  export declare function renderConformanceTests(options: RenderConformanceTestsOptions): Promise<RenderConformanceTestsResult>;
134
+ /** Sent back when the tests of a functional spec regenerated again are as they were: they are to be written for the new code. */
135
+ export declare function againPrompt(frids: string[]): string;
129
136
  /**
130
137
  * Why conformance tests did not land, in a line. Tests sent back to be labelled and never labelled may well pass: the
131
138
  * line says so, rather than that they failed.
@@ -10,7 +10,7 @@ import { firstLine, record } from "./ledger.js";
10
10
  import { createWorkLedger, describeAccounts, SUBMIT_WORK_NOTE, unanswered, unansweredPrompt } from "./submit-work.js";
11
11
  import { CONFORMANCE_COMPACTION } from "./compaction.js";
12
12
  import { NOTES_TURN, notesForSession, passedOutcome, readNotes, reflectOnReview, withNotes } from "./notes.js";
13
- import { acceptanceLabel, acceptanceText, labelsPrompt, labelsPromptFor, misplacedConformanceTests, misplacedTestsPrompt, missingTestLabels } from "./test-kinds.js";
13
+ import { acceptanceLabel, acceptanceText, labelledFiles, labelsPrompt, labelsPromptFor, misplacedConformanceTests, misplacedTestsPrompt, missingTestLabels } from "./test-kinds.js";
14
14
  import { createScriptTool, findTestScripts, projectRootFor, requireTestScripts, runConformanceScript, runUnitScript, failureSignature, STUCK_ROUNDS, scriptFailure, staticChecksOnly, withTestingScripts, } from "./test-scripts.js";
15
15
  import { createWebTools, WEB_TOOLS } from "./web.js";
16
16
  import { createMyDiffTool } from "./self-check.js";
@@ -345,6 +345,19 @@ export async function renderConformanceTests(options) {
345
345
  ? "turri is bringing the conformance tests in line with the spec change"
346
346
  : "turri is writing conformance tests from the spec");
347
347
  let response = await askConformance(BEGIN, options.onDelta);
348
+ // The functional specs of this module the job regenerates again, and of those, the ones whose tests this run has
349
+ // not written yet: no test file labelled with it added or changed since the run began.
350
+ const againFrids = (options.cases?.again ?? []).filter((spec) => spec.module === undefined || spec.module === moduleName).map((spec) => spec.frid);
351
+ const rewrittenAgain = () => {
352
+ const changes = diffFiles(testsAtStart, digestFiles(conformanceDir));
353
+ const touchedFiles = new Set([...changes.added, ...changes.changed]);
354
+ return againFrids.filter((id) => labelledFiles(conformanceDir, id).some((file) => touchedFiles.has(file)));
355
+ };
356
+ const staleAgain = () => {
357
+ const done = new Set(rewrittenAgain());
358
+ return againFrids.filter((id) => !done.has(id));
359
+ };
360
+ let againAsks = 0;
348
361
  const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
349
362
  let verified = false;
350
363
  let rounds = 0;
@@ -405,6 +418,20 @@ export async function renderConformanceTests(options) {
405
418
  continue;
406
419
  }
407
420
  }
421
+ // Regenerated again: its tests were written against the code it replaced, and their passing says nothing of the new
422
+ // code - they pass on what only the old code did as readily. Its tests have to be written again in this run.
423
+ const stale = staleAgain();
424
+ if (stale.length > 0) {
425
+ if (againAsks >= LABEL_ASKS) {
426
+ stage(`the tests of functionalit${stale.length === 1 ? "y" : "ies"} ${stale.join(", ")}, regenerated again, were sent back ${LABEL_ASKS} times to be written again, and still are not - stopping`);
427
+ unlabelled = stale.map((id) => `The tests of functionality ${id}, regenerated again, were not written again for the new code: no test file labelled with it changed.`);
428
+ break;
429
+ }
430
+ againAsks += 1;
431
+ stage(`the tests of functionalit${stale.length === 1 ? "y" : "ies"} ${stale.join(", ")}, regenerated again, are as they were - the conformance agent writes them again`);
432
+ response = await askConformance(againPrompt(stale), options.onDelta);
433
+ continue;
434
+ }
408
435
  // The suite is the tests folder: a conformance test in the code folder is in the wrong place,
409
436
  // and not the coding agent's to write.
410
437
  const misplaced = misplacedConformanceTests(buildDir);
@@ -830,6 +857,7 @@ export async function renderConformanceTests(options) {
830
857
  ...(stuck !== undefined ? { stuck } : {}),
831
858
  ...(escalation !== undefined ? { escalation } : {}),
832
859
  ...(unlabelled !== undefined ? { unlabelled } : {}),
860
+ ...(againFrids.length > 0 ? { rewritten: rewrittenAgain() } : {}),
833
861
  ...(reviewWaived !== undefined ? { reviewWaived } : {}),
834
862
  ...(verified && green !== undefined ? { green } : {}),
835
863
  ...(!verified && lastReview !== undefined ? { review: lastReview } : {}),
@@ -842,6 +870,18 @@ export async function renderConformanceTests(options) {
842
870
  await fixerServices.stopAll();
843
871
  }
844
872
  }
873
+ /** Sent back when the tests of a functional spec regenerated again are as they were: they are to be written for the new code. */
874
+ export function againPrompt(frids) {
875
+ const which = frids.length === 1 ? `functionality ${frids[0]}` : `functionalities ${frids.join(", ")}`;
876
+ return [
877
+ `${which[0].toUpperCase()}${which.slice(1)} ${frids.length === 1 ? "is" : "are"} regenerated again: ${frids.length === 1 ? "its" : "their"} code was built anew, and no test file labelled with ${frids.length === 1 ? "it" : "them"} has changed.`,
878
+ "Tests that pass prove nothing here: they were written against the code it replaced, and pass on what only the old",
879
+ "code did as readily as on what the specification says. Read the new code, and write the tests again for it: what",
880
+ "the specification says, checked as the new code is reached; what it leaves open - a path, a field, a message, a",
881
+ "format the old code chose - checked as the new code does it, or taken out. Keep each label. The tests of",
882
+ "everything else stay as they are.",
883
+ ].join("\n");
884
+ }
845
885
  /**
846
886
  * Why conformance tests did not land, in a line. Tests sent back to be labelled and never labelled may well pass: the
847
887
  * line says so, rather than that they failed.
@@ -8,6 +8,21 @@ import { type AgentTally, type BuildingSpec, type SpecConflict, type TranscriptE
8
8
  * carries on while the agents work.
9
9
  */
10
10
  export type JobStatus = "queued" | "running" | "succeeded" | "failed" | "stopped";
11
+ /** A functional spec a pulse covers: the module whose spec it is - the pulse's own, or one below it - and its number there. */
12
+ export interface PulseSpec {
13
+ module: string;
14
+ frid: string;
15
+ }
16
+ /**
17
+ * The functional specs a pulse covers, by what it does to each (`cases.ts`): new, changed, regenerated again - its text
18
+ * unchanged - or taken out. A pulse of none is mechanical: reqs, or definitions none uses.
19
+ */
20
+ export interface PulseCases {
21
+ add: PulseSpec[];
22
+ patch: PulseSpec[];
23
+ again: PulseSpec[];
24
+ delete: PulseSpec[];
25
+ }
11
26
  export interface Job {
12
27
  id: string;
13
28
  kind: string;
@@ -23,6 +38,8 @@ export interface Job {
23
38
  diff?: string;
24
39
  /** Where its diff's lines are in the .plain files - their files and line numbers there - when they could be placed. */
25
40
  diffWhere?: import("./diff-view.ts").DiffWhere;
41
+ /** The functional specs it covers, by case: what it does to each. */
42
+ cases?: PulseCases;
26
43
  /** For a conformance job: the render job it is the conformance of. */
27
44
  follows?: string;
28
45
  /** A note on it, for the person: why it did not run, or what became of it. */
@@ -118,6 +135,8 @@ export interface StartJobInput {
118
135
  diff?: string;
119
136
  /** Where the diff's lines are in the .plain files, when they could be placed. */
120
137
  diffWhere?: import("./diff-view.ts").DiffWhere;
138
+ /** The functional specs it covers, by case. */
139
+ cases?: PulseCases;
121
140
  /** "provider/model-id" its agents run on, to show where the job is shown. */
122
141
  model?: string;
123
142
  /** For a conformance job: the render it is the conformance of. */
@@ -79,6 +79,7 @@ export function createJobRegistry(options) {
79
79
  ...(input.title !== undefined ? { title: input.title } : {}),
80
80
  diff: input.diff,
81
81
  ...(input.diffWhere !== undefined ? { diffWhere: input.diffWhere } : {}),
82
+ ...(input.cases !== undefined ? { cases: input.cases } : {}),
82
83
  model: input.model,
83
84
  ...(stack !== undefined ? { stack } : {}),
84
85
  ...(chat !== undefined ? { chat } : {}),
@@ -107,6 +108,7 @@ export function createJobRegistry(options) {
107
108
  ...(input.title !== undefined ? { title: input.title } : {}),
108
109
  ...(input.model !== undefined ? { model: input.model } : {}),
109
110
  ...(stack !== undefined ? { stack } : {}),
111
+ ...(input.cases !== undefined ? { cases: input.cases } : {}),
110
112
  waitingOn: blockers,
111
113
  ...(input.follows !== undefined ? { follows: input.follows } : {}),
112
114
  }, place);
@@ -133,10 +133,17 @@ function nameOf(view) {
133
133
  return queued.summary;
134
134
  return queued.pulse !== undefined ? `${queued.module} · ${queued.jobKind} pulse ${queued.pulse}` : `${queued.module} ${queued.jobKind}`;
135
135
  }
136
+ /**
137
+ * How a pulse ended. One not ended yet is still running, or waiting, when it is this run's - the run that is reading the
138
+ * ledger; another run's never will: that run stopped while it ran.
139
+ */
136
140
  function outcomeOf(view) {
137
141
  const finished = view.finished;
138
- if (finished === undefined)
142
+ if (finished === undefined) {
143
+ if (view.run === ledgerRun())
144
+ return view.started !== undefined ? "still running" : "queued - not started yet";
139
145
  return view.started !== undefined ? "never ended - its run stopped while it ran" : "never started";
146
+ }
140
147
  const landed = finished.landed === true ? ", landed" : finished.landed === false ? ", did not land" : "";
141
148
  const stopped = finished.stoppedBy !== undefined ? ` by ${finished.stoppedBy === "turri" ? "turri" : "the person"}` : "";
142
149
  return `${finished.status}${stopped}${landed}, after ${formatElapsed(finished.ms)}`;
@@ -251,7 +258,7 @@ export function describeRuns(runs) {
251
258
  const started = view.started;
252
259
  const when = started?.ts !== undefined ? started.ts.slice(0, 19).replace("T", " ") : "";
253
260
  const version = started?.version !== undefined && started.version !== null ? ` · turri ${started.version}${started.commit !== null && started.commit !== undefined ? ` (${started.commit.slice(0, 7)}${started.dirty === true ? ", changed" : ""})` : ""}` : "";
254
- const head = `run ${view.run} · ${started?.mode ?? "?"} · ${when}${view.ended !== undefined ? ` · ${formatElapsed(view.ended.ms)}` : " · still running, or killed"}${version}`;
261
+ const head = `run ${view.run} · ${started?.mode ?? "?"} · ${when}${view.ended !== undefined ? ` · ${formatElapsed(view.ended.ms)}` : view.run === ledgerRun() ? " · this run, still running" : " · still running, or killed"}${version}`;
255
262
  const pulses = view.pulses.map((pulse) => ` ${pulse.id} ${nameOf(pulse).padEnd(32)} ${outcomeOf(pulse)}${pulse.usage !== undefined && describeUsage(pulse.usage) !== "" ? ` · ${describeUsage(pulse.usage)}` : ""}`);
256
263
  return [head, ...(pulses.length > 0 ? pulses : [" no pulses"])].join("\n");
257
264
  })
@@ -288,6 +295,7 @@ export function asJob(view) {
288
295
  ...(view.queued?.pulse !== undefined ? { pulse: view.queued.pulse } : {}),
289
296
  ...(view.queued?.title !== undefined ? { title: view.queued.title } : {}),
290
297
  ...(view.queued?.model !== undefined ? { model: view.queued.model } : {}),
298
+ ...(view.queued?.cases !== undefined ? { cases: view.queued.cases } : {}),
291
299
  // Its ***plain diff, shown a file at a time as this session's pulses' are.
292
300
  ...(view.diff !== undefined ? { diff: view.diff.diff, ...(view.diff.where !== undefined ? { diffWhere: view.diff.where } : {}) } : {}),
293
301
  status,
@@ -1,4 +1,5 @@
1
1
  import type { DiffWhere } from "./diff-view.ts";
2
+ import type { PulseCases } from "./jobs.ts";
2
3
  import { type UsageReport } from "./activity.ts";
3
4
  /**
4
5
  * The ledger: what turri does, as typed facts, one JSON line each, written as it happens - to
@@ -78,6 +79,7 @@ export type LedgerEvent = {
78
79
  title?: string;
79
80
  model?: string;
80
81
  stack?: string;
82
+ cases?: PulseCases;
81
83
  waitingOn: string[];
82
84
  follows?: string;
83
85
  } | {
@@ -1,6 +1,7 @@
1
+ import { type MarshalledPlainSource } from "../plain/spec.ts";
1
2
  import { type ChunkAgents } from "./chunk-plan.ts";
2
3
  import { type SpecGap } from "./notes.ts";
3
- import { type Job, type JobRegistry } from "./jobs.ts";
4
+ import { type Job, type JobRegistry, type PulseCases } from "./jobs.ts";
4
5
  import type { Steering } from "./steer.ts";
5
6
  import { type ChainEntry } from "./requires.ts";
6
7
  import { type SpecSnapshot } from "./regenerate.ts";
@@ -385,6 +386,14 @@ export declare function isUpToDate(status: ModuleStatus): boolean;
385
386
  export declare function describeModules(statuses: ModuleStatus[], options?: {
386
387
  for?: "chat" | "command line";
387
388
  }): string;
389
+ /** The functional specs a pulse covers, by case, each with the module whose spec it is: a host's to light and mark. */
390
+ export declare function pulseCases(cases: JobCases, moduleName: string): PulseCases;
391
+ /**
392
+ * What a conformance job records its tests are written for: its step's tree, but a functional spec of the module it
393
+ * regenerates again keeps its mark unless the run wrote its tests - tests that passed as they were are still the old
394
+ * code's, and still conformance work.
395
+ */
396
+ export declare function keepAgainMarks(tree: MarshalledPlainSource, cases: JobCases, moduleName: string, rewritten: string[]): MarshalledPlainSource;
388
397
  /**
389
398
  * A module's first pulse when it builds one functional spec - the project set up and the smallest thing that runs: it lands on its
390
399
  * unit tests alone, with no review. A reviewer, and an arbiter after it, on a scaffold is time spent before anything can
@@ -522,6 +522,8 @@ export function createWorkLoop(options) {
522
522
  diff: shown.diff,
523
523
  // Its lines where they are written: the module's .plain file, those it imports, and those of the modules below it.
524
524
  diffWhere: shown.where,
525
+ // The functional specs it covers, by case - its own module's, and those of modules below it.
526
+ cases: pulseCases(planned.cases, tip.moduleName),
525
527
  ...(state.model !== undefined ? { model: state.model } : {}),
526
528
  lanes: [...moduleLanes(tip.plainFile)],
527
529
  rerun: `turri regenerate ${relative(options.cwd, tip.plainFile) || basename(tip.plainFile)}`,
@@ -1677,11 +1679,28 @@ const defaultConformanceRunner = async (job, onDelta) => {
1677
1679
  onDelta,
1678
1680
  });
1679
1681
  if (result.passed) {
1680
- writeTestsRecord(result.conformanceDir, job.planned.step.tree);
1682
+ writeTestsRecord(result.conformanceDir, keepAgainMarks(job.planned.step.tree, job.planned.cases, job.moduleName, result.conformance.rewritten ?? []));
1681
1683
  landWork(job.buildDir, `conformance tests: ${job.planned.title}`);
1682
1684
  }
1683
1685
  return { landed: result.passed, report: describeSpecDiffConformance(result), ...(result.specGaps !== undefined ? { specGaps: result.specGaps } : {}) };
1684
1686
  };
1687
+ /** The functional specs a pulse covers, by case, each with the module whose spec it is: a host's to light and mark. */
1688
+ export function pulseCases(cases, moduleName) {
1689
+ const of = (specs) => specs.map((spec) => ({ module: spec.module ?? moduleName, frid: spec.frid }));
1690
+ return { add: of(cases.add), patch: of(cases.patch), again: of(cases.again), delete: of(cases.delete) };
1691
+ }
1692
+ /**
1693
+ * What a conformance job records its tests are written for: its step's tree, but a functional spec of the module it
1694
+ * regenerates again keeps its mark unless the run wrote its tests - tests that passed as they were are still the old
1695
+ * code's, and still conformance work.
1696
+ */
1697
+ export function keepAgainMarks(tree, cases, moduleName, rewritten) {
1698
+ const stale = new Set(cases.again.filter((spec) => (spec.module ?? moduleName) === moduleName && !rewritten.includes(spec.frid)).map((spec) => spec.frid));
1699
+ if (stale.size === 0)
1700
+ return tree;
1701
+ const items = tree[FUNCTIONAL_REQUIREMENTS] ?? [];
1702
+ return { ...tree, [FUNCTIONAL_REQUIREMENTS]: items.map((item) => (item.frid !== undefined && stale.has(item.frid) ? { ...item, [REGENERATE_AGAIN]: true } : item)) };
1703
+ }
1685
1704
  /**
1686
1705
  * A module's first pulse when it builds one functional spec - the project set up and the smallest thing that runs: it lands on its
1687
1706
  * unit tests alone, with no review. A reviewer, and an arbiter after it, on a scaffold is time spent before anything can
@@ -21,6 +21,11 @@ export declare function acceptanceText(item: string): string;
21
21
  * test file carries it, and each acceptance test no test is labelled as implementing.
22
22
  */
23
23
  export declare function missingTestLabels(testsDir: string, frid: string, acceptanceTests: string[]): string[];
24
+ /**
25
+ * The test files that carry a functionality's labels - its conformance tests' or any of its acceptance tests'. The
26
+ * harness's own files beside them - the notes, the record - are not tests, whatever they mention.
27
+ */
28
+ export declare function labelledFiles(testsDir: string, frid: string): string[];
24
29
  /** Sent back when the labels are missing: which, and how to label them. */
25
30
  export declare function labelsPrompt(frid: string, missing: string[], empty?: boolean, hasAcceptance?: boolean): string;
26
31
  /**
@@ -54,6 +54,24 @@ export function missingTestLabels(testsDir, frid, acceptanceTests) {
54
54
  });
55
55
  return missing;
56
56
  }
57
+ /**
58
+ * The test files that carry a functionality's labels - its conformance tests' or any of its acceptance tests'. The
59
+ * harness's own files beside them - the notes, the record - are not tests, whatever they mention.
60
+ */
61
+ export function labelledFiles(testsDir, frid) {
62
+ const id = frid.replace(/\./g, "\\.");
63
+ const label = new RegExp(`(conformance tests|acceptance test \\d+) of functionality ${id}(?![0-9.]*[0-9])`);
64
+ return listFiles(testsDir).filter((file) => {
65
+ if (file.split(/[\\/]/).at(-1).startsWith(".turri"))
66
+ return false;
67
+ try {
68
+ return label.test(normalise(readFileSync(join(testsDir, file), "utf8")));
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ });
74
+ }
57
75
  /** Sent back when the labels are missing: which, and how to label them. */
58
76
  export function labelsPrompt(frid, missing, empty = false, hasAcceptance = true) {
59
77
  return labelsPromptFor([{ frid, missing }], empty, hasAcceptance);
package/docs/hosting.md CHANGED
@@ -113,7 +113,7 @@ await turri.dispose(); // every chat let go, what activate start
113
113
  - **Interrupting.** `interrupt(id)` is the terminal's ctrl+c for a host's key or button: it stops what that chat is doing - its turn, a skill or a tool in it, a `!` command, a compaction - and nothing in the other chats (`stopped`). A skill stopped keeps its run, to be continued. A question of the chat's open in the host's dialogs when its turn is stopped is over - declined - and its dialog's `signal` aborted, for the host to close it. A chat doing nothing ends the session, every chat with it, as `dispose` does (`ended`): turri never exits the host's process, so the host closes what showed it. An interrupt straight after one that stopped something is `ignored`, so pressing twice to be sure ends nothing. `session.abort()` is the chat's turn alone, and never ends the session.
114
114
  - **Declining a question.** `decline(id)` is ctrl+c at a question in turri's terminal: the question the chat asks now - its ask_user, a skill's interview, a review gate's escalation, whatever goes through the host's dialogs - declined. The asker is told "The user declined to answer the question." and the turn goes on; a review gate declined stands. It declines each question it is called at, however many in a row, and never stops the turn - that is `interrupt`. `none` when the chat asks nothing now, `missing` for a chat not open. A dialog that resolves with `undefined` is something else - dismissed, and in a question of several answers "done" with the boxes ticked so far.
115
115
  - **The record.** The chats are recorded in `.turri/chats.json` as the terminal records them, and `restore: true` reopens the last ended session's - without it, a host starts with none and deletes nothing. A chat closed is deleted; a session's chats are kept until five newer sessions have recorded theirs.
116
- - **The jobs.** The host is told of each job as it changes, and has the registry and the loop themselves, so it shows the very jobs the chats run. Its buttons are best a chat's commands - `session.prompt("/regenerate app.plain")` - so the plan is the chats'. A job's \*\*\*plain diff is `job.diff`, and `job.diffWhere` places its lines in the `.plain` files: by a line's place in the diff, its `file`, its `line` there, the `section` it is in and `written` - the line as the file has it. The diff is \*\*\*plain as read, a link `[text](path)` in it as `` `[resource]text` ``, so a host that finds the lines again in a file's text - after an edit since the pulse started - matches them by `written`, never by the diff's text. A `job.diff` in a ledger written before `written` was added has none.
116
+ - **The jobs.** The host is told of each job as it changes, and has the registry and the loop themselves, so it shows the very jobs the chats run. Its buttons are best a chat's commands - `session.prompt("/regenerate app.plain")` - so the plan is the chats'. A job's \*\*\*plain diff is `job.diff`, and `job.diffWhere` places its lines in the `.plain` files: by a line's place in the diff, its `file`, its `line` there, the `section` it is in and `written` - the line as the file has it. The diff is \*\*\*plain as read, a link `[text](path)` in it as `` `[resource]text` ``, so a host that finds the lines again in a file's text - after an edit since the pulse started - matches them by `written`, never by the diff's text. A `job.diff` in a ledger written before `written` was added has none. `job.cases` is the functional specs the pulse covers, as planned, by what it does to each - `{ add, patch, again, delete }`, each a list of `{ module, frid }`, `module` the pulse's own or one below it: new, changed, regenerated again with its text unchanged, taken out. A host lights those specs from it, and marks a line of the diff as regenerated again from `again` - never from the diff, where such a line is unchanged. A pulse of none is mechanical - reqs, or definitions none uses; a job that is not a pulse has no `cases`. The ledger's `job.queued` carries them too.
117
117
  - **The questions.** `dialogs(chat)` gives a chat's two dialogs: `select(title, options, context, at, { signal })` and `input(title, context, { signal })`, each resolving with the answer, or undefined when the person dismissed it. `signal` is aborted when the question is over without the dialog's answer - declined by `decline`, or its turn stopped (`interrupt`, `session.abort()`): the host closes the dialog it opened for it, whatever it resolves with then being ignored. A host may leave it unread. The chat's questions, a skill's interview and a review gate's escalation are put in them; without them, in the `uiContext`'s own dialogs.
118
118
  - **The surface.** What turri tells the model of where the person watches the work is the terminal's by default: regen, the side panel on `ctrl+h`, `/pulses`. In a host none of that is there, and the model would send the person to keys and panels that do not exist. `surface` is the host's own description of its UI - the views it has in place of the terminal's, what its marks and buttons mean, where questions appear - in its own words. Given, turri leaves every terminal instruction out of what the model is told and puts the surface in turri's identity, as a `<surface>` block, where the identity speaks of regen. It reaches the side questions and the tools' own words too: a pulse not found says the `pulses` tool lists them, not `/pulses`. Text only the person sees in the terminal - the job panel, the side panel - is the terminal's and stays as it is. Without `surface`, the prompt is the terminal's, byte for byte. What the conversation is to do once something is released belongs there too - open it in the host's preview, say what to test: with `sentinel: false`, after a pulse released under `release-after-pulse`, the conversation does what it was told to do once something is released. With the sentinel on, the sentinel tests the release, and the conversation only says it landed.
119
119
  - **The system prompt.** `systemPrompt(prompt)` gets the whole system prompt for the turn - [pi](https://pi.dev)'s, turri's identity and blocks, the rules - after turri's own `before_agent_start` work, and returns what is sent. It is for what `surface` does not cover; to describe the UI, use `surface`, rather than words appended after turri's that contradict them.
@@ -192,7 +192,7 @@ const { jobs, more } = earlierPulses(ledgerProjects(projectDir), new Set(turri.j
192
192
  ```
193
193
 
194
194
  - **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.
195
- - **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, and with its `diff` and `diffWhere` - the \*\*\*plain diff it regenerated, as last steered, and where its lines are written - as a pulse of this session has them (a pulse recorded before `job.diff` was has none); `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**.
195
+ - **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, and with its `cases`, `diff` and `diffWhere` - the \*\*\*plain diff it regenerated, as last steered, and where its lines are written - as a pulse of this session has them (a pulse recorded before `job.diff` was has none); `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**.
196
196
  - **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`.
197
197
 
198
198
  ## Running it
@@ -284,11 +284,11 @@ It is told, too, what to do with each functional spec the job touches, by its ca
284
284
  | fresh | the module requires none and nothing is built | set up the project and its unit tests where the unit testing script runs them, then build | set up `tests/` so the conformance testing script, which brings the tests together with `code/`, runs them, then write |
285
285
  | add | a functional spec is new | build it and write its unit tests | write its tests |
286
286
  | patch | its text changed, or a concept it uses did | change the existing code and tests to match - never build it again from scratch | rewrite its existing tests to match |
287
- | again | `regenerate` named it: it is built, and marked in the code record to be regenerated again | take its implementation out and build it anew, doing what \*\*\*plain says now - the old code already doing it is no reason to keep it; no second implementation beside the old, nothing of it left unused; its unit tests written anew. A coding agent that writes nothing is sent back twice to build it anew, and then the pulse does not land | its tests were written against the code it replaced: run against the new code, what they check that \*\*\*plain says stays checked, and what they check that \*\*\*plain leaves open - a path, a field, a message the old code chose - is rewritten to what the new code does, or taken out |
287
+ | again | `regenerate` named it: it is built, and marked in the code record to be regenerated again | take its implementation out and build it anew, doing what \*\*\*plain says now - the old code already doing it is no reason to keep it; no second implementation beside the old, nothing of it left unused; its unit tests written anew. A coding agent that writes nothing is sent back twice to build it anew, and then the pulse does not land | its tests were written against the code it replaced, and pass on what only the old code did as readily: written again for the new code - what they check that \*\*\*plain says stays checked, and what they check that \*\*\*plain leaves open - a path, a field, a message the old code chose - is rewritten to what the new code does, or taken out. A run that leaves every test file labelled with it as it was is sent back, three times at most, and then does not land |
288
288
  | delete | a functional spec is gone | take out its code and the tests only it had | take out its tests |
289
289
  | mechanical | no functional spec is touched - only reqs, or definitions none uses | adapt the code; write no new test; an existing one may change only mechanically - an import, a fixture, its setup - never what it asserts | the same, for test reqs, definitions or implementation reqs alone |
290
290
 
291
- **Regenerating again.** `regenerate` with functionalities named regenerates them again though their \*\*\*plain did not change - made better, on another model, after many patches. Each is marked in the code record of every codebase it is built in (`regenerate_again`, committed), and nothing else changes: the record now differs from \*\*\*plain at that item, so the work loop plans it as it plans any change. A functional spec of a module below is regenerated again where its code lives - the codebase at the tip of each chain built on it, never the module's own codebase left behind - marked among that codebase's required functional specs. The planner groups the ones regenerated again as it groups new ones - up to three a job, only those next to each other in one module together - and the job is given the definitions and implementation reqs as every job is; `<spec change>` lists each under "To regenerate again", with its text as it is now. The job that regenerates it records it as \*\*\*plain has it, without the mark. Its conformance tests are marked with it, in the tests record of the codebase whose own functional spec it is: they were written against the code being replaced, and may hold the new code to what only the old code chose - `/healthcheck` where \*\*\*plain says only that there is a health check, and the new code answers `/health`. While the code regenerates they stay as they are; once it lands, the mark is conformance work like any change, and a conformance job writes them again, in the again case above, and runs the whole suite. Until it lands, regen shows its tests as written for an earlier build, and they are not counted as conformance tested.
291
+ **Regenerating again.** `regenerate` with functionalities named regenerates them again though their \*\*\*plain did not change - made better, on another model, after many patches. Each is marked in the code record of every codebase it is built in (`regenerate_again`, committed), and nothing else changes: the record now differs from \*\*\*plain at that item, so the work loop plans it as it plans any change. A functional spec of a module below is regenerated again where its code lives - the codebase at the tip of each chain built on it, never the module's own codebase left behind - marked among that codebase's required functional specs. The planner groups the ones regenerated again as it groups new ones - up to three a job, only those next to each other in one module together - and the job is given the definitions and implementation reqs as every job is; `<spec change>` lists each under "To regenerate again", with its text as it is now. The job that regenerates it records it as \*\*\*plain has it, without the mark. Its conformance tests are marked with it, in the tests record of the codebase whose own functional spec it is: they were written against the code being replaced, and may hold the new code to what only the old code chose - `/healthcheck` where \*\*\*plain says only that there is a health check, and the new code answers `/health`. While the code regenerates they stay as they are; once it lands, the mark is conformance work like any change, and a conformance job writes them again, in the again case above, and runs the whole suite. That they pass is not taken for written again: the run lands only once a test file labelled with it was added or changed in it, and the tests record drops the mark only of a functional spec whose tests the run wrote. Until it lands, regen shows its tests as written for an earlier build, and they are not counted as conformance tested.
292
292
 
293
293
  A functional spec is patched when a concept it uses changed as well as when its own text did: the harness follows each functional spec's concepts through the definitions. Fresh is only where nothing exists: a module that requires another starts from what it inherits, and its first job adds or patches.
294
294
 
@@ -473,7 +473,7 @@ What goes into them:
473
473
  - **A code job landed:** the code record becomes the \*\*\*plain description the job ends on.
474
474
  - **A conformance job landed:** the tests record becomes what its tests are written for.
475
475
  - **A module started from another's code:** that code's record, as a lower module's - see [where things go](#where-things-go) - and an empty tests record: the tests it inherits are the module below's.
476
- - **A functional spec named to `regenerate`:** it is marked to be regenerated again in the code record, so the next job regenerates it again; that job's record drops the mark. Its conformance tests, when it has them, are marked in the tests record too, and the conformance job that writes them again for the new code drops that mark.
476
+ - **A functional spec named to `regenerate`:** it is marked to be regenerated again in the code record, so the next job regenerates it again; that job's record drops the mark. Its conformance tests, when it has them, are marked in the tests record too, and the conformance job that writes them again for the new code drops that mark - only once a test file labelled with it changed in the run, never because the old tests still pass.
477
477
 
478
478
  A unit of work - a code job, a conformance job - lands whole or is undone whole. It starts from the module's last commit, putting back whatever an interrupted run left; a module with no commit yet is given one first. When it does not pass, the module goes back to that commit: its changes, the files it added and any commit it made on the way are gone, with the record they came with. Installed dependencies and caches stay. So the code, its tests and the records of what they were built from never disagree.
479
479
 
package/docs/using.md CHANGED
@@ -178,7 +178,7 @@ It is in `.turri/ledger/` in the project, a JSON line an event, one file a run -
178
178
  | | |
179
179
  | --- | --- |
180
180
  | runs | `run.started` (how it was started, turri's version and commit, the machine), `run.ended` |
181
- | pulses | `job.queued`, `job.diff` (its \*\*\*plain diff, whole, and where its lines are written - again each time steering moves it), `job.started`, every `job.stage`, `job.retried`, `job.finished` - its status, whether it landed, how long it ran, what it cost, how it ended |
181
+ | pulses | `job.queued` (with the functional specs it covers, by case), `job.diff` (its \*\*\*plain diff, whole, and where its lines are written - again each time steering moves it), `job.started`, every `job.stage`, `job.retried`, `job.finished` - its status, whether it landed, how long it ran, what it cost, how it ended |
182
182
  | the work loop | `loop.halted`, `loop.stopped` (a pulse \*\*\*plain moved on from), `planner.failed`, `planner.fellBack` |
183
183
  | 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) |
184
184
  | 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) |
@@ -191,7 +191,7 @@ It is in `.turri/ledger/` in the project, a JSON line an event, one file a run -
191
191
 
192
192
  Never in it: prompts, replies, thinking, tool arguments and results, file contents - the one body it keeps is each pulse's \*\*\*plain diff, so what an earlier session regenerated can be read back. What an event does carry - a stage, an error's first line - is masked as the logs are.
193
193
 
194
- `turri ledger` prints the last runs, a line a pulse - runs and pulses newest first, as every list of the ledger's is, the events and a pulse's history and stages too; `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, and the \*\*\*plain diff it regenerated; `--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)).
194
+ `turri ledger` prints the last runs, a line a pulse - runs and pulses newest first, as every list of the ledger's is, the events and a pulse's history and stages too; `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, and the \*\*\*plain diff it regenerated; `--json` the events themselves, for `jq`. A pulse not ended is still running when it is of the run reading the ledger, and never ended - its run stopped while it ran - when it is another's. 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)).
195
195
 
196
196
  It is for looking up, never for deciding: what to build comes from the records alone (see [what the code is built from](regenerating.md#what-the-code-is-built-from)), so deleting `.turri/ledger/` loses nothing but what can be looked up. When a run first writes to the project, the files of runs that have ended are gzipped and the oldest pruned past `ledger-keep:` megabytes - 100 unset, `0` for none at all; a run still going is never touched. `.turri` ignores itself, so the ledger is never committed, and nothing of it is sent anywhere. A ledger that cannot be written is said once and given up on, never failing the work.
197
197
 
@@ -26,7 +26,7 @@ The other rules say what a spec means. This one says what turri does with a chan
26
26
  - A definition changed patches every functional spec that uses its concept, directly or through other definitions. So does a change to a linked resource: the spec or definition linking it is changed though its text is not. A widely used definition is the costly edit - not a spec's position
27
27
  - Implementation reqs alone, or definitions no functional spec uses, are a mechanical change: the code adapts, and no test is written or changes what it asserts
28
28
  - Test reqs and acceptance tests reach the conformance tests only, never the code
29
- - A functional spec regenerated again by name has its code taken out and built anew - never left as it was, even where it already does what the spec says; a pulse that changes nothing does not land. Its conformance tests are written again once its new code lands: what they check that the spec leaves open - a path, a field, a message the old code chose - goes by the new code
29
+ - A functional spec regenerated again by name has its code taken out and built anew - never left as it was, even where it already does what the spec says; a pulse that changes nothing does not land. Its conformance tests are written again once its new code lands: what they check that the spec leaves open - a path, a field, a message the old code chose - goes by the new code. Old tests that still pass are not enough: a conformance pulse that leaves them as they were does not land
30
30
  - Definitions moved to a common module and reqs reordered are recorded, not rendered
31
31
  - A functional spec written on its happy path and hardened later - its validation, edge cases and errors added to it - is patched in place each time: growing a spec a step at a time costs no more than writing it whole up front
32
32
  - A module's first pulse, when it builds one functional spec - the project set up and the smallest thing that runs - lands on its unit tests with no review
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turri-dev/cli",
3
- "version": "0.1.26",
3
+ "version": "0.1.27-next.763d091",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {