@turri-dev/cli 0.0.0-stage → 0.1.2-next.48dacfc

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 (315) hide show
  1. package/.pi/themes/turri-dark.json +80 -0
  2. package/.pi/themes/turri-light.json +82 -0
  3. package/LICENSE +21 -0
  4. package/README.md +272 -2
  5. package/assets/jellyfish/dark-00.png +0 -0
  6. package/assets/jellyfish/dark-01.png +0 -0
  7. package/assets/jellyfish/dark-02.png +0 -0
  8. package/assets/jellyfish/dark-03.png +0 -0
  9. package/assets/jellyfish/dark-04.png +0 -0
  10. package/assets/jellyfish/dark-05.png +0 -0
  11. package/assets/jellyfish/dark-06.png +0 -0
  12. package/assets/jellyfish/dark-07.png +0 -0
  13. package/assets/jellyfish/dark-08.png +0 -0
  14. package/assets/jellyfish/dark-09.png +0 -0
  15. package/assets/jellyfish/dark-10.png +0 -0
  16. package/assets/jellyfish/dark-11.png +0 -0
  17. package/assets/jellyfish/dark-12.png +0 -0
  18. package/assets/jellyfish/dark-13.png +0 -0
  19. package/assets/jellyfish/dark-14.png +0 -0
  20. package/assets/jellyfish/fallback.txt +252 -0
  21. package/assets/jellyfish/light-00.png +0 -0
  22. package/assets/jellyfish/light-01.png +0 -0
  23. package/assets/jellyfish/light-02.png +0 -0
  24. package/assets/jellyfish/light-03.png +0 -0
  25. package/assets/jellyfish/light-04.png +0 -0
  26. package/assets/jellyfish/light-05.png +0 -0
  27. package/assets/jellyfish/light-06.png +0 -0
  28. package/assets/jellyfish/light-07.png +0 -0
  29. package/assets/jellyfish/light-08.png +0 -0
  30. package/assets/jellyfish/light-09.png +0 -0
  31. package/assets/jellyfish/light-10.png +0 -0
  32. package/assets/jellyfish/light-11.png +0 -0
  33. package/assets/jellyfish/light-12.png +0 -0
  34. package/assets/jellyfish/light-13.png +0 -0
  35. package/assets/jellyfish/light-14.png +0 -0
  36. package/bin/turri.mjs +29 -0
  37. package/dist/ask.js +149 -0
  38. package/dist/brand.js +50 -0
  39. package/dist/btw.js +174 -0
  40. package/dist/chat-models.js +37 -0
  41. package/dist/chat-skills.js +18 -0
  42. package/dist/cli.js +178 -0
  43. package/dist/config-command.js +197 -0
  44. package/dist/config-edit.js +367 -0
  45. package/dist/config.js +489 -0
  46. package/dist/conform-cli.js +29 -0
  47. package/dist/cultivate-cli.js +110 -0
  48. package/dist/drift-cli.js +25 -0
  49. package/dist/environment.js +70 -0
  50. package/dist/extension.js +2372 -0
  51. package/dist/genome.js +301 -0
  52. package/dist/graph.js +246 -0
  53. package/dist/identity.js +35 -0
  54. package/dist/init-config.js +511 -0
  55. package/dist/judge.js +62 -0
  56. package/dist/lineage-cli.js +13 -0
  57. package/dist/loop-cli.js +132 -0
  58. package/dist/loop-tools.js +225 -0
  59. package/dist/mature-tools.js +193 -0
  60. package/dist/model-check.js +117 -0
  61. package/dist/module-report.js +9 -0
  62. package/dist/pi-launch.js +31 -0
  63. package/dist/pi.js +813 -0
  64. package/dist/plain/check.js +76 -0
  65. package/dist/plain/concepts.js +213 -0
  66. package/dist/plain/errors.js +43 -0
  67. package/dist/plain/files.js +57 -0
  68. package/dist/plain/highlight.js +195 -0
  69. package/dist/plain/index.js +15 -0
  70. package/dist/plain/liquid.js +129 -0
  71. package/dist/plain/markdown.js +208 -0
  72. package/dist/plain/parser.js +752 -0
  73. package/dist/plain/plain.tmLanguage.json +124 -0
  74. package/dist/plain/spec.js +291 -0
  75. package/dist/plain/stdlib.js +44 -0
  76. package/dist/question-dialog.js +125 -0
  77. package/dist/questioning.js +109 -0
  78. package/dist/quiet.js +125 -0
  79. package/dist/regenerate/activity.js +187 -0
  80. package/dist/regenerate/agent-env.js +190 -0
  81. package/dist/regenerate/agent-shell.js +114 -0
  82. package/dist/regenerate/arbiter.js +133 -0
  83. package/dist/regenerate/audit.js +203 -0
  84. package/dist/regenerate/cases.js +83 -0
  85. package/dist/regenerate/change-view.js +102 -0
  86. package/dist/regenerate/choose-conformance.js +117 -0
  87. package/dist/regenerate/chunk-plan.js +625 -0
  88. package/dist/regenerate/codebase.js +75 -0
  89. package/dist/regenerate/command-check.js +312 -0
  90. package/dist/regenerate/compaction.js +20 -0
  91. package/dist/regenerate/confine.js +310 -0
  92. package/dist/regenerate/conform.js +133 -0
  93. package/dist/regenerate/conformance-agents.js +955 -0
  94. package/dist/regenerate/conformance.js +936 -0
  95. package/dist/regenerate/conversation-writes.js +100 -0
  96. package/dist/regenerate/credentials.js +88 -0
  97. package/dist/regenerate/debug-log.js +228 -0
  98. package/dist/regenerate/debugger.js +90 -0
  99. package/dist/regenerate/delivery.js +19 -0
  100. package/dist/regenerate/diagnose-job.js +40 -0
  101. package/dist/regenerate/diagnose.js +81 -0
  102. package/dist/regenerate/diff-view.js +240 -0
  103. package/dist/regenerate/drift.js +194 -0
  104. package/dist/regenerate/engine.js +118 -0
  105. package/dist/regenerate/environment-error.js +220 -0
  106. package/dist/regenerate/job-log.js +102 -0
  107. package/dist/regenerate/job-panel.js +336 -0
  108. package/dist/regenerate/jobs-tree.js +300 -0
  109. package/dist/regenerate/jobs.js +427 -0
  110. package/dist/regenerate/lanes.js +32 -0
  111. package/dist/regenerate/lineage.js +384 -0
  112. package/dist/regenerate/loop.js +1435 -0
  113. package/dist/regenerate/masking.js +95 -0
  114. package/dist/regenerate/notes.js +397 -0
  115. package/dist/regenerate/os-sandbox.js +243 -0
  116. package/dist/regenerate/paths.js +165 -0
  117. package/dist/regenerate/plain-palette.js +83 -0
  118. package/dist/regenerate/prompt.js +704 -0
  119. package/dist/regenerate/records.js +294 -0
  120. package/dist/regenerate/regenerate.js +598 -0
  121. package/dist/regenerate/regression.js +181 -0
  122. package/dist/regenerate/release.js +262 -0
  123. package/dist/regenerate/requires.js +241 -0
  124. package/dist/regenerate/resources.js +164 -0
  125. package/dist/regenerate/self-check.js +111 -0
  126. package/dist/regenerate/services.js +155 -0
  127. package/dist/regenerate/shell.js +320 -0
  128. package/dist/regenerate/side-panel.js +278 -0
  129. package/dist/regenerate/spec-map-view.js +574 -0
  130. package/dist/regenerate/spec-map.js +561 -0
  131. package/dist/regenerate/spec-steps.js +527 -0
  132. package/dist/regenerate/state.js +10 -0
  133. package/dist/regenerate/steer.js +91 -0
  134. package/dist/regenerate/steering.js +119 -0
  135. package/dist/regenerate/stop-report.js +161 -0
  136. package/dist/regenerate/submit-work.js +71 -0
  137. package/dist/regenerate/test-kinds.js +124 -0
  138. package/dist/regenerate/test-scripts.js +695 -0
  139. package/dist/regenerate/testing.js +406 -0
  140. package/dist/regenerate/transcript-view.js +1 -0
  141. package/dist/regenerate/unit-gate.js +20 -0
  142. package/dist/regenerate/unit-review.js +347 -0
  143. package/dist/regenerate/web.js +256 -0
  144. package/dist/regenerate/workspace.js +99 -0
  145. package/dist/regenerate-cli.js +68 -0
  146. package/dist/release-agent.js +114 -0
  147. package/dist/release-cli.js +131 -0
  148. package/dist/release-tools.js +148 -0
  149. package/dist/review/live.js +165 -0
  150. package/dist/rules.js +47 -0
  151. package/dist/self-invocation.js +110 -0
  152. package/dist/setup.js +245 -0
  153. package/dist/skill-progress.js +78 -0
  154. package/dist/skill.js +149 -0
  155. package/dist/skills.js +74 -0
  156. package/dist/spec-diff-cli.js +37 -0
  157. package/dist/spec-tools.js +155 -0
  158. package/dist/stack.js +158 -0
  159. package/dist/stacks.js +97 -0
  160. package/dist/stdlib.js +100 -0
  161. package/dist/terminal-images.js +46 -0
  162. package/dist/tools.js +242 -0
  163. package/dist/validate-cli.js +52 -0
  164. package/docs/README.md +58 -0
  165. package/docs/authoring-plain.md +245 -0
  166. package/docs/config.md +353 -0
  167. package/docs/deploy.md +109 -0
  168. package/docs/development.md +88 -0
  169. package/docs/headless.md +148 -0
  170. package/docs/images/how-turri-works.svg +50 -0
  171. package/docs/images/turri-banner.png +0 -0
  172. package/docs/installation.md +119 -0
  173. package/docs/parser.md +108 -0
  174. package/docs/regenerating.md +1116 -0
  175. package/docs/sandbox.md +233 -0
  176. package/docs/skills-and-gates.md +254 -0
  177. package/docs/theming.md +76 -0
  178. package/docs/using.md +454 -0
  179. package/genome/rules/bullet-continuation.md +69 -0
  180. package/genome/rules/concise-specs-examples.md +431 -0
  181. package/genome/rules/concise-specs-integration-examples.md +270 -0
  182. package/genome/rules/concise-specs.md +80 -0
  183. package/genome/rules/definitions.md +107 -0
  184. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
  185. package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
  186. package/genome/rules/exported-concepts.md +39 -0
  187. package/genome/rules/func-specs.md +129 -0
  188. package/genome/rules/impl-reqs.md +68 -0
  189. package/genome/rules/import-modules.md +48 -0
  190. package/genome/rules/integration-embedded-testing.md +276 -0
  191. package/genome/rules/integration-embedded.md +171 -0
  192. package/genome/rules/integration-standalone.md +135 -0
  193. package/genome/rules/integrations.md +137 -0
  194. package/genome/rules/linked-resources.md +69 -0
  195. package/genome/rules/module-structure.md +105 -0
  196. package/genome/rules/project-model.md +88 -0
  197. package/genome/rules/required-concepts.md +44 -0
  198. package/genome/rules/requires-modules.md +80 -0
  199. package/genome/rules/stdlib.md +73 -0
  200. package/genome/rules/test-reqs.md +57 -0
  201. package/genome/skills/add-acceptance-test/SKILL.md +100 -0
  202. package/genome/skills/add-concept/SKILL.md +71 -0
  203. package/genome/skills/add-functional-spec/SKILL.md +84 -0
  204. package/genome/skills/add-functional-specs/SKILL.md +118 -0
  205. package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
  206. package/genome/skills/add-resource/SKILL.md +105 -0
  207. package/genome/skills/add-template/SKILL.md +64 -0
  208. package/genome/skills/add-test-requirement/SKILL.md +73 -0
  209. package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
  210. package/genome/skills/analyze-func-specs/SKILL.md +123 -0
  211. package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
  212. package/genome/skills/break-down-func-spec/SKILL.md +205 -0
  213. package/genome/skills/brood/SKILL.md +229 -0
  214. package/genome/skills/brood/references/intent.md +35 -0
  215. package/genome/skills/brood/references/product.md +101 -0
  216. package/genome/skills/brood/references/stack.md +61 -0
  217. package/genome/skills/brood/references/testing.md +56 -0
  218. package/genome/skills/bud/SKILL.md +148 -0
  219. package/genome/skills/bud/references/checklist.md +44 -0
  220. package/genome/skills/bud/references/intent.md +30 -0
  221. package/genome/skills/check-plain-env/SKILL.md +285 -0
  222. package/genome/skills/consolidate-concepts/SKILL.md +192 -0
  223. package/genome/skills/create-import-module/SKILL.md +104 -0
  224. package/genome/skills/create-requires-module/SKILL.md +109 -0
  225. package/genome/skills/debug-specs/SKILL.md +188 -0
  226. package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
  227. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
  228. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
  229. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
  230. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
  231. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
  232. package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
  233. package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
  234. package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
  235. package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
  236. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
  237. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
  238. package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
  239. package/genome/skills/init-plain-project/SKILL.md +117 -0
  240. package/genome/skills/init-plain-project/references/checklist.md +30 -0
  241. package/genome/skills/mature/SKILL.md +126 -0
  242. package/genome/skills/refactor-module/SKILL.md +196 -0
  243. package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
  244. package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
  245. package/genome/skills/spawn/SKILL.md +163 -0
  246. package/package.json +68 -4
  247. package/stdlib/backend/common.plain +25 -0
  248. package/stdlib/backend/conformance/common.plain +12 -0
  249. package/stdlib/backend/conformance/go-gin.plain +9 -0
  250. package/stdlib/backend/conformance/go-nethttp.plain +9 -0
  251. package/stdlib/backend/conformance/java-springboot.plain +9 -0
  252. package/stdlib/backend/conformance/python-django.plain +9 -0
  253. package/stdlib/backend/conformance/python-fastapi.plain +9 -0
  254. package/stdlib/backend/conformance/python-flask.plain +9 -0
  255. package/stdlib/backend/conformance/rust-axum.plain +9 -0
  256. package/stdlib/backend/conformance/typescript-express.plain +9 -0
  257. package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
  258. package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
  259. package/stdlib/backend/go-gin.plain +18 -0
  260. package/stdlib/backend/go-nethttp.plain +18 -0
  261. package/stdlib/backend/java-springboot.plain +19 -0
  262. package/stdlib/backend/python-django.plain +19 -0
  263. package/stdlib/backend/python-fastapi.plain +23 -0
  264. package/stdlib/backend/python-flask.plain +23 -0
  265. package/stdlib/backend/rust-axum.plain +17 -0
  266. package/stdlib/backend/typescript-express.plain +19 -0
  267. package/stdlib/backend/typescript-fastify.plain +19 -0
  268. package/stdlib/backend/typescript-nestjs.plain +19 -0
  269. package/stdlib/cli/common.plain +17 -0
  270. package/stdlib/cli/conformance/common.plain +11 -0
  271. package/stdlib/cli/conformance/go-cobra.plain +9 -0
  272. package/stdlib/cli/conformance/go-flag.plain +9 -0
  273. package/stdlib/cli/conformance/java-picocli.plain +9 -0
  274. package/stdlib/cli/conformance/python-argparse.plain +9 -0
  275. package/stdlib/cli/conformance/python-click.plain +9 -0
  276. package/stdlib/cli/conformance/python-typer.plain +9 -0
  277. package/stdlib/cli/conformance/rust-clap.plain +9 -0
  278. package/stdlib/cli/conformance/typescript-commander.plain +9 -0
  279. package/stdlib/cli/go-cobra.plain +14 -0
  280. package/stdlib/cli/go-flag.plain +14 -0
  281. package/stdlib/cli/java-picocli.plain +15 -0
  282. package/stdlib/cli/python-argparse.plain +18 -0
  283. package/stdlib/cli/python-click.plain +16 -0
  284. package/stdlib/cli/python-typer.plain +16 -0
  285. package/stdlib/cli/rust-clap.plain +14 -0
  286. package/stdlib/cli/typescript-commander.plain +17 -0
  287. package/stdlib/frontend/angular.plain +17 -0
  288. package/stdlib/frontend/common.plain +17 -0
  289. package/stdlib/frontend/conformance/angular.plain +5 -0
  290. package/stdlib/frontend/conformance/common.plain +11 -0
  291. package/stdlib/frontend/conformance/react-vite.plain +5 -0
  292. package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
  293. package/stdlib/frontend/conformance/vue-vite.plain +5 -0
  294. package/stdlib/frontend/react-vite.plain +16 -0
  295. package/stdlib/frontend/sveltekit-spa.plain +16 -0
  296. package/stdlib/frontend/vue-vite.plain +16 -0
  297. package/stdlib/lang/go.plain +13 -0
  298. package/stdlib/lang/java.plain +12 -0
  299. package/stdlib/lang/python.plain +18 -0
  300. package/stdlib/lang/rust.plain +12 -0
  301. package/stdlib/lang/typescript.plain +13 -0
  302. package/stdlib/library/common.plain +15 -0
  303. package/stdlib/library/conformance/common.plain +9 -0
  304. package/stdlib/library/conformance/go.plain +9 -0
  305. package/stdlib/library/conformance/java-gradle.plain +9 -0
  306. package/stdlib/library/conformance/java-maven.plain +9 -0
  307. package/stdlib/library/conformance/python.plain +9 -0
  308. package/stdlib/library/conformance/rust.plain +9 -0
  309. package/stdlib/library/conformance/typescript.plain +9 -0
  310. package/stdlib/library/go.plain +12 -0
  311. package/stdlib/library/java-gradle.plain +13 -0
  312. package/stdlib/library/java-maven.plain +13 -0
  313. package/stdlib/library/python.plain +13 -0
  314. package/stdlib/library/rust.plain +12 -0
  315. package/stdlib/library/typescript.plain +15 -0
package/dist/genome.js ADDED
@@ -0,0 +1,301 @@
1
+ import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { Command, MemorySaver } from "@langchain/langgraph";
4
+ import { compileSkill } from "./graph.js";
5
+ import { createExecutor } from "./pi.js";
6
+ import { toolchainsBlock } from "./stack.js";
7
+ import { describeToolCall, describeToolResult } from "./regenerate/activity.js";
8
+ import { createLiveReviewer } from "./review/live.js";
9
+ import { loadSkill, readRubric } from "./skill.js";
10
+ import { createAskUserTool, createEndQuestionsTool, createReviewTools, createWriteArtifactTool, withReviewNote } from "./tools.js";
11
+ import { DISMISSED } from "./ask.js";
12
+ import { createQuestioning, levelOf, questioningQuestion } from "./questioning.js";
13
+ import { createSpecDiffTools } from "./spec-tools.js";
14
+ import { createMatureTools, MATURE_TOOLS } from "./mature-tools.js";
15
+ const WRITE_SPEC_DIFF = "write_spec_diff";
16
+ /** The conversation's tools a skill acts on the work loop with - never pi built-ins. */
17
+ const LOOP_TOOLS = ["regenerate", "drift", "halt", "conform"];
18
+ /** How much of what the agent said before a question is shown with it: its end, where the brief is. */
19
+ const SAID_MAX = 6000;
20
+ /**
21
+ * Run one skill to completion. The only entry point: the CLI and the Pi extension
22
+ * both go through here, so a skill behaves identically whether a human launched it
23
+ * or a conversational agent decided to.
24
+ */
25
+ export async function runSkill(opts) {
26
+ const skill = loadSkill(opts.skillDir);
27
+ const runDir = resolve(opts.runDir);
28
+ // A resumed run keeps its folder - the artifacts are in it - and starts where it stopped.
29
+ let resumed;
30
+ if (opts.resume === true) {
31
+ resumed = readManifest(runDir);
32
+ if (resumed === undefined || resumed.skill !== skill.name || resumed.status !== "running") {
33
+ throw new Error(resumed === undefined || resumed.skill !== skill.name
34
+ ? `There is no interrupted run of ${skill.name} to resume here.`
35
+ : `The last run of ${skill.name} here ${resumed.status === "finished" ? "finished" : "was stopped"}; there is nothing to resume.`);
36
+ }
37
+ }
38
+ else {
39
+ rmSync(runDir, { recursive: true, force: true });
40
+ }
41
+ mkdirSync(runDir, { recursive: true });
42
+ // The executor has to exist before a reviewer can borrow its auth, and the reviewer has
43
+ // to exist before the tools that report its findings. So the tool list is built in two
44
+ // passes with a box the reviewer is dropped into once there is one.
45
+ const liveReviewers = new Map();
46
+ const liveStep = skill.steps.find((step) => step.gate?.continuous !== undefined);
47
+ const holder = {};
48
+ const liveProxy = liveStep === undefined
49
+ ? undefined
50
+ : {
51
+ notice: (scope) => holder.reviewer?.notice(scope),
52
+ take: () => holder.reviewer?.take() ?? [],
53
+ takeQuestions: () => holder.reviewer?.takeQuestions() ?? [],
54
+ reply: (id, answer) => holder.reviewer?.reply(id, answer) ?? false,
55
+ settle: async () => holder.reviewer?.settle() ?? { clean: true, findings: [] },
56
+ history: () => holder.reviewer?.history() ?? [],
57
+ passes: () => holder.reviewer?.passes() ?? 0,
58
+ };
59
+ const writeArtifact = createWriteArtifactTool(runDir);
60
+ const customTools = [writeArtifact];
61
+ // The questioning level, asked first in a skill whose phases it caps - when there is someone to ask. A resumed run
62
+ // keeps the one it was given, and how far each phase's questions went.
63
+ const capsQuestions = skill.questioning.length > 0 && opts.askUser !== undefined;
64
+ let questioning;
65
+ const keepQuestioning = (state) => {
66
+ manifest.questioning = state;
67
+ writeManifest(runDir, manifest);
68
+ };
69
+ const startQuestioning = (level, from) => (questioning = createQuestioning({ level: levelOf(level), phases: skill.questioning, from, onChange: keepQuestioning }));
70
+ if (capsQuestions && resumed?.questioning !== undefined)
71
+ startQuestioning(resumed.questioning.level, resumed.questioning);
72
+ // The step running now, and whether each of its answers is a spec change: then the next question waits until the last
73
+ // answer is written, and a change the agent decides itself needs no answer before it.
74
+ let currentStep = "";
75
+ const diffPerAnswer = () => skill.diffPerAnswer.includes(currentStep);
76
+ const holdsQuestions = capsQuestions || (skill.diffPerAnswer.length > 0 && opts.askUser !== undefined);
77
+ const questions = {
78
+ refusal: (confirmsPhase) => diffPerAnswer() && diffs?.pace.owed() === true
79
+ ? "This question was not asked: the last answer is not in the spec yet. Every answer in this phase is a spec " +
80
+ "change, built at once - write it with write_spec_diff first, then ask."
81
+ : questioning?.refusal(confirmsPhase),
82
+ answered: (confirmsPhase) => questioning?.answered(confirmsPhase),
83
+ end: () => questioning?.end() ?? "No questioning level was chosen: ask what the phase needs.",
84
+ };
85
+ // A skill whose unit of work is a spec change gets write_spec_diff: the change is applied
86
+ // and its build queued in one step.
87
+ const diffs = opts.renderQueue !== undefined && skill.allowedTools.includes(WRITE_SPEC_DIFF)
88
+ ? createSpecDiffTools({
89
+ loop: opts.renderQueue,
90
+ cwd: opts.cwd,
91
+ liveReview: liveProxy,
92
+ paced: skill.paced,
93
+ free: diffPerAnswer,
94
+ })
95
+ : undefined;
96
+ // What the agent says between its questions - a brief to confirm, a plan - reaches the person only with
97
+ // the next question: the session it talks in is its own, and nobody reads it.
98
+ let said = "";
99
+ /** A new message of the agent's has started, and has said nothing yet. */
100
+ let saidFresh = false;
101
+ if (opts.askUser !== undefined) {
102
+ customTools.push(createAskUserTool(opts.askUser, (answer, confirmsPhase) => {
103
+ said = "";
104
+ // A phase's confirmation, or a question dismissed, owes the spec nothing.
105
+ diffs?.pace.answered(!confirmsPhase && answer !== DISMISSED);
106
+ }, () => said, holdsQuestions ? questions : undefined));
107
+ if (capsQuestions)
108
+ customTools.push(createEndQuestionsTool(questions));
109
+ }
110
+ if (opts.renderQueue !== undefined) {
111
+ customTools.push(...createReviewTools(liveProxy));
112
+ for (const tool of opts.loopTools ?? []) {
113
+ if (tool.name === "drift")
114
+ customTools.push(withReviewNote(tool, liveProxy));
115
+ else if (skill.allowedTools.includes(tool.name))
116
+ customTools.push(tool);
117
+ }
118
+ }
119
+ if (diffs !== undefined)
120
+ customTools.push(...diffs.tools);
121
+ // A skill that builds what the spec holds, waits for it, and reads back where the builds had to guess.
122
+ if (opts.renderQueue !== undefined && opts.jobs !== undefined && skill.allowedTools.some((tool) => MATURE_TOOLS.includes(tool))) {
123
+ customTools.push(...createMatureTools({ jobs: opts.jobs, cwd: opts.cwd }));
124
+ }
125
+ customTools.push(...(opts.sharedTools ?? []));
126
+ const names = customTools.map((tool) => tool.name);
127
+ // Tools this harness provides are never pi built-ins, even when it did not provide them.
128
+ const builtins = skill.allowedTools.filter((t) => !names.includes(t) && t !== WRITE_SPEC_DIFF && !MATURE_TOOLS.includes(t) && !LOOP_TOOLS.includes(t));
129
+ const executor = await createExecutor({
130
+ cwd: opts.cwd,
131
+ customTools,
132
+ tools: [...builtins, ...names],
133
+ model: opts.model,
134
+ // A skill writes specs; the rules it is held to are in its system prompt - and what this machine can build, so the
135
+ // stack it offers or chooses is one that can be regenerated here.
136
+ rules: true,
137
+ system: toolchainsBlock(),
138
+ resumeFrom: resumed?.sessionFile,
139
+ });
140
+ const manifest = resumed ?? {
141
+ skill: skill.name,
142
+ status: "running",
143
+ artifacts: {},
144
+ attempts: {},
145
+ updatedAt: "",
146
+ };
147
+ // What the agent does, as it does it: its session is its own, so without this a long step is silence.
148
+ executor.subscribe((event) => {
149
+ if (event.type === "tool_execution_start") {
150
+ opts.report.activity?.(describeToolCall(event.toolName, event.args));
151
+ return;
152
+ }
153
+ if (event.type === "tool_execution_end") {
154
+ const line = describeToolResult(event.toolName, event.result, event.isError);
155
+ if (line !== undefined)
156
+ opts.report.activity?.(line);
157
+ return;
158
+ }
159
+ // Shown with the next question is the last thing it said, not all it said since the last answer: a message of
160
+ // its own replaces what came before - its account of a probe, a regeneration - once it has words in it, so a brief
161
+ // said just before a bare ask_user call is still shown with it.
162
+ if (event.type === "message_start") {
163
+ if (event.message.role === "assistant")
164
+ saidFresh = true;
165
+ return;
166
+ }
167
+ if (event.type !== "message_update")
168
+ return;
169
+ const update = event.assistantMessageEvent;
170
+ if (update.type === "text_start") {
171
+ if (saidFresh)
172
+ said = "";
173
+ saidFresh = false;
174
+ if (said.trim() !== "")
175
+ said += "\n\n";
176
+ opts.report.said?.("", true);
177
+ }
178
+ else if (update.type === "text_delta") {
179
+ said = (said + update.delta).slice(-SAID_MAX);
180
+ opts.report.said?.(update.delta, false);
181
+ }
182
+ });
183
+ manifest.sessionFile = executor.sessionFile;
184
+ writeManifest(runDir, manifest);
185
+ // Built renders were lost with the process; queue what the specs hold that is not built.
186
+ if (resumed !== undefined && opts.renderQueue !== undefined) {
187
+ for (const file of plainFilesIn(opts.cwd))
188
+ void opts.renderQueue.bringUp(file).catch(() => undefined);
189
+ }
190
+ if (liveStep !== undefined && liveProxy !== undefined) {
191
+ const continuous = liveStep.gate.continuous;
192
+ holder.reviewer = createLiveReviewer({
193
+ cwd: opts.cwd,
194
+ spec: continuous,
195
+ rubric: readRubric(skill, continuous),
196
+ executor,
197
+ defaultModel: opts.model,
198
+ onPass: (pass) => opts.report.livePass?.(liveStep.name, pass.scope, pass.findings),
199
+ });
200
+ liveReviewers.set(liveStep.name, liveProxy);
201
+ }
202
+ try {
203
+ const app = compileSkill({
204
+ skill,
205
+ cwd: opts.cwd,
206
+ runDir,
207
+ executor,
208
+ report: opts.report,
209
+ model: opts.model,
210
+ context: opts.context,
211
+ liveReviewers,
212
+ startAt: resumed?.step,
213
+ // Where the run is, written as each step starts: what a resume picks up from.
214
+ onStepStart: (step, state) => {
215
+ manifest.step = step;
216
+ manifest.artifacts = state.artifacts;
217
+ manifest.attempts = state.attempts;
218
+ writeManifest(runDir, manifest);
219
+ },
220
+ beforeStep: async (step) => {
221
+ currentStep = step;
222
+ if (!capsQuestions)
223
+ return;
224
+ // The first question of the first phase is the harness's: how many the capped phases may ask.
225
+ if (questioning === undefined) {
226
+ const asked = questioningQuestion(skill.questioning);
227
+ startQuestioning(await opts.askUser({ ...asked, said: "" }));
228
+ keepQuestioning(questioning.state());
229
+ }
230
+ questioning.enter(step);
231
+ },
232
+ stepNote: (step) => questioning?.note(step, step === skill.steps[0].name),
233
+ }).compile({
234
+ checkpointer: new MemorySaver(),
235
+ });
236
+ const config = { configurable: { thread_id: `genome-${Date.now()}` }, recursionLimit: 50 };
237
+ let result = await app.invoke(resumed !== undefined ? { artifacts: resumed.artifacts, attempts: resumed.attempts } : {}, config);
238
+ // Escalation surfaces as a LangGraph interrupt: the graph is paused, not
239
+ // finished, and nothing advances until the handler answers.
240
+ while (result.__interrupt__?.length) {
241
+ const approved = await opts.escalate(result.__interrupt__[0].value);
242
+ result = await app.invoke(new Command({ resume: { approved } }), config);
243
+ }
244
+ const artifacts = result.artifacts ?? {};
245
+ const lastStep = skill.steps[skill.steps.length - 1].name;
246
+ manifest.status = result.decision === "abort" ? "stopped" : "finished";
247
+ writeManifest(runDir, manifest);
248
+ return {
249
+ skill,
250
+ ok: result.decision !== "abort",
251
+ artifacts,
252
+ output: artifacts[lastStep] ?? "",
253
+ };
254
+ }
255
+ finally {
256
+ executor.dispose();
257
+ }
258
+ }
259
+ const MANIFEST = "run.json";
260
+ function readManifest(runDir) {
261
+ try {
262
+ return JSON.parse(readFileSync(join(runDir, MANIFEST), "utf8"));
263
+ }
264
+ catch {
265
+ return undefined;
266
+ }
267
+ }
268
+ function writeManifest(runDir, manifest) {
269
+ manifest.updatedAt = new Date().toISOString();
270
+ writeFileSync(join(runDir, MANIFEST), JSON.stringify(manifest, null, 2));
271
+ }
272
+ /** The .plain files at the top of a project. */
273
+ function plainFilesIn(dir) {
274
+ try {
275
+ return readdirSync(dir)
276
+ .filter((name) => name.endsWith(".plain"))
277
+ .map((name) => join(dir, name));
278
+ }
279
+ catch {
280
+ return [];
281
+ }
282
+ }
283
+ /** Whether there is a run of this skill here that was interrupted, and could be resumed. */
284
+ export function interruptedRun(runDir, skillName) {
285
+ const manifest = readManifest(resolve(runDir));
286
+ return manifest?.skill === skillName && manifest.status === "running"
287
+ ? { step: manifest.step, updatedAt: manifest.updatedAt }
288
+ : undefined;
289
+ }
290
+ /** Human-readable one-liner per step, for logs and tool output. */
291
+ export function describeGates(skill) {
292
+ return skill.steps.map((s) => {
293
+ if (!s.gate)
294
+ return `${s.name} -> no gate`;
295
+ const live = s.gate.continuous
296
+ ? ` + continuous(${s.gate.continuous.rubric}, judges=${s.gate.continuous.judges})`
297
+ : "";
298
+ const r = s.gate.review;
299
+ return `${s.name} -> gate(${r.rubric}, judges=${r.judges}, quorum=${r.quorum}, on_fail=${r.onFail}, max_retries=${r.maxRetries})${live}`;
300
+ });
301
+ }
package/dist/graph.js ADDED
@@ -0,0 +1,246 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { Annotation, END, START, StateGraph, interrupt } from "@langchain/langgraph";
4
+ import { runGate } from "./judge.js";
5
+ import { ask } from "./pi.js";
6
+ import { describeFindings } from "./review/live.js";
7
+ import { readRubric } from "./skill.js";
8
+ export const GenomeState = Annotation.Root({
9
+ artifacts: Annotation({
10
+ reducer: (a, b) => ({ ...a, ...b }),
11
+ default: () => ({}),
12
+ }),
13
+ attempts: Annotation({
14
+ reducer: (a, b) => ({ ...a, ...b }),
15
+ default: () => ({}),
16
+ }),
17
+ /** Judge feedback for the step currently being retried. */
18
+ critique: Annotation({ reducer: (_a, b) => b, default: () => "" }),
19
+ decision: Annotation({ reducer: (_a, b) => b, default: () => "advance" }),
20
+ });
21
+ const stepNode = (s) => `step.${s}`;
22
+ const gateNode = (s) => `gate.${s}`;
23
+ const escalateNode = (s) => `escalate.${s}`;
24
+ /** Where the executor is required to put this step's deliverable. */
25
+ const artifactPath = (step) => `${step.name}.md`;
26
+ /**
27
+ * The prompt for a step picked up after the run was interrupted part-way through it. The
28
+ * session is the one that was running, so the step's instructions and everything said so far
29
+ * are already in it; what the agent needs is to know it stopped, and to look before it acts.
30
+ */
31
+ function resumePrompt(step) {
32
+ return [
33
+ `This skill run was interrupted during step '${step.name}' and has been resumed. Your session is as`,
34
+ "it was when it stopped.",
35
+ "",
36
+ "Check what is already on disk before doing anything - the specs written so far, and any artifact -",
37
+ "and continue the step from where it stopped. Do not redo what is done, and do not ask the user again",
38
+ "for anything they have already answered.",
39
+ step.gate ? `\nWhen the step is done, call write_artifact with path "${artifactPath(step)}" as before.` : "",
40
+ ]
41
+ .filter((part) => part !== "")
42
+ .join("\n");
43
+ }
44
+ /** How many times a step whose turn ends on a question in its reply is sent back to carry on. */
45
+ export const QUESTION_NUDGES = 2;
46
+ /** Whether a reply ends on a question: its last line, its markdown left aside, ends with a question mark. */
47
+ export function endsOnQuestion(reply) {
48
+ const last = reply.trim().split("\n").filter((line) => line.trim() !== "").at(-1) ?? "";
49
+ return /\?$/.test(last.replace(/[*_`)\]"'\s]+$/, ""));
50
+ }
51
+ /** Sent when a step's turn ended on a question in its reply: no one reads it, and the step is not done. */
52
+ function unansweredQuestionPrompt(skill, step) {
53
+ return [
54
+ "Your turn ended on a question in your reply. No one reads it: the person sees only what ask_user asks, and",
55
+ `the step ends when your turn does - so step '${step.name}' would end here, not done.`,
56
+ skill.paced
57
+ ? "If it is truly the person's to decide, ask it with ask_user. Otherwise decide it yourself."
58
+ : `'${skill.name}' asks the person nothing more: decide it yourself, as a careful engineer would, and say what you chose.`,
59
+ `Then carry on with step '${step.name}' until all of it is done, and end your turn only then.`,
60
+ ].join("\n");
61
+ }
62
+ function stepPrompt(skill, step, critique, context) {
63
+ if (critique) {
64
+ return [
65
+ `Your previous attempt at step '${step.name}' was rejected by review.`,
66
+ "",
67
+ "<violations>",
68
+ critique,
69
+ "</violations>",
70
+ "",
71
+ "Fix every violation and call write_artifact again with the same path.",
72
+ "Do not argue with the review and do not explain yourself - rewrite the artifact.",
73
+ ].join("\n");
74
+ }
75
+ return [
76
+ `You are executing step '${step.name}' of the skill '${skill.name}'.`,
77
+ "",
78
+ // A skill runs in its own session and has never seen the conversation that led here.
79
+ // Without this it re-asks what the person has already said, which reads as the skill
80
+ // not listening.
81
+ context
82
+ ? `<what the user has already said>\n${context}\n</what the user has already said>\n\nTake this as given. Do not ask again for anything it already answers; confirm it in one line and move to what is still unknown.\n`
83
+ : "",
84
+ step.instructions,
85
+ "",
86
+ // What the step names by path - its references, another skill to follow - is found from here.
87
+ `This skill's own files are in ${skill.dir}: a path its steps name, such as references/<file>.md, is there. ` +
88
+ `A skill it says to follow is beside it, in ${dirname(skill.dir)}/<skill name>/SKILL.md - read it and follow it.`,
89
+ "",
90
+ // The artifact exists for the gate's judges to read. Without a gate nothing reads it,
91
+ // and asking for one is a tool call spent on a file no one opens.
92
+ step.gate
93
+ ? `When you are done, call write_artifact with path "${artifactPath(step)}" holding the record the review will be checked against.`
94
+ : "",
95
+ ]
96
+ .filter((part) => part !== "")
97
+ .join("\n");
98
+ }
99
+ export function compileSkill(opts) {
100
+ const { skill, report } = opts;
101
+ const graph = new StateGraph(GenomeState);
102
+ // The step being resumed is told it was interrupted - once. A retry after its gate is an
103
+ // ordinary retry.
104
+ let resuming = opts.startAt;
105
+ skill.steps.forEach((step, index) => {
106
+ const nextNode = index + 1 < skill.steps.length ? stepNode(skill.steps[index + 1].name) : END;
107
+ graph.addNode(stepNode(step.name), async (state) => {
108
+ opts.onStepStart?.(step.name, state);
109
+ const attempt = (state.attempts[step.name] ?? 0) + 1;
110
+ report.step(step.name, attempt);
111
+ // Only the first step needs it: the executor keeps one session across every step,
112
+ // so it is already in the history by the time the second runs.
113
+ const context = index === 0 ? opts.context : undefined;
114
+ await opts.beforeStep?.(step.name);
115
+ const note = state.critique ? undefined : opts.stepNote?.(step.name);
116
+ const prompt = [
117
+ resuming === step.name && !state.critique ? resumePrompt(step) : stepPrompt(skill, step, state.critique, context),
118
+ note,
119
+ ]
120
+ .filter((part) => part !== undefined && part !== "")
121
+ .join("\n\n");
122
+ resuming = undefined;
123
+ let reply = await ask(opts.executor, prompt);
124
+ // A question in the reply reaches no one - the person sees only what ask_user asks - and the step ends with the
125
+ // turn: back to the work, so a step is not left half done on a question nobody answers.
126
+ for (let nudges = 0; nudges < QUESTION_NUDGES && endsOnQuestion(reply); nudges += 1) {
127
+ reply = await ask(opts.executor, unansweredQuestionPrompt(skill, step));
128
+ }
129
+ let artifact = "";
130
+ try {
131
+ artifact = readFileSync(join(opts.runDir, artifactPath(step)), "utf8").trim();
132
+ }
133
+ catch {
134
+ // Left empty on purpose: the gate treats a missing artifact as a failure.
135
+ }
136
+ if (artifact)
137
+ report.artifact(artifactPath(step), artifact);
138
+ return {
139
+ artifacts: { [step.name]: artifact },
140
+ attempts: { [step.name]: attempt },
141
+ critique: "",
142
+ };
143
+ });
144
+ if (!step.gate) {
145
+ graph.addEdge(stepNode(step.name), nextNode);
146
+ return;
147
+ }
148
+ const review = step.gate.review;
149
+ const rubric = readRubric(skill, review);
150
+ graph.addNode(gateNode(step.name), async (state) => {
151
+ const artifact = state.artifacts[step.name] ?? "";
152
+ // A reviewer that has been reading along all phase gets the last word before the
153
+ // judges are spent: it has seen every spec as it landed, and anything it is still
154
+ // unhappy about is a violation the step has to fix whatever the record says.
155
+ const live = opts.liveReviewers?.get(step.name);
156
+ if (live !== undefined) {
157
+ const settled = await live.settle();
158
+ report.liveReview?.(step.name, live.passes(), settled.findings.length);
159
+ if (!settled.clean) {
160
+ const attempts = state.attempts[step.name] ?? 1;
161
+ const decision = review.onFail === "abort"
162
+ ? "abort"
163
+ : review.onFail === "retry" && attempts <= review.maxRetries
164
+ ? "retry"
165
+ : "escalate";
166
+ const critique = describeFindings(settled.findings);
167
+ report.outcome(step.name, decision, critique);
168
+ return { decision, critique };
169
+ }
170
+ }
171
+ let outcome;
172
+ if (!artifact) {
173
+ // No judges needed - the step did not produce the thing it was asked to produce.
174
+ outcome = {
175
+ passed: false,
176
+ results: [],
177
+ critique: `- You never called write_artifact with path "${artifactPath(step)}", so there is nothing to review.`,
178
+ };
179
+ report.gate(step.name, 0);
180
+ }
181
+ else {
182
+ report.gate(step.name, review.judges);
183
+ outcome = await runGate({
184
+ cwd: opts.cwd,
185
+ review,
186
+ defaultModel: opts.model,
187
+ rubric,
188
+ artifactPath: artifactPath(step),
189
+ artifact,
190
+ executor: opts.executor,
191
+ onJudgeStart: () => { },
192
+ });
193
+ for (const r of outcome.results) {
194
+ report.judge(r.index, r.model, r.verdict?.verdict ?? "no verdict", r.verdict?.violations.length ?? 0);
195
+ }
196
+ }
197
+ const attempts = state.attempts[step.name] ?? 1;
198
+ const retriesLeft = attempts <= review.maxRetries;
199
+ let decision;
200
+ if (outcome.passed)
201
+ decision = "advance";
202
+ else if (review.onFail === "abort")
203
+ decision = "abort";
204
+ else if (review.onFail === "retry" && retriesLeft)
205
+ decision = "retry";
206
+ else
207
+ decision = "escalate";
208
+ report.outcome(step.name, decision, outcome.critique);
209
+ // The critique is for this step's retry; the next step starts fresh.
210
+ return { decision, critique: decision === "advance" ? "" : outcome.critique };
211
+ });
212
+ graph.addNode(escalateNode(step.name), async (state) => {
213
+ const answer = interrupt({
214
+ step: step.name,
215
+ artifact: state.artifacts[step.name] ?? "",
216
+ critique: state.critique,
217
+ question: `Review gate on '${step.name}' could not be satisfied. Accept the artifact anyway?`,
218
+ });
219
+ const approved = typeof answer === "boolean" ? answer : Boolean(answer?.approved);
220
+ // Accepted, the rejection stays with this step - the next one must not take it for its own.
221
+ return approved ? { decision: "advance", critique: "" } : { decision: "abort" };
222
+ });
223
+ graph.addEdge(stepNode(step.name), gateNode(step.name));
224
+ const route = (state) => {
225
+ if (state.decision === "advance")
226
+ return nextNode;
227
+ if (state.decision === "retry")
228
+ return stepNode(step.name);
229
+ if (state.decision === "escalate")
230
+ return escalateNode(step.name);
231
+ return END;
232
+ };
233
+ graph.addConditionalEdges(gateNode(step.name), route, [
234
+ nextNode,
235
+ stepNode(step.name),
236
+ escalateNode(step.name),
237
+ END,
238
+ ]);
239
+ graph.addConditionalEdges(escalateNode(step.name), (state) => (state.decision === "advance" ? nextNode : END), [nextNode, END]);
240
+ });
241
+ // Every step is a possible entry point, so none is unreachable when a resumed run starts
242
+ // part-way through; which one is taken is decided here.
243
+ const first = opts.startAt !== undefined && skill.steps.some((s) => s.name === opts.startAt) ? opts.startAt : skill.steps[0].name;
244
+ graph.addConditionalEdges(START, () => stepNode(first), skill.steps.map((s) => stepNode(s.name)));
245
+ return graph;
246
+ }
@@ -0,0 +1,35 @@
1
+ /** Who the conversation's agent is: turri, and what it does - first, so it speaks as turri, in the first person. */
2
+ export const TURRI_IDENTITY = [
3
+ "<turri>",
4
+ "You are turri - always lowercase - a coding agent with the power of regenerative software. The software is its",
5
+ "***plain; the code is regenerated from it. You help the person write ***plain, and you regenerate the code from it a",
6
+ "change at a time - with agents that write the code and tests, while you decide what runs next and when the work is",
7
+ "done - record what each piece was regenerated from, and prove it against conformance tests written from the ***plain",
8
+ "alone. When the code goes bad, you regenerate it from the ***plain. You are named after Turritopsis dohrnii, the",
9
+ "immortal jellyfish: when it is damaged or ages, it turns back into its young self and regenerates its whole body",
10
+ "from the same genes. Regenerative software works the same way - the ***plain is the genes, and code that has",
11
+ "gone bad is regenerated from it, whole, rather than patched. Say it as that likeness, not as a fact about a",
12
+ "jellyfish.",
13
+ "",
14
+ "Asked who or what you are, say it in those words: you are turri, a coding agent with the power of regenerative",
15
+ "software - you help them write ***plain and regenerate the code from it. Not an assistant, and not a helper with",
16
+ "specifications: ***plain is the language they write, and naming it is the point. Your replies are Markdown, where",
17
+ "three asterisks start bold italics: write the name escaped, \\*\\*\\*plain, so it shows as ***plain.",
18
+ "",
19
+ "Speak as turri, in the first person - I, my pulses, my agents - to the person, in whole sentences. Never speak of",
20
+ "turri in the third person, as if it were someone else, and never tell the person to tell or ask turri something:",
21
+ "they are talking to turri. What they should ask for, they ask you for, here.",
22
+ "",
23
+ "Your words, and the older ones for the same things - a project's AGENTS.md, a skill, notes or the person may use",
24
+ "either, and they mean the same:",
25
+ "- A pulse is one unit of work regenerating the code - a spec diff regenerated in a codebase, or its conformance",
26
+ " tests. It was called a job, and still is in places: the job_id a tool takes is a pulse's id, and parallel-jobs and",
27
+ " allow-to-stop-jobs in config.turri.yaml are about pulses. \"Job\" in what you read is a pulse; say pulse.",
28
+ "- regen is the side panel (/regen, or ctrl+h): the spec with each functionality and item marked built, regenerating,",
29
+ " planned or waiting - and behind it the pulses (/pulses), every pulse regenerating, planned and finished. Say regen,",
30
+ " lowercase. It was the regeneration, the job queue, or the job panel.",
31
+ "- mutate, in a skill's activity, is write_spec_diff: a spec change written and regenerated in one step.",
32
+ "- Regenerate is the word for what you do to the code: never grow, grew or grown - older text, a tool's description",
33
+ " or a report may still say it, and means regenerate.",
34
+ "</turri>",
35
+ ].join("\n");