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

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 (216) hide show
  1. package/README.md +6 -6
  2. package/dist/advisor-chat.d.ts +63 -0
  3. package/dist/advisor-chat.js +290 -0
  4. package/dist/background-skills.d.ts +53 -0
  5. package/dist/background-skills.js +121 -0
  6. package/dist/bedrock-region.d.ts +66 -0
  7. package/dist/bedrock-region.js +144 -0
  8. package/dist/btw.d.ts +1 -1
  9. package/dist/btw.js +1 -1
  10. package/dist/change-plain-cli.js +20 -13
  11. package/dist/chat-idle.d.ts +7 -0
  12. package/dist/chat-idle.js +20 -0
  13. package/dist/chats.d.ts +10 -0
  14. package/dist/chats.js +23 -0
  15. package/dist/cli-options.d.ts +1 -2
  16. package/dist/cli-options.js +2 -7
  17. package/dist/cli.js +11 -3
  18. package/dist/config-command.d.ts +14 -0
  19. package/dist/config-command.js +35 -15
  20. package/dist/config-edit.d.ts +3 -3
  21. package/dist/config-edit.js +16 -12
  22. package/dist/config.d.ts +54 -34
  23. package/dist/config.js +40 -44
  24. package/dist/conform-cli.js +12 -10
  25. package/dist/conformance-switch.d.ts +34 -0
  26. package/dist/conformance-switch.js +108 -0
  27. package/dist/cultivate-cli.js +15 -5
  28. package/dist/drift-cli.js +2 -0
  29. package/dist/extension.d.ts +15 -0
  30. package/dist/extension.js +260 -83
  31. package/dist/host-session.d.ts +26 -0
  32. package/dist/host-session.js +30 -2
  33. package/dist/host.d.ts +5 -2
  34. package/dist/host.js +7 -1
  35. package/dist/init-config.d.ts +1 -1
  36. package/dist/init-config.js +20 -11
  37. package/dist/loop-cli.d.ts +3 -3
  38. package/dist/loop-cli.js +13 -16
  39. package/dist/loop-tools.js +18 -10
  40. package/dist/model-check.d.ts +8 -3
  41. package/dist/model-check.js +13 -8
  42. package/dist/new-project.js +12 -8
  43. package/dist/{validate-cli.js → parse-cli.js} +11 -5
  44. package/dist/parse-tool.d.ts +9 -0
  45. package/dist/parse-tool.js +33 -0
  46. package/dist/pi-launch.js +1 -1
  47. package/dist/plain/check.d.ts +53 -6
  48. package/dist/plain/check.js +217 -24
  49. package/dist/plain/concepts.d.ts +8 -0
  50. package/dist/plain/concepts.js +66 -23
  51. package/dist/plain/cycles.d.ts +21 -0
  52. package/dist/plain/cycles.js +190 -0
  53. package/dist/plain/errors.d.ts +4 -0
  54. package/dist/plain/errors.js +7 -0
  55. package/dist/plain/files.d.ts +18 -1
  56. package/dist/plain/files.js +39 -2
  57. package/dist/plain/graph.d.ts +33 -4
  58. package/dist/plain/graph.js +60 -12
  59. package/dist/plain/highlight.d.ts +3 -2
  60. package/dist/plain/highlight.js +4 -3
  61. package/dist/plain/index.d.ts +8 -5
  62. package/dist/plain/index.js +8 -5
  63. package/dist/plain/liquid.js +1 -6
  64. package/dist/plain/markdown.d.ts +32 -3
  65. package/dist/plain/markdown.js +146 -24
  66. package/dist/plain/module-concepts.js +2 -2
  67. package/dist/plain/parser.d.ts +83 -28
  68. package/dist/plain/parser.js +381 -260
  69. package/dist/plain/plain.tmLanguage.json +29 -3
  70. package/dist/plain/project.d.ts +48 -0
  71. package/dist/plain/project.js +155 -0
  72. package/dist/plain/spec.d.ts +90 -28
  73. package/dist/plain/spec.js +107 -133
  74. package/dist/plain/test-plain-file.d.ts +59 -0
  75. package/dist/plain/test-plain-file.js +232 -0
  76. package/dist/regenerate/activity.d.ts +20 -9
  77. package/dist/regenerate/activity.js +11 -4
  78. package/dist/regenerate/advisor-records.d.ts +93 -0
  79. package/dist/regenerate/advisor-records.js +110 -0
  80. package/dist/regenerate/advisor-runs.d.ts +87 -0
  81. package/dist/regenerate/advisor-runs.js +327 -0
  82. package/dist/regenerate/base-folder.d.ts +68 -0
  83. package/dist/regenerate/base-folder.js +168 -0
  84. package/dist/regenerate/cases.d.ts +9 -6
  85. package/dist/regenerate/cases.js +71 -15
  86. package/dist/regenerate/change-view.d.ts +8 -3
  87. package/dist/regenerate/change-view.js +71 -22
  88. package/dist/regenerate/chunk-plan.d.ts +36 -15
  89. package/dist/regenerate/chunk-plan.js +165 -59
  90. package/dist/regenerate/conform.d.ts +3 -0
  91. package/dist/regenerate/conform.js +6 -5
  92. package/dist/regenerate/conformance-agents.d.ts +18 -39
  93. package/dist/regenerate/conformance-agents.js +60 -88
  94. package/dist/regenerate/conformance.d.ts +23 -20
  95. package/dist/regenerate/conformance.js +118 -121
  96. package/dist/regenerate/diff-view.d.ts +1 -1
  97. package/dist/regenerate/diff-view.js +20 -4
  98. package/dist/regenerate/drift.d.ts +47 -15
  99. package/dist/regenerate/drift.js +101 -30
  100. package/dist/regenerate/jobs.d.ts +2 -2
  101. package/dist/regenerate/jobs.js +1 -1
  102. package/dist/regenerate/ledger.d.ts +19 -0
  103. package/dist/regenerate/loop.d.ts +61 -49
  104. package/dist/regenerate/loop.js +473 -340
  105. package/dist/regenerate/paths.d.ts +4 -0
  106. package/dist/regenerate/paths.js +7 -1
  107. package/dist/regenerate/plain-palette.js +2 -0
  108. package/dist/regenerate/prompt.d.ts +57 -19
  109. package/dist/regenerate/prompt.js +157 -122
  110. package/dist/regenerate/records.d.ts +43 -12
  111. package/dist/regenerate/records.js +102 -88
  112. package/dist/regenerate/regenerate.js +29 -11
  113. package/dist/regenerate/regression.d.ts +4 -4
  114. package/dist/regenerate/regression.js +14 -10
  115. package/dist/regenerate/renames.d.ts +42 -0
  116. package/dist/regenerate/renames.js +185 -0
  117. package/dist/regenerate/requires.d.ts +1 -1
  118. package/dist/regenerate/requires.js +10 -3
  119. package/dist/regenerate/resources.d.ts +5 -3
  120. package/dist/regenerate/resources.js +4 -4
  121. package/dist/regenerate/scripts-being-written.d.ts +19 -0
  122. package/dist/regenerate/scripts-being-written.js +30 -0
  123. package/dist/regenerate/spec-map-view.d.ts +2 -2
  124. package/dist/regenerate/spec-map-view.js +46 -15
  125. package/dist/regenerate/spec-map.d.ts +27 -5
  126. package/dist/regenerate/spec-map.js +92 -60
  127. package/dist/regenerate/spec-steps.d.ts +48 -5
  128. package/dist/regenerate/spec-steps.js +213 -28
  129. package/dist/regenerate/state.d.ts +4 -3
  130. package/dist/regenerate/stop-report.d.ts +1 -1
  131. package/dist/regenerate/stop-report.js +10 -10
  132. package/dist/regenerate/test-advisor.d.ts +101 -0
  133. package/dist/regenerate/test-advisor.js +379 -0
  134. package/dist/regenerate/test-kinds.d.ts +69 -31
  135. package/dist/regenerate/test-kinds.js +109 -80
  136. package/dist/regenerate/test-scripts.d.ts +2 -0
  137. package/dist/regenerate/test-scripts.js +4 -0
  138. package/dist/regenerate/testing-steps.d.ts +41 -0
  139. package/dist/regenerate/testing-steps.js +110 -0
  140. package/dist/regenerate/testing.d.ts +1 -1
  141. package/dist/regenerate/testing.js +5 -4
  142. package/dist/regenerate/unit-review.d.ts +1 -1
  143. package/dist/regenerate/unit-review.js +3 -3
  144. package/dist/regenerate-cli.js +11 -12
  145. package/dist/release-agent.js +4 -0
  146. package/dist/release-cli.js +2 -0
  147. package/dist/release-tools.js +2 -0
  148. package/dist/run-report.d.ts +2 -2
  149. package/dist/run-report.js +4 -3
  150. package/dist/self-invocation.d.ts +1 -1
  151. package/dist/self-invocation.js +1 -1
  152. package/dist/sentinel.js +1 -1
  153. package/dist/setup.d.ts +10 -0
  154. package/dist/setup.js +52 -4
  155. package/dist/skill.d.ts +10 -0
  156. package/dist/skill.js +6 -0
  157. package/dist/skills.js +3 -1
  158. package/dist/spec-tools.js +21 -31
  159. package/dist/test-plain-files.d.ts +41 -0
  160. package/dist/test-plain-files.js +177 -0
  161. package/docs/README.md +3 -1
  162. package/docs/authoring-plain.md +46 -13
  163. package/docs/chats.md +3 -1
  164. package/docs/config.md +20 -15
  165. package/docs/deploy.md +1 -1
  166. package/docs/development.md +2 -1
  167. package/docs/headless.md +15 -18
  168. package/docs/hosting.md +28 -12
  169. package/docs/installation.md +1 -0
  170. package/docs/parser.md +88 -27
  171. package/docs/regenerating.md +89 -54
  172. package/docs/sentinel.md +1 -1
  173. package/docs/skills-and-gates.md +3 -1
  174. package/docs/test-advisor.md +110 -0
  175. package/docs/theming.md +1 -1
  176. package/docs/using.md +17 -15
  177. package/genome/rules/bullet-continuation.md +8 -8
  178. package/genome/rules/concise-specs-examples.md +111 -80
  179. package/genome/rules/concise-specs-integration-examples.md +56 -41
  180. package/genome/rules/concise-specs.md +17 -16
  181. package/genome/rules/definitions.md +5 -5
  182. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
  183. package/genome/rules/func-specs.md +69 -74
  184. package/genome/rules/impl-reqs.md +4 -4
  185. package/genome/rules/integration-embedded-testing.md +20 -20
  186. package/genome/rules/integration-embedded.md +31 -31
  187. package/genome/rules/integration-standalone.md +9 -9
  188. package/genome/rules/integrations.md +18 -18
  189. package/genome/rules/linked-resources.md +19 -16
  190. package/genome/rules/module-structure.md +21 -14
  191. package/genome/rules/project-model.md +18 -17
  192. package/genome/rules/required-concepts.md +1 -1
  193. package/genome/rules/requires-modules.md +6 -5
  194. package/genome/rules/semantic-ids.md +49 -0
  195. package/genome/rules/stdlib.md +4 -5
  196. package/genome/rules/test-plain-files.md +130 -0
  197. package/genome/rules/test-reqs.md +8 -8
  198. package/genome/rules/turri-regeneration.md +24 -8
  199. package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
  200. package/genome/skills/analyze-func-specs/SKILL.md +46 -32
  201. package/genome/skills/break-down-func-spec/SKILL.md +91 -67
  202. package/genome/skills/consolidate-concepts/SKILL.md +10 -10
  203. package/genome/skills/convert-old-syntax/SKILL.md +68 -0
  204. package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
  205. package/genome/skills/debug-specs/SKILL.md +62 -56
  206. package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
  207. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
  208. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
  209. package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
  210. package/genome/skills/refactor-module/SKILL.md +35 -35
  211. package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
  212. package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
  213. package/package.json +7 -6
  214. package/dist/regenerate/choose-conformance.d.ts +0 -32
  215. package/dist/regenerate/choose-conformance.js +0 -117
  216. /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
@@ -13,7 +13,7 @@ import { firstRegeneration } from "./regenerate/jobs.js";
13
13
  * pulse failed, a crash, a signal. A process killed outright (SIGKILL) leaves the one written as the last pulse ended.
14
14
  * Each write is atomic: the file is written beside it and renamed over it, so a reader never sees half of one.
15
15
  */
16
- export const REPORT_SCHEMA_VERSION = 1;
16
+ export const REPORT_SCHEMA_VERSION = 2;
17
17
  const SIGNALS = { 129: "SIGHUP", 130: "SIGINT", 143: "SIGTERM" };
18
18
  const emptyTotals = () => ({
19
19
  input_tokens: 0,
@@ -229,13 +229,15 @@ export function startRunReport(options) {
229
229
  const modules = counts.flatMap((chain) => chain.modules.map((module) => ({
230
230
  module: module.module,
231
231
  specified: module.specified,
232
+ with_scenarios: module.withScenarios,
232
233
  completed: module.completed,
233
234
  conformance_tested: module.conformanceTested,
234
- incomplete_frids: module.incomplete,
235
+ incomplete_func_ids: module.incomplete,
235
236
  })));
236
237
  return {
237
238
  total: {
238
239
  specified: modules.reduce((sum, module) => sum + module.specified, 0),
240
+ with_scenarios: modules.reduce((sum, module) => sum + module.with_scenarios, 0),
239
241
  completed: modules.reduce((sum, module) => sum + module.completed, 0),
240
242
  conformance_tested: modules.reduce((sum, module) => sum + module.conformance_tested, 0),
241
243
  },
@@ -268,7 +270,6 @@ export function startRunReport(options) {
268
270
  spec_file: resolve(options.specFile),
269
271
  project_dir: projectRoot,
270
272
  build_dir: options.buildDir !== undefined ? resolve(options.buildDir) : null,
271
- conformance_tests: options.conformanceTests,
272
273
  review: options.review ?? null,
273
274
  mutation_testing: options.mutationTesting ?? null,
274
275
  models: {
@@ -10,7 +10,7 @@
10
10
  */
11
11
  /**
12
12
  * What a shell command would run of turri or pi itself, if anything: the subcommand,
13
- * or "pi" for another pi. `turri validate`, `drift` and `lineage` are not in it - they start no
13
+ * or "pi" for another pi. `turri parse`, `drift` and `lineage` are not in it - they start no
14
14
  * agent - and neither are setup and doctor.
15
15
  */
16
16
  export declare function selfInvocation(command: string): string | undefined;
@@ -39,7 +39,7 @@ const ENTRY = {
39
39
  const AT_COMMAND = String.raw `(?:^|[\s;&|()\x60$])`;
40
40
  /**
41
41
  * What a shell command would run of turri or pi itself, if anything: the subcommand,
42
- * or "pi" for another pi. `turri validate`, `drift` and `lineage` are not in it - they start no
42
+ * or "pi" for another pi. `turri parse`, `drift` and `lineage` are not in it - they start no
43
43
  * agent - and neither are setup and doctor.
44
44
  */
45
45
  export function selfInvocation(command) {
package/dist/sentinel.js CHANGED
@@ -244,7 +244,7 @@ export function sentinelBrief(job, context) {
244
244
  // ---------------------------------------------------------------------------------------------
245
245
  // Its tools.
246
246
  /** The tools the sentinel's agent may call: what reads, its notices and its guidance - and what a host gives it. */
247
- export const SENTINEL_TOOLS = ["read", "ls", "grep", "find", "bash", "read_output", "ledger", "pulses", "diagnose", "validate", "doctor", "web_search", "web_fetch", "notify_person", "conclude", "keep_guidance"];
247
+ export const SENTINEL_TOOLS = ["read", "ls", "grep", "find", "bash", "read_output", "ledger", "pulses", "diagnose", "parse", "doctor", "web_search", "web_fetch", "notify_person", "conclude", "keep_guidance"];
248
248
  /**
249
249
  * Its file tools and shell: the whole project to read and nothing to write - the shell behind the command check, the OS
250
250
  * sandbox where one is up, and the audit, with the network open. Its home and temporary folder are its own, in the
package/dist/setup.d.ts CHANGED
@@ -55,6 +55,16 @@ export interface DoctorOptions {
55
55
  * nothing, so a host can show them in its own way; `turri doctor` prints the same.
56
56
  */
57
57
  export declare function doctor(options?: DoctorOptions): Promise<Check[]>;
58
+ /**
59
+ * The base folders the project's config names - its own, and each stack's - each there with its files, or not: a codebase
60
+ * that requires nothing starts from one, and its first pulse stops when it is not there. Nothing when none is named.
61
+ */
62
+ export declare function checkBaseFolders(project: string): Check[];
63
+ /**
64
+ * The AWS region a project's Bedrock models are called in, when it uses any: turri's, the person's or the profile's - and
65
+ * what is wrong with it, when the models need two, or the person's is not in their geography. Nothing when it uses none.
66
+ */
67
+ export declare function checkBedrockRegion(project: string, env?: NodeJS.ProcessEnv): Check[];
58
68
  /** What the checks add up to: whether turri can run here, and a line saying so - with its marks, uncoloured. */
59
69
  export declare function verdictOf(checks: Check[]): {
60
70
  ready: boolean;
package/dist/setup.js CHANGED
@@ -1,14 +1,16 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync, rmSync } from "node:fs";
3
- import { join, relative } from "node:path";
2
+ import { existsSync, rmSync, statSync } from "node:fs";
3
+ import { join, relative, resolve } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
6
6
  import { globalExtensionFile, MIN_NODE, nodeVersionOk, PACKAGE_ROOT, turriOnPath } from "./environment.js";
7
7
  import { sandboxAvailable, sandboxWith } from "./regenerate/os-sandbox.js";
8
8
  import { findTestScripts, onThisMachine } from "./regenerate/test-scripts.js";
9
9
  import { configKeyOptions, parseArgs } from "./cli-options.js";
10
- import { hasProjectConfig, readProjectConfig, stackOf } from "./config.js";
10
+ import { CONFIG_FILE, hasProjectConfig, readProjectConfig, stackConfigFile, stackOf, stacksOf } from "./config.js";
11
11
  import { checkModels, modelsInUse, piModelAccess } from "./model-check.js";
12
+ import { bedrockRegionFor, describeBedrockRegion, regionInGeography } from "./bedrock-region.js";
13
+ import { baseFiles } from "./regenerate/base-folder.js";
12
14
  import { brand } from "./brand.js";
13
15
  import { readScript, scriptAgainstMachine, stackOnMachine } from "./stack.js";
14
16
  const MARK = { ok: "✓", missing: "✗", limited: "!", info: "·" };
@@ -156,7 +158,8 @@ export async function checkProjectModels(project, access) {
156
158
  }
157
159
  let verdicts;
158
160
  try {
159
- verdicts = await checkModels(used, access ?? (await piModelAccess()));
161
+ const region = describeBedrockRegion(bedrockRegionFor(project));
162
+ verdicts = await checkModels(used, access ?? (await piModelAccess()), region !== undefined ? { bedrockRegion: region } : {});
160
163
  }
161
164
  catch (error) {
162
165
  return [{ level: "missing", name: "Models", detail: `pi's models could not be read: ${error instanceof Error ? error.message : String(error)}` }];
@@ -194,10 +197,55 @@ async function allChecks(options) {
194
197
  return checks;
195
198
  if (hasProjectConfig(project) || existsSync(join(project, "plain_modules")))
196
199
  checks.push(...checkProject(project, { has: (command) => onThisMachine(command, env) !== undefined }));
200
+ if (hasProjectConfig(project))
201
+ checks.push(...checkBedrockRegion(project, env), ...checkBaseFolders(project));
197
202
  if (asksModels(options))
198
203
  checks.push(...(await checkProjectModels(project)));
199
204
  return checks;
200
205
  }
206
+ /**
207
+ * The base folders the project's config names - its own, and each stack's - each there with its files, or not: a codebase
208
+ * that requires nothing starts from one, and its first pulse stops when it is not there. Nothing when none is named.
209
+ */
210
+ export function checkBaseFolders(project) {
211
+ const named = new Map();
212
+ for (const [stack, config] of [[undefined, readProjectConfig(project)], ...stacksOf(project).map((stack) => [stack, readProjectConfig(project, stack)])]) {
213
+ if (config.baseFolder === undefined)
214
+ continue;
215
+ named.set(config.baseFolder, [...(named.get(config.baseFolder) ?? []), stack === undefined ? CONFIG_FILE : stackConfigFile(stack)]);
216
+ }
217
+ return [...named].map(([folder, files]) => {
218
+ const path = resolve(project, folder);
219
+ const name = `base folder ${folder}`;
220
+ if (!existsSync(path) || !statSync(path).isDirectory()) {
221
+ return { level: "missing", name, detail: `${existsSync(path) ? "not a folder" : "not there"} - named in ${files.join(", ")}`, fix: "make the folder, or name the right one in base-folder:, by path from the project's folder" };
222
+ }
223
+ const count = baseFiles(path).length;
224
+ return { level: "ok", name, detail: `${count} file${count === 1 ? "" : "s"} to start a codebase from - named in ${files.join(", ")}` };
225
+ });
226
+ }
227
+ /**
228
+ * The AWS region a project's Bedrock models are called in, when it uses any: turri's, the person's or the profile's - and
229
+ * what is wrong with it, when the models need two, or the person's is not in their geography. Nothing when it uses none.
230
+ */
231
+ export function checkBedrockRegion(project, env = process.env) {
232
+ let decided;
233
+ try {
234
+ decided = bedrockRegionFor(project, { env });
235
+ }
236
+ catch {
237
+ return [];
238
+ }
239
+ const line = describeBedrockRegion(decided);
240
+ if (decided.models.length === 0 || line === undefined)
241
+ return [];
242
+ const outside = decided.source === "set" && decided.needs !== undefined && decided.region !== undefined && !regionInGeography(decided.region, decided.needs.geography);
243
+ if (decided.conflict !== undefined)
244
+ return [{ level: "missing", name: "AWS region", detail: "the Bedrock models need two regions", more: [line] }];
245
+ if (outside)
246
+ return [{ level: "missing", name: "AWS region", detail: `${decided.region}, which does not serve ${decided.needs.geography}'s models`, more: [line] }];
247
+ return [{ level: "ok", name: "AWS region", detail: line }];
248
+ }
201
249
  /** What the checks add up to: whether turri can run here, and a line saying so - with its marks, uncoloured. */
202
250
  export function verdictOf(checks) {
203
251
  const missing = checks.filter((check) => check.level === "missing").length;
package/dist/skill.d.ts CHANGED
@@ -90,6 +90,16 @@ export interface Skill {
90
90
  * (`metadata.command-only: true`): cultivate runs it only when the person has just typed its command.
91
91
  */
92
92
  commandOnly: boolean;
93
+ /**
94
+ * Run in the background (`metadata.background: true`): in the chat, cultivate starts it and returns at once, and what
95
+ * it did comes back to the chat once the chat is idle. Nobody is asked anything while it runs.
96
+ */
97
+ background: boolean;
98
+ /**
99
+ * The model it runs on (`metadata.model`): `coding`, the coding model of the module it is for. Omitted: the chat's
100
+ * model, or `harness-model` from the command line.
101
+ */
102
+ model?: "coding";
93
103
  }
94
104
  export declare function loadSkill(skillDir: string): Skill;
95
105
  export declare function readRubric(skill: Skill, review: {
package/dist/skill.js CHANGED
@@ -121,6 +121,10 @@ export function loadSkill(skillDir) {
121
121
  throw new Error(`${name}: gate references unknown step '${stepName}'`);
122
122
  }
123
123
  }
124
+ const rawModel = data.metadata?.model;
125
+ if (rawModel !== undefined && rawModel !== "coding")
126
+ throw new Error(`${name}: 'metadata.model' is coding, or left out for the chat's model`);
127
+ const model = rawModel === "coding" ? "coding" : undefined;
124
128
  return {
125
129
  name,
126
130
  description,
@@ -132,6 +136,8 @@ export function loadSkill(skillDir) {
132
136
  questioning: capped,
133
137
  diffPerAnswer,
134
138
  commandOnly: data.metadata?.["command-only"] === true,
139
+ background: data.metadata?.background === true,
140
+ ...(model !== undefined ? { model } : {}),
135
141
  };
136
142
  }
137
143
  export function readRubric(skill, review) {
package/dist/skills.js CHANGED
@@ -63,7 +63,9 @@ export function catalogueLine(skill) {
63
63
  const whose = skill.origin === "turri" ? "" : ` (this project's, ${skill.origin.project})`;
64
64
  // One the person starts themselves - typed, or picked among the options the model offers: never run unasked.
65
65
  const byCommand = skill.commandOnly ? ` Started only by the person, with /${skill.name} or by picking it when you offer it with ask_user: never run it unasked.` : "";
66
- return `- ${skill.name}${whose}: ${skill.description}${byCommand}`;
66
+ // One that runs in the background: the chat goes on, and its end comes back to it.
67
+ const background = skill.background ? " Runs in the background: cultivate returns at once, and its end comes back to this chat - pass plainFile, the module it is for." : "";
68
+ return `- ${skill.name}${whose}: ${skill.description}${byCommand}${background}`;
67
69
  }
68
70
  /** What is said under the list about the skills it leaves out. */
69
71
  export function leftOutNote(catalogue) {
@@ -1,11 +1,10 @@
1
- import { basename, relative, resolve } from "node:path";
1
+ import { resolve } from "node:path";
2
2
  import { defineTool } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
- import { plainFileParser } from "./plain/parser.js";
5
- import { importingModules, isImportModule } from "./regenerate/requires.js";
6
- import { resolveTemplateDirs } from "./regenerate/paths.js";
4
+ import { advisingNote } from "./advisor-chat.js";
5
+ import { checkTouchedPlain, formatCheckResults } from "./plain/check.js";
7
6
  import { applySpecDiff, plainFilesOf, revertSpecDiff } from "./regenerate/regenerate.js";
8
- import { MissingFunctionalitiesError } from "./plain/errors.js";
7
+ import { refuseTestPlainFile } from "./test-plain-files.js";
9
8
  import { describeModules } from "./regenerate/loop.js";
10
9
  import { reviewNote } from "./tools.js";
11
10
  import { STACK_PARAM, stacklessBuild, withCallStack } from "./loop-tools.js";
@@ -33,10 +32,13 @@ export function createSpecDiffTools(options) {
33
32
  description: "Apply a change to the ***plain specs, given as a git diff of the .plain files, and start building it in " +
34
33
  "the background. The diff is applied with git; if the specs no longer parse, it is undone and nothing is " +
35
34
  "built. Any section may change - definitions, implementation reqs, functional specs, added, changed or removed - and " +
36
- "every change is built, whatever it touches.",
35
+ "every change is built, whatever it touches. A diff may touch test plain files too - tests/<module>/<func-id>.plain, " +
36
+ "a functional spec's conformance and acceptance scenarios. A functional spec with none has no test plain file: " +
37
+ "write one only to give it scenarios.",
37
38
  promptSnippet: "apply a git diff of the .plain specs and start building it",
38
39
  promptGuidelines: [
39
40
  "Every change to a .plain file in the product phase goes through change_plain, as a git diff with paths relative to the project root. It writes the change and starts its build in one step.",
41
+ "A functional spec is a `# <func-id>` heading and its text. One with scenarios has a test plain file, tests/<module>/<func-id>.plain: its `module:` and `func_id:`, then `***conformance tests***` and `***acceptance tests***`, each scenario a `# <scenario-id>` heading and its text - a section only when it has a scenario, an empty one being a syntax error. One with no scenarios has no test plain file. plain_file stays the module's .plain file when the diff touches only its test plain files.",
40
42
  skillPaced
41
43
  ? "One change per answer, and a new one only after the user has answered a question about it. Set fixes_earlier to correct something already written."
42
44
  : "Write the changes one after another, on your own: no answer is needed between them, and none is asked for. Set fixes_earlier to correct something already written.",
@@ -62,31 +64,17 @@ export function createSpecDiffTools(options) {
62
64
  const touched = plainFilesOf(params.diff);
63
65
  if (touched.length === 0)
64
66
  throw new Error("The diff changes no .plain file. Nothing was written.");
67
+ refuseTestPlainFile(resolve(cwd, params.plain_file));
65
68
  const result = applySpecDiff(params.diff, cwd);
66
- // A change that breaks a spec is not written at all, so nothing is ever built from a
67
- // file that cannot be parsed. A module with no functional specs yet is not broken. An import
68
- // module - definitions only, say - is not a module of its own: it is parsed as the modules
69
- // that import it read it, the one the change is built for among them.
70
- const toParse = [
71
- ...new Set([params.plain_file, ...touched].flatMap((file) => {
72
- const path = resolve(cwd, file);
73
- const importing = isImportModule(path) ? importingModules(path) : [];
74
- return importing.length > 0 ? importing : [path];
75
- })),
76
- ];
77
- for (const path of toParse) {
78
- const file = relative(cwd, path);
79
- try {
80
- plainFileParser(basename(path), resolveTemplateDirs(path), { allowNoFunctionalities: true });
81
- }
82
- catch (error) {
83
- if (error instanceof MissingFunctionalitiesError)
84
- continue;
85
- if (result === "applied")
86
- revertSpecDiff(params.diff, cwd);
87
- const reason = error instanceof Error ? error.message : String(error);
88
- throw new Error(`Nothing was written: with this change ${file} no longer parses. ${reason}`);
89
- }
69
+ // A change that breaks a spec is not written at all, so nothing is ever built from a file that cannot be parsed:
70
+ // each module it touched is parsed with its test plain files - an import module as the modules that import it
71
+ // read it, a test plain file as its module - as parse reads them. A module with no functional specs yet is
72
+ // not broken.
73
+ const failed = checkTouchedPlain([params.plain_file, ...touched].map((file) => resolve(cwd, file))).filter((check) => !check.ok);
74
+ if (failed.length > 0) {
75
+ if (result === "applied")
76
+ revertSpecDiff(params.diff, cwd);
77
+ throw new Error(`Nothing was written: with this change the ***plain no longer checks.\n${formatCheckResults(failed)}`);
90
78
  }
91
79
  if (params.fixes_earlier !== true)
92
80
  answeredSinceLastDiff = false;
@@ -118,8 +106,10 @@ export function createSpecDiffTools(options) {
118
106
  ? "Carry on: write the next change you decided yourself, or ask the next question - every answer is written as a change before the question after it."
119
107
  : "Write the next change now, without stopping to ask: this skill goes on until everything it is to write is written.",
120
108
  ];
109
+ // The functional specs the test advisor took up, for the agent to say so rather than offer their scenarios.
110
+ const advising = advisingNote(loop, building);
121
111
  return {
122
- content: [{ type: "text", text: lines.join(" ") + reviewNote(liveReview) }],
112
+ content: [{ type: "text", text: lines.join(" ") + reviewNote(liveReview) + (advising !== "" ? `\n\n${advising}` : "") }],
123
113
  details: { jobIds: statuses.flatMap((status) => (status.running !== undefined ? [status.running.jobId] : [])), files: touched },
124
114
  };
125
115
  },
@@ -0,0 +1,41 @@
1
+ /** A module's test plain files: every .plain file under `<test-dir>/<module>/`, sorted, as absolute paths. */
2
+ export declare function testPlainFilesOf(plainFile: string): string[];
3
+ /** The modules of a project, by name: the .plain files in its folder and its `template/` folder. */
4
+ export declare function projectModules(projectDir: string): Map<string, string>;
5
+ /** What a path under a project's test folder is: the test folder itself, or a module's folder or file in it. */
6
+ export interface TestPlace {
7
+ projectDir: string;
8
+ testDir: string;
9
+ /** The module whose folder it is in - `tests/<module>/...` - when it is in one. */
10
+ folderModule?: string;
11
+ /** For a file: the `module:` and `func_id:` it names. */
12
+ module?: string;
13
+ funcId?: string;
14
+ directory: boolean;
15
+ }
16
+ /**
17
+ * Where a path is, when it is a test plain file - under the project's test folder, or a .plain file whose frontmatter
18
+ * binds it with `module:` and `func_id:` - or the test folder, or a folder in it. Undefined for anything else.
19
+ */
20
+ export declare function testPlaceOf(path: string): TestPlace | undefined;
21
+ /**
22
+ * Why a tool refuses a path in place of a module: it is a test plain file - naming its module and the functional
23
+ * spec it binds to - or a test folder. Undefined for a path that is not one.
24
+ */
25
+ export declare function testPlainFileRefusal(path: string): string | undefined;
26
+ /** Refuse a path that is a test plain file or a test folder, as every tool that takes a module does. */
27
+ export declare function refuseTestPlainFile(path: string): void;
28
+ /**
29
+ * The test plain files of a project that are not where they belong, each with where it does: a file outside any
30
+ * module's folder, in the folder of a module there is not, or in a module's folder its `module:` does not name. Strays
31
+ * are not read when a module is parsed; this is where they are found.
32
+ */
33
+ export declare function strayTestPlainFiles(projectDir: string): Array<{
34
+ file: string;
35
+ problem: string;
36
+ }>;
37
+ /** The module a test plain file belongs to, by its folder, and that module's .plain file when the project has one. */
38
+ export declare function moduleOfTestPlainFile(path: string): {
39
+ module: string;
40
+ plainFile: string | undefined;
41
+ } | undefined;
@@ -0,0 +1,177 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
3
+ import { DEFAULT_TEST_DIR, testDirOf } from "./config.js";
4
+ import { PLAIN_SOURCE_FILE_EXTENSION } from "./plain/parser.js";
5
+ import { FUNC_ID_KEY, loadFrontmatter, MODULE_KEY } from "./plain/test-plain-file.js";
6
+ import { isProjectDir, projectDirOf } from "./regenerate/paths.js";
7
+ /**
8
+ * Where a project's test plain files are, and what each belongs to: `<test-dir>/<module>/`, every `.plain` file in it
9
+ * a test plain file of that module - its functional specs' conformance and acceptance scenarios. turri finds modules
10
+ * only in the project's folder and its `template/` folder, so a test plain file is never taken for a module; and no
11
+ * tool takes one in place of its module.
12
+ */
13
+ /** Every .plain file under a folder, at any depth, sorted by path; none when it is not there. */
14
+ function plainFilesUnder(dir) {
15
+ let names;
16
+ try {
17
+ names = readdirSync(dir).sort();
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ const found = [];
23
+ for (const name of names) {
24
+ const path = join(dir, name);
25
+ let directory = false;
26
+ try {
27
+ directory = statSync(path).isDirectory();
28
+ }
29
+ catch {
30
+ continue;
31
+ }
32
+ if (directory)
33
+ found.push(...plainFilesUnder(path));
34
+ else if (name.endsWith(PLAIN_SOURCE_FILE_EXTENSION))
35
+ found.push(path);
36
+ }
37
+ return found;
38
+ }
39
+ /** A module's test plain files: every .plain file under `<test-dir>/<module>/`, sorted, as absolute paths. */
40
+ export function testPlainFilesOf(plainFile) {
41
+ return plainFilesUnder(join(testDirOf(plainFile), basename(plainFile, PLAIN_SOURCE_FILE_EXTENSION)));
42
+ }
43
+ /** Whether a path is a folder, or below one. */
44
+ function within(path, folder) {
45
+ const from = relative(folder, path);
46
+ return from === "" || (!from.startsWith("..") && !from.startsWith(sep) && from !== "..");
47
+ }
48
+ /** The frontmatter of a .plain file, as a test plain file's is read; empty when it cannot be read. */
49
+ function frontmatterOf(path) {
50
+ try {
51
+ return loadFrontmatter(readFileSync(path, "utf8").split("\r\n").join("\n")).metadata;
52
+ }
53
+ catch {
54
+ return {};
55
+ }
56
+ }
57
+ /** The modules of a project, by name: the .plain files in its folder and its `template/` folder. */
58
+ export function projectModules(projectDir) {
59
+ const modules = new Map();
60
+ for (const dir of [join(projectDir, "template"), projectDir]) {
61
+ let names = [];
62
+ try {
63
+ names = readdirSync(dir).filter((name) => name.endsWith(PLAIN_SOURCE_FILE_EXTENSION));
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ for (const name of names)
69
+ modules.set(basename(name, PLAIN_SOURCE_FILE_EXTENSION), join(dir, name));
70
+ }
71
+ return modules;
72
+ }
73
+ /**
74
+ * Where a path is, when it is a test plain file - under the project's test folder, or a .plain file whose frontmatter
75
+ * binds it with `module:` and `func_id:` - or the test folder, or a folder in it. Undefined for anything else.
76
+ */
77
+ export function testPlaceOf(path) {
78
+ const absolute = resolve(path);
79
+ const directory = existsSync(absolute) && statSync(absolute).isDirectory();
80
+ const projectDir = projectOfTestPath(absolute, directory);
81
+ const testDir = testDirOf(join(projectDir, "x.plain"));
82
+ const metadata = directory || !existsSync(absolute) ? {} : frontmatterOf(absolute);
83
+ const bound = typeof metadata[MODULE_KEY] === "string" && typeof metadata[FUNC_ID_KEY] === "string";
84
+ if (!within(absolute, testDir) && !bound)
85
+ return undefined;
86
+ const parts = within(absolute, testDir) ? relative(testDir, absolute).split(sep).filter(Boolean) : [];
87
+ const folderModule = directory ? parts[0] : parts.length > 1 ? parts[0] : undefined;
88
+ return {
89
+ projectDir,
90
+ testDir,
91
+ directory,
92
+ ...(folderModule !== undefined ? { folderModule } : {}),
93
+ ...(bound ? { module: metadata[MODULE_KEY], funcId: metadata[FUNC_ID_KEY] } : {}),
94
+ };
95
+ }
96
+ /**
97
+ * The project a path in a test folder belongs to. One with no config yet has nothing above the path to say so: its
98
+ * test folder is `tests`, and the project the folder that holds it.
99
+ */
100
+ function projectOfTestPath(absolute, directory) {
101
+ const found = projectDirOf(directory ? join(absolute, "x.plain") : absolute);
102
+ if (isProjectDir(found))
103
+ return found;
104
+ for (let dir = directory ? absolute : dirname(absolute); dirname(dir) !== dir; dir = dirname(dir)) {
105
+ if (basename(dir) === DEFAULT_TEST_DIR)
106
+ return dirname(dir);
107
+ }
108
+ return found;
109
+ }
110
+ /** A path as a person reads it in the project: from its folder. */
111
+ function shown(path, projectDir) {
112
+ return relative(projectDir, resolve(path)).split(sep).join("/");
113
+ }
114
+ /**
115
+ * Why a tool refuses a path in place of a module: it is a test plain file - naming its module and the functional
116
+ * spec it binds to - or a test folder. Undefined for a path that is not one.
117
+ */
118
+ export function testPlainFileRefusal(path) {
119
+ const place = testPlaceOf(path);
120
+ if (place === undefined)
121
+ return undefined;
122
+ const given = shown(path, place.projectDir) + (place.directory ? "/" : "");
123
+ const module = place.module ?? place.folderModule;
124
+ if (place.directory) {
125
+ return module === undefined
126
+ ? `\`${given}\` is the test folder of the project, which holds the test plain files; name a module's .plain file.`
127
+ : `\`${given}\` is the test folder of \`${module}\`; name \`${module}.plain\`.`;
128
+ }
129
+ if (module === undefined)
130
+ return `\`${given}\` is in the test folder, where test plain files are; name a module's .plain file.`;
131
+ const funcId = place.funcId ?? basename(path, PLAIN_SOURCE_FILE_EXTENSION);
132
+ return `\`${given}\` is a test plain file of \`${module}\`; name \`${module}.plain\`, or \`${module}:${funcId}\` for that one functional spec.`;
133
+ }
134
+ /** Refuse a path that is a test plain file or a test folder, as every tool that takes a module does. */
135
+ export function refuseTestPlainFile(path) {
136
+ const refusal = testPlainFileRefusal(path);
137
+ if (refusal !== undefined)
138
+ throw new Error(refusal);
139
+ }
140
+ /**
141
+ * The test plain files of a project that are not where they belong, each with where it does: a file outside any
142
+ * module's folder, in the folder of a module there is not, or in a module's folder its `module:` does not name. Strays
143
+ * are not read when a module is parsed; this is where they are found.
144
+ */
145
+ export function strayTestPlainFiles(projectDir) {
146
+ const testDir = testDirOf(join(projectDir, "x.plain"));
147
+ const modules = projectModules(projectDir);
148
+ const strays = [];
149
+ for (const path of plainFilesUnder(testDir)) {
150
+ const file = shown(path, projectDir);
151
+ const folder = relative(testDir, dirname(path)).split(sep).filter(Boolean)[0];
152
+ const metadata = frontmatterOf(path);
153
+ const named = typeof metadata[MODULE_KEY] === "string" ? metadata[MODULE_KEY] : undefined;
154
+ const home = (module) => `${shown(join(testDir, module), projectDir)}/`;
155
+ if (folder === undefined) {
156
+ strays.push({ file, problem: named === undefined ? "move it to the folder of its module, in the test folder" : `move it to ${home(named)} (its module: ${named})` });
157
+ }
158
+ else if (!modules.has(folder)) {
159
+ strays.push({
160
+ file,
161
+ problem: named === undefined ? `no module ${folder}` : `no module ${folder}; module: ${named} is in ${home(named)}`,
162
+ });
163
+ }
164
+ else if (named !== undefined && named !== folder) {
165
+ strays.push({ file, problem: `its module: is ${named}, not ${folder}; move it to ${home(named)}` });
166
+ }
167
+ }
168
+ return strays;
169
+ }
170
+ /** The module a test plain file belongs to, by its folder, and that module's .plain file when the project has one. */
171
+ export function moduleOfTestPlainFile(path) {
172
+ const place = testPlaceOf(path);
173
+ const module = place?.folderModule;
174
+ if (place === undefined || module === undefined)
175
+ return undefined;
176
+ return { module, plainFile: projectModules(place.projectDir).get(module) };
177
+ }
package/docs/README.md CHANGED
@@ -18,6 +18,7 @@ turri is the first coding agent with the power of regenerative software: [\*\*\*
18
18
  | how to start a project, or add a feature, in a conversation | [using](using.md#in-a-conversation) |
19
19
  | how to have several chats open at once, and reopen them | [chats](chats.md) |
20
20
  | what checks each pulse as it finishes, and tells me what needs me | [the sentinel](sentinel.md) |
21
+ | why the chat wrote scenarios I did not ask for, and how to turn that off | [the test advisor](test-advisor.md) |
21
22
  | which model codes, and how to change it | [using](using.md#choosing-a-model), [the project config](config.md#models) |
22
23
  | what `config.turri.yaml` can set, and what happens without it | [the project config](config.md) |
23
24
  | how a backend and a frontend on different stacks share one project | [the project config](config.md#stacks) |
@@ -44,6 +45,7 @@ turri is the first coding agent with the power of regenerative software: [\*\*\*
44
45
  | [Using turri](using.md) | the conversation, the tools, background jobs, choosing a model, module history, the command line |
45
46
  | [Chats](chats.md) | several chats in one session: what each keeps and what they share, `/chat` and its keys, `turri --continue` and `.turri/chats.json` |
46
47
  | [The sentinel](sentinel.md) | the chat every finished pulse goes to: what it checks and tests, its notices and how to answer them, what it may do, closing it, its model, in a host |
48
+ | [The test advisor](test-advisor.md) | turri's background check of new and changed functional specs: when it runs, how it decides, what you see, its records, model and setting |
47
49
  | [The project config](config.md) | `config.turri.yaml` and each stack's `config.turri.<stack>.yaml`: where they are read from, every key, models and their inheritance, conformance, the testing scripts, stacks and choosing one, the old `config.yaml` |
48
50
  | [Deploy](deploy.md) | trying what is built: dist/<module>, the last commit, starting it on your machine, `turri release` |
49
51
  | [Running unattended](headless.md) | regenerating overnight or in CI: before you leave it, the command, the results, resuming, rebuilding one functionality |
@@ -51,7 +53,7 @@ turri is the first coding agent with the power of regenerative software: [\*\*\*
51
53
  | [Regenerating](regenerating.md) | where things go, one job, a whole module and how its work is cut into jobs, each job's cases, \*\*\*plain changes, conformance tests, the two records of what is built |
52
54
  | [The sandbox](sandbox.md) | what an agent can reach, the file-tool fence, the command check, the OS sandbox, the audit |
53
55
  | [Skills and gates](skills-and-gates.md) | the gate syntax, what the harness enforces, what a step can do, the rules, trying the gates |
54
- | [The \*\*\*plain parser](parser.md) | the TypeScript port, `turri validate`, inheritance, the iterator, fidelity |
56
+ | [The \*\*\*plain parser](parser.md) | the TypeScript port, `turri parse`, inheritance, the iterator, fidelity |
55
57
  | [Theming](theming.md) | the turri themes, contrast, backgrounds, the font |
56
58
  | [Running turri in a host](hosting.md) | `@turri-dev/cli/host`: the session and its chats in a host's own windows - their jobs, their questions - and what a host shows beside them |
57
59
  | [Development](development.md) | tests, the source layout, known edges |