@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,119 @@
1
+ import { DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS } from "../plain/spec.js";
2
+ import { describeSpecChange } from "./prompt.js";
3
+ import { applyChanges, changesBetween, parseTree, serializeTree } from "./spec-steps.js";
4
+ /**
5
+ * JobSteering a running code job: the spec changed while it regenerates, inside what it is regenerating - a functional
6
+ * spec it builds reworded, a sub-bullet added under one, a definition or an implementation req it is given edited, a
7
+ * file one links edited - and the coding agent is told, rather than the job stopped or the change left to the next
8
+ * one. A new or removed item is not a steer: the work changes shape, and that is the next job's. At most MAX_STEERS a
9
+ * job; the one after is the next job's, or stops this one, as `allow-to-stop-jobs` says.
10
+ *
11
+ * Three trees. `target`: what the job lands as built from - its code record. `agent`: what its agent is to build -
12
+ * the target, and a definition or req edited that functional specs built before the job use too: the agent is told,
13
+ * but the record keeps what those were built from, so the loop patches them - this one's with them - once it lands.
14
+ * `told`: what the agent has been told. A steer is waiting while `told` is not `agent`; edits made before it is
15
+ * given are one steer, the newest of each item.
16
+ */
17
+ export const MAX_STEERS = 2;
18
+ export function createSteering(tree, module) {
19
+ return { target: tree, agent: tree, told: tree, steers: 0, notes: [], ...(module !== undefined ? { module } : {}) };
20
+ }
21
+ /** What the agent is to be told and has not been: the steer waiting, or undefined when there is none. */
22
+ export function waitingSteer(steering) {
23
+ const changes = changesBetween(serializeTree(steering.told), serializeTree(steering.agent));
24
+ return changes.length === 0 ? undefined : steerText(changes, steering.module);
25
+ }
26
+ /** The agent has it now. */
27
+ export function markTold(steering) {
28
+ steering.told = steering.agent;
29
+ }
30
+ /** The message a steer is: the change since it was last told, as its first one was - and to make the code match. */
31
+ export function steerText(changes, moduleName) {
32
+ return [
33
+ "The spec changed while you work, in what you are building. The change, from the spec as you were given it to the spec as it is now:",
34
+ "",
35
+ describeSpecChange(changes, moduleName),
36
+ "",
37
+ "Make the code and its unit tests match the spec as it is now - this change, and nothing else changes. Carry on from where you are: the harness runs the unit tests again once you have.",
38
+ ].join("\n");
39
+ }
40
+ /** A definition's concept: `:User:` of `- :User: is a person.`. */
41
+ function conceptOf(serialized) {
42
+ if (serialized === undefined)
43
+ return undefined;
44
+ try {
45
+ const item = JSON.parse(serialized);
46
+ const text = typeof item === "string" ? item : (item.markdown ?? "");
47
+ return /^-\s+(:[^:\s]+:)/.exec(text)?.[1];
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ /** The functional specs of a tree, a line each, as its serialized form - and so a change's text - has them. */
54
+ function functionalSpecs(tree) {
55
+ const header = JSON.stringify([FUNCTIONAL_REQUIREMENTS]);
56
+ const lines = serializeTree(tree).split("\n");
57
+ const start = lines.indexOf(header);
58
+ if (start === -1)
59
+ return [];
60
+ const own = [];
61
+ for (const line of lines.slice(start + 1)) {
62
+ if (line === "" || line.startsWith("["))
63
+ break;
64
+ own.push(line);
65
+ }
66
+ return own;
67
+ }
68
+ /**
69
+ * The part of `changes` - from a tree the job regenerates towards, to the spec - that steers it: changes to items it
70
+ * already has, never one added or removed. A functional spec only when it is one of the job's own (`mine`, as the job
71
+ * regenerates it); a definition or an implementation req always. `shared`: those of them that functional specs built
72
+ * before the job (`built`) use too - the agent is told, but they stay out of the record.
73
+ */
74
+ export function steerableChanges(changes, mine, built) {
75
+ const steer = [];
76
+ const shared = [];
77
+ for (const change of changes) {
78
+ if (change.kind !== "change")
79
+ continue;
80
+ if (change.section === FUNCTIONAL_REQUIREMENTS) {
81
+ if (mine.has(change.before ?? ""))
82
+ steer.push(change);
83
+ }
84
+ else if (change.section === DEFINITIONS) {
85
+ steer.push(change);
86
+ const concept = conceptOf(change.before) ?? conceptOf(change.after);
87
+ if (concept !== undefined && built.some((spec) => spec.includes(concept)))
88
+ shared.push(change);
89
+ }
90
+ else if (change.section === NON_FUNCTIONAL_REQUIREMENTS) {
91
+ steer.push(change);
92
+ if (built.length > 0)
93
+ shared.push(change);
94
+ }
95
+ }
96
+ return { steer, shared };
97
+ }
98
+ /**
99
+ * Steer a job, when the spec moved on inside what it regenerates: the trees it moves to, or undefined when nothing in
100
+ * the change steers it. `toSpec(tree)` is the work from a tree to the spec, as the job's track sees it; `own` the job's
101
+ * changes, from its record `from` to what it regenerates towards now.
102
+ */
103
+ export function steer(steering, from, own, toSpec) {
104
+ // The job's own functional specs, as it regenerates them; those built before it that it does not touch.
105
+ const mine = new Set(own.filter((change) => change.section === FUNCTIONAL_REQUIREMENTS && change.after !== undefined).map((change) => change.after));
106
+ const patched = new Set(own.filter((change) => change.section === FUNCTIONAL_REQUIREMENTS && change.kind === "change").map((change) => change.before ?? ""));
107
+ const built = functionalSpecs(from).filter((spec) => !patched.has(spec));
108
+ const forAgent = steerableChanges(toSpec(steering.agent), mine, built);
109
+ if (forAgent.steer.length === 0)
110
+ return undefined;
111
+ // The record takes what the agent is told but what was built before it uses too.
112
+ const forRecord = steerableChanges(toSpec(steering.target), mine, built);
113
+ const recordChanges = forRecord.steer.filter((change) => !forRecord.shared.includes(change));
114
+ return {
115
+ target: parseTree(applyChanges(serializeTree(steering.target), recordChanges)),
116
+ agent: parseTree(applyChanges(serializeTree(steering.agent), forAgent.steer)),
117
+ steered: forAgent.steer,
118
+ };
119
+ }
@@ -0,0 +1,161 @@
1
+ import { basename } from "node:path";
2
+ /** How many times each gate is tried before a build stops there. */
3
+ export const ATTEMPTS = {
4
+ "unit tests": 20,
5
+ conformance: 20,
6
+ regression: 20,
7
+ };
8
+ /**
9
+ * Where it stopped, why, and what to do about it - for the person, in a few lines, and in terms
10
+ * of the spec. The spec is all the person works on: the code is generated from it, so every fix
11
+ * offered here is a change to a `.plain` file, never to code, tests or the machine.
12
+ */
13
+ export function describeStop(facts) {
14
+ const what = facts.frid !== undefined ? `${facts.moduleName} functionality ${facts.frid}` : `the spec change in ${facts.moduleName}`;
15
+ // What actually happened, not the most it could have been.
16
+ const tries = facts.at === "unit tests" ? (facts.attempts ?? ATTEMPTS[facts.at]) : facts.at === "conformance" ? (facts.conformance?.rounds ?? ATTEMPTS[facts.at]) : ATTEMPTS[facts.at];
17
+ let why;
18
+ let todo;
19
+ // Not the spec's: what the tests are given to run with - only the person can fix it, and nothing in the spec changes.
20
+ if (facts.at === "conformance" && facts.conformance?.environment !== undefined) {
21
+ return [
22
+ `Stopped at ${what}: its conformance tests cannot pass with what their run is given - judged so ${facts.conformance.environment.times} times in a row:`,
23
+ facts.conformance.environment.reason.trim(),
24
+ "",
25
+ "What to do:",
26
+ "- Provide what the run is missing - a valid credential, an account, a service it can reach - where the conformance testing script reads it.",
27
+ "- The testing scripts run on this machine in your own environment: a credential they need is read from there - your shell's " +
28
+ "variables, or a .env the script loads.",
29
+ "",
30
+ "Then build again: it picks up here, and what is built stays built.",
31
+ ].join("\n");
32
+ }
33
+ // Not the spec's either: the script cannot run at all, so no code would pass it.
34
+ if (facts.at === "unit tests" && facts.missingToolchain !== undefined) {
35
+ const { command } = facts.missingToolchain;
36
+ const needs = command !== undefined ? `\`${command}\`` : "a toolchain it needs";
37
+ return [
38
+ `Stopped at ${what}: the unit testing script cannot run - ${needs} is not installed on this machine, where the testing scripts run.`,
39
+ "This needs you: no change to the spec or the code makes it run, and nothing was built.",
40
+ "",
41
+ "What to do:",
42
+ `- Install ${command ?? "it"} on this machine, so the testing script finds it on its PATH.`,
43
+ "",
44
+ "Then build again: it picks up here, and what is built stays built.",
45
+ ].join("\n");
46
+ }
47
+ if (facts.at === "unit tests" && facts.staticChecks === true) {
48
+ why = `after ${tries} attempt${tries === 1 ? "" : "s"} its unit tests passed, but the static checks the unit testing script runs with them - its formatter, linter and type checker, which count as the tests do - still did not.`;
49
+ todo = [
50
+ "If a check keeps failing on something the spec or the stack asks for - a library with no type information, a rule that " +
51
+ "does not fit the stack - say in the implementation reqs how the code is to be checked, or change the checks in the unit testing script.",
52
+ "If the functional spec leaves the shape of its data open, say exactly what it is: a type checker holds the code to one shape.",
53
+ ];
54
+ }
55
+ else if (facts.at === "unit tests") {
56
+ why = `after ${tries} attempt${tries === 1 ? "" : "s"} it still could not be built to pass its own unit tests.`;
57
+ todo = [
58
+ "The functional spec is most likely too big for one functionality, or leaves a choice open that keeps coming " +
59
+ "out wrong. Split it into smaller functional specs (`analyze-if-func-spec-too-complex`, `break-down-func-spec`), " +
60
+ "or say exactly what it should do.",
61
+ "If it needs something the stack does not name - a library, a tool, a service - add it to the implementation reqs.",
62
+ "If its unit tests have to be run in a particular way, say how in the implementation reqs.",
63
+ ];
64
+ }
65
+ else if (facts.at === "conformance" && facts.conformance?.specConflict !== undefined) {
66
+ const { functionalities, reason } = facts.conformance.specConflict;
67
+ const [one, two] = functionalities.map((ref) => `${ref.module} ${ref.frid}`);
68
+ why = `its spec contradicts itself: functionalities ${one} and ${two} cannot both hold. ${reason.trim()}`;
69
+ todo = [
70
+ `Compare functional specs ${one} and ${two} (\`analyze-2-func-specs\`, \`resolve-spec-conflict\`) and change whichever is wrong - ` +
71
+ "or both, so they say the same thing about where they meet.",
72
+ "If one is meant to change what the other does, say so in the one that changes it, and update its acceptance tests to match.",
73
+ ];
74
+ }
75
+ else if (facts.at === "conformance" && facts.conformance?.review !== undefined && facts.conformance.stuck === undefined) {
76
+ const problems = facts.conformance.review;
77
+ why =
78
+ `its conformance tests pass, but never passed review in ${tries} round${tries === 1 ? "" : "s"}: they do not yet test what the spec says, ` +
79
+ `black box. What the review last found:\n${problems.slice(0, 8).map((problem) => ` - ${problem.kind === "missing" ? "missing" : "bad"} · ${problem.file}${problem.test ? ` · ${problem.test}` : ""}: ${problem.what.trim().replace(/\s+/g, " ")}`).join("\n")}`;
80
+ todo = [
81
+ "If an acceptance test asks for something the tests cannot do - a service they cannot reach, data they cannot set up - say in the test " +
82
+ "reqs how they do it, or change the acceptance test to what can be checked.",
83
+ "If the test reqs leave open how the software is reached or what may be mocked, say so exactly.",
84
+ ];
85
+ }
86
+ else if (facts.at === "conformance" && facts.conformance?.unlabelled !== undefined) {
87
+ why =
88
+ "its conformance tests were never written and labelled as the two kinds - conformance tests and each acceptance test - " +
89
+ `though they were sent back for it again and again. Still missing:\n${facts.conformance.unlabelled.map((line) => ` - ${line}`).join("\n")}`;
90
+ todo = ["If the test reqs leave open where the tests go or how they are built, say so exactly - the folder, the files, the build."];
91
+ }
92
+ else if (facts.at === "conformance" && facts.conformance?.stuck !== undefined) {
93
+ const { error, times, rounds, changedNothing } = facts.conformance.stuck;
94
+ why =
95
+ changedNothing === true
96
+ ? `its conformance tests kept failing with ${error}, and the last ${times} fixes - in ${rounds} attempts - changed nothing, ` +
97
+ "so nothing more would have come of the rest of them."
98
+ : `its conformance tests kept failing with ${error} - ${times} times, and ${times} more after the explorer was asked again ` +
99
+ `what they lacked, in ${rounds} attempts - so nothing more would have come of the rest of them.`;
100
+ todo = [
101
+ "The tests most likely cannot reach the software the way the test reqs have them do it. Say exactly how in the test reqs: " +
102
+ "what the tests depend on or start - the package or entry point the conformance testing script builds or installs, by its exact name - " +
103
+ "and how they start it.",
104
+ "If the tests need longer than the time limit for a run - a long-running request - raise TURRI_COMMAND_TIMEOUT, or say in " +
105
+ "the test reqs how they keep it short.",
106
+ "If the test reqs contradict each other, settle which is meant.",
107
+ ];
108
+ }
109
+ else if (facts.at === "conformance") {
110
+ why = `after ${tries} round${tries === 1 ? "" : "s"}, its conformance tests and its code still disagree about what the spec means.`;
111
+ todo = [
112
+ "The functional spec can be read more than one way. Say exactly what should happen - the inputs, the outputs, " +
113
+ "the edge cases - and add acceptance tests that pin down the expected result.",
114
+ "If the conformance tests are meant to be written or run differently - framework, command, setup - change the test reqs.",
115
+ ];
116
+ }
117
+ else {
118
+ why =
119
+ `building it broke what was built before it, and ${facts.repairs ?? tries} attempt${facts.repairs === 1 ? "" : "s"} ` +
120
+ "to reconcile the two did not succeed.";
121
+ todo = [
122
+ "This most often means its spec contradicts an earlier one. Compare them (`analyze-func-specs`, " +
123
+ "`resolve-spec-conflict`) and change whichever is wrong.",
124
+ "If the new behaviour is meant to change what an earlier functionality does, change that earlier spec and its " +
125
+ "acceptance tests to say so.",
126
+ ];
127
+ }
128
+ return [
129
+ `Stopped at ${what}: ${why}`,
130
+ "Nothing after it was built - the functionalities after it, and the modules built on this one, wait on it.",
131
+ "",
132
+ "What to change in the spec:",
133
+ ...todo.map((line) => `- ${line}`),
134
+ "",
135
+ "Then build again: it picks up here, and what is built stays built.",
136
+ ].join("\n");
137
+ }
138
+ /**
139
+ * A spec that contradicts itself, for the person: exactly which two functional specs, as they
140
+ * read, and what cannot hold - then that the decision is theirs. Nothing about code or tests: the
141
+ * spec is all they work on, and the fix is theirs to choose.
142
+ */
143
+ export function describeSpecConflict(conflict) {
144
+ const name = (spec) => `functional spec ${spec.frid} of ${spec.module}`;
145
+ const [one, two] = conflict.specs;
146
+ return [
147
+ `The spec contradicts itself: ${name(one)} and ${name(two)} cannot both hold.`,
148
+ "",
149
+ ...conflict.specs.flatMap((spec) => [
150
+ `Functional spec ${spec.frid} of ${spec.module} (${basename(spec.plainFile)}):`,
151
+ ...spec.text.trim().split("\n").map((line) => ` ${line}`),
152
+ "",
153
+ ]),
154
+ "What cannot hold:",
155
+ ...[...new Set(conflict.reasons)].map((reason) => `- ${reason.replace(/\s+/g, " ")}`),
156
+ "",
157
+ `This was found ${conflict.reasons.length} times running the conformance tests, and nothing in the code or the tests can`,
158
+ "settle it: only you can say what is meant. Decide which spec is right - or how the two should read together -",
159
+ "and the spec is changed to say it.",
160
+ ].join("\n");
161
+ }
@@ -0,0 +1,71 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ /** What the agent is told, at the end of its task: how it hands its work over. */
4
+ export const SUBMIT_WORK_NOTE = [
5
+ "When you are done with a turn - the work written and its tests passing - call submit_work, last: a short summary of",
6
+ "what you did and why, and after a review, an answer for each problem it raised - fixed, or declined and why. Its",
7
+ "reviewer gets your account beside the diff of what you changed, and checks the one against the other: say what you",
8
+ "did, not what you meant to.",
9
+ ];
10
+ /** What the agent is told with a review's problems: how it answers them. */
11
+ export const SUBMIT_AFTER_REVIEW = [
12
+ "Then call submit_work, last: an answer for each problem, by its number - fixed, or declined and why - and the files.",
13
+ ];
14
+ export function createWorkLedger() {
15
+ let accounts = [];
16
+ const tool = defineTool({
17
+ name: "submit_work",
18
+ label: "Submit the work",
19
+ description: "Hand your work over for review, last in a turn: a short summary of what you did, and for each problem a review " +
20
+ "raised, by its number - fixed, or declined and why, and the files. The reviewer checks it against the diff.",
21
+ promptSnippet: "hand the work over for review - what you did, and your answer to each problem raised",
22
+ parameters: Type.Object({
23
+ summary: Type.String({ description: "What you did in this turn and why, in a few sentences" }),
24
+ answers: Type.Optional(Type.Array(Type.Object({
25
+ problem: Type.Number({ description: "The problem's number in the review's list" }),
26
+ outcome: Type.Union([Type.Literal("fixed"), Type.Literal("declined")]),
27
+ why: Type.String({ description: "How you fixed it, or why you did not" }),
28
+ files: Type.Optional(Type.Array(Type.String(), { description: "The files you changed for it" })),
29
+ }), { description: "After a review: one answer for each problem it raised. Omit on a first turn." })),
30
+ }),
31
+ async execute(_id, params) {
32
+ accounts.push({ summary: params.summary, answers: (params.answers ?? []) });
33
+ return { content: [{ type: "text", text: "Handed over." }], details: {}, terminate: true };
34
+ },
35
+ });
36
+ return {
37
+ tool,
38
+ peek: () => [...accounts],
39
+ take() {
40
+ const taken = accounts;
41
+ accounts = [];
42
+ return taken;
43
+ },
44
+ };
45
+ }
46
+ /** The problems, numbered from 1, that no account answered. */
47
+ export function unanswered(accounts, problems) {
48
+ const answered = new Set(accounts.flatMap((account) => account.answers.map((answer) => answer.problem)));
49
+ return Array.from({ length: problems }, (_, index) => index + 1).filter((n) => !answered.has(n));
50
+ }
51
+ /** The prompt that sends the agent back once for the problems its account left out. */
52
+ export function unansweredPrompt(numbers) {
53
+ return [
54
+ `Your account answers no problem ${numbers.join(", ")} of the review's list. For each: fix it, or say why not - then`,
55
+ "call submit_work again, with an answer for each of them.",
56
+ ].join("\n");
57
+ }
58
+ /** The accounts, for the reviewer: labelled as a claim, the problems they answer named. */
59
+ export function describeAccounts(accounts, problems) {
60
+ if (accounts.length === 0)
61
+ return "(the agent that did the work gave no account - go by the diff)";
62
+ return accounts
63
+ .map((account) => [
64
+ account.summary.trim(),
65
+ ...account.answers.map((answer) => {
66
+ const problem = problems?.[answer.problem - 1];
67
+ return `- problem ${answer.problem}${problem !== undefined ? ` (${problem.what.trim().slice(0, 160)})` : ""}: ${answer.outcome} - ${answer.why.trim()}${answer.files !== undefined && answer.files.length > 0 ? ` [${answer.files.join(", ")}]` : ""}`;
68
+ }),
69
+ ].join("\n"))
70
+ .join("\n\n");
71
+ }
@@ -0,0 +1,124 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { listFiles } from "./testing.js";
4
+ /**
5
+ * The two kinds of test the conformance step writes, and how each is told apart in the code.
6
+ *
7
+ * Conformance tests are written from the functional spec itself: what the functionality does,
8
+ * its edge cases, its errors - as if no acceptance tests had been given. Acceptance tests are the
9
+ * ones the spec's author wrote, each implemented as a test of its own, doing what it says. The
10
+ * two are kept apart even when the acceptance tests would seem to cover the functionality: tests
11
+ * derived only from the acceptance tests check what the author thought to check, and nothing
12
+ * the spec says beyond it.
13
+ *
14
+ * Each kind is labelled where anyone reading the tests can see it - in its file names, and in a
15
+ * comment the harness looks for - so which test is which, and which acceptance test a test
16
+ * implements, never has to be guessed.
17
+ */
18
+ export function conformanceLabel(frid) {
19
+ return `conformance tests of functionality ${frid}`;
20
+ }
21
+ export function acceptanceLabel(frid, n) {
22
+ return `acceptance test ${n} of functionality ${frid}`;
23
+ }
24
+ /** An acceptance test as the spec gives it, without its bullet. */
25
+ export function acceptanceText(item) {
26
+ return item.trim().replace(/^[-*]\s+/, "");
27
+ }
28
+ function normalise(text) {
29
+ return text.toLowerCase().replace(/\s+/g, " ");
30
+ }
31
+ /**
32
+ * What is missing from the labelling of a functionality's tests: the conformance label, when no
33
+ * test file carries it, and each acceptance test no test is labelled as implementing.
34
+ */
35
+ export function missingTestLabels(testsDir, frid, acceptanceTests) {
36
+ const text = normalise(listFiles(testsDir)
37
+ .map((file) => {
38
+ try {
39
+ return readFileSync(join(testsDir, file), "utf8");
40
+ }
41
+ catch {
42
+ return "";
43
+ }
44
+ })
45
+ .join("\n"));
46
+ // Functionality 2's label must not be found in functionality 2.1's.
47
+ const has = (label) => new RegExp(`${normalise(label).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![0-9.]*[0-9])`).test(text);
48
+ const missing = [];
49
+ if (!has(conformanceLabel(frid)))
50
+ missing.push(`No test file is labelled "${conformanceLabel(frid)}".`);
51
+ acceptanceTests.forEach((item, i) => {
52
+ if (!has(acceptanceLabel(frid, i + 1)))
53
+ missing.push(`No test is labelled "${acceptanceLabel(frid, i + 1)}": ${acceptanceText(item)}`);
54
+ });
55
+ return missing;
56
+ }
57
+ /** Sent back when the labels are missing: which, and how to label them. */
58
+ export function labelsPrompt(frid, missing, empty = false, hasAcceptance = true) {
59
+ return labelsPromptFor([{ frid, missing }], empty, hasAcceptance);
60
+ }
61
+ /**
62
+ * Sent back when the labels of several functionalities are missing - a spec change that touches them: every one that is
63
+ * missing, in one request, so none is asked for in place of another.
64
+ */
65
+ export function labelsPromptFor(items, empty = false, hasAcceptance = true) {
66
+ const pending = items.filter((item) => item.missing.length > 0);
67
+ const frids = pending.map((item) => item.frid);
68
+ const which = frids.length === 1 ? `functionality ${frids[0]}` : `functionalities ${frids.slice(0, -1).join(", ")} and ${frids.at(-1)}`;
69
+ return [
70
+ empty
71
+ ? "There are no tests in your tests folder - nothing ran. Whatever a build folder or an earlier report there says, write " +
72
+ `the conformance tests of ${which} now, as files in your tests folder, labelled as the kinds they are:`
73
+ : `The tests are not labelled yet as the kinds they must be kept apart as. Missing, for ${which}:`,
74
+ "",
75
+ ...pending.flatMap((item) => item.missing.map((line) => `- ${line}`)),
76
+ "",
77
+ "Every functionality's conformance tests carry its own label, in a comment of its own line: " +
78
+ `"${conformanceLabel(frids[0] ?? "1")}"${frids.length > 1 ? `, "${conformanceLabel(frids[1])}", and so on - one line a functionality` : ""}. ` +
79
+ `A file that tests several functionalities has one such line for each. A label naming several at once - "conformance tests of functionality 1, 2 and 3" - counts for the first alone.`,
80
+ ...(hasAcceptance
81
+ ? [
82
+ `Every acceptance test is its own test, with a comment right above it: "${acceptanceLabel(frids[0] ?? "1", 1)}:" and the acceptance test's text - one not implemented yet is written as a test of its own, not folded into a conformance test.`,
83
+ ]
84
+ : []),
85
+ "Add every label missing above. Keep every label and every test that is there already: adding one functionality's label never takes another's away. Then run the tests again.",
86
+ ].join("\n");
87
+ }
88
+ /**
89
+ * Conformance and acceptance tests in the code folder, where they do not belong: they live in the
90
+ * module's tests folder, which the harness runs as the conformance suite. Found by their label, or
91
+ * by a test file under a name or folder that says conformance or acceptance - one written without
92
+ * a label is misplaced all the same.
93
+ */
94
+ export function misplacedConformanceTests(codeDir) {
95
+ const label = /conformance tests of (functionality [0-9]|the spec)|acceptance test [0-9]+ of functionality/i;
96
+ return listFiles(codeDir).filter((file) => {
97
+ const parts = file.toLowerCase().split("/");
98
+ const named = parts.some((part) => /conformance|acceptance/.test(part)) && /test/.test(parts.at(-1));
99
+ if (named)
100
+ return true;
101
+ try {
102
+ const text = readFileSync(join(codeDir, file), "utf8");
103
+ return text.length < 500_000 && label.test(text);
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ });
109
+ }
110
+ /** Sent back when there are: to the agent writing the code, or to the one writing the tests. */
111
+ export function misplacedTestsPrompt(files, codeDir, testsDir) {
112
+ return [
113
+ `These are conformance or acceptance tests, in the code folder (${codeDir}), where they do not belong:`,
114
+ "",
115
+ ...files.map((file) => `- ${file}`),
116
+ "",
117
+ testsDir === undefined
118
+ ? "Conformance and acceptance tests are not yours to write: the conformance step writes them, in the module's " +
119
+ "tests folder. Remove these from the code folder. Your tests are the unit tests - write them as unit tests."
120
+ : `Conformance and acceptance tests live in your working directory, ${testsDir}, and nowhere else. Move these ` +
121
+ "there - labelled as they must be - and remove them from the code folder; the harness runs only that folder as " +
122
+ "the conformance suite.",
123
+ ].join("\n");
124
+ }