@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
@@ -0,0 +1,241 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { PLAIN_SOURCE_FILE_EXTENSION, plainFileParser, withParseCache } from "../plain/parser.js";
4
+ import { getFrids, getInheritedFunctionalSpecs } from "../plain/spec.js";
5
+ import { buildDirUnder, modulesRoot, projectDirOf } from "./paths.js";
6
+ import { notesPath } from "./notes.js";
7
+ import { resolveTemplateDirs } from "./paths.js";
8
+ import { inheritedRecord, readRecord, RECORD_FILE, writeCodeRecord } from "./records.js";
9
+ import { head, historyLog, recordHistory } from "./lineage.js";
10
+ import { listFiles } from "./testing.js";
11
+ function locateModule(moduleName, templateDirs) {
12
+ for (const dir of templateDirs) {
13
+ const candidate = join(dir, moduleName + PLAIN_SOURCE_FILE_EXTENSION);
14
+ if (existsSync(candidate))
15
+ return candidate;
16
+ }
17
+ throw new Error(`Required module '${moduleName}' not found in: ${templateDirs.join(", ")}`);
18
+ }
19
+ /**
20
+ * The chain to build, bottom first.
21
+ *
22
+ * Depth-first, post-order: everything a module requires is listed before the module
23
+ * itself. The parser has already rejected circular requires, and a module reached twice is
24
+ * emitted once.
25
+ */
26
+ export function resolveRequiresChain(plainFile, templateDirs = [], buildRoot,
27
+ /** Accept the named module without functional specs yet. Those it requires must have them. */
28
+ options = {}) {
29
+ const dirs = resolveTemplateDirs(plainFile, templateDirs);
30
+ const root = resolve(buildRoot ?? modulesRoot(projectDirOf(plainFile)));
31
+ const chain = [];
32
+ const seen = new Set();
33
+ const visit = (file, requiredBy) => {
34
+ const [moduleName, tree, requires] = plainFileParser(basename(file), dirs, requiredBy.length === 0 ? options : {});
35
+ if (requiredBy.includes(moduleName)) {
36
+ throw new Error(`Circular requires: ${[...requiredBy, moduleName].join(" -> ")}`);
37
+ }
38
+ if (seen.has(moduleName))
39
+ return moduleName;
40
+ let previous = null;
41
+ for (const required of requires) {
42
+ previous = visit(locateModule(required, dirs), [...requiredBy, moduleName]);
43
+ }
44
+ seen.add(moduleName);
45
+ chain.push({
46
+ moduleName,
47
+ plainFile: resolve(file),
48
+ buildDir: buildDirUnder(root, moduleName),
49
+ requires: previous,
50
+ frids: [...getFrids(tree)],
51
+ });
52
+ return moduleName;
53
+ };
54
+ visit(resolve(plainFile), []);
55
+ return chain;
56
+ }
57
+ /**
58
+ * The codebase a change to `plainFile`'s module lands in: the furthest-built module whose
59
+ * requires chain includes it, with that chain.
60
+ *
61
+ * Code accumulates up a chain, so once modules have been built on top of this one, its own
62
+ * build is only an early snapshot - the living codebase is the one at the tip. A change to
63
+ * this module's spec is made there, where everything built since can be kept working, not
64
+ * in the snapshot, where it would have to be copied up over newer work.
65
+ *
66
+ * Found by reading every .plain file beside this one. Where built descendants sit on
67
+ * different branches there is no single tip, and choosing one is a decision: `inModule`
68
+ * names it, and without it this refuses rather than guesses.
69
+ */
70
+ export function findCodebaseTip(plainFile, options = {}) {
71
+ return withParseCache(() => findTip(plainFile, options));
72
+ }
73
+ /**
74
+ * The chains of the built modules whose requires chain includes `plainFile`'s module, and that no other
75
+ * built module is built on - where its code lives now, one chain per branch. Empty when nothing built is
76
+ * built on it. Found by reading every .plain file beside it.
77
+ */
78
+ export function builtTips(plainFile, options = {}) {
79
+ return withParseCache(() => {
80
+ const templateDirs = options.templateDirs ?? [];
81
+ const self = resolveRequiresChain(plainFile, templateDirs, options.buildRoot, { allowNoFunctionalities: true }).at(-1);
82
+ return tipsOf(builtChainsOver(self.moduleName, plainFile, templateDirs, options.buildRoot));
83
+ });
84
+ }
85
+ /** Every chain of a built module whose requires chain includes `moduleName`. */
86
+ function builtChainsOver(moduleName, plainFile, templateDirs, buildRoot) {
87
+ const candidates = [];
88
+ const seen = new Set();
89
+ for (const dir of resolveTemplateDirs(plainFile, templateDirs)) {
90
+ let names;
91
+ try {
92
+ names = readdirSync(dir).filter((name) => name.endsWith(PLAIN_SOURCE_FILE_EXTENSION));
93
+ }
94
+ catch {
95
+ continue;
96
+ }
97
+ for (const name of names) {
98
+ const file = join(dir, name);
99
+ if (seen.has(file))
100
+ continue;
101
+ seen.add(file);
102
+ let chain;
103
+ try {
104
+ chain = resolveRequiresChain(file, templateDirs, buildRoot);
105
+ }
106
+ catch {
107
+ // Import modules, and anything that does not parse, are not codebases.
108
+ continue;
109
+ }
110
+ const top = chain.at(-1);
111
+ const includes = chain.some((entry) => entry.moduleName === moduleName);
112
+ if (includes && listFiles(top.buildDir).length > 0)
113
+ candidates.push(chain);
114
+ }
115
+ }
116
+ return candidates;
117
+ }
118
+ /** Of built chains, those no other is built on top of. */
119
+ function tipsOf(candidates) {
120
+ const within = (chain) => chain.slice(0, -1).map((entry) => entry.moduleName);
121
+ return candidates.filter((chain) => !candidates.some((other) => other !== chain && within(other).includes(chain.at(-1).moduleName)));
122
+ }
123
+ function findTip(plainFile, options) {
124
+ const templateDirs = options.templateDirs ?? [];
125
+ const own = resolveRequiresChain(plainFile, templateDirs, options.buildRoot, {
126
+ allowNoFunctionalities: options.allowNoFunctionalities,
127
+ });
128
+ const self = own.at(-1);
129
+ const candidates = builtChainsOver(self.moduleName, plainFile, templateDirs, options.buildRoot);
130
+ if (options.inModule !== undefined) {
131
+ const chosen = candidates.find((chain) => chain.at(-1).moduleName === options.inModule);
132
+ if (chosen === undefined) {
133
+ throw new Error(`'${options.inModule}' is not a built module that requires '${self.moduleName}'. ` +
134
+ `Built ones that do: ${candidates.map((c) => c.at(-1).moduleName).join(", ") || "none"}.`);
135
+ }
136
+ return { tip: chosen.at(-1), chain: chosen };
137
+ }
138
+ // A tip is a candidate that no other candidate is built on top of.
139
+ const tips = tipsOf(candidates);
140
+ if (tips.length === 0)
141
+ return { tip: self, chain: own };
142
+ if (tips.length > 1) {
143
+ throw new Error(`'${self.moduleName}' is built on in more than one branch: ${tips.map((c) => c.at(-1).moduleName).join(", ")}. ` +
144
+ "Say which codebase to regenerate in.");
145
+ }
146
+ return { tip: tips[0].at(-1), chain: tips[0] };
147
+ }
148
+ /**
149
+ * Copy a required module's codebase into the build that is about to be created.
150
+ *
151
+ * What the code is built from comes too, converted: what the source module built becomes what
152
+ * the new one inherits, while the new module's own functional specs start empty. Copied as-is, it would claim the source's work as the new
153
+ * module's own. And the conformance tests come, into the new module's tests directory.
154
+ */
155
+ export function copyRequiredCode(from, to, toModule,
156
+ /** The module the code comes from: its functional specs are what the new one inherits. */
157
+ fromModule) {
158
+ if (!existsSync(from))
159
+ return [];
160
+ mkdirSync(to, { recursive: true });
161
+ const copied = [];
162
+ for (const file of listFiles(from)) {
163
+ if (basename(file) === RECORD_FILE)
164
+ continue;
165
+ const target = join(to, file);
166
+ mkdirSync(dirname(target), { recursive: true });
167
+ copyFileSync(join(from, file), target);
168
+ copied.push(file);
169
+ }
170
+ // The working notes describe this code, so they come up with it.
171
+ if (existsSync(notesPath(from)) && !existsSync(notesPath(to)))
172
+ copyFileSync(notesPath(from), notesPath(to));
173
+ // What the code is built from comes with it, as what the new module inherits - not as its own work.
174
+ const built = readRecord(from);
175
+ writeCodeRecord(to, built !== null ? inheritedRecord(built, fromModule) : {});
176
+ // A new module's history starts here: where its code came from, and at which commit.
177
+ const source = basename(dirname(from));
178
+ const at = head(from);
179
+ const recordStart = () => recordHistory(to, `start from ${source}${at ? ` at ${at}` : ""}`);
180
+ // So do the conformance tests: they check the code that was just copied, and the new
181
+ // module's regression suite has to keep them passing. They sit beside the code, in the
182
+ // module's `tests` directory.
183
+ const testsFrom = join(dirname(from), "tests");
184
+ const testsTo = join(dirname(to), "tests");
185
+ if (existsSync(testsFrom)) {
186
+ for (const file of listFiles(testsFrom)) {
187
+ const target = join(testsTo, file);
188
+ mkdirSync(dirname(target), { recursive: true });
189
+ copyFileSync(join(testsFrom, file), target);
190
+ }
191
+ // And their notes, which listFiles leaves out as it does the code's.
192
+ if (existsSync(notesPath(testsFrom)) && !existsSync(notesPath(testsTo)))
193
+ copyFileSync(notesPath(testsFrom), notesPath(testsTo));
194
+ }
195
+ recordStart();
196
+ return copied;
197
+ }
198
+ /**
199
+ * A module the chain requires now that a module built in it was not built on: one required for
200
+ * the first time - a new module the specs were split into, say - and the built modules above it,
201
+ * whose code was started without it and so has to start again from scratch.
202
+ *
203
+ * A module was built on what its record lists as inherited; one with nothing recorded, on what it
204
+ * was started from, as its history says. A module below it with nothing built was never under it.
205
+ */
206
+ export function newlyRequired(chain) {
207
+ for (const [index, entry] of chain.entries()) {
208
+ if (index === 0 || listFiles(entry.buildDir).length === 0)
209
+ continue;
210
+ const record = readRecord(entry.buildDir);
211
+ if (record === null)
212
+ continue;
213
+ const recorded = getInheritedFunctionalSpecs(record);
214
+ const known = new Set(recorded.map((spec) => spec.module));
215
+ const history = known.size === 0 ? historyLog(entry.buildDir, 10_000) : "";
216
+ const missing = chain.slice(0, index).filter((below) => {
217
+ if (listFiles(below.buildDir).length === 0)
218
+ return true;
219
+ if (known.size > 0)
220
+ return !known.has(below.moduleName);
221
+ // Nothing inherited on record: new only when the history shows the code was never started from it.
222
+ return below.moduleName === entry.requires && !history.startsWith("No history") && !history.includes(`start from ${below.moduleName}`);
223
+ });
224
+ if (missing.length > 0) {
225
+ return { modules: missing.map((below) => below.moduleName), rebuild: chain.slice(index).filter((above) => listFiles(above.buildDir).length > 0) };
226
+ }
227
+ }
228
+ return undefined;
229
+ }
230
+ /**
231
+ * Take a module back to nothing - its code, its conformance tests and what they were built from -
232
+ * so it is built again from the start, on the module it now requires. Committed, so the code that
233
+ * was there is in its history.
234
+ */
235
+ export function startFromScratch(entry, why) {
236
+ const testsDir = join(dirname(entry.buildDir), "tests");
237
+ for (const dir of [entry.buildDir, testsDir])
238
+ rmSync(dir, { recursive: true, force: true });
239
+ mkdirSync(entry.buildDir, { recursive: true });
240
+ recordHistory(entry.buildDir, `start from scratch: ${why}`);
241
+ }
@@ -0,0 +1,164 @@
1
+ import { copyFileSync, mkdirSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
3
+ import { defineTool } from "@earendil-works/pi-coding-agent";
4
+ import { Type } from "typebox";
5
+ import { collectLinkedResources } from "../plain/spec.js";
6
+ import { scriptKind } from "./test-scripts.js";
7
+ const MAX_RESOURCE_CHARS = 8000;
8
+ /** The images a spec can link, by extension: a model reading images is sent them as they are. */
9
+ const IMAGE_TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg" };
10
+ /** The largest image a model provider takes in one piece. */
11
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
12
+ /** The MIME type of a linked image, by its extension; undefined for any other file. */
13
+ export function imageTypeOf(target) {
14
+ return IMAGE_TYPES[extname(target).toLowerCase()];
15
+ }
16
+ /** A linked image too large to send, or unreadable: the job stops, saying which file and why. */
17
+ export class LinkedImageError extends Error {
18
+ }
19
+ /**
20
+ * Load the resources linked from the specifications in scope for one functionality.
21
+ *
22
+ * `collectLinkedResources` stops at the requested FRID, so a resource introduced by a
23
+ * later functionality is not pulled into this prompt.
24
+ */
25
+ export function loadLinkedResources(tree,
26
+ /** Up to and including this functionality. Null: the whole module. */
27
+ frid, templateDirs) {
28
+ const collected = [];
29
+ collectLinkedResources(tree, collected, null, true, frid);
30
+ const loaded = [];
31
+ for (const resource of collected) {
32
+ // A testing script reaches an agent as one - whole, and only when it is run on its work -
33
+ // not as a resource, cut short and whether it is used or not.
34
+ if (scriptKind(resource.target) !== undefined)
35
+ continue;
36
+ const mimeType = imageTypeOf(resource.target);
37
+ if (mimeType !== undefined) {
38
+ const image = loadImage(templateDirs, resource.target, mimeType);
39
+ if (image === undefined)
40
+ continue;
41
+ const number = loaded.filter((other) => other.image !== undefined).length + 1;
42
+ loaded.push({ text: resource.text, target: resource.target, content: `[an image: attached to your first message, image ${number}]`, image });
43
+ continue;
44
+ }
45
+ const source = [...templateDirs.map((dir) => join(dir, resource.target)), resource.target].find((path) => isFileAt(path));
46
+ if (source === undefined)
47
+ continue;
48
+ let content = readFileSync(source, "utf8");
49
+ const whole = content.length;
50
+ if (whole > MAX_RESOURCE_CHARS) {
51
+ content = isCodeResource(resource.target)
52
+ ? `${content.slice(0, MAX_RESOURCE_CHARS)}\n... [cut here: ${whole} characters whole - code that lives outside your codebase: build on it as the specification says, never copy it]`
53
+ : `${content.slice(0, MAX_RESOURCE_CHARS)}\n... [cut here: ${whole} characters whole - copy_resource puts the whole file at its code-copy path, to read it there]`;
54
+ }
55
+ loaded.push({ text: resource.text, target: resource.target, content, source: resolve(source), ...(whole > MAX_RESOURCE_CHARS ? { cutFrom: whole } : {}) });
56
+ }
57
+ return loaded;
58
+ }
59
+ function isFileAt(path) {
60
+ try {
61
+ return statSync(path).isFile();
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ /** Source files, by extension: a linked resource that is one is code that lives elsewhere, never copied in. */
68
+ const CODE_EXTENSIONS = new Set([
69
+ ".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java", ".kt", ".kts", ".scala", ".groovy", ".go", ".rs", ".rb",
70
+ ".php", ".cs", ".fs", ".vb", ".swift", ".m", ".mm", ".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".dart", ".lua", ".ex",
71
+ ".exs", ".erl", ".clj", ".hs", ".ml", ".r", ".jl", ".sh", ".bash", ".zsh", ".ps1", ".vue", ".svelte",
72
+ ]);
73
+ /** Build manifests and lock files, by name: the dependencies of a codebase that is not this one. */
74
+ const MANIFESTS = /^(requirements[\w.-]*\.txt|pyproject\.toml|setup\.cfg|pipfile(\.lock)?|poetry\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|pom\.xml|build\.gradle(\.kts)?|settings\.gradle(\.kts)?|go\.(mod|sum)|cargo\.(toml|lock)|gemfile(\.lock)?|composer\.(json|lock)|[\w.-]+\.csproj|makefile|dockerfile)$/i;
75
+ /**
76
+ * A linked resource that is code - a source file, or a build manifest: the specification points at code that lives
77
+ * elsewhere, a host's module the code plugs into, for the code to build on as it says. It is never copied into the
78
+ * codebase, nor recreated there; a resource that is data - a fixture, a schema, a sample - is.
79
+ */
80
+ export function isCodeResource(target) {
81
+ const name = target.split(/[\\/]/).pop() ?? target;
82
+ return CODE_EXTENSIONS.has(extname(name).toLowerCase()) || MANIFESTS.test(name);
83
+ }
84
+ /** Where in a codebase a linked resource's one copy goes: its path, without the steps up and the leading `./`. */
85
+ export function codeCopyOf(target) {
86
+ return target
87
+ .split(/[\\/]/)
88
+ .filter((part) => part !== "" && part !== "." && part !== "..")
89
+ .join("/");
90
+ }
91
+ /**
92
+ * `copy_resource`: a linked resource copied whole into the agent's own folder - at its code-copy path, or where it
93
+ * says - by the harness, byte for byte. The agent's prompt shows a long resource cut short, and the file itself is
94
+ * outside its walls: copied, it reads the whole of it there, and the code or the tests use that one copy.
95
+ */
96
+ export function createCopyResourceTool(options) {
97
+ const root = resolve(options.dir);
98
+ const copyable = options.resources.filter((resource) => resource.source !== undefined && resource.image === undefined);
99
+ return defineTool({
100
+ name: "copy_resource",
101
+ label: "Copy a linked resource",
102
+ description: "Copy a linked resource that is data - a fixture, a schema, a sample - whole into your folder, at its code-copy path " +
103
+ "unless you give another inside your folder, byte for byte: for a file the code or the tests read, or to read one the " +
104
+ "prompt shows cut short. A resource that is code - a source file, a build manifest - is never copied: it lives elsewhere.",
105
+ promptSnippet: "copy_resource: a linked resource copied whole into your folder, at its code-copy path",
106
+ parameters: Type.Object({
107
+ path: Type.String({ description: "The resource's path, as <linked resources> gives it" }),
108
+ to: Type.Optional(Type.String({ description: "Where in your folder, relative to it - its code-copy path when left out" })),
109
+ }),
110
+ async execute(_id, params) {
111
+ const { path, to } = params;
112
+ const resource = copyable.find((candidate) => candidate.target === path || codeCopyOf(candidate.target) === codeCopyOf(path));
113
+ if (resource === undefined) {
114
+ const known = copyable.map((candidate) => candidate.target).join(", ");
115
+ return { content: [{ type: "text", text: `No linked resource ${path}. ${known !== "" ? `The ones you have: ${known}.` : "You have none to copy."}` }], details: {} };
116
+ }
117
+ if (isCodeResource(resource.target)) {
118
+ return {
119
+ content: [
120
+ {
121
+ type: "text",
122
+ text: `Not copied: ${resource.target} is code - a source file or a build manifest that lives outside your codebase, linked ` +
123
+ "so you know what your code plugs into. Build on it as the specification says - import it, call it, depend on " +
124
+ "what it lists - and never copy it, nor recreate it or what it belongs to in your codebase.",
125
+ },
126
+ ],
127
+ details: {},
128
+ };
129
+ }
130
+ const target = resolve(root, to ?? codeCopyOf(resource.target));
131
+ const inside = relative(root, target);
132
+ if (inside === "" || inside.startsWith("..") || isAbsolute(inside)) {
133
+ return { content: [{ type: "text", text: `Not copied: ${to} is not inside your folder, ${root}.` }], details: {} };
134
+ }
135
+ mkdirSync(dirname(target), { recursive: true });
136
+ copyFileSync(resource.source, target);
137
+ const size = statSync(target).size;
138
+ return { content: [{ type: "text", text: `Copied ${resource.target} whole to ${inside} (${size} bytes). Read it there.` }], details: {} };
139
+ },
140
+ });
141
+ }
142
+ /** The images among loaded resources, in the order the prompt numbers them. */
143
+ export function resourceImages(resources) {
144
+ return resources.flatMap((resource) => (resource.image !== undefined ? [resource.image] : []));
145
+ }
146
+ /** A linked image's bytes, found as a text resource is: the template folders first, then the current directory. */
147
+ function loadImage(templateDirs, target, mimeType) {
148
+ for (const path of [...templateDirs.map((dir) => join(dir, target)), target]) {
149
+ let size;
150
+ try {
151
+ if (!statSync(path).isFile())
152
+ continue;
153
+ size = statSync(path).size;
154
+ }
155
+ catch {
156
+ continue;
157
+ }
158
+ if (size > MAX_IMAGE_BYTES) {
159
+ throw new LinkedImageError(`The linked image ${target} is ${(size / 1024 / 1024).toFixed(1)} MB - a model takes images up to ${MAX_IMAGE_BYTES / 1024 / 1024} MB. Make it smaller, or link a smaller one.`);
160
+ }
161
+ return { type: "image", data: readFileSync(path).toString("base64"), mimeType };
162
+ }
163
+ return undefined;
164
+ }
@@ -0,0 +1,111 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { join, relative, resolve, sep } from "node:path";
3
+ import { defineTool } from "@earendil-works/pi-coding-agent";
4
+ import { Type } from "typebox";
5
+ import { workDiff } from "./lineage.js";
6
+ import { lastPassingMs, runOnCode } from "./test-scripts.js";
7
+ /**
8
+ * Tools an agent checks its own work with, before the harness and the reviewer do: the diff the reviewer is shown, and
9
+ * whether a test catches a change to the code. Neither decides anything - the testing scripts, run by the harness, still do.
10
+ */
11
+ /** The work so far, as the reviewer is shown it: a diff of the folder from the commit the job started from. */
12
+ export function createMyDiffTool(options) {
13
+ const folder = options.what === "code" ? "code folder" : "tests folder";
14
+ return defineTool({
15
+ name: "my_diff",
16
+ label: "My diff",
17
+ description: `The diff of the ${folder} from where this work started - every file changed, added or taken out, new files ` +
18
+ "whole - exactly what the reviewer is shown of your work. Read it before you hand the work in: every part of the " +
19
+ "change should be in it, and nothing else.",
20
+ promptSnippet: `my_diff: your work so far, as the reviewer sees it - the ${folder} from where the work started`,
21
+ parameters: Type.Object({}),
22
+ async execute() {
23
+ const diff = workDiff(options.dir);
24
+ return { content: [{ type: "text", text: diff === "" ? `Nothing in the ${folder} has changed yet.` : diff }], details: {} };
25
+ },
26
+ });
27
+ }
28
+ const outputOf = (run) => `${run.command} exited ${run.exitCode}${run.timedOut === true ? " - stopped by the time limit" : ""}\n\n${run.output}`;
29
+ /** How many times can_it_fail runs in one session, by default: each is a run of the unit testing script. */
30
+ export const CAN_IT_FAIL_RUNS = 6;
31
+ /**
32
+ * How long a can_it_fail run may take: three times the last passing run of the unit tests on this code, within 15 and
33
+ * 60 seconds - 60 before one has passed. A bug put in the code often makes it loop or wait for ever: that is found out
34
+ * in a short time, not the whole limit of a test run, and well within the agent's turn.
35
+ */
36
+ export function canItFailLimitMs(buildDir, script) {
37
+ const last = lastPassingMs(buildDir, script);
38
+ return last === undefined ? 60_000 : Math.min(Math.max(last * 3, 15_000), 60_000);
39
+ }
40
+ /**
41
+ * Whether the unit tests catch a change to the code: the change made to a fresh copy - never to the code itself - and
42
+ * the unit testing script run on it. A test that passes whatever the code does tests nothing.
43
+ */
44
+ export function createCanItFailTool(options) {
45
+ let left = options.runs ?? CAN_IT_FAIL_RUNS;
46
+ return defineTool({
47
+ name: "can_it_fail",
48
+ label: "Can it fail",
49
+ description: "Check that the unit tests catch a bug: give one change to the code that breaks a behaviour the specification " +
50
+ "states - a value or a status changed, a check taken out, a condition turned round where it cannot make a loop run for " +
51
+ "ever - and the unit testing script " +
52
+ "runs on a copy of the code with that change. The code itself is never touched. The tests should fail; if they all " +
53
+ "pass, no test catches that bug. Make a change that still builds and runs: a failure only because it no longer " +
54
+ `compiles or imports says nothing about the tests. ${options.runs ?? CAN_IT_FAIL_RUNS} runs at most.`,
55
+ promptSnippet: "can_it_fail: whether the unit tests catch a bug you put in a copy of the code",
56
+ parameters: Type.Object({
57
+ file: Type.String({ description: "The code file to change, relative to the code folder" }),
58
+ find: Type.String({ description: "Text that is in the file exactly once - the code to change" }),
59
+ replace: Type.String({ description: "What it becomes: the bug" }),
60
+ }),
61
+ async execute(_id, params) {
62
+ const { file, find, replace } = params;
63
+ const say = (text) => ({ content: [{ type: "text", text }], details: {} });
64
+ if (left <= 0)
65
+ return say("can_it_fail has no runs left in this session: decide with what it showed.");
66
+ const path = resolve(options.buildDir, file);
67
+ if (relative(options.buildDir, path).startsWith(`..${sep}`) || relative(options.buildDir, path) === "..")
68
+ return say(`${file} is not in the code folder.`);
69
+ let text;
70
+ try {
71
+ text = readFileSync(path, "utf8");
72
+ }
73
+ catch {
74
+ return say(`There is no ${file} in the code folder.`);
75
+ }
76
+ const at = text.indexOf(find);
77
+ if (at === -1)
78
+ return say(`"${find}" is not in ${file}: give text that is in it exactly as written.`);
79
+ if (text.indexOf(find, at + 1) !== -1)
80
+ return say(`"${find}" is in ${file} more than once: give more of it, so it is there once.`);
81
+ if (find === replace)
82
+ return say("The change changes nothing.");
83
+ left -= 1;
84
+ let run;
85
+ try {
86
+ run = await runOnCode(options.buildDir, options.script, "unit testing script, with the change", (copy) => writeFileSync(join(copy, relative(options.buildDir, path)), text.slice(0, at) + replace + text.slice(at + find.length)), canItFailLimitMs(options.buildDir, options.script));
87
+ }
88
+ catch (error) {
89
+ return say(error instanceof Error ? error.message : String(error));
90
+ }
91
+ const verdict = run.timedOut === true
92
+ ? `The unit tests did not finish in ${Math.round(run.timeoutMs / 1000)}s with ${file} changed: the bug makes the code hang - a loop that never ends, a wait for something that never comes. That shows the code breaks, not that a test catches it: the tests hang too. Put in a bug that makes a result wrong instead - a value, a status, a field - and check again.`
93
+ : run.exitCode === 0
94
+ ? `Every unit test passes with ${file} changed: no test catches this bug. A test that should - one for the behaviour this code is for - does not check what it says.`
95
+ : `The unit tests fail with ${file} changed: something catches it. Check in the output below that a test's assertion caught it - a failure only because the copy no longer builds or imports says nothing about the tests.`;
96
+ // A hang prints the same thing over and over: its last lines are enough.
97
+ const shown = run.timedOut === true ? `${run.command} was stopped after ${Math.round(run.timeoutMs / 1000)}s. Its last output:\n\n${run.output.trimEnd().slice(-2_000)}` : outputOf(run);
98
+ return { content: [{ type: "text", text: `${verdict} (${left} run${left === 1 ? "" : "s"} left)\n\n${shown}` }], details: { exitCode: run.exitCode } };
99
+ },
100
+ });
101
+ }
102
+ /** When the coding agent checks its own work, by the tools it has: before the unit tests, and before it hands the work in. */
103
+ export function selfCheckNote(tools) {
104
+ const has = (name) => tools.includes(name);
105
+ const steps = [
106
+ ...(has("can_it_fail") ? ["for each behaviour you built, check with can_it_fail that a test fails when it breaks"] : []),
107
+ ...(has("start_service") ? ["where you built a server, start it with start_service and try what you built with curl, as a client would"] : []),
108
+ ...(has("my_diff") ? ["read my_diff: every part of the change in it, and nothing else"] : []),
109
+ ];
110
+ return steps.length > 0 ? [`Check your own work before you hand it in: ${steps.join("; ")}.`, ""] : [];
111
+ }